Category Archives: Posts

PowerShell Script: New-RandomString.ps1

I need to automate the setting of passwords on some Active Directory accounts. Since resetting passwords is also a task that I’m asked to perform with some routine, I decided to make a more generic tool script that could be used in a variety of tasks ( I listened to Don Jones‘ advice on building Tools and Controllers).

I also got a head start from Bill Stewart’s useful Windows IT Pro article Generating Random Passwords in PowerShell.  Among the changes I made are source character class handling, and a new SecureString output option. Please let me know if you find the script useful, or if you find any bugs.

<#
.SYNOPSIS
Generates one or more randomized strings containing specified
character classes.

.PARAMETER Length
The length of the string to be generated.

.PARAMETER CharacterClasses
An array of Character Classes from which to generate the string. The string
will contain at least one character from each specificied class. You may also use the alias 'Classes' for the parameter name

Valid Character classes are:

    Upper    - A..Z
    Lower    - a..z
    Digits   - 0..9
    AlphaNum - shorthand for Upper,Lower,Digits
    Symbols  - !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
    Safe     - #$%+-./:=\_~  (ODBC Safe, Shell Safe if quoted)

If no classes are specified, a string is generated with mixed-case letters,
digits, and symbol characters (i.e., ALL the classes).

.PARAMETER IncludeCharacters
A string of characters to include in the generated string:

.PARAMETER ExcludeCharacters
A string a characters to exclude in the generated string:

.PARAMETER Count
The number of strings to be generated.

.PARAMETER AsSecureString
Specifies that the new random string(s) will be returned as Secure String
objects, to make their use as passwords easier.

.EXAMPLE
> New-RandomString.ps1 -CharacterClasses Lower,Digits -Length 14 -Count 5
Generated five strings, each fourteen characters long, comprised of lowercase
letters and digits.

.EXAMPLE
> New-RandomString.ps1 -Classes AlphaNum,Symbols -length 21
> New-RandomString.ps1 -length 21

The previous two commands are equivalent, because the default character classes
used are upper and lowercase letters, digits, and symbol characters.

.EXAMPLE
> New-RandomString.ps1 -Class 'AlphaNum' -Include '#$%^'

The generated string will contain characters from the UpperCase, LowerCase
and Digits classes, as well as at least one character from among the four
specified.

.EXAMPLE
> New-RandomString.ps1 -Class 'AlphaNum' -Exclude 'O0l1'

The generated string will contain characters from the UpperCase, LowerCase
and Digits classes, but will not contain the "look-alike' characters.

.Notes
Author     : Geoff Duke <Geoffrey.Duke@uvm.edu>
Last Edit  : 2014-11-07 

Based on script "Get-RandomString.ps1" for Windows IT Pro:

http://windowsitpro.com/powershell/generating-random-passwords-powershell

#>

#Requires -version 3

[cmdletbinding()]
Param(
        [alias('Size')]
        [ValidateRange(7,256)]
        [int]
        $length = 21,

        [int]
        $count = 1,

        [Parameter()]
        [ValidateSet('Upper','Lower','Digits','AlphaNum','Symbols','Safe')]
        [alias('Classes')]
        [String[]]
        $CharacterClasses = @('Upper','Lower','Digits','Symbols'),

        [Parameter()]
        [string]
        $IncludeCharacters = '',

        [string]
        $ExcludeCharacters = '',

        [switch]
        $AsSecureString

     )

Set-StrictMode -version 'Latest'

# Additional parameter wrangling
# --------------------------------------------------------------------
[string[]] $Classes = $CharacterClasses.ToLower()

if ( $Classes.Contains('safe') -and $Classes.Contains('symbols') ) {
    write-warning 'You specified both "Symbols" and "Safe" character classes; this is the same as just specifying "Symbols".'
    $Classes = $Classes | where { $_ -ne 'safe' }
}

# Replace alphanum with the upper,lower, and digits classes
if ( $Classes.Contains('alphanum') ) {
    $Classes = $Classes | where { $_ -ne 'alphanum' }
    $Classes += 'upper','lower','digits'
}

# remove any duplicated classes
$Classes = $Classes | select -unique

# Setup source characters
# -------------------------------------------------------------------- 

# Character classes - functionally, a strongly-typed hash of string arrays
#       (addresses issue of singleton arrays turning into simple strings)
$chars = New-Object 'Collections.Generic.Dictionary[string,char[]]'

