Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" Add-Type -AssemblyName System.Windows.Forms Add-Type -AssemblyName System.Drawing function New-RoundedRectPath { param( [System.Drawing.RectangleF]$Rect, [float]$Radius ) $path = New-Object System.Drawing.Drawing2D.GraphicsPath $diameter = $Radius * 2 $path.AddArc($Rect.X, $Rect.Y, $diameter, $diameter, 180, 90) | Out-Null $path.AddArc($Rect.Right - $diameter, $Rect.Y, $diameter, $diameter, 270, 90) | Out-Null $path.AddArc($Rect.Right - $diameter, $Rect.Bottom - $diameter, $diameter, $diameter, 0, 90) | Out-Null $path.AddArc($Rect.X, $Rect.Bottom - $diameter, $diameter, $diameter, 90, 90) | Out-Null $path.CloseFigure() return $path } function New-CertyLogoBitmap { param([int]$Size = 64) $bmp = New-Object System.Drawing.Bitmap($Size, $Size) $g = [System.Drawing.Graphics]::FromImage($bmp) $g.SmoothingMode = "AntiAlias" $g.Clear([System.Drawing.Color]::Transparent) $accent = [System.Drawing.Color]::FromArgb(32, 46, 77) $accentSoft = [System.Drawing.Color]::FromArgb(41, 58, 96) $paper = [System.Drawing.Color]::FromArgb(255, 255, 255) $border = [System.Drawing.Color]::FromArgb(210, 214, 222) $pad = [Math]::Floor($Size * 0.14) $rect = New-Object System.Drawing.RectangleF($pad, $pad, ($Size - ($pad * 2)), ($Size - ($pad * 2))) $radius = [Math]::Max(4, [Math]::Floor($Size * 0.1)) $path = New-RoundedRectPath -Rect $rect -Radius $radius $g.FillPath((New-Object System.Drawing.SolidBrush($paper)), $path) $g.DrawPath((New-Object System.Drawing.Pen($border, 1.5)), $path) $sealSize = [Math]::Floor($Size * 0.26) $sealX = $rect.Right - $sealSize - ($pad * 0.2) $sealY = $rect.Bottom - $sealSize - ($pad * 0.2) $sealRect = New-Object System.Drawing.RectangleF($sealX, $sealY, $sealSize, $sealSize) $g.FillEllipse((New-Object System.Drawing.SolidBrush($accent)), $sealRect) $tri1 = @( [System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.18), $sealRect.Bottom + 2), [System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.42), $sealRect.Bottom + 2), [System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.30), $sealRect.Bottom + ($sealSize * 0.28)) ) $tri2 = @( [System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.58), $sealRect.Bottom + 2), [System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.82), $sealRect.Bottom + 2), [System.Drawing.PointF]::new($sealRect.X + ($sealSize * 0.70), $sealRect.Bottom + ($sealSize * 0.28)) ) $g.FillPolygon((New-Object System.Drawing.SolidBrush($accentSoft)), $tri1) $g.FillPolygon((New-Object System.Drawing.SolidBrush($accentSoft)), $tri2) $fontSize = [Math]::Floor($Size * 0.36) $font = New-Object System.Drawing.Font("Segoe UI Semibold", $fontSize, [System.Drawing.FontStyle]::Regular, [System.Drawing.GraphicsUnit]::Pixel) $format = New-Object System.Drawing.StringFormat $format.Alignment = "Center" $format.LineAlignment = "Center" $textRect = New-Object System.Drawing.RectangleF($rect.X, $rect.Y, $rect.Width, $rect.Height) $g.DrawString("C", $font, (New-Object System.Drawing.SolidBrush($accent)), $textRect, $format) $g.Dispose() return $bmp } 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(163, 163, 163) $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 $logoSmall = New-CertyLogoBitmap -Size 32 $logoLarge = New-CertyLogoBitmap -Size 64 $form.Icon = [System.Drawing.Icon]::FromHandle($logoSmall.GetHicon()) $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) $logoBox = New-Object System.Windows.Forms.PictureBox $logoBox.Image = $logoLarge $logoBox.SizeMode = "Zoom" $logoBox.Location = [System.Drawing.Point]::new(12, 12) $logoBox.Size = [System.Drawing.Size]::new(40, 40) $header.Controls.Add($logoBox) $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(60, 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(60, 38) $headerSub.Size = [System.Drawing.Size]::new(600, 20) $header.Controls.Add($headerSub) $helpBtn = New-Object System.Windows.Forms.Button $helpBtn.Text = "How to use me" $helpBtn.Size = [System.Drawing.Size]::new(140, 28) $helpBtn.Location = [System.Drawing.Point]::new(($header.Width - 152), 20) $header.Controls.Add($helpBtn) Style-ButtonSecondary $helpBtn $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 = "1) 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 = "2) 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 $perHostBox = Add-CheckBox "One cert per host" ($xInput + 430) $y 180 $rowHeight $disableCertsBox = Add-CheckBox "Turn off cert generation (DNS-only mode)" ($xInput + 140) $y 360 $rowHeight $y += $rowHeight + ($gap * 2) $runBtn = New-Object System.Windows.Forms.Button $runBtn.Text = "5) 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 Show-HelpDialog { $helpForm = New-Object System.Windows.Forms.Form $helpForm.Text = "How it works" $helpForm.Size = [System.Drawing.Size]::new(760, 560) $helpForm.StartPosition = "CenterParent" $helpForm.BackColor = $colorBg $panelHelp = New-Object System.Windows.Forms.Panel $panelHelp.Dock = "Fill" $panelHelp.BackColor = $colorBg $panelHelp.Padding = New-Object System.Windows.Forms.Padding(16) $helpForm.Controls.Add($panelHelp) $headerPanel = New-Object System.Windows.Forms.Panel $headerPanel.BackColor = $colorPanel $headerPanel.BorderStyle = "FixedSingle" $headerPanel.Size = [System.Drawing.Size]::new(700, 70) $headerPanel.Location = [System.Drawing.Point]::new(0, 0) $panelHelp.Controls.Add($headerPanel) $title = New-Object System.Windows.Forms.Label $title.Text = "Certy - How it works" $title.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14) $title.ForeColor = $colorText $title.Location = [System.Drawing.Point]::new(12, 12) $title.Size = [System.Drawing.Size]::new(500, 24) $headerPanel.Controls.Add($title) $subtitle = New-Object System.Windows.Forms.Label $subtitle.Text = "Follow these steps to create DNS records and issue certificates." $subtitle.Font = New-Object System.Drawing.Font("Segoe UI", 9) $subtitle.ForeColor = $colorMuted $subtitle.Location = [System.Drawing.Point]::new(12, 38) $subtitle.Size = [System.Drawing.Size]::new(640, 18) $headerPanel.Controls.Add($subtitle) $contentPanel = New-Object System.Windows.Forms.Panel $contentPanel.Location = [System.Drawing.Point]::new(0, 86) $contentPanel.Size = [System.Drawing.Size]::new(700, 380) $contentPanel.BackColor = $colorBg $panelHelp.Controls.Add($contentPanel) $stepTitle = New-Object System.Windows.Forms.Label $stepTitle.Text = "Basic steps" $stepTitle.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10) $stepTitle.ForeColor = $colorText $stepTitle.Location = [System.Drawing.Point]::new(0, 0) $stepTitle.Size = [System.Drawing.Size]::new(200, 20) $contentPanel.Controls.Add($stepTitle) $stepY = 28 $steps = @( "Add hostnames (one per line, CSV, or CSR folder) then click 1) Preview.", "Click 2) Scan and select the DNS server (Primary is 10.106.60.1). Replication runs from there.", "Set the replication wait time. Typical value: 15 minutes.", "Choose output type (PEM or PFX). If PFX, supply a password.", "Click 5) Run." ) $index = 1 foreach ($step in $steps) { $num = New-Object System.Windows.Forms.Label $num.Text = "$index." $num.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 9) $num.ForeColor = $colorText $num.Location = [System.Drawing.Point]::new(0, $stepY) $num.Size = [System.Drawing.Size]::new(24, 20) $contentPanel.Controls.Add($num) $text = New-Object System.Windows.Forms.Label $text.Text = $step $text.Font = $font $text.ForeColor = $colorText $text.Location = [System.Drawing.Point]::new(28, $stepY) $text.Size = [System.Drawing.Size]::new(660, 32) $contentPanel.Controls.Add($text) $stepY += 34 $index++ } $optionsTitle = New-Object System.Windows.Forms.Label $optionsTitle.Text = "Options" $optionsTitle.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10) $optionsTitle.ForeColor = $colorText $optionsTitle.Location = [System.Drawing.Point]::new(0, ($stepY + 10)) $optionsTitle.Size = [System.Drawing.Size]::new(200, 20) $contentPanel.Controls.Add($optionsTitle) $optY = $stepY + 36 $options = @( "Turn off cert generation (DNS-only mode) to add DNS now and generate certs later.", "Disable DNS replication if records already point correctly and you only need a renewal." ) foreach ($opt in $options) { $bullet = New-Object System.Windows.Forms.Label $bullet.Text = "•" $bullet.Font = $font $bullet.ForeColor = $colorText $bullet.Location = [System.Drawing.Point]::new(0, $optY) $bullet.Size = [System.Drawing.Size]::new(12, 20) $contentPanel.Controls.Add($bullet) $optLabel = New-Object System.Windows.Forms.Label $optLabel.Text = $opt $optLabel.Font = $font $optLabel.ForeColor = $colorText $optLabel.Location = [System.Drawing.Point]::new(16, $optY) $optLabel.Size = [System.Drawing.Size]::new(660, 32) $contentPanel.Controls.Add($optLabel) $optY += 30 } $closeBtn = New-Object System.Windows.Forms.Button $closeBtn.Text = "Close" $closeBtn.Size = [System.Drawing.Size]::new(100, 30) $closeBtn.Location = [System.Drawing.Point]::new(600, 470) $panelHelp.Controls.Add($closeBtn) Style-ButtonSecondary $closeBtn $closeBtn.Add_Click({ $helpForm.Close() }) $helpForm.ShowDialog() | Out-Null } 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 $perHostBox.Enabled = -not $disabled $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 "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 } $helpBtn.Left = $header.Width - $helpBtn.Width - 12 $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 }) $helpBtn.Add_Click({ Show-HelpDialog }) $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 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." } else { $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.Add_FormClosed({ try { $logoBox.Image = $null } catch {} try { $logoLarge.Dispose() } catch {} try { $logoSmall.Dispose() } catch {} }) [void]$form.ShowDialog()