Public/Test-PasswordEntropy.ps1

function Test-PasswordEntropy {
    <#
    .SYNOPSIS
        Evaluates the entropy and strength of a password.
    .DESCRIPTION
        Accepts a secure string, plain string, PSCredential, or SecretManagement secret object
        from the pipeline or parameter and evaluates password entropy.
    .PARAMETER InputObject
        The object to evaluate, as a secure string, plain string, PSCredential, or SecretManagement
    .INPUTS
        string[], System.Security.SecureString[], System.Management.Automation.PSCredential[],
        SecretManagement secret wrapper objects
    .OUTPUTS
        System.Management.Automation.PSCustomObject
    .EXAMPLE
        Read-Host -AsSecureString | Test-PasswordEntropy

        Provide a secure string at the command line, then evaluate it.
    .EXAMPLE
        Get-Secret -Name 'SuperSecret' | Test-PasswordEntropy

        Retrieve a secret from a secret store, then evaluate it.
    .EXAMPLE
        Get-Credential | Test-PasswordEntropy

        Consume a PSCredential object from the pipeline, and evalute its password property.
    .NOTES
        While you could provide an insecure string at the command line, pipe it to
        `ConvertTo-SecureString`, then pipe it to the function, this is not recommended as it
        would expose the secret in plaintext.
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
        [AllowNull()]
        [object]$InputObject
    )

    begin {
        function Convert-SecureStringToPlain {
            param([System.Security.SecureString]$ss)
            if ($null -eq $ss) { return $null }
            $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($ss)
            try {
                [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
            }
            finally {
                [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
            }
        }

        function Get-PlainFromPossibleSecret {
            param($obj)
            if ( $null -eq $obj ) { 
                return $null 
            }

            # If it's already a string
            if ( $obj -is [string] ) {
                return $obj
            }

            # SecureString directly
            if ( $obj -is [System.Security.SecureString] ) {
                return Convert-SecureStringToPlain -ss $obj
            }

            # PSCredential: use password (or username+password if you want)
            if ( $obj -is [System.Management.Automation.PSCredential] ) {
                return $obj.GetNetworkCredential().Password
            }

            # SecretManagement secret wrapper: many providers return an object with a property 'Secret' that is a SecureString
            if ($obj -is [object]) {
                # Try common property names
                foreach ( $propName in @('Secret','Password','Value') ) {
                    if ($obj.PSObject.Properties.Match($propName)) {
                        $val = $obj.$propName
                        if ($val -is [System.Security.SecureString]) {
                            return Convert-SecureStringToPlain -ss $val
                        } elseif ($val -is [string]) {
                            return $val
                        } elseif ($val -is [System.Management.Automation.PSCredential]) {
                            return $val.GetNetworkCredential().Password
                        }
                    }
                }

                # If object is a wrapper and has a ToString that reveals the secret (rare), don't auto-expose; prefer explicit extraction.
            }

            return $null
        }

        function Assert-Entropy {
            param( [double]$entropyBits )
            switch ($entropyBits) {
                { $_ -le 28 } { 'Very Weak'; break }
                { $_ -in (29..35) } { 'Weak'; break }
                { $_ -in (36..59) } { 'Moderate'; break }
                { $_ -in (60..79) } { 'Strong'; break }
                default { 'Very Strong' }
            }
        }
    }

    process {
        $plain = Get-PlainFromPossibleSecret -obj $InputObject

        if ([string]::IsNullOrEmpty($plain)) {
            Write-Verbose "No plaintext password extracted from input of type: $($InputObject.GetType().FullName)"
            return
        }

        # Calculate Shannon entropy per character
        $length = $plain.Length
        if ($length -eq 0) {
            Write-Verbose "Empty string provided."
            return
        }

        $charCounts = @{}
        foreach ($c in $plain.ToCharArray()) {
            if ($charCounts.ContainsKey($c)) { $charCounts[$c]++ } else { $charCounts[$c] = 1 }
        }

        $hPerChar = 0.0
        foreach ($count in $charCounts.Values) {
            $p = $count / $length
            $hPerChar += -1 * $p * [Math]::Log($p, 2)
        }

        $totalEntropy = $hPerChar * $length
        $roundedTotal = [Math]::Round($totalEntropy, 2)
        $roundedPerChar = [Math]::Round($hPerChar, 4)
        $strength = Assert-Entropy -entropyBits $roundedTotal

        [PSCustomObject]@{
            Length          = $length
            EntropyPerChar  = $roundedPerChar
            EntropyBits     = $roundedTotal
            Strength        = $strength
        }
    }

    end {}
}