$chars['lower']    =  97..122 | foreach-object { [Char] $_ }
$chars['upper']    =  65..90  | foreach-object { [Char] $_ }
$chars['digits']   =  48..57  | foreach-object { [Char] $_ }
$chars['symbols']  = (33..47+58..64+91..96+123..126) | foreach-object { [Char] $_ }
$chars['safe']     = '#$%+-./:=\_~'.ToCharArray()

write-verbose $( 'String must include a character from each of ' +
              $( $Classes -join ',' ) +
              $( if ( $IncludeCharacters ) { " plus [$IncludeCharacters] " } ) +
              $( if ( $ExcludeCharacters ) {
                  "but must not include any of [$ExcludeCharacters]" } ) )

if ( $IncludeCharacters ) {
    $Classes += 'include'
    $chars['include'] = $IncludeCharacters.ToCharArray()
}

[char[]] $char_source  = $chars[ $Classes ] | % { $_ } | select -unique

if ( $ExcludeCharacters ) {
    $char_source = $char_source | Where { $_ -NotIn $ExcludeCharacters.ToCharArray() }
}

write-verbose "Source chars: $(-join $char_source)"

# Generating the random string(s)
# --------------------------------------------------------------------
$string_count = 0
:NewString while ( $string_count -lt $Count )  {

    $output = ''
    for ( $i=0; $i -lt $length; $i++) {
        $output += get-random @($char_source)
    }
    write-debug "NewString: generated string is -> $output"

    # Ensure that the requested character classes are present
    :CharClass foreach ($class in $Classes) {
        foreach ( $char in $output.ToCharArray() ) {
            if ( $chars[$class] -Ccontains $char ) {
                write-debug "CharClass: '$char' is in $class"
                continue CharClass # check the next character class
            }
        } # end foreach $char, didn't match the current character class
        write-debug "CharClass: No character from $class! Start again"
        continue NewString # Need to generate a new string
    } # end foreach #class

    # string matches required character classes"
    $string_count++

    if ( $AsSecureString ) {
        ConvertTo-SecureString $output -AsPlainText -Force
    }
    else {
        $output
    }
} # end while

It was while I was writing this script that I ran into the Loop Label documentation error. In PowerShell, as in Perl, Loop Labels do not include the colon when used with a break or continue statement.

PowerShell documentation error – loop labels

I’ve been banging my head on a problem with a script I’m writing. I want to stop executing an inner loop and resume with the next iteration of an outer loop. In Perl, I’d use a next statement with a loop label. In PowerShell, the analogous statement is continue, and loop labels are supported, as described in the about_Break help document.

I finally wrote simplified test code, following the documentation carefully. However, the documentation is wrong. It indicates that the break or continue statement should include the colon in the loop label. This doesn’t throw an error, but it executes as though the label isn’t present at all. The code below includes the colon.

$VerbosePreference = 'Continue'

write-warning 'There should be no output; the outer loop should be exited during first iteration'
:outer foreach ($a in ('red','green') ) {
    write-verbose "Outer loop"

    :inner foreach ($b in ('red','blue','green') ) {
        write-verbose "Inner loop"
 
        write-verbose "`$a is $a ; `$b is $b"
        if ( $a -eq $b ) {
            break :outer
        }
        "$a $b"
    }
}

Then cracked my copy of PowerShell in Action and saw that the loop label does not include the colon, just like Perl. Remove the colon and everything is good. Wish it hadn’t taken me hours to work it out.

 

Get-PrintJobs.ps1 PowerShell script

After a recent upgrade of our print servers, I discovered that the Print Spooler service event logging had been enhanced, and changed enough that some PowerShell reporting scripts that worked just fine on Windows Server 2008 (32-bit) no longer worked on Server 2012 R2.

To get the reports working again, I had to enable the Microsoft-Windows-PrintService/Operational log. I also had to increase the log size from the default in order to retain more than one day’s events. The trickiest part was figuring out the XPath query syntax for retrieving events from a particular printer. The newer syntax makes more sense to me, but it took me a long time to arrive at it.

Following Don Jones‘ entreaty to build tools and controllers, I offer this tool script, which retrieves (simplified) print job events, and cares not a whit about formatting or saving.


<#
.SYNOPSIS
Gets successful print job events and returns simplified objects with relevant
details.

