Files
cert-management/certy.ps1
2026-01-21 03:13:29 +00:00

1019 lines
46 KiB
PowerShell

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 = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Certy - Certificate Helper" Height="820" Width="1040"
WindowStartupLocation="CenterScreen" UseLayoutRounding="True">
<Window.Resources>
<SolidColorBrush x:Key="PrimaryBrush" Color="#1E4D8C"/>
<SolidColorBrush x:Key="AccentBrush" Color="#3CB371"/>
<SolidColorBrush x:Key="WarmBrush" Color="#F4A261"/>
<SolidColorBrush x:Key="SurfaceBrush" Color="#F6F8FB"/>
<SolidColorBrush x:Key="BorderBrush" Color="#D6DEE8"/>
<Style TargetType="GroupBox">
<Setter Property="Margin" Value="0,0,0,14"/>
<Setter Property="BorderBrush" Value="{StaticResource BorderBrush}"/>
<Setter Property="Background" Value="White"/>
<Setter Property="Padding" Value="6"/>
</Style>
<Style TargetType="Button">
<Setter Property="Padding" Value="10,6"/>
<Setter Property="Margin" Value="0,4,0,4"/>
</Style>
<Style TargetType="TextBox">
<Setter Property="Margin" Value="0,4,8,4"/>
</Style>
</Window.Resources>
<Grid Background="{StaticResource SurfaceBrush}" Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Grid.Row="0" CornerRadius="10" Background="{StaticResource PrimaryBrush}" Padding="14" Margin="0,0,0,12">
<DockPanel LastChildFill="True">
<Button Name="btnOpenSettings" Content="Settings" DockPanel.Dock="Right" Background="{StaticResource WarmBrush}" Foreground="White" Padding="12,6" Margin="8,0,0,0"/>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left" Margin="0,0,12,0">
<Border Width="44" Height="44" Background="{StaticResource AccentBrush}" CornerRadius="8">
<Viewbox Stretch="Uniform">
<Canvas Width="32" Height="32">
<Ellipse Width="28" Height="28" Canvas.Left="2" Canvas.Top="2" Fill="White"/>
<Rectangle Width="12" Height="12" Canvas.Left="10" Canvas.Top="10" Fill="{StaticResource PrimaryBrush}"/>
</Canvas>
</Viewbox>
</Border>
</StackPanel>
<StackPanel>
<TextBlock Text="Certy - Certificate Helper" FontSize="22" FontWeight="Bold" Foreground="White"/>
<TextBlock Text="Step-by-step tools for requests, DNS, and renewals." Foreground="#E6EEF8" Margin="0,2,0,0"/>
</StackPanel>
</DockPanel>
</Border>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<StackPanel Visibility="Collapsed">
<TextBox Name="txtSettingsPath"/>
<TextBox Name="txtLogPath"/>
</StackPanel>
<GroupBox Header="Step 1: What do you need to do?">
<StackPanel Margin="6">
<TextBlock Text="Pick one path below. The screen will guide you step-by-step." Foreground="#5B6B7C" Margin="2,0,0,8"/>
<StackPanel Orientation="Horizontal">
<RadioButton Name="rbFlowCsr" Content="Create CSR placeholders + DNS + WACS" Margin="0,0,20,0" IsChecked="True"/>
<RadioButton Name="rbFlowInf" Content="Generate INF files from a list"/>
</StackPanel>
</StackPanel>
</GroupBox>
<StackPanel Name="pnlCsrFlow">
<GroupBox Header="Step 2: Build a CSR List">
<StackPanel Margin="4">
<TextBlock Text="Load a .txt list and choose how names should look before you start." Foreground="#5B6B7C" Margin="2,0,0,6"/>
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="110"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="List of names (.txt)" Grid.Row="0" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtDnsListInputPath" Grid.Row="0" Grid.Column="1"/>
<Button Name="btnBrowseDnsListInput" Grid.Row="0" Grid.Column="2" Content="Browse"/>
<TextBlock Text="If missing a domain, add" Grid.Row="1" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<ComboBox Name="cmbCsrDomainSuffix" Grid.Row="1" Grid.Column="1" Margin="0,4,8,4"/>
<TextBlock Text="CSR file name style" Grid.Row="2" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal">
<RadioButton Name="rbCsrFileFqdn" Content="Use full FQDN" Margin="0,0,12,0" IsChecked="True"/>
<RadioButton Name="rbCsrFileHost" Content="Use device name only"/>
</StackPanel>
<TextBlock Text="Save blank CSR files to" Grid.Row="3" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtDnsPlaceholderOutput" Grid.Row="3" Grid.Column="1"/>
<Button Name="btnBrowseDnsPlaceholderOutput" Grid.Row="3" Grid.Column="2" Content="Browse"/>
<CheckBox Name="chkDnsCreatePlaceholders" Grid.Row="4" Grid.Column="1" Content="Create blank CSR files" Margin="0,6,0,0"/>
<Button Name="btnPreviewCsr" Grid.Row="5" Grid.Column="2" Content="Preview" Width="110" HorizontalAlignment="Right" Margin="0,8,0,0"/>
<TextBox Name="txtCsrPreview" Grid.Row="6" Grid.Column="0" Grid.ColumnSpan="3" Height="90"
Margin="0,6,0,0" AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" IsReadOnly="True"/>
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="Step 3: DNS Update (optional)">
<StackPanel Margin="4">
<TextBlock Text="Update DNS for each FQDN if needed." Foreground="#5B6B7C" Margin="2,0,0,6"/>
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="110"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="DNS zone (domain)" Grid.Row="0" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtDnsZone" Grid.Row="0" Grid.Column="1"/>
<TextBlock Text="Target IP address" Grid.Row="1" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtDnsTargetIp" Grid.Row="1" Grid.Column="1"/>
<TextBlock Text="DNS server name" Grid.Row="2" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtDnsServer" Grid.Row="2" Grid.Column="1"/>
<CheckBox Name="chkDnsUpdate" Grid.Row="3" Grid.Column="1" Content="Update DNS A records" Margin="0,6,0,0" IsChecked="True"/>
</Grid>
</StackPanel>
</GroupBox>
<GroupBox Header="Step 4: Request Certificates (WACS)">
<StackPanel Margin="4">
<TextBlock Text="This runs WACS for each name and saves output files." Foreground="#5B6B7C" Margin="2,0,0,6"/>
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="110"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="WACS app location" Grid.Row="0" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtWacsPath" Grid.Row="0" Grid.Column="1"/>
<Button Name="btnBrowseWacsPath" Grid.Row="0" Grid.Column="2" Content="Browse"/>
<TextBlock Text="Output folder" Grid.Row="1" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtWacsPfxFolder" Grid.Row="1" Grid.Column="1"/>
<Button Name="btnBrowseWacsPfxFolder" Grid.Row="1" Grid.Column="2" Content="Browse"/>
<TextBlock Text="Output format" Grid.Row="2" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtWacsStore" Grid.Row="2" Grid.Column="1"/>
<TextBlock Text="ACME server URL" Grid.Row="3" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtWacsBaseUri" Grid.Row="3" Grid.Column="1"/>
<TextBlock Text="Validation method" Grid.Row="4" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtWacsValidation" Grid.Row="4" Grid.Column="1"/>
<TextBlock Text="Validation port" Grid.Row="5" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtWacsValidationPort" Grid.Row="5" Grid.Column="1"/>
<TextBlock Text="Request type" Grid.Row="6" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtWacsTarget" Grid.Row="6" Grid.Column="1"/>
<CheckBox Name="chkWacsAddSans" Grid.Row="7" Grid.Column="1" Content="Add extra SANs (optional)" Margin="0,6,0,0"/>
<TextBox Name="txtWacsSans" Grid.Row="8" Grid.Column="0" Grid.ColumnSpan="3" Height="70"
Margin="0,6,0,0" AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"/>
<StackPanel Grid.Row="9" Grid.Column="1" Orientation="Horizontal" Margin="0,4,0,4">
<CheckBox Name="chkWacsVerbose" Content="Show extra details"/>
</StackPanel>
<Button Name="btnRunWacs" Grid.Row="10" Grid.Column="2" Content="Start" Width="110" Background="{StaticResource AccentBrush}" Foreground="White" HorizontalAlignment="Right" Margin="0,8,0,0"/>
</Grid>
</StackPanel>
</GroupBox>
</StackPanel>
<StackPanel Name="pnlInfFlow">
<GroupBox Header="Step 2: Generate INF Files">
<StackPanel Margin="4">
<TextBlock Text="Use this when a request needs INF files (with SANs if needed)." Foreground="#5B6B7C" Margin="2,0,0,6"/>
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="110"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="List of names (.txt)" Grid.Row="0" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtInfInputPath" Grid.Row="0" Grid.Column="1"/>
<Button Name="btnBrowseInfInput" Grid.Row="0" Grid.Column="2" Content="Browse"/>
<TextBlock Text="Save INF files to" Grid.Row="1" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<TextBox Name="txtInfOutputDir" Grid.Row="1" Grid.Column="1"/>
<Button Name="btnBrowseInfOutput" Grid.Row="1" Grid.Column="2" Content="Browse"/>
<TextBlock Text="Template preset" Grid.Row="2" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<ComboBox Name="cmbInfTemplatePreset" Grid.Row="2" Grid.Column="1" Margin="0,4,8,4"/>
<TextBlock Text="Primary domain suffix" Grid.Row="3" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Center"/>
<ComboBox Name="cmbInfPrimaryDomain" Grid.Row="3" Grid.Column="1" Margin="0,4,8,4"/>
<TextBlock Text="Extra SAN domains (one per line)" Grid.Row="4" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Top"/>
<TextBox Name="txtInfAdditionalDomains" Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2"
Margin="0,4,0,4" Height="70" AcceptsReturn="True"
TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"/>
<CheckBox Name="chkInfAdditionalSans" Grid.Row="5" Grid.Column="1" Content="Add SANs from the domain list" Margin="0,6,0,0"/>
<TextBlock Text="Template (advanced)" Grid.Row="6" Grid.Column="0" Margin="0,4,8,4" VerticalAlignment="Top"/>
<TextBox Name="txtInfTemplate" Grid.Row="6" Grid.Column="1" Grid.ColumnSpan="2"
Margin="0,4,0,4" Height="170" AcceptsReturn="True"
TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"/>
<Button Name="btnRunInf" Grid.Row="7" Grid.Column="2" Content="Start" Width="110" Background="{StaticResource AccentBrush}" Foreground="White" HorizontalAlignment="Right" Margin="0,8,0,0"/>
</Grid>
</StackPanel>
</GroupBox>
</StackPanel>
<GroupBox Header="What Happened (Logs)">
<StackPanel Margin="4">
<TextBlock Text="Live notes show here and in the log file." Foreground="#5B6B7C" Margin="2,0,0,6"/>
<Grid Margin="4">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,0,0,8">
<Button Name="btnClearLog" Content="Clear Screen Log" Width="150" Background="{StaticResource WarmBrush}" Foreground="White"/>
</StackPanel>
<TextBox Name="txtLogOutput" Grid.Row="1" Height="180" AcceptsReturn="True" TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" IsReadOnly="True"/>
</Grid>
</StackPanel>
</GroupBox>
</StackPanel>
</ScrollViewer>
</Grid>
</Window>
'@
$settingsXaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Certy Settings" Height="380" Width="540"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize" UseLayoutRounding="True">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="90"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Settings file" Grid.Row="0" Grid.Column="0" Margin="0,6,8,6" VerticalAlignment="Center"/>
<TextBox Name="txtSettingsPathWin" Grid.Row="0" Grid.Column="1" Margin="0,6,8,6"/>
<Button Name="btnBrowseSettingsWin" Grid.Row="0" Grid.Column="2" Content="Browse" Margin="0,6,0,6"/>
<TextBlock Text="Activity log file" Grid.Row="1" Grid.Column="0" Margin="0,6,8,6" VerticalAlignment="Center"/>
<TextBox Name="txtLogPathWin" Grid.Row="1" Grid.Column="1" Margin="0,6,8,6"/>
<Button Name="btnBrowseLogWin" Grid.Row="1" Grid.Column="2" Content="Browse" Margin="0,6,0,6"/>
<TextBlock Text="FQDN suffix list (one per line)" Grid.Row="2" Grid.Column="0" Margin="0,6,8,6" VerticalAlignment="Top"/>
<TextBox Name="txtFqdnListWin" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Height="80" Margin="0,6,0,6"
AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"/>
<StackPanel Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" Orientation="Horizontal" HorizontalAlignment="Left" Margin="0,10,0,0">
<Button Name="btnSaveSettingsWin" Content="Save These Settings" Width="160" Margin="0,0,8,0"/>
<Button Name="btnLoadSettingsWin" Content="Load Saved Settings" Width="160"/>
</StackPanel>
<StackPanel Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,0,0">
<Button Name="btnCloseSettingsWin" Content="Close" Width="90"/>
</StackPanel>
</Grid>
</Window>
'@
[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