Set-NTPConfig.psm1

<#
.SYNOPSIS
    Configures Windows NTP client with secure, reliable time synchronization.
 
.DESCRIPTION
    Sets NTP servers, configures polling intervals, and ensures Windows Time service
    is properly configured. Includes error handling, validation, and geographic detection.
 
.PARAMETER NtpServers
    Comma-separated list of NTP servers. If not specified, automatically detects region.
 
.PARAMETER Region
    Geographic region for NTP pool selection. Options: NorthAmerica, Europe, Asia, Oceania, SouthAmerica, Africa, Auto.
    Default is Auto (detects based on timezone).
 
.PARAMETER SpecialPollInterval
    Poll interval in seconds. Default is 900 (15 minutes) for workstations, 300 (5 minutes) for servers.
    Valid range: 64-86400. Lower values increase accuracy but also network traffic.
 
.PARAMETER ServerType
    System type. Options: Workstation, Server. Adjusts default poll interval accordingly.
 
.PARAMETER Force
    Skip confirmation prompts.
 
.EXAMPLE
    Set-NTPConfig
    Automatically detects region and configures with appropriate defaults.
 
.EXAMPLE
    Set-NTPConfig -Region Europe -SpecialPollInterval 300
    Uses European NTP pool servers with 5-minute polling.
 
.EXAMPLE
    Set-NTPConfig -NtpServers "time.cloudflare.com,0x9 time.google.com,0x9" -Force
    Uses custom NTP servers without confirmation.
 
.EXAMPLE
    Set-NTPConfig -ServerType Server
    Configures for server with 5-minute default polling interval.
 
.NOTES
    Requires Administrator privileges.
    Author: Collin George
    Version: 2.0.5
    Last Updated: October 2, 2025
    License: MIT License
 
    MIT License:
    Copyright (c) 2025 Collin George
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    The above copyright notice and this permission notice shall be included in all
    copies or substantial portions of the Software.
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    SOFTWARE.
 
    Changelog:
    - v2.0.5 (2025-10-02): Fixed syntax error in Write-Log function.
    - v2.0.4 (2025-10-02): Moved main logic into Set-NTPConfig function to fix $PSCmdlet error on import.
    - v2.0.3 (2025-10-02): Moved SpecialPollInterval validation to script body.
    - v2.0.2 (2025-10-02): Attempted fix using Nullable[int] for SpecialPollInterval.
    - v2.0.1 (2025-10-02): Attempted fix for SpecialPollInterval validation error.
    - v2.0 (2025-10-01): Added MIT License, enhanced error handling, region-based NTP pools.
    - v1.0: Initial release.
#>


function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Message,
        
        [Parameter(Mandatory=$false)]
        [ValidateSet('Info', 'Warning', 'Error', 'Success')]
        [string]$Level = 'Info'
    )
    
    $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
    $color = switch ($Level) {
        'Info'    { 'Cyan' }
        'Warning' { 'Yellow' }
        'Error'   { 'Red' }
        'Success' { 'Green' }
    }
    
    Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
}

function Get-RegionFromTimezone {
    [CmdletBinding()]
    param()
    
    try {
        $timezone = Get-TimeZone
        $timezoneId = $timezone.Id
        
        switch -Regex ($timezoneId) {
            'Pacific|Mountain|Central|Eastern|Alaska|Hawaii|US|Canada|Mexico' { return 'NorthAmerica' }
            'Europe|GMT|UTC|London|Paris|Berlin|Rome|Madrid' { return 'Europe' }
            'Asia|Tokyo|Seoul|Shanghai|Hong Kong|Singapore|India' { return 'Asia' }
            'Australia|New Zealand|Pacific/Auckland' { return 'Oceania' }
            'South America|Argentina|Brazil|Chile' { return 'SouthAmerica' }
            'Africa|Cairo|Johannesburg' { return 'Africa' }
            default { 
                Write-Log "Could not detect region from timezone: $timezoneId. Defaulting to NorthAmerica." -Level Warning
                return 'NorthAmerica' 
            }
        }
    }
    catch {
        Write-Log "Error detecting timezone: $_. Defaulting to NorthAmerica." -Level Warning
        return 'NorthAmerica'
    }
}

