diff --git a/certy.ps1 b/certy.ps1 index 81c44e6..c5951a1 100644 --- a/certy.ps1 +++ b/certy.ps1 @@ -69,6 +69,33 @@ function Get-FilePreview { } } +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 Resolve-HostEntry { param( [string]$Name, @@ -182,7 +209,7 @@ function Invoke-Replication { function Invoke-Wacs { param( [string]$WacsPath, - [string]$HostFqdn, + [string[]]$HostFqdns, [string]$OutputType, [string]$OutputPath, [string]$PfxPassword, @@ -193,7 +220,12 @@ function Invoke-Wacs { [scriptblock]$Log ) - $args = @("--target", "manual", "--host", $HostFqdn) + $args = @("--target", "manual") + foreach ($host in $HostFqdns) { + if (-not [string]::IsNullOrWhiteSpace($host)) { + $args += @("--host", $host) + } + } if ($OutputType -eq "PEM") { $args += @("--store", "pemfiles", "--pemfilespath", $OutputPath) @@ -255,8 +287,9 @@ $rowHeight = 24 $gap = 8 $leftMargin = 20 $rightMargin = 20 -$buttonWidth = 80 -$buttonGap = 8 +$buttonWidth = 110 +$buttonGap = 10 +$actionButtonWidth = 130 $navTitle = New-Object System.Windows.Forms.Label $navTitle.Text = "CERTY" @@ -400,38 +433,38 @@ $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 - 90) $rowHeight $false +$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(80, 26) +$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 - 90) 80 $true +$filePreviewBox = Add-TextBox $xInput $y ($inputWidth - ($buttonWidth + $buttonGap)) 80 $true $filePreviewBox.ReadOnly = $true $filePreviewBtn = New-Object System.Windows.Forms.Button $filePreviewBtn.Text = "Preview" $filePreviewBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1)) -$filePreviewBtn.Size = [System.Drawing.Size]::new(80, 26) +$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 - 180) $rowHeight $false +$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(80, 26) +$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(80, 26) +$csrImportBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($csrImportBtn) Style-ButtonSecondary $csrImportBtn $y += $rowHeight + $gap @@ -446,12 +479,12 @@ $zoneBox.Text = "record.domain.govt.nz" $y += $rowHeight + $gap Add-Label "Target IPv4 for A records" $xLabel $y $labelWidth $rowHeight -$ipBox = Add-TextBox $xInput $y ($inputWidth - 90) $rowHeight $false +$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(80, 26) +$ipRefreshBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($ipRefreshBtn) Style-ButtonSecondary $ipRefreshBtn $y += $rowHeight + $gap @@ -459,7 +492,7 @@ $y += $rowHeight + $gap Add-Label "Primary DNS server" $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 - 90), $rowHeight) +$dnsServerBox.Size = [System.Drawing.Size]::new(($inputWidth - ($buttonWidth + $buttonGap)), $rowHeight) $dnsServerBox.DropDownStyle = "DropDown" $dnsServerBox.Text = "DC01.example.local" $dnsServerBox.FlatStyle = "Flat" @@ -469,7 +502,7 @@ $panel.Controls.Add($dnsServerBox) $dnsScanBtn = New-Object System.Windows.Forms.Button $dnsScanBtn.Text = "Scan" $dnsScanBtn.Location = [System.Drawing.Point]::new(($xInput + $inputWidth - $buttonWidth), ($y - 1)) -$dnsScanBtn.Size = [System.Drawing.Size]::new(80, 26) +$dnsScanBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($dnsScanBtn) Style-ButtonSecondary $dnsScanBtn $y += $rowHeight + $gap @@ -486,17 +519,17 @@ $panel.Controls.Add($dnsListBox) $y += 82 + $gap Add-Label "Replication targets (one per line)" $xLabel $y $labelWidth $rowHeight -$replicationTargetsBox = Add-TextBox $xInput $y ($inputWidth - 180) 70 $true +$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(80, 26) +$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(80, 26) +$primaryFromSelectedBtn.Size = [System.Drawing.Size]::new($buttonWidth, 26) $panel.Controls.Add($primaryFromSelectedBtn) Style-ButtonSecondary $primaryFromSelectedBtn $y += 70 + $gap @@ -554,6 +587,7 @@ $y += $rowHeight + $gap Add-SectionHeader "Run" $verboseBox = Add-CheckBox "Verbose" $xInput $y 120 $rowHeight $runWacsBox = Add-CheckBox "Run WACS after DNS update" ($xInput + 140) $y 260 $rowHeight +$perHostBox = Add-CheckBox "One cert per host" ($xInput + 430) $y 180 $rowHeight $runWacsBox.Checked = $true $y += $rowHeight + ($gap * 2) @@ -567,10 +601,17 @@ 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(120, 30) +$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 @@ -583,6 +624,42 @@ $logAction = { $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 + } +} + +$loadedDefaults = Load-Defaults +if ($loadedDefaults) { + if ($loadedDefaults.DefaultZone) { $zoneBox.Text = $loadedDefaults.DefaultZone } + if ($loadedDefaults.TargetIp) { $ipBox.Text = $loadedDefaults.TargetIp } + if ($loadedDefaults.DnsServer) { $dnsServerBox.Text = $loadedDefaults.DnsServer } + if ($loadedDefaults.ReplicationTargets) { $replicationTargetsBox.Text = $loadedDefaults.ReplicationTargets } + if ($loadedDefaults.ReplicationCommand) { $replicationCmdBox.Text = $loadedDefaults.ReplicationCommand } + if ($loadedDefaults.WacsPath) { $wacsPathBox.Text = $loadedDefaults.WacsPath } + if ($loadedDefaults.OutputPath) { $outputPathBox.Text = $loadedDefaults.OutputPath } + if ($loadedDefaults.PfxPassword) { $pfxPasswordBox.Text = $loadedDefaults.PfxPassword } + if ($loadedDefaults.BaseUri) { $baseUriBox.Text = $loadedDefaults.BaseUri } + if ($loadedDefaults.Validation) { $validationBox.Text = $loadedDefaults.Validation } + if ($loadedDefaults.ValidationPort) { $validationPortBox.Text = $loadedDefaults.ValidationPort } + if ($null -ne $loadedDefaults.UseProvidedFqdn) { $useFqdnBox.Checked = [bool]$loadedDefaults.UseProvidedFqdn } + if ($null -ne $loadedDefaults.RunWacs) { $runWacsBox.Checked = [bool]$loadedDefaults.RunWacs } + if ($null -ne $loadedDefaults.Verbose) { $verboseBox.Checked = [bool]$loadedDefaults.Verbose } + if ($null -ne $loadedDefaults.PerHostCerts) { $perHostBox.Checked = [bool]$loadedDefaults.PerHostCerts } + if ($loadedDefaults.OutputType) { $outputTypeBox.SelectedItem = $loadedDefaults.OutputType } + if (-not $outputTypeBox.SelectedItem) { $outputTypeBox.SelectedIndex = 0 } + Update-OutputTypeUI + & $logAction "Defaults loaded from $(Get-DefaultsPath)." +} + function Apply-Layout { if ($panel.ClientSize.Width -le 0) { return } $contentWidth = $panel.ClientSize.Width - $leftMargin - $rightMargin @@ -620,6 +697,8 @@ function Apply-Layout { $validationBox.Width = $inputWidthCalc $validationPortBox.Width = $inputWidthCalc $logBox.Width = $inputWidthCalc + $clearBtn.Left = $xInput + 140 + $saveDefaultsBtn.Left = $clearBtn.Left + $clearBtn.Width + $buttonGap } $browseBtn.Add_Click({ @@ -714,15 +793,32 @@ $primaryFromSelectedBtn.Add_Click({ }) $outputTypeBox.Add_SelectedIndexChanged({ - 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 + Update-OutputTypeUI +}) + +$saveDefaultsBtn.Add_Click({ + $defaults = [pscustomobject]@{ + DefaultZone = $zoneBox.Text + TargetIp = $ipBox.Text + DnsServer = $dnsServerBox.Text + ReplicationTargets = $replicationTargetsBox.Text + ReplicationCommand = $replicationCmdBox.Text + WacsPath = $wacsPathBox.Text + OutputType = $outputTypeBox.SelectedItem.ToString() + OutputPath = $outputPathBox.Text + PfxPassword = $pfxPasswordBox.Text + BaseUri = $baseUriBox.Text + Validation = $validationBox.Text + ValidationPort = $validationPortBox.Text + UseProvidedFqdn = $useFqdnBox.Checked + RunWacs = $runWacsBox.Checked + Verbose = $verboseBox.Checked + PerHostCerts = $perHostBox.Checked + } + Save-Defaults -Defaults $defaults + & $logAction "Defaults saved to $(Get-DefaultsPath)." + if (-not [string]::IsNullOrWhiteSpace($pfxPasswordBox.Text)) { + & $logAction "Warning: PFX password is stored in plaintext." } }) @@ -785,10 +881,27 @@ $runBtn.Add_Click({ if (-not (Test-Path -Path $wacsPath -PathType Leaf)) { throw "WACS not found at: $wacsPath" } - foreach ($entry in $hostEntries) { + 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 } + & $logAction "Requesting one certificate with $($hostList.Count) hostname(s)." Invoke-Wacs ` -WacsPath $wacsPath ` - -HostFqdn $entry.Fqdn ` + -HostFqdns $hostList ` -OutputType $outputType ` -OutputPath $outputPath ` -PfxPassword $pfxPasswordBox.Text `