Files
cert-management/certy.ps1

1855 lines
71 KiB
PowerShell

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 Get-InfRequestFromLines {
param([string[]]$Lines)
$commonName = $null
$sans = New-Object System.Collections.Generic.List[string]
$sanSet = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase)
if (-not $Lines) {
return [pscustomobject]@{
CommonName = $null
Sans = @()
Hosts = @()
}
}
foreach ($line in $Lines) {
if ([string]::IsNullOrWhiteSpace($line)) { continue }
if ($line.TrimStart() -match '^[;#]') { continue }
if (-not $commonName -and ($line -match '(?i)^\s*subject\s*=\s*"?([^"]+)"?')) {
$subject = $Matches[1]
if ($subject -match '(?i)\bCN\s*=\s*([^,"]+)') {
$commonName = $Matches[1].Trim()
}
}
$matches = [regex]::Matches($line, '(?i)\bdns\s*=\s*([^&",\s]+)')
foreach ($match in $matches) {
$value = $match.Groups[1].Value.Trim()
if ($value -and $sanSet.Add($value)) {
$sans.Add($value)
}
}
}
$hosts = New-Object System.Collections.Generic.List[string]
if ($commonName) {
$hosts.Add($commonName)
}
foreach ($san in $sans) {
if ($commonName -and $san.Equals($commonName, [System.StringComparison]::OrdinalIgnoreCase)) { continue }
$hosts.Add($san)
}
return [pscustomobject]@{
CommonName = $commonName
Sans = $sans
Hosts = $hosts
}
}
function Remove-InfSubjectLines {
param([string[]]$Lines)
$removed = $false
$filtered = foreach ($line in $Lines) {
if ($line -match '(?i)^\s*subject\s*=') {
$removed = $true
continue
}
$line
}
return [pscustomobject]@{
Lines = $filtered
Removed = $removed
}
}
function Save-SanitizedInf {
param(
[string]$FileName,
[string[]]$Lines,
[string]$Subdir = "inf-sanitized"
)
$dir = Join-Path $env:ProgramData ("Certy\\" + $Subdir)
if (-not (Test-Path -Path $dir -PathType Container)) {
New-Item -Path $dir -ItemType Directory -Force | Out-Null
}
$outPath = Join-Path $dir $FileName
Set-Content -Path $outPath -Value $Lines -Encoding ascii
return $outPath
}
function Sanitize-InfSubjectForCsr {
param([string[]]$Lines)
$updated = $false
$output = foreach ($line in $Lines) {
if ($line -match '(?i)^\s*subject\s*=\s*(.+)$') {
$value = $Matches[1].Trim()
$value = $value.Trim('"')
$parts = $value -split '\s*,\s*' | Where-Object { $_ }
$kept = @()
foreach ($part in $parts) {
if ($part -match '^(?i)OU\s*=') { continue }
$kept += $part.Trim()
}
if ($kept.Count -gt 0) {
$updated = $true
"Subject = `"$($kept -join ', ')`""
} else {
$updated = $true
continue
}
} else {
$line
}
}
return [pscustomobject]@{
Lines = $output
Updated = $updated
}
}
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")
$navLabels = @{}
$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)
$navLabels[$item] = $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
}
$helpNavBtn = New-Object System.Windows.Forms.Button
$helpNavBtn.Text = "How to use"
$helpNavBtn.Location = [System.Drawing.Point]::new(16, ($navY + 8))
$helpNavBtn.Size = [System.Drawing.Size]::new(130, 28)
$sidebar.Controls.Add($helpNavBtn)
Style-ButtonSecondary $helpNavBtn
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
return $sectionLabel
}
$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 (1-5)"
$helpBtn.Size = [System.Drawing.Size]::new(170, 32)
$helpBtn.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 9)
$helpBtn.Location = [System.Drawing.Point]::new(($header.Width - 182), 18)
$header.Controls.Add($helpBtn)
Style-ButtonPrimary $helpBtn
$y = $header.Bottom + 16
$sectionInput = 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
$infLabel = Add-Label "INF folder (optional)" $xLabel $y $labelWidth $rowHeight
$infFolderBox = Add-TextBox $xInput $y ($inputWidth - ((2 * $buttonWidth) + $buttonGap)) $rowHeight $false
$infBrowseBtn = New-Object System.Windows.Forms.Button
$infBrowseBtn.Text = "Browse"
$infBrowseBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - (2 * $buttonWidth + $buttonGap)), ($y - 1))
$infBrowseBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26)
$panel.Controls.Add($infBrowseBtn)
Style-ButtonSecondary $infBrowseBtn
$infImportBtn = New-Object System.Windows.Forms.Button
$infImportBtn.Text = "Import INF"
$infImportBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1))
$infImportBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26)
$panel.Controls.Add($infImportBtn)
Style-ButtonSecondary $infImportBtn
$y += $rowHeight + $gap
$infCsrOnlyBox = Add-CheckBox "Generate CSR from INF only (skip WACS)" $xInput $y $inputWidth $rowHeight
$infCsrOnlyBox.Checked = $false
$y += $rowHeight + $gap
$infCsrOutputLabel = Add-Label "CSR output folder" $xLabel $y $labelWidth $rowHeight
$infCsrOutputBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
$infCsrOutputBox.Text = "C:\ProgramData\Certy\csr-output"
$infCsrOutputLabel.Enabled = $false
$infCsrOutputBox.Enabled = $false
$y += $rowHeight + $gap
$useFqdnBox = Add-CheckBox "Input contains FQDNs (otherwise default zone is appended)" $xInput $y $inputWidth $rowHeight
$y += $rowHeight + $gap
$sectionDns = 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
$sectionAcme = 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
$pfxPasswordLabel = Add-Label "PFX password" $xLabel $y $labelWidth $rowHeight
$pfxPasswordBox = Add-TextBox $xInput $y $inputWidth $rowHeight $false
$pfxPasswordBox.UseSystemPasswordChar = $true
$y += $rowHeight + $gap
$pfxPasswordToggle = Add-CheckBox "Use PFX password" $xInput $y $inputWidth $rowHeight
$pfxPasswordToggle.Checked = $true
$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
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
$sectionRun = Add-SectionHeader "Run"
$verboseBox = Add-CheckBox "Verbose" $xInput $y $inputWidth $rowHeight
$y += $rowHeight + $gap
$disableCertsBox = Add-CheckBox "Turn off cert generation (DNS-only mode)" $xInput $y $inputWidth $rowHeight
$y += $rowHeight + $gap
$perHostBox = Add-CheckBox "One cert per host" $xInput $y $inputWidth $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
$sectionActivity = 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 Scroll-ToSection {
param([System.Windows.Forms.Control]$Target)
if (-not $Target) { return }
$panel.AutoScrollPosition = [System.Drawing.Point]::new(0, $Target.Top)
}
$sectionAnchors = @{
Input = $sectionInput
DNS = $sectionDns
ACME = $sectionAcme
Run = $sectionRun
Logs = $sectionActivity
}
$script:infRequests = @()
foreach ($key in $sectionAnchors.Keys) {
$label = $navLabels[$key]
if (-not $label) { continue }
$target = $sectionAnchors[$key]
$localLabel = $label
$localTarget = $target
$label.Cursor = [System.Windows.Forms.Cursors]::Hand
$label.Add_Click({
Scroll-ToSection -Target $localTarget
})
$label.Add_MouseEnter({
$localLabel.ForeColor = [System.Drawing.Color]::White
})
$label.Add_MouseLeave({
$localLabel.ForeColor = [System.Drawing.Color]::FromArgb(210, 220, 236)
})
}
function Update-OutputTypeUI {
if ($outputTypeBox.SelectedItem -eq "PEM") {
$outputPathLabel.Text = "PEM output path"
$pfxPasswordLabel.Visible = $false
$pfxPasswordBox.Visible = $false
$pfxPasswordToggle.Visible = $false
$pfxPasswordBox.Text = ""
$pfxPasswordToggle.Checked = $false
} else {
$outputPathLabel.Text = "PFX output path"
$pfxPasswordLabel.Visible = $true
$pfxPasswordBox.Visible = $true
$pfxPasswordToggle.Visible = $true
$pfxPasswordBox.Enabled = $pfxPasswordToggle.Checked -and -not $disableCertsBox.Checked
}
}
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, CSR, or INF 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.",
"Generate CSR from INF only to produce .req files without running WACS."
)
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 -or $infCsrOnlyBox.Checked
$perHostBox.Enabled = -not $disabled
$wacsPathBox.Enabled = -not $disabled
$outputTypeBox.Enabled = -not $disabled
$outputPathBox.Enabled = -not $disabled
$pfxPasswordToggle.Enabled = -not $disabled
$pfxPasswordBox.Enabled = -not $disabled -and $pfxPasswordToggle.Checked
$baseUriBox.Enabled = -not $disabled
$validationBox.Enabled = -not $disabled
$validationPortBox.Enabled = -not $disabled
}
function Update-InfCsrUI {
$enabled = $infCsrOnlyBox.Checked
$infCsrOutputLabel.Enabled = $enabled
$infCsrOutputBox.Enabled = $enabled
}
Update-InfCsrUI
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 "UsePfxPassword"
if ($null -ne $value) { $pfxPasswordToggle.Checked = [bool]$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 }
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "InfFolder"
if (-not [string]::IsNullOrWhiteSpace($value)) { $infFolderBox.Text = $value }
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "InfCsrOnly"
if ($null -ne $value) { $infCsrOnlyBox.Checked = [bool]$value }
$value = Get-DefaultValue -Defaults $loadedDefaults -Name "InfCsrOutput"
if (-not [string]::IsNullOrWhiteSpace($value)) { $infCsrOutputBox.Text = $value }
Update-OutputTypeUI
if (Test-Path function:Update-ReplicationUI) { Update-ReplicationUI }
Update-CertGenerationUI
Update-InfCsrUI
& $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
$infFolderBox.Width = $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap)
$infBrowseBtn.Left = $xInput + $inputWidthCalc - ((2 * $buttonWidth) + $buttonGap)
$infImportBtn.Left = $xInput + $inputWidthCalc - $buttonWidth
$infCsrOutputBox.Width = $inputWidthCalc
$infCsrOnlyBox.Width = $inputWidthCalc
$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
$pfxPasswordToggle.Width = $inputWidthCalc
$verboseBox.Width = $inputWidthCalc
$disableCertsBox.Width = $inputWidthCalc
$perHostBox.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 }
})
$pfxPasswordToggle.Add_CheckedChanged({
$pfxPasswordBox.Enabled = $pfxPasswordToggle.Checked -and -not $disableCertsBox.Checked
})
$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
}
})
$infBrowseBtn.Add_Click({
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
$infFolderBox.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)"
}
})
$infImportBtn.Add_Click({
try {
$folder = $infFolderBox.Text.Trim()
if (-not $folder) { throw "INF folder is empty." }
if (-not (Test-Path -Path $folder -PathType Container)) { throw "INF folder not found: $folder" }
$infFiles = Get-ChildItem -Path $folder -Filter *.inf -File -Recurse
if (-not $infFiles) {
& $logAction "No INF files found in $folder"
return
}
$script:infRequests = @()
$infHosts = @()
$subjectRemovedCount = 0
foreach ($infFile in $infFiles) {
$lines = Get-Content -Path $infFile.FullName
$reqData = Get-InfRequestFromLines -Lines $lines
$hosts = @($reqData.Hosts)
$sanitize = Remove-InfSubjectLines -Lines $lines
$sanitizedPath = Save-SanitizedInf -FileName $infFile.Name -Lines $sanitize.Lines -Subdir "inf-sanitized"
if ($sanitize.Removed) { $subjectRemovedCount++ }
$csrSanitize = Sanitize-InfSubjectForCsr -Lines $lines
$csrInfPath = Save-SanitizedInf -FileName $infFile.Name -Lines $csrSanitize.Lines -Subdir "inf-csr"
if ($hosts.Count -eq 0) {
& $logAction "INF $($infFile.Name): no hostnames detected."
continue
}
$script:infRequests += [pscustomobject]@{
File = $infFile.FullName
Hosts = $hosts
CommonName = $reqData.CommonName
Sans = @($reqData.Sans)
Sanitized = $sanitizedPath
CsrInf = $csrInfPath
}
$infHosts += $hosts
if ($reqData.CommonName) {
$sanList = if ($reqData.Sans.Count -gt 0) { $reqData.Sans -join ", " } else { "none" }
& $logAction "INF $($infFile.Name): CN=$($reqData.CommonName); SANs=$sanList"
}
}
$infHosts = @($infHosts | Where-Object { $_ } | Sort-Object -Unique)
if ($infHosts.Count -gt 0) {
$currentHosts = Split-List $hostsBox.Text
$merged = Merge-Hostnames -Existing $currentHosts -NewItems $infHosts
$hostsBox.Text = ($merged -join [Environment]::NewLine)
}
& $logAction "Imported $($infFiles.Count) INF file(s), added $($infHosts.Count) hostname(s)."
if ($subjectRemovedCount -gt 0) {
& $logAction "Removed Subject line from $subjectRemovedCount INF file(s) (sanitized copies saved)."
}
} 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
})
$helpNavBtn.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
})
$infCsrOnlyBox.Add_CheckedChanged({
Update-CertGenerationUI
Update-InfCsrUI
})
$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
UsePfxPassword = $pfxPasswordToggle.Checked
BaseUri = $baseUriBox.Text
Validation = $validationBox.Text
ValidationPort = $validationPortBox.Text
UseProvidedFqdn = $useFqdnBox.Checked
Verbose = $verboseBox.Checked
PerHostCerts = $perHostBox.Checked
DisableCertGeneration = $disableCertsBox.Checked
InfFolder = $infFolderBox.Text
InfCsrOnly = $infCsrOnlyBox.Checked
InfCsrOutput = $infCsrOutputBox.Text
}
Save-Defaults -Defaults $defaults
& $logAction "Defaults saved to $(Get-DefaultsPath)."
if ($pfxPasswordToggle.Checked -and -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." }
$usePfxPassword = $outputType -eq "PFX" -and $pfxPasswordToggle.Checked
if ($usePfxPassword -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 ($infCsrOnlyBox.Checked) {
if (-not $script:infRequests -or $script:infRequests.Count -eq 0) {
throw "INF CSR generation enabled, but no INF files were imported."
}
$csrOutputDir = $infCsrOutputBox.Text.Trim()
if (-not $csrOutputDir) { throw "CSR output folder is required." }
if (-not (Test-Path -Path $csrOutputDir -PathType Container)) {
New-Item -Path $csrOutputDir -ItemType Directory -Force | Out-Null
}
foreach ($req in $script:infRequests) {
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($req.File)
$csrPath = Join-Path $csrOutputDir ($baseName + ".req")
if (Test-Path -Path $csrPath) {
$csrPath = Join-Path $csrOutputDir ($baseName + "-" + (Get-Date -Format "yyyyMMddHHmmss") + ".req")
}
$infPath = if ($req.CsrInf) { $req.CsrInf } else { $req.File }
& $logAction "Generating CSR from $([System.IO.Path]::GetFileName($infPath)) -> $csrPath"
$output = & certreq.exe -new $infPath $csrPath 2>&1
foreach ($line in $output) {
& $logAction $line
}
}
} elseif ($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 ($script:infRequests -and $script:infRequests.Count -gt 0) {
& $logAction "INF requests detected; issuing one certificate per INF file."
foreach ($req in $script:infRequests) {
$reqEntries = @($req.Hosts | ForEach-Object { Resolve-HostEntry -Name $_ -Zone $zone -UseProvidedFqdn $useFqdnBox.Checked } | Where-Object { $_ })
$reqFqdns = @($reqEntries | ForEach-Object { $_.Fqdn } | Where-Object { $_ })
if ($reqFqdns.Count -eq 0) {
& $logAction "INF $([System.IO.Path]::GetFileName($req.File)) skipped (no hosts)."
continue
}
& $logAction "Requesting certificate for INF $([System.IO.Path]::GetFileName($req.File)) with $($reqFqdns.Count) hostname(s)."
Invoke-Wacs `
-WacsPath $wacsPath `
-HostFqdns $reqFqdns `
-OutputType $outputType `
-OutputPath $outputPath `
-PfxPassword ($(if ($usePfxPassword) { $pfxPasswordBox.Text } else { "" })) `
-BaseUri $baseUriBox.Text.Trim() `
-Validation $validationBox.Text.Trim() `
-ValidationPort $validationPortBox.Text.Trim() `
-Verbose $verboseBox.Checked `
-Log $logAction
}
} elseif ($perHostBox.Checked) {
foreach ($entry in $hostEntries) {
& $logAction "Requesting certificate for $($entry.Fqdn)."
Invoke-Wacs `
-WacsPath $wacsPath `
-HostFqdns @($entry.Fqdn) `
-OutputType $outputType `
-OutputPath $outputPath `
-PfxPassword ($(if ($usePfxPassword) { $pfxPasswordBox.Text } else { "" })) `
-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 ($(if ($usePfxPassword) { $pfxPasswordBox.Text } else { "" })) `
-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()