function Get-NtpServersForRegion {
    [CmdletBinding()]
    param([string]$Region)
    
    $ntpPools = @{
        'NorthAmerica' = "0.north-america.pool.ntp.org,0x9 1.north-america.pool.ntp.org,0x9 2.north-america.pool.ntp.org,0x9 3.north-america.pool.ntp.org,0x9"
        'Europe'       = "0.europe.pool.ntp.org,0x9 1.europe.pool.ntp.org,0x9 2.europe.pool.ntp.org,0x9 3.europe.pool.ntp.org,0x9"
        'Asia'         = "0.asia.pool.ntp.org,0x9 1.asia.pool.ntp.org,0x9 2.asia.pool.ntp.org,0x9 3.asia.pool.ntp.org,0x9"
        'Oceania'      = "0.oceania.pool.ntp.org,0x9 1.oceania.pool.ntp.org,0x9 2.oceania.pool.ntp.org,0x9 3.oceania.pool.ntp.org,0x9"
        'SouthAmerica' = "0.south-america.pool.ntp.org,0x9 1.south-america.pool.ntp.org,0x9 2.south-america.pool.ntp.org,0x9 3.south-america.pool.ntp.org,0x9"
        'Africa'       = "0.africa.pool.ntp.org,0x9 1.africa.pool.ntp.org,0x9 2.africa.pool.ntp.org,0x9 3.africa.pool.ntp.org,0x9"
    }
    
    return $ntpPools[$Region]
}

function Test-RegistryPath {
    [CmdletBinding()]
    param([string]$Path)
    
    try {
        return Test-Path -Path $Path -ErrorAction Stop
    }
    catch {
        Write-Log "Failed to access registry path: $Path" -Level Error
        return $false
    }
}

function Set-RegistryValue {
    [CmdletBinding()]
    param(
        [string]$Path,
        [string]$Name,
        [object]$Value,
        [string]$Type = 'String'
    )
    
    try {
        if (-not (Test-RegistryPath -Path $Path)) {
            Write-Log "Registry path does not exist: $Path" -Level Error
            throw "Registry path not found"
        }
        
        Set-ItemProperty -Path $Path -Name $Name -Value $Value -Type $Type -ErrorAction Stop
        Write-Log "Set $Name = $Value at $Path" -Level Success
        return $true
    }
    catch {
        Write-Log "Failed to set registry value $Name at $Path : $_" -Level Error
        throw
    }
}

function Wait-ServiceState {
    [CmdletBinding()]
    param(
        [string]$ServiceName,
        [string]$DesiredState,
        [int]$TimeoutSeconds = 30
    )
    
    $elapsed = 0
    $intervalMs = 500
    
    while ($elapsed -lt ($TimeoutSeconds * 1000)) {
        $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
        if ($service.Status -eq $DesiredState) {
            return $true
        }
        Start-Sleep -Milliseconds $intervalMs
        $elapsed += $intervalMs
    }
    
    return $false
}

function Test-NtpServer {
    [CmdletBinding()]
    param([string]$Server)
    
    try {
        Write-Log "Testing connectivity to $Server..." -Level Info
        $result = w32tm /stripchart /computer:$Server /samples:1 /dataonly 2>&1
        
        if ($LASTEXITCODE -eq 0) {
            Write-Log "Successfully contacted $Server" -Level Success
            return $true
        }
        else {
            Write-Log "Could not reach $Server" -Level Warning
            return $false
        }
    }
    catch {
        Write-Log "Error testing $Server : $_" -Level Warning
        return $false
    }
}

