昨天寫好的網站 TLS 憑證 CLI 快速檢視工具只算是原型機,在工作環境 EXE 檔使用前需先部署,易有病毒木馬疑慮,應用阻力較大。相形之下,腳本形式的 PowerShell 傳送方便(可以貼在 Word 或 Email 裡),程式為明文可直接審閱,從管理稽查角度較易接受,是更可行的方案。因此 PowerShell 版才是最終量產的實用版本,但我通常會先用熟悉順手的 C# 完成雛型再轉 PowerShell 版,事半功倍。

Github Copilot Labs 包含程式語言翻譯功能(Language Translation),這回 C# 轉 PowerShell 的任務就交給它,實測一下效果。

選取程式碼後,點左側的飛行員圖示[1],找到 Language Translation,程式碼會自動帶入[2],從下拉選單選取要轉換成的程式語言 PowerShell [3],按下【Ask Copilot】就能開始轉換:

翻譯結果會顯示在下方視窗:

PowerShell 不算熱門語言,訓練資料量不比 Python、JavaScript,翻譯品質略差在預期內,以下是原始翻譯結果。

$hostPort = $args[0];
 #
if (!$hostPort.Contains(":")) {
 #
    $hostPort+=":443";
 #
} #
$p = Start-Process "openssl" -ArgumentList "s_client -showcerts -connect $hostPort" -PassThru -NoNewWindow -Wait -RedirectStandardOutput $true -RedirectStandardInput $true -RedirectStandardError $true;
 #
$p.StandardInput.WriteLine("Q"); #按Q+Enter結束openssl
 #
$output = $p.StandardOutput.ReadToEnd();
 #
$p.WaitForExit();
 #
$highlightCN = {param($s)
 #
    $m = [Regex]::Match($s, '^CN=[^,]+');
 #
    if (!$m.Success) {return $s;}
 #
    return "$([char]0x1b)[33;1m" + $m.Value + "$([char]0x1b)[0m" + $s.Substring($m.Value.Length);
 #
};
 #
foreach ($certB64 in [Regex]::Matches($output, "(?ims)-----BEGIN CERTIFICATE-----(?<c>.+?)-----END CERTIFICATE-----"))
 #
{
 #
    $pem = $certB64.Groups["c"].Value.Replace("`r", "").Replace("`n", "");
 #
    $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2([Convert]::FromBase64String($pem));
 #
    Write-Host "$([char]0x1b)[36m$([char]0x1b)[0m" -NoNewline;
 #
    Write-Host "$([char]0x1b)[36m$([char]0x1b)[0m".PadRight(64,"=");
 #
    Write-Host "發給: $($highlightCN.Invoke($cert.Subject))";
 #
    Write-Host "簽發者: $($highlightCN.Invoke($cert.Issuer))";
 #
    Write-Host "有效期自 $([char]0x1b)[33m$($cert.NotBefore.ToString("yyyy-MM-dd HH:mm:ss"))$([char]0x1b)[0m 到 $([char]0x1b)[33m$($cert.NotAfter.ToString("yyyy-MM-dd HH:mm:ss"))$([char]0x1b)[0m";
 #
    Write-Host "憑證指紋: $([char]0x1b)[33;3m$($cert.Thumbprint)$([char]0x1b)[0m";
 #
    Write-Host "";
 #
} #

Copilot 翻譯的版本有幾個問題:

  1. args[0] 被硬翻成 $arg[0],對應 PowerShell 應宣告 Param([string]$hostPort) 才順
  2. Start-Process 雖有 -RedirectStandardInput、-RedirectStandardOutput 等參數,但須事先將輸入內容寫成檔案及從輸出檔案讀取結果,後面的 $p.StandardInput.WriteLine("Q")、$p.StandardOutput.ReadToEnd() 屬無效寫法。 我想維持資料不落地,仍得依賴 System.Diagnostics.Process。參考
  3. New-Object System.Security.Cryptography.X509Certificates.X509Certificate2([Convert]::FromBase64String($pem)) 寫法傳入 byte[] 當建構式參數,byte[1200] 會被當成傳入 1200 個 byte 型別參數,正確做法是寫成 @(,$certBinary) 強制當成單一 byte[] 型別參數;或是改用 PS5 支援的 ::new 寫法 [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certBinary) 參考

微調後的版本如下:

param (
    [string]$hostPort
)
$ErrorActionPreference = "Stop"
if (!$hostPort) { $hostPort = 'www.google.com' }
if (!$hostPort.Contains(":")) {
    $hostPort+=":443";
} 
$p = New-Object System.Diagnostics.Process
$p.StartInfo.FileName = "openssl.exe"
$p.StartInfo.Arguments = "s_client -showcerts -connect $hostPort"
$p.StartInfo.UseShellExecute = $false
$p.StartInfo.RedirectStandardOutput = $true
$p.StartInfo.RedirectStandardInput = $true
$p.StartInfo.RedirectStandardError = $true
$p.Start() | Out-Null
$p.StandardInput.WriteLine("Q") #按Q+Enter結束openssl
$output = $p.StandardOutput.ReadToEnd();
$p.WaitForExit();
$highlightCN = {param($s)
    $m = [Regex]::Match($s, '^CN=[^,]+');
    if (!$m.Success) {return $s;}
    return "$([char]0x1b)[33;1m" + $m.Value + "$([char]0x1b)[0m" + $s.Substring($m.Value.Length);
};
foreach ($certB64 in [Regex]::Matches($output, "(?ims)-----BEGIN CERTIFICATE-----(?<c>.+?)-----END CERTIFICATE-----"))
{
    $pem = $certB64.Groups["c"].Value.Replace("`r", "").Replace("`n", "");
    $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new([Convert]::FromBase64String($pem))
    Write-Host "$([char]0x1b)[36m$([char]0x1b)[0m" -NoNewline;
    Write-Host "$([char]0x1b)[36m$([char]0x1b)[0m".PadRight(64,"=");
    Write-Host "發給: $($highlightCN.Invoke($cert.Subject))";
    Write-Host "簽發者: $($highlightCN.Invoke($cert.Issuer))";
    Write-Host "有效期自 $([char]0x1b)[33m$($cert.NotBefore.ToString("yyyy-MM-dd HH:mm:ss"))$([char]0x1b)[0m 到 $([char]0x1b)[33m$($cert.NotAfter.ToString("yyyy-MM-dd HH:mm:ss"))$([char]0x1b)[0m";
    Write-Host "憑證指紋: $([char]0x1b)[33;3m$($cert.Thumbprint)$([char]0x1b)[0m";
    Write-Host "";
}

使用 git diff -U0 ./show-cert.ps1 統計所有修改的行數並不多(忽略無效 # 註解),而該解該查的問題純手刻也得花時間查,算算省下蠻多時間。

但我覺得使用 AI 輔助開發,仍需具備識別及驗證結果正確優劣的基本能力,才能充分發揮其威力,用到開心盡興。AI 生成結果不可能 100% 沒有錯誤,若缺乏基本知識背景抓不出問題,連為什麼能動跟為什麼不能動都沒頭緒,只會得到滿滿的挫折吧!

原本就會寫 PowerShell 用 Github Copilot 翻譯 C# 程式,基本上會用得很開心,輕鬆搞定。

Experience of using Github Copilot Labs to translate C# to PowerShell.


Comments

# by 小黑

問下哪個開發工具會有PowerShell 的輸入提示

# by froce

VSCODE有extension,再不行PS也有自帶的ISE

# by 小黑

to froce, 感謝 ~

# by 閔峻承

版主請問?執行此 PowerShell 要有什麼環境, OS: Win10 執行會報錯! PS D:\Tej_Var\Src\Check_Tomcat> cd D:\Test\MaildSSLPackage PS D:\Test\MaildSSLPackage> .\show-certs.ps1 以 "0" 引數呼叫 "Start" 時發生例外狀況: "系統找不到指定的檔案。" 位於 D:\Test\MaildSSLPackage\show-certs.ps1:21 字元:1 + $p.Start() | Out-Null + ~~~~~~~~~~~~~~~~~~~~~ + CategoryInfo : NotSpecified: (:) [], ParentContainsErrorRecordException + FullyQualifiedErrorId : Win32Exception PS D:\Test\MaildSSLPackage> .\openssl.exe -showcerts www.google.com WARNING: can't open config file: /usr/local/ssl/openssl.cnf openssl:Error: '-showcerts' is an invalid command. Standard commands asn1parse ca ciphers cms crl crl2pkcs7 dgst dh dhparam dsa dsaparam ec ecparam enc engine errstr gendh gendsa genpkey genrsa nseq ocsp passwd pkcs12 pkcs7 pkcs8 pkey pkeyparam pkeyutl prime rand req rsa rsautl s_client s_server s_time sess_id smime speed spkac srp ts verify version x509

# by Jeffrey

to 閔峻承,推測你沒共用 openssl.exe 及加入環境變數 PATH 的尋找路徑(檢測方法:在 CMD 用 where openssl 指令要能直接找到 openssl.exe 所在位置)。若你不是用 Git for Windows 或 Cmder 的 openssl 而是將其放在 show-certs.ps1 同目錄,一個簡單解法是改成 $p.StartInfo.FileName = "$PSScriptRoot\openssl.exe"

# by 閔峻承

版主: 謝謝回覆.成功了.

Post a comment