.DESCRIPTION
Collects the successful print jobs from the PrintService Operational log, with
optional query parameters including Printer name and start and end times.

.PARAMETER PrinterName
The share name of the printer for which events will be retrieved.

.PARAMETER StartTime
The beginning of the interval during which events will be retrieved.

.PARAMETER EndTime
The end of the interval during which events will be retrieved.

.EXAMPLE
C:\> Get-PrintJobs.ps1
Returns objects representing all the successful print jobs (events with id 307).

.EXAMPLE
C:\> Get-PrintJobs.ps1 -PrinterName 'Accounting HP LaserJet'
Returns objects for all the jobs on the Accounting printer.

.EXAMPLE
C:\> Get-PrintJobs.ps1 -PrinterName 'Accounting HP LaserJet' -StartTime (Get-Date).AddHours(-12)
Returns objects for all the jobs on the Accounting printer generated in the last twelve hours.
.NOTES
Script Name: Get-PrintJobs.ps1
Author : Geoff Duke <Geoffrey.Duke@uvm.edu>

Edit 2014-10-08: Generalizing from dept printer report script, fixing XPath
query syntax.

Edit 2012-11-29: Job is run as SYSTEM, and computer object has been granted
Modify rights to the destination directory.
#>

Param(
[string] $PrinterName,

[datetime] $StartTime,

[datetime] $EndTime
)
Set-StrictMode -version latest
# Building XPath query to select the right events
$filter_start = @'
<QueryList>
  <Query Id="0" Path="Microsoft-Windows-PrintService/Operational">
    <Select Path="Microsoft-Windows-PrintService/Operational">
'@

$filter_end = @'
    </Select>
  </Query>
</QueryList>
'@

$filter_match = '*[System[(EventID=307)' #need to add ']]' to close

if ( $StartTime -or $EndTime) {
    $filter_match += ' and TimeCreated[' #need to add ']' to close
    $time_conds = @()

    if ( $StartTime ) {
        $time_conds += ( '@SystemTime&gt;=' +
            "'{0:yyyy-MM-ddTHH:mm:ss.000Z}'" -f $StartTime.ToUniversalTime()
        )
    }
    if ( $EndTime ) {
        $time_conds += ( '@SystemTime&lt;=' +
            "'{0:yyyy-MM-ddTHH:mm:ss.000Z}'" -f $EndTime.ToUniversalTime()
        )
    }

    $filter_match += ( $time_conds -join ' and ' ) + ' ]' # Closing TimeCreated[
}

$filter_match += "]]`n" # Closing [System[

if ( $PrinterName ) {
    $filter_match += @"
  and
*[UserData[DocumentPrinted[(Param5='$PrinterName')]]]
"@
}

write-debug "Using Filter:`n $filter_match"

# The $filter variable below is cast as XML, that's getting munged
# by WordPress or the SyntaxHighlighter as '1'
1 $filter = ($filter_start + $filter_match + $filter_end)

get-winevent -filterXML $filter | foreach {
    $Properties = @{
        'Time' = $_.TimeCreated;
        'Printer' = $_.Properties[4].value;
        'ClientIP' = $_.properties[3].value.SubString(2);
        'User' = $_.properties[2].value;
        'Pages' = [int] $_.properties[7].value;
        'Size' = [int] $_.properties[6].value
    }

    New-Object PsObject -Property $Properties
}

If you find this script useful, please let me know. If you find any bugs, definitely let me know!

Set default printer with PowerShell

Closely related to my previous post, this simple script uses a WScript.Network COM object to set the default printer. The comment block is longer than the script, but I think it’s a useful little tool.

<#
.SYNOPSIS
Sets a Network Printer connection as the default printer.

.DESCRIPTION
Uses a COM object to sets the specified, installed printer as the default. If
an error is encountered, e.g., the specified printer isn't installed, the
exception is written to a file called Set-DefaultPrinter.err in the current
$env:temp directory, and then the script terminates, throwing the exception.

Based on my colleague's VBScript solution:

http://blog.uvm.edu/jgm/2014/06/11/parting-scripts-add-a-new-network-printer-and-set-it-as-default/

.PARAMETER PrinterShare
The UNC path to the shared printer.
e.g. \\printers1.campus.ad.uvm.edu\ETS-SAA-SamsungML-3560

