
About the Author Kirk Munro, the world's first self-proclaimed Poshoholic, is a Senior Software Developer for Quest Software where he is very happy working as a member of the PowerGUI team. He is also the recipient of the Microsoft Most Valued Professional (MVP) award for his work in the Windows Server - Admin Frameworks (PowerShell) category. Kirk has spent the last four years working on Windows Management products at Quest, specializing in data retrieval from Active Directory and Exchange data stores. These days he spends pretty much all of his time working with PowerShell and PowerGUI, writing his blog, and helping others in the PowerShell community. You can contact him by leaving him a note on his blog at http://www.poshoholic.com. |
When the names were drawn out of the virtual hat to decide who would provide the Windows PowerShell Advanced Division solutions, I was assigned with Event 5, entitled “You Call That a Strong Password?” The objective in this event is to measure the strength of a potential password against a list of 13 rules and to provide a rating for that password indicating how strong it is. Well, when working with PowerShell I’m all about following rules, so this sounded like it would be right up my alley!
In addition to testing a password to see which rules it passes and which rules it fails, I decided to follow a few rules of my own when writing my solution for this event, as follows:
1. | Support localized PowerShell users. When PowerShell 1.0 was released it had already been localized in many different languages. Script authors often don’t acknowledge this because the possible ways you can avoid hard-coding strings in your PowerShell 1.0 scripts aren’t readily available at your fingertips. That’s not to say there aren’t any options in PowerShell 1.0 – there are a few tricks you can use to get localized strings. I take advantage of the tricks I have discovered and don’t use hard-coded strings unless there is no other option. |
2. | Be flexible. If you watch the PowerShell newsgroups, you often see that there are more than three solutions to a problem that someone posts. PowerShell is very flexible and allows people to use it in whatever way they feel comfortable. So I decided to allow the resulting .PS! script file to be run by dot-sourcing it (and therefore importing functions into PowerShell) or by calling it using the call operator. This provides flexibility to meet the needs of any PowerShell user. |
3. | Support pipelines without compromise. PowerShell is all about pipelines, so I wanted to make whatever solution I came up with as pipeline-savvy as possible, regardless of how it is used. And while providing pipeline support, users might not want to use pipelines so we need to support that, too. Flexibility in PowerShell is key. |
4. | Expect the unexpected. Administrators using this sort of script might try to throw you a curve ball or two, so you need to make sure you’re getting what you ask for. I’ve added a few extra checks here and there to make sure that I don’t get tripped up by surprises. |
5. | Raise the bar. The event description asked for a mechanism to test a single password and use the words defined in a single text file. What if you want to test multiple passwords or you want to define your word vocabulary using multiple text files? There’s no reason you wouldn’t want to do this, so I decided to support it. |
6. | Follow the standards. PowerShell does a very good job of following standards in the cmdlets that were included in the first release. I try to follow those standards as well whenever working with PowerShell scripts. Namely, this means creating functions that follow the Verb-SingularNoun naming convention and keeping parameter names consistent when possible. I also try to provide PowerShell-like documentation in comments above any PowerShell function I share with the community (although don’t expect those comments to be localized!). |
7. | Use the right tool for the job. The solution I’ve provided here includes a pretty lengthy PowerShell script. Trying to create such a script using the PowerShell console and simple editors like notepad is just making life difficult. I used the free PowerGUI Scripting Editor that comes with Release Candidate of PowerGUI 1.0.14 (also free) to create and debug this PowerShell script. And it came along just in time too – the step-able pipeline support and enhanced debugging options were really handy when troubleshooting pipeline issues and trying to get Intellisense for the objects I was using. |
What is the result of following all of these rules? Enter Measure-PasswordStrength.ps1. Measure-PasswordStrength.ps1 is the script file containing my solution for the event. Let’s break down the contents of that file to explain what it does.
First, let’s look at the overall script file itself. It contains a begin block, a process block and an end block. For those of you who don’t know, when a PowerShell pipeline is executed the begin block of each command in the pipeline is executed first to allow for any initialization that is required to happen once. After that, the process block is executed for each object that is passed through the pipeline, and, finally, the end block is executed at the end to allow commands to perform any cleanup or other finalization steps that they need to perform. This is necessary if you want to support pipelining while using the call operator to call a PowerShell script. The begin block simply defines the functions that are being used. The meat of the work happens in the process block. The process block takes a look at how the script was invoked by examining the built-in $MyInvocation variable. $MyInvocation contains a lot of information about how a given script was invoked, so the Measure-PasswordStrength.ps1 script file uses this to determine if the script was called using the call operator (&) or dot-sourced using the dot-sourcing operator (.).
If it was dot-sourced, the process block does nothing, because users who dot-source a file do so to import functions or other PowerShell constructs into PowerShell where they can invoke them without the use of the call operator or the script file. If they’re importing the functions to invoke them directly within PowerShell, we don’t have to do anything other than include the functions in the begin block. If it was called, however, the process block checks to see if there is an object coming down the pipeline by looking at the $_ variable ($_ represents the current object in the pipeline). If there is an object in the pipeline, then the script is being called in a pipeline; therefore, we simply pass that pipeline object to the internal Measure-PasswordStrength function (as well as the parameters that were passed to the script when it was called) and let it do the rest of the work for us. Otherwise, we call the Measure-PasswordStrength function using the parameters that were passed to the script when it was called. Invoke-Expression is used in both of these cases because it allows us to pass parameters in such a manner that the parameter identifiers are persisted from the .PS! script file call operator statement down into the internal Measure-PasswordStrength function.
Now to the meat of the solution: the Measure-PasswordStrength function. Measure-PasswordStrength is designed similar to the script file that contains it. It contains a begin block, a process block and an end block, just like the script file itself, so that it will work both inside and outside of a pipeline. It performs the same tests (at least some of them) that you would expect from a first-class PowerShell cmdlet by checking variable values for validity, and it throws localized PowerShell errors (using the Get-PSResourceString helper function) if those tests fail. It also supports both individual strings as well as arrays of strings (and wildcards) in both the passwords it will measure and the word list files that it will use to load its vocabulary. That’s all been designed that way so that it satisfies my list of rules: as localized as possible, flexible, pipeline-able, exception-ready, first-class, standards following, and designed using, the latest and greatest PowerShell tools available.
What about the rules outlined in the scripting event itself? How does it measure a password against those rules?
The Measure-PasswordStrength function contains a begin block in which two enumerations are created that will be used when generating the output for the script. These enumerations are created using the New-Enum helper function, a function that was originally published on the Microsoft PowerShell Team blog back in January 2007. One enumeration contains values identifying password rules that fail and the other contains values identifying the overall password strength. Once those enumerations are created it also loads the word list in the begin block within the function, but only if it isn’t already cached and if you aren’t instructing it to refresh that cache (why load a text file every time if it isn’t being modified?). Caching the word list in the begin block allows for you to avoid re-loading the contents of files over and over again.
Once we have created our enumerations and initialized the cache, processing moves on to the process block in the function. In the process block of the Measure-PasswordStrength function, we check to see if you’re calling the function inside of a pipeline or not. If not, it calls itself inside of a pipeline. This promotes better reuse of our processing code because we only have to write it once, supporting the pipeline scenario. When you are calling it inside a pipeline, it tests the current password (which is the object coming down the pipeline) against the 13 rules and generates a PasswordStrengthReportCard object that stores the results of those tests. This PasswordStrengthReportCard object contains five members: the password that was tested, the score that password received, the reason it received that score (which is an array of enumerations that were created in the begin block and that identify which tests failed), a formatted-output version of the reason member, and the overall password strength as defined by the event documentation (also an enumeration that was created in the begin block). This approach was chosen over the output of strings as recommended in the event description for two reasons: it improves localization support and it is a better PowerShell solution to the problem at hand by only outputting errors during script execution (if any) and by passing a well defined object down the pipeline for further processing. The PasswordStrengthReportCard object is then passed down the pipeline for further processing. While it wasn’t necessary, I used the Write-Output cmdlet to identify the output point of the process block a little more clearly.
When it comes to the actual password rules, of the 13 password rules we need to measure our password(s) against, they can be broken up into a few categories. The first 5 rules are testing the password to see if the password itself, some part of the password, or a slightly modified version of the password are in the vocabulary defined by our wordlist. Rule 6 tests the password length. Rules 7 through 10 make sure the password contains specific kinds of characters. And lastly, rules 11, 12 and 13 identify passwords that don’t vary the character usage enough.
For the first 5 rules, simple string modification methods and the contains operator were chosen. String modification methods return a modified string without changing the original value, so these allow us to identify slight variations of the password that might be in the cached word vocabulary more easily. And since the word vocabulary is cached, the contains operator provides us with a very simple and efficient manner to check to see if the (modified) password matches a word in the vocabulary. Pretty straight-forward stuff here.
The 6th rule is even easier, because it simply requires checking the Length member of the password (it is a string object, remember) to make sure it fits in the defined restrictions. Again, really straight-forward.
Rules 7 through 12 use regular expressions to identify passwords that break rules. This is where the some users might get confused. Regular expressions are a very, very powerful mechanism that allows you to test to see if strings match the patterns identified by the regular expression. When using regular expressions in PowerShell, you need to use the match operator, or one if the many variants of the match operator, to do this sort of pattern matching. In this event I used notmatch, cnotmatch and cmatch to perform my regular expression matching (note: the c prefix identifies case-sensitive pattern matching and the not prefix identifies that we are looking for passwords that don’t match a specified pattern). The first three regular expressions simply test the password to see if it does not contain a character in the specified range. The regular expression for rule 10 tests to see if the password doesn’t contain any characters that are not in the specified ranges (in this case lowercase letters, uppercase letters, numbers and spaces – I added the spaces test to distinguish them from symbols). And the last two regular expressions in the tests for rules 11 and 12 test to see if the password contains 4 or more characters in sequence in the specified range of characters. These regular expressions can look really complicated to the untrained eye, but when you break them down and learn how they work they really aren’t that complicated. It’s all about learning how to read them and understand how they work.
The last rule test breaks the password into an array of characters and then leverages the power in the Group-Object cmdlet by having it group matching characters together. The result of this is an array where any duplicate characters will have a count greater than 1, so Where-Object is used to filter those out. The result is an array of characters that are used 2 or more times in the password, or, if the password is strong enough to pass this test, null. Since we’re testing the result of that pipeline as a whole, if it is not null we know we’ve got duplicate characters in the array that was output and the rule test fails.
Also you’ll find a little extra error handling in the Measure-PasswordStrength function to prevent it from being misused and support for using -? to get the syntax, whether you call the function that was dot-sourced or the script using the call operator. And that pretty much wraps it up.
Hopefully through this solution I’ve given you a few PowerShell tips and tricks that you can use in your own scripts.
Enjoy!
Kirk out.
Here’s the code:
########################################################################################################################
# #
# File: Measure-PasswordStrength.ps1 #
# Author: Kirk Munro #
# Author's Blog: http://poshoholic.com #
# Revision: 1.0.0 #
# Contents: This script contains a function called Measure-PasswordStrength and two helper functions. The #
# The Measure-PasswordStrength function is designed to measure the strength of a password against 13 #
# rules, as defined in Event 5 in the Advanced Division of the 2008 Scripting Games. This solution #
# tries to answer the requirements put forth in Event 5 while maintaining support for localized #
# versions of PowerShell through the use of enumerations and resource strings (see the two helper #
# functions, Get-PSResourceString and New-Enum). Note that New-Enum is a function that was posted #
# on the PowerShell Team blog (yay for the community!), but Get-PSResourceString is my own creation. #
# The output of this script is a report card including the score, the reasons for the score and #
# the password strength rating. #
# History: v1.0.0 - Initial release submitted to the 2008 Scripting Games team #
# #
########################################################################################################################
BEGIN {
########################################################################################################################
# NAME
# Get-PSResourceString
#
# SYNOPSIS
# Returns a resource string that is looked up in the System.Management.Automation namespace or the
# Microsoft.PowerShell.ConsoleHost namespace, or a list of resource root names or resource identifiers that are
# available.
#
# SYNTAX
# Get-PSResourceString [-baseName] <string> [-resourceId] <string> [[-defaultValue] <string>]
# [[-culture] <System.Globalization.CultureInfo>]
# Get-PSResourceString [[-baseName] <string>] -list
#
# DETAILED DESCRIPTION
# The Get-PSResourceString function returns a resource string that is looked up in the System.Management.Automation
# namespace or the Microsoft.PowerShell.ConsoleHost namespace, or a list of resource root names or resource
# identifiers that are available. If a resource string was requested and it is not found, the default value (if
# present) will be returned.
#
# PARAMETERS
# -baseName <string>
# Specifies the root name of the resources.
#
# Required? true
# Position? 1
# Default value
# Accept pipeline input? false
# Accept wildcard characters? false
#
# -resourceId <string>
# Specifies the identifier of the resource that is being retrieved.
#
# Required? true
# Position? 2
# Default value
# Accept pipeline input? false
# Accept wildcard characters? false
#
# -defaultValue <string>
# Specifies the default value for the resource string. If the string is not found, the default value will be
# returned.
#
# Required? false
# Position? 3
# Default value null
# Accept pipeline input? false
# Accept wildcard characters? false
#
# -culture <System.Globalization.CultureInfo>
# Specifies the culture to use when looking up the resource string.
#
# Required? false
# Position? 4
# Default value $host.CurrentCulture
# Accept pipeline input? false
# Accept wildcard characters? false
#
# -list <Switch>
# When this parameter is used by itself, this function outputs the root names that are available. When this
# parameter is used in conjunction with the baseName parameter, this function outputs the resource identifiers
# that are availab.e
#
# Required? false
# Position? named
# Default value false
# Accept pipeline input? false
# Accept wildcard characters? false
#
# INPUT TYPE
# String,System.Globalization.CultureInfo,Switch
#
# RETURN TYPE
# String,String[]
#
# NOTES
# For more information the System.Globalization.CultureInfo type consult the relevant MSDN documentation.
#
# -------------------------- EXAMPLE 1 --------------------------
#
# C:\PS>get-psresourcestring -basename helpdisplaystrings -resourceid falseshort
#
#
# This command retrieves the string associated with the 'falseshort' resource id using the current culture.
#
#
function Get-PSResourceString {
param(
[string]$baseName = $null,
[string]$resourceId = $null,
[string]$defaultValue = $null,
[System.Globalization.CultureInfo]$culture = $host.CurrentUICulture,
[Switch]$list
)
if ($list -and ($resourceId -or $defaultValue)) {
throw $(Get-PSResourceString -BaseName 'ParameterBinderStrings' -ResourceId 'AmbiguousParameterSet')
}
if ($list) {
$engineAssembly = [System.Reflection.Assembly]::GetExecutingAssembly()
$hostAssembly = [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.PowerShell.ConsoleHost')
if ($baseName) {
$engineAssembly.GetManifestResourceNames() | Where-Object { $_ -eq "$baseName.resources" } |
ForEach-Object {
$resourceManager = New-Object -TypeName System.Resources.ResourceManager($baseName, $engineAssembly)
$resourceManager.GetResourceSet($host.CurrentCulture,$true,$true) |
Add-Member -Name BaseName -MemberType NoteProperty -Value $baseName -Force -PassThru | ForEach-Object {
$_.PSObject.TypeNames.Clear()
$_.PSObject.TypeNames.Add('ResourceString')
$_ | Write-Output
}
}
$hostAssembly.GetManifestResourceNames() | Where-Object { $_ -eq "$baseName.resources" } | ForEach-Object {
$resourceManager = New-Object -TypeName System.Resources.ResourceManager($baseName, $hostAssembly)
$resourceManager.GetResourceSet($host.CurrentCulture,$true,$true) |
Add-Member -Name BaseName -MemberType NoteProperty -Value $baseName -Force -PassThru | ForEach-Object {
$_.PSObject.TypeNames.Clear()
$_.PSObject.TypeNames.Add('ResourceString')
$_ | Write-Output
}
}
} else {
$engineAssembly.GetManifestResourceNames() | Where-Object { $_ -match '\.resources$' } | ForEach-Object { $_.Replace('.resources','') }
$hostAssembly.GetManifestResourceNames() | Where-Object { $_ -match '\.resources$' } | ForEach-Object { $_.Replace('.resources','') }
}
} else {
if (-not $baseName) {
throw $($(Get-PSResourceString -BaseName 'ParameterBinderStrings' -ResourceId `
'ParameterArgumentValidationErrorNullNotAllowed') -f $null,'BaseName')
}
if (-not $resourceId) {
throw $($(Get-PSResourceString -BaseName 'ParameterBinderStrings' -ResourceId `
'ParameterArgumentValidationErrorNullNotAllowed') -f $null,'ResourceId')
}
if (-not $global:PSResourceStringTable) {
$engineAssembly = [System.Reflection.Assembly]::GetExecutingAssembly()
$hostAssembly = [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.PowerShell.ConsoleHost')
if ($engineAssembly.GetManifestResourceNames() -contains "$baseName.resources") {
New-Variable -Scope Global -Name PSResourceStringTable -Value @{} -Description `
'A cache of PowerShell resource strings. To access data in this table, use Get-ResourceString.'
$global:PSResourceStringTable['EngineAssembly'] = @{'Assembly'=$engineAssembly;'Cultures'=@{}}
$global:PSResourceStringTable['HostAssembly'] = @{'Assembly'=$hostAssembly;'Cultures'=@{}}
$resourceManager = (New-Object -TypeName System.Resources.ResourceManager`
($baseName, $global:PSResourceStringTable.EngineAssembly.Assembly));
$global:PSResourceStringTable.EngineAssembly.Cultures["$($culture.Name)"] = @{"$baseName"=`
@{'ResourceManager'=$resourceManager;'Strings'=$resourceManager.GetResourceSet($culture,$true,$true)}};
} elseif ($hostAssembly.GetManifestResourceNames() -contains "$baseName.resources") {
New-Variable -Scope Global -Name PSResourceStringTable -Value @{} -Description `
'A cache of PowerShell resource strings. To access data in this table, use Get-ResourceString.'
$global:PSResourceStringTable['EngineAssembly'] = @{'Assembly'=$engineAssembly;'Cultures'=@{}}
$global:PSResourceStringTable['HostAssembly'] = @{'Assembly'=$hostAssembly;'Cultures'=@{}}
$resourceManager = (New-Object -TypeName System.Resources.ResourceManager`
($baseName, $global:PSResourceStringTable.HostAssembly.Assembly));
$global:PSResourceStringTable.HostAssembly.Cultures["$($culture.Name)"] = `
@{"$baseName"=@{'ResourceManager'=$resourceManager;'Strings'=$resourceManager.GetResourceSet($culture,$true,$true)}};
}
} elseif ($global:PSResourceStringTable.EngineAssembly.Assembly.GetManifestResourceNames() -contains "$baseName.resources") {
if (-not $global:PSResourceStringTable.EngineAssembly.Cultures.ContainsKey($culture.Name)) {
$resourceManager = (New-Object -TypeName System.Resources.ResourceManager`
($baseName, $global:PSResourceStringTable.EngineAssembly.Assembly));
$global:PSResourceStringTable.EngineAssembly.Cultures["$($culture.Name)"] = `
@{"$baseName"=@{'ResourceManager'=$resourceManager;'Strings'=$resourceManager.GetResourceSet($culture,$true,$true)}};
} elseif (-not $global:PSResourceStringTable.EngineAssembly.Cultures["$($culture.Name)"].ContainsKey($baseName)) {
$resourceManager = (New-Object -TypeName System.Resources.ResourceManager`
($baseName, $global:PSResourceStringTable.EngineAssembly.Assembly));
$global:PSResourceStringTable.EngineAssembly.Cultures["$($culture.Name)"]["$baseName"] = `
@{'ResourceManager'=$resourceManager;'Strings'=$resourceManager.GetResourceSet($culture,$true,$true)};
}
} elseif ($global:PSResourceStringTable.HostAssembly.Assembly.GetManifestResourceNames() -contains "$baseName.resources") {
if (-not $global:PSResourceStringTable.HostAssembly.Cultures.ContainsKey($culture.Name)) {
$resourceManager = (New-Object -TypeName System.Resources.ResourceManager`
($baseName, $global:PSResourceStringTable.HostAssembly.Assembly));
$global:PSResourceStringTable.HostAssembly.Cultures["$($culture.Name)"] = @{"$baseName"=`
@{'ResourceManager'=$resourceManager;'Strings'=$resourceManager.GetResourceSet($culture,$true,$true)}};
} elseif (-not $global:PSResourceStringTable.HostAssembly.Cultures["$($culture.Name)"].ContainsKey($baseName)) {
$resourceManager = (New-Object -TypeName System.Resources.ResourceManager`
($baseName, $global:PSResourceStringTable.HostAssembly.Assembly));
$global:PSResourceStringTable.HostAssembly.Cultures["$($culture.Name)"]["$baseName"] = `
@{'ResourceManager'=$resourceManager;'Strings'=$resourceManager.GetResourceSet($culture,$true,$true)};
}
}
$resourceString = $null
if ($global:PSResourceStringTable) {
if ($global:PSResourceStringTable.EngineAssembly.Cultures -and $global:PSResourceStringTable.EngineAssembly.Cultures.ContainsKey`
($culture.Name) -and $global:PSResourceStringTable.EngineAssembly.Cultures[$culture.Name].ContainsKey($baseName)) {
$resourceString = ($global:PSResourceStringTable.EngineAssembly.Cultures["$($culture.Name)"]["$baseName"].Strings |
Where-Object { $_.Name -eq $resourceId }).Value
} elseif ($global:PSResourceStringTable.HostAssembly.Cultures -and $global:PSResourceStringTable.HostAssembly.Cultures.ContainsKey`
($culture.Name) -and $global:PSResourceStringTable.HostAssembly.Cultures[$culture.Name].ContainsKey($baseName)) {
$resourceString = ($global:PSResourceStringTable.HostAssembly.Cultures["$($culture.Name)"]["$baseName"].Strings |
Where-Object { $_.Name -eq $resourceId }).Value
}
}
if (-not $resourceString) {
$resourceString = $defaultValue
}
return $resourceString
}
}
if (-not (Get-Alias -Name grs -ErrorAction SilentlyContinue)) {
New-Alias -Name grs -Value Get-PSResourceString -Description `
'Returns a resource string that is looked up in the System.Management.Automation namespace.'
}
########################################################################################################################
# NAME
# New-Enum
#
# SYNOPSIS
# Creates a new enum.
#
# SYNTAX
# Measure-PasswordStrength [-name] <string>
#
# DETAILED DESCRIPTION
# The New-Enum function creates a new enum with values identified by the trailing arguments. It is copied from the
# function of the same name that was published on the Microsoft PowerShell Team blog.
#
# PARAMETERS
# -name <string>
# Specifies the name of the enum.
#
# Required? true
# Position? 1
# Default value
# Accept pipeline input? false
# Accept wildcard characters? false
#
#
# INPUT TYPE
# String
#
# RETURN TYPE
# $null
#
# NOTES
# http://blogs.msdn.com/powershell/archive/2007/01/23/how-to-create-enum-in-powershell.aspx
#
# -------------------------- EXAMPLE 1 --------------------------
#
# C:\PS>New-Enum 'Color' 'Red' 'Green' 'Blue'
#
#
# This command creates a new enum called 'Color' with three values: Red, Green and Blue
#
#
function New-Enum {
param(
[string]$Name = $(throw $($(Get-PSResourceString -BaseName 'ParameterBinderStrings' -ResourceId `
'ParameterArgumentValidationErrorNullNotAllowed') -f $null,'Name'))
)
$appdomain = [System.Threading.Thread]::GetDomain()
$assembly = new-object System.Reflection.AssemblyName
$assembly.Name = "EmittedEnum"
$assemblyBuilder = $appdomain.DefineDynamicAssembly(
$assembly,
[System.Reflection.Emit.AssemblyBuilderAccess]::Save -bor [System.Reflection.Emit.AssemblyBuilderAccess]::Run
)
$moduleBuilder = $assemblyBuilder.DefineDynamicModule("DynamicModule", "DynamicModule.mod")
$enumBuilder = $moduleBuilder.DefineEnum($name, [System.Reflection.TypeAttributes]::Public, [System.Int32])
for ($i = 0; $i -lt $args.Length; $i++) {
$enumBuilder.DefineLiteral($args[$i], $i) | Out-Null
}
$enumBuilder.CreateType() | Out-Null
}
if (-not (Get-Alias -Name ne -ErrorAction SilentlyContinue)) {
New-Alias -Name ne -Value New-Enum -Description 'Creates a new enumeration with the values specified in the trailing arguments.'
}
########################################################################################################################
# NAME
# Measure-PasswordStrength
#
# SYNOPSIS
# Measures a password against 13 criterion and returns a report card indicating the score, the reason(s) for the
# score, and the password strength rating (weak, moderate or strong).
#
# SYNTAX
# Measure-PasswordStrength [-password] <string[]> [[-wordListPath] <string[]>] [-refreshCache]
#
# DETAILED DESCRIPTION
# The Measure-PasswordStrength function returns a report card indicating the strength of the password and the
# reason that password was rated accordingly. It supports testing multiple passwords in a pipeline as well as
# using multiple word list files. The cached word list will be used unless the refreshCache switch parameter
# is used when the function is called.
#
# PARAMETERS
# -password <string[]>
# Specifies the password to measure.
#
# Required? true
# Position? 1
# Default value
# Accept pipeline input? true
# Accept wildcard characters? false
#
# -wordListPath <string[]>
# Specifies the path to one or more word list files. Wildcards are accepted.
#
# Required? true (unless already cached)
# Position? 2
# Default value
# Accept pipeline input? false
# Accept wildcard characters? false
#
# -refreshCache <switch>
# When this parameter is used, the function deletes any existing word list cache and rebuilds it using the
# files identified by the wordListPath parameter.
#
# Required? false
# Position? 3
# Default value $false
# Accept pipeline input? false
# Accept wildcard characters? false
#
#
# INPUT TYPE
# String[],Switch
#
# RETURN TYPE
# PasswordStrengthReportCard
#
# NOTES
# For more information see Event 5 of the Scripting Games 2008 Advanced Division.
#
# -------------------------- EXAMPLE 1 --------------------------
#
# C:\PS>'P4ssw0rd','P0$h0h01ic' | measure-passwordstrength -wordListPath C:\wordlist.txt
#
#
# This command measures the strength of the 'P4ssw0rd' and 'P0$h0h01ic' passwords.
#
#
function Measure-PasswordStrength {
param(
[string[]]$Password = $null,
[string[]]$WordListPath = $null,
[Switch]$RefreshCache
)
BEGIN {
if (($Password -contains '-?') -or ($WordListPath -contains '-?') -or ($args -contains '-?')) {
Get-PSResourceString -BaseName 'HelpDisplayStrings' -ResourceId 'Syntax' | Write-Host
" Measure-PasswordStrength [-password] <string[]> [[-wordListPath] <string[]>] [-refreshCache]" | Write-Host
break
}
New-Enum 'PasswordScoreReason' 'ActualWord' 'ActualWordWithOneCharacterSuffix' 'ActualWordWithOneCharacterPrefix' `
'ActualWordWith0Substitution' 'ActualWordWith1Substitution' 'TooShortOrTooLong' 'MissingDigit' 'MissingUppercaseLetter' `
'MissingLowercaseLetter' 'MissingSymbol' 'ContainsFourAdjacentLowercaseCharacters' 'ContainsFourAdjacentUppercaseCharacters' `
'ContainsDuplicateCharacters'
New-Enum 'PasswordStrength' 'Weak' 'Moderate' 'Strong'
if ((Get-Variable -Scope 1 MyInvocation -ValueOnly).MyCommand.ScriptBlock -ne $MyInvocation.MyCommand.ScriptBlock) {
if ((-not $global:wordListCache) -or ($RefreshCache)) {
if ($WordListPath -eq $null) {
throw $($(Get-PSResourceString -BaseName 'ParameterBinderStrings' -ResourceId `
'ParameterArgumentValidationErrorNullNotAllowed') -f $null,'WordListPath')
} elseif ($WordListPath.Length -eq 0) {
throw $($(Get-PSResourceString -BaseName 'ParameterBinderStrings' -ResourceId `
'ParameterArgumentValidationErrorEmptyArrayNotAllowed') -f $null,'WordListPath')
} else {
$global:wordListCache = $null
foreach ($path in $WordListPath) {
if (-not (Test-Path -Path $path)) {
throw $($(Get-PSResourceString -BaseName 'SessionStateStrings' -ResourceId 'PathNotFound') -f $path)
} else {
foreach ($item in Get-Item -Path $path) {
$global:wordListCache += Get-Content -LiteralPath $item.PSPath
}
}
}
}
}
}
}
PROCESS {
if ($password -and $_) {
throw $(Get-PSResourceString -BaseName 'ParameterBinderStrings' -ResourceId 'AmbiguousParameterSet')
} elseif ($Password) {
$Password | Measure-PasswordStrength -WordListPath $WordListPath -RefreshCache:$RefreshCache
} elseif ($_) {
# Rule 1 test
$passwordStrengthReportCard = New-Object System.Management.Automation.PSObject
$passwordStrengthReportCard.PSObject.TypeNames[0] += '#PasswordStrengthReportCard'
$passwordStrengthReportCard `
| Add-Member -Name Password -MemberType NoteProperty -Value $_ -PassThru `
| Add-Member -Name Score -MemberType NoteProperty -Value 13 -PassThru `
| Add-Member -Name Reason -MemberType NoteProperty -Value @() -PassThru `
| Add-Member -Name DisplayReason -MemberType ScriptProperty -Value { ($this.Reason | Out-String).Trim("`r`n") } -PassThru `
| Add-Member -Name Strength -MemberType ScriptProperty -Value {if ($this.Score -le 6) {[PasswordStrength]::Weak} `
elseif ($this.Score -le 10) {[PasswordStrength]::Moderate} else {[PasswordStrength]::Strong}}
# Measure against rule 1
if ($global:wordListCache -contains $_) {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::ActualWord
}
# Measure against rule 2
if ($global:wordListCache -contains $_.Remove($_.Length - 1)) {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::ActualWordWithOneCharacterSuffix
}
# Measure against rule 3
if ($global:wordListCache -contains $_.Remove(0,1)) {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::ActualWordWithOneCharacterPrefix
}
# Measure against rule 4
if ($global:wordListCache -contains $_.Replace('0','o')) {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::ActualWordWith0Substitution
}
# Measure against rule 5
if ($global:wordListCache -contains $_.Replace('1','l')) {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::ActualWordWith1Substitution
}
# Measure against rule 6
if (($_.Length -lt 10) -or ($_.Length -gt 20)) {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::TooShortOrTooLong
}
# Measure against rule 7
if ($_ -notmatch '[0-9]') {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::MissingDigit
}
# Measure against rule 8
if ($_ -cnotmatch '[A-Z]') {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::MissingUppercaseLetter
}
# Measure against rule 9
if ($_ -cnotmatch '[a-z]') {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::MissingLowercaseLetter
}
# Measure against rule 10
if ($_ -cnotmatch '[^a-zA-Z0-9\s]') {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::MissingSymbol
}
# Measure against rule 11
if ($_ -cmatch '[a-z]{4,}') {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::ContainsFourAdjacentLowercaseCharacters
}
# Measure against rule 12
if ($_ -cmatch '[A-Z]{4,}') {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::ContainsFourAdjacentUppercaseCharacters
}
# Measure against rule 13
if ([char[]]$_ | Group-Object -NoElement | Where-Object {$_.Count -gt 1}) {
$passwordStrengthReportCard.Score -= 1
$passwordStrengthReportCard.Reason += [PasswordScoreReason]::ContainsDuplicateCharacters
}
$passwordStrengthReportCard | Write-Output
} else {
throw $(Get-PSResourceString -BaseName 'ParameterBinderStrings' -ResourceId 'InputObjectNotBound')
}
}
END {
}
}
}
PROCESS {
if ($MyInvocation.InvocationName -eq '&') {
if ($_) {
Invoke-Expression "`$_ | Measure-PasswordStrength $($passThruArgs = $args; for ($i = 0; $i -lt $passThruArgs.Count; $i++) `
{ if ($passThruArgs[$i] -match '^-') { $passThruArgs[$i] } else { `"`$passThruArgs[$i]`" } })"
} else {
Invoke-Expression "Measure-PasswordStrength $($passThruArgs = $args; for ($i = 0; $i -lt $passThruArgs.Count; $i++) `
{ if ($passThruArgs[$i] -match '^-') { $passThruArgs[$i] } else { `"`$passThruArgs[$i]`" } })"
}
}
}
END {
}