diff --git a/CHANGELOG.md b/CHANGELOG.md index d554b9a..4a66f20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,36 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased +## 2026.06.16.0217 + +- Build produced from commit 6318d06362ad. + +## Unreleased (carried forward) + +## 2026.06.16.0215 + +- Build produced from commit 6318d06362ad. + +## Unreleased (carried forward) + +## 2026.06.16.0213 + +- Build produced from commit 6318d06362ad. + +## Unreleased (carried forward) + +## 2026.06.16.0207 + +- Build produced from commit 6318d06362ad. + +## Unreleased (carried forward) + +## 2026.06.16.0156 + +- Build produced from commit 6318d06362ad. + +## Unreleased (carried forward) + ## 2026.06.10.2018 - Build produced from commit daf1cdce6576. @@ -51,7 +81,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos - Added `Get-InfisicalSANList` cmdlet: emits a deduplicated SAN candidate set containing the local device name, the device name suffixed with each non-empty DNS suffix found across operational adapters and the system primary domain, every IPv4 unicast address falling within RFC 1918 (10/8, 172.16/12, 192.168/16) or CGNAT (100.64/10), and the IPv4/IPv6 loopback addresses (127.0.0.1, ::1). Intended to feed `Request-InfisicalCertificate -DnsName` directly. - `Get-InfisicalSANList`: added optional `-InclusionExpression` and `-ExclusionExpression` case-insensitive regex filters. Applied in fetch -> include -> exclude -> output order after the deduplicated set is built; both default to unset (no filtering). - `Get-InfisicalSANList`: output is a single strongly-typed `System.String[]` array emitted non-enumerated (`OutputType(string[])`), so variable assignment yields `string[]` rather than `object[]`. This lets `[System.Collections.Generic.List[string]]::AddRange()` consume the result directly and lets the array bind straight to `string[]` parameters such as `Request-InfisicalCertificate -DnsName`. -- `build.ps1` `CmdletsToExport` and `Test-ModuleImports` expected list now contain 51 cmdlets. `docs/DesignSpec.md` updated with `§16.7` (Organizations) and `§16.8` (Sub-Organizations); full MAML help added for all 9 new cmdlets in `Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`. +- `build.ps1` `CmdletsToExport` and `Test-ModuleImports` expected list now contain 51 cmdlets. `docs/DesignSpec.md` updated with `§16.7` (Organizations) and `§16.8` (Sub-Organizations); full MAML help added for all 9 new cmdlets in `Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`. ## 2026.06.06.2229 @@ -213,7 +243,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos - **BREAKING**: Removed the plural-noun discovery cmdlets `Get-InfisicalProjects`, `Get-InfisicalEnvironments`, `Get-InfisicalFolders`, `Get-InfisicalTags`, `Get-InfisicalSecrets`, and `Get-InfisicalCertificates`. Their behavior is now folded into the corresponding singular cmdlets via a `List` (default) / single-record parameter set pair, matching the existing `Get-InfisicalCertificateAuthority` precedent. Callers should drop the trailing `s`; invocation without the identity parameter (`-ProjectId`, `-EnvironmentSlugOrId`, `-FolderNameOrId`, `-TagSlugOrId`, `-SecretName`, `-SerialNumber`) now returns the list, and supplying the identity parameter returns the single record. No back-compat aliases were added. - Added `Get-InfisicalPkiSubscriber` with `List` (default) and `ByName` parameter sets, backed by new `InfisicalPkiClient.ListPkiSubscribers` and `GetPkiSubscriber` methods, an `InfisicalPkiSubscriber` model, and corresponding DTOs/mapper. Use the emitted `Name` (slug) on `Request-InfisicalCertificate -PkiSubscriberSlug`. - **Bug fix**: `Request-InfisicalCertificate -PkiSubscriberSlug ...` was returning 404 because the registry's `SignCertificateBySubscriber` endpoint pointed at `/api/v1/pki/pki-subscribers/{subscriberName}/sign-certificate` and `/api/v1/cert-manager/pki-subscribers/...`. Per Infisical's `v1/index.ts`, the subscriber router is mounted at `/pki/subscribers`, so the single correct path is `/api/v1/pki/subscribers/{subscriberName}/sign-certificate`. The redundant `cert-manager` template was removed; the PKI endpoint registry tests were updated to match. -- Updated MAML help in `Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`: the six consolidated cmdlets and the new `Get-InfisicalPkiSubscriber` each ship three examples — two straight-line invocations (one per parameter set) plus one `OrderedDictionary` splat example. All in-text references to the removed plural cmdlets across other cmdlets' examples were updated to the singular form. +- Updated MAML help in `Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`: the six consolidated cmdlets and the new `Get-InfisicalPkiSubscriber` each ship three examples — two straight-line invocations (one per parameter set) plus one `OrderedDictionary` splat example. All in-text references to the removed plural cmdlets across other cmdlets' examples were updated to the singular form. - `build.ps1`: `CmdletsToExport` and the `Test-ModuleImports` expected cmdlet list were updated to drop the six plural cmdlets and add `Get-InfisicalPkiSubscriber` (total: 34 exported cmdlets). ## 2026.06.04.1825 @@ -291,9 +321,9 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos - 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. + - **`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`. @@ -315,7 +345,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## 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)). +- **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 @@ -323,7 +353,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased (carried forward) -- **M10 polish — formatting, type metadata, and PKI route aliases**: +- **M10 polish — formatting, type metadata, and PKI route aliases**: - Added default table views and `DefaultDisplayPropertySet` entries for `InfisicalCertificateAuthority`, `InfisicalCertificate`, and `InfisicalCertificateBundle` in the module `Format.ps1xml` / `Types.ps1xml`. - Realigned PKI endpoint registry to current Infisical paths: `ListInternalCertificateAuthorities` and `RetrieveInternalCertificateAuthority` now use `/api/v1/cert-manager/ca/internal[/{caId}]` as primary, with legacy `/api/v1/pki/ca/internal[/{caId}]` retained as a fallback alias. `GetCertificateBundle` and `RetrieveCertificate` similarly carry `cert-manager` fallback aliases. - `InfisicalApiInvoker.InvokeWithCandidateFallback` walks the candidate list and falls back on `404`/`405`, used by `InfisicalPkiClient` so older self-hosted Infisical instances are tolerated transparently. @@ -334,7 +364,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased (carried forward) -- **M10 — PKI Internal CAs, Certificates & Windows Store integration**: +- **M10 — PKI Internal CAs, Certificates & Windows Store integration**: - **`Get-InfisicalCertificateAuthority`** lists internal certificate authorities for the current project, or returns a single CA with `-CaId`. - **`Search-InfisicalCertificate`** wraps `POST /api/v1/projects/{projectId}/certificates/search` with rich filters (`-CommonName`, `-FriendlyName`, `-Search`, `-Status`, `-CaId`, `-ProfileId`, `-ApplicationId`, `-EnrollmentType`, `-KeyAlgorithm`, `-SignatureAlgorithm`, `-Source`, `-NotAfterFrom/To`, `-NotBeforeFrom/To`, `-SortBy/-SortOrder`, `-Limit/-Offset`). Auto-paginates unless `-NoAutoPage` is set. - **`ConvertTo-InfisicalCertificate`** accepts an `InfisicalCertificate`, `InfisicalCertificateBundle`, or `-SerialNumber`, fetches the bundle endpoint when needed, and emits a `System.Security.Cryptography.X509Certificates.X509Certificate2` with the private key attached. `-NoPrivateKey` skips key parsing; `-IncludeChain` additionally emits intermediates; `-KeyStorageFlags` controls import behavior. @@ -361,7 +391,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## 2026.06.03.2207 - Build produced from commit 09c3d5c68bbc. -- **M9 — Bulk, Duplicate & Inheritance**: +- **M9 — Bulk, Duplicate & Inheritance**: - **Bulk parameter sets** added to `New-InfisicalSecret`, `Update-InfisicalSecret`, and `Remove-InfisicalSecret` accepting `-Secrets Hashtable[]`; client methods `CreateBatch`/`UpdateBatch`/`DeleteBatch` wrap `POST|PATCH|DELETE /api/v3/secrets/batch/raw`. - **`Copy-InfisicalSecret`** cmdlet added, wrapping `POST /api/v4/secrets/duplicate` with source/destination environment + path parameters and per-attribute copy toggles. - **Connection inheritance** centralized in `InfisicalCmdletBase` (`ResolveProjectId`/`ResolveEnvironment`/`ResolveSecretPath`/`ResolveApiVersion`/`ResolveOrganizationId`). Explicit parameters always win; missing values fall back to the active connection and emit a `-Verbose` line. diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 index a730dbe..8f802e3 100644 --- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 +++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSInfisicalAPI.psm1' - ModuleVersion = '2026.06.10.2018' + ModuleVersion = '2026.06.16.0217' GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51' Author = 'Grace Solutions' CompanyName = 'Grace Solutions' @@ -74,7 +74,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 = 'daf1cdce6576' + CommitHash = '6318d06362ad' } } } \ No newline at end of file diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll index f776afa..ef4f494 100644 Binary files a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll and b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll differ diff --git a/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs index 1bc6213..fa96447 100644 --- a/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs @@ -49,14 +49,18 @@ namespace PSInfisicalAPI.Cmdlets { try { + Logger.Information("ConvertTo-InfisicalSecretDictionary", string.Concat("Processing ", _buffer.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " input secret(s).")); + if (AsPlainText.IsPresent) { Dictionary plain = BuildDictionary(secret => secret.GetPlainTextValue()); + Logger.Information("ConvertTo-InfisicalSecretDictionary", string.Concat("Built plain-text dictionary with ", plain.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries.")); WriteObject(plain); } else { Dictionary secure = BuildDictionary(secret => secret.SecretValue); + Logger.Information("ConvertTo-InfisicalSecretDictionary", string.Concat("Built SecureString dictionary with ", secure.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries.")); WriteObject(secure); } } diff --git a/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs index eb4f459..af61551 100644 --- a/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs @@ -75,6 +75,8 @@ namespace PSInfisicalAPI.Cmdlets { } + Logger.Information("Export-InfisicalSecrets", string.Concat("Exporting ", _buffer.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s) as ", Format.ToString(), (Path != null ? string.Concat(" to '", Path.FullName, "'") : string.Empty), ".")); + InfisicalExportRequest request = new InfisicalExportRequest { Secrets = ApplySecretsPrefix(_buffer, SecretsPrefix, ForceSecretsPrefix.IsPresent), diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs index 9fa6433..cfa4f95 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateApplicationCmdlet.cs @@ -46,6 +46,7 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalCertificateApplication[] all = client.ListCertificateApplications(connection, ProjectId, Limit, Offset); + Logger.Information("Get-InfisicalCertificateApplication", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate application(s).")); foreach (InfisicalCertificateApplication app in all) { WriteObject(app); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs index d712ac4..b5435e7 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateAuthorityCmdlet.cs @@ -52,6 +52,7 @@ namespace PSInfisicalAPI.Cmdlets } } + Logger.Information("Get-InfisicalCertificateAuthority", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate authority/authorities (kind=", Kind, ").")); foreach (InfisicalCertificateAuthority ca in all) { WriteObject(ca); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs index 079f57b..7109748 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateCmdlet.cs @@ -129,6 +129,8 @@ namespace PSInfisicalAPI.Cmdlets query.Offset = (query.Offset ?? 0) + page.Certificates.Length; } + + Logger.Information("Get-InfisicalCertificate", string.Concat("Returned ", emitted.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate(s).")); } catch (Exception exception) { diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs index 0472507..840b9c8 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificatePolicyCmdlet.cs @@ -39,6 +39,7 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalCertificatePolicy[] all = client.ListCertificatePolicies(connection, ProjectId, Limit, Offset); + Logger.Information("Get-InfisicalCertificatePolicy", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate policy/policies.")); foreach (InfisicalCertificatePolicy policy in all) { WriteObject(policy); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs index 02cf1ae..737d3ee 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalCertificateProfileCmdlet.cs @@ -42,6 +42,7 @@ namespace PSInfisicalAPI.Cmdlets bool? includeConfigs = MyInvocation.BoundParameters.ContainsKey("IncludeConfigs") ? (bool?)IncludeConfigs.IsPresent : null; InfisicalCertificateProfile[] all = client.ListCertificateProfiles(connection, ProjectId, Limit, Offset, includeConfigs); + Logger.Information("Get-InfisicalCertificateProfile", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " certificate profile(s).")); foreach (InfisicalCertificateProfile profile in all) { WriteObject(profile); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentCmdlet.cs index 1b90d75..01efdf6 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentCmdlet.cs @@ -35,6 +35,7 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalEnvironment[] envs = client.List(connection, ProjectId); + Logger.Information("Get-InfisicalEnvironment", string.Concat("Returned ", envs.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " environment(s).")); foreach (InfisicalEnvironment env in envs) { WriteObject(env); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentVariableCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentVariableCmdlet.cs index 472c423..87f1624 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentVariableCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentVariableCmdlet.cs @@ -5,8 +5,10 @@ namespace PSInfisicalAPI.Cmdlets { [Cmdlet(VerbsCommon.Get, "InfisicalEnvironmentVariable")] [OutputType(typeof(string))] - public sealed class GetInfisicalEnvironmentVariableCmdlet : PSCmdlet + public sealed class GetInfisicalEnvironmentVariableCmdlet : InfisicalCmdletBase { + private const string Component = "Get-InfisicalEnvironmentVariable"; + private static readonly EnvironmentVariableTarget[] TargetOrder = new[] { EnvironmentVariableTarget.Process, @@ -18,26 +20,38 @@ namespace PSInfisicalAPI.Cmdlets [ValidateNotNullOrEmpty] public string Name { get; set; } + [Parameter(Position = 1)] + public EnvironmentVariableTarget? Scope { get; set; } + protected override void ProcessRecord() { - foreach (EnvironmentVariableTarget target in TargetOrder) + EnvironmentVariableTarget[] targets = Scope.HasValue ? new[] { Scope.Value } : TargetOrder; + + foreach (EnvironmentVariableTarget target in targets) { + Logger.Verbose(Component, string.Concat("Searching ", target.ToString(), " scope for environment variable '", Name, "'.")); + string value; try { value = Environment.GetEnvironmentVariable(Name, target); } - catch + catch (Exception exception) { + Logger.Verbose(Component, string.Concat("Failed to read ", target.ToString(), " scope for environment variable '", Name, "': ", exception.Message)); continue; } if (!string.IsNullOrEmpty(value)) { + Logger.Information(Component, string.Concat("Found environment variable '", Name, "' in ", target.ToString(), " scope.")); WriteObject(value); return; } } + + string scopeDescription = Scope.HasValue ? string.Concat(Scope.Value.ToString(), " scope") : "Process, User, or Machine scope"; + Logger.Information(Component, string.Concat("Environment variable '", Name, "' was not found in ", scopeDescription, ".")); } } } diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs index 457cf00..2ae3ce8 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalFolderCmdlet.cs @@ -37,6 +37,7 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalFolder[] folders = client.List(connection, ProjectId, Environment, Path); + Logger.Information("Get-InfisicalFolder", string.Concat("Returned ", folders.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " folder(s) from '", Path ?? "/", "'.")); foreach (InfisicalFolder folder in folders) { WriteObject(folder); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalOrganizationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalOrganizationCmdlet.cs index 783dc0a..5e891b6 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalOrganizationCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalOrganizationCmdlet.cs @@ -33,6 +33,7 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalOrganization[] organizations = client.List(connection); + Logger.Information("Get-InfisicalOrganization", string.Concat("Returned ", organizations.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " organization(s).")); foreach (InfisicalOrganization organization in organizations) { WriteObject(organization); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs index 5cfebda..35b4c39 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalPkiSubscriberCmdlet.cs @@ -35,6 +35,7 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalPkiSubscriber[] all = client.ListPkiSubscribers(connection, ProjectId); + Logger.Information("Get-InfisicalPkiSubscriber", string.Concat("Returned ", all.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " PKI subscriber(s).")); foreach (InfisicalPkiSubscriber subscriber in all) { WriteObject(subscriber); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs index ee58c45..a239a79 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalProjectCmdlet.cs @@ -39,6 +39,7 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalProject[] projects = client.List(connection, Type, IncludeRoles.IsPresent); + Logger.Information("Get-InfisicalProject", string.Concat("Returned ", projects.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " project(s).")); foreach (InfisicalProject project in projects) { WriteObject(project); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretCmdlet.cs index 16cc545..972ded1 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSecretCmdlet.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Management.Automation; using PSInfisicalAPI.Connections; using PSInfisicalAPI.Models; @@ -57,8 +58,13 @@ namespace PSInfisicalAPI.Cmdlets InfisicalSecret secret = client.Retrieve(connection, query); if (secret != null) { + Logger.Information("Get-InfisicalSecret", string.Concat("Returned 1 secret for '", SecretName, "'.")); WriteObject(secret); } + else + { + Logger.Information("Get-InfisicalSecret", string.Concat("No secret returned for '", SecretName, "'.")); + } return; } @@ -79,6 +85,7 @@ namespace PSInfisicalAPI.Cmdlets }; InfisicalSecret[] secrets = client.List(connection, listQuery); + Logger.Information("Get-InfisicalSecret", string.Concat("Returned ", secrets.Length.ToString(CultureInfo.InvariantCulture), " secret(s) from '", SecretPath ?? "/", "' (recursive=", Recursive.IsPresent ? "true" : "false", ", includeImports=", IncludeImports.IsPresent ? "true" : "false", ").")); foreach (InfisicalSecret secret in secrets) { WriteObject(secret); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalSubOrganizationCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSubOrganizationCmdlet.cs index b967590..ab8ff47 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalSubOrganizationCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalSubOrganizationCmdlet.cs @@ -45,6 +45,7 @@ namespace PSInfisicalAPI.Cmdlets bool? isAccessible = MyInvocation.BoundParameters.ContainsKey("IsAccessible") ? (bool?)IsAccessible.IsPresent : null; InfisicalSubOrganization[] subOrganizations = client.List(connection, Limit, Offset, Search, OrderBy, OrderDirection, isAccessible); + Logger.Information("Get-InfisicalSubOrganization", string.Concat("Returned ", subOrganizations.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " sub-organization(s).")); foreach (InfisicalSubOrganization subOrganization in subOrganizations) { WriteObject(subOrganization); diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs index 1cfd9d2..06d9dce 100644 --- a/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalTagCmdlet.cs @@ -35,6 +35,7 @@ namespace PSInfisicalAPI.Cmdlets } InfisicalTag[] tags = client.List(connection, ProjectId); + Logger.Information("Get-InfisicalTag", string.Concat("Returned ", tags.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " tag(s).")); foreach (InfisicalTag tag in tags) { WriteObject(tag); diff --git a/src/PSInfisicalAPI/Cmdlets/ImportInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ImportInfisicalSecretCmdlet.cs index 636c1a8..4eaa520 100644 --- a/src/PSInfisicalAPI/Cmdlets/ImportInfisicalSecretCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/ImportInfisicalSecretCmdlet.cs @@ -49,15 +49,18 @@ namespace PSInfisicalAPI.Cmdlets IInfisicalImporter importer = InfisicalImporterFactory.Create(Format); IList> pairs = importer.Import(Path); + Logger.Information("Import-InfisicalSecret", string.Concat("Parsed ", pairs.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret pair(s) from '", Path.FullName, "' (format=", Format.ToString(), ").")); if (AsPlainText.IsPresent) { Dictionary plain = BuildDictionary(pairs, value => value ?? string.Empty); + Logger.Information("Import-InfisicalSecret", string.Concat("Built plain-text dictionary with ", plain.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries.")); WriteObject(plain); } else { Dictionary secure = BuildDictionary(pairs, value => SecureStringUtility.ToReadOnlySecureString(value ?? string.Empty)); + Logger.Information("Import-InfisicalSecret", string.Concat("Built SecureString dictionary with ", secure.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " entry/entries.")); WriteObject(secure); } } diff --git a/src/PSInfisicalAPI/Cmdlets/NewInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/NewInfisicalSecretCmdlet.cs index 2b41f51..604de84 100644 --- a/src/PSInfisicalAPI/Cmdlets/NewInfisicalSecretCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/NewInfisicalSecretCmdlet.cs @@ -58,10 +58,12 @@ namespace PSInfisicalAPI.Cmdlets Secrets = InfisicalBulkSecretConverter.ToCreateItems(Secrets) }; + Logger.Information("New-InfisicalSecret", string.Concat("Bulk-creating ", Secrets.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s).")); InfisicalSecretsClient bulkClient = new InfisicalSecretsClient(HttpClient, Logger); InfisicalSecret[] created = bulkClient.CreateBatch(connection, bulk); if (created != null) { + Logger.Information("New-InfisicalSecret", string.Concat("Server returned ", created.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " created secret(s).")); foreach (InfisicalSecret secret in created) { WriteObject(secret); } } diff --git a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSecretCmdlet.cs index e02d6ab..bccec64 100644 --- a/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSecretCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/RemoveInfisicalSecretCmdlet.cs @@ -47,6 +47,7 @@ namespace PSInfisicalAPI.Cmdlets SecretNames = SecretNames }; + Logger.Information("Remove-InfisicalSecret", string.Concat("Bulk-removing ", SecretNames.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s).")); client.DeleteBatch(connection, bulk); if (PassThru.IsPresent) diff --git a/src/PSInfisicalAPI/Cmdlets/StartInfisicalProcessCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/StartInfisicalProcessCmdlet.cs index 393eea2..2165517 100644 --- a/src/PSInfisicalAPI/Cmdlets/StartInfisicalProcessCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/StartInfisicalProcessCmdlet.cs @@ -121,6 +121,9 @@ namespace PSInfisicalAPI.Cmdlets if (!ShouldProcess(target, "Start process with Infisical secrets")) { return; } + int envVarCount = EnvironmentVariables != null ? EnvironmentVariables.Count : 0; + Logger.Information("Start-InfisicalProcess", string.Concat("Injecting ", _secretBuffer.Count.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s) and ", envVarCount.ToString(System.Globalization.CultureInfo.InvariantCulture), " explicit environment variable(s) into process environment.")); + InfisicalProcessOptions options = new InfisicalProcessOptions { FilePath = FilePath, diff --git a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSecretCmdlet.cs index 30047b1..1586e10 100644 --- a/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSecretCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/UpdateInfisicalSecretCmdlet.cs @@ -56,10 +56,12 @@ namespace PSInfisicalAPI.Cmdlets Secrets = InfisicalBulkSecretConverter.ToUpdateItems(Secrets) }; + Logger.Information("Update-InfisicalSecret", string.Concat("Bulk-updating ", Secrets.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " secret(s).")); InfisicalSecretsClient bulkClient = new InfisicalSecretsClient(HttpClient, Logger); InfisicalSecret[] updated = bulkClient.UpdateBatch(connection, bulk); if (updated != null) { + Logger.Information("Update-InfisicalSecret", string.Concat("Server returned ", updated.Length.ToString(System.Globalization.CultureInfo.InvariantCulture), " updated secret(s).")); foreach (InfisicalSecret secret in updated) { WriteObject(secret); } } diff --git a/src/PSInfisicalAPI/Process/InfisicalProcessRunnerHelpers.cs b/src/PSInfisicalAPI/Process/InfisicalProcessRunnerHelpers.cs index 2554a2b..be86553 100644 --- a/src/PSInfisicalAPI/Process/InfisicalProcessRunnerHelpers.cs +++ b/src/PSInfisicalAPI/Process/InfisicalProcessRunnerHelpers.cs @@ -24,7 +24,7 @@ namespace PSInfisicalAPI.Process if (options.EnvironmentVariables != null && options.EnvironmentVariables.Count > 0) { - Log(logger, string.Concat("Injecting ", options.EnvironmentVariables.Count, " explicit environment variable(s) into the process.")); + LogInformation(logger, string.Concat("Injecting ", options.EnvironmentVariables.Count, " explicit environment variable(s) into the process.")); foreach (DictionaryEntry entry in options.EnvironmentVariables) { if (entry.Key == null) { continue; } @@ -36,7 +36,7 @@ namespace PSInfisicalAPI.Process if (options.Secrets == null || options.Secrets.Length == 0) { return; } - Log(logger, string.Concat("Injecting ", options.Secrets.Length, " Infisical secret(s) into the process environment.")); + LogInformation(logger, string.Concat("Injecting ", options.Secrets.Length, " Infisical secret(s) into the process environment.")); foreach (InfisicalSecret secret in options.Secrets) { if (secret == null || string.IsNullOrEmpty(secret.SecretName) || secret.SecretValue == null) { continue; } @@ -193,5 +193,10 @@ namespace PSInfisicalAPI.Process { if (logger != null) { logger.Verbose(Component, message); } } + + private static void LogInformation(IInfisicalLogger logger, string message) + { + if (logger != null) { logger.Information(Component, message); } + } } } diff --git a/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs b/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs index f13bc53..3521c37 100644 --- a/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs +++ b/src/PSInfisicalAPI/Secrets/InfisicalSecretsClient.cs @@ -75,8 +75,8 @@ namespace PSInfisicalAPI.Secrets InfisicalSecretListResponseDto dto = _serializer.Deserialize(response.Body); response.Clear(); - InfisicalSecret[] mapped = InfisicalSecretMapper.MapMany(dto != null ? dto.Secrets : null); - _logger.Information(Component, "Infisical secrets retrieval was successful."); + InfisicalSecret[] mapped = MergeListAndImports(dto); + _logger.Information(Component, string.Concat("Infisical secrets retrieval was successful. Returned ", mapped.Length.ToString(CultureInfo.InvariantCulture), " secret(s).")); return mapped; } catch (Exception) @@ -465,6 +465,66 @@ namespace PSInfisicalAPI.Secrets } } + private InfisicalSecret[] MergeListAndImports(InfisicalSecretListResponseDto dto) + { + if (dto == null) { return Array.Empty(); } + + InfisicalSecret[] local = InfisicalSecretMapper.MapMany(dto.Secrets); + + if (dto.Imports == null || dto.Imports.Count == 0) + { + return local; + } + + Dictionary merged = new Dictionary(StringComparer.Ordinal); + int importsTotal = 0; + + foreach (InfisicalSecretImportDto import in dto.Imports) + { + if (import == null) { continue; } + InfisicalSecret[] importedSecrets = InfisicalSecretMapper.MapMany(import.Secrets); + importsTotal += importedSecrets.Length; + + _logger.Information(Component, string.Concat( + "Including ", + importedSecrets.Length.ToString(CultureInfo.InvariantCulture), + " secret(s) from import '", + import.SecretPath ?? string.Empty, + "' (environment='", + import.Environment ?? string.Empty, + "').")); + + foreach (InfisicalSecret secret in importedSecrets) + { + if (secret == null || string.IsNullOrEmpty(secret.SecretName)) { continue; } + merged[secret.SecretName] = secret; + } + } + + int overrides = 0; + foreach (InfisicalSecret secret in local) + { + if (secret == null || string.IsNullOrEmpty(secret.SecretName)) { continue; } + if (merged.ContainsKey(secret.SecretName)) { overrides++; } + merged[secret.SecretName] = secret; + } + + _logger.Information(Component, string.Concat( + "Merged secrets: local=", + local.Length.ToString(CultureInfo.InvariantCulture), + ", imports=", + importsTotal.ToString(CultureInfo.InvariantCulture), + ", local-overrode-import=", + overrides.ToString(CultureInfo.InvariantCulture), + ", final=", + merged.Count.ToString(CultureInfo.InvariantCulture), + ".")); + + InfisicalSecret[] result = new InfisicalSecret[merged.Count]; + merged.Values.CopyTo(result, 0); + return result; + } + private InfisicalHttpResponse SendWithVersionFallback( InfisicalConnection connection, string endpointName,