Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing function Split-List { param([string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return @() } return $Text -split '[,\r\n;]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ } } function Merge-Hostnames { param( [string[]]$Existing, [string[]]$NewItems ) $set = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase) foreach ($item in $Existing) { [void]$set.Add($item) } foreach ($item in $NewItems) { [void]$set.Add($item) } return $set | Sort-Object } function Get-LocalIpv4 { try { $candidates = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction Stop | Where-Object { $_.IPAddress -and $_.IPAddress -notlike "169.254.*" -and $_.IPAddress -ne "127.0.0.1" -and $_.PrefixOrigin -in @("Dhcp", "Manual") } | Sort-Object -Property InterfaceMetric, SkipAsSource $ip = $candidates | Select-Object -First 1 -ExpandProperty IPAddress if ($ip) { return $ip } } catch { # Ignore and fallback to DNS lookup. } try { $dnsIps = [System.Net.Dns]::GetHostAddresses($env:COMPUTERNAME) | Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork -and $_.ToString() -notlike "169.254.*" -and $_.ToString() -ne "127.0.0.1" } | Select-Object -First 1 if ($dnsIps) { return $dnsIps.ToString() } } catch { # Ignore. } return "" } function Get-FilePreview { param( [string]$Path, [int]$MaxLines = 200 ) if ([string]::IsNullOrWhiteSpace($Path)) { return "" } if (-not (Test-Path -Path $Path -PathType Leaf)) { return "" } try { $lines = Get-Content -Path $Path -TotalCount $MaxLines -ErrorAction Stop return ($lines -join [Environment]::NewLine) } catch { return "Failed to read file preview: $($_.Exception.Message)" } } function Get-CommonZoneFromHosts { param([string[]]$Hosts) $fqdnHosts = @($Hosts | Where-Object { $_ -and $_.Contains(".") }) if (-not $fqdnHosts -or $fqdnHosts.Count -eq 0) { return "" } $commonSuffix = $null foreach ($hostName in $fqdnHosts) { $clean = $hostName.Trim().TrimEnd(".") $labels = $clean -split "\." if ($labels.Count -lt 2) { continue } $zoneLabels = $labels[1..($labels.Count - 1)] if (-not $commonSuffix) { $commonSuffix = $zoneLabels continue } $i = $commonSuffix.Count - 1 $j = $zoneLabels.Count - 1 $newSuffix = @() while ($i -ge 0 -and $j -ge 0) { if ($commonSuffix[$i].ToLower() -ne $zoneLabels[$j].ToLower()) { break } $newSuffix = ,$zoneLabels[$j] + $newSuffix $i-- $j-- } $commonSuffix = $newSuffix if (-not $commonSuffix -or $commonSuffix.Count -eq 0) { break } } if (-not $commonSuffix -or $commonSuffix.Count -eq 0) { return "" } return ($commonSuffix -join ".") } function Get-DefaultsPath { $dir = Join-Path $env:ProgramData "Certy" return Join-Path $dir "defaults.json" } function Load-Defaults { $path = Get-DefaultsPath if (-not (Test-Path -Path $path -PathType Leaf)) { return $null } try { $raw = Get-Content -Path $path -Raw -ErrorAction Stop return $raw | ConvertFrom-Json } catch { return $null } } function Save-Defaults { param([pscustomobject]$Defaults) $path = Get-DefaultsPath $dir = Split-Path -Path $path -Parent if (-not (Test-Path -Path $dir -PathType Container)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null } $json = $Defaults | ConvertTo-Json -Depth 6 Set-Content -Path $path -Value $json -Encoding ascii } function Get-ReplicationCredentialPath { $dir = Join-Path $env:ProgramData "Certy" return Join-Path $dir "replication-cred.xml" } function Load-ReplicationCredential { $path = Get-ReplicationCredentialPath if (-not (Test-Path -Path $path -PathType Leaf)) { return $null } try { $cred = Import-Clixml -Path $path if ($cred -is [pscredential]) { return $cred } } catch { # Ignore load errors and treat as missing. } return $null } function Save-ReplicationCredential { param([pscredential]$Credential) $path = Get-ReplicationCredentialPath $dir = Split-Path -Path $path -Parent if (-not (Test-Path -Path $dir -PathType Container)) { New-Item -Path $dir -ItemType Directory -Force | Out-Null } $Credential | Export-Clixml -Path $path } function Get-DefaultValue { param( [object]$Defaults, [string]$Name ) if ($null -eq $Defaults) { return $null } $prop = $Defaults.PSObject.Properties[$Name] if ($null -eq $prop) { return $null } return $prop.Value } function Resolve-HostEntry { param( [string]$Name, [string]$Zone, [bool]$UseProvidedFqdn ) $name = $Name.Trim() if (-not $name) { return $null } $zoneLower = $Zone.ToLower() $nameLower = $name.ToLower() if ($UseProvidedFqdn) { if ($name -like "*.*") { $fqdn = $name } else { $fqdn = "$name.$Zone" } } else { if ($name -like "*.*") { if ($nameLower.EndsWith(".$zoneLower") -or $nameLower -eq $zoneLower) { $fqdn = $name } else { $fqdn = "$name.$Zone" } } else { $fqdn = "$name.$Zone" } } $fqdnLower = $fqdn.ToLower() if ($fqdnLower.EndsWith(".$zoneLower")) { $hostLabel = $fqdn.Substring(0, $fqdn.Length - $Zone.Length - 1) } elseif ($fqdnLower -eq $zoneLower) { $hostLabel = "@" } else { $hostLabel = $fqdn } return [pscustomobject]@{ Input = $name Fqdn = $fqdn HostLabel = $hostLabel } } function Get-DnsServerCandidates { $servers = @() try { Import-Module ActiveDirectory -ErrorAction Stop | Out-Null $servers += Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName } catch { # Ignore AD lookup failures and fallback to local DNS client config. } try { $local = Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction Stop | ForEach-Object { $_.ServerAddresses } | Where-Object { $_ } $servers += $local } catch { # Ignore if DnsClient module is unavailable. } return $servers | Sort-Object -Unique } function Ensure-ARecord { param( [string]$Zone, [string]$HostLabel, [string]$TargetIp, [string]$DnsServer, [scriptblock]$Log ) $existing = Get-DnsServerResourceRecord -ZoneName $Zone -Name $HostLabel -RRType "A" -ComputerName $DnsServer -ErrorAction SilentlyContinue if ($existing) { $record = $existing | Select-Object -First 1 $currentIp = $record.RecordData.IPv4Address.ToString() if ($currentIp -eq $TargetIp) { & $Log "DNS A record exists: $HostLabel.$Zone -> $TargetIp" return } $newRecord = $record.Clone() $newRecord.RecordData.IPv4Address = [ipaddress]$TargetIp Set-DnsServerResourceRecord -ZoneName $Zone -OldInputObject $record -NewInputObject $newRecord -ComputerName $DnsServer & $Log "DNS A record updated: $HostLabel.$Zone -> $TargetIp" return } Add-DnsServerResourceRecordA -Name $HostLabel -ZoneName $Zone -IPv4Address $TargetIp -ComputerName $DnsServer & $Log "DNS A record added: $HostLabel.$Zone -> $TargetIp" } function Invoke-Replication { param( [string[]]$Servers, [string]$Command, [bool]$UseRemote, [pscredential]$Credential, [string]$SourceDc, [scriptblock]$Log ) if ([string]::IsNullOrWhiteSpace($Command)) { return } $targets = @($Servers | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($targets.Count -eq 0) { & $Log "Replication skipped: no target servers." return } if ([string]::IsNullOrWhiteSpace($SourceDc)) { & $Log "Replication skipped: source DC is empty." return } $session = $null try { if ($UseRemote) { try { if ($Credential) { $session = New-PSSession -ComputerName $SourceDc -Credential $Credential -ErrorAction Stop } else { $session = New-PSSession -ComputerName $SourceDc -ErrorAction Stop } & $Log "Replication session opened: $SourceDc" } catch { & $Log ("Replication session error on {0}: {1}" -f $SourceDc, $_.Exception.Message) return } } foreach ($server in $targets) { $usesToken = $Command -match "\{server\}|\{dest\}" $cmd = $Command.Replace("{server}", $server).Replace("{dest}", $server) $cmd = $cmd.Trim() if ($cmd -match "^(?i)\s*/repadmin\b") { $cmd = $cmd -replace "^(?i)\s*/repadmin\b", "repadmin" } if ($UseRemote) { $taskName = ("OneShot_AD_DNS_Repl_{0}" -f ($server -replace "[^A-Za-z0-9_-]", "_")) $outFile = ("C:\Windows\Temp\repadmin-{0}.txt" -f $server) $repadminCmd = $cmd $repadminCmd = [regex]::Replace($repadminCmd, "\s{2,}", " ").Trim() if ([string]::IsNullOrWhiteSpace($repadminCmd)) { & $Log "Replication skipped: empty command for $server." continue } & $Log "Replication (scheduled task): $SourceDc -> $server" try { Invoke-Command -Session $session -ScriptBlock { param($DestDC, $TaskName, $OutFile, $RepadminCmd) $cmdLine = "cmd.exe /c $RepadminCmd > `"$OutFile`" 2>&1" schtasks /Create /F /TN $TaskName /RU SYSTEM /SC ONCE /ST 00:00 /TR $cmdLine | Out-Null schtasks /Run /TN $TaskName | Out-Null Start-Sleep 6 $output = if (Test-Path $OutFile) { Get-Content $OutFile } else { "No output file found" } schtasks /Delete /F /TN $TaskName | Out-Null Remove-Item $OutFile -Force -ErrorAction SilentlyContinue $output } -ArgumentList $server, $taskName, $outFile, $repadminCmd | ForEach-Object { & $Log $_ } } catch { & $Log ("Replication error on {0}: {1}" -f $server, $_.Exception.Message) } continue } if (-not $usesToken -and $server -and $cmd -match "(?i)\brepadmin\b" -and $cmd -match "(?i)\bsyncall\b") { if ($cmd -notmatch "(?i)\\bsyncall\\s+\\S+") { $cmd = $cmd -replace "(?i)\\bsyncall\\b", "syncall $server" } } if ([string]::IsNullOrWhiteSpace($cmd)) { continue } & $Log "Replication: $cmd" & $env:ComSpec /c $cmd | ForEach-Object { & $Log $_ } } } finally { if ($session) { try { Remove-PSSession -Session $session } catch {} } } } function Invoke-Wacs { param( [string]$WacsPath, [string[]]$HostFqdns, [string]$OutputType, [string]$OutputPath, [string]$PfxPassword, [string]$BaseUri, [string]$Validation, [string]$ValidationPort, [bool]$Verbose, [scriptblock]$Log ) $args = @("--target", "manual") foreach ($hostName in $HostFqdns) { if (-not [string]::IsNullOrWhiteSpace($hostName)) { $args += @("--host", $hostName) } } if ($OutputType -eq "PEM") { $args += @("--store", "pemfiles", "--pemfilespath", $OutputPath) } else { $args += @("--store", "pfxfile", "--pfxfilepath", $OutputPath) if (-not [string]::IsNullOrWhiteSpace($PfxPassword)) { $args += @("--pfxpassword", $PfxPassword) } } $args += @( "--baseuri", $BaseUri, "--validation", $Validation, "--validationport", $ValidationPort ) if ($Verbose) { $args += "--verbose" } & $Log "WACS: $WacsPath $($args -join ' ')" & $WacsPath @args } $form = New-Object System.Windows.Forms.Form $form.Text = "Certy - WACS Helper" $form.Size = [System.Drawing.Size]::new(1000, 860) $form.StartPosition = "CenterScreen" $form.AutoScaleMode = "Dpi" $colorBg = [System.Drawing.Color]::FromArgb(245, 246, 248) $colorPanel = [System.Drawing.Color]::FromArgb(255, 255, 255) $colorText = [System.Drawing.Color]::FromArgb(30, 37, 45) $colorMuted = [System.Drawing.Color]::FromArgb(90, 98, 110) $colorAccent = [System.Drawing.Color]::FromArgb(32, 46, 77) $colorAccentSoft = [System.Drawing.Color]::FromArgb(41, 58, 96) $colorBorder = [System.Drawing.Color]::FromArgb(220, 224, 230) $colorInput = [System.Drawing.Color]::FromArgb(255, 255, 255) $form.BackColor = $colorBg $sidebarWidth = 170 $sidebar = New-Object System.Windows.Forms.Panel $sidebar.Dock = "Left" $sidebar.Width = $sidebarWidth $sidebar.BackColor = $colorAccent $panel = New-Object System.Windows.Forms.Panel $panel.Dock = "Fill" $panel.AutoScroll = $true $panel.BackColor = $colorBg $form.Controls.Add($panel) $form.Controls.Add($sidebar) $font = New-Object System.Drawing.Font("Segoe UI", 9) $labelWidth = 200 $inputWidth = 720 $xLabel = 20 $xInput = 230 $y = 20 $rowHeight = 24 $gap = 8 $leftMargin = 20 $rightMargin = 20 $buttonWidth = 110 $buttonGap = 10 $actionButtonWidth = 130 $navTitle = New-Object System.Windows.Forms.Label $navTitle.Text = "CERTY" $navTitle.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 12) $navTitle.ForeColor = [System.Drawing.Color]::White $navTitle.Location = [System.Drawing.Point]::new(16, 16) $navTitle.Size = [System.Drawing.Size]::new(130, 24) $sidebar.Controls.Add($navTitle) $navSub = New-Object System.Windows.Forms.Label $navSub.Text = "Enterprise Console" $navSub.Font = New-Object System.Drawing.Font("Segoe UI", 8) $navSub.ForeColor = [System.Drawing.Color]::FromArgb(200, 214, 240) $navSub.Location = [System.Drawing.Point]::new(16, 38) $navSub.Size = [System.Drawing.Size]::new(140, 18) $sidebar.Controls.Add($navSub) $navItems = @("Input", "DNS", "ACME", "Run", "Logs") $navY = 80 foreach ($item in $navItems) { $navLabel = New-Object System.Windows.Forms.Label $navLabel.Text = $item $navLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 9) $navLabel.ForeColor = [System.Drawing.Color]::FromArgb(210, 220, 236) $navLabel.Location = [System.Drawing.Point]::new(16, $navY) $navLabel.Size = [System.Drawing.Size]::new(140, 20) $sidebar.Controls.Add($navLabel) $navY += 26 } function Style-ButtonPrimary { param([System.Windows.Forms.Button]$Button) $Button.BackColor = $colorAccent $Button.ForeColor = [System.Drawing.Color]::White $Button.FlatStyle = "Flat" $Button.FlatAppearance.BorderSize = 0 $Button.UseVisualStyleBackColor = $false } function Style-ButtonSecondary { param([System.Windows.Forms.Button]$Button) $Button.BackColor = $colorPanel $Button.ForeColor = $colorText $Button.FlatStyle = "Flat" $Button.FlatAppearance.BorderColor = $colorBorder $Button.FlatAppearance.BorderSize = 1 $Button.UseVisualStyleBackColor = $false } function Add-Label { param([string]$Text, [int]$X, [int]$Y, [int]$W, [int]$H) $label = New-Object System.Windows.Forms.Label $label.Text = $Text $label.Location = [System.Drawing.Point]::new($X, $Y) $label.Size = [System.Drawing.Size]::new($W, $H) $label.Font = $font $label.ForeColor = $colorMuted $panel.Controls.Add($label) return $label } function Add-TextBox { param([int]$X, [int]$Y, [int]$W, [int]$H, [bool]$Multiline = $false) $tb = New-Object System.Windows.Forms.TextBox $tb.Location = [System.Drawing.Point]::new($X, $Y) $tb.Size = [System.Drawing.Size]::new($W, $H) $tb.Font = $font $tb.BackColor = $colorInput $tb.ForeColor = $colorText $tb.BorderStyle = "FixedSingle" $tb.Multiline = $Multiline if ($Multiline) { $tb.ScrollBars = "Vertical" } $panel.Controls.Add($tb) return $tb } function Add-CheckBox { param([string]$Text, [int]$X, [int]$Y, [int]$W, [int]$H) $cb = New-Object System.Windows.Forms.CheckBox $cb.Text = $Text $cb.Location = [System.Drawing.Point]::new($X, $Y) $cb.Size = [System.Drawing.Size]::new($W, $H) $cb.Font = $font $cb.ForeColor = $colorText $panel.Controls.Add($cb) return $cb } function Add-SectionHeader { param([string]$Text) $sectionLabel = New-Object System.Windows.Forms.Label $sectionLabel.Text = $Text $sectionLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10) $sectionLabel.ForeColor = $colorText $sectionLabel.Location = [System.Drawing.Point]::new($xLabel, $script:y) $sectionLabel.Size = [System.Drawing.Size]::new(300, 20) $panel.Controls.Add($sectionLabel) $sectionLine = New-Object System.Windows.Forms.Panel $sectionLine.BackColor = $colorBorder $sectionLine.Location = [System.Drawing.Point]::new($xLabel, ($script:y + 22)) $sectionLine.Size = [System.Drawing.Size]::new(($inputWidth + ($xInput - $xLabel)), 1) $panel.Controls.Add($sectionLine) $script:sectionLines.Add($sectionLine) | Out-Null $script:y += 30 } $sectionLines = New-Object System.Collections.Generic.List[System.Windows.Forms.Panel] $header = New-Object System.Windows.Forms.Panel $header.Location = [System.Drawing.Point]::new($xLabel, $y) $header.Size = [System.Drawing.Size]::new(($inputWidth + ($xInput - $xLabel)), 70) $header.BackColor = $colorPanel $header.BorderStyle = "FixedSingle" $panel.Controls.Add($header) $headerTitle = New-Object System.Windows.Forms.Label $headerTitle.Text = "Certy Enterprise" $headerTitle.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 16) $headerTitle.ForeColor = $colorText $headerTitle.Location = [System.Drawing.Point]::new(12, 10) $headerTitle.Size = [System.Drawing.Size]::new(300, 28) $header.Controls.Add($headerTitle) $headerSub = New-Object System.Windows.Forms.Label $headerSub.Text = "WACS helper for DNS + ACME proxy workflows" $headerSub.Font = New-Object System.Drawing.Font("Segoe UI", 9) $headerSub.ForeColor = $colorMuted $headerSub.Location = [System.Drawing.Point]::new(12, 38) $headerSub.Size = [System.Drawing.Size]::new(600, 20) $header.Controls.Add($headerSub) $y = $header.Bottom + 16 Add-SectionHeader "Input" Add-Label "Hostnames (one per line)" $xLabel $y $labelWidth $rowHeight $hostsBox = Add-TextBox $xInput $y $inputWidth 100 $true $y += 100 + $gap Add-Label "Hostnames file (optional)" $xLabel $y $labelWidth $rowHeight $fileBox = Add-TextBox $xInput $y ($inputWidth - ($buttonWidth + $buttonGap)) $rowHeight $false $browseBtn = New-Object System.Windows.Forms.Button $browseBtn.Text = "Browse" $browseBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1)) $browseBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($browseBtn) Style-ButtonSecondary $browseBtn $y += $rowHeight + $gap Add-Label "File preview (first 200 lines)" $xLabel $y $labelWidth $rowHeight $filePreviewBox = Add-TextBox $xInput $y ($inputWidth - ($buttonWidth + $buttonGap)) 80 $true $filePreviewBox.ReadOnly = $true $filePreviewBtn = New-Object System.Windows.Forms.Button $filePreviewBtn.Text = "Preview" $filePreviewBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1)) $filePreviewBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($filePreviewBtn) Style-ButtonSecondary $filePreviewBtn $y += 82 + $gap $csrLabel = Add-Label "CSR folder (optional)" $xLabel $y $labelWidth $rowHeight $csrFolderBox = Add-TextBox $xInput $y ($inputWidth - ((2 * $buttonWidth) + $buttonGap)) $rowHeight $false $csrBrowseBtn = New-Object System.Windows.Forms.Button $csrBrowseBtn.Text = "Browse" $csrBrowseBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - (2 * $buttonWidth + $buttonGap)), ($y - 1)) $csrBrowseBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($csrBrowseBtn) Style-ButtonSecondary $csrBrowseBtn $csrImportBtn = New-Object System.Windows.Forms.Button $csrImportBtn.Text = "Import CSR" $csrImportBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1)) $csrImportBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($csrImportBtn) Style-ButtonSecondary $csrImportBtn $y += $rowHeight + $gap $useFqdnBox = Add-CheckBox "Input contains FQDNs (otherwise default zone is appended)" $xInput $y $inputWidth $rowHeight $y += $rowHeight + $gap Add-SectionHeader "DNS & Replication" Add-Label "Default DNS zone" $xLabel $y $labelWidth $rowHeight $zoneBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false $zoneBox.Text = "record.domain.govt.nz" $y += $rowHeight + $gap $replicationEnabledBox = Add-CheckBox "Enable DNS replication" $xInput $y 200 $rowHeight $replicationEnabledBox.Checked = $true $y += $rowHeight + $gap Add-Label "Target IPv4 for A records" $xLabel $y $labelWidth $rowHeight $ipBox = Add-TextBox $xInput $y ($inputWidth - ($buttonWidth + $buttonGap)) $rowHeight $false $ipBox.Text = Get-LocalIpv4 $ipRefreshBtn = New-Object System.Windows.Forms.Button $ipRefreshBtn.Text = "Use Local" $ipRefreshBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1)) $ipRefreshBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($ipRefreshBtn) Style-ButtonSecondary $ipRefreshBtn $y += $rowHeight + $gap Add-Label "Primary DNS server (source DC)" $xLabel $y $labelWidth $rowHeight $dnsServerBox = New-Object System.Windows.Forms.ComboBox $dnsServerBox.Location = [System.Drawing.Point]::new($xInput, $y) $dnsServerBox.Size = [System.Drawing.Size]::new(($inputWidth - ($buttonWidth + $buttonGap)), $rowHeight) $dnsServerBox.DropDownStyle = "DropDown" $dnsServerBox.Text = "DC01.example.local" $dnsServerBox.FlatStyle = "Flat" $dnsServerBox.BackColor = $colorInput $dnsServerBox.ForeColor = $colorText $panel.Controls.Add($dnsServerBox) $dnsScanBtn = New-Object System.Windows.Forms.Button $dnsScanBtn.Text = "Scan" $dnsScanBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1)) $dnsScanBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($dnsScanBtn) Style-ButtonSecondary $dnsScanBtn $y += $rowHeight + $gap Add-Label "DNS servers (select for replication)" $xLabel $y $labelWidth $rowHeight $dnsListBox = New-Object System.Windows.Forms.ListBox $dnsListBox.Location = [System.Drawing.Point]::new($xInput, $y) $dnsListBox.Size = [System.Drawing.Size]::new($inputWidth, 80) $dnsListBox.SelectionMode = "MultiExtended" $dnsListBox.BackColor = $colorInput $dnsListBox.ForeColor = $colorText $dnsListBox.BorderStyle = "FixedSingle" $panel.Controls.Add($dnsListBox) $y += 82 + $gap Add-Label "Replication targets (one per line)" $xLabel $y $labelWidth $rowHeight $replicationTargetsBox = Add-TextBox $xInput $y ($inputWidth - ((2 * $buttonWidth) + $buttonGap)) 70 $true $replicationFromSelectedBtn = New-Object System.Windows.Forms.Button $replicationFromSelectedBtn.Text = "Use Selected" $replicationFromSelectedBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - (2 * $buttonWidth + $buttonGap)), ($y - 1)) $replicationFromSelectedBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($replicationFromSelectedBtn) Style-ButtonSecondary $replicationFromSelectedBtn $primaryFromSelectedBtn = New-Object System.Windows.Forms.Button $primaryFromSelectedBtn.Text = "Use Primary" $primaryFromSelectedBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1)) $primaryFromSelectedBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($primaryFromSelectedBtn) Style-ButtonSecondary $primaryFromSelectedBtn $y += 70 + $gap Add-Label "Replication command ({dest} optional)" $xLabel $y $labelWidth $rowHeight $replicationCmdBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false $replicationCmdBox.Text = "repadmin /syncall {dest} /AdeP" $y += $rowHeight + $gap Add-Label "Replication wait (seconds)" $xLabel $y $labelWidth $rowHeight $replicationDelayBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false $replicationDelayBox.Text = "30" $y += $rowHeight + $gap $replicationRemoteBox = Add-CheckBox "Run replication via scheduled task on source DC" $xInput $y $inputWidth $rowHeight $replicationRemoteBox.Checked = $true $y += $rowHeight + $gap $replicationCredBtn = New-Object System.Windows.Forms.Button $replicationCredBtn.Text = "Set Replication Credentials" $replicationCredBtn.Location = [System.Drawing.Point]::new($xInput, $y) $replicationCredBtn.Size = [System.Drawing.Size]::new(220, 26) $panel.Controls.Add($replicationCredBtn) Style-ButtonSecondary $replicationCredBtn $y += $rowHeight + $gap $y += $gap Add-SectionHeader "ACME / Output" Add-Label "WACS path" $xLabel $y $labelWidth $rowHeight $wacsPathBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false $wacsPathBox.Text = "C:\ProgramData\Wacs\wacs.exe" $y += $rowHeight + $gap Add-Label "Output type" $xLabel $y $labelWidth $rowHeight $outputTypeBox = New-Object System.Windows.Forms.ComboBox $outputTypeBox.Location = [System.Drawing.Point]::new($xInput, $y) $outputTypeBox.Size = [System.Drawing.Size]::new($inputWidth, $rowHeight) $outputTypeBox.DropDownStyle = "DropDownList" $outputTypeBox.FlatStyle = "Flat" $outputTypeBox.BackColor = $colorInput $outputTypeBox.ForeColor = $colorText [void]$outputTypeBox.Items.Add("PFX") [void]$outputTypeBox.Items.Add("PEM") $outputTypeBox.SelectedIndex = 0 $panel.Controls.Add($outputTypeBox) $y += $rowHeight + $gap $outputPathLabel = Add-Label "PFX output path" $xLabel $y $labelWidth $rowHeight $outputPathBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false $outputPathBox.Text = "C:\programdata\wacs\output\" $y += $rowHeight + $gap $pfxPasswordLabel = Add-Label "PFX password" $xLabel $y $labelWidth $rowHeight $pfxPasswordBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false $pfxPasswordBox.UseSystemPasswordChar = $true $y += $rowHeight + $gap Add-Label "ACME base URI" $xLabel $y $labelWidth $rowHeight $baseUriBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false $baseUriBox.Text = "https://acmeprod.wd.govt.nz:9999/acme/rsa/" $y += $rowHeight + $gap Add-Label "Validation method" $xLabel $y $labelWidth $rowHeight $validationBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false $validationBox.Text = "selfhosting" $y += $rowHeight + $gap Add-Label "Validation port" $xLabel $y $labelWidth $rowHeight $validationPortBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false $validationPortBox.Text = "9998" $y += $rowHeight + $gap Add-SectionHeader "Run" $verboseBox = Add-CheckBox "Verbose" $xInput $y 120 $rowHeight $runWacsBox = Add-CheckBox "Run WACS after DNS update" ($xInput + 140) $y 260 $rowHeight $perHostBox = Add-CheckBox "One cert per host" ($xInput + 430) $y 180 $rowHeight $runWacsBox.Checked = $true $y += $rowHeight + $gap $disableCertsBox = Add-CheckBox "Turn off cert generation (DNS-only mode)" $xInput $y 360 $rowHeight $y += $rowHeight + ($gap * 2) $runBtn = New-Object System.Windows.Forms.Button $runBtn.Text = "Run" $runBtn.Location = [System.Drawing.Point]::new($xInput, $y) $runBtn.Size = [System.Drawing.Size]::new(120, 30) $panel.Controls.Add($runBtn) Style-ButtonPrimary $runBtn $clearBtn = New-Object System.Windows.Forms.Button $clearBtn.Text = "Clear Log" $clearBtn.Location = [System.Drawing.Point]::new(($xInput + 140), $y) $clearBtn.Size = [System.Drawing.Size]::new($actionButtonWidth, 30) $panel.Controls.Add($clearBtn) Style-ButtonSecondary $clearBtn $saveDefaultsBtn = New-Object System.Windows.Forms.Button $saveDefaultsBtn.Text = "Save Defaults" $saveDefaultsBtn.Location = [System.Drawing.Point]::new(($xInput + 140 + $actionButtonWidth + $buttonGap), $y) $saveDefaultsBtn.Size = [System.Drawing.Size]::new($actionButtonWidth, 30) $panel.Controls.Add($saveDefaultsBtn) Style-ButtonSecondary $saveDefaultsBtn $y += 40 Add-SectionHeader "Activity" Add-Label "Activity log" $xLabel $y $labelWidth $rowHeight $logBox = Add-TextBox $xInput $y $inputWidth 200 $true $logBox.ReadOnly = $true $logAction = { param([string]$Message) $timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") $logBox.AppendText("[$timestamp] $Message`r`n") } function Update-OutputTypeUI { if ($outputTypeBox.SelectedItem -eq "PEM") { $outputPathLabel.Text = "PEM output path" $pfxPasswordLabel.Visible = $false $pfxPasswordBox.Visible = $false $pfxPasswordBox.Text = "" } else { $outputPathLabel.Text = "PFX output path" $pfxPasswordLabel.Visible = $true $pfxPasswordBox.Visible = $true } } function Update-ReplicationUI { $enabled = $replicationEnabledBox.Checked $replicationTargetsBox.Enabled = $enabled $replicationFromSelectedBtn.Enabled = $enabled $primaryFromSelectedBtn.Enabled = $enabled $replicationCmdBox.Enabled = $enabled $replicationDelayBox.Enabled = $enabled $replicationRemoteBox.Enabled = $enabled $replicationCredBtn.Enabled = $enabled $dnsListBox.Enabled = $enabled } function Update-CertGenerationUI { $disabled = $disableCertsBox.Checked $runWacsBox.Enabled = -not $disabled $perHostBox.Enabled = -not $disabled if ($disabled) { $runWacsBox.Checked = $false } $wacsPathBox.Enabled = -not $disabled $outputTypeBox.Enabled = -not $disabled $outputPathBox.Enabled = -not $disabled $pfxPasswordBox.Enabled = -not $disabled $baseUriBox.Enabled = -not $disabled $validationBox.Enabled = -not $disabled $validationPortBox.Enabled = -not $disabled } function Update-ZoneFromHostInput { $hostList = @(Split-List $hostsBox.Text) if ($hostList.Count -eq 0) { return } $zoneGuess = Get-CommonZoneFromHosts -Hosts $hostList if (-not [string]::IsNullOrWhiteSpace($zoneGuess) -and $zoneBox.Text -ne $zoneGuess) { $zoneBox.Text = $zoneGuess & $logAction "Default DNS zone set to $zoneGuess (from hostnames)." } } $loadedDefaults = Load-Defaults if ($loadedDefaults) { $value = Get-DefaultValue -Defaults $loadedDefaults -Name "DefaultZone" if (-not [string]::IsNullOrWhiteSpace($value)) { $zoneBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "TargetIp" if (-not [string]::IsNullOrWhiteSpace($value)) { $ipBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "DnsServer" if (-not [string]::IsNullOrWhiteSpace($value)) { $dnsServerBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "ReplicationTargets" if (-not [string]::IsNullOrWhiteSpace($value)) { $replicationTargetsBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "ReplicationCommand" if (-not [string]::IsNullOrWhiteSpace($value)) { $replicationCmdBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "ReplicationEnabled" if ($null -ne $value) { $replicationEnabledBox.Checked = [bool]$value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "ReplicationDelaySeconds" if ($null -ne $value) { $replicationDelayBox.Text = $value.ToString() } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "ReplicationRemote" if ($null -ne $value) { $replicationRemoteBox.Checked = [bool]$value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "WacsPath" if (-not [string]::IsNullOrWhiteSpace($value)) { $wacsPathBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "OutputPath" if (-not [string]::IsNullOrWhiteSpace($value)) { $outputPathBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "PfxPassword" if (-not [string]::IsNullOrWhiteSpace($value)) { $pfxPasswordBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "BaseUri" if (-not [string]::IsNullOrWhiteSpace($value)) { $baseUriBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "Validation" if (-not [string]::IsNullOrWhiteSpace($value)) { $validationBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "ValidationPort" if (-not [string]::IsNullOrWhiteSpace($value)) { $validationPortBox.Text = $value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "UseProvidedFqdn" if ($null -ne $value) { $useFqdnBox.Checked = [bool]$value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "RunWacs" if ($null -ne $value) { $runWacsBox.Checked = [bool]$value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "Verbose" if ($null -ne $value) { $verboseBox.Checked = [bool]$value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "PerHostCerts" if ($null -ne $value) { $perHostBox.Checked = [bool]$value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "DisableCertGeneration" if ($null -ne $value) { $disableCertsBox.Checked = [bool]$value } $value = Get-DefaultValue -Defaults $loadedDefaults -Name "OutputType" if (-not [string]::IsNullOrWhiteSpace($value)) { $outputTypeBox.SelectedItem = $value } if (-not $outputTypeBox.SelectedItem) { $outputTypeBox.SelectedIndex = 0 } Update-OutputTypeUI if (Test-Path function:Update-ReplicationUI) { Update-ReplicationUI } Update-CertGenerationUI & $logAction "Defaults loaded from $(Get-DefaultsPath)." } function Apply-Layout { if ($panel.ClientSize.Width -le 0) { return } $contentWidth = $panel.ClientSize.Width - $leftMargin - $rightMargin $inputWidthCalc = [Math]::Max(420, ($contentWidth - ($xInput - $xLabel))) $script:inputWidth = $inputWidthCalc $header.Width = $contentWidth foreach ($line in $sectionLines) { $line.Width = $contentWidth } $hostsBox.Width = $inputWidthCalc $fileBox.Width = $inputWidthCalc - ($buttonWidth + $buttonGap) $browseBtn.Left = $xInput + $inputWidthCalc - $buttonWidth $filePreviewBox.Width = $inputWidthCalc - ($buttonWidth + $buttonGap) $filePreviewBtn.Left = $xInput + $inputWidthCalc - $buttonWidth $csrFolderBox.Width = $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap) $csrBrowseBtn.Left = $xInput + $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap) $csrImportBtn.Left = $xInput + $inputWidthCalc - $buttonWidth $zoneBox.Width = $inputWidthCalc $ipBox.Width = $inputWidthCalc - ($buttonWidth + $buttonGap) $ipRefreshBtn.Left = $xInput + $inputWidthCalc - $buttonWidth $dnsServerBox.Width = $inputWidthCalc - ($buttonWidth + $buttonGap) $dnsScanBtn.Left = $xInput + $inputWidthCalc - $buttonWidth $dnsListBox.Width = $inputWidthCalc $replicationTargetsBox.Width = $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap) $replicationFromSelectedBtn.Left = $xInput + $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap) $primaryFromSelectedBtn.Left = $xInput + $inputWidthCalc - $buttonWidth $replicationCmdBox.Width = $inputWidthCalc $replicationDelayBox.Width = $inputWidthCalc $replicationRemoteBox.Width = $inputWidthCalc $replicationCredBtn.Width = [Math]::Min($inputWidthCalc, 260) $wacsPathBox.Width = $inputWidthCalc $outputTypeBox.Width = $inputWidthCalc $outputPathBox.Width = $inputWidthCalc $pfxPasswordBox.Width = $inputWidthCalc $baseUriBox.Width = $inputWidthCalc $validationBox.Width = $inputWidthCalc $validationPortBox.Width = $inputWidthCalc $logBox.Width = $inputWidthCalc $clearBtn.Left = $xInput + 140 $saveDefaultsBtn.Left = $clearBtn.Left + $clearBtn.Width + $buttonGap } $browseBtn.Add_Click({ $dialog = New-Object System.Windows.Forms.OpenFileDialog $dialog.Filter = "Text/CSV files (*.txt;*.csv)|*.txt;*.csv|All files (*.*)|*.*" $dialog.Multiselect = $false if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $fileBox.Text = $dialog.FileName $filePreviewBox.Text = Get-FilePreview -Path $dialog.FileName $fileHosts = Get-Content -Path $dialog.FileName | ForEach-Object { $_.Trim() } | Where-Object { $_ } $zoneGuess = Get-CommonZoneFromHosts -Hosts $fileHosts if ($zoneGuess) { $zoneBox.Text = $zoneGuess & $logAction "Default DNS zone set to $zoneGuess (from file)." } } }) $replicationEnabledBox.Add_CheckedChanged({ if (Test-Path function:Update-ReplicationUI) { Update-ReplicationUI } }) $filePreviewBtn.Add_Click({ $path = $fileBox.Text.Trim() if ([string]::IsNullOrWhiteSpace($path)) { $previewHosts = @(Split-List $hostsBox.Text) if ($previewHosts.Count -gt 0) { $filePreviewBox.Text = ($previewHosts | Select-Object -First 200) -join [Environment]::NewLine return } $filePreviewBox.Text = "" & $logAction "No file selected and no hostnames to preview." return } $filePreviewBox.Text = Get-FilePreview -Path $path if (Test-Path -Path $path -PathType Leaf) { $fileHosts = Get-Content -Path $path | ForEach-Object { $_.Trim() } | Where-Object { $_ } $zoneGuess = Get-CommonZoneFromHosts -Hosts $fileHosts if ($zoneGuess) { $zoneBox.Text = $zoneGuess & $logAction "Default DNS zone set to $zoneGuess (from file)." } } }) $csrBrowseBtn.Add_Click({ $dialog = New-Object System.Windows.Forms.FolderBrowserDialog if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $csrFolderBox.Text = $dialog.SelectedPath } }) $csrImportBtn.Add_Click({ try { $folder = $csrFolderBox.Text.Trim() if (-not $folder) { throw "CSR folder is empty." } if (-not (Test-Path -Path $folder -PathType Container)) { throw "CSR folder not found: $folder" } $csrFiles = Get-ChildItem -Path $folder -Include *.csr, *.pem -File -Recurse if (-not $csrFiles) { & $logAction "No CSR files found in $folder" return } $csrHosts = @($csrFiles | ForEach-Object { $_.BaseName.Trim() } | Where-Object { $_ }) $currentHosts = Split-List $hostsBox.Text $merged = Merge-Hostnames -Existing $currentHosts -NewItems $csrHosts $hostsBox.Text = ($merged -join [Environment]::NewLine) & $logAction "Imported $($csrHosts.Count) CSR hostname(s)." } catch { & $logAction "Error: $($_.Exception.Message)" } }) $dnsScanBtn.Add_Click({ try { $dnsListBox.Items.Clear() $servers = @(Get-DnsServerCandidates) if (-not $servers -or $servers.Count -eq 0) { & $logAction "No DNS servers found. Enter servers manually." return } foreach ($server in $servers) { [void]$dnsListBox.Items.Add($server) } $dnsServerBox.Items.Clear() foreach ($server in $servers) { [void]$dnsServerBox.Items.Add($server) } if ($dnsServerBox.Items.Count -gt 0) { $dnsServerBox.SelectedIndex = 0 } & $logAction "Loaded $($servers.Count) DNS server(s)." } catch { & $logAction "Error: $($_.Exception.Message)" } }) $ipRefreshBtn.Add_Click({ $ipBox.Text = Get-LocalIpv4 }) $replicationFromSelectedBtn.Add_Click({ $selected = @($dnsListBox.SelectedItems | ForEach-Object { $_.ToString() }) if ($selected.Count -eq 0) { & $logAction "No DNS servers selected." return } $replicationTargetsBox.Text = ($selected -join [Environment]::NewLine) & $logAction "Replication targets set from selected DNS servers." }) $primaryFromSelectedBtn.Add_Click({ $selected = @($dnsListBox.SelectedItems | ForEach-Object { $_.ToString() }) if ($selected.Count -eq 0) { & $logAction "No DNS servers selected." return } $dnsServerBox.Text = $selected[0] & $logAction "Primary DNS server set to $($selected[0])." }) $outputTypeBox.Add_SelectedIndexChanged({ Update-OutputTypeUI }) $replicationCredBtn.Add_Click({ $cred = Get-Credential -Message "Enter credentials for replication sessions." if (-not $cred) { & $logAction "Replication credentials not set." return } Save-ReplicationCredential -Credential $cred & $logAction "Replication credentials saved for this user." }) $hostsBox.Add_TextChanged({ Update-ZoneFromHostInput }) $disableCertsBox.Add_CheckedChanged({ Update-CertGenerationUI }) $saveDefaultsBtn.Add_Click({ $defaults = [pscustomobject]@{ DefaultZone = $zoneBox.Text TargetIp = $ipBox.Text DnsServer = $dnsServerBox.Text ReplicationTargets = $replicationTargetsBox.Text ReplicationCommand = $replicationCmdBox.Text ReplicationEnabled = $replicationEnabledBox.Checked ReplicationDelaySeconds = $replicationDelayBox.Text ReplicationRemote = $replicationRemoteBox.Checked WacsPath = $wacsPathBox.Text OutputType = $outputTypeBox.SelectedItem.ToString() OutputPath = $outputPathBox.Text PfxPassword = $pfxPasswordBox.Text BaseUri = $baseUriBox.Text Validation = $validationBox.Text ValidationPort = $validationPortBox.Text UseProvidedFqdn = $useFqdnBox.Checked RunWacs = $runWacsBox.Checked Verbose = $verboseBox.Checked PerHostCerts = $perHostBox.Checked DisableCertGeneration = $disableCertsBox.Checked } Save-Defaults -Defaults $defaults & $logAction "Defaults saved to $(Get-DefaultsPath)." if (-not [string]::IsNullOrWhiteSpace($pfxPasswordBox.Text)) { & $logAction "Warning: PFX password is stored in plaintext." } }) $clearBtn.Add_Click({ $logBox.Clear() }) $runBtn.Add_Click({ $runBtn.Enabled = $false try { $hosts = @() $hosts += Split-List $hostsBox.Text if (-not [string]::IsNullOrWhiteSpace($fileBox.Text)) { if (-not (Test-Path -Path $fileBox.Text -PathType Leaf)) { throw "Hostnames file not found: $($fileBox.Text)" } $fileHosts = Get-Content -Path $fileBox.Text | ForEach-Object { $_.Trim() } | Where-Object { $_ } $hosts += $fileHosts } if ($hosts.Count -eq 0) { throw "No hostnames provided." } $zone = $zoneBox.Text.Trim() if (-not $zone) { throw "Default DNS zone is required." } $targetIp = $ipBox.Text.Trim() if (-not $targetIp) { throw "Target IPv4 is required." } $selectedReplicationTargets = @($dnsListBox.SelectedItems | ForEach-Object { $_.ToString() }) $dnsServer = $dnsServerBox.Text.Trim() if (-not $dnsServer -and $selectedReplicationTargets.Count -gt 0) { $dnsServer = $selectedReplicationTargets[0] } if (-not $dnsServer) { throw "Primary DNS server is required." } $outputType = $outputTypeBox.SelectedItem.ToString() $outputPath = $outputPathBox.Text.Trim() if (-not $outputPath) { throw "Output path is required." } if ($outputType -eq "PFX" -and [string]::IsNullOrWhiteSpace($pfxPasswordBox.Text)) { throw "PFX password is required." } $hostEntries = @($hosts | ForEach-Object { Resolve-HostEntry -Name $_ -Zone $zone -UseProvidedFqdn $useFqdnBox.Checked } | Where-Object { $_ }) & $logAction "Processing $($hostEntries.Count) hostname(s)." foreach ($entry in $hostEntries) { Ensure-ARecord -Zone $zone -HostLabel $entry.HostLabel -TargetIp $targetIp -DnsServer $dnsServer -Log $logAction } if ($replicationEnabledBox.Checked) { $replicationDelaySeconds = 0 $delayRaw = $replicationDelayBox.Text.Trim() if (-not [string]::IsNullOrWhiteSpace($delayRaw)) { if (-not [int]::TryParse($delayRaw, [ref]$replicationDelaySeconds) -or $replicationDelaySeconds -lt 0) { throw "Replication wait seconds must be a non-negative integer." } } if ($selectedReplicationTargets.Count -gt 0) { $replicationTargets = $selectedReplicationTargets } else { $replicationTargets = @(Split-List $replicationTargetsBox.Text) if ($replicationTargets.Count -eq 0 -and $dnsServer) { $replicationTargets = @($dnsServer) & $logAction "Replication targets empty; using primary DNS server $dnsServer." } } $replicationCredential = $null if ($replicationRemoteBox.Checked) { $replicationCredential = Load-ReplicationCredential if (-not $replicationCredential) { $replicationCredential = Get-Credential -Message "Enter credentials for replication targets." if (-not $replicationCredential) { & $logAction "Replication canceled: credentials not provided." return } Save-ReplicationCredential -Credential $replicationCredential & $logAction "Replication credentials saved for this user." } } Invoke-Replication ` -Servers $replicationTargets ` -Command $replicationCmdBox.Text ` -UseRemote $replicationRemoteBox.Checked ` -Credential $replicationCredential ` -SourceDc $dnsServer ` -Log $logAction if ($replicationDelaySeconds -gt 0) { & $logAction "Waiting $replicationDelaySeconds seconds for replication." Start-Sleep -Seconds $replicationDelaySeconds } } else { & $logAction "Replication disabled." } if ($disableCertsBox.Checked) { & $logAction "Cert generation disabled; DNS updates/replication only." } elseif ($runWacsBox.Checked) { $wacsPath = $wacsPathBox.Text.Trim() if (-not (Test-Path -Path $wacsPath -PathType Leaf)) { throw "WACS not found at: $wacsPath" } if ($perHostBox.Checked) { foreach ($entry in $hostEntries) { & $logAction "Requesting certificate for $($entry.Fqdn)." Invoke-Wacs ` -WacsPath $wacsPath ` -HostFqdns @($entry.Fqdn) ` -OutputType $outputType ` -OutputPath $outputPath ` -PfxPassword $pfxPasswordBox.Text ` -BaseUri $baseUriBox.Text.Trim() ` -Validation $validationBox.Text.Trim() ` -ValidationPort $validationPortBox.Text.Trim() ` -Verbose $verboseBox.Checked ` -Log $logAction } } else { $hostList = @($hostEntries | ForEach-Object { $_.Fqdn } | Where-Object { $_ }) & $logAction "Requesting one certificate with $($hostList.Count) hostname(s)." Invoke-Wacs ` -WacsPath $wacsPath ` -HostFqdns $hostList ` -OutputType $outputType ` -OutputPath $outputPath ` -PfxPassword $pfxPasswordBox.Text ` -BaseUri $baseUriBox.Text.Trim() ` -Validation $validationBox.Text.Trim() ` -ValidationPort $validationPortBox.Text.Trim() ` -Verbose $verboseBox.Checked ` -Log $logAction } } & $logAction "Done." } catch { & $logAction "Error: $($_.Exception.Message)" } finally { $runBtn.Enabled = $true } }) [void]$form.Add_Load({ try { Apply-Layout } catch {} }) [void]$form.Add_Shown({ try { $form.BeginInvoke([Action]{ try { Apply-Layout } catch {} }) } catch {} }) [void]$panel.Add_SizeChanged({ try { Apply-Layout } catch {} }) [void]$form.Add_Resize({ try { Apply-Layout } catch {} }) [void]$form.ShowDialog()