function Set-NTPConfig {
    [CmdletBinding(SupportsShouldProcess=$true)]
    param(
        [Parameter(Mandatory=$false)]
        [string]$NtpServers,
        
        [Parameter(Mandatory=$false)]
        [ValidateSet('NorthAmerica', 'Europe', 'Asia', 'Oceania', 'SouthAmerica', 'Africa', 'Auto')]
        [string]$Region = 'Auto',
        
        [Parameter(Mandatory=$false)]
        [Nullable[int]]$SpecialPollInterval = $null,
        
        [Parameter(Mandatory=$false)]
        [ValidateSet('Workstation', 'Server')]
        [string]$ServerType = 'Workstation',
        
        [Parameter(Mandatory=$false)]
        [switch]$Force
    )

    #Requires -RunAsAdministrator

    # Set strict mode for better error detection
    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    try {
        Write-Log "=== Windows NTP Configuration Script v2.0.5 ===" -Level Info
        Write-Log "Starting NTP configuration..." -Level Info
        
        # Validate SpecialPollInterval
        if ($PSBoundParameters.ContainsKey('SpecialPollInterval') -and $null -ne $SpecialPollInterval) {
            if ($SpecialPollInterval -lt 64 -or $SpecialPollInterval -gt 86400) {
                Write-Log "SpecialPollInterval must be between 64 and 86400 seconds. Got: $SpecialPollInterval" -Level Error
                throw "Invalid SpecialPollInterval value"
            }
        }
        
        # Set default poll interval based on server type if not specified
        if (-not $PSBoundParameters.ContainsKey('SpecialPollInterval') -or $null -eq $SpecialPollInterval) {
            $SpecialPollInterval = if ($ServerType -eq 'Server') { 300 } else { 900 }
            Write-Log "Using default poll interval for ${ServerType}: $SpecialPollInterval seconds" -Level Info
        }
        
        # Determine region if auto-detect
        if (-not $NtpServers) {
            if ($Region -eq 'Auto') {
                $Region = Get-RegionFromTimezone
                Write-Log "Auto-detected region: $Region" -Level Info
            }
            
            $NtpServers = Get-NtpServersForRegion -Region $Region
            Write-Log "Using $Region NTP pool servers" -Level Info
        }
        else {
            Write-Log "Using custom NTP servers" -Level Info
        }
        
        Write-Log "NTP Servers: $NtpServers" -Level Info
        Write-Log "Poll Interval: $SpecialPollInterval seconds ($([Math]::Round($SpecialPollInterval/60, 1)) minutes)" -Level Info
        
        # Test connectivity to first NTP server
        $firstServer = ($NtpServers -split ' ')[0] -replace ',0x9', ''
        Test-NtpServer -Server $firstServer
        
        # Backup current configuration
        Write-Log "`nBacking up current configuration..." -Level Info
        $w32timeParams = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters"
        $currentNtpServer = (Get-ItemProperty -Path $w32timeParams -Name "NtpServer" -ErrorAction SilentlyContinue).NtpServer
        $currentType = (Get-ItemProperty -Path $w32timeParams -Name "Type" -ErrorAction SilentlyContinue).Type
        
        if ($currentNtpServer) {
            Write-Log "Current NTP Server: $currentNtpServer" -Level Info
            Write-Log "Current Type: $currentType" -Level Info
        }
        else {
            Write-Log "No existing NTP configuration found" -Level Info
        }
        
        # Confirm changes if not forced
        if (-not $Force -and $PSCmdlet.ShouldProcess("Windows Time Service", "Configure NTP settings")) {
            Write-Host "`nThis will configure Windows Time with the following settings:" -ForegroundColor Yellow
            Write-Host " Region: $Region" -ForegroundColor White
            Write-Host " NTP Servers: $NtpServers" -ForegroundColor White
            Write-Host " Poll Interval: $SpecialPollInterval seconds" -ForegroundColor White
            Write-Host ""
            $continue = Read-Host "Continue with configuration? (Y/N)"
            if ($continue -ne 'Y') {
                Write-Log "Configuration cancelled by user." -Level Warning
                return
            }
        }
        
        # 1. Configure W32Time Parameters
        Write-Log "`nConfiguring W32Time parameters..." -Level Info
        Set-RegistryValue -Path $w32timeParams -Name "NtpServer" -Value $NtpServers
        Set-RegistryValue -Path $w32timeParams -Name "Type" -Value "NTP"
        
        # 2. Configure NtpClient Provider
        Write-Log "Configuring NtpClient provider..." -Level Info
        $ntpClientPath = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient"
        Set-RegistryValue -Path $ntpClientPath -Name "SpecialPollInterval" -Value $SpecialPollInterval -Type DWord
        Set-RegistryValue -Path $ntpClientPath -Name "Enabled" -Value 1 -Type DWord
        
        # 3. Configure additional reliability settings
        Write-Log "Configuring additional time service settings..." -Level Info
        $configPath = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config"
        Set-RegistryValue -Path $configPath -Name "MaxPosPhaseCorrection" -Value 3600 -Type DWord
        Set-RegistryValue -Path $configPath -Name "MaxNegPhaseCorrection" -Value 3600 -Type DWord
        Set-RegistryValue -Path $configPath -Name "UpdateInterval" -Value 100 -Type DWord
        
        # 4. Configure Windows Time Service
        Write-Log "Configuring Windows Time service..." -Level Info
        $service = Get-Service -Name w32time -ErrorAction Stop
        
        if ($service.StartType -ne 'Automatic') {
            Set-Service -Name w32time -StartupType Automatic -ErrorAction Stop
            Write-Log "Set w32time service to Automatic startup" -Level Success
        }
        
        # 5. Restart Service
        Write-Log "Restarting Windows Time service..." -Level Info
        
        if ($service.Status -eq 'Running') {
            Stop-Service -Name w32time -Force -ErrorAction Stop
            
            if (-not (Wait-ServiceState -ServiceName w32time -DesiredState Stopped -TimeoutSeconds 30)) {
                throw "Service did not stop within timeout period"
            }
            Write-Log "Service stopped successfully" -Level Success
        }
        
        Start-Service -Name w32time -ErrorAction Stop
        
        if (-not (Wait-ServiceState -ServiceName w32time -DesiredState Running -TimeoutSeconds 30)) {
            throw "Service did not start within timeout period"
        }
        Write-Log "Service started successfully" -Level Success
        
        # 6. Apply Configuration and Sync
        Write-Log "Applying configuration changes..." -Level Info
        $updateResult = w32tm /config /update 2>&1
        if ($LASTEXITCODE -ne 0) {
            Write-Log "w32tm /config /update returned non-zero exit code: $LASTEXITCODE" -Level Warning
            Write-Log "Output: $updateResult" -Level Warning
        }
        else {
            Write-Log "Configuration updated successfully" -Level Success
        }
        
        # Wait for service to process configuration
        Start-Sleep -Seconds 5
        
        Write-Log "Forcing immediate time synchronization..." -Level Info
        $resyncResult = w32tm /resync /rediscover 2>&1
        if ($LASTEXITCODE -eq 0) {
            Write-Log "Time synchronization initiated successfully" -Level Success
        }
        else {
            Write-Log "Resync returned code $LASTEXITCODE - this may be normal if sync is in progress" -Level Warning
        }
        
        # 7. Verify Configuration
        Write-Log "`n=== VERIFICATION ===" -Level Info
        
        $verifyNtpServer = (Get-ItemProperty -Path $w32timeParams -Name "NtpServer").NtpServer
        $verifyType = (Get-ItemProperty -Path $w32timeParams -Name "Type").Type
        $verifyPollInterval = (Get-ItemProperty -Path $ntpClientPath -Name "SpecialPollInterval").SpecialPollInterval
        $verifyService = Get-Service -Name w32time
        
        Write-Host "`nConfigured NTP Servers: " -NoNewline
        Write-Host $verifyNtpServer -ForegroundColor Green
        Write-Host "Poll Interval: " -NoNewline
        Write-Host "$verifyPollInterval seconds ($([Math]::Round($verifyPollInterval/60, 1)) minutes)" -ForegroundColor Green
        Write-Host "Service Status: " -NoNewline
        Write-Host "$($verifyService.Status) ($($verifyService.StartType))" -ForegroundColor Green
        
        # 8. Display Current Status
        Write-Log "`n=== CURRENT TIME SYNCHRONIZATION STATUS ===" -Level Info
        w32tm /query /status
        
        Write-Log "`n=== CONFIGURATION COMPLETE ===" -Level Success
        Write-Host "`nNTP configuration completed successfully!" -ForegroundColor Green
        Write-Host "Note: It may take a few minutes for initial time synchronization to complete." -ForegroundColor Yellow
        Write-Host "`nTo verify sync status later, run: " -NoNewline -ForegroundColor Cyan
        Write-Host "w32tm /query /status" -ForegroundColor White
    }
    catch {
        Write-Log "`n=== FATAL ERROR ===" -Level Error
        Write-Log "Error: $_" -Level Error
        Write-Log "Stack Trace: $($_.ScriptStackTrace)" -Level Error
        
        # Attempt to restore service if it's stopped
        $service = Get-Service -Name w32time -ErrorAction SilentlyContinue
        if ($service -and $service.Status -ne 'Running') {
            Write-Log "Attempting to restart Windows Time service..." -Level Warning
            try {
                Start-Service -Name w32time -ErrorAction Stop
                Write-Log "Service restarted successfully" -Level Success
            }
            catch {
                Write-Log "Failed to restart service: $_" -Level Error
                Write-Log "You may need to manually restart the Windows Time service" -Level Error
            }
        }
        
        throw
    }
    finally {
        Write-Log "`nScript execution completed." -Level Info
    }
}

# Export the main function
Export-ModuleMember -Function Set-NTPConfig