From 848844fc994bef0eab71c92df56b4c073482676b Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 11 Jun 2026 15:47:07 -0400 Subject: [PATCH 1/5] chore: update workflow --- .github/workflows/psscriptanalyzer.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/psscriptanalyzer.yml b/.github/workflows/psscriptanalyzer.yml index 3f94b3f..5123833 100644 --- a/.github/workflows/psscriptanalyzer.yml +++ b/.github/workflows/psscriptanalyzer.yml @@ -3,13 +3,15 @@ name: Run PSScriptAnalyzer on: push: branches: [main] + pull_request: + branches: [main] jobs: check: name: Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Run PSScriptAnalyzer uses: microsoft/psscriptanalyzer-action@v1.1 @@ -19,6 +21,6 @@ jobs: output: results.sarif - name: Upload SARIF results file - uses: github/codeql-action/upload-sarif@v3 + uses: github/codeql-action/upload-sarif@v4 with: sarif_file: results.sarif From 10c0254b2a032c73be1bf53854cec52765d8b7a9 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 11 Jun 2026 15:47:56 -0400 Subject: [PATCH 2/5] Update psscriptanalyzer.yml --- .github/workflows/psscriptanalyzer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/psscriptanalyzer.yml b/.github/workflows/psscriptanalyzer.yml index 5123833..b5941cf 100644 --- a/.github/workflows/psscriptanalyzer.yml +++ b/.github/workflows/psscriptanalyzer.yml @@ -2,7 +2,7 @@ name: Run PSScriptAnalyzer on: push: - branches: [main] + branches: [main, dev] pull_request: branches: [main] From b69e748cba7b3789aa63ff7164539e01a20d6e5c Mon Sep 17 00:00:00 2001 From: John Duprey Date: Thu, 11 Jun 2026 15:56:29 -0400 Subject: [PATCH 3/5] fix permissions --- .github/workflows/psscriptanalyzer.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/psscriptanalyzer.yml b/.github/workflows/psscriptanalyzer.yml index b5941cf..7ea5f08 100644 --- a/.github/workflows/psscriptanalyzer.yml +++ b/.github/workflows/psscriptanalyzer.yml @@ -6,12 +6,18 @@ on: pull_request: branches: [main] +permissions: + security-events: write + contents: read + jobs: check: name: Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Run PSScriptAnalyzer uses: microsoft/psscriptanalyzer-action@v1.1 From 033d47cf257de255e859999a301105427e37d495 Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:34:30 +0800 Subject: [PATCH 4/5] feat: ship SevenTiny RSA helper as a precompiled assembly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Get-RsaPublicKeyInfo currently compiles its embedded SevenTiny C# at runtime via `Add-Type -Language CSharp`. That works fine under stock pwsh, but in any host that embeds PowerShell as a library (Azure Functions custom-handler images, embedded SDK hosts, etc.) `$PSHOME` resolves to the host directory and Roslyn can't find its reference assemblies — Add-Type throws DirectoryNotFoundException and every Read-DkimRecord call silently returns empty records. This commit extracts the C# into Source/SevenTinyRsa/SevenTinyRsa.csproj and bundles the precompiled SevenTinyRsa.dll alongside the manifest. DNSHealth.psd1's new RequiredAssemblies entry preloads the type before the .psm1 runs; Get-RsaPublicKeyInfo's existing `if (!('SevenTiny.Bantina.Security.RSACommon' -as [type]))` guard then short-circuits, so the runtime Add-Type fallback stays in place untouched for any host that doesn't ship the DLL. Behaviour on a normal pwsh import is unchanged — the type just resolves to the bundled assembly instead of going through the compile step. Read-DkimRecord starts working on embedded-PS hosts where it previously failed silently. Build pipeline: dotnet build Source/SevenTinyRsa/SevenTinyRsa.csproj -c Release -o DNSHealth/. build.psd1's CopyDirectories now includes SevenTinyRsa.dll so Build-Module carries it into Output/DNSHealth/. --- DNSHealth/DNSHealth.psd1 | 10 +- DNSHealth/SevenTinyRsa.dll | Bin 0 -> 5120 bytes Source/SevenTinyRsa/.gitignore | 4 + Source/SevenTinyRsa/README.md | 44 ++++++++ Source/SevenTinyRsa/RSACommon.cs | 128 ++++++++++++++++++++++++ Source/SevenTinyRsa/SevenTinyRsa.csproj | 17 ++++ build.psd1 | 8 +- 7 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 DNSHealth/SevenTinyRsa.dll create mode 100644 Source/SevenTinyRsa/.gitignore create mode 100644 Source/SevenTinyRsa/README.md create mode 100644 Source/SevenTinyRsa/RSACommon.cs create mode 100644 Source/SevenTinyRsa/SevenTinyRsa.csproj diff --git a/DNSHealth/DNSHealth.psd1 b/DNSHealth/DNSHealth.psd1 index 1e969f2..1eb07d1 100644 --- a/DNSHealth/DNSHealth.psd1 +++ b/DNSHealth/DNSHealth.psd1 @@ -53,8 +53,14 @@ # Modules that must be imported into the global environment prior to importing this module #RequiredModules = @('') - # Assemblies that must be loaded prior to importing this module - # RequiredAssemblies = @() + # Assemblies that must be loaded prior to importing this module. + # Preloads the precompiled SevenTinyRsa.dll (Source/SevenTinyRsa/) so + # Get-RsaPublicKeyInfo finds [SevenTiny.Bantina.Security.RSACommon] + # already registered — its existing Add-Type guard then short-circuits + # via the `-as [type]` check. Required for hosts where runtime + # `Add-Type -Language CSharp` fails (e.g. embedded-PowerShell hosts + # with no $PSHOME/ref reference-assemblies directory). + RequiredAssemblies = @('SevenTinyRsa.dll') # Script files (.ps1) that are run in the caller's environment prior to importing this module. ScriptsToProcess = @() diff --git a/DNSHealth/SevenTinyRsa.dll b/DNSHealth/SevenTinyRsa.dll new file mode 100644 index 0000000000000000000000000000000000000000..1770c9c9f3e7e8a0a55e02b03a7ed2c77652c6a9 GIT binary patch literal 5120 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 CQzm2p literal 0 HcmV?d00001 diff --git a/Source/SevenTinyRsa/.gitignore b/Source/SevenTinyRsa/.gitignore new file mode 100644 index 0000000..9673e94 --- /dev/null +++ b/Source/SevenTinyRsa/.gitignore @@ -0,0 +1,4 @@ +# .NET build intermediates — the built DLL is committed under +# ../../DNSHealth/SevenTinyRsa.dll (see README.md). +bin/ +obj/ diff --git a/Source/SevenTinyRsa/README.md b/Source/SevenTinyRsa/README.md new file mode 100644 index 0000000..283d88c --- /dev/null +++ b/Source/SevenTinyRsa/README.md @@ -0,0 +1,44 @@ +# SevenTinyRsa + +Precompiled `[SevenTiny.Bantina.Security.RSACommon]` — the RSA public-key +parser that `Private/Get-RsaPublicKeyInfo.ps1` uses to decode DKIM keys. + +## Why this exists + +Upstream `Get-RsaPublicKeyInfo.ps1` ships the C# source inline and compiles +it at runtime via `Add-Type -TypeDefinition $source -Language CSharp`. That +works fine when DNSHealth is loaded by a normal `pwsh` process (which has +the .NET reference assemblies bundled at `$PSHOME/ref/`), but fails in any +host that embeds PowerShell as a library — `$PSHOME` resolves to the host's +own directory and Roslyn can't find the ref assemblies, so `Add-Type` +throws `DirectoryNotFoundException`. + +Bundling a precompiled DLL and listing it in `DNSHealth.psd1`'s +`RequiredAssemblies` registers the type before the module's runtime code +runs. `Get-RsaPublicKeyInfo`'s existing +`if (!('SevenTiny.Bantina.Security.RSACommon' -as [type]))` guard then +short-circuits and `Add-Type` is never reached. The inline source path +stays in place as a fallback for any future host that doesn't ship the +DLL. + +Behaviour on standard `pwsh` is unchanged — the type lookup just hits the +preloaded assembly instead of going through the runtime compile. + +## Rebuild + +```pwsh +cd Source/SevenTinyRsa +dotnet build SevenTinyRsa.csproj -c Release -o ../../DNSHealth/ +``` + +The output `SevenTinyRsa.dll` lands next to `DNSHealth.psd1`. +`build.psd1`'s `CopyDirectories` includes `SevenTinyRsa.dll`, so +`Build-Module` carries it into the built `Output/DNSHealth/` directory +beside the generated `.psm1`. + +## Provenance + +The C# source is verbatim from +[sevenTiny/Bamboo](https://github.com/sevenTiny/Bamboo) at +`10-Code/SevenTiny.Bantina/Security/RSACommon.cs`. The original +`Get-RsaPublicKeyInfo.ps1` cites the same source in its `.NOTES` block. diff --git a/Source/SevenTinyRsa/RSACommon.cs b/Source/SevenTinyRsa/RSACommon.cs new file mode 100644 index 0000000..a7b3571 --- /dev/null +++ b/Source/SevenTinyRsa/RSACommon.cs @@ -0,0 +1,128 @@ +// Sourced verbatim from DNSHealth 1.1.8's vendored inline C# (DNSHealth.psm1 +// Private/Get-RsaPublicKeyInfo.ps1, lines 71-183 of the built .psm1). +// +// Compiled to a DLL and preloaded via CRAFT's Worker.SharedAssemblies so the +// type [SevenTiny.Bantina.Security.RSACommon] is already registered by the +// time DNSHealth's runtime `Add-Type` check (`-as [type]`) runs in a cloned +// HTTP worker runspace. CRAFT embeds PowerShell as a library inside its own +// host (Craft.dll at /app), so $PSHOME resolves to /app and PowerShell's +// Add-Type can't find Roslyn reference assemblies at /app/ref — runtime +// CSharp compilation throws DirectoryNotFoundException. Preloading the +// type-equivalent assembly is the supported escape hatch. +// +// Origin: https://github.com/sevenTiny/Bamboo +// 10-Code/SevenTiny.Bantina/Security/RSACommon.cs + +using System; +using System.IO; +using System.Security.Cryptography; + +namespace SevenTiny.Bantina.Security { + public static class RSACommon { + public static RSA CreateRsaProviderFromPublicKey(string publicKeyString) + { + // encoded OID sequence for PKCS #1 rsaEncryption szOID_RSA_RSA = "1.2.840.113549.1.1.1" + byte[] seqOid = { 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00 }; + byte[] seq = new byte[15]; + + var x509Key = Convert.FromBase64String(publicKeyString); + + // --------- Set up stream to read the asn.1 encoded SubjectPublicKeyInfo blob ------ + using (MemoryStream mem = new MemoryStream(x509Key)) + { + using (BinaryReader binr = new BinaryReader(mem)) //wrap Memory Stream with BinaryReader for easy reading + { + byte bt = 0; + ushort twobytes = 0; + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + seq = binr.ReadBytes(15); //read the Sequence OID + if (!CompareBytearrays(seq, seqOid)) //make sure Sequence for OID is correct + return null; + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8103) //data read as little endian order (actual data order for Bit String is 03 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8203) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + bt = binr.ReadByte(); + if (bt != 0x00) //expect null byte next + return null; + + twobytes = binr.ReadUInt16(); + if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81) + binr.ReadByte(); //advance 1 byte + else if (twobytes == 0x8230) + binr.ReadInt16(); //advance 2 bytes + else + return null; + + twobytes = binr.ReadUInt16(); + byte lowbyte = 0x00; + byte highbyte = 0x00; + + if (twobytes == 0x8102) //data read as little endian order (actual data order for Integer is 02 81) + lowbyte = binr.ReadByte(); // read next bytes which is bytes in modulus + else if (twobytes == 0x8202) + { + highbyte = binr.ReadByte(); //advance 2 bytes + lowbyte = binr.ReadByte(); + } + else + return null; + byte[] modint = { lowbyte, highbyte, 0x00, 0x00 }; //reverse byte order since asn.1 key uses big endian order + int modsize = BitConverter.ToInt32(modint, 0); + + int firstbyte = binr.PeekChar(); + if (firstbyte == 0x00) + { //if first byte (highest order) of modulus is zero, don't include it + binr.ReadByte(); //skip this null byte + modsize -= 1; //reduce modulus buffer size by 1 + } + + byte[] modulus = binr.ReadBytes(modsize); //read the modulus bytes + + if (binr.ReadByte() != 0x02) //expect an Integer for the exponent data + return null; + int expbytes = (int)binr.ReadByte(); // should only need one byte for actual exponent data (for all useful values) + byte[] exponent = binr.ReadBytes(expbytes); + + // ------- create RSACryptoServiceProvider instance and initialize with public key ----- + var rsa = System.Security.Cryptography.RSA.Create(); + RSAParameters rsaKeyInfo = new RSAParameters + { + Modulus = modulus, + Exponent = exponent + }; + rsa.ImportParameters(rsaKeyInfo); + + return rsa; + } + } + } + + private static bool CompareBytearrays(byte[] a, byte[] b) + { + if (a.Length != b.Length) + return false; + int i = 0; + foreach (byte c in a) + { + if (c != b[i]) + return false; + i++; + } + return true; + } + } +} diff --git a/Source/SevenTinyRsa/SevenTinyRsa.csproj b/Source/SevenTinyRsa/SevenTinyRsa.csproj new file mode 100644 index 0000000..8ac743b --- /dev/null +++ b/Source/SevenTinyRsa/SevenTinyRsa.csproj @@ -0,0 +1,17 @@ + + + net8.0 + SevenTinyRsa + SevenTiny.Bantina.Security + disable + latest + Library + true + none + false + false + false + bin\ + false + + diff --git a/build.psd1 b/build.psd1 index 6d82c35..2f13f6c 100644 --- a/build.psd1 +++ b/build.psd1 @@ -5,5 +5,11 @@ # Subsequent relative paths are to the ModuleManifest OutputDirectory = '..\Output\' VersionedOutputDirectory = $false - CopyDirectories = @('MailProviders') + # SevenTinyRsa.dll is the precompiled [SevenTiny.Bantina.Security.RSACommon] + # helper used by Get-RsaPublicKeyInfo. Bundling it lets DNSHealth.psd1's + # RequiredAssemblies preload the type so the inline Add-Type fallback + # short-circuits — required on hosts where runtime CSharp compilation + # fails (no $PSHOME/ref). Source: Source/SevenTinyRsa/. CopyDirectories + # is ModuleBuilder's alias for CopyPaths, which accepts files too. + CopyDirectories = @('MailProviders', 'SevenTinyRsa.dll') } \ No newline at end of file From f00315fd988665472cdcf5c23e87bd1b8e136b0b Mon Sep 17 00:00:00 2001 From: Zacgoose <107489668+Zacgoose@users.noreply.github.com> Date: Sat, 13 Jun 2026 19:35:09 +0800 Subject: [PATCH 5/5] Update RSACommon.cs --- Source/SevenTinyRsa/RSACommon.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Source/SevenTinyRsa/RSACommon.cs b/Source/SevenTinyRsa/RSACommon.cs index a7b3571..a22d7f9 100644 --- a/Source/SevenTinyRsa/RSACommon.cs +++ b/Source/SevenTinyRsa/RSACommon.cs @@ -1,15 +1,6 @@ // Sourced verbatim from DNSHealth 1.1.8's vendored inline C# (DNSHealth.psm1 // Private/Get-RsaPublicKeyInfo.ps1, lines 71-183 of the built .psm1). // -// Compiled to a DLL and preloaded via CRAFT's Worker.SharedAssemblies so the -// type [SevenTiny.Bantina.Security.RSACommon] is already registered by the -// time DNSHealth's runtime `Add-Type` check (`-as [type]`) runs in a cloned -// HTTP worker runspace. CRAFT embeds PowerShell as a library inside its own -// host (Craft.dll at /app), so $PSHOME resolves to /app and PowerShell's -// Add-Type can't find Roslyn reference assemblies at /app/ref — runtime -// CSharp compilation throws DirectoryNotFoundException. Preloading the -// type-equivalent assembly is the supported escape hatch. -// // Origin: https://github.com/sevenTiny/Bamboo // 10-Code/SevenTiny.Bantina/Security/RSACommon.cs