diff --git a/certy.ps1 b/certy.ps1 index 6d91121..daa6c80 100644 --- a/certy.ps1 +++ b/certy.ps1 @@ -234,6 +234,62 @@ function Get-DefaultValue { return $prop.Value } +function Get-HostsFromInfLines { + param([string[]]$Lines) + $set = New-Object System.Collections.Generic.HashSet[string] ([System.StringComparer]::OrdinalIgnoreCase) + if (-not $Lines) { return @() } + + foreach ($line in $Lines) { + if ([string]::IsNullOrWhiteSpace($line)) { continue } + if ($line.TrimStart() -match '^[;#]') { continue } + $matches = [regex]::Matches($line, '(?i)\bdns\s*=\s*([^&",\s]+)') + foreach ($match in $matches) { + $value = $match.Groups[1].Value.Trim() + if ($value) { [void]$set.Add($value) } + } + } + + if ($set.Count -gt 0) { return $set | Sort-Object } + + $subjectLine = $Lines | Where-Object { $_ -match '(?i)^\s*subject\s*=' } | Select-Object -First 1 + if ($subjectLine -and ($subjectLine -match '(?i)\bCN\s*=\s*([^,"]+)')) { + $cn = $Matches[1].Trim() + if ($cn) { [void]$set.Add($cn) } + } + + return $set | Sort-Object +} + +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 + ) + $dir = Join-Path $env:ProgramData "Certy\\inf-sanitized" + 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 Resolve-HostEntry { param( [string]$Name, @@ -712,6 +768,22 @@ $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 + $useFqdnBox = Add-CheckBox "Input contains FQDNs (otherwise default zone is appended)" $xInput $y $inputWidth $rowHeight $y += $rowHeight + $gap @@ -909,6 +981,8 @@ $sectionAnchors = @{ Logs = $sectionActivity } +$script:infRequests = @() + foreach ($key in $sectionAnchors.Keys) { $label = $navLabels[$key] if (-not $label) { continue } @@ -996,7 +1070,7 @@ function Show-HelpDialog { $stepY = 28 $steps = @( - "Add hostnames (one per line, CSV, or CSR folder) then click 1) Preview.", + "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.", @@ -1192,6 +1266,9 @@ function Apply-Layout { $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 $zoneBox.Width = $inputWidthCalc $ipBox.Width = $inputWidthCalc - ($buttonWidth + $buttonGap) $ipRefreshBtn.Left = $xInput + $inputWidthCalc - $buttonWidth @@ -1276,6 +1353,13 @@ $csrBrowseBtn.Add_Click({ } }) +$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() @@ -1298,6 +1382,58 @@ $csrImportBtn.Add_Click({ } }) +$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 + $hosts = @(Get-HostsFromInfLines -Lines $lines) + $sanitize = Remove-InfSubjectLines -Lines $lines + $sanitizedPath = Save-SanitizedInf -FileName $infFile.Name -Lines $sanitize.Lines + if ($sanitize.Removed) { $subjectRemovedCount++ } + + if ($hosts.Count -eq 0) { + & $logAction "INF $($infFile.Name): no hostnames detected." + continue + } + + $script:infRequests += [pscustomobject]@{ + File = $infFile.FullName + Hosts = $hosts + Sanitized = $sanitizedPath + } + $infHosts += $hosts + } + + $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() @@ -1506,7 +1642,29 @@ $runBtn.Add_Click({ if (-not (Test-Path -Path $wacsPath -PathType Leaf)) { throw "WACS not found at: $wacsPath" } - if ($perHostBox.Checked) { + 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 ` @@ -1522,7 +1680,7 @@ $runBtn.Add_Click({ -Log $logAction } } else { - $hostList = @($hostEntries | ForEach-Object { $_.Fqdn } | Where-Object { $_ }) + $hostList = @($hostEntries | ForEach-Object { $_.Fqdn } | Where-Object { $_ }) & $logAction "Requesting one certificate with $($hostList.Count) hostname(s)." Invoke-Wacs ` -WacsPath $wacsPath `