diff --git a/certy.ps1 b/certy.ps1
new file mode 100644
index 0000000..7cd0382
--- /dev/null
+++ b/certy.ps1
@@ -0,0 +1,1018 @@
+Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Xaml
+Add-Type -AssemblyName System.Windows.Forms
+
+$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
+$defaultSettingsPath = Join-Path $scriptRoot "certy.settings.json"
+$defaultLogPath = Join-Path $scriptRoot "certy.log"
+
+$xaml = @'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'@
+
+$settingsXaml = @'
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+'@
+
+[xml]$xamlDoc = $xaml
+$reader = New-Object System.Xml.XmlNodeReader $xamlDoc
+$window = [Windows.Markup.XamlReader]::Load($reader)
+
+function Get-Control([string]$name) {
+ return $window.FindName($name)
+}
+
+$txtSettingsPath = Get-Control "txtSettingsPath"
+$txtLogPath = Get-Control "txtLogPath"
+$btnOpenSettings = Get-Control "btnOpenSettings"
+$rbFlowCsr = Get-Control "rbFlowCsr"
+$rbFlowInf = Get-Control "rbFlowInf"
+$pnlCsrFlow = Get-Control "pnlCsrFlow"
+$pnlInfFlow = Get-Control "pnlInfFlow"
+
+$txtInfInputPath = Get-Control "txtInfInputPath"
+$txtInfOutputDir = Get-Control "txtInfOutputDir"
+$txtInfTemplate = Get-Control "txtInfTemplate"
+$chkInfAdditionalSans = Get-Control "chkInfAdditionalSans"
+$cmbInfTemplatePreset = Get-Control "cmbInfTemplatePreset"
+$cmbInfPrimaryDomain = Get-Control "cmbInfPrimaryDomain"
+$txtInfAdditionalDomains = Get-Control "txtInfAdditionalDomains"
+$btnBrowseInfInput = Get-Control "btnBrowseInfInput"
+$btnBrowseInfOutput = Get-Control "btnBrowseInfOutput"
+$btnRunInf = Get-Control "btnRunInf"
+
+$txtDnsListInputPath = Get-Control "txtDnsListInputPath"
+$cmbCsrDomainSuffix = Get-Control "cmbCsrDomainSuffix"
+$rbCsrFileFqdn = Get-Control "rbCsrFileFqdn"
+$rbCsrFileHost = Get-Control "rbCsrFileHost"
+$btnPreviewCsr = Get-Control "btnPreviewCsr"
+$txtCsrPreview = Get-Control "txtCsrPreview"
+$txtDnsZone = Get-Control "txtDnsZone"
+$txtDnsTargetIp = Get-Control "txtDnsTargetIp"
+$txtDnsServer = Get-Control "txtDnsServer"
+$txtDnsPlaceholderOutput = Get-Control "txtDnsPlaceholderOutput"
+$chkDnsCreatePlaceholders = Get-Control "chkDnsCreatePlaceholders"
+$chkDnsUpdate = Get-Control "chkDnsUpdate"
+$btnBrowseDnsListInput = Get-Control "btnBrowseDnsListInput"
+$btnBrowseDnsPlaceholderOutput = Get-Control "btnBrowseDnsPlaceholderOutput"
+
+$txtWacsPath = Get-Control "txtWacsPath"
+$txtWacsPfxFolder = Get-Control "txtWacsPfxFolder"
+$txtWacsBaseUri = Get-Control "txtWacsBaseUri"
+$txtWacsValidation = Get-Control "txtWacsValidation"
+$txtWacsValidationPort = Get-Control "txtWacsValidationPort"
+$txtWacsTarget = Get-Control "txtWacsTarget"
+$txtWacsStore = Get-Control "txtWacsStore"
+$chkWacsAddSans = Get-Control "chkWacsAddSans"
+$txtWacsSans = Get-Control "txtWacsSans"
+$chkWacsVerbose = Get-Control "chkWacsVerbose"
+$btnBrowseWacsPath = Get-Control "btnBrowseWacsPath"
+$btnBrowseWacsPfxFolder = Get-Control "btnBrowseWacsPfxFolder"
+$btnRunWacs = Get-Control "btnRunWacs"
+
+$txtLogOutput = Get-Control "txtLogOutput"
+$btnClearLog = Get-Control "btnClearLog"
+
+$txtSettingsPath.Text = $defaultSettingsPath
+$txtLogPath.Text = $defaultLogPath
+$script:FqdnListText = ""
+$pnlCsrFlow.Visibility = "Visible"
+$pnlInfFlow.Visibility = "Collapsed"
+
+$defaultTemplate = @'
+[Version]
+Signature="$Windows NT$"
+[NewRequest]
+Subject = "CN=$Placeholder"
+KeySpec = 1
+KeyLength = 2048
+Exportable = TRUE
+MachineKeySet = True
+ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
+HashAlgorithm = sha256
+RequestType = PKCS10
+KeyUsage = 0xa0
+FriendlyName = "$Placeholder"
+[EnhancedKeyUsageExtension]
+OID=1.3.6.1.5.5.7.3.1 ; Server Authentication
+OID=1.3.6.1.5.5.7.3.2 ; Client Authentication
+'@
+
+$legacyTemplate = @'
+[Version]
+Signature="$Windows NT$"
+[NewRequest]
+Subject = "CN=$Placeholder.printer.MBIE.govt.nz;OU=ICT;O=Ministry of Business, Innovation and Employment;L=Wellington;S=Wellington;C=NZ"
+X500NameFlags = 0x40000000
+Exportable = TRUE
+KeyLength = 2048
+KeySpec = 1
+KeyUsage = 0xA0
+MachineKeySet = True
+ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
+HashAlgorithm = sha256
+RequestType = PKCS10
+FriendlyName = "$Placeholder.printer.MBIE.govt.nz - 2026"
+[EnhancedKeyUsageExtension]
+OID=1.3.6.1.5.5.7.3.1 ; Server Authentication
+OID=1.3.6.1.5.5.7.3.2 ; Client Authentication
+'@
+
+$txtInfTemplate.Text = $defaultTemplate
+$chkInfAdditionalSans.IsChecked = $false
+
+$txtDnsListInputPath.Text = ''
+$txtDnsZone.Text = 'record.domain.govt.nz'
+$txtDnsTargetIp.Text = 'managementboxIP'
+$txtDnsServer.Text = 'DC01.example.local'
+$txtDnsPlaceholderOutput.Text = 'C:\Temp\CSRs'
+$chkDnsCreatePlaceholders.IsChecked = $false
+$chkDnsUpdate.IsChecked = $true
+
+$txtWacsPath.Text = 'C:\ProgramData\Wacs\wacs.exe'
+$txtWacsPfxFolder.Text = 'C:\ProgramData\Wacs\output'
+$txtWacsBaseUri.Text = 'https://acmeprod.domain.govt.nz:9999/acme/rsa/'
+$txtWacsValidation.Text = 'selfhosting'
+$txtWacsValidationPort.Text = '9998'
+$txtWacsTarget.Text = 'manual'
+$txtWacsStore.Text = 'pfxfile'
+$chkWacsAddSans.IsChecked = $false
+$txtWacsSans.Text = ""
+$chkWacsVerbose.IsChecked = $true
+
+function Write-Log([string]$message, [string]$level = "INFO") {
+ $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
+ $line = "{0} [{1}] {2}" -f $timestamp, $level, $message
+ $txtLogOutput.AppendText($line + [Environment]::NewLine)
+ $txtLogOutput.ScrollToEnd()
+
+ $logPath = $txtLogPath.Text.Trim()
+ if ([string]::IsNullOrWhiteSpace($logPath)) { return }
+
+ try {
+ $logDir = Split-Path -Parent $logPath
+ if ($logDir -and -not (Test-Path $logDir)) {
+ New-Item -Path $logDir -ItemType Directory -Force | Out-Null
+ }
+ Add-Content -Path $logPath -Value $line -Encoding ascii
+ } catch {
+ $txtLogOutput.AppendText("Failed to write log file: $($_.Exception.Message)" + [Environment]::NewLine)
+ }
+}
+
+function Save-Settings {
+ $settingsPath = $txtSettingsPath.Text.Trim()
+ if ([string]::IsNullOrWhiteSpace($settingsPath)) {
+ Write-Log "Settings path is empty." "ERROR"
+ return
+ }
+
+ $settings = @{
+ SettingsPath = $settingsPath
+ LogPath = $txtLogPath.Text.Trim()
+ FqdnList = $(if ($script:FqdnListText) { $script:FqdnListText } else { "" })
+ Inf = @{
+ InputPath = $txtInfInputPath.Text.Trim()
+ OutputDir = $txtInfOutputDir.Text.Trim()
+ Template = $txtInfTemplate.Text
+ IncludeAdditionalSans = [bool]$chkInfAdditionalSans.IsChecked
+ PrimaryDomain = $cmbInfPrimaryDomain.Text.Trim()
+ AdditionalDomains = $txtInfAdditionalDomains.Text
+ TemplatePreset = $cmbInfTemplatePreset.Text
+ }
+ Dns = @{
+ ListInputPath = $txtDnsListInputPath.Text.Trim()
+ Zone = $txtDnsZone.Text.Trim()
+ TargetIp = $txtDnsTargetIp.Text.Trim()
+ DnsServer = $txtDnsServer.Text.Trim()
+ PlaceholderOutput = $txtDnsPlaceholderOutput.Text.Trim()
+ CreatePlaceholders = [bool]$chkDnsCreatePlaceholders.IsChecked
+ UpdateDns = [bool]$chkDnsUpdate.IsChecked
+ }
+ Wacs = @{
+ WacsPath = $txtWacsPath.Text.Trim()
+ PfxFolder = $txtWacsPfxFolder.Text.Trim()
+ BaseUri = $txtWacsBaseUri.Text.Trim()
+ Validation = $txtWacsValidation.Text.Trim()
+ ValidationPort = $txtWacsValidationPort.Text.Trim()
+ Target = $txtWacsTarget.Text.Trim()
+ Store = $txtWacsStore.Text.Trim()
+ AddSans = [bool]$chkWacsAddSans.IsChecked
+ Sans = $txtWacsSans.Text
+ Verbose = [bool]$chkWacsVerbose.IsChecked
+ }
+ SavedAt = (Get-Date).ToString("s")
+ }
+
+ try {
+ $settingsDir = Split-Path -Parent $settingsPath
+ if ($settingsDir -and -not (Test-Path $settingsDir)) {
+ New-Item -Path $settingsDir -ItemType Directory -Force | Out-Null
+ }
+ $json = $settings | ConvertTo-Json -Depth 6
+ Set-Content -Path $settingsPath -Value $json -Encoding ascii
+ Write-Log "Settings saved to $settingsPath"
+ } catch {
+ Write-Log "Failed to save settings: $($_.Exception.Message)" "ERROR"
+ }
+}
+
+function Load-Settings {
+ $settingsPath = $txtSettingsPath.Text.Trim()
+ if (-not (Test-Path $settingsPath -PathType Leaf)) {
+ Write-Log "Settings file not found: $settingsPath" "ERROR"
+ return
+ }
+
+ try {
+ $raw = Get-Content -Path $settingsPath -Raw
+ $settings = $raw | ConvertFrom-Json
+
+ if ($settings.LogPath) { $txtLogPath.Text = $settings.LogPath }
+
+ if ($settings.FqdnList) { $script:FqdnListText = $settings.FqdnList }
+
+ if ($settings.Inf) {
+ $txtInfInputPath.Text = $settings.Inf.InputPath
+ $txtInfOutputDir.Text = $settings.Inf.OutputDir
+ if ($settings.Inf.Template) { $txtInfTemplate.Text = $settings.Inf.Template }
+ if ($settings.Inf.TemplatePreset) { $cmbInfTemplatePreset.Text = $settings.Inf.TemplatePreset }
+ if ($settings.Inf.PrimaryDomain) { $cmbInfPrimaryDomain.Text = $settings.Inf.PrimaryDomain }
+ if ($settings.Inf.AdditionalDomains) { $txtInfAdditionalDomains.Text = $settings.Inf.AdditionalDomains }
+ if ($settings.Inf.PSObject.Properties.Name -contains "IncludeAdditionalSans") {
+ $chkInfAdditionalSans.IsChecked = [bool]$settings.Inf.IncludeAdditionalSans
+ } elseif ($settings.Inf.PSObject.Properties.Name -contains "UseSimpleTemplate") {
+ $chkInfAdditionalSans.IsChecked = -not [bool]$settings.Inf.UseSimpleTemplate
+ }
+ }
+
+ if ($settings.Dns) {
+ if ($settings.Dns.ListInputPath) { $txtDnsListInputPath.Text = $settings.Dns.ListInputPath }
+ if ($settings.Dns.Zone) { $txtDnsZone.Text = $settings.Dns.Zone }
+ if ($settings.Dns.TargetIp) { $txtDnsTargetIp.Text = $settings.Dns.TargetIp }
+ if ($settings.Dns.DnsServer) { $txtDnsServer.Text = $settings.Dns.DnsServer }
+ if ($settings.Dns.PlaceholderOutput) { $txtDnsPlaceholderOutput.Text = $settings.Dns.PlaceholderOutput }
+ if ($settings.Dns.PSObject.Properties.Name -contains "CreatePlaceholders") { $chkDnsCreatePlaceholders.IsChecked = [bool]$settings.Dns.CreatePlaceholders }
+ if ($settings.Dns.PSObject.Properties.Name -contains "UpdateDns") { $chkDnsUpdate.IsChecked = [bool]$settings.Dns.UpdateDns }
+ }
+
+ if ($settings.Wacs) {
+ if ($settings.Wacs.WacsPath) { $txtWacsPath.Text = $settings.Wacs.WacsPath }
+ if ($settings.Wacs.PfxFolder) { $txtWacsPfxFolder.Text = $settings.Wacs.PfxFolder }
+ if ($settings.Wacs.BaseUri) { $txtWacsBaseUri.Text = $settings.Wacs.BaseUri }
+ if ($settings.Wacs.Validation) { $txtWacsValidation.Text = $settings.Wacs.Validation }
+ if ($settings.Wacs.ValidationPort) { $txtWacsValidationPort.Text = $settings.Wacs.ValidationPort }
+ if ($settings.Wacs.Target) { $txtWacsTarget.Text = $settings.Wacs.Target }
+ if ($settings.Wacs.Store) { $txtWacsStore.Text = $settings.Wacs.Store }
+ if ($settings.Wacs.PSObject.Properties.Name -contains "AddSans") { $chkWacsAddSans.IsChecked = [bool]$settings.Wacs.AddSans }
+ if ($settings.Wacs.PSObject.Properties.Name -contains "Sans") { $txtWacsSans.Text = $settings.Wacs.Sans }
+ $chkWacsVerbose.IsChecked = [bool]$settings.Wacs.Verbose
+ }
+
+ Refresh-FqdnLists
+
+ Write-Log "Settings loaded from $settingsPath"
+ } catch {
+ Write-Log "Failed to load settings: $($_.Exception.Message)" "ERROR"
+ }
+}
+
+function Open-FileDialog([string]$filter = "All files (*.*)|*.*") {
+ $dialog = New-Object Microsoft.Win32.OpenFileDialog
+ $dialog.Filter = $filter
+ $dialog.Multiselect = $false
+ if ($dialog.ShowDialog() -eq $true) { return $dialog.FileName }
+ return $null
+}
+
+function Open-FolderDialog {
+ $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
+ if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
+ return $dialog.SelectedPath
+ }
+ return $null
+}
+
+function Get-FqdnSuffixes {
+ if ([string]::IsNullOrWhiteSpace($script:FqdnListText)) { return @() }
+ return $script:FqdnListText -split "(\r?\n)" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
+}
+
+function Refresh-FqdnLists {
+ $suffixes = Get-FqdnSuffixes
+ $cmbCsrDomainSuffix.Items.Clear()
+ $cmbInfPrimaryDomain.Items.Clear()
+
+ foreach ($suffix in $suffixes) {
+ $null = $cmbCsrDomainSuffix.Items.Add($suffix)
+ $null = $cmbInfPrimaryDomain.Items.Add($suffix)
+ }
+
+ if ($suffixes.Count -gt 0) {
+ if ([string]::IsNullOrWhiteSpace($cmbCsrDomainSuffix.Text)) { $cmbCsrDomainSuffix.SelectedIndex = 0 }
+ if ([string]::IsNullOrWhiteSpace($cmbInfPrimaryDomain.Text)) { $cmbInfPrimaryDomain.SelectedIndex = 0 }
+ }
+}
+
+function Set-InfTemplatePreset([string]$preset) {
+ switch ($preset) {
+ "Default (certmgr)" { $txtInfTemplate.Text = $defaultTemplate }
+ "Legacy (MBIE)" { $txtInfTemplate.Text = $legacyTemplate }
+ default { }
+ }
+}
+
+function Get-NameItemsFromFile([string]$path) {
+ if (-not (Test-Path -Path $path -PathType Leaf)) {
+ throw "Input file not found: $path"
+ }
+ return Get-Content -Path $path | ForEach-Object { $_.Trim() } | Where-Object { $_ }
+}
+
+function Get-HostLabel([string]$fqdnOrName) {
+ if ($fqdnOrName -match '\.') { return $fqdnOrName.Split('.')[0] }
+ return $fqdnOrName
+}
+
+function Resolve-Fqdn([string]$name, [string]$suffix) {
+ if ($name -match '\.') { return $name }
+ if ([string]::IsNullOrWhiteSpace($suffix)) {
+ throw "Missing domain suffix for name: $name"
+ }
+ return "$name.$suffix"
+}
+
+function Get-SafeFileName([string]$name) {
+ $invalidChars = [IO.Path]::GetInvalidFileNameChars()
+ return -join ($name.ToCharArray() | ForEach-Object {
+ if ($invalidChars -contains $_) { '_' } else { $_ }
+ })
+}
+
+function Build-CsrItems([string[]]$names, [string]$suffix, [bool]$useFqdnFileName) {
+ $items = @()
+ foreach ($name in $names) {
+ $fqdn = Resolve-Fqdn $name $suffix
+ $hostLabel = Get-HostLabel $fqdn
+ $fileBase = if ($useFqdnFileName) { $fqdn } else { $hostLabel }
+ $items += [PSCustomObject]@{
+ InputName = $name
+ HostLabel = $hostLabel
+ Fqdn = $fqdn
+ FileBase = $fileBase
+ }
+ }
+ return $items
+}
+
+function Get-SanValues([string]$hostLabel, [string]$suffix, [string]$rawText) {
+ $values = @()
+ if ([string]::IsNullOrWhiteSpace($rawText)) { return $values }
+
+ $rawItems = $rawText -split "(\r?\n|,)" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
+ foreach ($item in $rawItems) {
+ $value = $item.Replace("{host}", $hostLabel)
+ if ($value -notmatch '\.' -and -not [string]::IsNullOrWhiteSpace($suffix)) {
+ $value = "$value.$suffix"
+ }
+ $values += $value
+ }
+
+ return $values | Select-Object -Unique
+}
+
+function Build-SanBlock([string[]]$fqdns) {
+ if (-not $fqdns -or $fqdns.Count -eq 0) { return "" }
+ $lines = @(
+ "[Extensions]",
+ "2.5.29.17 = ""{text}"""
+ )
+ foreach ($fqdn in $fqdns) {
+ $lines += "_continue_ = ""dns=$fqdn&"""
+ }
+ return ($lines -join "`r`n")
+}
+
+$cmbInfTemplatePreset.Items.Clear()
+$cmbInfTemplatePreset.Items.Clear()
+$null = $cmbInfTemplatePreset.Items.Add("Default (certmgr)")
+$null = $cmbInfTemplatePreset.Items.Add("Legacy (MBIE)")
+$null = $cmbInfTemplatePreset.Items.Add("Custom (edit below)")
+if ([string]::IsNullOrWhiteSpace($cmbInfTemplatePreset.Text)) {
+ $cmbInfTemplatePreset.SelectedIndex = 0
+ Set-InfTemplatePreset "Default (certmgr)"
+}
+
+$cmbInfTemplatePreset.Add_SelectionChanged({
+ $selected = $cmbInfTemplatePreset.SelectedItem
+ if ($selected) { Set-InfTemplatePreset $selected.ToString() }
+})
+
+Refresh-FqdnLists
+
+if (Test-Path -Path $txtSettingsPath.Text -PathType Leaf) {
+ Load-Settings
+}
+
+$btnOpenSettings.Add_Click({
+ try {
+ [xml]$settingsDoc = $settingsXaml
+ $settingsReader = New-Object System.Xml.XmlNodeReader $settingsDoc
+ $settingsWindow = [Windows.Markup.XamlReader]::Load($settingsReader)
+
+ function Get-SettingsControl([string]$name) {
+ return $settingsWindow.FindName($name)
+ }
+
+ $txtSettingsPathWin = Get-SettingsControl "txtSettingsPathWin"
+ $txtLogPathWin = Get-SettingsControl "txtLogPathWin"
+ $btnBrowseSettingsWin = Get-SettingsControl "btnBrowseSettingsWin"
+ $btnBrowseLogWin = Get-SettingsControl "btnBrowseLogWin"
+ $txtFqdnListWin = Get-SettingsControl "txtFqdnListWin"
+ $btnSaveSettingsWin = Get-SettingsControl "btnSaveSettingsWin"
+ $btnLoadSettingsWin = Get-SettingsControl "btnLoadSettingsWin"
+ $btnCloseSettingsWin = Get-SettingsControl "btnCloseSettingsWin"
+
+ $txtSettingsPathWin.Text = $txtSettingsPath.Text
+ $txtLogPathWin.Text = $txtLogPath.Text
+ $txtFqdnListWin.Text = $script:FqdnListText
+
+ $btnBrowseSettingsWin.Add_Click({
+ $path = Open-FileDialog "JSON (*.json)|*.json|All files (*.*)|*.*"
+ if ($path) { $txtSettingsPathWin.Text = $path }
+ })
+
+ $btnBrowseLogWin.Add_Click({
+ $path = Open-FileDialog "Log (*.log)|*.log|All files (*.*)|*.*"
+ if ($path) { $txtLogPathWin.Text = $path }
+ })
+
+ $btnSaveSettingsWin.Add_Click({
+ $txtSettingsPath.Text = $txtSettingsPathWin.Text
+ $txtLogPath.Text = $txtLogPathWin.Text
+ $script:FqdnListText = $txtFqdnListWin.Text
+ Refresh-FqdnLists
+ Save-Settings
+ })
+
+ $btnLoadSettingsWin.Add_Click({
+ $txtSettingsPath.Text = $txtSettingsPathWin.Text
+ Load-Settings
+ $txtSettingsPathWin.Text = $txtSettingsPath.Text
+ $txtLogPathWin.Text = $txtLogPath.Text
+ $txtFqdnListWin.Text = $script:FqdnListText
+ })
+
+ $btnCloseSettingsWin.Add_Click({ $settingsWindow.Close() })
+
+ $settingsWindow.Owner = $window
+ $settingsWindow.ShowDialog() | Out-Null
+ } catch {
+ Write-Log "Failed to open settings: $($_.Exception.Message)" "ERROR"
+ }
+})
+
+$btnBrowseInfInput.Add_Click({
+ $path = Open-FileDialog "Text files (*.txt)|*.txt|All files (*.*)|*.*"
+ if ($path) { $txtInfInputPath.Text = $path }
+})
+
+$btnBrowseInfOutput.Add_Click({
+ $path = Open-FolderDialog
+ if ($path) { $txtInfOutputDir.Text = $path }
+})
+
+$btnBrowseDnsListInput.Add_Click({
+ $path = Open-FileDialog "Text files (*.txt)|*.txt|All files (*.*)|*.*"
+ if ($path) { $txtDnsListInputPath.Text = $path }
+})
+
+$btnBrowseDnsPlaceholderOutput.Add_Click({
+ $path = Open-FolderDialog
+ if ($path) { $txtDnsPlaceholderOutput.Text = $path }
+})
+
+$btnBrowseWacsPath.Add_Click({
+ $path = Open-FileDialog "Executable (*.exe)|*.exe|All files (*.*)|*.*"
+ if ($path) { $txtWacsPath.Text = $path }
+})
+
+$btnBrowseWacsPfxFolder.Add_Click({
+ $path = Open-FolderDialog
+ if ($path) { $txtWacsPfxFolder.Text = $path }
+})
+
+$btnClearLog.Add_Click({
+ $txtLogOutput.Clear()
+})
+
+$rbFlowCsr.Add_Click({
+ $pnlCsrFlow.Visibility = "Visible"
+ $pnlInfFlow.Visibility = "Collapsed"
+})
+
+$rbFlowInf.Add_Click({
+ $pnlCsrFlow.Visibility = "Collapsed"
+ $pnlInfFlow.Visibility = "Visible"
+})
+
+$btnPreviewCsr.Add_Click({
+ try {
+ $listPath = $txtDnsListInputPath.Text.Trim()
+ $suffix = $cmbCsrDomainSuffix.Text.Trim()
+ $names = Get-NameItemsFromFile $listPath
+ $useFqdnFileName = [bool]$rbCsrFileFqdn.IsChecked
+ $items = Build-CsrItems $names $suffix $useFqdnFileName
+
+ $previewLines = $items | ForEach-Object {
+ "{0} -> {1} -> {2}.csr" -f $_.InputName, $_.Fqdn, $_.FileBase
+ }
+ $txtCsrPreview.Text = ($previewLines -join "`r`n")
+ } catch {
+ Write-Log "Preview failed: $($_.Exception.Message)" "ERROR"
+ }
+})
+
+$btnRunInf.Add_Click({
+ try {
+ $inputPath = $txtInfInputPath.Text.Trim()
+ $outputDir = $txtInfOutputDir.Text.Trim()
+ $template = $txtInfTemplate.Text
+ $placeholderToken = '$Placeholder'
+ $primaryDomain = $cmbInfPrimaryDomain.Text.Trim()
+ $additionalDomains = @()
+ if (-not [string]::IsNullOrWhiteSpace($txtInfAdditionalDomains.Text)) {
+ $additionalDomains = $txtInfAdditionalDomains.Text -split "(\r?\n|,)" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
+ }
+
+ if (-not (Test-Path -Path $inputPath -PathType Leaf)) {
+ throw "Input file not found: $inputPath"
+ }
+ if (-not (Test-Path -Path $outputDir -PathType Container)) {
+ throw "Output directory not found: $outputDir"
+ }
+
+ $lines = Get-Content -Path $inputPath
+ $count = 0
+
+ foreach ($line in $lines) {
+ $name = $line.Trim()
+ if ([string]::IsNullOrWhiteSpace($name)) { continue }
+
+ $hostLabel = Get-HostLabel $name
+ $primaryFqdn = if ($name -match '\.') { $name } else { Resolve-Fqdn $name $primaryDomain }
+
+ $content = $template.Replace($placeholderToken, $hostLabel)
+ if ([bool]$chkInfAdditionalSans.IsChecked) {
+ $sanFqdns = @($primaryFqdn)
+ foreach ($domain in $additionalDomains) {
+ $sanFqdns += "$hostLabel.$domain"
+ }
+ $sanFqdns = $sanFqdns | Select-Object -Unique
+ if ($content -notmatch '\[Extensions\]') {
+ $content = $content.TrimEnd() + "`r`n`r`n" + (Build-SanBlock $sanFqdns)
+ }
+ }
+
+ $safeName = Get-SafeFileName $hostLabel
+ $outPath = Join-Path -Path $outputDir -ChildPath ($safeName + ".inf")
+ Set-Content -Path $outPath -Value $content -Encoding ascii
+ $count++
+ Write-Log "Generated: $outPath"
+ }
+
+ Write-Log "INF generation completed. Files created: $count"
+ } catch {
+ Write-Log "INF generation failed: $($_.Exception.Message)" "ERROR"
+ }
+})
+
+$btnRunWacs.Add_Click({
+ try {
+ $listPath = $txtDnsListInputPath.Text.Trim()
+ $suffix = $cmbCsrDomainSuffix.Text.Trim()
+ $wacsPath = $txtWacsPath.Text.Trim()
+ $pfxFolder = $txtWacsPfxFolder.Text.Trim()
+ $baseUri = $txtWacsBaseUri.Text.Trim()
+ $validation = $txtWacsValidation.Text.Trim()
+ $validationPort = $txtWacsValidationPort.Text.Trim()
+ $target = $txtWacsTarget.Text.Trim()
+ $store = $txtWacsStore.Text.Trim()
+ $createPlaceholders = [bool]$chkDnsCreatePlaceholders.IsChecked
+ $updateDns = [bool]$chkDnsUpdate.IsChecked
+ $dnsZone = $txtDnsZone.Text.Trim()
+ $targetIp = $txtDnsTargetIp.Text.Trim()
+ $dnsServer = $txtDnsServer.Text.Trim()
+ $placeholderOutput = $txtDnsPlaceholderOutput.Text.Trim()
+ $addSans = [bool]$chkWacsAddSans.IsChecked
+ $rawSans = $txtWacsSans.Text
+ $verbose = [bool]$chkWacsVerbose.IsChecked
+
+ $names = Get-NameItemsFromFile $listPath
+ $useFqdnFileName = [bool]$rbCsrFileFqdn.IsChecked
+ $items = Build-CsrItems $names $suffix $useFqdnFileName
+
+ if (-not (Test-Path -Path $wacsPath -PathType Leaf)) {
+ throw "WACS executable not found: $wacsPath"
+ }
+ if ([string]::IsNullOrWhiteSpace($pfxFolder)) { throw "PFX output folder is required." }
+ if (-not (Test-Path -Path $pfxFolder -PathType Container)) {
+ New-Item -Path $pfxFolder -ItemType Directory -Force | Out-Null
+ }
+
+ if ($createPlaceholders) {
+ if ([string]::IsNullOrWhiteSpace($placeholderOutput)) {
+ throw "Placeholder output folder is required when creating CSR placeholders."
+ }
+ if (-not (Test-Path -Path $placeholderOutput -PathType Container)) {
+ New-Item -Path $placeholderOutput -ItemType Directory -Force | Out-Null
+ }
+ foreach ($item in $items) {
+ $safeName = Get-SafeFileName $item.FileBase
+ $outPath = Join-Path -Path $placeholderOutput -ChildPath ($safeName + ".csr")
+ Set-Content -Path $outPath -Value "" -Encoding ascii
+ Write-Log "Created placeholder CSR: $outPath"
+ }
+ }
+
+ if ($updateDns) {
+ if ([string]::IsNullOrWhiteSpace($dnsZone)) { throw "DNS zone is required." }
+ if ([string]::IsNullOrWhiteSpace($targetIp)) { throw "Target IP is required." }
+ if ([string]::IsNullOrWhiteSpace($dnsServer)) { throw "DNS server is required." }
+
+ foreach ($item in $items) {
+ $fqdn = $item.Fqdn
+ if ([string]::IsNullOrWhiteSpace($fqdn)) { continue }
+
+ if ($fqdn.ToLower().EndsWith(".$($dnsZone.ToLower())")) {
+ $hostname = $fqdn.Substring(0, $fqdn.Length - $dnsZone.Length - 1)
+ } else {
+ $hostname = $fqdn
+ }
+
+ try {
+ Add-DnsServerResourceRecordA `
+ -Name $hostname `
+ -ZoneName $dnsZone `
+ -IPv4Address $targetIp `
+ -ComputerName $dnsServer `
+ -ErrorAction Stop
+ Write-Log "DNS record added: $hostname.$dnsZone -> $targetIp"
+ } catch {
+ Write-Log "DNS record failed for ${fqdn}: $($_.Exception.Message)" "WARN"
+ }
+ }
+ }
+
+ foreach ($item in $items) {
+ $hostValue = $item.Fqdn
+ if ([string]::IsNullOrWhiteSpace($hostValue)) { continue }
+
+ $args = @(
+ "--target", $target,
+ "--host", $hostValue,
+ "--store", $store,
+ "--pfxfilepath", $pfxFolder,
+ "--baseuri", $baseUri,
+ "--validation", $validation,
+ "--validationport", $validationPort
+ )
+
+ if ($addSans) {
+ $sanValues = Get-SanValues $item.HostLabel $suffix $rawSans
+ foreach ($san in $sanValues) {
+ $args += @("--san", $san)
+ }
+ }
+
+ if ($verbose) { $args += "--verbose" }
+
+ Write-Log "Running WACS for $hostValue"
+ Write-Log "& $wacsPath $($args -join ' ')"
+
+ try {
+ & $wacsPath @args
+ Write-Log "Completed: $hostValue"
+ } catch {
+ Write-Log "WACS failed for ${hostValue}: $($_.Exception.Message)" "WARN"
+ }
+ }
+
+ Write-Log "WACS processing completed."
+ } catch {
+ Write-Log "WACS processing failed: $($_.Exception.Message)" "ERROR"
+ }
+})
+
+$window.ShowDialog() | Out-Null