Private/Config/Get-PukModuleConfig.ps1

function Get-PukModuleConfig {
    [CmdletBinding()]
    param(
        [string]$Path,
        [switch]$Force
    )

    if (-not $Path) {
        if ($script:ModuleRoot) {
            $Path = Join-Path -Path $script:ModuleRoot -ChildPath 'Config/config.json'
        }
        else {
            $Path = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Config/config.json'
        }
    }

    if (-not $Force -and $script:PukModuleConfig -and $script:PukModuleConfigPath -eq $Path) {
        return $script:PukModuleConfig
    }

    if (-not (Test-Path -LiteralPath $Path)) {
        throw "PWSHPUKMGT configuration file not found at '$Path'."
    }

    $raw = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
    try {
        $config = $raw | ConvertFrom-Json -ErrorAction Stop
    }
    catch {
        throw "PWSHPUKMGT configuration file at '$Path' is not valid JSON: $($_.Exception.Message)"
    }

    function Test-PukPositiveInteger {
        param($Value)
        if ($null -eq $Value) { return $false }
        if ($Value -isnot [int] -and $Value -isnot [long] -and $Value -isnot [double]) { return $false }
        return ($Value -eq [Math]::Truncate($Value)) -and $Value -gt 0
    }

    $validationErrors = [System.Collections.Generic.List[string]]::new()

    if ([string]::IsNullOrWhiteSpace($config.schemaVersion)) {
        $validationErrors.Add("'schemaVersion' is required.")
    }

    if ([string]::IsNullOrWhiteSpace($config.certificate.thumbprint)) {
        $validationErrors.Add("'certificate.thumbprint' is required.")
    }
    elseif ($config.certificate.thumbprint -notmatch '^[0-9A-Fa-f]{40}$') {
        $validationErrors.Add("'certificate.thumbprint' must be a 40-character hexadecimal SHA-1 thumbprint.")
    }

    if ($config.certificate.storeLocation -notin @('CurrentUser', 'LocalMachine')) {
        $validationErrors.Add("'certificate.storeLocation' must be 'CurrentUser' or 'LocalMachine'.")
    }

    $requiredEkuOids = @($config.certificate.requiredEkuOids)
    if ($requiredEkuOids.Count -eq 0) {
        $validationErrors.Add("'certificate.requiredEkuOids' must contain at least one OID.")
    }
    else {
        foreach ($oid in $requiredEkuOids) {
            if ($oid -notmatch '^\d+(\.\d+)+$') {
                $validationErrors.Add("'certificate.requiredEkuOids' contains an invalid OID: '$oid'.")
            }
        }
    }

    if ($config.hosting.model -notin @('FlatFile', 'ActiveDirectory')) {
        $validationErrors.Add("'hosting.model' must be 'FlatFile' or 'ActiveDirectory'.")
    }

    if ([string]::IsNullOrWhiteSpace($config.hosting.flatFile.path)) {
        $validationErrors.Add("'hosting.flatFile.path' is required.")
    }

    if ([string]::IsNullOrWhiteSpace($config.hosting.activeDirectory.attributeName)) {
        $validationErrors.Add("'hosting.activeDirectory.attributeName' is required.")
    }

    if (-not (Test-PukPositiveInteger -Value $config.hosting.activeDirectory.maxAttributeValueLength)) {
        $validationErrors.Add("'hosting.activeDirectory.maxAttributeValueLength' must be a positive integer.")
    }

    if ($config.puk.characterSet -ne 'Digits') {
        $validationErrors.Add("'puk.characterSet' must be 'Digits' (no other character set is currently implemented).")
    }

    foreach ($lengthField in @('minLength', 'maxLength', 'defaultLength')) {
        if (-not (Test-PukPositiveInteger -Value $config.puk.$lengthField)) {
            $validationErrors.Add("'puk.$lengthField' must be a positive integer.")
        }
    }

    if ($validationErrors.Count -eq 0) {
        if ($config.puk.minLength -gt $config.puk.defaultLength -or $config.puk.defaultLength -gt $config.puk.maxLength) {
            $validationErrors.Add("'puk.minLength' <= 'puk.defaultLength' <= 'puk.maxLength' must hold.")
        }
    }

    if (-not (Test-PukPositiveInteger -Value $config.puk.maxGenerationAttempts)) {
        $validationErrors.Add("'puk.maxGenerationAttempts' must be a positive integer.")
    }

    $weaknessRules = $config.puk.weaknessRules
    if (-not $weaknessRules) {
        $validationErrors.Add("'puk.weaknessRules' is required.")
    }
    else {
        foreach ($ruleName in @('rejectSequentialRun', 'rejectRepeatedDigitRun', 'rejectLowUniqueDigitCount', 'rejectRepeatingPattern', 'rejectPalindrome', 'rejectDatePattern')) {
            $rule = $weaknessRules.$ruleName
            if (-not $rule -or -not ($rule.enabled -is [bool])) {
                $validationErrors.Add("'puk.weaknessRules.$ruleName.enabled' must be a boolean.")
            }
        }

        if ($weaknessRules.rejectSequentialRun.enabled -and (-not (Test-PukPositiveInteger -Value $weaknessRules.rejectSequentialRun.runLength) -or $weaknessRules.rejectSequentialRun.runLength -le 1)) {
            $validationErrors.Add("'puk.weaknessRules.rejectSequentialRun.runLength' must be an integer greater than 1 when enabled.")
        }

        if ($weaknessRules.rejectRepeatedDigitRun.enabled -and (-not (Test-PukPositiveInteger -Value $weaknessRules.rejectRepeatedDigitRun.runLength) -or $weaknessRules.rejectRepeatedDigitRun.runLength -le 1)) {
            $validationErrors.Add("'puk.weaknessRules.rejectRepeatedDigitRun.runLength' must be an integer greater than 1 when enabled.")
        }

        if ($weaknessRules.rejectLowUniqueDigitCount.enabled -and -not (Test-PukPositiveInteger -Value $weaknessRules.rejectLowUniqueDigitCount.minUniqueDigits)) {
            $validationErrors.Add("'puk.weaknessRules.rejectLowUniqueDigitCount.minUniqueDigits' must be a positive integer when enabled.")
        }

        if ($null -ne $weaknessRules.customDenyList) {
            foreach ($entry in @($weaknessRules.customDenyList)) {
                if ($entry -isnot [string]) {
                    $validationErrors.Add("'puk.weaknessRules.customDenyList' must contain only strings.")
                    break
                }
            }
        }
    }

    if ($config.logging.target -notin @('File', 'EventLog', 'Both')) {
        $validationErrors.Add("'logging.target' must be one of: File, EventLog, Both.")
    }

    if ($config.logging.level -notin @('Debug', 'Info', 'Warning', 'Error')) {
        $validationErrors.Add("'logging.level' must be one of: Debug, Info, Warning, Error.")
    }

    if ($config.logging.target -in @('File', 'Both') -and [string]::IsNullOrWhiteSpace($config.logging.file.path)) {
        $validationErrors.Add("'logging.file.path' is required when 'logging.target' is 'File' or 'Both'.")
    }

    if ($config.logging.target -in @('EventLog', 'Both')) {
        if ([string]::IsNullOrWhiteSpace($config.logging.eventLog.logName)) {
            $validationErrors.Add("'logging.eventLog.logName' is required when 'logging.target' is 'EventLog' or 'Both'.")
        }
        if ([string]::IsNullOrWhiteSpace($config.logging.eventLog.source)) {
            $validationErrors.Add("'logging.eventLog.source' is required when 'logging.target' is 'EventLog' or 'Both'.")
        }
        if (-not (Test-PukPositiveInteger -Value $config.logging.eventLog.eventId)) {
            $validationErrors.Add("'logging.eventLog.eventId' must be a positive integer when 'logging.target' is 'EventLog' or 'Both'.")
        }
    }

    if ($validationErrors.Count -gt 0) {
        throw "PWSHPUKMGT configuration at '$Path' is invalid:`n - $($validationErrors -join "`n - ")"
    }

    $config.hosting.flatFile.path = [System.Environment]::ExpandEnvironmentVariables($config.hosting.flatFile.path)
    if ($config.logging.file.path) {
        $config.logging.file.path = [System.Environment]::ExpandEnvironmentVariables($config.logging.file.path)
    }

    $script:PukModuleConfig = $config
    $script:PukModuleConfigPath = $Path

    return $config
}