Private/New-GkPassword.ps1

function New-GkPassword {
    <#
    .SYNOPSIS
        Generate a random password that satisfies the default Entra complexity policy.
    .DESCRIPTION
        Uses the cryptographic RNG, not Get-Random, because Get-Random is seeded from a predictable
        source and is not fit for credential material.

        Guarantees at least one character from each of the four classes Entra requires, then fills
        the remainder from the combined alphabet and shuffles, so the guaranteed characters do not
        always land in the same positions.

        Ambiguous glyphs (O/0, l/1/I) are excluded: these passwords get read aloud and typed by
        hand, and a reset that fails because someone read a zero as an O is a support call.
    .OUTPUTS
        System.String
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [ValidateRange(12, 128)]
        [int] $Length = 20
    )

    $upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ'      # no I, O
    $lower = 'abcdefghijkmnpqrstuvwxyz'      # no l, o
    $digit = '23456789'                      # no 0, 1
    $symbol = '!#%&*+-=?@_'                  # shell- and CSV-safe

    $classes = @($upper, $lower, $digit, $symbol)
    $all = -join $classes

    # One from each class first, so complexity is guaranteed rather than probable.
    $chars = [System.Collections.Generic.List[char]]::new()
    foreach ($c in $classes) { $chars.Add((Get-GkRandomChar -Alphabet $c)) }
    while ($chars.Count -lt $Length) { $chars.Add((Get-GkRandomChar -Alphabet $all)) }

    # Fisher-Yates with crypto-random indices, so the guaranteed characters are not always first.
    for ($i = $chars.Count - 1; $i -gt 0; $i--) {
        $j = [System.Security.Cryptography.RandomNumberGenerator]::GetInt32(0, $i + 1)
        $tmp = $chars[$i]; $chars[$i] = $chars[$j]; $chars[$j] = $tmp
    }

    return (-join $chars)
}

function Get-GkRandomChar {
    <#
    .SYNOPSIS
        Pick one character from an alphabet using the cryptographic RNG.
    #>

    [CmdletBinding()]
    [OutputType([char])]
    param(
        [Parameter(Mandatory)] [string] $Alphabet
    )
    return $Alphabet[[System.Security.Cryptography.RandomNumberGenerator]::GetInt32(0, $Alphabet.Length)]
}