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.8
    Last Updated: October 7, 2025
    License: MIT License
#>


function Write-Log {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Message,
        
        [Parameter()]
        [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 {
    try {
        $timezoneId = (Get-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 {
    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-NtpServer {
    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(
        [string]$NtpServers,
        [ValidateSet('NorthAmerica','Europe','Asia','Oceania','SouthAmerica','Africa','Auto')]
        [string]$Region = 'Auto',
        [Nullable[int]]$SpecialPollInterval = $null,
        [ValidateSet('Workstation','Server')]
        [string]$ServerType = 'Workstation',
        [switch]$Force
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'

    try {
        Write-Log "=== Windows NTP Configuration Script v2.0.8 ===" -Level Info

        if (-not $SpecialPollInterval) {
            $SpecialPollInterval = if ($ServerType -eq 'Server') { 300 } else { 900 }
            Write-Log "Using default poll interval for ${ServerType}: $SpecialPollInterval seconds" -Level Info
        }
        elseif ($SpecialPollInterval -lt 64 -or $SpecialPollInterval -gt 86400) {
            throw "SpecialPollInterval must be between 64 and 86400 seconds."
        }

        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" -Level Info

        $firstServer = ($NtpServers -split ' ')[0] -replace ',0x9',''
        Test-NtpServer -Server $firstServer

        # Registry paths
        $w32timeParams = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters"
        $ntpClientPath = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\TimeProviders\NtpClient"
        $configPath = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Config"

        if ($PSCmdlet.ShouldProcess("Windows Time Service", "Configure NTP settings")) {

            # Configure parameters
            Set-ItemProperty -Path $w32timeParams -Name "NtpServer" -Value $NtpServers
            Set-ItemProperty -Path $w32timeParams -Name "Type" -Value "NTP"
            Set-ItemProperty -Path $ntpClientPath -Name "SpecialPollInterval" -Value $SpecialPollInterval -Type DWord
            Set-ItemProperty -Path $ntpClientPath -Name "Enabled" -Value 1 -Type DWord
            Set-ItemProperty -Path $configPath -Name "MaxPosPhaseCorrection" -Value 3600 -Type DWord
            Set-ItemProperty -Path $configPath -Name "MaxNegPhaseCorrection" -Value 3600 -Type DWord
            Set-ItemProperty -Path $configPath -Name "UpdateInterval" -Value 100 -Type DWord

            # Service
            $service = Get-Service -Name w32time
            if ($service.StartType -ne 'Automatic') { Set-Service -Name w32time -StartupType Automatic }

            if ($service.Status -eq 'Running') { Stop-Service -Name w32time -Force; Start-Sleep 2 }

            Start-Service -Name w32time
            Start-Sleep 5

            w32tm /config /update
            w32tm /resync /rediscover
        }

        Write-Log "=== NTP configuration complete ===" -Level Success
        Write-Host "Configured NTP Servers: $NtpServers" -ForegroundColor Green
        Write-Host "Poll Interval: $SpecialPollInterval seconds" -ForegroundColor Green
        Get-Service w32time | Format-Table -Property Name, Status, StartType
        w32tm /query /status
    }
    catch {
        Write-Log "Error: $_" -Level Error
        throw
    }
}

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