.EXAMPLE
Set-DefaultPrinter.ps1 -PrinterShare '\\printers1.campus.ad.uvm.edu\ETS-SAA-SamsungML-3560'

.NOTES
    Script Name: Set-DefaultPrinter.ps1
    Author     : Geoff Duke <Geoffrey.Duke@uvm.edu>
#>

[cmdletbinding()]

Param(
    [Parameter(Mandatory=$true,
        HelpMessage="Enter the UNC path to the network printer")]
    [ValidateNotNullOrEmpty()]
    [string] $PrinterShare
)

Set-PSDebug -Strict

$PSDefaultParameterValues = @{"out-file:Encoding"="ASCII"}

$ws_net = New-Object -COM WScript.Network

try {
    $ws_net.SetDefaultPrinter($PrinterShare)
}
catch {
    $error[0].exception | out-file (join-path $env:temp 'Set-DefaultPrinter.err') 
    throw $error[0]
}
write-verbose "Default printer now $PrinterShare"

Add network printer with PowerShell

This is my PowerShwell translation of my colleague’s VBScript solution for mapping network printers with a script.

<#
.SYNOPSIS
Add a Network Printer connection, optionally making it the default printer.

.DESCRIPTION
Uses a COM object to add a Network Printer, and optionally sets that printer
as the default. If an error is encountered, the exception is written to a
file called Add-NetworkPrinter.err in the current $env:temp directory, and then
the script terminates.

This is my PowerShell translation of my colleague's VBScript solution:

http://blog.uvm.edu/jgm/2014/06/11/parting-scripts-add-a-new-network-printer-and-set-it-as-default/

.PARAMETER PrinterShare
The UNC path to the shared printer.
e.g. \\printers1.campus.ad.uvm.edu\ETS-SAA-SamsungML-3560

.PARAMETER Default
Specifies that the printer will also be set as the default printer for the current user.

.EXAMPLE
Add-NetworkPrinter.ps1 -PrinterShare '\\printers1.campus.ad.uvm.edu\ETS-SAA-SamsungML-3560' -Default

.NOTES
    Script Name: Add-NetworkPrinter.ps1
    Author     : Geoff Duke <Geoffrey.Duke@uvm.edu>
#>

[cmdletbinding()]

Param(
    [Parameter(Mandatory=$true,
        HelpMessage="Enter the UNC path to the network printer")]
    [ValidateNotNullOrEmpty()]
    [string] $PrinterShare,

    [parameter(Mandatory=$false)]
    [switch] $Default
)

Set-PSDebug -Strict

$PSDefaultParameterValues = @{"out-file:Encoding"="ASCII"}

$ws_net = New-Object -COM WScript.Network

write-verbose "Adding connection to $PrinterShare"
try {
    $ws_net.AddWindowsPrinterConnection($PrinterShare)
}
catch {
    $error[0].exception | out-file (join-path $env:temp 'Add-NetworkPrinter.err')
    throw $error[0]
}

write-verbose "Setting the printer as the default"
if ( $Default ) {
    try {
        $ws_net.SetDefaultPrinter($PrinterShare)
    }
    catch {
        $error[0].exception | out-file (join-path $env:temp 'Add-NetworkPrinter.err')
        throw $error[0]
    }
}

# the end

For use with Group Policy, it will probably be helpful to create a simple Set-DefaultPrinter.ps1 script. But that’s just the second stanza from the script above.

Quick tip – Human readable folder sizes in Excel

From the school of There’s Probably a Better Way, But…

I was taking the output of SysInternals’ du.exe utility and wanted to email it to a colleague, but the folder sizes were in KB. Since most of them were many Gigabytes in size, I wanted a quick way to convert them to the appropriate size in GB, MB, or KB.

I copied the du.exe size column and pasted it into Excel. Then I created the following formula:

=IF(E6>(2^20),TEXT(E6/(2^20),"0.0") & " GB","smaller")

…where E6 is the cell reference. Also note that du.exe’s output was in KB, so my test is one order of magnitude smaller than it would need to be if I was formatting a size in bytes. (i.e., 2^20 is 1 MB).

I then nested another similar if() function with a final formatting option to get my final formula:

=IF(E6>(2^20),TEXT(E6/(2^20),"0.0") & " GB",IF(E6>(2^10),TEXT(E6/(2^10),"0.0") & " MB",TEXT(E6,"0.0") & " KB"))