From 51bf819c37e5438efea4a0d15f2ecfc44d4794d7 Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Thu, 4 Jun 2026 14:26:40 -0400 Subject: [PATCH] Request-InfisicalCertificate + PKI lifecycle, MAML help for all 39 cmdlets, chain-store routing fix Cmdlets added: Request-InfisicalCertificate, Get-InfisicalCertificate, Get-InfisicalCertificates. Request supports BySubscriber/ByCa parameter sets, BouncyCastle CSR generation (RSA/ECDSA/Ed25519), local-key generation, -Install/-InstallChain (chain certs routed to Root vs CertificateAuthority by self-signed status), idempotency reuse with -AllowRenewal/-RenewalThresholdDays, local chain reconstruction with -LocalChainOnly opt-out, Infisical bundle fallback when local stores are incomplete, and private-key protection modes (Exportable/LocalOnly/NonExportable/Ephemeral) via -PrivateKeyProtection plus -PersistKey/-MachineKey/-PrivateKeyPath. Install-InfisicalCertificate fix: chain certs were previously dumped into CertificateAuthority unconditionally. They are now routed by Subject==Issuer (self-signed -> Root, otherwise -> CertificateAuthority), matching Request-InfisicalCertificate. Routing centralized in InfisicalCertificateRequestHelpers.GetChainCertificateTargetStore and a new InstallChain(IEnumerable,...) overload. Help: authored Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml covering all 39 cmdlets (synopsis, description, notes, two examples per cmdlet: one-liner + OrderedDictionary splat with preceding Get- resolvers for IDs/slugs). Build pipeline: build.ps1 stages the help XML into bin// next to the DLL during publish (hard-fails if missing or has zero entries). Test-ModuleImports now enumerates every exported cmdlet via Get-Command, cross-checks against expected names, and asserts non-empty synopsis (rejecting auto-generated cmdlet-name fallback), non-empty description, and at least one example with a non-empty block. Tests: 230/230 passing (up from 190). --- CHANGELOG.md | 99 ++ Module/PSInfisicalAPI/PSInfisicalAPI.psd1 | 7 +- Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll | Bin 241664 -> 275968 bytes .../bin/en-US/PSInfisicalAPI.dll-Help.xml | 1531 +++++++++++++++++ .../en-US/PSInfisicalAPI.dll-Help.xml | 1531 +++++++++++++++++ build.ps1 | 81 +- .../CertificateMapperTests.cs | 38 + .../CsrAndRequestCmdletTests.cs | 479 ++++++ .../PemCertificateBuilderTests.cs | 5 + .../PkiClientParseTests.cs | 86 + .../PkiEndpointRegistryTests.cs | 61 + .../GetInfisicalCertificateAuthorityCmdlet.cs | 5 +- .../Cmdlets/GetInfisicalCertificateCmdlet.cs | 36 + .../Cmdlets/GetInfisicalCertificatesCmdlet.cs | 76 + .../InstallInfisicalCertificateCmdlet.cs | 3 +- .../RequestInfisicalCertificateCmdlet.cs | 204 +++ .../Endpoints/InfisicalEndpointNames.cs | 2 + .../Endpoints/InfisicalEndpointRegistry.cs | 44 + .../Models/InfisicalCertificateResult.cs | 23 + .../Models/InfisicalSignedCertificate.cs | 16 + src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs | 21 + src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs | 39 +- .../Pki/InfisicalCertificateRequestHelpers.cs | 338 ++++ src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs | 201 +++ .../Pki/InfisicalLocalCertificateLookup.cs | 85 + src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs | 182 +- .../Pki/InfisicalPrivateKeyProtection.cs | 10 + .../Pki/InfisicalSignCertificateDtos.cs | 33 + 28 files changed, 5192 insertions(+), 44 deletions(-) create mode 100644 Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml create mode 100644 Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml create mode 100644 src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs create mode 100644 src/PSInfisicalAPI.Tests/PkiClientParseTests.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatesCmdlet.cs create mode 100644 src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs create mode 100644 src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs create mode 100644 src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs create mode 100644 src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs create mode 100644 src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs create mode 100644 src/PSInfisicalAPI/Pki/InfisicalLocalCertificateLookup.cs create mode 100644 src/PSInfisicalAPI/Pki/InfisicalPrivateKeyProtection.cs create mode 100644 src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index da75758..6703ba2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,105 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased +## 2026.06.04.1825 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1820 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- `Install-InfisicalCertificate` now routes chain certificates by self-signed status instead of dumping every chain entry into the Intermediate Certification Authorities store. Self-signed roots are installed into `StoreName.Root` (Trusted Root Certification Authorities) and non-self-signed intermediates are installed into `StoreName.CertificateAuthority` (Intermediate Certification Authorities). The leaf continues to use the user-specified `-StoreName`/`-StoreLocation` (default `My`/`CurrentUser`). `Request-InfisicalCertificate` already routed chain certs correctly; the same routing helper is now shared by both cmdlets. +- `InfisicalCertificateRequestHelpers` exposes a new public `GetChainCertificateTargetStore(X509Certificate2)` classifier and a new `InstallChain(IEnumerable, StoreLocation, bool, IInfisicalLogger, string)` overload. The existing `InstallChain(InfisicalSignedCertificate, ...)` overload now delegates to the new collection-based overload, so PKI chain-installation routing is centralized in one place. + +## 2026.06.04.1810 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- Authored MAML help (`Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`) covering all 39 exported cmdlets. Every entry includes a synopsis, description, notes section, and two examples: a one-liner and an `OrderedDictionary` splat (with `OrdinalIgnoreCase`) that includes preceding `Get-` resolver commands wherever IDs or slugs are required. +- `build.ps1` now stages the cmdlet help XML next to the deployed binary. After the publish step, every culture directory under `Module/PSInfisicalAPI/` (matching `xx` or `xx-XX`) that contains `PSInfisicalAPI.dll-Help.xml` is mirrored into `bin//`. The script hard-fails if `bin/en-US/PSInfisicalAPI.dll-Help.xml` is missing or contains zero `` entries. +- `Test-ModuleImports` in `build.ps1` now dynamically enumerates exported cmdlets via `Get-Command -Module PSInfisicalAPI -CommandType Cmdlet`, cross-checks the result against an expected list of 39 cmdlet names (including the previously-missing `Copy-InfisicalSecret`), and for each cmdlet asserts that `Get-Help -Full` returns a non-empty synopsis (rejecting PowerShell's auto-generated cmdlet-name fallback), a non-empty description, and that `Get-Help -Examples` returns at least one example node whose `` block is non-empty. + +## 2026.06.04.1808 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1658 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- `Request-InfisicalCertificate` reuse path now falls back to the Infisical certificate-bundle endpoint when the local trust stores do not contain the issuing intermediates or root. The cmdlet builds the local chain first; if the result has no intermediates and no root, it fetches `GetCertificateBundle(serialNumber)` and rebuilds the result with the bundle's chain PEM merged in. A new `-LocalChainOnly` switch opts out of the bundle fetch for strict offline behavior. Bundle-fetch failures are logged at verbose level and the cmdlet returns the local-only result. +- `InfisicalCertificateRequestHelpers.BuildResultFromExistingLocal` adds a second overload that accepts an `InfisicalCertificateBundle`; when supplied, chain certs from the bundle are deduplicated by thumbprint and merged with the locally-resolved chain before classification. + +## 2026.06.04.1652 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1651 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1634 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1631 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1622 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +- **PKI contract fixes and cmdlet expansion**: + - `InfisicalPkiClient` no longer auto-injects `connection.ProjectId` into PKI CA list/retrieve calls; only the caller's explicit `-ProjectId` is forwarded so that cert-manager primary routes (which do not accept the query parameter) succeed. + - List/single CA and single certificate response parsing now tolerate raw arrays, wrapper objects (`{certificate: {...}}`, `{certificates: [...]}`), and nested `configuration` blocks. `InfisicalCaMapper` reads CA detail fields from `configuration` first, falling back to top-level. + - `RetrieveCertificate(connection, identifier)` added on `InfisicalPkiClient`. +- **New cmdlets**: + - **`Get-InfisicalCertificate`** — single-record retrieval by `-SerialNumber`/`-Id` (mandatory positional). + - **`Get-InfisicalCertificates`** — listing with light filtering (`-CommonName`, `-FriendlyName`, `-Status`, `-CaId`, `-Limit`, `-Offset`, `-NoAutoPage`). Auto-paginates by default. + - **`Request-InfisicalCertificate`** — generates a keypair locally (private key never leaves the device), submits a PKCS#10 CSR to either `pki-subscribers/{name}/sign-certificate` (`-PkiSubscriberSlug`) or `ca/{caId}/sign-certificate` (`-CertificateAuthorityId`), and returns a single `InfisicalCertificateResult` object with the leaf and chain pre-classified. The result exposes `Leaf : X509Certificate2`, `Intermediates : X509Certificate2[]`, `Root : X509Certificate2` (nullable), `Chain : X509Certificate2[]` (ordered leaf → intermediates → root, deduplicated by thumbprint), plus pass-through `SerialNumber`, `CertificatePem`, `CertificateChainPem`, and `PrivateKeyPem`. Supports `-Subject` (`IDictionary` with `CN`/`C`/`ST`/`L`/`O`/`OU`/`E` keys) merged with individual `-CommonName`/`-Country`/etc. parameters (individual params win), `-DnsName`/`-IpAddress` SANs (auto-populated from local FQDN when omitted). Idempotency: scans the local `X509Store` for an existing certificate matching `CN` and an Infisical-known serial number; returns the existing certificate wrapped in an `InfisicalCertificateResult` whose `Intermediates`/`Root`/`Chain` are populated by walking the local trust stores via `X509Chain` (no network calls, revocation checks disabled), and whose `CertificatePem`/`CertificateChainPem` are reconstructed from the resolved certs. Reuse is short-circuited unless `-Force` or `-AllowRenewal` (with optional `-RenewalThresholdDays`, default 30) requests a new one. Installation: `-Install` adds the leaf to `-StoreName`/`-StoreLocation` (default `My`/`CurrentUser`); `-InstallChain` additionally places intermediates into `CertificateAuthority` and self-signed roots into `Root` for the same `-StoreLocation`. `-KeyStorageFlags` is passed through to `X509Certificate2` import. + - **Multi-algorithm CSR support** on `Request-InfisicalCertificate` via split parameters: `-KeyAlgorithm` (`Rsa`/`Ecdsa`/`Ed25519`, default `Rsa`), `-KeySize` (`2048`/`3072`/`4096`, default `2048`, applies to RSA only), `-Curve` (`P256`/`P384`, default `P256`, applies to ECDSA only). Signature algorithms are picked automatically: SHA256WITHRSA for RSA, SHA256WITHECDSA / SHA384WITHECDSA for ECDSA P-256/P-384, and Ed25519 (pure-EdDSA) for Ed25519. The underlying `InfisicalCsrBuilder.Build(subject, dns, ip, options)` API was updated to take an `InfisicalCsrOptions` object in place of the prior `keySize` int. + - **Sign-certificate endpoint registrations**: `SignCertificateBySubscriber` and `SignCertificateByCa` registered with both `/api/v1/pki/...` and `/api/v1/cert-manager/...` candidate paths and marked `ContainsSecretMaterialInResponse = true`. + +## 2026.06.04.1554 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1512 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + +## 2026.06.04.1508 + +- Build produced from commit 19615363e356. + +## Unreleased (carried forward) + - **CI — Gitea artifact upload fix**: Replaced `actions/upload-artifact@v4` and `actions/download-artifact@v4` with the Gitea-compatible forks `christopherhx/gitea-upload-artifact@v4` and `christopherhx/gitea-download-artifact@v4` in `.gitea/workflows/publish-psgallery.yml`. The upstream v4 actions abort on Gitea because Gitea is detected as GHES, which the upstream v4 actions do not support (see [go-gitea/gitea#28853](https://github.com/go-gitea/gitea/issues/28853)). ## 2026.06.04.0123 diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 index e0de7f0..5ffa120 100644 --- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 +++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSInfisicalAPI.psm1' - ModuleVersion = '2026.06.04.0123' + ModuleVersion = '2026.06.04.1825' GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51' Author = 'Grace Solutions' CompanyName = 'Grace Solutions' @@ -41,7 +41,10 @@ 'Update-InfisicalTag', 'Remove-InfisicalTag', 'Get-InfisicalCertificateAuthority', + 'Get-InfisicalCertificate', + 'Get-InfisicalCertificates', 'Search-InfisicalCertificate', + 'Request-InfisicalCertificate', 'ConvertTo-InfisicalCertificate', 'Install-InfisicalCertificate', 'Uninstall-InfisicalCertificate', @@ -57,7 +60,7 @@ LicenseUri = 'https://www.gnu.org/licenses/agpl-3.0.html' ProjectUri = 'https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI' ReleaseNotes = 'See CHANGELOG.md in the project repository for release history.' - CommitHash = '2cbd5c2008f5' + CommitHash = '19615363e356' } } } \ No newline at end of file diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll index e4308f8e79234bfda8890d715ee41923a3fcb18f..634d6d87badd09d7fd2f64c4c144439ae966c096 100644 GIT binary patch literal 275968 zcmd442b^40`Nw_t&di;*n`E+^-A#dnz=oTh*^oj>LPv`9-i!1OC)@#qgoH?wj-Y^` zs30JA6bn`m8zL$wDk?+}6}w>l+rs;Oo^zj>yCntXeLwF)=KRj@^mCrm%ia5kw*Xj3tRXB{}Y_87CZf z_Sx>-8ONVABRTKv8K<2+W9NMjoN<;r@uWFjU6oBzulL&}2=?m81%G@i!BH_Z5J@ z&q+p%{9Bn1I^j0iGano@(rj(|89`9Wn3vGMKBJ#=?nxJ%i~gcUnl4Fe8gH|PlVj&3 z&Ey0KjT>uoNiXqS`W`7qZk z6u`tIKNw7%TMS+xSRB{H6Wphs^j~*A^gwU^u7SRsn-c}mU2zdlDpA}|AdPA=BzB*` zucgh&Faybv5Rsb#u7=Ab;Kacsm-6FY$_G)nOajd%FA6I^#hxA);X!A(OrlnDv(Px= zQtv9c2Dm(w>#0vDyQu^`FjSmXb{l}AcyoTULHxsl=+3X|pOrN^u!F$=Lg1^X)JSiK z%I03dMUMvkz>aRp_G&GQ|0Vqe%aS_?&1XrNi(;}Ms`hr)<6`OduCjZMSQMtlq@^-6 zzU+S1);7ZOV5#r0&QkIE)8>D-XE4xmnNLQLE&oHgwBq&sR=h?RRkJRcTX3^P8JBml zss%?Z+66;W>Rs7<{Rci4lGla7l6V>k8dkoiz$Wrj>?v#ccISNo*YduENmaeOfKJM| zR**uTSSz?067o=K7H}poiWl&k4HTjBlGGiIDCTd7Nm|SiG56(sRd9V@hnBO*`S#=| z0{B|wXEA5xJSsm2k|7KWz8W;=OOnV`>I!PXZKi7E=ApKvo=6SP+(qKU8Z!4%%o6!| ztuyyl{M2F`mATK0Njr0W-rf>izezi9e?%QB{C}IbS;3f^Uvq|?gjpg#*4n7lWnEv0 zp{9Ir^pKe4dm8mm=gCDlGBMb)M=`w5KP}nmVR(h-TiBa+P&XP6=Lt$yK*mqd9 z7*VZ%)ibzZxQsl?56%p8%N0Q2*b@pMXk$I00D^AK6AB>8Mks)&7@+{7YJ>s^x+(96 z0*Edn6l~~w5sBb9Gf)6A-UtO5n>3%^PX*Yl8KEF!bD|k2z-G4*3LtunP>^vt$qW== zlitpUssMr>%M%LBY4_Y*a1UieCZ=WM2t4h&`a?%JsV0}JkMsI^6so04^N%#m1EC|s zQ%j}Lk++f95Yj#GEs&d$d%h}Ri-IHfYfll~DBeQ&-+F>hG$noyA@VhK zZW!!=N(jU`iV>0eU~@xS*GlN7ORrk*D@r3-$!W{jS!xuclG_NPC=F(1==oKZVk0WL zjnO<4xlK^;Sn6vS^adL*3P?l{NS{Xs8X!Kk(Dz)Gi5;sa5#H3|U>FbJZ2Eo^gP0zn zLJ-YO#h7uB8r4^;TlYN@Z1lHDR!_q$urXC7#PqvH4Bg)ZO8I?py+6`D^0qdiu)QtL6BJ~@u-B| zsV+5>Tp1Hcxk1uv&HpROP%yWA(|nMR zmdWq1@WNf-)aUU_ABW!(isKRykEzTl5hht^fev&G~YRoLelm6JT%70azW6De!u*`G6c zb_+3i)p>T6Pg^U`UXqL;n>&lG47p~6l`*pQx)D~!$krRYl}xtYm@@5beXBB>tycmq zTc;71cDC-;&ep%K9+|DL*K7@Tq%wwN4V^?Vk^Gv?Kh0qD(rz4xO1D*ucZYB8!4KKH zCrY(GyXf`;bWRN8y-|IY?TJz?%094eUw+b7_&dW2*2;8Zu*GiH{^kN2#efc}mXjgF z1~Oy_OA~XBs$6MQ%03Jx1+CG;vrG!4{z>~mr&$Yg69aF%l;%ohVz_z|UQR6yMiV>Y z{qboi!xftczzlZJEd*!a2Nik@KN!PAeb-$UmMBU>s1*Pqrr2({YT+f|=l zD&Ou7Mki(Bqgg5<+J``u`wr_Wml*2&r05Ps-zXN{VW@*T@?nbT`)LJq+{_a_p#Xw5 z+!G2QC^JtefMD|H2?Y>zj-F5eK{x3M1rRfgPyn%!5egt?8leD!-qZV`0D^YP6AB=x zgq~0UG0O-A5OkT|h5`sWNKYt$psVzR0voqrK(p=*6hI&=o=^Zm_vr}*5Ok%UP>}J9 zUep^Xz$X2uClq9C&NTxC*qmpCf{abNNAHCKY%VZD0R;V}x1k{8lupkZD99Ml?|B0S zctHo~2?Y=cz9$qw&>wn20R%mqClo-?*Lgw#1ihXo6hP4Zc|t)ZKJKH=K%8NO0*Et>P>}KKEHhAm&9jYA0O5>K0I|df z1rX;Lp#UN=LIFh62n7)58lk}Aqy6jTjv$yMeWT9jrKgLAxh4|syw5V;QtZU;!aMSwl- zBTBpby-g%Kav%2?S&iH$JVq*`n6XXdKII97FeM)J1hN*n&w7khMRB*-{=6p;qA2bW z;)|X@R-$;45D!CyJ&xKJ4Q?4G_b~dJqRW`FejEm*m)WJ{SrnIa&Pudo{6xiJ{M1`? zM6D8!!>e@4N`0}x($9tsmK<@GDQj!aI&+~PGwl&3PZF$OJ#B=2gS|2y`d{a%y*O7c(5!yK{ zI++1&Q1f9#aJ(_g(N^Ituij&VL}%F&wQ|FxkSU zL6ER3K5WrlM+s=y4Z@~mHSEW{ztoe+v9u-ephiUQ8<{Y~@%(Q_vJ!Nj)e;2|=Nq8_ z;*CZqfVjX21rQe+p#Wm35egud8KD5;A|n()EH^>{#0n!6KwNBu0;|s$tTY1!5N|R< z0mLOnD1f-s2n7(A8KD5;aw8N#Tw#O)h&LOd0OCp`6hK^Mgn~?ruQme(*u2IF1rXO7 zp#b7KBNRYfZ-fGf8;no@@fIT#K-_4A0*IT8Pyq2(BNSv}{5CUCfX$nYPylg@5egt~ zH9`Tz+l^2FahnkeAZ|B80mM6uPyq2xBNRZ~VT1yRcNw7|lZ1DhfdXv4#|Q-wcN(Dp z;w~c;K)lxo1rT=|p&%35J!YT)oA(-_0OEZ{D1f-n2n7)DH$nl#2aHgV3GIVspa7d6 zGC~2whmBAG@ev~wKz!5)1rYZep#b7zMkvSx|8X-=fXxStPyq1>BNRY<(g+0*pE5#0 z#`8~`fdXtkXoLcY&lsTq;<#oMTVOoQ{vwFH+Yoz0#bj?Z^+IwuO7kS*<4)!$?`B{s z>q)1eYcz_<3F^k!Q(@Qab|!8clZwfka5Ku|O-6Y9fO>4~+C82%!tL|w*4VYX9h68l z@=nyQ-tK&Ta=Dhjy}M9MI?BP4T0!%&mhU@EmW|3R5k1E~v$#3PtL`+?+#S^$6QlTa zfyH9djmwEeU2{t|1?xUD#6Fm-f7m@kvg(_w#!ZY?_fBQl=661VaG6L|QpZ)Y8$W(` zI+uI~oOja*IC&D-Q|yUXq3JG#%S7^O=>dc)1F7yZI^CYuGYG|6*`3c|DV~7+toJXQ z_ucGPaPpuX@a~n??qSW3X{X5=pDN9fKWxz{(p;=XUYd(?*rHe~x=TcwWlolr*>mvHp8+V1 zkmj$8NqPpLWanst@X9OO@;edvMa0LD->yz;v-55zf*dWsY>vRwyl)^@pO9^~w(Pzh zqY<*3k?cLpv@Ms_RtQ5J4D#L;Ezo^iA*5$Fe5H|iX{G6GGH}&>lgM2GsuhavN>$t4 zz+M*SXc>1`tKGcY2Rj<3$hT;+^6jooZAr4-^{Hl8(Yz(q93UEo-$Wqq4iXI`Y&0Ch zk>V}8E-6?}*0h_PH<2`-!=t6;Nj2LKe-eSE9@g)*@%tad<+pe=wzzEHHB4O6vlC}` zT=jQ5Gg1mVk(UkDDKCc-V=KCLd)F3@H($c2**3rZr+`QotAKjW|(l?k=i;^449Ot2plaM<{zp#Tr^BIRCAhWm`$kW z4AC%gK+}>Gx|mYU2XqJh-hF+Wnxa$=>&InTjw$DByni(wtp z9@V@_G|X^RbE#;U%&6vaH1RXSd;!KKcu|WOg+(>Q?2cEBhW!$7>ZvXgXf2_#Un@_IT|lc;p4KI--YLG@&hs)j>+i z%i`xcZatvg$E`pXr>|2MKSY#87DZi?EZ${#>5ta_9BrmkyW33v3t7}qPivk_#v+TN z^Rmd|N=6nh^Rk%TR(~@&;Hylj$z)C?NX*^Cspe|YJd$dz6%CV7wRXK|n0Kn?Euwie z)!ZZ+W|M02HZ)!ynLS4Cypi&Fw8-PHF!J*FEJ)<>r>Yj+^QyMx@m{=JlRUnlcJuB7 z683<=c8tQ4yH+tC=oyul? zcB_%}y<&H%$*@Lch6RJjT`)pY=6|R8XLv6rztK{S-0xH^x<9De7Lx}o!ecaBFR9&< z`;)3I$*7HzjNK=SWL&r&l5sIM^X`)bn%oI&Nk)3^lys$2)30Kx#XX`W+PJ$_*zI;` zkB-wn(e4x9L_4(6I?MVi#6ikhR4MCo@OWfdf7UW*%y#JWX1bnb{pnT+dG|%!+Q{v% zExuhW51Zeki>0$YzI(*mD)~j0uAc6E-aQJ}U1Tjl7Ep#3cOl;d85h5+P)4D4YD{Jc~In9dQC-5@40f@jw z|MR-W$M9(Erl>lpC7rcQKl!bW+fKH&C?A|o?=lnl)V;B#Qc7|@+6ihdcrS@NmY zhd8T*Wj3gq^ajyDdaO!@DyrEBGe=j(VLlL+b;;*3mV7$0OFniUG^7Pwd_=&2m$eku z5#LFhxbZsc$=PI`N$Oa0j3t<_xjkfc)0?hm9a+N!R3eeiF+N6m{T$;1Aen%ENY!@T zIoHA*U3c6^)NbCLkDb;WLo~zY8278KR)g0)s=+&$a?U%6)#xe0J!*D=mtPT5()4{uv=lkK|@zGJ^q7 zFn<%%HSlM%-ZZ)_#+30#4-M8zgS8SB_IYvKT68S*9p;m;I^SLKOBcPXJNUm=c|&7K ztsFmx^~Ncg$^QnH((hza`U{`ZG(1V>f0UWZVEKNDesyBC-Ll*@FfFO~SvI&oQ*5;| zi@CX4d1;Lp=~+_E*zJGKLCYG~%z9@f=inl**{6f|<`-n~M$S~y#LE4MTJufh9cR}2 zr9`#~W#x&Azd_Mm3g|3pR^-y1eL2w1$ka{;{AAeafWh>{;WG1My7z<9mh8-@ev~;t zEq<)(Ne^WHDC>*4o#EZ;+0u;bp^}``%|zkptmB9_%twDh>byYp%v%4|aO)kL%bM?` zVD@(Y977l!9@H;0d~?wFlHsR+3T8?B={`kvrg!d!Hja|7liO3>4$2=D6?g7C$SCE?W8IP-iKj-b}T|o|i>)i>-vo903Smf=WdN#d%ly`Zoht%aa z*_v>Z-TBeVmv`gwu`!r)Tca{UIs=pD!+5&XQygRemmFhfA+RFnB_T#sNbeD#VNEt4=^zj9m&^Y&_zhz&jN7CZyYjY-)VE4nB$=_X&n+BKO#TXdkCKhR z)G5;5jlB8jlYI_miTp%qvME+ccF`|XOd{ExMeQU@db73UCXhV2*;;ZepGWbgBT{=K zeramEMx^%FV&GF-CQ*JqxFERv!FGBXvdTI0HKljnRGSPmr%NP|Sy2kEUlc@mimC-S zO;tn|wN;G0Qi9oRIRWq1lr5jcERmlcn&kAtuVhQQ-zM2I;=-?FhQIJD88j+ep2RQB zmh`@FTM9Ov*2{h(2|>@gR5U`=g*zU4VkbexmjEOWK+#m?KP(QOPD3{WA!8*IJV32 zS&3+C;MnFDY}iTw(QnE9!B_hDa~^?3OMk*w0mKiDPyoSFrMIB~;zvd(fM5~R+fV?( z8mT7~Ks;lF0tnWsybT2qtBgGIt6SbL1rR?s zLP5sqFU&vzHh*b^f{aZT$Go2ku*pK4Clq9C{@M%_VDmRdD1i8_5eguFXM_TX-y5MI z<2kE0K2!zR{G$;HAXu~UHWZjsJ-_>TWJ3P*{BHAI^hk^>e#=c>Gsz%agyO5l@74H+c*H5d1ce|0Vbqk2`1^;@SMTw|nA0 z7{-G_-0q22TEsgc^iJgNq+t$qP|nmcZss27n;8W#YrYai&(vNeBNFK@(lfPJqff`X zBhJ)bgNfA@xd&=zYUS+i>4dYHgd=BIuf-zaO_uPyPA~ot>UEe@>zgqBz8*w~lS{57 z>C!QMCxdp;=`A4a+#vpZ&ri`AET*S2B7Ygk*V&NS-MoYSJpF$?$BRU~Y!aaWg5^9f zA_^e>Y=i=czZjta;;%+1fcTpc3Lsb;@}4Sy_&*~QK(J!uZ76{Frx6Mu=#;$;1rYx> zLIK2oj8Fiv+6V;@uNa{Kf^{hGsRFAI7<8C{0tos(Z&Lw8&Ikn%EMR#X3LpwbD1cyX z%iB-@Q8YpU1S?+Nh60GP5egufR(l%?AgV?vfMA8p+fV?((wip~K#Vg&0R#v1y$uBr z6O2#*!HIisLjlA@BNRYzB;MOl0MTQF0*FaQD1hMfnRlW9qR$8g5S%XaHWXyaXR;Y6 zz$WL*yiElen^Vm|0XC-@p}=fPT{BHQY@Nm$VFb&KmBbhDpnC*R0|0YIttT@bzLCy7Ps&njg1^CoX6adqE7& zPp?aaxxvui0oj9oWM0}|P0{}9EhLzp<3<#Hx8No~E`Bo}r5)B=A;x~U;BA=DVac54 zwS2dLO0$7g8U+xX)AaRC0mO786hO={LIK1^Mks)oX@mlZjg3$Mv5651AO?(305Qu5 z1rVDWp#UN_LIDIp05R7H1rYO$ zPyjLC2n7%ej8Fivxe*E=wlG2g#Fj=VfY{0i1rQ63Pyn&W2n7&}jZgrwwGj#+wlP8h z#I{B#fY{Cm1rXaCp#WkBBNRaFXoLcYos3Wb@j4?EKi_V^uw zf9&xcf}ipD-GWzne5YVu-IMNzgV2%tnaB4C{<+8R6Z}h$-!J%A9)A!lqZC%Y1?oJ* z{l=SpxNRoToBht4eN>1)c>FQJFM0fc;6Hi%Nx^^d_|t;_=J96)|DVU76Z}t)zaaSE z9zP^_wZ~r)9QgeIvf$9;uL{n4{B^;R$KMcK^7xyAD;|GKaHq%L5j@W0Ck0RN_!)^RIwegR(jdO;_XZm!Xrj1o) z|3*}wwk9^yJ-j0Oppw*dlcE^qoutdZHaJv_3Qi(FE;TNJoWylpY5}6)Bx>xTO3TBR z+qlR{OxQy;qUa<*-P@79#Qn(o%_)ocb{tfCs_{Ee-8=cIt}4vxsjdoAr)-h!PNa+a zSkzQ^hxpHCmglCS>s(ciI>k-rs(^h0D0y+KYf?}c$BA-9hVvZ z-i*bf9to~IlfLOx3Lbn|o<<9TUBj5%jdJ+{#TA2yxx6{FJ!V^K{qA5X=srBJ@Jx++ zKcHG{bQj~<*zGKqyu(>iRBk4QL+sc_ydmW;;@I0a)nNAKes7bR!#pexg_8%T5tQy{ z?SZ-#Vf9wRMifq-Q>xF2rW{+T$4?BAUGWF-cKVEKS@`ORKPW*|lb1x^*$^&rjc*XGoT)Gp%Jk&KGT+J=Q^C0IAgwvX_7;gg~k>(e+I^(lcIMre%ZtZzvLRSpbSW?}XqDkU{;VsUjZt$4aX$3}MQ ze!$$9Yvo2i?#pfWY}cXv3_h4g-yP{u^Rt+C_H?!4vz7T*sa4b`FZ1bJsFQ0I6^cfw zJk(XIbk$2GtYt%Zcw`7HIDU>W+)-osgnbJvybYROXHZ?WyHRcoETR35@gE9ZVXGCJkPxLUQGu^H8>TK(P0 z5on^9+NJ1@m7p+|jY~E*&&KGT;4Z$G}q_iwv!TTZ9Z{r3OKtOAGuI zUaRDQe;`;S2mG{P4L9eW5qv5?xuK$FP@OhhD!gi{J)J(y`T9(5QK`gmvmq^v5`|Id ztW(?tUl@5WH{U1q`K;U^ckRL`wF{#wY!u7BK*~ie4IV5kmwaKA+hw8oqjUW?1e;xN zXSc`+(!N;gk0vP=d1rv}V^ZULiEUGj7-%}W>NW0)X)^Ls+A2ahAM0Nd>s&V0?TS+!7W2U|;dOBJd#AK|xJbewFUm-!069?$sW_#kj!6@4hDt}C5F@x-Q3{9g?GDeX5CO^%`1xY@Y-M9E1dVKit6;gxj zY9AT%Fof?HLtZd+%j~R5UgR^_M#`zreH%Y2TAv1zchcm!??B8Ni2C9u0I4o{s%4^; zM8c5vc{|i?@kif8qid5l(cF``YfJ}x7tmF1@S?EvtCA=$1v$2PThF9DrcaHXWYZx9 z%#ThsLIK1nMks(d)d&R;rx~FD;&dYvK%8NO0*Et>Pylh35egvAHbMb}GeQBx5+f8q zoMVInh{Om55KSW#K%8rY0*LdBPylhh5egvQXoLcY3ye?zaiI|kAeI`T0AiUD3Lq{r zLV?XnF}TbO6hK^VgaU{wj8FjaW+N0pTxo;?h^vfH0CBYu3Npc8V+IPad94u&Ag(h) z!5-lZd6RXyn6@9vz2597z|jpxD6sHUwhIBvpd#Pw=T(!^OdAV??qu37mE#7(5qNK? z!;u0_HqW9*6LQV(A@P+$6kme2T8Mf5ISS*;`0dQ)<10|RI79OkmS|_ckJ9{sR*~jP zj5uu^zZuo>`n#N27IRNr4B_iuKA=UAF&}O1d9amemmEZae|z!oV*V|Z6ol@Lm{yCL zA9H4)cCt;`k4p1BXF_tAq~JbDo~EGrL&6%!6`N0^x*y5UkNKhbyAW^s<`h@_1Z};r z;u)0Ar^LgQO&7TSLg8Z=0T^ZECe4MY{f%qj~5kJ`J zt}K-{B^Mk@tT7m#F2-g0v49Ow>l-EihnrTMqNHbIw8`fBdyxK0iq;T_oQZo2-t0&i z&0i93^F@9t1JR0Kp@!~M5*lC556rKXIX%PtMwv4+4BO?nIy=MsPMIYc=J(2| z9T)Z2^3A`(xxevK_CtM+RhAo!zBYz>CfC?4mpUIzoy$-{r*o5Q1ulC19czto(FV1` zD4)Ka^@(K_Y|43YJ%L|o{u6Dr`7eG4R{Wda-eU7VD9zRUxK{)*Z>w~!=-{{14Eb&5 zoJo$q2<2<)_4t-nL}2nVH=b z@!aoGM8S+%ZT~)ke>>w}A-0fD!|*QR-dXC^8D$R#pxz@1 zDiauIZV#iIt_Um?8#M+U4`WueqFn;ry9eJ1w(<6?gl&q4dd;f4h(thAn-A^7ui8 z&8*y+@^qQ3_K4TAyEPG2pA+-46$X3aZdcs3M_CoTkmIu=V%7SzqKcTt+g?jAF)G&? zO1et)%Dn%8|nyuFP*3R=H2b2{DdJeUwFQF11EJ=OIPvqmZEcatQ>QIf!}I0qZ@ zXhpVSqqx1>(ddW?qNk6agR_P6-o)rTI$H8vo#qjf!J*i*} z^)*iajmy*bXrqXdSfTfT2HMI5#-w&h|Mm+9COl$%u5N zfmV}C@W3CXPtVca3z43q>!BCZrXid7Li?m^by(u>7l)%xT>5<2Vix%zKT$E851l?A zO7V^4L)s3u@}baG){V$)K3t3!{>YsFf=D|fqD@;FAtwrboezS8gXQ0C=f&sYcVBZ} zT!bIP^5SWT*4ff_Sbq|F4Jq9(CZ+NtXXeHz@ta7gFKJzM%_i;wTxsI`nb~%DPm94{ zm{F~sVrOO-1T1Hz-$wCc@;xM7evm@_0%=WhlTrE?u(C8w)_)`UGE2S6`SPB@qFauX z$??siyBI{KNn3I(c|^OgCcpaTNRxjqEpZ3W_QbP^*;Il!PY78;F1ZaQpki{sFeBy! z#d2!o6$y(rONK7%7uI3JJoI5r*RbT-{koL7)Z@7wL9qAeGG;_K)|*6IP#moO*{5@E zN1HAlt?8_B`sti}{C4befK-~3lAA#cI1}0~vn3MSmnbu93VsEV$d7yxL-2#ovSg&{hR>rdlpkb?Ue23%~(ggI_74YT=ftPjx5 zyi?VpdzU!(clz5=JXNfFvEq7*Z#$a7U6btoVlkLWUhXqGFTIpilaJ$puP^B;GI{o! z7H3QF{x*=mx44e{?W|jUt2XbJsPmM9C@f9C7qyk!Ge_q3pg3GZjl3GOM1Izk@f+h? zn(gr|1e49uM{wsG9Dhc)oqG!#?c8gh(Y-lHitYSk-YVbGT6f!&dm zO32%H5#+G;DbrRrKO-4caQCM9b)TvQ_W@OTv{coi`-rL~cfYD-_i#h0@NHmx$q-%3+5g%0YhQ47F4M>Wg@jo}rdf z@F%E86~d`F8oSm1wwRFjM%O3A2_|G1{PViqnW^T$oU5|GY{11`>@PUy z@qsAuZ9)`0ad3+$dg9O)QTD{)Eu!j)BU?n5Cys6r<2~^P6j$^3IKkZ>pCEV=KkQrP z^iA6Rk&W>|ya64c^r*lMrPt+t)pV~Wk5Cr=vegV2>7%6Iqu#9l-5?e%-bOXi?-410 zxY-B=wu*tlJIp`<#5;{pkg<7(87RQ!yNpl(@opm&K)lBY1rT={p#b78BNRZq*9Zj= zcN?Jq;vORuK-_DDf=rCxX9fzed7lvqAl`3;0*DV7p}_oNoRANgA_oUhe(52(3CY(e zsw9|8mDXjEF#ak<&G7$gDAjai)fu^sC`Vn8WsLQ8j5?e2k`=cxKV8jD_>~EH)Ep3U z7C%L|sgO~I58uIs9eQoc0w zVs@^dY$L_D1umBABtY7}kZE##zRmkX{e}p0&suX!2_};+4YyjbD1i8Z5eguFXoLcYr;Shm@gpM?K>XMU1rR?mLIK1xMku%? zj9;Lk92lC)zW*;2)j-cHDuTWNLf-Y$tRnZ%nsWsZtBg6e?#7jmffOy#m1rUESLIK2|jZi?L4ai7b3vFXXQfON%l0us-ASN530D`O5-i88*sYWP( z;1agCp#Wk7BNRYzJ=@z*05RPN1rRfgP(Zq_%ce^-r0beix~>|Lu8qvO0{ok4gaQbz zL;ENwfY`(c1rP&9D1hKXwRfTbVpAg&K*UBUfS7HB0*FB)6hPFCPyo>|LIK2(5egvY z7@+`SGb0p0Fmm-VRRA&12nAb)@rTKb7G(HuwXt(>IWmUoptL5CcPf zvsTOotd3U88xz|wN*Y)xCUGo&(0+VyL=nulAQcdY1x6^yv|gK=fdXu9VT1yREsanB zv6T@DGJb7s1`4pbjS&hUwlzWl#CAp~fY{y$1$ZvUm$J?64rW0CE_O6R0mM#5D1dmK z5egu7HbMc!E=DMT*wqLHw1ht+8UsVp6Z~9}G(*p2qbBp}e6WHsE_=x`C*PK0;!W!0 zQHI8T0=^xL?dIh2ykH&93zk$1t@lnkH?+5TreMILFbl)|$%cw2%dya7Wo!MtjKt-I z9?wrL%4YG(r;Kf4&y!Nu|R1jU;m@n5r1j0Ww$InBRguySun<@v*2;NKjd`~k$} z`-my&k+p_iRM|i4#m~Rtl=mIPIC47>5SFrb`qslv|42JKj`nBrNIN@?wlif+JHwO2 zQj;ZVUrACo=T?`Hp2JAb@V&Oyp43jQT*$}Md3vSdA9Uh`GKZ7pJtwwhqe77t(lQ$6 zYVDVt#;2MF!EVNfo%!x!1v~zc_@xr|6NG*E`h~5JTJ^|RUzg#->qvi>k4d=|6BhAv zWftwzxX9Z}nb^pdu*OZJ!@lky-#OI?$y&Z{e)0X-NM6;htj=>hkxw%b+Xhe3R(a1U zTB=`{_2%1O$g7(?hQ(V>N0C3H-f}uW^McSEzLk7G9_i~s{%Ebr$n&KA2-4nsW#rpC zb=tFNnf#7;d&iE0PfAViCG1!CG7n!c{_kJrkr=J-%RC}p>B~H%^Z&Q+AT4dz_-NJ& zX#|$<&rg_Net^^u*~2=OvoA-cGTMHXJ?HrNbJP>>g0!$3@UYa9FT?N%(s&dr$YOu4o!iBfVzp>qV4r=0jZQydQ8ynKfZLY**A40#|l@O$n+fHb0)ZEK7JKi|n z4byYkwqyCg``|;SZisj8^%&E-9*bMIJD6kr^6tXMckz>RlLhLA_0jQxn0iKDW#@CvIXAUs z%pS97h^0pDVDX9XU7YR?5hM?8>ndKk7t$Suj`g!V>t{SABkO0)AsqAO)pQhB=Z9s% z!?ATPdp#Z91=i!@aj-Zl;Hcmoja3d;Xtwu7?g(5r|gcz{dpR?oCWVG@${U;fFs~XVIuY>Cy(H_tPYX_J=;<%HNS$LrJO41U?dZA zlV;pC9lHJ(2A}GvJnV50yuX7H;*RWt=(oG?qi!9gvf06dT)!?Q(Yz;6UNE(hN4Pnl`0jF+b@&SCT-~qw@K+GOQ+090u zbH|C%@%+e_fM@8bSI4K{BP2+h{KIg0KuC8-i88*BaKjy8JZqt1`4owv=Is*jxj<3#2buI z0CB7l3eM&Vhh$S2ODQy}UUjFCudU }^x=S?k%OR>PTJK*RY2&X&sfV{u#Ox)^M< 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 + + + + + Connect-Infisical + Establishes an authenticated session with an Infisical server and stores it for use by subsequent cmdlets. + Connect + Infisical + + + Authenticates against an Infisical instance using one of the supported auth providers (UniversalAuth, Token, JWT, OIDC, LDAP, Azure, GCP IAM) and stores the resulting connection in the module-level session manager. Subsequent cmdlets pick up the connection automatically. If parameters such as BaseUri, OrganizationId, ProjectId, Environment, ClientId, or ClientSecret are not supplied, the cmdlet attempts to resolve them from a curated list of environment-variable name patterns across Process, User, and Machine scopes. + + + Notes + + Use -PassThru to emit the resulting InfisicalConnection object; by default the connection is stored silently. SecureString-typed parameters such as ClientSecret, AccessToken, Jwt, and Password are never logged. + The cmdlet pins the API version to the bound value when -ApiVersion is supplied explicitly; otherwise the default 'v4' is used and remains overridable per-call. + + + + + EXAMPLE 1 + Connect-Infisical -BaseUri 'https://app.infisical.com' -ClientId $ClientId -ClientSecret $ClientSecret -OrganizationId $OrgId -ProjectId $ProjectId -Environment 'dev' + Performs a Universal-Auth machine-identity login and stores the resulting session for subsequent cmdlets. + + + EXAMPLE 2 + $ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com' +$ConnectInfisicalParameters.OrganizationId = $OrganizationId +$ConnectInfisicalParameters.ProjectId = $ProjectId +$ConnectInfisicalParameters.Environment = 'dev' +$ConnectInfisicalParameters.ClientId = $ClientId +$ConnectInfisicalParameters.ClientSecret = $ClientSecret +$ConnectInfisicalParameters.SecretPath = '/' +$ConnectInfisicalParameters.ApiVersion = 'v4' +$ConnectInfisicalParameters.PassThru = $True +$ConnectInfisicalParameters.Verbose = $True + +$ConnectInfisicalResult = Connect-Infisical @ConnectInfisicalParameters + Builds an ordered parameter dictionary, splats it onto Connect-Infisical, and captures the returned InfisicalConnection for later reuse. + + + + + + + Disconnect-Infisical + Clears the current Infisical session from the module-level session manager. + Disconnect + Infisical + + + Removes the cached InfisicalConnection so subsequent cmdlets that require an active session will fail until Connect-Infisical is invoked again. The cmdlet does not contact the Infisical server. + + + Notes + + Use -PassThru to receive a status object that includes the disconnect timestamp; by default the cmdlet returns no output. + + + + + EXAMPLE 1 + Disconnect-Infisical + Clears the active Infisical session silently. + + + EXAMPLE 2 + $DisconnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$DisconnectInfisicalParameters.PassThru = $True +$DisconnectInfisicalParameters.Verbose = $True + +$DisconnectInfisicalResult = Disconnect-Infisical @DisconnectInfisicalParameters + Disconnects and captures a status object that includes IsConnected and DisconnectedAtUtc for logging. + + + + + + + Get-InfisicalSecret + Retrieves a single Infisical secret by name from the active session's project and environment. + Get + InfisicalSecret + + + Fetches a single secret by name. Project, Environment, SecretPath, and ApiVersion default to the values pinned on the active InfisicalConnection but can be overridden per call. Optional flags request reference-expansion, import inclusion, or a specific historical version. + + + Notes + + The returned InfisicalSecret stores the value as SecureString; call .GetPlainTextValue() to materialize the cleartext value only when strictly required. + + + + + EXAMPLE 1 + Get-InfisicalSecret -SecretName 'DATABASE_URL' + Retrieves the DATABASE_URL secret from the project and environment pinned by Connect-Infisical. + + + EXAMPLE 2 + $GetInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalSecretParameters.SecretName = 'DATABASE_URL' +$GetInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalSecretParameters.ExpandSecretReferences = $True +$GetInfisicalSecretParameters.IncludeImports = $True +$GetInfisicalSecretParameters.Verbose = $True + +$GetInfisicalSecretResult = Get-InfisicalSecret @GetInfisicalSecretParameters + Retrieves a single secret from a script-specific subpath with secret-reference expansion and folder imports enabled. + + + + + + + Get-InfisicalSecrets + Lists Infisical secrets within a project, environment, and optional folder path. + Get + InfisicalSecrets + + + Enumerates secrets under the active session's project and environment, optionally recursing through subfolders. Supports metadata-based filtering, tag-slug filtering, secret-reference expansion, and personal-override inclusion. + + + Notes + + Use -Recursive together with -SecretPath to walk an entire folder subtree. Pipe the result into ConvertTo-InfisicalSecretDictionary for hashtable-style lookup. + + + + + EXAMPLE 1 + Get-InfisicalSecrets -SecretPath '/Windows' -Recursive + Lists every secret under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalSecretsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalSecretsParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalSecretsParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalSecretsParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalSecretsParameters.Recursive = $True +$GetInfisicalSecretsParameters.ExpandSecretReferences = $True +$GetInfisicalSecretsParameters.IncludeImports = $True +$GetInfisicalSecretsParameters.IncludePersonalOverrides = $True +$GetInfisicalSecretsParameters.Verbose = $True + +$GetInfisicalSecretsResult = Get-InfisicalSecrets @GetInfisicalSecretsParameters + Lists secrets under a script-specific subpath with imports, personal overrides, and reference expansion enabled. + + + + + + + New-InfisicalSecret + Creates a new Infisical secret, with support for SecureString values and bulk creation. + New + InfisicalSecret + + + Creates one or many secrets. Three parameter sets are supported: PlainText (SecretName + SecretValue), SecureString (SecretName + SecureSecretValue), and Bulk (an array of hashtables piped or supplied via -Secrets). Honors -WhatIf and -Confirm. + + + Notes + + Pass -SkipMultilineEncoding when the value already contains literal newlines that the server should preserve verbatim. Use -TagIds to attach tag references at creation time. + + + + + EXAMPLE 1 + New-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'super-secret-value' + Creates a single shared secret in the active project/environment. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags -ProjectId $ConnectInfisicalParameters.ProjectId + +$NewInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalSecretParameters.SecretName = 'API_KEY' +$NewInfisicalSecretParameters.SecretValue = 'super-secret-value' +$NewInfisicalSecretParameters.SecretComment = 'Issued by deployment pipeline' +$NewInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$NewInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$NewInfisicalSecretParameters.TagIds = @($GetInfisicalTagsResult[0].Id) +$NewInfisicalSecretParameters.Verbose = $True + +$NewInfisicalSecretResult = New-InfisicalSecret @NewInfisicalSecretParameters + Looks up tags to attach, then creates a single secret with a comment and tag association under a script-specific subpath. + + + + + + + Update-InfisicalSecret + Updates an existing Infisical secret value, comment, name, or tags. + Update + InfisicalSecret + + + Updates one or many secrets. Supports PlainText, SecureString, and Bulk parameter sets. Use -NewSecretName to rename a secret, -SecretComment to update its comment, and -TagIds to replace tag associations. Honors -WhatIf and -Confirm. + + + Notes + + Only the parameters you bind are sent; omitted scalar parameters are not modified server-side. The Bulk parameter set accepts pipeline input of hashtables containing SecretName/SecretValue/etc. + + + + + EXAMPLE 1 + Update-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'rotated-value' + Rotates the API_KEY secret in the active project/environment. + + + EXAMPLE 2 + $UpdateInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalSecretParameters.SecretName = 'API_KEY' +$UpdateInfisicalSecretParameters.NewSecretName = 'API_KEY_V2' +$UpdateInfisicalSecretParameters.SecretValue = 'rotated-value' +$UpdateInfisicalSecretParameters.SecretComment = 'Rotated by scheduled job' +$UpdateInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$UpdateInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$UpdateInfisicalSecretParameters.Verbose = $True + +$UpdateInfisicalSecretResult = Update-InfisicalSecret @UpdateInfisicalSecretParameters + Rotates the value, renames the secret, and updates its comment in a single call. + + + + + + + Remove-InfisicalSecret + Deletes one or many Infisical secrets by name. + Remove + InfisicalSecret + + + Deletes a single secret (Single parameter set) or a batch of secrets by name (Bulk parameter set). High ConfirmImpact triggers prompts by default. -PassThru emits the removed secret names. + + + Notes + + Removal is irreversible from this cmdlet's perspective; rely on Infisical's audit log or secret-version history for forensics. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalSecret -SecretName 'API_KEY_V1' -Confirm:$False + Deletes a single secret without prompting. + + + EXAMPLE 2 + $RemoveInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalSecretParameters.SecretNames = @('LEGACY_KEY_1','LEGACY_KEY_2','LEGACY_KEY_3') +$RemoveInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$RemoveInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$RemoveInfisicalSecretParameters.PassThru = $True +$RemoveInfisicalSecretParameters.Confirm = $False +$RemoveInfisicalSecretParameters.Verbose = $True + +$RemoveInfisicalSecretResult = Remove-InfisicalSecret @RemoveInfisicalSecretParameters + Bulk-deletes three legacy secrets and returns the removed names for audit logging. + + + + + + + Copy-InfisicalSecret + Duplicates one or more secrets into a different environment or secret path. + Copy + InfisicalSecret + + + Server-side duplicates an array of secret IDs into a destination environment (and optional destination path), with switches that control whether the value, comment, tags, and metadata are copied. Use Get-InfisicalSecrets followed by selection of the desired Id values to feed -SecretId. + + + Notes + + Set -OverwriteExisting to replace same-named secrets at the destination. Without -CopySecretValue, the destination secrets are created with empty values, preserving only metadata. + + + + + EXAMPLE 1 + Get-InfisicalSecrets | Select-Object -ExpandProperty Id | Copy-InfisicalSecret -DestinationEnvironment 'staging' -CopySecretValue + Copies all secrets from the active environment into 'staging', including their values. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath '/Windows' -Recursive + +$CopyInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$CopyInfisicalSecretParameters.SecretId = $GetInfisicalSecretsResult.Id +$CopyInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$CopyInfisicalSecretParameters.SourceEnvironment = $ConnectInfisicalParameters.Environment +$CopyInfisicalSecretParameters.SourceSecretPath = '/Windows' +$CopyInfisicalSecretParameters.DestinationEnvironment = 'staging' +$CopyInfisicalSecretParameters.DestinationSecretPath = '/Windows' +$CopyInfisicalSecretParameters.OverwriteExisting = $True +$CopyInfisicalSecretParameters.CopySecretValue = $True +$CopyInfisicalSecretParameters.CopySecretComment = $True +$CopyInfisicalSecretParameters.CopyTags = $True +$CopyInfisicalSecretParameters.CopyMetadata = $True +$CopyInfisicalSecretParameters.Verbose = $True + +$CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters + Promotes every Windows secret from the active environment into staging with full value/comment/tag/metadata propagation. + + + + + + + ConvertTo-InfisicalSecretDictionary + Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. + ConvertTo + InfisicalSecretDictionary + + + Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). + + + Notes + + Use this conversion before splatting secrets into another process (-AsPlainText) or before passing them to libraries that expect SecureString-keyed lookups (default). + + + + + EXAMPLE 1 + Get-InfisicalSecrets | ConvertTo-InfisicalSecretDictionary -AsPlainText + Builds a plain-text dictionary of every secret in the active environment. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ConvertToInfisicalSecretDictionaryParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalSecretDictionaryParameters.InputObject = $GetInfisicalSecretsResult +$ConvertToInfisicalSecretDictionaryParameters.DuplicateKeyBehavior = 'LastWins' +$ConvertToInfisicalSecretDictionaryParameters.AsPlainText = $True +$ConvertToInfisicalSecretDictionaryParameters.Verbose = $True + +$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters + Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions. + + + + + + + Export-InfisicalSecrets + Exports InfisicalSecret objects to disk or environment variables in a chosen file format. + Export + InfisicalSecrets + + + Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. + + + Notes + + EnvironmentVariables format does not require -Path; all other formats do. User/Machine scopes require appropriate privileges (Machine scope requires elevation on Windows). + + + + + EXAMPLE 1 + Get-InfisicalSecrets | Export-InfisicalSecrets -Format DotEnv -Path '.\.env' -Force + Writes the active environment's secrets to a .env file. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ExportInfisicalSecretsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalSecretsParameters.InputObject = $GetInfisicalSecretsResult +$ExportInfisicalSecretsParameters.Format = 'EnvironmentVariables' +$ExportInfisicalSecretsParameters.Scope = 'Process' +$ExportInfisicalSecretsParameters.Force = $True +$ExportInfisicalSecretsParameters.Verbose = $True + +$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters + Projects the recursive secret result into Process-scope environment variables for the current PowerShell session. + + + + + + + Get-InfisicalProjects + Lists Infisical projects accessible to the current identity. + Get + InfisicalProjects + + + Returns every project the active session can see. The cmdlet requires an active InfisicalConnection but takes no parameters; project visibility is governed by Infisical's role assignments. + + + Notes + + The result is an array of InfisicalProject objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. + + + + + EXAMPLE 1 + Get-InfisicalProjects + Lists every project the current session can see. + + + EXAMPLE 2 + $GetInfisicalProjectsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalProjectsParameters.Verbose = $True + +$GetInfisicalProjectsResult = Get-InfisicalProjects @GetInfisicalProjectsParameters | Where-Object { $_.Slug -ilike 'platform-*' } + Lists projects and filters down to those whose slug begins with 'platform-'. + + + + + + + Get-InfisicalProject + Retrieves a single Infisical project by its identifier. + Get + InfisicalProject + + + Retrieves one project by Id. If -ProjectId is not supplied, the cmdlet falls back to the ProjectId pinned on the active InfisicalConnection. + + + Notes + + The cmdlet accepts pipeline input by property name; objects emitted by Get-InfisicalProjects can be piped in directly to refresh a single record. + + + + + EXAMPLE 1 + Get-InfisicalProject + Retrieves the project pinned by the active session. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects + +$GetInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult[0].Id +$GetInfisicalProjectParameters.Verbose = $True + +$GetInfisicalProjectResult = Get-InfisicalProject @GetInfisicalProjectParameters + Looks up the first project in the list and retrieves its full record. + + + + + + + New-InfisicalProject + Creates a new Infisical project in the active organization. + New + InfisicalProject + + + Creates a project with the supplied name and optional slug, description, type, and organization id. If -OrganizationId is not supplied, the active session's organization is used. Honors -WhatIf and -Confirm. + + + Notes + + Slug must be unique within the organization; if not supplied, the server derives one from the project name. + + + + + EXAMPLE 1 + New-InfisicalProject -ProjectName 'Platform Telemetry' + Creates a new project named 'Platform Telemetry' in the active organization. + + + EXAMPLE 2 + $NewInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalProjectParameters.ProjectName = 'Platform Telemetry' +$NewInfisicalProjectParameters.Slug = 'platform-telemetry' +$NewInfisicalProjectParameters.Description = 'Secrets for platform telemetry pipeline' +$NewInfisicalProjectParameters.Type = 'secret-manager' +$NewInfisicalProjectParameters.OrganizationId = $ConnectInfisicalParameters.OrganizationId +$NewInfisicalProjectParameters.Verbose = $True + +$NewInfisicalProjectResult = New-InfisicalProject @NewInfisicalProjectParameters + Creates a project with an explicit slug, description, and type bound to a specific organization id. + + + + + + + Update-InfisicalProject + Updates the name, description, or auto-capitalization flag on an existing project. + Update + InfisicalProject + + + Updates mutable attributes on a project. -ProjectId defaults to the pinned session project when omitted. Only parameters that are bound are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + AutoCapitalization controls whether secret names submitted in mixed case are stored uppercase server-side; setting it false preserves the literal case supplied by clients. + + + + + EXAMPLE 1 + Update-InfisicalProject -Name 'Platform Telemetry (v2)' + Renames the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$UpdateInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult.Id +$UpdateInfisicalProjectParameters.Name = 'Platform Telemetry (v2)' +$UpdateInfisicalProjectParameters.Description = 'Migrated to v2 pipeline' +$UpdateInfisicalProjectParameters.AutoCapitalization = $False +$UpdateInfisicalProjectParameters.Verbose = $True + +$UpdateInfisicalProjectResult = Update-InfisicalProject @UpdateInfisicalProjectParameters + Locates the project by slug, renames it, updates the description, and disables auto-capitalization. + + + + + + + Remove-InfisicalProject + Deletes an Infisical project. + Remove + InfisicalProject + + + Deletes a project by Id. Defaults to the session-pinned project when -ProjectId is omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed project id. + + + Notes + + This is destructive and removes all secrets, environments, folders, and tags within the project. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalProject -Confirm:$False + Deletes the session-pinned project without prompting. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'sandbox-temp' } + +$RemoveInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult.Id +$RemoveInfisicalProjectParameters.PassThru = $True +$RemoveInfisicalProjectParameters.Confirm = $False +$RemoveInfisicalProjectParameters.Verbose = $True + +$RemoveInfisicalProjectResult = Remove-InfisicalProject @RemoveInfisicalProjectParameters + Finds the sandbox project by slug, removes it without confirmation, and emits the removed project id for logging. + + + + + + + Get-InfisicalEnvironments + Lists environments defined on an Infisical project. + Get + InfisicalEnvironments + + + Returns all environments configured on a project. -ProjectId defaults to the session-pinned project id when omitted. + + + Notes + + Each InfisicalEnvironment carries both Id and Slug; downstream cmdlets accept either form on -Environment-like parameters. + + + + + EXAMPLE 1 + Get-InfisicalEnvironments + Lists environments for the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalEnvironmentsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalEnvironmentsParameters.ProjectId = $GetInfisicalProjectsResult.Id +$GetInfisicalEnvironmentsParameters.Verbose = $True + +$GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments @GetInfisicalEnvironmentsParameters + Resolves a project by slug and lists every environment defined on it. + + + + + + + Get-InfisicalEnvironment + Retrieves a single Infisical environment by slug or id. + Get + InfisicalEnvironment + + + Returns one environment record by slug or id (-EnvironmentSlugOrId). -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalEnvironment objects from Get-InfisicalEnvironments can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalEnvironment -EnvironmentSlugOrId 'dev' + Retrieves the 'dev' environment from the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'dev' } + +$GetInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalEnvironmentParameters.EnvironmentSlugOrId = $GetInfisicalEnvironmentsResult.Slug +$GetInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalEnvironmentParameters.Verbose = $True + +$GetInfisicalEnvironmentResult = Get-InfisicalEnvironment @GetInfisicalEnvironmentParameters + Looks up the dev environment by slug and re-fetches the canonical record by slug or id. + + + + + + + New-InfisicalEnvironment + Creates a new environment on an Infisical project. + New + InfisicalEnvironment + + + Creates an environment with the supplied display name and slug, optionally setting its sort -Position. -ProjectId defaults to the session-pinned project when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Slugs must be unique within the project and are used as the canonical -Environment value across all other cmdlets. + + + + + EXAMPLE 1 + New-InfisicalEnvironment -Name 'Staging' -Slug 'staging' + Adds a Staging environment to the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$NewInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalEnvironmentParameters.ProjectId = $GetInfisicalProjectsResult.Id +$NewInfisicalEnvironmentParameters.Name = 'Staging' +$NewInfisicalEnvironmentParameters.Slug = 'staging' +$NewInfisicalEnvironmentParameters.Position = 20 +$NewInfisicalEnvironmentParameters.Verbose = $True + +$NewInfisicalEnvironmentResult = New-InfisicalEnvironment @NewInfisicalEnvironmentParameters + Adds a Staging environment at sort position 20 on the resolved project. + + + + + + + Update-InfisicalEnvironment + Updates the name, slug, or sort order of an existing Infisical environment. + Update + InfisicalEnvironment + + + Updates an environment identified by -EnvironmentId. -ProjectId defaults to the session-pinned project when omitted. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug can break downstream automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalEnvironment -EnvironmentId $EnvId -Name 'Pre-Production' + Renames an environment in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'staging' } + +$UpdateInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentsResult.Id +$UpdateInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalEnvironmentParameters.Name = 'Pre-Production' +$UpdateInfisicalEnvironmentParameters.Slug = 'preprod' +$UpdateInfisicalEnvironmentParameters.Position = 25 +$UpdateInfisicalEnvironmentParameters.Verbose = $True + +$UpdateInfisicalEnvironmentResult = Update-InfisicalEnvironment @UpdateInfisicalEnvironmentParameters + Locates the staging environment, renames it to Pre-Production, rotates its slug, and updates its sort order. + + + + + + + Remove-InfisicalEnvironment + Deletes an Infisical environment from a project. + Remove + InfisicalEnvironment + + + Removes an environment by Id. -ProjectId defaults to the session-pinned project when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed environment id. + + + Notes + + Removing an environment deletes every secret and folder scoped to it. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalEnvironment -EnvironmentId $EnvId -Confirm:$False + Deletes an environment without prompting. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'sandbox' } + +$RemoveInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentsResult.Id +$RemoveInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalEnvironmentParameters.PassThru = $True +$RemoveInfisicalEnvironmentParameters.Confirm = $False +$RemoveInfisicalEnvironmentParameters.Verbose = $True + +$RemoveInfisicalEnvironmentResult = Remove-InfisicalEnvironment @RemoveInfisicalEnvironmentParameters + Removes the sandbox environment without prompting and emits its id for the audit trail. + + + + + + + Get-InfisicalFolders + Lists Infisical folders at a given secret path. + Get + InfisicalFolders + + + Enumerates folders directly under the supplied -Path within the active project and environment. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. + + + Notes + + This is a non-recursive listing of immediate subfolders. To enumerate secrets across a folder subtree use Get-InfisicalSecrets -Recursive. + + + + + EXAMPLE 1 + Get-InfisicalFolders -Path '/Windows' + Lists every folder directly under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalFoldersParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalFoldersParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalFoldersParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalFoldersParameters.Path = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalFoldersParameters.Verbose = $True + +$GetInfisicalFoldersResult = Get-InfisicalFolders @GetInfisicalFoldersParameters + Lists folders under a script-specific subpath using the session-pinned project and environment. + + + + + + + Get-InfisicalFolder + Retrieves a single Infisical folder by name or id. + Get + InfisicalFolder + + + Returns one folder record by name or id (-FolderNameOrId) under the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalFolder objects from Get-InfisicalFolders can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalFolder -FolderNameOrId 'Deployments' -Path '/Windows' + Retrieves the Deployments folder under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$GetInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalFolderParameters.FolderNameOrId = $GetInfisicalFoldersResult.Id +$GetInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalFolderParameters.Path = '/Windows' +$GetInfisicalFolderParameters.Verbose = $True + +$GetInfisicalFolderResult = Get-InfisicalFolder @GetInfisicalFolderParameters + Locates the folder by name first, then re-fetches it by id to refresh the canonical record. + + + + + + + New-InfisicalFolder + Creates a new Infisical folder under the supplied parent path. + New + InfisicalFolder + + + Creates a folder with the supplied -Name beneath the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Folder names are case-sensitive and must be unique within a parent path; the cmdlet does not create intermediate folders. + + + + + EXAMPLE 1 + New-InfisicalFolder -Name 'Deployments' -Path '/Windows' + Creates the Deployments folder under /Windows in the active project and environment. + + + EXAMPLE 2 + $NewInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalFolderParameters.Name = $CallingScriptPath.BaseName +$NewInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$NewInfisicalFolderParameters.Path = '/Windows' +$NewInfisicalFolderParameters.Verbose = $True + +$NewInfisicalFolderResult = New-InfisicalFolder @NewInfisicalFolderParameters + Creates a script-named folder under /Windows using the session-pinned project and environment. + + + + + + + Update-InfisicalFolder + Renames an existing Infisical folder. + Update + InfisicalFolder + + + Renames a folder identified by -FolderId to the supplied -Name. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Renaming a folder rewrites the path component for every secret beneath it; coordinate with consumers that pin to the previous path. + + + + + EXAMPLE 1 + Update-InfisicalFolder -FolderId $FolderId -Name 'Deployments-Archive' + Renames a folder in the session-pinned project/environment. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$UpdateInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalFolderParameters.FolderId = $GetInfisicalFoldersResult.Id +$UpdateInfisicalFolderParameters.Name = 'Deployments-Archive' +$UpdateInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$UpdateInfisicalFolderParameters.Path = '/Windows' +$UpdateInfisicalFolderParameters.Verbose = $True + +$UpdateInfisicalFolderResult = Update-InfisicalFolder @UpdateInfisicalFolderParameters + Resolves the folder by name and renames it to Deployments-Archive. + + + + + + + Remove-InfisicalFolder + Deletes an Infisical folder and all secrets it contains. + Remove + InfisicalFolder + + + Removes a folder by Id from the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed folder id. + + + Notes + + This is destructive and removes every secret and subfolder under the target folder. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalFolder -FolderId $FolderId -Confirm:$False + Deletes a folder from the session-pinned project/environment without prompting. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq $CallingScriptPath.BaseName } + +$RemoveInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalFolderParameters.FolderId = $GetInfisicalFoldersResult.Id +$RemoveInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$RemoveInfisicalFolderParameters.Path = '/Windows' +$RemoveInfisicalFolderParameters.PassThru = $True +$RemoveInfisicalFolderParameters.Confirm = $False +$RemoveInfisicalFolderParameters.Verbose = $True + +$RemoveInfisicalFolderResult = Remove-InfisicalFolder @RemoveInfisicalFolderParameters + Resolves the script-named folder under /Windows and removes it without prompting, returning its id for logging. + + + + + + + Get-InfisicalTags + Lists Infisical tags defined on a project. + Get + InfisicalTags + + + Returns every tag configured on a project. -ProjectId defaults to the session-pinned project id when omitted. + + + Notes + + Tag Ids returned here are the values to pass on -TagIds when creating or updating secrets. + + + + + EXAMPLE 1 + Get-InfisicalTags + Lists every tag defined on the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalTagsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalTagsParameters.ProjectId = $GetInfisicalProjectsResult.Id +$GetInfisicalTagsParameters.Verbose = $True + +$GetInfisicalTagsResult = Get-InfisicalTags @GetInfisicalTagsParameters + Resolves a project by slug and lists every tag defined on it. + + + + + + + Get-InfisicalTag + Retrieves a single Infisical tag by slug or id. + Get + InfisicalTag + + + Returns one tag record by slug or id (-TagSlugOrId). -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalTag objects from Get-InfisicalTags can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalTag -TagSlugOrId 'critical' + Retrieves the 'critical' tag from the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical' } + +$GetInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalTagParameters.TagSlugOrId = $GetInfisicalTagsResult.Slug +$GetInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalTagParameters.Verbose = $True + +$GetInfisicalTagResult = Get-InfisicalTag @GetInfisicalTagParameters + Filters tags to the critical slug and refetches the canonical record. + + + + + + + New-InfisicalTag + Creates a new Infisical tag on a project. + New + InfisicalTag + + + Creates a tag with the supplied -Slug, optional -Name and -Color. -ProjectId defaults to the session-pinned project when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Tag slugs must be unique within the project and are the canonical reference used by tag-filtered secret lookups. + + + + + EXAMPLE 1 + New-InfisicalTag -Slug 'critical' -Name 'Critical' -Color '#FF0000' + Creates a red Critical tag in the session-pinned project. + + + EXAMPLE 2 + $NewInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalTagParameters.Slug = 'critical' +$NewInfisicalTagParameters.Name = 'Critical' +$NewInfisicalTagParameters.Color = '#FF0000' +$NewInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalTagParameters.Verbose = $True + +$NewInfisicalTagResult = New-InfisicalTag @NewInfisicalTagParameters + Creates a red Critical tag against an explicitly supplied project id. + + + + + + + Update-InfisicalTag + Updates the slug, name, or color of an existing Infisical tag. + Update + InfisicalTag + + + Updates a tag identified by -TagId. -ProjectId defaults to the session-pinned project when omitted. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug breaks tag-filtered automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalTag -TagId $TagId -Color '#FFA500' + Changes the display color of a tag in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical' } + +$UpdateInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalTagParameters.TagId = $GetInfisicalTagsResult.Id +$UpdateInfisicalTagParameters.Slug = 'critical-v2' +$UpdateInfisicalTagParameters.Name = 'Critical (v2)' +$UpdateInfisicalTagParameters.Color = '#FFA500' +$UpdateInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalTagParameters.Verbose = $True + +$UpdateInfisicalTagResult = Update-InfisicalTag @UpdateInfisicalTagParameters + Locates the critical tag and rotates its slug, display name, and color. + + + + + + + Remove-InfisicalTag + Deletes an Infisical tag from a project. + Remove + InfisicalTag + + + Removes a tag by Id. -ProjectId defaults to the session-pinned project when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed tag id. + + + Notes + + Removing a tag detaches it from every secret it was applied to but does not delete the secrets themselves. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalTag -TagId $TagId -Confirm:$False + Deletes a tag from the session-pinned project without prompting. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical-v2' } + +$RemoveInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalTagParameters.TagId = $GetInfisicalTagsResult.Id +$RemoveInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalTagParameters.PassThru = $True +$RemoveInfisicalTagParameters.Confirm = $False +$RemoveInfisicalTagParameters.Verbose = $True + +$RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters + Resolves a tag by slug and removes it without prompting, returning its id for the audit trail. + + + + + + + Get-InfisicalCertificateAuthority + Lists or retrieves Infisical internal Certificate Authorities. + Get + InfisicalCertificateAuthority + + + When -CaId is supplied (ById parameter set) returns a single CA. Otherwise (List parameter set) returns every internal CA visible in the project. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Only internal CAs are surfaced; external/ACME issuers are not enumerated by this cmdlet. CA Ids returned here are the values to pass on -CertificateAuthorityId to Request-InfisicalCertificate. + + + + + EXAMPLE 1 + Get-InfisicalCertificateAuthority + Lists every internal CA visible in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificateAuthorityParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateAuthorityParameters.CaId = $GetInfisicalCertificateAuthorityListResult.Id +$GetInfisicalCertificateAuthorityParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalCertificateAuthorityParameters.Verbose = $True + +$GetInfisicalCertificateAuthorityResult = Get-InfisicalCertificateAuthority @GetInfisicalCertificateAuthorityParameters + Filters the CA list by friendly name and then re-fetches the canonical CA record by id. + + + + + + + Get-InfisicalCertificates + Lists Infisical certificates in a project, with optional filters and automatic paging. + Get + InfisicalCertificates + + + Enumerates certificates with optional filters for -CommonName, -FriendlyName, -Status, and -CaId. -Limit and -Offset drive a single page; pages are walked automatically until exhausted unless -NoAutoPage is supplied. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + For advanced filtering (validity window, key algorithm, extended key usage, etc.) use Search-InfisicalCertificate instead. + + + + + EXAMPLE 1 + Get-InfisicalCertificates -Status 'active' + Lists every active certificate in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificatesParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificatesParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalCertificatesParameters.CommonName = $env:COMPUTERNAME +$GetInfisicalCertificatesParameters.FriendlyName = 'web-tier' +$GetInfisicalCertificatesParameters.Status = 'active' +$GetInfisicalCertificatesParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$GetInfisicalCertificatesParameters.Limit = 100 +$GetInfisicalCertificatesParameters.Verbose = $True + +$GetInfisicalCertificatesResult = Get-InfisicalCertificates @GetInfisicalCertificatesParameters + Resolves the issuing CA, then lists active certificates scoped to that CA, the local hostname, and the 'web-tier' friendly name. + + + + + + + Get-InfisicalCertificate + Retrieves a single Infisical certificate by serial number. + Get + InfisicalCertificate + + + Returns one certificate record by -SerialNumber. Accepts pipeline input by property name so InfisicalCertificate objects from list/search cmdlets can be re-fetched directly. + + + Notes + + This returns metadata only. To obtain certificate and chain PEM material use ConvertTo-InfisicalCertificate or Export-InfisicalCertificate. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -SerialNumber '7A:F2:1B:...:9E' + Retrieves the certificate record for the supplied serial number. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$GetInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$GetInfisicalCertificateParameters.Verbose = $True + +$GetInfisicalCertificateResult = Get-InfisicalCertificate @GetInfisicalCertificateParameters + Selects the active certificate whose common name matches the host and refetches its canonical record. + + + + + + + Search-InfisicalCertificate + Searches Infisical certificates with advanced filters and automatic paging. + Search + InfisicalCertificate + + + Performs a server-side search across certificates with filters for friendly name, common name, free-text search, status, CA/profile/application/enrollment scope, key/signature algorithm, source, and validity window (-NotBeforeFrom/-NotBeforeTo/-NotAfterFrom/-NotAfterTo). Results are paged automatically unless -NoAutoPage is supplied. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Use -SortBy together with -SortOrder ('asc'/'desc') to control result ordering. Pair with Get-InfisicalCertificate or Export-InfisicalCertificate to drill into specific hits. + + + + + EXAMPLE 1 + Search-InfisicalCertificate -Search $env:COMPUTERNAME -Status 'active' + Finds active certificates whose searchable fields contain the local hostname. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$SearchInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$SearchInfisicalCertificateParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$SearchInfisicalCertificateParameters.CommonName = $env:COMPUTERNAME +$SearchInfisicalCertificateParameters.Status = 'active' +$SearchInfisicalCertificateParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$SearchInfisicalCertificateParameters.KeyAlgorithm = @('RSA') +$SearchInfisicalCertificateParameters.NotAfterTo = (Get-Date).AddDays(30) +$SearchInfisicalCertificateParameters.SortBy = 'notAfter' +$SearchInfisicalCertificateParameters.SortOrder = 'asc' +$SearchInfisicalCertificateParameters.Limit = 100 +$SearchInfisicalCertificateParameters.Verbose = $True + +$SearchInfisicalCertificateResult = Search-InfisicalCertificate @SearchInfisicalCertificateParameters + Searches for RSA certificates from a specific CA, scoped to the local hostname, that expire within the next 30 days, sorted soonest-first. + + + + + + + Request-InfisicalCertificate + Requests a new Infisical certificate (local CSR + sign) or reuses a still-valid existing one. + Request + InfisicalCertificate + + + Generates a keypair locally, builds a CSR, and submits it for signing either via a PKI subscriber (-PkiSubscriberSlug, default parameter set) or by direct CA signing (-CertificateAuthorityId). On subsequent runs an existing certificate whose CN matches and whose remaining lifetime exceeds -RenewalThresholdDays is reused; pass -Force to always issue or -AllowRenewal to allow rotation inside the threshold. Optional flags install the leaf (-Install) and chain (-InstallChain) into a Windows certificate store, and control private-key protection (-PrivateKeyProtection, -PersistKey, -MachineKey, -PrivateKeyPath, -KeyStorageFlags). Honors -WhatIf and -Confirm. + + + Notes + + Default -PrivateKeyProtection is 'LocalOnly': the leaf is loaded into memory without persisting the private key and PrivateKeyPem is scrubbed from the emitted result unless -PrivateKeyPath or an explicit -KeyStorageFlags binding overrides it. The reuse path completes its chain from the Infisical bundle when local stores are incomplete; pass -LocalChainOnly to suppress that fetch entirely. + + + + + EXAMPLE 1 + Request-InfisicalCertificate -PkiSubscriberSlug 'web-tier' -Install + Requests (or reuses) a certificate for the 'web-tier' subscriber and installs it into CurrentUser\My. + + + EXAMPLE 2 + $RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RequestInfisicalCertificateParameters.PkiSubscriberSlug = 'web-tier' +$RequestInfisicalCertificateParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RequestInfisicalCertificateParameters.CommonName = ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName +$RequestInfisicalCertificateParameters.DnsName = @(([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName, $env:COMPUTERNAME) +$RequestInfisicalCertificateParameters.KeyAlgorithm = 'Rsa' +$RequestInfisicalCertificateParameters.KeySize = 3072 +$RequestInfisicalCertificateParameters.Install = $True +$RequestInfisicalCertificateParameters.InstallChain = $True +$RequestInfisicalCertificateParameters.StoreName = 'My' +$RequestInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$RequestInfisicalCertificateParameters.PrivateKeyProtection = 'NonExportable' +$RequestInfisicalCertificateParameters.MachineKey = $True +$RequestInfisicalCertificateParameters.PersistKey = $True +$RequestInfisicalCertificateParameters.AllowRenewal = $True +$RequestInfisicalCertificateParameters.RenewalThresholdDays = 30 +$RequestInfisicalCertificateParameters.Verbose = $True + +$RequestInfisicalCertificateResult = Request-InfisicalCertificate @RequestInfisicalCertificateParameters + Issues (or renews within 30 days) a 3072-bit RSA certificate for the local FQDN, installs the leaf and chain into LocalMachine\My with a non-exportable machine-bound persistent key. + + + + + + + ConvertTo-InfisicalCertificate + Materializes an X509Certificate2 from an Infisical certificate record, bundle, or serial number. + ConvertTo + InfisicalCertificate + + + Fetches the certificate bundle (when given an InfisicalCertificate or -SerialNumber), or accepts an already-fetched -Bundle, and constructs an X509Certificate2 from the PEM material. Use -NoPrivateKey to omit the private key, -KeyStorageFlags to control how the key is loaded, and -IncludeChain to additionally emit each chain certificate as a separate X509Certificate2 in the pipeline. + + + Notes + + The bundle for any given certificate is typically retrievable only once after issuance; -SerialNumber and pipeline modes will fail with a bundle-not-available error for older certificates. Use -KeyStorageFlags Exportable when callers need to re-export the resulting cert as PFX. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -SerialNumber $Serial | ConvertTo-InfisicalCertificate -IncludeChain + Materializes the certificate and emits each chain element individually. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ConvertToInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$ConvertToInfisicalCertificateParameters.NoPrivateKey = $False +$ConvertToInfisicalCertificateParameters.IncludeChain = $True +$ConvertToInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable +$ConvertToInfisicalCertificateParameters.Verbose = $True + +$ConvertToInfisicalCertificateResult = ConvertTo-InfisicalCertificate @ConvertToInfisicalCertificateParameters + Selects the active certificate whose CN matches the host and materializes it (with private key and chain) as exportable X509Certificate2 objects. + + + + + + + Export-InfisicalCertificate + Exports an Infisical certificate to disk in PEM, PFX, or CER format. + Export + InfisicalCertificate + + + Writes a certificate to -Path in the supplied -Format. Accepts an X509Certificate2, an InfisicalCertificateBundle, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -Password (SecureString) supplies the PFX password. -IncludeChain appends chain certificates (PEM only). -NoPrivateKey omits the private key. -Force overwrites an existing file. Honors -WhatIf and -Confirm. + + + Notes + + PFX export requires the cert to have been loaded with X509KeyStorageFlags.Exportable; bundle/serial modes import with Exportable automatically. CER and PFX formats ignore -IncludeChain. + + + + + EXAMPLE 1 + Export-InfisicalCertificate -Path 'C:\Temp\web-tier.pem' -Format Pem -SerialNumber $Serial -IncludeChain + Exports a certificate, its chain, and private key (when available) as a single PEM bundle. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ExportInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$ExportInfisicalCertificateParameters.Path = "C:\Temp\$($env:COMPUTERNAME).pfx" +$ExportInfisicalCertificateParameters.Format = 'Pfx' +$ExportInfisicalCertificateParameters.Password = (Read-Host -AsSecureString -Prompt 'PFX password') +$ExportInfisicalCertificateParameters.Force = $True +$ExportInfisicalCertificateParameters.PassThru = $True +$ExportInfisicalCertificateParameters.Verbose = $True + +$ExportInfisicalCertificateResult = Export-InfisicalCertificate @ExportInfisicalCertificateParameters + Resolves the active host certificate by serial and exports it as a password-protected PFX, overwriting any existing file and emitting a FileInfo for downstream use. + + + + + + + Install-InfisicalCertificate + Installs an Infisical certificate (and optional chain) into a Windows certificate store. + Install + InfisicalCertificate + + + Adds a certificate to the supplied -StoreName and -StoreLocation. Accepts an X509Certificate2, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -KeyStorageFlags controls private-key loading. -IncludeChain installs each chain certificate to the CertificateAuthority store of the same -StoreLocation. -Force replaces an existing thumbprint. -PassThru emits the installed certificate. Honors -WhatIf and -Confirm. + + + Notes + + Installing into LocalMachine stores typically requires elevation. -IncludeChain only fires for serial/InfisicalCertificate inputs because the X509Certificate2 input has no associated bundle to walk. + + + + + EXAMPLE 1 + Install-InfisicalCertificate -SerialNumber $Serial -StoreLocation LocalMachine -IncludeChain + Installs the leaf into LocalMachine\My and each chain element into LocalMachine\CertificateAuthority. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$InstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$InstallInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$InstallInfisicalCertificateParameters.StoreName = 'My' +$InstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$InstallInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet +$InstallInfisicalCertificateParameters.IncludeChain = $True +$InstallInfisicalCertificateParameters.Force = $True +$InstallInfisicalCertificateParameters.PassThru = $True +$InstallInfisicalCertificateParameters.Verbose = $True + +$InstallInfisicalCertificateResult = Install-InfisicalCertificate @InstallInfisicalCertificateParameters + Resolves the active host certificate and installs the leaf (with a machine-bound persistent key) plus its chain into LocalMachine, replacing any existing thumbprint match. + + + + + + + Uninstall-InfisicalCertificate + Removes a certificate from a Windows certificate store by thumbprint, subject, or pipeline input. + Uninstall + InfisicalCertificate + + + Removes matching certificates from the supplied -StoreName and -StoreLocation. Accepts -Thumbprint, -Subject, an X509Certificate2 (-Certificate), or an InfisicalCertificate (-InfisicalCertificate, uses FingerprintSha1). -Force allows removing multiple matches in one call; -PassThru emits each removed certificate. Honors -WhatIf and -Confirm. + + + Notes + + When more than one certificate matches -Subject and -Force is not supplied the cmdlet throws to prevent accidental bulk removal. Uninstalling from LocalMachine stores typically requires elevation. + + + + + EXAMPLE 1 + Uninstall-InfisicalCertificate -Thumbprint $Thumbprint -StoreLocation LocalMachine + Removes the certificate with the supplied thumbprint from LocalMachine\My. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'revoked' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$UninstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UninstallInfisicalCertificateParameters.InfisicalCertificate = $GetInfisicalCertificatesResult[0] +$UninstallInfisicalCertificateParameters.StoreName = 'My' +$UninstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$UninstallInfisicalCertificateParameters.Force = $True +$UninstallInfisicalCertificateParameters.PassThru = $True +$UninstallInfisicalCertificateParameters.Verbose = $True + +$UninstallInfisicalCertificateResult = Uninstall-InfisicalCertificate @UninstallInfisicalCertificateParameters + Picks the revoked host certificate and removes it from LocalMachine\My using its SHA1 fingerprint, emitting the removed object for the audit trail. + + + + + + + diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml new file mode 100644 index 0000000..dc9fb90 --- /dev/null +++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml @@ -0,0 +1,1531 @@ + + + + + + Connect-Infisical + Establishes an authenticated session with an Infisical server and stores it for use by subsequent cmdlets. + Connect + Infisical + + + Authenticates against an Infisical instance using one of the supported auth providers (UniversalAuth, Token, JWT, OIDC, LDAP, Azure, GCP IAM) and stores the resulting connection in the module-level session manager. Subsequent cmdlets pick up the connection automatically. If parameters such as BaseUri, OrganizationId, ProjectId, Environment, ClientId, or ClientSecret are not supplied, the cmdlet attempts to resolve them from a curated list of environment-variable name patterns across Process, User, and Machine scopes. + + + Notes + + Use -PassThru to emit the resulting InfisicalConnection object; by default the connection is stored silently. SecureString-typed parameters such as ClientSecret, AccessToken, Jwt, and Password are never logged. + The cmdlet pins the API version to the bound value when -ApiVersion is supplied explicitly; otherwise the default 'v4' is used and remains overridable per-call. + + + + + EXAMPLE 1 + Connect-Infisical -BaseUri 'https://app.infisical.com' -ClientId $ClientId -ClientSecret $ClientSecret -OrganizationId $OrgId -ProjectId $ProjectId -Environment 'dev' + Performs a Universal-Auth machine-identity login and stores the resulting session for subsequent cmdlets. + + + EXAMPLE 2 + $ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConnectInfisicalParameters.BaseUri = 'https://app.infisical.com' +$ConnectInfisicalParameters.OrganizationId = $OrganizationId +$ConnectInfisicalParameters.ProjectId = $ProjectId +$ConnectInfisicalParameters.Environment = 'dev' +$ConnectInfisicalParameters.ClientId = $ClientId +$ConnectInfisicalParameters.ClientSecret = $ClientSecret +$ConnectInfisicalParameters.SecretPath = '/' +$ConnectInfisicalParameters.ApiVersion = 'v4' +$ConnectInfisicalParameters.PassThru = $True +$ConnectInfisicalParameters.Verbose = $True + +$ConnectInfisicalResult = Connect-Infisical @ConnectInfisicalParameters + Builds an ordered parameter dictionary, splats it onto Connect-Infisical, and captures the returned InfisicalConnection for later reuse. + + + + + + + Disconnect-Infisical + Clears the current Infisical session from the module-level session manager. + Disconnect + Infisical + + + Removes the cached InfisicalConnection so subsequent cmdlets that require an active session will fail until Connect-Infisical is invoked again. The cmdlet does not contact the Infisical server. + + + Notes + + Use -PassThru to receive a status object that includes the disconnect timestamp; by default the cmdlet returns no output. + + + + + EXAMPLE 1 + Disconnect-Infisical + Clears the active Infisical session silently. + + + EXAMPLE 2 + $DisconnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$DisconnectInfisicalParameters.PassThru = $True +$DisconnectInfisicalParameters.Verbose = $True + +$DisconnectInfisicalResult = Disconnect-Infisical @DisconnectInfisicalParameters + Disconnects and captures a status object that includes IsConnected and DisconnectedAtUtc for logging. + + + + + + + Get-InfisicalSecret + Retrieves a single Infisical secret by name from the active session's project and environment. + Get + InfisicalSecret + + + Fetches a single secret by name. Project, Environment, SecretPath, and ApiVersion default to the values pinned on the active InfisicalConnection but can be overridden per call. Optional flags request reference-expansion, import inclusion, or a specific historical version. + + + Notes + + The returned InfisicalSecret stores the value as SecureString; call .GetPlainTextValue() to materialize the cleartext value only when strictly required. + + + + + EXAMPLE 1 + Get-InfisicalSecret -SecretName 'DATABASE_URL' + Retrieves the DATABASE_URL secret from the project and environment pinned by Connect-Infisical. + + + EXAMPLE 2 + $GetInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalSecretParameters.SecretName = 'DATABASE_URL' +$GetInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalSecretParameters.ExpandSecretReferences = $True +$GetInfisicalSecretParameters.IncludeImports = $True +$GetInfisicalSecretParameters.Verbose = $True + +$GetInfisicalSecretResult = Get-InfisicalSecret @GetInfisicalSecretParameters + Retrieves a single secret from a script-specific subpath with secret-reference expansion and folder imports enabled. + + + + + + + Get-InfisicalSecrets + Lists Infisical secrets within a project, environment, and optional folder path. + Get + InfisicalSecrets + + + Enumerates secrets under the active session's project and environment, optionally recursing through subfolders. Supports metadata-based filtering, tag-slug filtering, secret-reference expansion, and personal-override inclusion. + + + Notes + + Use -Recursive together with -SecretPath to walk an entire folder subtree. Pipe the result into ConvertTo-InfisicalSecretDictionary for hashtable-style lookup. + + + + + EXAMPLE 1 + Get-InfisicalSecrets -SecretPath '/Windows' -Recursive + Lists every secret under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalSecretsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalSecretsParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalSecretsParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalSecretsParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalSecretsParameters.Recursive = $True +$GetInfisicalSecretsParameters.ExpandSecretReferences = $True +$GetInfisicalSecretsParameters.IncludeImports = $True +$GetInfisicalSecretsParameters.IncludePersonalOverrides = $True +$GetInfisicalSecretsParameters.Verbose = $True + +$GetInfisicalSecretsResult = Get-InfisicalSecrets @GetInfisicalSecretsParameters + Lists secrets under a script-specific subpath with imports, personal overrides, and reference expansion enabled. + + + + + + + New-InfisicalSecret + Creates a new Infisical secret, with support for SecureString values and bulk creation. + New + InfisicalSecret + + + Creates one or many secrets. Three parameter sets are supported: PlainText (SecretName + SecretValue), SecureString (SecretName + SecureSecretValue), and Bulk (an array of hashtables piped or supplied via -Secrets). Honors -WhatIf and -Confirm. + + + Notes + + Pass -SkipMultilineEncoding when the value already contains literal newlines that the server should preserve verbatim. Use -TagIds to attach tag references at creation time. + + + + + EXAMPLE 1 + New-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'super-secret-value' + Creates a single shared secret in the active project/environment. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags -ProjectId $ConnectInfisicalParameters.ProjectId + +$NewInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalSecretParameters.SecretName = 'API_KEY' +$NewInfisicalSecretParameters.SecretValue = 'super-secret-value' +$NewInfisicalSecretParameters.SecretComment = 'Issued by deployment pipeline' +$NewInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$NewInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$NewInfisicalSecretParameters.TagIds = @($GetInfisicalTagsResult[0].Id) +$NewInfisicalSecretParameters.Verbose = $True + +$NewInfisicalSecretResult = New-InfisicalSecret @NewInfisicalSecretParameters + Looks up tags to attach, then creates a single secret with a comment and tag association under a script-specific subpath. + + + + + + + Update-InfisicalSecret + Updates an existing Infisical secret value, comment, name, or tags. + Update + InfisicalSecret + + + Updates one or many secrets. Supports PlainText, SecureString, and Bulk parameter sets. Use -NewSecretName to rename a secret, -SecretComment to update its comment, and -TagIds to replace tag associations. Honors -WhatIf and -Confirm. + + + Notes + + Only the parameters you bind are sent; omitted scalar parameters are not modified server-side. The Bulk parameter set accepts pipeline input of hashtables containing SecretName/SecretValue/etc. + + + + + EXAMPLE 1 + Update-InfisicalSecret -SecretName 'API_KEY' -SecretValue 'rotated-value' + Rotates the API_KEY secret in the active project/environment. + + + EXAMPLE 2 + $UpdateInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalSecretParameters.SecretName = 'API_KEY' +$UpdateInfisicalSecretParameters.NewSecretName = 'API_KEY_V2' +$UpdateInfisicalSecretParameters.SecretValue = 'rotated-value' +$UpdateInfisicalSecretParameters.SecretComment = 'Rotated by scheduled job' +$UpdateInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$UpdateInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$UpdateInfisicalSecretParameters.Verbose = $True + +$UpdateInfisicalSecretResult = Update-InfisicalSecret @UpdateInfisicalSecretParameters + Rotates the value, renames the secret, and updates its comment in a single call. + + + + + + + Remove-InfisicalSecret + Deletes one or many Infisical secrets by name. + Remove + InfisicalSecret + + + Deletes a single secret (Single parameter set) or a batch of secrets by name (Bulk parameter set). High ConfirmImpact triggers prompts by default. -PassThru emits the removed secret names. + + + Notes + + Removal is irreversible from this cmdlet's perspective; rely on Infisical's audit log or secret-version history for forensics. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalSecret -SecretName 'API_KEY_V1' -Confirm:$False + Deletes a single secret without prompting. + + + EXAMPLE 2 + $RemoveInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalSecretParameters.SecretNames = @('LEGACY_KEY_1','LEGACY_KEY_2','LEGACY_KEY_3') +$RemoveInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalSecretParameters.Environment = $ConnectInfisicalParameters.Environment +$RemoveInfisicalSecretParameters.SecretPath = "/Windows/$($CallingScriptPath.BaseName)" +$RemoveInfisicalSecretParameters.PassThru = $True +$RemoveInfisicalSecretParameters.Confirm = $False +$RemoveInfisicalSecretParameters.Verbose = $True + +$RemoveInfisicalSecretResult = Remove-InfisicalSecret @RemoveInfisicalSecretParameters + Bulk-deletes three legacy secrets and returns the removed names for audit logging. + + + + + + + Copy-InfisicalSecret + Duplicates one or more secrets into a different environment or secret path. + Copy + InfisicalSecret + + + Server-side duplicates an array of secret IDs into a destination environment (and optional destination path), with switches that control whether the value, comment, tags, and metadata are copied. Use Get-InfisicalSecrets followed by selection of the desired Id values to feed -SecretId. + + + Notes + + Set -OverwriteExisting to replace same-named secrets at the destination. Without -CopySecretValue, the destination secrets are created with empty values, preserving only metadata. + + + + + EXAMPLE 1 + Get-InfisicalSecrets | Select-Object -ExpandProperty Id | Copy-InfisicalSecret -DestinationEnvironment 'staging' -CopySecretValue + Copies all secrets from the active environment into 'staging', including their values. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath '/Windows' -Recursive + +$CopyInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$CopyInfisicalSecretParameters.SecretId = $GetInfisicalSecretsResult.Id +$CopyInfisicalSecretParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$CopyInfisicalSecretParameters.SourceEnvironment = $ConnectInfisicalParameters.Environment +$CopyInfisicalSecretParameters.SourceSecretPath = '/Windows' +$CopyInfisicalSecretParameters.DestinationEnvironment = 'staging' +$CopyInfisicalSecretParameters.DestinationSecretPath = '/Windows' +$CopyInfisicalSecretParameters.OverwriteExisting = $True +$CopyInfisicalSecretParameters.CopySecretValue = $True +$CopyInfisicalSecretParameters.CopySecretComment = $True +$CopyInfisicalSecretParameters.CopyTags = $True +$CopyInfisicalSecretParameters.CopyMetadata = $True +$CopyInfisicalSecretParameters.Verbose = $True + +$CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters + Promotes every Windows secret from the active environment into staging with full value/comment/tag/metadata propagation. + + + + + + + ConvertTo-InfisicalSecretDictionary + Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. + ConvertTo + InfisicalSecretDictionary + + + Aggregates an incoming pipeline of InfisicalSecret objects into a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText to materialize string values. Duplicate keys are handled via the -DuplicateKeyBehavior parameter (Error, FirstWins, LastWins). + + + Notes + + Use this conversion before splatting secrets into another process (-AsPlainText) or before passing them to libraries that expect SecureString-keyed lookups (default). + + + + + EXAMPLE 1 + Get-InfisicalSecrets | ConvertTo-InfisicalSecretDictionary -AsPlainText + Builds a plain-text dictionary of every secret in the active environment. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ConvertToInfisicalSecretDictionaryParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalSecretDictionaryParameters.InputObject = $GetInfisicalSecretsResult +$ConvertToInfisicalSecretDictionaryParameters.DuplicateKeyBehavior = 'LastWins' +$ConvertToInfisicalSecretDictionaryParameters.AsPlainText = $True +$ConvertToInfisicalSecretDictionaryParameters.Verbose = $True + +$ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters + Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions. + + + + + + + Export-InfisicalSecrets + Exports InfisicalSecret objects to disk or environment variables in a chosen file format. + Export + InfisicalSecrets + + + Buffers an incoming pipeline of InfisicalSecret objects and writes them to a file in the requested format (DotEnv, Json, Yaml, EnvironmentVariables, etc.) or sets them as environment variables on the chosen scope (Process, User, Machine). -Encoding controls text encoding for file outputs. + + + Notes + + EnvironmentVariables format does not require -Path; all other formats do. User/Machine scopes require appropriate privileges (Machine scope requires elevation on Windows). + + + + + EXAMPLE 1 + Get-InfisicalSecrets | Export-InfisicalSecrets -Format DotEnv -Path '.\.env' -Force + Writes the active environment's secrets to a .env file. + + + EXAMPLE 2 + $GetInfisicalSecretsResult = Get-InfisicalSecrets -SecretPath "/Windows/$($CallingScriptPath.BaseName)" -Recursive + +$ExportInfisicalSecretsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalSecretsParameters.InputObject = $GetInfisicalSecretsResult +$ExportInfisicalSecretsParameters.Format = 'EnvironmentVariables' +$ExportInfisicalSecretsParameters.Scope = 'Process' +$ExportInfisicalSecretsParameters.Force = $True +$ExportInfisicalSecretsParameters.Verbose = $True + +$ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsParameters + Projects the recursive secret result into Process-scope environment variables for the current PowerShell session. + + + + + + + Get-InfisicalProjects + Lists Infisical projects accessible to the current identity. + Get + InfisicalProjects + + + Returns every project the active session can see. The cmdlet requires an active InfisicalConnection but takes no parameters; project visibility is governed by Infisical's role assignments. + + + Notes + + The result is an array of InfisicalProject objects; pipe into Where-Object or Select-Object to filter by Slug, Name, or Id. + + + + + EXAMPLE 1 + Get-InfisicalProjects + Lists every project the current session can see. + + + EXAMPLE 2 + $GetInfisicalProjectsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalProjectsParameters.Verbose = $True + +$GetInfisicalProjectsResult = Get-InfisicalProjects @GetInfisicalProjectsParameters | Where-Object { $_.Slug -ilike 'platform-*' } + Lists projects and filters down to those whose slug begins with 'platform-'. + + + + + + + Get-InfisicalProject + Retrieves a single Infisical project by its identifier. + Get + InfisicalProject + + + Retrieves one project by Id. If -ProjectId is not supplied, the cmdlet falls back to the ProjectId pinned on the active InfisicalConnection. + + + Notes + + The cmdlet accepts pipeline input by property name; objects emitted by Get-InfisicalProjects can be piped in directly to refresh a single record. + + + + + EXAMPLE 1 + Get-InfisicalProject + Retrieves the project pinned by the active session. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects + +$GetInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult[0].Id +$GetInfisicalProjectParameters.Verbose = $True + +$GetInfisicalProjectResult = Get-InfisicalProject @GetInfisicalProjectParameters + Looks up the first project in the list and retrieves its full record. + + + + + + + New-InfisicalProject + Creates a new Infisical project in the active organization. + New + InfisicalProject + + + Creates a project with the supplied name and optional slug, description, type, and organization id. If -OrganizationId is not supplied, the active session's organization is used. Honors -WhatIf and -Confirm. + + + Notes + + Slug must be unique within the organization; if not supplied, the server derives one from the project name. + + + + + EXAMPLE 1 + New-InfisicalProject -ProjectName 'Platform Telemetry' + Creates a new project named 'Platform Telemetry' in the active organization. + + + EXAMPLE 2 + $NewInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalProjectParameters.ProjectName = 'Platform Telemetry' +$NewInfisicalProjectParameters.Slug = 'platform-telemetry' +$NewInfisicalProjectParameters.Description = 'Secrets for platform telemetry pipeline' +$NewInfisicalProjectParameters.Type = 'secret-manager' +$NewInfisicalProjectParameters.OrganizationId = $ConnectInfisicalParameters.OrganizationId +$NewInfisicalProjectParameters.Verbose = $True + +$NewInfisicalProjectResult = New-InfisicalProject @NewInfisicalProjectParameters + Creates a project with an explicit slug, description, and type bound to a specific organization id. + + + + + + + Update-InfisicalProject + Updates the name, description, or auto-capitalization flag on an existing project. + Update + InfisicalProject + + + Updates mutable attributes on a project. -ProjectId defaults to the pinned session project when omitted. Only parameters that are bound are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + AutoCapitalization controls whether secret names submitted in mixed case are stored uppercase server-side; setting it false preserves the literal case supplied by clients. + + + + + EXAMPLE 1 + Update-InfisicalProject -Name 'Platform Telemetry (v2)' + Renames the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$UpdateInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult.Id +$UpdateInfisicalProjectParameters.Name = 'Platform Telemetry (v2)' +$UpdateInfisicalProjectParameters.Description = 'Migrated to v2 pipeline' +$UpdateInfisicalProjectParameters.AutoCapitalization = $False +$UpdateInfisicalProjectParameters.Verbose = $True + +$UpdateInfisicalProjectResult = Update-InfisicalProject @UpdateInfisicalProjectParameters + Locates the project by slug, renames it, updates the description, and disables auto-capitalization. + + + + + + + Remove-InfisicalProject + Deletes an Infisical project. + Remove + InfisicalProject + + + Deletes a project by Id. Defaults to the session-pinned project when -ProjectId is omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed project id. + + + Notes + + This is destructive and removes all secrets, environments, folders, and tags within the project. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalProject -Confirm:$False + Deletes the session-pinned project without prompting. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'sandbox-temp' } + +$RemoveInfisicalProjectParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalProjectParameters.ProjectId = $GetInfisicalProjectsResult.Id +$RemoveInfisicalProjectParameters.PassThru = $True +$RemoveInfisicalProjectParameters.Confirm = $False +$RemoveInfisicalProjectParameters.Verbose = $True + +$RemoveInfisicalProjectResult = Remove-InfisicalProject @RemoveInfisicalProjectParameters + Finds the sandbox project by slug, removes it without confirmation, and emits the removed project id for logging. + + + + + + + Get-InfisicalEnvironments + Lists environments defined on an Infisical project. + Get + InfisicalEnvironments + + + Returns all environments configured on a project. -ProjectId defaults to the session-pinned project id when omitted. + + + Notes + + Each InfisicalEnvironment carries both Id and Slug; downstream cmdlets accept either form on -Environment-like parameters. + + + + + EXAMPLE 1 + Get-InfisicalEnvironments + Lists environments for the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalEnvironmentsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalEnvironmentsParameters.ProjectId = $GetInfisicalProjectsResult.Id +$GetInfisicalEnvironmentsParameters.Verbose = $True + +$GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments @GetInfisicalEnvironmentsParameters + Resolves a project by slug and lists every environment defined on it. + + + + + + + Get-InfisicalEnvironment + Retrieves a single Infisical environment by slug or id. + Get + InfisicalEnvironment + + + Returns one environment record by slug or id (-EnvironmentSlugOrId). -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalEnvironment objects from Get-InfisicalEnvironments can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalEnvironment -EnvironmentSlugOrId 'dev' + Retrieves the 'dev' environment from the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'dev' } + +$GetInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalEnvironmentParameters.EnvironmentSlugOrId = $GetInfisicalEnvironmentsResult.Slug +$GetInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalEnvironmentParameters.Verbose = $True + +$GetInfisicalEnvironmentResult = Get-InfisicalEnvironment @GetInfisicalEnvironmentParameters + Looks up the dev environment by slug and re-fetches the canonical record by slug or id. + + + + + + + New-InfisicalEnvironment + Creates a new environment on an Infisical project. + New + InfisicalEnvironment + + + Creates an environment with the supplied display name and slug, optionally setting its sort -Position. -ProjectId defaults to the session-pinned project when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Slugs must be unique within the project and are used as the canonical -Environment value across all other cmdlets. + + + + + EXAMPLE 1 + New-InfisicalEnvironment -Name 'Staging' -Slug 'staging' + Adds a Staging environment to the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$NewInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalEnvironmentParameters.ProjectId = $GetInfisicalProjectsResult.Id +$NewInfisicalEnvironmentParameters.Name = 'Staging' +$NewInfisicalEnvironmentParameters.Slug = 'staging' +$NewInfisicalEnvironmentParameters.Position = 20 +$NewInfisicalEnvironmentParameters.Verbose = $True + +$NewInfisicalEnvironmentResult = New-InfisicalEnvironment @NewInfisicalEnvironmentParameters + Adds a Staging environment at sort position 20 on the resolved project. + + + + + + + Update-InfisicalEnvironment + Updates the name, slug, or sort order of an existing Infisical environment. + Update + InfisicalEnvironment + + + Updates an environment identified by -EnvironmentId. -ProjectId defaults to the session-pinned project when omitted. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug can break downstream automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalEnvironment -EnvironmentId $EnvId -Name 'Pre-Production' + Renames an environment in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'staging' } + +$UpdateInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentsResult.Id +$UpdateInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalEnvironmentParameters.Name = 'Pre-Production' +$UpdateInfisicalEnvironmentParameters.Slug = 'preprod' +$UpdateInfisicalEnvironmentParameters.Position = 25 +$UpdateInfisicalEnvironmentParameters.Verbose = $True + +$UpdateInfisicalEnvironmentResult = Update-InfisicalEnvironment @UpdateInfisicalEnvironmentParameters + Locates the staging environment, renames it to Pre-Production, rotates its slug, and updates its sort order. + + + + + + + Remove-InfisicalEnvironment + Deletes an Infisical environment from a project. + Remove + InfisicalEnvironment + + + Removes an environment by Id. -ProjectId defaults to the session-pinned project when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed environment id. + + + Notes + + Removing an environment deletes every secret and folder scoped to it. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalEnvironment -EnvironmentId $EnvId -Confirm:$False + Deletes an environment without prompting. + + + EXAMPLE 2 + $GetInfisicalEnvironmentsResult = Get-InfisicalEnvironments | Where-Object { $_.Slug -eq 'sandbox' } + +$RemoveInfisicalEnvironmentParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalEnvironmentParameters.EnvironmentId = $GetInfisicalEnvironmentsResult.Id +$RemoveInfisicalEnvironmentParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalEnvironmentParameters.PassThru = $True +$RemoveInfisicalEnvironmentParameters.Confirm = $False +$RemoveInfisicalEnvironmentParameters.Verbose = $True + +$RemoveInfisicalEnvironmentResult = Remove-InfisicalEnvironment @RemoveInfisicalEnvironmentParameters + Removes the sandbox environment without prompting and emits its id for the audit trail. + + + + + + + Get-InfisicalFolders + Lists Infisical folders at a given secret path. + Get + InfisicalFolders + + + Enumerates folders directly under the supplied -Path within the active project and environment. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. + + + Notes + + This is a non-recursive listing of immediate subfolders. To enumerate secrets across a folder subtree use Get-InfisicalSecrets -Recursive. + + + + + EXAMPLE 1 + Get-InfisicalFolders -Path '/Windows' + Lists every folder directly under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalFoldersParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalFoldersParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalFoldersParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalFoldersParameters.Path = "/Windows/$($CallingScriptPath.BaseName)" +$GetInfisicalFoldersParameters.Verbose = $True + +$GetInfisicalFoldersResult = Get-InfisicalFolders @GetInfisicalFoldersParameters + Lists folders under a script-specific subpath using the session-pinned project and environment. + + + + + + + Get-InfisicalFolder + Retrieves a single Infisical folder by name or id. + Get + InfisicalFolder + + + Returns one folder record by name or id (-FolderNameOrId) under the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalFolder objects from Get-InfisicalFolders can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalFolder -FolderNameOrId 'Deployments' -Path '/Windows' + Retrieves the Deployments folder under /Windows in the active project and environment. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$GetInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalFolderParameters.FolderNameOrId = $GetInfisicalFoldersResult.Id +$GetInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$GetInfisicalFolderParameters.Path = '/Windows' +$GetInfisicalFolderParameters.Verbose = $True + +$GetInfisicalFolderResult = Get-InfisicalFolder @GetInfisicalFolderParameters + Locates the folder by name first, then re-fetches it by id to refresh the canonical record. + + + + + + + New-InfisicalFolder + Creates a new Infisical folder under the supplied parent path. + New + InfisicalFolder + + + Creates a folder with the supplied -Name beneath the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Folder names are case-sensitive and must be unique within a parent path; the cmdlet does not create intermediate folders. + + + + + EXAMPLE 1 + New-InfisicalFolder -Name 'Deployments' -Path '/Windows' + Creates the Deployments folder under /Windows in the active project and environment. + + + EXAMPLE 2 + $NewInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalFolderParameters.Name = $CallingScriptPath.BaseName +$NewInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$NewInfisicalFolderParameters.Path = '/Windows' +$NewInfisicalFolderParameters.Verbose = $True + +$NewInfisicalFolderResult = New-InfisicalFolder @NewInfisicalFolderParameters + Creates a script-named folder under /Windows using the session-pinned project and environment. + + + + + + + Update-InfisicalFolder + Renames an existing Infisical folder. + Update + InfisicalFolder + + + Renames a folder identified by -FolderId to the supplied -Name. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Renaming a folder rewrites the path component for every secret beneath it; coordinate with consumers that pin to the previous path. + + + + + EXAMPLE 1 + Update-InfisicalFolder -FolderId $FolderId -Name 'Deployments-Archive' + Renames a folder in the session-pinned project/environment. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq 'Deployments' } + +$UpdateInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalFolderParameters.FolderId = $GetInfisicalFoldersResult.Id +$UpdateInfisicalFolderParameters.Name = 'Deployments-Archive' +$UpdateInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$UpdateInfisicalFolderParameters.Path = '/Windows' +$UpdateInfisicalFolderParameters.Verbose = $True + +$UpdateInfisicalFolderResult = Update-InfisicalFolder @UpdateInfisicalFolderParameters + Resolves the folder by name and renames it to Deployments-Archive. + + + + + + + Remove-InfisicalFolder + Deletes an Infisical folder and all secrets it contains. + Remove + InfisicalFolder + + + Removes a folder by Id from the supplied -Path. -ProjectId, -Environment, and -Path default to the session-pinned values when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed folder id. + + + Notes + + This is destructive and removes every secret and subfolder under the target folder. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalFolder -FolderId $FolderId -Confirm:$False + Deletes a folder from the session-pinned project/environment without prompting. + + + EXAMPLE 2 + $GetInfisicalFoldersResult = Get-InfisicalFolders -Path '/Windows' | Where-Object { $_.Name -eq $CallingScriptPath.BaseName } + +$RemoveInfisicalFolderParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalFolderParameters.FolderId = $GetInfisicalFoldersResult.Id +$RemoveInfisicalFolderParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalFolderParameters.Environment = $ConnectInfisicalParameters.Environment +$RemoveInfisicalFolderParameters.Path = '/Windows' +$RemoveInfisicalFolderParameters.PassThru = $True +$RemoveInfisicalFolderParameters.Confirm = $False +$RemoveInfisicalFolderParameters.Verbose = $True + +$RemoveInfisicalFolderResult = Remove-InfisicalFolder @RemoveInfisicalFolderParameters + Resolves the script-named folder under /Windows and removes it without prompting, returning its id for logging. + + + + + + + Get-InfisicalTags + Lists Infisical tags defined on a project. + Get + InfisicalTags + + + Returns every tag configured on a project. -ProjectId defaults to the session-pinned project id when omitted. + + + Notes + + Tag Ids returned here are the values to pass on -TagIds when creating or updating secrets. + + + + + EXAMPLE 1 + Get-InfisicalTags + Lists every tag defined on the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalProjectsResult = Get-InfisicalProjects | Where-Object { $_.Slug -eq 'platform-telemetry' } + +$GetInfisicalTagsParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalTagsParameters.ProjectId = $GetInfisicalProjectsResult.Id +$GetInfisicalTagsParameters.Verbose = $True + +$GetInfisicalTagsResult = Get-InfisicalTags @GetInfisicalTagsParameters + Resolves a project by slug and lists every tag defined on it. + + + + + + + Get-InfisicalTag + Retrieves a single Infisical tag by slug or id. + Get + InfisicalTag + + + Returns one tag record by slug or id (-TagSlugOrId). -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Accepts pipeline input by property name so InfisicalTag objects from Get-InfisicalTags can be refreshed directly. + + + + + EXAMPLE 1 + Get-InfisicalTag -TagSlugOrId 'critical' + Retrieves the 'critical' tag from the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical' } + +$GetInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalTagParameters.TagSlugOrId = $GetInfisicalTagsResult.Slug +$GetInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalTagParameters.Verbose = $True + +$GetInfisicalTagResult = Get-InfisicalTag @GetInfisicalTagParameters + Filters tags to the critical slug and refetches the canonical record. + + + + + + + New-InfisicalTag + Creates a new Infisical tag on a project. + New + InfisicalTag + + + Creates a tag with the supplied -Slug, optional -Name and -Color. -ProjectId defaults to the session-pinned project when omitted. Honors -WhatIf and -Confirm. + + + Notes + + Tag slugs must be unique within the project and are the canonical reference used by tag-filtered secret lookups. + + + + + EXAMPLE 1 + New-InfisicalTag -Slug 'critical' -Name 'Critical' -Color '#FF0000' + Creates a red Critical tag in the session-pinned project. + + + EXAMPLE 2 + $NewInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$NewInfisicalTagParameters.Slug = 'critical' +$NewInfisicalTagParameters.Name = 'Critical' +$NewInfisicalTagParameters.Color = '#FF0000' +$NewInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$NewInfisicalTagParameters.Verbose = $True + +$NewInfisicalTagResult = New-InfisicalTag @NewInfisicalTagParameters + Creates a red Critical tag against an explicitly supplied project id. + + + + + + + Update-InfisicalTag + Updates the slug, name, or color of an existing Infisical tag. + Update + InfisicalTag + + + Updates a tag identified by -TagId. -ProjectId defaults to the session-pinned project when omitted. Only bound parameters are sent to the server. Honors -WhatIf and -Confirm. + + + Notes + + Changing -Slug breaks tag-filtered automation that pins to the previous slug. Coordinate slug rotation with consumers. + + + + + EXAMPLE 1 + Update-InfisicalTag -TagId $TagId -Color '#FFA500' + Changes the display color of a tag in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical' } + +$UpdateInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UpdateInfisicalTagParameters.TagId = $GetInfisicalTagsResult.Id +$UpdateInfisicalTagParameters.Slug = 'critical-v2' +$UpdateInfisicalTagParameters.Name = 'Critical (v2)' +$UpdateInfisicalTagParameters.Color = '#FFA500' +$UpdateInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$UpdateInfisicalTagParameters.Verbose = $True + +$UpdateInfisicalTagResult = Update-InfisicalTag @UpdateInfisicalTagParameters + Locates the critical tag and rotates its slug, display name, and color. + + + + + + + Remove-InfisicalTag + Deletes an Infisical tag from a project. + Remove + InfisicalTag + + + Removes a tag by Id. -ProjectId defaults to the session-pinned project when omitted. High ConfirmImpact prompts unless -Confirm:$False is supplied. -PassThru emits the removed tag id. + + + Notes + + Removing a tag detaches it from every secret it was applied to but does not delete the secrets themselves. Honors -WhatIf and -Confirm. + + + + + EXAMPLE 1 + Remove-InfisicalTag -TagId $TagId -Confirm:$False + Deletes a tag from the session-pinned project without prompting. + + + EXAMPLE 2 + $GetInfisicalTagsResult = Get-InfisicalTags | Where-Object { $_.Slug -eq 'critical-v2' } + +$RemoveInfisicalTagParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RemoveInfisicalTagParameters.TagId = $GetInfisicalTagsResult.Id +$RemoveInfisicalTagParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RemoveInfisicalTagParameters.PassThru = $True +$RemoveInfisicalTagParameters.Confirm = $False +$RemoveInfisicalTagParameters.Verbose = $True + +$RemoveInfisicalTagResult = Remove-InfisicalTag @RemoveInfisicalTagParameters + Resolves a tag by slug and removes it without prompting, returning its id for the audit trail. + + + + + + + Get-InfisicalCertificateAuthority + Lists or retrieves Infisical internal Certificate Authorities. + Get + InfisicalCertificateAuthority + + + When -CaId is supplied (ById parameter set) returns a single CA. Otherwise (List parameter set) returns every internal CA visible in the project. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Only internal CAs are surfaced; external/ACME issuers are not enumerated by this cmdlet. CA Ids returned here are the values to pass on -CertificateAuthorityId to Request-InfisicalCertificate. + + + + + EXAMPLE 1 + Get-InfisicalCertificateAuthority + Lists every internal CA visible in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificateAuthorityParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateAuthorityParameters.CaId = $GetInfisicalCertificateAuthorityListResult.Id +$GetInfisicalCertificateAuthorityParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalCertificateAuthorityParameters.Verbose = $True + +$GetInfisicalCertificateAuthorityResult = Get-InfisicalCertificateAuthority @GetInfisicalCertificateAuthorityParameters + Filters the CA list by friendly name and then re-fetches the canonical CA record by id. + + + + + + + Get-InfisicalCertificates + Lists Infisical certificates in a project, with optional filters and automatic paging. + Get + InfisicalCertificates + + + Enumerates certificates with optional filters for -CommonName, -FriendlyName, -Status, and -CaId. -Limit and -Offset drive a single page; pages are walked automatically until exhausted unless -NoAutoPage is supplied. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + For advanced filtering (validity window, key algorithm, extended key usage, etc.) use Search-InfisicalCertificate instead. + + + + + EXAMPLE 1 + Get-InfisicalCertificates -Status 'active' + Lists every active certificate in the session-pinned project. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$GetInfisicalCertificatesParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificatesParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$GetInfisicalCertificatesParameters.CommonName = $env:COMPUTERNAME +$GetInfisicalCertificatesParameters.FriendlyName = 'web-tier' +$GetInfisicalCertificatesParameters.Status = 'active' +$GetInfisicalCertificatesParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$GetInfisicalCertificatesParameters.Limit = 100 +$GetInfisicalCertificatesParameters.Verbose = $True + +$GetInfisicalCertificatesResult = Get-InfisicalCertificates @GetInfisicalCertificatesParameters + Resolves the issuing CA, then lists active certificates scoped to that CA, the local hostname, and the 'web-tier' friendly name. + + + + + + + Get-InfisicalCertificate + Retrieves a single Infisical certificate by serial number. + Get + InfisicalCertificate + + + Returns one certificate record by -SerialNumber. Accepts pipeline input by property name so InfisicalCertificate objects from list/search cmdlets can be re-fetched directly. + + + Notes + + This returns metadata only. To obtain certificate and chain PEM material use ConvertTo-InfisicalCertificate or Export-InfisicalCertificate. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -SerialNumber '7A:F2:1B:...:9E' + Retrieves the certificate record for the supplied serial number. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$GetInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$GetInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$GetInfisicalCertificateParameters.Verbose = $True + +$GetInfisicalCertificateResult = Get-InfisicalCertificate @GetInfisicalCertificateParameters + Selects the active certificate whose common name matches the host and refetches its canonical record. + + + + + + + Search-InfisicalCertificate + Searches Infisical certificates with advanced filters and automatic paging. + Search + InfisicalCertificate + + + Performs a server-side search across certificates with filters for friendly name, common name, free-text search, status, CA/profile/application/enrollment scope, key/signature algorithm, source, and validity window (-NotBeforeFrom/-NotBeforeTo/-NotAfterFrom/-NotAfterTo). Results are paged automatically unless -NoAutoPage is supplied. -ProjectId defaults to the session-pinned project when omitted. + + + Notes + + Use -SortBy together with -SortOrder ('asc'/'desc') to control result ordering. Pair with Get-InfisicalCertificate or Export-InfisicalCertificate to drill into specific hits. + + + + + EXAMPLE 1 + Search-InfisicalCertificate -Search $env:COMPUTERNAME -Status 'active' + Finds active certificates whose searchable fields contain the local hostname. + + + EXAMPLE 2 + $GetInfisicalCertificateAuthorityListResult = Get-InfisicalCertificateAuthority | Where-Object { $_.FriendlyName -eq 'Issuing CA - Platform' } + +$SearchInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$SearchInfisicalCertificateParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$SearchInfisicalCertificateParameters.CommonName = $env:COMPUTERNAME +$SearchInfisicalCertificateParameters.Status = 'active' +$SearchInfisicalCertificateParameters.CaId = @($GetInfisicalCertificateAuthorityListResult.Id) +$SearchInfisicalCertificateParameters.KeyAlgorithm = @('RSA') +$SearchInfisicalCertificateParameters.NotAfterTo = (Get-Date).AddDays(30) +$SearchInfisicalCertificateParameters.SortBy = 'notAfter' +$SearchInfisicalCertificateParameters.SortOrder = 'asc' +$SearchInfisicalCertificateParameters.Limit = 100 +$SearchInfisicalCertificateParameters.Verbose = $True + +$SearchInfisicalCertificateResult = Search-InfisicalCertificate @SearchInfisicalCertificateParameters + Searches for RSA certificates from a specific CA, scoped to the local hostname, that expire within the next 30 days, sorted soonest-first. + + + + + + + Request-InfisicalCertificate + Requests a new Infisical certificate (local CSR + sign) or reuses a still-valid existing one. + Request + InfisicalCertificate + + + Generates a keypair locally, builds a CSR, and submits it for signing either via a PKI subscriber (-PkiSubscriberSlug, default parameter set) or by direct CA signing (-CertificateAuthorityId). On subsequent runs an existing certificate whose CN matches and whose remaining lifetime exceeds -RenewalThresholdDays is reused; pass -Force to always issue or -AllowRenewal to allow rotation inside the threshold. Optional flags install the leaf (-Install) and chain (-InstallChain) into a Windows certificate store, and control private-key protection (-PrivateKeyProtection, -PersistKey, -MachineKey, -PrivateKeyPath, -KeyStorageFlags). Honors -WhatIf and -Confirm. + + + Notes + + Default -PrivateKeyProtection is 'LocalOnly': the leaf is loaded into memory without persisting the private key and PrivateKeyPem is scrubbed from the emitted result unless -PrivateKeyPath or an explicit -KeyStorageFlags binding overrides it. The reuse path completes its chain from the Infisical bundle when local stores are incomplete; pass -LocalChainOnly to suppress that fetch entirely. + + + + + EXAMPLE 1 + Request-InfisicalCertificate -PkiSubscriberSlug 'web-tier' -Install + Requests (or reuses) a certificate for the 'web-tier' subscriber and installs it into CurrentUser\My. + + + EXAMPLE 2 + $RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$RequestInfisicalCertificateParameters.PkiSubscriberSlug = 'web-tier' +$RequestInfisicalCertificateParameters.ProjectId = $ConnectInfisicalParameters.ProjectId +$RequestInfisicalCertificateParameters.CommonName = ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName +$RequestInfisicalCertificateParameters.DnsName = @(([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME)).HostName, $env:COMPUTERNAME) +$RequestInfisicalCertificateParameters.KeyAlgorithm = 'Rsa' +$RequestInfisicalCertificateParameters.KeySize = 3072 +$RequestInfisicalCertificateParameters.Install = $True +$RequestInfisicalCertificateParameters.InstallChain = $True +$RequestInfisicalCertificateParameters.StoreName = 'My' +$RequestInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$RequestInfisicalCertificateParameters.PrivateKeyProtection = 'NonExportable' +$RequestInfisicalCertificateParameters.MachineKey = $True +$RequestInfisicalCertificateParameters.PersistKey = $True +$RequestInfisicalCertificateParameters.AllowRenewal = $True +$RequestInfisicalCertificateParameters.RenewalThresholdDays = 30 +$RequestInfisicalCertificateParameters.Verbose = $True + +$RequestInfisicalCertificateResult = Request-InfisicalCertificate @RequestInfisicalCertificateParameters + Issues (or renews within 30 days) a 3072-bit RSA certificate for the local FQDN, installs the leaf and chain into LocalMachine\My with a non-exportable machine-bound persistent key. + + + + + + + ConvertTo-InfisicalCertificate + Materializes an X509Certificate2 from an Infisical certificate record, bundle, or serial number. + ConvertTo + InfisicalCertificate + + + Fetches the certificate bundle (when given an InfisicalCertificate or -SerialNumber), or accepts an already-fetched -Bundle, and constructs an X509Certificate2 from the PEM material. Use -NoPrivateKey to omit the private key, -KeyStorageFlags to control how the key is loaded, and -IncludeChain to additionally emit each chain certificate as a separate X509Certificate2 in the pipeline. + + + Notes + + The bundle for any given certificate is typically retrievable only once after issuance; -SerialNumber and pipeline modes will fail with a bundle-not-available error for older certificates. Use -KeyStorageFlags Exportable when callers need to re-export the resulting cert as PFX. + + + + + EXAMPLE 1 + Get-InfisicalCertificate -SerialNumber $Serial | ConvertTo-InfisicalCertificate -IncludeChain + Materializes the certificate and emits each chain element individually. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ConvertToInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ConvertToInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$ConvertToInfisicalCertificateParameters.NoPrivateKey = $False +$ConvertToInfisicalCertificateParameters.IncludeChain = $True +$ConvertToInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable +$ConvertToInfisicalCertificateParameters.Verbose = $True + +$ConvertToInfisicalCertificateResult = ConvertTo-InfisicalCertificate @ConvertToInfisicalCertificateParameters + Selects the active certificate whose CN matches the host and materializes it (with private key and chain) as exportable X509Certificate2 objects. + + + + + + + Export-InfisicalCertificate + Exports an Infisical certificate to disk in PEM, PFX, or CER format. + Export + InfisicalCertificate + + + Writes a certificate to -Path in the supplied -Format. Accepts an X509Certificate2, an InfisicalCertificateBundle, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -Password (SecureString) supplies the PFX password. -IncludeChain appends chain certificates (PEM only). -NoPrivateKey omits the private key. -Force overwrites an existing file. Honors -WhatIf and -Confirm. + + + Notes + + PFX export requires the cert to have been loaded with X509KeyStorageFlags.Exportable; bundle/serial modes import with Exportable automatically. CER and PFX formats ignore -IncludeChain. + + + + + EXAMPLE 1 + Export-InfisicalCertificate -Path 'C:\Temp\web-tier.pem' -Format Pem -SerialNumber $Serial -IncludeChain + Exports a certificate, its chain, and private key (when available) as a single PEM bundle. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$ExportInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ExportInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$ExportInfisicalCertificateParameters.Path = "C:\Temp\$($env:COMPUTERNAME).pfx" +$ExportInfisicalCertificateParameters.Format = 'Pfx' +$ExportInfisicalCertificateParameters.Password = (Read-Host -AsSecureString -Prompt 'PFX password') +$ExportInfisicalCertificateParameters.Force = $True +$ExportInfisicalCertificateParameters.PassThru = $True +$ExportInfisicalCertificateParameters.Verbose = $True + +$ExportInfisicalCertificateResult = Export-InfisicalCertificate @ExportInfisicalCertificateParameters + Resolves the active host certificate by serial and exports it as a password-protected PFX, overwriting any existing file and emitting a FileInfo for downstream use. + + + + + + + Install-InfisicalCertificate + Installs an Infisical certificate (and optional chain) into a Windows certificate store. + Install + InfisicalCertificate + + + Adds a certificate to the supplied -StoreName and -StoreLocation. Accepts an X509Certificate2, an InfisicalCertificate (refetches bundle by serial), or a -SerialNumber. -KeyStorageFlags controls private-key loading. -IncludeChain installs each chain certificate to the CertificateAuthority store of the same -StoreLocation. -Force replaces an existing thumbprint. -PassThru emits the installed certificate. Honors -WhatIf and -Confirm. + + + Notes + + Installing into LocalMachine stores typically requires elevation. -IncludeChain only fires for serial/InfisicalCertificate inputs because the X509Certificate2 input has no associated bundle to walk. + + + + + EXAMPLE 1 + Install-InfisicalCertificate -SerialNumber $Serial -StoreLocation LocalMachine -IncludeChain + Installs the leaf into LocalMachine\My and each chain element into LocalMachine\CertificateAuthority. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'active' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$InstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$InstallInfisicalCertificateParameters.SerialNumber = $GetInfisicalCertificatesResult[0].SerialNumber +$InstallInfisicalCertificateParameters.StoreName = 'My' +$InstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$InstallInfisicalCertificateParameters.KeyStorageFlags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet +$InstallInfisicalCertificateParameters.IncludeChain = $True +$InstallInfisicalCertificateParameters.Force = $True +$InstallInfisicalCertificateParameters.PassThru = $True +$InstallInfisicalCertificateParameters.Verbose = $True + +$InstallInfisicalCertificateResult = Install-InfisicalCertificate @InstallInfisicalCertificateParameters + Resolves the active host certificate and installs the leaf (with a machine-bound persistent key) plus its chain into LocalMachine, replacing any existing thumbprint match. + + + + + + + Uninstall-InfisicalCertificate + Removes a certificate from a Windows certificate store by thumbprint, subject, or pipeline input. + Uninstall + InfisicalCertificate + + + Removes matching certificates from the supplied -StoreName and -StoreLocation. Accepts -Thumbprint, -Subject, an X509Certificate2 (-Certificate), or an InfisicalCertificate (-InfisicalCertificate, uses FingerprintSha1). -Force allows removing multiple matches in one call; -PassThru emits each removed certificate. Honors -WhatIf and -Confirm. + + + Notes + + When more than one certificate matches -Subject and -Force is not supplied the cmdlet throws to prevent accidental bulk removal. Uninstalling from LocalMachine stores typically requires elevation. + + + + + EXAMPLE 1 + Uninstall-InfisicalCertificate -Thumbprint $Thumbprint -StoreLocation LocalMachine + Removes the certificate with the supplied thumbprint from LocalMachine\My. + + + EXAMPLE 2 + $GetInfisicalCertificatesResult = Get-InfisicalCertificates -Status 'revoked' | Where-Object { $_.CommonName -eq $env:COMPUTERNAME } + +$UninstallInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$UninstallInfisicalCertificateParameters.InfisicalCertificate = $GetInfisicalCertificatesResult[0] +$UninstallInfisicalCertificateParameters.StoreName = 'My' +$UninstallInfisicalCertificateParameters.StoreLocation = 'LocalMachine' +$UninstallInfisicalCertificateParameters.Force = $True +$UninstallInfisicalCertificateParameters.PassThru = $True +$UninstallInfisicalCertificateParameters.Verbose = $True + +$UninstallInfisicalCertificateResult = Uninstall-InfisicalCertificate @UninstallInfisicalCertificateParameters + Picks the revoked host certificate and removes it from LocalMachine\My using its SHA1 fingerprint, emitting the removed object for the audit trail. + + + + + + + diff --git a/build.ps1 b/build.ps1 index ae27921..b928622 100644 --- a/build.ps1 +++ b/build.ps1 @@ -129,7 +129,10 @@ function Write-Manifest { 'Update-InfisicalTag', 'Remove-InfisicalTag', 'Get-InfisicalCertificateAuthority', + 'Get-InfisicalCertificate', + 'Get-InfisicalCertificates', 'Search-InfisicalCertificate', + 'Request-InfisicalCertificate', 'ConvertTo-InfisicalCertificate', 'Install-InfisicalCertificate', 'Uninstall-InfisicalCertificate', @@ -193,15 +196,50 @@ if (`$null -eq `$manifest) { Import-Module -Name '$($ModuleDirectory.FullName)' -Force -`$cmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTags','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Search-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate') -foreach (`$c in `$cmds) { - if (-not (Get-Command -Name `$c -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { - throw "Cmdlet not found: `$c" +`$cmds = @(Get-Command -Module PSInfisicalAPI -CommandType Cmdlet) +if (`$cmds.Count -eq 0) { + throw "No cmdlets were exported by the PSInfisicalAPI module." +} + +`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecrets','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProjects','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironments','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolders','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTags','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalCertificateAuthority','Get-InfisicalCertificate','Get-InfisicalCertificates','Search-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate') +foreach (`$expected in `$expectedCmds) { + if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { + throw "Cmdlet not found: `$expected" + } +} + +foreach (`$cmd in `$cmds) { + `$name = `$cmd.Name + `$help = Get-Help -Name `$name -Full -ErrorAction SilentlyContinue + if (`$null -eq `$help) { + throw "Get-Help returned nothing for cmdlet: `$name" } - `$help = Get-Help -Name `$c -ErrorAction SilentlyContinue - if (`$null -eq `$help) { - throw "Get-Help returned nothing for cmdlet: `$c" + `$synopsis = (`$help.Synopsis | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace(`$synopsis) -or `$synopsis.StartsWith(`$name, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Get-Help synopsis is missing or auto-generated for cmdlet: `$name" + } + + `$description = (`$help.description | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace(`$description)) { + throw "Get-Help description is empty for cmdlet: `$name" + } + + `$examples = Get-Help -Name `$name -Examples -ErrorAction SilentlyContinue + if (`$null -eq `$examples -or `$null -eq `$examples.examples -or `$null -eq `$examples.examples.example) { + throw "Get-Help -Examples returned no examples for cmdlet: `$name" + } + + `$exampleNodes = @(`$examples.examples.example) + if (`$exampleNodes.Count -lt 1) { + throw "Get-Help -Examples returned zero examples for cmdlet: `$name" + } + + foreach (`$example in `$exampleNodes) { + `$code = (`$example.code | Out-String).Trim() + if ([string]::IsNullOrWhiteSpace(`$code)) { + throw "Example with empty code block found for cmdlet: `$name" + } } } @@ -297,6 +335,35 @@ foreach ($assembly in $desiredAssemblies) { } } +Write-Step "Staging cmdlet help XML next to module binary" +$moduleCultureDirs = Get-ChildItem -LiteralPath $ModuleRoot.FullName -Directory -Force -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^[a-z]{2}(-[A-Za-z0-9]+)*$' } +foreach ($cultureDir in $moduleCultureDirs) { + $helpXmlSource = [System.IO.FileInfo][System.IO.Path]::Combine($cultureDir.FullName, 'PSInfisicalAPI.dll-Help.xml') + if (-not $helpXmlSource.Exists) { continue } + + $binCultureDir = [System.IO.DirectoryInfo][System.IO.Path]::Combine($ModuleBinDir.FullName, $cultureDir.Name) + Ensure-Directory -Directory $binCultureDir + Copy-Item -LiteralPath $helpXmlSource.FullName -Destination $binCultureDir.FullName -Force +} + +$primaryHelpXml = [System.IO.FileInfo][System.IO.Path]::Combine($ModuleBinDir.FullName, 'en-US', 'PSInfisicalAPI.dll-Help.xml') +if (-not $primaryHelpXml.Exists) { + throw "Help XML not found at '$($primaryHelpXml.FullName)'. Ensure Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml exists." +} + +try { + [xml]$helpDocument = Get-Content -LiteralPath $primaryHelpXml.FullName -Raw +} catch { + throw "Help XML at '$($primaryHelpXml.FullName)' failed to parse as XML: $_" +} + +$helpCommandCount = @($helpDocument.helpItems.command).Count +if ($helpCommandCount -lt 1) { + throw "Help XML at '$($primaryHelpXml.FullName)' contains no entries." +} +Write-Step "Help XML contains $helpCommandCount cmdlet entries." + $manifestPath = [System.IO.FileInfo][System.IO.Path]::Combine($ModuleRoot.FullName, 'PSInfisicalAPI.psd1') Write-Manifest -Path $manifestPath -ModuleVersion $buildVersion -CommitHash $commitHash diff --git a/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs b/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs index a9653b3..13d21f4 100644 --- a/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs +++ b/src/PSInfisicalAPI.Tests/CertificateMapperTests.cs @@ -12,6 +12,7 @@ namespace PSInfisicalAPI.Tests private static readonly Type CertDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateResponseDto", true); private static readonly Type CaMapperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCaMapper", true); private static readonly Type CaDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalInternalCaResponseDto", true); + private static readonly Type CaConfigDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalInternalCaConfigurationDto", true); private static readonly Type BundleDtoType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateBundleResponseDto", true); private static InfisicalCertificate InvokeCertMap(object dto, string fallbackProjectId) @@ -90,6 +91,43 @@ namespace PSInfisicalAPI.Tests Assert.Equal("proj-fallback", mapped.ProjectId); } + [Fact] + public void CaMap_Prefers_Configuration_Fields_Over_TopLevel() + { + object cfg = Activator.CreateInstance(CaConfigDtoType); + CaConfigDtoType.GetProperty("FriendlyName").SetValue(cfg, "C=US, CN=GSPA Intermediate"); + CaConfigDtoType.GetProperty("CommonName").SetValue(cfg, "GSPA Intermediate"); + CaConfigDtoType.GetProperty("OrganizationName").SetValue(cfg, "GSPA"); + CaConfigDtoType.GetProperty("OrganizationUnit").SetValue(cfg, "MECM"); + CaConfigDtoType.GetProperty("Country").SetValue(cfg, "US"); + CaConfigDtoType.GetProperty("KeyAlgorithm").SetValue(cfg, "RSA_2048"); + CaConfigDtoType.GetProperty("DistinguishedName").SetValue(cfg, "CN=GSPA Intermediate"); + CaConfigDtoType.GetProperty("SerialNumber").SetValue(cfg, "74a4b62197ad"); + CaConfigDtoType.GetProperty("MaxPathLength").SetValue(cfg, 0); + CaConfigDtoType.GetProperty("Type").SetValue(cfg, "intermediate"); + + object dto = Activator.CreateInstance(CaDtoType); + CaDtoType.GetProperty("Id").SetValue(dto, "ca-9"); + CaDtoType.GetProperty("Name").SetValue(dto, "intermediate-ca"); + CaDtoType.GetProperty("Type").SetValue(dto, "internal"); + CaDtoType.GetProperty("Status").SetValue(dto, "active"); + CaDtoType.GetProperty("Configuration").SetValue(dto, cfg); + + InfisicalCertificateAuthority mapped = InvokeCaMap(dto, "proj-fallback"); + Assert.Equal("ca-9", mapped.Id); + Assert.Equal("intermediate-ca", mapped.Name); + Assert.Equal("internal", mapped.Type); + Assert.Equal("C=US, CN=GSPA Intermediate", mapped.FriendlyName); + Assert.Equal("GSPA Intermediate", mapped.CommonName); + Assert.Equal("GSPA", mapped.OrganizationName); + Assert.Equal("MECM", mapped.OrganizationUnit); + Assert.Equal("US", mapped.Country); + Assert.Equal("RSA_2048", mapped.KeyAlgorithm); + Assert.Equal("CN=GSPA Intermediate", mapped.DistinguishedName); + Assert.Equal("74a4b62197ad", mapped.SerialNumber); + Assert.Equal(0, mapped.MaxPathLength); + } + [Fact] public void BundleMap_Maps_All_Pem_Fields() { diff --git a/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs b/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs new file mode 100644 index 0000000..e048a6a --- /dev/null +++ b/src/PSInfisicalAPI.Tests/CsrAndRequestCmdletTests.cs @@ -0,0 +1,479 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Reflection; +using PSInfisicalAPI.Endpoints; +using PSInfisicalAPI.Pki; +using Xunit; + +namespace PSInfisicalAPI.Tests +{ + public class CsrAndRequestCmdletTests + { + private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly; + + [Fact] + public void CsrBuilder_Rsa2048_Produces_Pem_Csr_And_PrivateKey_With_Subject_And_Sans() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject + { + CommonName = "test.contoso.local", + Organization = "Contoso", + Country = "US" + }; + + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Rsa, RsaKeySize = 2048 }; + InfisicalCsrResult result = InfisicalCsrBuilder.Build(subject, new[] { "test.contoso.local", "alt.contoso.local" }, new[] { "10.0.0.5" }, options); + + Assert.NotNull(result); + Assert.Contains("BEGIN CERTIFICATE REQUEST", result.CsrPem); + Assert.Contains("END CERTIFICATE REQUEST", result.CsrPem); + Assert.Contains("BEGIN RSA PRIVATE KEY", result.PrivateKeyPem); + + Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest pkcs10 = ReadCsr(result.CsrPem); + Assert.True(pkcs10.Verify()); + Org.BouncyCastle.Crypto.Parameters.RsaKeyParameters rsa = Assert.IsAssignableFrom(pkcs10.GetPublicKey()); + Assert.Equal(2048, rsa.Modulus.BitLength); + } + + [Theory] + [InlineData(InfisicalEcCurve.P256, "1.2.840.10045.3.1.7")] + [InlineData(InfisicalEcCurve.P384, "1.3.132.0.34")] + public void CsrBuilder_Ecdsa_Produces_Verifiable_Csr(InfisicalEcCurve curve, string expectedCurveOid) + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = "ec.contoso.local" }; + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Ecdsa, EcCurve = curve }; + InfisicalCsrResult result = InfisicalCsrBuilder.Build(subject, new[] { "ec.contoso.local" }, null, options); + + Assert.Contains("BEGIN CERTIFICATE REQUEST", result.CsrPem); + Assert.True(result.PrivateKeyPem.Contains("BEGIN EC PRIVATE KEY") || result.PrivateKeyPem.Contains("BEGIN PRIVATE KEY")); + + Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest pkcs10 = ReadCsr(result.CsrPem); + Assert.True(pkcs10.Verify()); + Org.BouncyCastle.Crypto.Parameters.ECPublicKeyParameters ec = Assert.IsAssignableFrom(pkcs10.GetPublicKey()); + Assert.Equal(expectedCurveOid, ec.PublicKeyParamSet.Id); + } + + [Fact] + public void CsrBuilder_Ed25519_Produces_Verifiable_Csr() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = "ed.contoso.local" }; + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Ed25519 }; + InfisicalCsrResult result = InfisicalCsrBuilder.Build(subject, new[] { "ed.contoso.local" }, null, options); + + Assert.Contains("BEGIN CERTIFICATE REQUEST", result.CsrPem); + Assert.Contains("BEGIN PRIVATE KEY", result.PrivateKeyPem); + + Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest pkcs10 = ReadCsr(result.CsrPem); + Assert.True(pkcs10.Verify()); + Assert.IsAssignableFrom(pkcs10.GetPublicKey()); + } + + [Fact] + public void CsrBuilder_Rsa_Rejects_Invalid_KeySize() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { CommonName = "test.local" }; + InfisicalCsrOptions options = new InfisicalCsrOptions { KeyAlgorithm = InfisicalKeyAlgorithm.Rsa, RsaKeySize = 1024 }; + Assert.Throws(() => InfisicalCsrBuilder.Build(subject, null, null, options)); + } + + [Fact] + public void CsrBuilder_Throws_When_CommonName_Missing() + { + InfisicalCsrSubject subject = new InfisicalCsrSubject { Organization = "Contoso" }; + Assert.Throws(() => InfisicalCsrBuilder.Build(subject, null, null, new InfisicalCsrOptions())); + } + + private static Org.BouncyCastle.Pkcs.Pkcs10CertificationRequest ReadCsr(string pem) + { + using (System.IO.StringReader reader = new System.IO.StringReader(pem)) + { + Org.BouncyCastle.OpenSsl.PemReader pemReader = new Org.BouncyCastle.OpenSsl.PemReader(reader); + object obj = pemReader.ReadObject(); + return Assert.IsType(obj); + } + } + + [Fact] + public void MergeSubject_Hashtable_Then_Individual_Params_Override() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo merge = helperType.GetMethod("MergeSubject", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(merge); + + Hashtable subject = new Hashtable { { "CN", "fallback.local" }, { "O", "FallbackOrg" }, { "C", "DE" } }; + object result = merge.Invoke(null, new object[] { subject, "explicit.local", null, null, null, "ExplicitOrg", null, null }); + + PropertyInfo commonNameProp = result.GetType().GetProperty("CommonName"); + PropertyInfo organizationProp = result.GetType().GetProperty("Organization"); + PropertyInfo countryProp = result.GetType().GetProperty("Country"); + + Assert.Equal("explicit.local", commonNameProp.GetValue(result)); + Assert.Equal("ExplicitOrg", organizationProp.GetValue(result)); + Assert.Equal("DE", countryProp.GetValue(result)); + } + + [Fact] + public void Candidates_For_SignCertificateBySubscriber_Include_Pki_And_CertManager() + { + IReadOnlyList candidates = InfisicalEndpointRegistry.GetCandidates(InfisicalEndpointNames.SignCertificateBySubscriber); + Assert.Contains(candidates, c => c.Template == "/api/v1/pki/pki-subscribers/{subscriberName}/sign-certificate"); + Assert.Contains(candidates, c => c.Template == "/api/v1/cert-manager/pki-subscribers/{subscriberName}/sign-certificate"); + foreach (InfisicalEndpointDefinition candidate in candidates) + { + Assert.Equal("POST", candidate.Method); + Assert.True(candidate.RequiresAuthorization); + Assert.True(candidate.ContainsSecretMaterialInResponse); + } + } + + [Fact] + public void Candidates_For_SignCertificateByCa_Include_Pki_And_CertManager() + { + IReadOnlyList candidates = InfisicalEndpointRegistry.GetCandidates(InfisicalEndpointNames.SignCertificateByCa); + Assert.Contains(candidates, c => c.Template == "/api/v1/pki/ca/{caId}/sign-certificate"); + Assert.Contains(candidates, c => c.Template == "/api/v1/cert-manager/ca/{caId}/sign-certificate"); + } + + [Fact] + public void RequestInfisicalCertificate_Cmdlet_Has_Both_Parameter_Sets() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.RequestInfisicalCertificateCmdlet", true); + Assert.True(typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).IsAssignableFrom(cmdletType)); + + CustomAttributeData cmdletData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(CmdletAttribute)) { cmdletData = candidate; break; } + } + Assert.NotNull(cmdletData); + Assert.Equal(VerbsLifecycle.Request, cmdletData.ConstructorArguments[0].Value); + Assert.Equal("InfisicalCertificate", cmdletData.ConstructorArguments[1].Value); + + string defaultParameterSetName = null; + foreach (CustomAttributeNamedArgument named in cmdletData.NamedArguments) + { + if (named.MemberName == "DefaultParameterSetName") { defaultParameterSetName = (string)named.TypedValue.Value; break; } + } + Assert.Equal("BySubscriber", defaultParameterSetName); + + Assert.NotNull(cmdletType.GetProperty("PkiSubscriberSlug")); + Assert.NotNull(cmdletType.GetProperty("CertificateAuthorityId")); + Assert.NotNull(cmdletType.GetProperty("Subject")); + Assert.NotNull(cmdletType.GetProperty("CommonName")); + Assert.NotNull(cmdletType.GetProperty("DnsName")); + Assert.NotNull(cmdletType.GetProperty("IpAddress")); + Assert.NotNull(cmdletType.GetProperty("Install")); + Assert.NotNull(cmdletType.GetProperty("StoreName")); + Assert.NotNull(cmdletType.GetProperty("StoreLocation")); + Assert.NotNull(cmdletType.GetProperty("AllowRenewal")); + Assert.NotNull(cmdletType.GetProperty("RenewalThresholdDays")); + Assert.NotNull(cmdletType.GetProperty("Force")); + Assert.NotNull(cmdletType.GetProperty("InstallChain")); + + PropertyInfo keyAlgorithmProp = cmdletType.GetProperty("KeyAlgorithm"); + PropertyInfo curveProp = cmdletType.GetProperty("Curve"); + Assert.NotNull(keyAlgorithmProp); + Assert.NotNull(curveProp); + Assert.Equal(typeof(InfisicalKeyAlgorithm), keyAlgorithmProp.PropertyType); + Assert.Equal(typeof(InfisicalEcCurve), curveProp.PropertyType); + + PropertyInfo protectionProp = cmdletType.GetProperty("PrivateKeyProtection"); + Assert.NotNull(protectionProp); + Assert.Equal(typeof(InfisicalPrivateKeyProtection), protectionProp.PropertyType); + Assert.NotNull(cmdletType.GetProperty("PersistKey")); + Assert.NotNull(cmdletType.GetProperty("MachineKey")); + Assert.NotNull(cmdletType.GetProperty("PrivateKeyPath")); + Assert.NotNull(cmdletType.GetProperty("LocalChainOnly")); + + CustomAttributeData outputTypeData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(OutputTypeAttribute)) { outputTypeData = candidate; break; } + } + Assert.NotNull(outputTypeData); + IList outputTypeArgs = (IList)outputTypeData.ConstructorArguments[0].Value; + Assert.Contains(outputTypeArgs, a => (Type)a.Value == typeof(PSInfisicalAPI.Models.InfisicalCertificateResult)); + } + + [Fact] + public void BuildResult_Splits_Chain_Into_Leaf_Intermediates_And_Root() + { + (string leafPem, _, string leafThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("BuildResult.Leaf"); + (string intermediatePem, _, string intermediateThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("BuildResult.Intermediate"); + (string rootPem, _, string rootThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("BuildResult.Root"); + + PSInfisicalAPI.Models.InfisicalSignedCertificate signed = new PSInfisicalAPI.Models.InfisicalSignedCertificate + { + SerialNumber = "ABC123", + CertificatePem = leafPem, + CertificateChainPem = intermediatePem + rootPem, + IssuingCaCertificatePem = rootPem + }; + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo buildResult = helperType.GetMethod("BuildResult", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(buildResult); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)buildResult.Invoke(null, new object[] { leaf, signed }); + + Assert.Same(leaf, result.Leaf); + Assert.Equal("ABC123", result.SerialNumber); + Assert.Empty(result.Intermediates); + Assert.NotNull(result.Root); + Assert.Equal(2, result.Chain.Length); + Assert.Same(leaf, result.Chain[0]); + } + } + + [Theory] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, false, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, false, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable)] + [InlineData(InfisicalPrivateKeyProtection.NonExportable, false, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet)] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, true, false, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.PersistKeySet)] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, false, true, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.MachineKeySet)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, true, true, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.Exportable | System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.MachineKeySet | System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.PersistKeySet)] + public void ResolveKeyStorageFlags_Maps_Protection_And_Switches(InfisicalPrivateKeyProtection protection, bool persist, bool machine, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags expected) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo method = helperType.GetMethod("ResolveKeyStorageFlags", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + + System.Security.Cryptography.X509Certificates.X509KeyStorageFlags actual = (System.Security.Cryptography.X509Certificates.X509KeyStorageFlags)method.Invoke(null, new object[] { protection, persist, machine }); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, false, false)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, false, false)] + [InlineData(InfisicalPrivateKeyProtection.NonExportable, false, true)] + [InlineData(InfisicalPrivateKeyProtection.Ephemeral, false, true)] + [InlineData(InfisicalPrivateKeyProtection.LocalOnly, true, true)] + [InlineData(InfisicalPrivateKeyProtection.Exportable, true, true)] + public void ShouldScrubPrivateKeyPem_Returns_Expected(InfisicalPrivateKeyProtection protection, bool hasPath, bool expected) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo method = helperType.GetMethod("ShouldScrubPrivateKeyPem", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + + bool actual = (bool)method.Invoke(null, new object[] { protection, hasPath }); + Assert.Equal(expected, actual); + } + + [Fact] + public void WritePrivateKeyPem_Writes_File_And_Creates_Directory() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo method = helperType.GetMethod("WritePrivateKeyPem", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(method); + + string tempRoot = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "PSInfisicalAPI_PemWrite_" + Guid.NewGuid().ToString("N")); + string nested = System.IO.Path.Combine(tempRoot, "nested", "key.pem"); + const string pem = "-----BEGIN PRIVATE KEY-----\nMIIBVgIBADANBgkqhkiG9w0BAQEFAA==\n-----END PRIVATE KEY-----\n"; + try + { + method.Invoke(null, new object[] { pem, nested }); + Assert.True(System.IO.File.Exists(nested)); + Assert.Equal(pem, System.IO.File.ReadAllText(nested)); + } + finally + { + if (System.IO.Directory.Exists(tempRoot)) { System.IO.Directory.Delete(tempRoot, true); } + } + } + + [Fact] + public void BuildResultFromExistingLocal_Populates_Leaf_And_Pem_For_Selfsigned() + { + (string leafPem, _, string leafThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Leaf"); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo build = helperType.GetMethod("BuildResultFromExistingLocal", BindingFlags.Public | BindingFlags.Static, null, new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2) }, null); + Assert.NotNull(build); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)build.Invoke(null, new object[] { leaf }); + + Assert.Same(leaf, result.Leaf); + Assert.Equal(leaf.SerialNumber, result.SerialNumber); + Assert.Contains("BEGIN CERTIFICATE", result.CertificatePem); + Assert.NotNull(result.Chain); + Assert.NotEmpty(result.Chain); + Assert.Same(leaf, result.Chain[0]); + Assert.Empty(result.Intermediates); + } + } + + [Fact] + public void BuildResultFromExistingLocal_Has_Bundle_Fallback_Overload() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo overload = helperType.GetMethod( + "BuildResultFromExistingLocal", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2), typeof(PSInfisicalAPI.Models.InfisicalCertificateBundle) }, + null); + Assert.NotNull(overload); + Assert.Equal(typeof(PSInfisicalAPI.Models.InfisicalCertificateResult), overload.ReturnType); + } + + [Fact] + public void BuildResultFromExistingLocal_With_Null_Bundle_Matches_LocalOnly_Behavior() + { + (string leafPem, _, _) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Bundle.Null.Leaf"); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo overload = helperType.GetMethod( + "BuildResultFromExistingLocal", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2), typeof(PSInfisicalAPI.Models.InfisicalCertificateBundle) }, + null); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)overload.Invoke(null, new object[] { leaf, null }); + + Assert.Same(leaf, result.Leaf); + Assert.Empty(result.Intermediates); + Assert.Single(result.Chain); + } + } + + [Fact] + public void BuildResultFromExistingLocal_With_Bundle_Merges_Chain_From_Bundle() + { + (string leafPem, _, string leafThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Bundle.Leaf"); + (string caPem, _, string caThumb) = PemCertificateBuilderTests.CreateSelfSignedExposed("ReuseLookup.Bundle.Ca"); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 leaf = new System.Security.Cryptography.X509Certificates.X509Certificate2(System.Text.Encoding.ASCII.GetBytes(leafPem))) + { + PSInfisicalAPI.Models.InfisicalCertificateBundle bundle = new PSInfisicalAPI.Models.InfisicalCertificateBundle + { + SerialNumber = leaf.SerialNumber, + CertificatePem = leafPem, + CertificateChainPem = caPem + }; + + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo overload = helperType.GetMethod( + "BuildResultFromExistingLocal", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] { typeof(System.Security.Cryptography.X509Certificates.X509Certificate2), typeof(PSInfisicalAPI.Models.InfisicalCertificateBundle) }, + null); + + PSInfisicalAPI.Models.InfisicalCertificateResult result = (PSInfisicalAPI.Models.InfisicalCertificateResult)overload.Invoke(null, new object[] { leaf, bundle }); + + Assert.Same(leaf, result.Leaf); + Assert.NotNull(result.Root); + Assert.Equal(caThumb, result.Root.Thumbprint); + Assert.Equal(2, result.Chain.Length); + Assert.Same(leaf, result.Chain[0]); + Assert.Equal(caThumb, result.Chain[1].Thumbprint); + + Assert.NotNull(result.CertificateChainPem); + Assert.Contains("BEGIN CERTIFICATE", result.CertificateChainPem); + } + } + + [Fact] + public void GetChainCertificateTargetStore_SelfSigned_Returns_Root() + { + using (System.Security.Cryptography.RSA rsa = System.Security.Cryptography.RSA.Create(2048)) + { + System.Security.Cryptography.X509Certificates.CertificateRequest request = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=ChainRouting.SelfSigned", + rsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + using (System.Security.Cryptography.X509Certificates.X509Certificate2 selfSigned = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1))) + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo classify = helperType.GetMethod("GetChainCertificateTargetStore", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(classify); + + object result = classify.Invoke(null, new object[] { selfSigned }); + Assert.Equal(System.Security.Cryptography.X509Certificates.StoreName.Root, result); + } + } + } + + [Fact] + public void GetChainCertificateTargetStore_NonSelfSigned_Returns_CertificateAuthority() + { + using (System.Security.Cryptography.RSA rootRsa = System.Security.Cryptography.RSA.Create(2048)) + using (System.Security.Cryptography.RSA intermediateRsa = System.Security.Cryptography.RSA.Create(2048)) + { + System.Security.Cryptography.X509Certificates.CertificateRequest rootRequest = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=ChainRouting.Root", + rootRsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + rootRequest.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension(true, false, 0, true)); + + using (System.Security.Cryptography.X509Certificates.X509Certificate2 rootCert = rootRequest.CreateSelfSigned(DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1))) + { + System.Security.Cryptography.X509Certificates.CertificateRequest intermediateRequest = new System.Security.Cryptography.X509Certificates.CertificateRequest( + "CN=ChainRouting.Intermediate", + intermediateRsa, + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); + intermediateRequest.CertificateExtensions.Add(new System.Security.Cryptography.X509Certificates.X509BasicConstraintsExtension(true, false, 0, true)); + + byte[] serial = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8 }; + using (System.Security.Cryptography.X509Certificates.X509Certificate2 intermediate = intermediateRequest.Create(rootCert, DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1), serial)) + { + Assert.NotEqual(intermediate.Subject, intermediate.Issuer); + + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo classify = helperType.GetMethod("GetChainCertificateTargetStore", BindingFlags.Public | BindingFlags.Static); + + object result = classify.Invoke(null, new object[] { intermediate }); + Assert.Equal(System.Security.Cryptography.X509Certificates.StoreName.CertificateAuthority, result); + } + } + } + } + + [Fact] + public void InstallChain_Has_X509Collection_Overload() + { + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + Type loggerType = ModuleAssembly.GetType("PSInfisicalAPI.Logging.IInfisicalLogger", true); + + MethodInfo overload = helperType.GetMethod( + "InstallChain", + BindingFlags.Public | BindingFlags.Static, + null, + new Type[] + { + typeof(System.Collections.Generic.IEnumerable), + typeof(System.Security.Cryptography.X509Certificates.StoreLocation), + typeof(bool), + loggerType, + typeof(string) + }, + null); + + Assert.NotNull(overload); + Assert.Equal(typeof(void), overload.ReturnType); + } + + [Fact] + public void InstallInfisicalCertificateCmdlet_Uses_ChainRouting_Helper() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.InstallInfisicalCertificateCmdlet", true); + Assert.NotNull(cmdletType); + + Type helperType = ModuleAssembly.GetType("PSInfisicalAPI.Pki.InfisicalCertificateRequestHelpers", true); + MethodInfo classify = helperType.GetMethod("GetChainCertificateTargetStore", BindingFlags.Public | BindingFlags.Static); + Assert.NotNull(classify); + } + } +} diff --git a/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs b/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs index b147725..17d3d01 100644 --- a/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs +++ b/src/PSInfisicalAPI.Tests/PemCertificateBuilderTests.cs @@ -8,6 +8,11 @@ namespace PSInfisicalAPI.Tests { public class PemCertificateBuilderTests { + public static (string CertPem, string KeyPem, string Thumbprint) CreateSelfSignedExposed(string commonName) + { + return CreateSelfSigned(commonName); + } + private static (string CertPem, string KeyPem, string Thumbprint) CreateSelfSigned(string commonName) { using (RSA rsa = RSA.Create(2048)) diff --git a/src/PSInfisicalAPI.Tests/PkiClientParseTests.cs b/src/PSInfisicalAPI.Tests/PkiClientParseTests.cs new file mode 100644 index 0000000..8ccbf5d --- /dev/null +++ b/src/PSInfisicalAPI.Tests/PkiClientParseTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections; +using System.Reflection; +using PSInfisicalAPI.Http; +using PSInfisicalAPI.Logging; +using PSInfisicalAPI.Pki; +using Xunit; + +namespace PSInfisicalAPI.Tests +{ + public class PkiClientParseTests + { + private sealed class NoopHttpClient : IInfisicalHttpClient + { + public InfisicalHttpResponse Send(InfisicalHttpRequest request) { throw new NotImplementedException(); } + } + + private static InfisicalPkiClient CreateClient() + { + return new InfisicalPkiClient(new NoopHttpClient(), NullInfisicalLogger.Instance); + } + + private static object InvokeNonPublic(InfisicalPkiClient client, string methodName, string body) + { + MethodInfo method = typeof(InfisicalPkiClient).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance); + return method.Invoke(client, new object[] { body }); + } + + [Fact] + public void ParseCaListBody_Reads_Raw_Json_Array() + { + string body = "[{\"id\":\"ca-1\",\"name\":\"intermediate\",\"projectId\":\"p1\",\"configuration\":{\"commonName\":\"Intermediate CA\",\"keyAlgorithm\":\"RSA_2048\"}}]"; + object result = InvokeNonPublic(CreateClient(), "ParseCaListBody", body); + IList list = (IList)result; + Assert.Single(list); + object dto = list[0]; + Assert.Equal("ca-1", dto.GetType().GetProperty("Id").GetValue(dto)); + object cfg = dto.GetType().GetProperty("Configuration").GetValue(dto); + Assert.NotNull(cfg); + Assert.Equal("Intermediate CA", cfg.GetType().GetProperty("CommonName").GetValue(cfg)); + } + + [Fact] + public void ParseCaListBody_Reads_CertificateAuthorities_Wrapper() + { + string body = "{\"certificateAuthorities\":[{\"id\":\"ca-2\",\"name\":\"root\"}]}"; + object result = InvokeNonPublic(CreateClient(), "ParseCaListBody", body); + IList list = (IList)result; + Assert.Single(list); + object dto = list[0]; + Assert.Equal("ca-2", dto.GetType().GetProperty("Id").GetValue(dto)); + } + + [Fact] + public void ParseCaSingleBody_Reads_Raw_Object_With_Configuration() + { + string body = "{\"id\":\"ca-9\",\"name\":\"intermediate-ca\",\"status\":\"active\",\"configuration\":{\"commonName\":\"GSPA Intermediate\",\"organization\":\"GSPA\"}}"; + object result = InvokeNonPublic(CreateClient(), "ParseCaSingleBody", body); + Assert.NotNull(result); + Assert.Equal("ca-9", result.GetType().GetProperty("Id").GetValue(result)); + object cfg = result.GetType().GetProperty("Configuration").GetValue(result); + Assert.NotNull(cfg); + Assert.Equal("GSPA Intermediate", cfg.GetType().GetProperty("CommonName").GetValue(cfg)); + Assert.Equal("GSPA", cfg.GetType().GetProperty("OrganizationName").GetValue(cfg)); + } + + [Fact] + public void ParseCaSingleBody_Reads_CertificateAuthority_Wrapper() + { + string body = "{\"certificateAuthority\":{\"id\":\"ca-7\",\"name\":\"root\"}}"; + object result = InvokeNonPublic(CreateClient(), "ParseCaSingleBody", body); + Assert.NotNull(result); + Assert.Equal("ca-7", result.GetType().GetProperty("Id").GetValue(result)); + } + + [Fact] + public void ParseCertificateSingleBody_Reads_Certificate_Wrapper() + { + string body = "{\"certificate\":{\"id\":\"cert-1\",\"serialNumber\":\"ABCD\",\"commonName\":\"host.example\"}}"; + object result = InvokeNonPublic(CreateClient(), "ParseCertificateSingleBody", body); + Assert.NotNull(result); + Assert.Equal("cert-1", result.GetType().GetProperty("Id").GetValue(result)); + Assert.Equal("ABCD", result.GetType().GetProperty("SerialNumber").GetValue(result)); + } + } +} diff --git a/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs b/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs index 70c3ca2..9ecd5c9 100644 --- a/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs +++ b/src/PSInfisicalAPI.Tests/PkiEndpointRegistryTests.cs @@ -1,4 +1,7 @@ +using System; using System.Collections.Generic; +using System.Management.Automation; +using System.Reflection; using PSInfisicalAPI.Endpoints; using Xunit; @@ -6,6 +9,64 @@ namespace PSInfisicalAPI.Tests { public class PkiEndpointRegistryTests { + private static readonly Assembly ModuleAssembly = typeof(PSInfisicalAPI.Connections.InfisicalConnection).Assembly; + + [Fact] + public void GetInfisicalCertificate_Cmdlet_Is_Singular_With_Mandatory_SerialNumber() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificateCmdlet", true); + Assert.True(typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).IsAssignableFrom(cmdletType)); + + CustomAttributeData cmdletData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(CmdletAttribute)) { cmdletData = candidate; break; } + } + Assert.NotNull(cmdletData); + Assert.Equal(2, cmdletData.ConstructorArguments.Count); + Assert.Equal(VerbsCommon.Get, cmdletData.ConstructorArguments[0].Value); + Assert.Equal("InfisicalCertificate", cmdletData.ConstructorArguments[1].Value); + + PropertyInfo serialProp = cmdletType.GetProperty("SerialNumber"); + Assert.NotNull(serialProp); + + CustomAttributeData parameterAttr = null; + foreach (CustomAttributeData candidate in serialProp.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(ParameterAttribute)) { parameterAttr = candidate; break; } + } + Assert.NotNull(parameterAttr); + + bool mandatory = false; + foreach (CustomAttributeNamedArgument named in parameterAttr.NamedArguments) + { + if (named.MemberName == "Mandatory") { mandatory = (bool)named.TypedValue.Value; break; } + } + Assert.True(mandatory); + } + + [Fact] + public void GetInfisicalCertificates_Cmdlet_Is_Registered_For_Listing() + { + Type cmdletType = ModuleAssembly.GetType("PSInfisicalAPI.Cmdlets.GetInfisicalCertificatesCmdlet", true); + Assert.True(typeof(PSInfisicalAPI.Cmdlets.InfisicalCmdletBase).IsAssignableFrom(cmdletType)); + + CustomAttributeData cmdletData = null; + foreach (CustomAttributeData candidate in cmdletType.GetCustomAttributesData()) + { + if (candidate.AttributeType == typeof(CmdletAttribute)) { cmdletData = candidate; break; } + } + Assert.NotNull(cmdletData); + Assert.Equal(VerbsCommon.Get, cmdletData.ConstructorArguments[0].Value); + Assert.Equal("InfisicalCertificates", cmdletData.ConstructorArguments[1].Value); + + Assert.NotNull(cmdletType.GetProperty("CommonName")); + Assert.NotNull(cmdletType.GetProperty("FriendlyName")); + Assert.NotNull(cmdletType.GetProperty("CaId")); + Assert.NotNull(cmdletType.GetProperty("Limit")); + Assert.NotNull(cmdletType.GetProperty("Offset")); + } + [Fact] public void Get_ListInternalCertificateAuthorities_Returns_CertManager_Primary() { diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs index d80992e..bd90777 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs @@ -21,12 +21,11 @@ namespace PSInfisicalAPI.Cmdlets try { InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); - string resolvedProjectId = ResolveProjectId(connection, ProjectId); InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); if (string.Equals(ParameterSetName, "ById", StringComparison.Ordinal)) { - InfisicalCertificateAuthority ca = client.GetInternalCertificateAuthority(connection, CaId, resolvedProjectId); + InfisicalCertificateAuthority ca = client.GetInternalCertificateAuthority(connection, CaId, ProjectId); if (ca != null) { WriteObject(ca); @@ -35,7 +34,7 @@ namespace PSInfisicalAPI.Cmdlets return; } - InfisicalCertificateAuthority[] all = client.ListInternalCertificateAuthorities(connection, resolvedProjectId); + InfisicalCertificateAuthority[] all = client.ListInternalCertificateAuthorities(connection, ProjectId); foreach (InfisicalCertificateAuthority ca in all) { WriteObject(ca); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs new file mode 100644 index 0000000..df5970c --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs @@ -0,0 +1,36 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalCertificate")] + [OutputType(typeof(InfisicalCertificate))] + public sealed class GetInfisicalCertificateCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)] + [Alias("Id", "Identifier")] + public string SerialNumber { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + + InfisicalCertificate cert = client.RetrieveCertificate(connection, SerialNumber); + if (cert != null) + { + WriteObject(cert); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalCertificateCmdlet", "GetCertificate", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatesCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatesCmdlet.cs new file mode 100644 index 0000000..7c90278 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatesCmdlet.cs @@ -0,0 +1,76 @@ +using System; +using System.Management.Automation; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalCertificates")] + [OutputType(typeof(InfisicalCertificate))] + public sealed class GetInfisicalCertificatesCmdlet : InfisicalCmdletBase + { + [Parameter] public string ProjectId { get; set; } + [Parameter] public string CommonName { get; set; } + [Parameter] public string FriendlyName { get; set; } + [Parameter] public string Status { get; set; } + [Parameter] public string[] CaId { get; set; } + [Parameter] public int? Limit { get; set; } + [Parameter] public int? Offset { get; set; } + [Parameter] public SwitchParameter NoAutoPage { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + string resolvedProjectId = ResolveProjectId(connection, ProjectId); + + InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery + { + ProjectId = resolvedProjectId, + CommonName = CommonName, + FriendlyName = FriendlyName, + Status = Status, + CaIds = CaId, + Limit = Limit ?? 100, + Offset = Offset ?? 0 + }; + + int requestedLimit = query.Limit ?? 100; + int emitted = 0; + while (true) + { + InfisicalCertificateSearchResult page = client.SearchCertificates(connection, query); + if (page == null || page.Certificates == null || page.Certificates.Length == 0) + { + break; + } + + foreach (InfisicalCertificate cert in page.Certificates) + { + WriteObject(cert); + emitted++; + } + + if (NoAutoPage.IsPresent || page.Certificates.Length < requestedLimit) + { + break; + } + + if (page.TotalCount > 0 && emitted >= page.TotalCount) + { + break; + } + + query.Offset = (query.Offset ?? 0) + page.Certificates.Length; + } + } + catch (Exception exception) + { + ThrowTerminatingForException("GetInfisicalCertificatesCmdlet", "GetCertificates", exception); + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs index af32a3e..03b635a 100644 --- a/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/InstallInfisicalCertificateCmdlet.cs @@ -44,7 +44,8 @@ namespace PSInfisicalAPI.Cmdlets { foreach (X509Certificate2 chainCert in ResolveChain()) { - InstallCertificate(chainCert, StoreName.CertificateAuthority, StoreLocation); + StoreName chainStore = InfisicalCertificateRequestHelpers.GetChainCertificateTargetStore(chainCert); + InstallCertificate(chainCert, chainStore, StoreLocation); } } diff --git a/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs new file mode 100644 index 0000000..1fc3b09 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/RequestInfisicalCertificateCmdlet.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Security.Cryptography.X509Certificates; +using PSInfisicalAPI.Connections; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Pki; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsLifecycle.Request, "InfisicalCertificate", SupportsShouldProcess = true, DefaultParameterSetName = "BySubscriber")] + [OutputType(typeof(InfisicalCertificateResult))] + public sealed class RequestInfisicalCertificateCmdlet : InfisicalCmdletBase + { + private const string Component = "RequestInfisicalCertificateCmdlet"; + + [Parameter(ParameterSetName = "BySubscriber", Mandatory = true, Position = 0)] + [Alias("Subscriber")] + public string PkiSubscriberSlug { get; set; } + + [Parameter(ParameterSetName = "ByCa", Mandatory = true, Position = 0)] + [Alias("CaId")] + public string CertificateAuthorityId { get; set; } + + [Parameter] public string ProjectId { get; set; } + [Parameter] public IDictionary Subject { get; set; } + [Parameter] public string CommonName { get; set; } + [Parameter] public string Country { get; set; } + [Parameter] public string State { get; set; } + [Parameter] public string Locality { get; set; } + [Parameter] public string Organization { get; set; } + [Parameter] public string OrganizationalUnit { get; set; } + [Parameter] public string EmailAddress { get; set; } + [Parameter] public string[] DnsName { get; set; } + [Parameter] public string[] IpAddress { get; set; } + [Parameter] public InfisicalKeyAlgorithm KeyAlgorithm { get; set; } = InfisicalKeyAlgorithm.Rsa; + [Parameter] public int KeySize { get; set; } = 2048; + [Parameter] public InfisicalEcCurve Curve { get; set; } = InfisicalEcCurve.P256; + + [Parameter(ParameterSetName = "ByCa")] public string Ttl { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string NotBefore { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string NotAfter { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string FriendlyName { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string PkiCollectionId { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string[] KeyUsage { get; set; } + [Parameter(ParameterSetName = "ByCa")] public string[] ExtendedKeyUsage { get; set; } + + [Parameter] public SwitchParameter Install { get; set; } + [Parameter] public StoreName StoreName { get; set; } = StoreName.My; + [Parameter] public StoreLocation StoreLocation { get; set; } = StoreLocation.CurrentUser; + [Parameter] public X509KeyStorageFlags KeyStorageFlags { get; set; } = X509KeyStorageFlags.DefaultKeySet; + [Parameter] public SwitchParameter InstallChain { get; set; } + + [Parameter] public InfisicalPrivateKeyProtection PrivateKeyProtection { get; set; } = InfisicalPrivateKeyProtection.LocalOnly; + [Parameter] public SwitchParameter PersistKey { get; set; } + [Parameter] public SwitchParameter MachineKey { get; set; } + [Parameter] public string PrivateKeyPath { get; set; } + + [Parameter] public SwitchParameter AllowRenewal { get; set; } + [Parameter] public int RenewalThresholdDays { get; set; } = 30; + [Parameter] public SwitchParameter Force { get; set; } + [Parameter] public SwitchParameter LocalChainOnly { get; set; } + + protected override void ProcessRecord() + { + try + { + InfisicalConnection connection = InfisicalSessionManager.RequireCurrent(); + InfisicalPkiClient client = new InfisicalPkiClient(HttpClient, Logger); + string resolvedProjectId = ResolveProjectId(connection, ProjectId); + + InfisicalCsrSubject csrSubject = InfisicalCertificateRequestHelpers.MergeSubject(Subject, CommonName, Country, State, Locality, Organization, OrganizationalUnit, EmailAddress); + List dnsNames = BuildDnsNames(csrSubject); + if (string.IsNullOrEmpty(csrSubject.CommonName) && dnsNames.Count > 0) { csrSubject.CommonName = dnsNames[0]; } + if (string.IsNullOrEmpty(csrSubject.CommonName)) { throw new InvalidOperationException("Subject CommonName could not be determined and no DnsName was provided."); } + + X509Certificate2 existing = TryFindExisting(client, connection, resolvedProjectId, csrSubject.CommonName); + if (existing != null && !Force.IsPresent && !(AllowRenewal.IsPresent && InfisicalLocalCertificateLookup.IsRenewable(existing, RenewalThresholdDays))) + { + Logger.Information(Component, string.Concat("Reusing existing certificate (Thumbprint=", existing.Thumbprint, ", NotAfter=", existing.NotAfter.ToString("u"), ").")); + InfisicalCertificateResult reuseResult = InfisicalCertificateRequestHelpers.BuildResultFromExistingLocal(existing); + + if (!LocalChainOnly.IsPresent + && (reuseResult.Root == null || reuseResult.Intermediates == null || reuseResult.Intermediates.Length == 0) + && !string.IsNullOrEmpty(existing.SerialNumber)) + { + try + { + InfisicalCertificateBundle bundle = client.GetCertificateBundle(connection, existing.SerialNumber); + if (bundle != null && !string.IsNullOrEmpty(bundle.CertificateChainPem)) + { + reuseResult = InfisicalCertificateRequestHelpers.BuildResultFromExistingLocal(existing, bundle); + Logger.Information(Component, "Reused certificate chain completed from Infisical bundle."); + } + } + catch (Exception bundleException) + { + Logger.Verbose(Component, string.Concat("Infisical bundle fetch for reuse path failed (continuing with local-only chain): ", bundleException.Message)); + } + } + + WriteObject(reuseResult); + return; + } + + string target = string.Concat("PKI subscriber '", PkiSubscriberSlug ?? "(n/a)", "' or CA '", CertificateAuthorityId ?? "(n/a)", "' for CN=", csrSubject.CommonName); + if (!ShouldProcess(target, "Request new certificate")) { return; } + + InfisicalCsrOptions csrOptions = new InfisicalCsrOptions { KeyAlgorithm = KeyAlgorithm, RsaKeySize = KeySize, EcCurve = Curve }; + InfisicalCsrResult csr = InfisicalCsrBuilder.Build(csrSubject, dnsNames, IpAddress, csrOptions); + InfisicalSignedCertificate signed = SignCertificate(client, connection, resolvedProjectId, csr.CsrPem); + signed.PrivateKeyPem = csr.PrivateKeyPem; + + X509KeyStorageFlags resolvedFlags = ResolveEffectiveKeyStorageFlags(); + X509Certificate2 cert = PemCertificateBuilder.Build(signed.CertificatePem, signed.PrivateKeyPem, signed.CertificateChainPem, resolvedFlags); + + if (Install.IsPresent) + { + InfisicalCertificateRequestHelpers.InstallToStore(cert, StoreName, StoreLocation, Force.IsPresent, Logger, Component); + if (InstallChain.IsPresent) + { + InfisicalCertificateRequestHelpers.InstallChain(signed, StoreLocation, Force.IsPresent, Logger, Component); + } + } + + InfisicalCertificateResult resultObj = InfisicalCertificateRequestHelpers.BuildResult(cert, signed); + + bool hasExplicitPath = !string.IsNullOrEmpty(PrivateKeyPath); + if (hasExplicitPath && !string.IsNullOrEmpty(resultObj.PrivateKeyPem)) + { + InfisicalCertificateRequestHelpers.WritePrivateKeyPem(resultObj.PrivateKeyPem, PrivateKeyPath); + Logger.Information(Component, string.Concat("Wrote private key PEM to '", PrivateKeyPath, "'.")); + } + + if (!MyInvocation.BoundParameters.ContainsKey("KeyStorageFlags") + && InfisicalCertificateRequestHelpers.ShouldScrubPrivateKeyPem(PrivateKeyProtection, hasExplicitPath)) + { + resultObj.PrivateKeyPem = null; + } + + WriteObject(resultObj); + } + catch (Exception exception) + { + ThrowTerminatingForException(Component, "RequestCertificate", exception); + } + } + + private List BuildDnsNames(InfisicalCsrSubject subject) + { + List result = new List(); + if (DnsName != null) { foreach (string dns in DnsName) { if (!string.IsNullOrEmpty(dns)) { result.Add(dns); } } } + if (result.Count == 0) + { + string fqdn = InfisicalCertificateRequestHelpers.ResolveLocalFqdn(); + if (!string.IsNullOrEmpty(fqdn)) { result.Add(fqdn); } + } + + if (!string.IsNullOrEmpty(subject.CommonName) && !result.Contains(subject.CommonName)) { result.Insert(0, subject.CommonName); } + return result; + } + + private X509Certificate2 TryFindExisting(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string commonName) + { + List candidateSerials = new List(); + try + { + InfisicalCertificateSearchQuery query = new InfisicalCertificateSearchQuery { ProjectId = projectId, CommonName = commonName, Status = "active", Limit = 50 }; + InfisicalCertificateSearchResult page = client.SearchCertificates(connection, query); + if (page != null && page.Certificates != null) + { + foreach (InfisicalCertificate hit in page.Certificates) { if (!string.IsNullOrEmpty(hit.SerialNumber)) { candidateSerials.Add(hit.SerialNumber); } } + } + } + catch (Exception searchException) + { + Logger.Verbose(Component, string.Concat("Infisical search for idempotency check failed: ", searchException.Message)); + } + + return InfisicalLocalCertificateLookup.FindMatch(StoreName, StoreLocation, commonName, candidateSerials); + } + + private X509KeyStorageFlags ResolveEffectiveKeyStorageFlags() + { + if (MyInvocation.BoundParameters.ContainsKey("KeyStorageFlags")) + { + return KeyStorageFlags; + } + + return InfisicalCertificateRequestHelpers.ResolveKeyStorageFlags(PrivateKeyProtection, PersistKey.IsPresent, MachineKey.IsPresent); + } + + private InfisicalSignedCertificate SignCertificate(InfisicalPkiClient client, InfisicalConnection connection, string projectId, string csrPem) + { + if (string.Equals(ParameterSetName, "BySubscriber", StringComparison.Ordinal)) + { + return client.SignCertificateBySubscriber(connection, PkiSubscriberSlug, projectId, csrPem); + } + + return client.SignCertificateByCa(connection, CertificateAuthorityId, csrPem, CommonName, null, Ttl, NotBefore, NotAfter, FriendlyName, PkiCollectionId, KeyUsage, ExtendedKeyUsage); + } + } +} diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs index 6d01d4a..22f08bf 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointNames.cs @@ -49,5 +49,7 @@ namespace PSInfisicalAPI.Endpoints public const string SearchCertificates = "SearchCertificates"; public const string RetrieveCertificate = "RetrieveCertificate"; public const string GetCertificateBundle = "GetCertificateBundle"; + public const string SignCertificateBySubscriber = "SignCertificateBySubscriber"; + public const string SignCertificateByCa = "SignCertificateByCa"; } } diff --git a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs index 9d1e305..2bd0f58 100644 --- a/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs +++ b/src/PSInfisicalAPI/Endpoints/InfisicalEndpointRegistry.cs @@ -589,6 +589,50 @@ namespace PSInfisicalAPI.Endpoints RequiresAuthorization = true, ContainsSecretMaterialInResponse = true }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateBySubscriber, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/pki/pki-subscribers/{subscriberName}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateBySubscriber, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/cert-manager/pki-subscribers/{subscriberName}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateByCa, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/pki/ca/{caId}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); + + Add(map, new InfisicalEndpointDefinition + { + Name = InfisicalEndpointNames.SignCertificateByCa, + Resource = "Pki", + Version = "v1", + Method = "POST", + Template = "/api/v1/cert-manager/ca/{caId}/sign-certificate", + RequiresAuthorization = true, + ContainsSecretMaterialInResponse = true + }); } public static InfisicalEndpointDefinition Get(string name) diff --git a/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs b/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs new file mode 100644 index 0000000..38dd62a --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalCertificateResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalCertificateResult + { + public X509Certificate2 Leaf { get; set; } + public X509Certificate2[] Intermediates { get; set; } + public X509Certificate2 Root { get; set; } + public X509Certificate2[] Chain { get; set; } + public string SerialNumber { get; set; } + public string CertificatePem { get; set; } + public string CertificateChainPem { get; set; } + public string PrivateKeyPem { get; set; } + + public override string ToString() + { + if (Leaf != null) { return Leaf.Subject; } + return SerialNumber; + } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs b/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs new file mode 100644 index 0000000..e40d62f --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalSignedCertificate.cs @@ -0,0 +1,16 @@ +namespace PSInfisicalAPI.Models +{ + public sealed class InfisicalSignedCertificate + { + public string SerialNumber { get; set; } + public string CertificatePem { get; set; } + public string CertificateChainPem { get; set; } + public string IssuingCaCertificatePem { get; set; } + public string PrivateKeyPem { get; set; } + + public override string ToString() + { + return SerialNumber; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs index 01c45fc..553dd5e 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalCaDtos.cs @@ -3,6 +3,26 @@ using Newtonsoft.Json; namespace PSInfisicalAPI.Pki { + internal sealed class InfisicalInternalCaConfigurationDto + { + [JsonProperty("type")] public string Type { get; set; } + [JsonProperty("friendlyName")] public string FriendlyName { get; set; } + [JsonProperty("commonName")] public string CommonName { get; set; } + [JsonProperty("organization")] public string OrganizationName { get; set; } + [JsonProperty("ou")] public string OrganizationUnit { get; set; } + [JsonProperty("country")] public string Country { get; set; } + [JsonProperty("province")] public string State { get; set; } + [JsonProperty("locality")] public string Locality { get; set; } + [JsonProperty("notBefore")] public string NotBefore { get; set; } + [JsonProperty("notAfter")] public string NotAfter { get; set; } + [JsonProperty("maxPathLength")] public int? MaxPathLength { get; set; } + [JsonProperty("keyAlgorithm")] public string KeyAlgorithm { get; set; } + [JsonProperty("dn")] public string DistinguishedName { get; set; } + [JsonProperty("parentCaId")] public string ParentCaId { get; set; } + [JsonProperty("serialNumber")] public string SerialNumber { get; set; } + [JsonProperty("activeCaCertId")] public string ActiveCaCertId { get; set; } + } + internal sealed class InfisicalInternalCaResponseDto { [JsonProperty("id")] public string Id { get; set; } @@ -28,6 +48,7 @@ namespace PSInfisicalAPI.Pki [JsonProperty("activeCaCertId")] public string ActiveCaCertId { get; set; } [JsonProperty("createdAt")] public string CreatedAt { get; set; } [JsonProperty("updatedAt")] public string UpdatedAt { get; set; } + [JsonProperty("configuration")] public InfisicalInternalCaConfigurationDto Configuration { get; set; } } internal sealed class InfisicalInternalCaListResponseDto diff --git a/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs b/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs index dc83822..aa1c3dd 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalCaMapper.cs @@ -14,34 +14,41 @@ namespace PSInfisicalAPI.Pki return null; } + InfisicalInternalCaConfigurationDto cfg = dto.Configuration; + return new InfisicalCertificateAuthority { Id = dto.Id, ProjectId = !string.IsNullOrEmpty(dto.ProjectId) ? dto.ProjectId : fallbackProjectId, Name = dto.Name, - FriendlyName = dto.FriendlyName, - Type = dto.Type, + FriendlyName = Coalesce(cfg != null ? cfg.FriendlyName : null, dto.FriendlyName), + Type = Coalesce(dto.Type, cfg != null ? cfg.Type : null), Status = dto.Status, EnableDirectIssuance = dto.EnableDirectIssuance, - KeyAlgorithm = dto.KeyAlgorithm, - DistinguishedName = dto.DistinguishedName, - OrganizationName = dto.OrganizationName, - OrganizationUnit = dto.OrganizationUnit, - Country = dto.Country, - State = dto.State, - Locality = dto.Locality, - CommonName = dto.CommonName, - MaxPathLength = dto.MaxPathLength, - NotBefore = dto.NotBefore, - NotAfter = dto.NotAfter, - SerialNumber = dto.SerialNumber, - ParentCaId = dto.ParentCaId, - ActiveCaCertId = dto.ActiveCaCertId, + KeyAlgorithm = Coalesce(cfg != null ? cfg.KeyAlgorithm : null, dto.KeyAlgorithm), + DistinguishedName = Coalesce(cfg != null ? cfg.DistinguishedName : null, dto.DistinguishedName), + OrganizationName = Coalesce(cfg != null ? cfg.OrganizationName : null, dto.OrganizationName), + OrganizationUnit = Coalesce(cfg != null ? cfg.OrganizationUnit : null, dto.OrganizationUnit), + Country = Coalesce(cfg != null ? cfg.Country : null, dto.Country), + State = Coalesce(cfg != null ? cfg.State : null, dto.State), + Locality = Coalesce(cfg != null ? cfg.Locality : null, dto.Locality), + CommonName = Coalesce(cfg != null ? cfg.CommonName : null, dto.CommonName), + MaxPathLength = (cfg != null && cfg.MaxPathLength.HasValue) ? cfg.MaxPathLength : dto.MaxPathLength, + NotBefore = Coalesce(cfg != null ? cfg.NotBefore : null, dto.NotBefore), + NotAfter = Coalesce(cfg != null ? cfg.NotAfter : null, dto.NotAfter), + SerialNumber = Coalesce(cfg != null ? cfg.SerialNumber : null, dto.SerialNumber), + ParentCaId = Coalesce(cfg != null ? cfg.ParentCaId : null, dto.ParentCaId), + ActiveCaCertId = Coalesce(cfg != null ? cfg.ActiveCaCertId : null, dto.ActiveCaCertId), CreatedAtUtc = ParseTimestamp(dto.CreatedAt), UpdatedAtUtc = ParseTimestamp(dto.UpdatedAt) }; } + private static string Coalesce(string primary, string fallback) + { + return !string.IsNullOrEmpty(primary) ? primary : fallback; + } + public static InfisicalCertificateAuthority[] MapMany(IEnumerable items, string fallbackProjectId) { if (items == null) diff --git a/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs b/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs new file mode 100644 index 0000000..15c296b --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCertificateRequestHelpers.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using PSInfisicalAPI.Logging; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalCertificateRequestHelpers + { + public static InfisicalCsrSubject MergeSubject(IDictionary subject, string commonName, string country, string state, string locality, string organization, string organizationalUnit, string emailAddress) + { + InfisicalCsrSubject result = new InfisicalCsrSubject(); + if (subject != null) + { + result.CommonName = ReadString(subject, "CN", "CommonName"); + result.Country = ReadString(subject, "C", "Country"); + result.State = ReadString(subject, "ST", "S", "State"); + result.Locality = ReadString(subject, "L", "Locality"); + result.Organization = ReadString(subject, "O", "Organization"); + result.OrganizationalUnit = ReadString(subject, "OU", "OrganizationalUnit"); + result.EmailAddress = ReadString(subject, "E", "EMAIL", "EmailAddress"); + } + + if (!string.IsNullOrEmpty(commonName)) { result.CommonName = commonName; } + if (!string.IsNullOrEmpty(country)) { result.Country = country; } + if (!string.IsNullOrEmpty(state)) { result.State = state; } + if (!string.IsNullOrEmpty(locality)) { result.Locality = locality; } + if (!string.IsNullOrEmpty(organization)) { result.Organization = organization; } + if (!string.IsNullOrEmpty(organizationalUnit)) { result.OrganizationalUnit = organizationalUnit; } + if (!string.IsNullOrEmpty(emailAddress)) { result.EmailAddress = emailAddress; } + + return result; + } + + public static string ResolveLocalFqdn() + { + try + { + string host = System.Net.Dns.GetHostName(); + string domain = null; + try { domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; } + catch { domain = null; } + + if (!string.IsNullOrEmpty(domain) && !host.EndsWith("." + domain, StringComparison.OrdinalIgnoreCase)) + { + return string.Concat(host, ".", domain); + } + + return host; + } + catch + { + return null; + } + } + + public static void InstallToStore(X509Certificate2 cert, StoreName storeName, StoreLocation storeLocation, bool force, IInfisicalLogger logger, string component) + { + X509Store store = new X509Store(storeName, storeLocation); + try + { + store.Open(OpenFlags.ReadWrite); + X509Certificate2Collection existing = store.Certificates.Find(X509FindType.FindByThumbprint, cert.Thumbprint, false); + string target = string.Concat(storeLocation.ToString(), @"\", storeName.ToString(), " [", cert.Thumbprint, "]"); + if (existing.Count > 0) + { + if (!force) + { + logger.Information(component, string.Concat("Certificate already present in ", target, "; no action taken.")); + return; + } + + store.RemoveRange(existing); + } + + store.Add(cert); + logger.Information(component, string.Concat("Installed certificate to ", target, ".")); + } + finally + { + store.Close(); + } + } + + public static void InstallChain(InfisicalSignedCertificate signed, StoreLocation storeLocation, bool force, IInfisicalLogger logger, string component) + { + List chainCerts = CollectChainCertificates(signed); + InstallChain(chainCerts, storeLocation, force, logger, component); + } + + public static void InstallChain(IEnumerable chainCerts, StoreLocation storeLocation, bool force, IInfisicalLogger logger, string component) + { + if (chainCerts == null) { return; } + foreach (X509Certificate2 chainCert in chainCerts) + { + if (chainCert == null) { continue; } + StoreName targetStore = GetChainCertificateTargetStore(chainCert); + InstallToStore(chainCert, targetStore, storeLocation, force, logger, component); + } + } + + public static StoreName GetChainCertificateTargetStore(X509Certificate2 cert) + { + return IsSelfSigned(cert) ? StoreName.Root : StoreName.CertificateAuthority; + } + + public static X509KeyStorageFlags ResolveKeyStorageFlags(InfisicalPrivateKeyProtection protection, bool persistKey, bool machineKey) + { + X509KeyStorageFlags flags = X509KeyStorageFlags.DefaultKeySet; + switch (protection) + { + case InfisicalPrivateKeyProtection.Exportable: + flags |= X509KeyStorageFlags.Exportable; + break; + case InfisicalPrivateKeyProtection.Ephemeral: + const int ephemeralValue = 32; + if (Enum.GetName(typeof(X509KeyStorageFlags), ephemeralValue) == null) + { + throw new PlatformNotSupportedException("InfisicalPrivateKeyProtection.Ephemeral requires .NET Core 3.0 or later (PowerShell 7+). Use LocalOnly or NonExportable on Windows PowerShell 5.1."); + } + flags |= (X509KeyStorageFlags)ephemeralValue; + break; + } + + if (machineKey) { flags |= X509KeyStorageFlags.MachineKeySet; } + if (persistKey) { flags |= X509KeyStorageFlags.PersistKeySet; } + + return flags; + } + + public static bool ShouldScrubPrivateKeyPem(InfisicalPrivateKeyProtection protection, bool hasExplicitPrivateKeyPath) + { + if (hasExplicitPrivateKeyPath) { return true; } + return protection == InfisicalPrivateKeyProtection.NonExportable + || protection == InfisicalPrivateKeyProtection.Ephemeral; + } + + public static void WritePrivateKeyPem(string privateKeyPem, string path) + { + if (string.IsNullOrEmpty(privateKeyPem)) { throw new ArgumentException("PrivateKeyPem is empty.", nameof(privateKeyPem)); } + if (string.IsNullOrEmpty(path)) { throw new ArgumentException("Path is required.", nameof(path)); } + + string fullPath = System.IO.Path.GetFullPath(path); + string directory = System.IO.Path.GetDirectoryName(fullPath); + if (!string.IsNullOrEmpty(directory) && !System.IO.Directory.Exists(directory)) + { + System.IO.Directory.CreateDirectory(directory); + } + + System.IO.File.WriteAllText(fullPath, privateKeyPem); + } + + public static InfisicalCertificateResult BuildResultFromExistingLocal(X509Certificate2 leaf) + { + return BuildResultFromExistingLocal(leaf, null); + } + + public static InfisicalCertificateResult BuildResultFromExistingLocal(X509Certificate2 leaf, InfisicalCertificateBundle fallbackBundle) + { + if (leaf == null) { throw new ArgumentNullException(nameof(leaf)); } + + InfisicalCertificateResult result = new InfisicalCertificateResult + { + Leaf = leaf, + SerialNumber = leaf.SerialNumber, + CertificatePem = ExportCertificateToPem(leaf) + }; + + List chainElements = BuildLocalChain(leaf); + + if (fallbackBundle != null && !string.IsNullOrEmpty(fallbackBundle.CertificateChainPem)) + { + List bundleChain = PemCertificateBuilder.ReadCertificateChain(fallbackBundle.CertificateChainPem); + HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (X509Certificate2 c in chainElements) { if (c != null) { seen.Add(c.Thumbprint); } } + foreach (X509Certificate2 c in bundleChain) + { + if (c == null) { continue; } + if (seen.Add(c.Thumbprint)) { chainElements.Add(c); } + } + } + + List intermediates = new List(); + X509Certificate2 root = null; + foreach (X509Certificate2 cert in chainElements) + { + if (string.Equals(cert.Thumbprint, leaf.Thumbprint, StringComparison.OrdinalIgnoreCase)) { continue; } + if (IsSelfSigned(cert)) { if (root == null) { root = cert; } } + else { intermediates.Add(cert); } + } + + result.Intermediates = intermediates.ToArray(); + result.Root = root; + + List ordered = new List { leaf }; + ordered.AddRange(intermediates); + if (root != null) { ordered.Add(root); } + result.Chain = ordered.ToArray(); + + if (intermediates.Count > 0 || root != null) + { + StringBuilder sb = new StringBuilder(); + foreach (X509Certificate2 c in intermediates) { sb.Append(ExportCertificateToPem(c)); } + if (root != null) { sb.Append(ExportCertificateToPem(root)); } + result.CertificateChainPem = sb.ToString(); + } + + return result; + } + + public static InfisicalCertificateResult BuildResult(X509Certificate2 leaf, InfisicalSignedCertificate signed) + { + InfisicalCertificateResult result = new InfisicalCertificateResult { Leaf = leaf }; + if (signed != null) + { + result.SerialNumber = signed.SerialNumber; + result.CertificatePem = signed.CertificatePem; + result.CertificateChainPem = signed.CertificateChainPem; + result.PrivateKeyPem = signed.PrivateKeyPem; + } + + List chainCerts = signed != null ? CollectChainCertificates(signed) : new List(); + List intermediates = new List(); + X509Certificate2 root = null; + foreach (X509Certificate2 cert in chainCerts) + { + if (IsSelfSigned(cert)) { if (root == null) { root = cert; } } + else { intermediates.Add(cert); } + } + + result.Intermediates = intermediates.ToArray(); + result.Root = root; + + List ordered = new List(); + if (leaf != null) { ordered.Add(leaf); } + ordered.AddRange(intermediates); + if (root != null) { ordered.Add(root); } + result.Chain = ordered.ToArray(); + return result; + } + + private static List CollectChainCertificates(InfisicalSignedCertificate signed) + { + List chainCerts = PemCertificateBuilder.ReadCertificateChain(signed.CertificateChainPem); + if (!string.IsNullOrEmpty(signed.IssuingCaCertificatePem)) + { + foreach (X509Certificate2 issuing in PemCertificateBuilder.ReadCertificateChain(signed.IssuingCaCertificatePem)) + { + chainCerts.Add(issuing); + } + } + + HashSet seen = new HashSet(StringComparer.OrdinalIgnoreCase); + List deduped = new List(); + foreach (X509Certificate2 cert in chainCerts) + { + if (cert == null) { continue; } + if (seen.Add(cert.Thumbprint)) { deduped.Add(cert); } + } + + return deduped; + } + + private static bool IsSelfSigned(X509Certificate2 cert) + { + if (cert == null) { return false; } + return string.Equals(cert.Subject, cert.Issuer, StringComparison.OrdinalIgnoreCase); + } + + private static List BuildLocalChain(X509Certificate2 leaf) + { + List result = new List(); + using (X509Chain chain = new X509Chain()) + { + chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + chain.ChainPolicy.VerificationFlags = + X509VerificationFlags.IgnoreNotTimeValid | + X509VerificationFlags.IgnoreNotTimeNested | + X509VerificationFlags.IgnoreInvalidName | + X509VerificationFlags.IgnoreInvalidPolicy | + X509VerificationFlags.IgnoreEndRevocationUnknown | + X509VerificationFlags.IgnoreCertificateAuthorityRevocationUnknown | + X509VerificationFlags.IgnoreRootRevocationUnknown | + X509VerificationFlags.IgnoreCtlNotTimeValid | + X509VerificationFlags.IgnoreCtlSignerRevocationUnknown | + X509VerificationFlags.IgnoreInvalidBasicConstraints | + X509VerificationFlags.IgnoreWrongUsage; + + try { chain.Build(leaf); } + catch { return result; } + + foreach (X509ChainElement element in chain.ChainElements) + { + if (element != null && element.Certificate != null) + { + result.Add(new X509Certificate2(element.Certificate.RawData)); + } + } + } + + return result; + } + + private static string ExportCertificateToPem(X509Certificate2 cert) + { + byte[] der = cert.Export(X509ContentType.Cert); + StringBuilder sb = new StringBuilder(); + sb.AppendLine("-----BEGIN CERTIFICATE-----"); + sb.AppendLine(Convert.ToBase64String(der, Base64FormattingOptions.InsertLineBreaks)); + sb.AppendLine("-----END CERTIFICATE-----"); + return sb.ToString(); + } + + private static string ReadString(IDictionary source, params string[] keys) + { + foreach (string key in keys) + { + if (source.Contains(key)) + { + object value = source[key]; + if (value != null) + { + string text = value.ToString(); + if (!string.IsNullOrEmpty(text)) + { + return text; + } + } + } + } + + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs b/src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs new file mode 100644 index 0000000..2aae813 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalCsrBuilder.cs @@ -0,0 +1,201 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Pkcs; +using Org.BouncyCastle.Asn1.Sec; +using Org.BouncyCastle.Asn1.X509; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Pkcs; +using Org.BouncyCastle.Security; +using BcAttribute = Org.BouncyCastle.Asn1.Cms.Attribute; + +namespace PSInfisicalAPI.Pki +{ + public enum InfisicalKeyAlgorithm + { + Rsa = 0, + Ecdsa = 1, + Ed25519 = 2 + } + + public enum InfisicalEcCurve + { + P256 = 0, + P384 = 1 + } + + public sealed class InfisicalCsrSubject + { + public string CommonName { get; set; } + public string Country { get; set; } + public string State { get; set; } + public string Locality { get; set; } + public string Organization { get; set; } + public string OrganizationalUnit { get; set; } + public string EmailAddress { get; set; } + } + + public sealed class InfisicalCsrOptions + { + public InfisicalKeyAlgorithm KeyAlgorithm { get; set; } = InfisicalKeyAlgorithm.Rsa; + public int RsaKeySize { get; set; } = 2048; + public InfisicalEcCurve EcCurve { get; set; } = InfisicalEcCurve.P256; + } + + public sealed class InfisicalCsrResult + { + public string CsrPem { get; set; } + public string PrivateKeyPem { get; set; } + } + + public static class InfisicalCsrBuilder + { + public static InfisicalCsrResult Build(InfisicalCsrSubject subject, IEnumerable dnsNames, IEnumerable ipAddresses, InfisicalCsrOptions options) + { + if (subject == null) { throw new ArgumentNullException(nameof(subject)); } + if (string.IsNullOrEmpty(subject.CommonName)) { throw new ArgumentException("Subject.CommonName is required.", nameof(subject)); } + if (options == null) { options = new InfisicalCsrOptions(); } + + SecureRandom random = new SecureRandom(); + AsymmetricCipherKeyPair keyPair = GenerateKeyPair(options, random); + string signatureAlgorithm = ResolveSignatureAlgorithm(options); + + X509Name x509Name = BuildX509Name(subject); + Asn1Set attributes = BuildSanAttributes(dnsNames, ipAddresses); + + Pkcs10CertificationRequest pkcs10 = new Pkcs10CertificationRequest(signatureAlgorithm, x509Name, keyPair.Public, attributes, keyPair.Private); + + return new InfisicalCsrResult + { + CsrPem = WritePem(pkcs10), + PrivateKeyPem = WritePem(keyPair.Private) + }; + } + + private static AsymmetricCipherKeyPair GenerateKeyPair(InfisicalCsrOptions options, SecureRandom random) + { + switch (options.KeyAlgorithm) + { + case InfisicalKeyAlgorithm.Rsa: + { + int keySize = options.RsaKeySize; + if (keySize != 2048 && keySize != 3072 && keySize != 4096) + { + throw new ArgumentException("RsaKeySize must be 2048, 3072, or 4096.", nameof(options)); + } + + RsaKeyPairGenerator generator = new RsaKeyPairGenerator(); + generator.Init(new KeyGenerationParameters(random, keySize)); + return generator.GenerateKeyPair(); + } + case InfisicalKeyAlgorithm.Ecdsa: + { + DerObjectIdentifier curveOid = options.EcCurve == InfisicalEcCurve.P384 + ? SecObjectIdentifiers.SecP384r1 + : SecObjectIdentifiers.SecP256r1; + ECKeyPairGenerator generator = new ECKeyPairGenerator("ECDSA"); + generator.Init(new ECKeyGenerationParameters(curveOid, random)); + return generator.GenerateKeyPair(); + } + case InfisicalKeyAlgorithm.Ed25519: + { + Ed25519KeyPairGenerator generator = new Ed25519KeyPairGenerator(); + generator.Init(new Ed25519KeyGenerationParameters(random)); + return generator.GenerateKeyPair(); + } + default: + throw new ArgumentOutOfRangeException(nameof(options), options.KeyAlgorithm, "Unsupported KeyAlgorithm."); + } + } + + private static string ResolveSignatureAlgorithm(InfisicalCsrOptions options) + { + switch (options.KeyAlgorithm) + { + case InfisicalKeyAlgorithm.Rsa: + return "SHA256WITHRSA"; + case InfisicalKeyAlgorithm.Ecdsa: + return options.EcCurve == InfisicalEcCurve.P384 ? "SHA384WITHECDSA" : "SHA256WITHECDSA"; + case InfisicalKeyAlgorithm.Ed25519: + return "Ed25519"; + default: + throw new ArgumentOutOfRangeException(nameof(options), options.KeyAlgorithm, "Unsupported KeyAlgorithm."); + } + } + + private static X509Name BuildX509Name(InfisicalCsrSubject subject) + { + List order = new List(); + Dictionary values = new Dictionary(); + + AppendComponent(order, values, X509Name.C, subject.Country); + AppendComponent(order, values, X509Name.ST, subject.State); + AppendComponent(order, values, X509Name.L, subject.Locality); + AppendComponent(order, values, X509Name.O, subject.Organization); + AppendComponent(order, values, X509Name.OU, subject.OrganizationalUnit); + AppendComponent(order, values, X509Name.CN, subject.CommonName); + AppendComponent(order, values, X509Name.EmailAddress, subject.EmailAddress); + + return new X509Name(order, values); + } + + private static void AppendComponent(List order, Dictionary values, DerObjectIdentifier oid, string value) + { + if (string.IsNullOrEmpty(value)) { return; } + order.Add(oid); + values[oid] = value; + } + + private static Asn1Set BuildSanAttributes(IEnumerable dnsNames, IEnumerable ipAddresses) + { + List generalNames = new List(); + if (dnsNames != null) + { + foreach (string dns in dnsNames) + { + if (string.IsNullOrEmpty(dns)) { continue; } + generalNames.Add(new GeneralName(GeneralName.DnsName, dns)); + } + } + + if (ipAddresses != null) + { + foreach (string ip in ipAddresses) + { + if (string.IsNullOrEmpty(ip)) { continue; } + IPAddress parsed; + if (!IPAddress.TryParse(ip, out parsed)) { continue; } + generalNames.Add(new GeneralName(GeneralName.IPAddress, ip)); + } + } + + if (generalNames.Count == 0) { return null; } + + GeneralNames sanValue = new GeneralNames(generalNames.ToArray()); + X509Extensions extensions = new X509Extensions( + new Dictionary + { + { X509Extensions.SubjectAlternativeName, new X509Extension(false, new DerOctetString(sanValue)) } + }); + + BcAttribute extensionRequest = new BcAttribute(PkcsObjectIdentifiers.Pkcs9AtExtensionRequest, new DerSet(extensions)); + return new DerSet(extensionRequest); + } + + private static string WritePem(object obj) + { + using (StringWriter sw = new StringWriter()) + { + PemWriter pemWriter = new PemWriter(sw); + pemWriter.WriteObject(obj); + pemWriter.Writer.Flush(); + return sw.ToString(); + } + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalLocalCertificateLookup.cs b/src/PSInfisicalAPI/Pki/InfisicalLocalCertificateLookup.cs new file mode 100644 index 0000000..366ed98 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalLocalCertificateLookup.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; + +namespace PSInfisicalAPI.Pki +{ + internal static class InfisicalLocalCertificateLookup + { + public static X509Certificate2 FindMatch(StoreName storeName, StoreLocation storeLocation, string commonName, IEnumerable candidateSerialNumbers) + { + HashSet serialSet = NormalizeSerials(candidateSerialNumbers); + string subjectFilter = !string.IsNullOrEmpty(commonName) ? string.Concat("CN=", commonName) : null; + + X509Store store = new X509Store(storeName, storeLocation); + try + { + store.Open(OpenFlags.ReadOnly); + + X509Certificate2 bestMatch = null; + foreach (X509Certificate2 candidate in store.Certificates) + { + if (subjectFilter != null && candidate.Subject.IndexOf(subjectFilter, StringComparison.OrdinalIgnoreCase) < 0) + { + continue; + } + + if (serialSet.Count > 0) + { + string normalizedSerial = NormalizeSerial(candidate.SerialNumber); + if (!serialSet.Contains(normalizedSerial)) + { + continue; + } + } + + if (bestMatch == null || candidate.NotAfter > bestMatch.NotAfter) + { + bestMatch = candidate; + } + } + + return bestMatch; + } + finally + { + store.Close(); + } + } + + public static bool IsRenewable(X509Certificate2 cert, int renewalThresholdDays) + { + if (cert == null) { return true; } + DateTime threshold = DateTime.UtcNow.AddDays(renewalThresholdDays); + return cert.NotAfter.ToUniversalTime() <= threshold; + } + + private static HashSet NormalizeSerials(IEnumerable serials) + { + HashSet set = new HashSet(StringComparer.OrdinalIgnoreCase); + if (serials == null) { return set; } + foreach (string serial in serials) + { + string normalized = NormalizeSerial(serial); + if (!string.IsNullOrEmpty(normalized)) + { + set.Add(normalized); + } + } + + return set; + } + + private static string NormalizeSerial(string value) + { + if (string.IsNullOrEmpty(value)) { return null; } + string trimmed = value.Trim(); + if (trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + trimmed = trimmed.Substring(2); + } + + return trimmed.Replace(":", string.Empty).Replace(" ", string.Empty).TrimStart('0'); + } + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs index 06b38a3..ff8de17 100644 --- a/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs +++ b/src/PSInfisicalAPI/Pki/InfisicalPkiClient.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using Newtonsoft.Json.Linq; using PSInfisicalAPI.Connections; using PSInfisicalAPI.Endpoints; using PSInfisicalAPI.Errors; @@ -8,6 +9,7 @@ using PSInfisicalAPI.Http; using PSInfisicalAPI.Logging; using PSInfisicalAPI.Models; using PSInfisicalAPI.Serialization; +using System.Linq; namespace PSInfisicalAPI.Pki { @@ -30,23 +32,23 @@ namespace PSInfisicalAPI.Pki public InfisicalCertificateAuthority[] ListInternalCertificateAuthorities(InfisicalConnection connection, string projectId) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } - string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); List> query = null; - if (!string.IsNullOrEmpty(resolvedProjectId)) + if (!string.IsNullOrEmpty(projectId)) { - query = new List> { new KeyValuePair("projectId", resolvedProjectId) }; + query = new List> { new KeyValuePair("projectId", projectId) }; } try { _logger.Information(Component, "Attempting to list Infisical internal certificate authorities. Please Wait..."); InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.ListInternalCertificateAuthorities, "ListInternalCertificateAuthorities", null, query, null); - InfisicalInternalCaListResponseDto dto = _serializer.Deserialize(response.Body); + string body = response.Body; response.Clear(); - List source = dto != null ? (dto.CertificateAuthorities ?? dto.Cas) : null; - InfisicalCertificateAuthority[] mapped = InfisicalCaMapper.MapMany(source, resolvedProjectId); + List source = ParseCaListBody(body); + string fallbackProjectId = !string.IsNullOrEmpty(projectId) ? projectId : connection.ProjectId; + InfisicalCertificateAuthority[] mapped = InfisicalCaMapper.MapMany(source, fallbackProjectId); _logger.Information(Component, "Infisical internal certificate authority list retrieval was successful."); return mapped; } @@ -63,21 +65,22 @@ namespace PSInfisicalAPI.Pki if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); } Dictionary pathParameters = new Dictionary { { "caId", caId } }; + List> query = null; + if (!string.IsNullOrEmpty(projectId)) + { + query = new List> { new KeyValuePair("projectId", projectId) }; + } try { _logger.Information(Component, string.Concat("Attempting to retrieve Infisical internal certificate authority '", caId, "'. Please Wait...")); - InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.RetrieveInternalCertificateAuthority, "RetrieveInternalCertificateAuthority", pathParameters, null, null); - InfisicalInternalCaSingleResponseDto dto = _serializer.Deserialize(response.Body); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.RetrieveInternalCertificateAuthority, "RetrieveInternalCertificateAuthority", pathParameters, query, null); + string body = response.Body; response.Clear(); - InfisicalInternalCaResponseDto inner = dto != null ? (dto.CertificateAuthority ?? dto.Ca) : null; - if (inner == null) - { - inner = _serializer.Deserialize(response.Body); - } - - InfisicalCertificateAuthority mapped = InfisicalCaMapper.Map(inner, FirstNonEmpty(projectId, connection.ProjectId)); + InfisicalInternalCaResponseDto inner = ParseCaSingleBody(body); + string fallbackProjectId = !string.IsNullOrEmpty(projectId) ? projectId : connection.ProjectId; + InfisicalCertificateAuthority mapped = InfisicalCaMapper.Map(inner, fallbackProjectId); _logger.Information(Component, "Infisical internal certificate authority retrieval was successful."); return mapped; } @@ -88,6 +91,68 @@ namespace PSInfisicalAPI.Pki } } + public InfisicalCertificate RetrieveCertificate(InfisicalConnection connection, string identifier) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(identifier)) { throw new InfisicalConfigurationException("Identifier (serial number or id) is required."); } + + Dictionary pathParameters = new Dictionary { { "serialNumber", identifier } }; + + try + { + _logger.Information(Component, string.Concat("Attempting to retrieve Infisical certificate '", identifier, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.RetrieveCertificate, "RetrieveCertificate", pathParameters, null, null); + string body = response.Body; + response.Clear(); + + InfisicalCertificateResponseDto inner = ParseCertificateSingleBody(body); + InfisicalCertificate mapped = InfisicalCertificateMapper.Map(inner, connection.ProjectId); + _logger.Information(Component, "Infisical certificate retrieval was successful."); + return mapped; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate retrieval failed."); + throw; + } + } + + private List ParseCaListBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type == JTokenType.Array) + { + return token.ToObject>(); + } + + InfisicalInternalCaListResponseDto wrapper = token.ToObject(); + return wrapper != null ? (wrapper.CertificateAuthorities ?? wrapper.Cas) : null; + } + + private InfisicalInternalCaResponseDto ParseCaSingleBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return null; } + JObject obj = (JObject)token; + + if (obj["certificateAuthority"] is JObject ca1) { return ca1.ToObject(); } + if (obj["ca"] is JObject ca2) { return ca2.ToObject(); } + return obj.ToObject(); + } + + private InfisicalCertificateResponseDto ParseCertificateSingleBody(string body) + { + if (string.IsNullOrEmpty(body)) { return null; } + JToken token = JToken.Parse(body); + if (token.Type != JTokenType.Object) { return null; } + JObject obj = (JObject)token; + + if (obj["certificate"] is JObject cert) { return cert.ToObject(); } + return obj.ToObject(); + } + public InfisicalCertificateSearchResult SearchCertificates(InfisicalConnection connection, InfisicalCertificateSearchQuery query) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } @@ -118,6 +183,93 @@ namespace PSInfisicalAPI.Pki } } + public InfisicalSignedCertificate SignCertificateBySubscriber(InfisicalConnection connection, string subscriberName, string projectId, string csrPem) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(subscriberName)) { throw new InfisicalConfigurationException("SubscriberName is required."); } + if (string.IsNullOrEmpty(csrPem)) { throw new InfisicalConfigurationException("CSR is required."); } + string resolvedProjectId = FirstNonEmpty(projectId, connection.ProjectId); + if (string.IsNullOrEmpty(resolvedProjectId)) { throw new InfisicalConfigurationException("ProjectId is required."); } + + Dictionary pathParameters = new Dictionary { { "subscriberName", subscriberName } }; + InfisicalSignCertificateBySubscriberRequestDto request = new InfisicalSignCertificateBySubscriberRequestDto + { + ProjectId = resolvedProjectId, + Csr = csrPem + }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to sign certificate via subscriber '", subscriberName, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.SignCertificateBySubscriber, "SignCertificateBySubscriber", pathParameters, null, body); + InfisicalSignCertificateResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalSignedCertificate signed = MapSigned(dto); + _logger.Information(Component, "Infisical certificate signing (subscriber) was successful."); + return signed; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate signing (subscriber) failed."); + throw; + } + } + + public InfisicalSignedCertificate SignCertificateByCa(InfisicalConnection connection, string caId, string csrPem, string commonName, string altNames, string ttl, string notBefore, string notAfter, string friendlyName, string pkiCollectionId, IEnumerable keyUsages, IEnumerable extendedKeyUsages) + { + if (connection == null) { throw new ArgumentNullException(nameof(connection)); } + if (string.IsNullOrEmpty(caId)) { throw new InfisicalConfigurationException("CaId is required."); } + if (string.IsNullOrEmpty(csrPem)) { throw new InfisicalConfigurationException("CSR is required."); } + if (string.IsNullOrEmpty(ttl) && string.IsNullOrEmpty(notAfter)) { throw new InfisicalConfigurationException("Either Ttl or NotAfter must be provided."); } + + Dictionary pathParameters = new Dictionary { { "caId", caId } }; + InfisicalSignCertificateByCaRequestDto request = new InfisicalSignCertificateByCaRequestDto + { + Csr = csrPem, + CommonName = commonName, + AltNames = altNames, + Ttl = ttl, + NotBefore = notBefore, + NotAfter = notAfter, + FriendlyName = friendlyName, + PkiCollectionId = pkiCollectionId, + KeyUsages = keyUsages != null ? keyUsages.ToList() : null, + ExtendedKeyUsages = extendedKeyUsages != null ? extendedKeyUsages.ToList() : null + }; + string body = _serializer.Serialize(request); + + try + { + _logger.Information(Component, string.Concat("Attempting to sign certificate via CA '", caId, "'. Please Wait...")); + InfisicalHttpResponse response = _invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.SignCertificateByCa, "SignCertificateByCa", pathParameters, null, body); + InfisicalSignCertificateResponseDto dto = _serializer.Deserialize(response.Body); + response.Clear(); + + InfisicalSignedCertificate signed = MapSigned(dto); + _logger.Information(Component, "Infisical certificate signing (CA) was successful."); + return signed; + } + catch (Exception) + { + _logger.Error(Component, "Infisical certificate signing (CA) failed."); + throw; + } + } + + private static InfisicalSignedCertificate MapSigned(InfisicalSignCertificateResponseDto dto) + { + if (dto == null) { return null; } + return new InfisicalSignedCertificate + { + SerialNumber = dto.SerialNumber, + CertificatePem = dto.Certificate, + CertificateChainPem = dto.CertificateChain, + IssuingCaCertificatePem = dto.IssuingCaCertificate + }; + } + public InfisicalCertificateBundle GetCertificateBundle(InfisicalConnection connection, string serialNumber) { if (connection == null) { throw new ArgumentNullException(nameof(connection)); } diff --git a/src/PSInfisicalAPI/Pki/InfisicalPrivateKeyProtection.cs b/src/PSInfisicalAPI/Pki/InfisicalPrivateKeyProtection.cs new file mode 100644 index 0000000..0a793b9 --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalPrivateKeyProtection.cs @@ -0,0 +1,10 @@ +namespace PSInfisicalAPI.Pki +{ + public enum InfisicalPrivateKeyProtection + { + Exportable = 0, + LocalOnly = 1, + NonExportable = 2, + Ephemeral = 3 + } +} diff --git a/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs b/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs new file mode 100644 index 0000000..e6a82eb --- /dev/null +++ b/src/PSInfisicalAPI/Pki/InfisicalSignCertificateDtos.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace PSInfisicalAPI.Pki +{ + internal sealed class InfisicalSignCertificateBySubscriberRequestDto + { + [JsonProperty("projectId")] public string ProjectId { get; set; } + [JsonProperty("csr")] public string Csr { get; set; } + } + + internal sealed class InfisicalSignCertificateByCaRequestDto + { + [JsonProperty("csr")] public string Csr { get; set; } + [JsonProperty("commonName", NullValueHandling = NullValueHandling.Ignore)] public string CommonName { get; set; } + [JsonProperty("altNames", NullValueHandling = NullValueHandling.Ignore)] public string AltNames { get; set; } + [JsonProperty("ttl", NullValueHandling = NullValueHandling.Ignore)] public string Ttl { get; set; } + [JsonProperty("notBefore", NullValueHandling = NullValueHandling.Ignore)] public string NotBefore { get; set; } + [JsonProperty("notAfter", NullValueHandling = NullValueHandling.Ignore)] public string NotAfter { get; set; } + [JsonProperty("friendlyName", NullValueHandling = NullValueHandling.Ignore)] public string FriendlyName { get; set; } + [JsonProperty("pkiCollectionId", NullValueHandling = NullValueHandling.Ignore)] public string PkiCollectionId { get; set; } + [JsonProperty("keyUsages", NullValueHandling = NullValueHandling.Ignore)] public List KeyUsages { get; set; } + [JsonProperty("extendedKeyUsages", NullValueHandling = NullValueHandling.Ignore)] public List ExtendedKeyUsages { get; set; } + } + + internal sealed class InfisicalSignCertificateResponseDto + { + [JsonProperty("certificate")] public string Certificate { get; set; } + [JsonProperty("certificateChain")] public string CertificateChain { get; set; } + [JsonProperty("issuingCaCertificate")] public string IssuingCaCertificate { get; set; } + [JsonProperty("serialNumber")] public string SerialNumber { get; set; } + } +}