diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dde243..46a3eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,37 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loos ## Unreleased +## 2026.06.07.1435 + +- Build produced from commit 97193d46f2ff. + +## Unreleased (carried forward) + +## 2026.06.07.1426 + +- Build produced from commit b5575222eb36. + +## Unreleased (carried forward) + +- Added `-ForcePrefix` switch to `ConvertTo-InfisicalSecretDictionary`, `Import-InfisicalSecret`, `Export-InfisicalSecrets`, and `Start-InfisicalProcess`. When `-Prefix` is supplied, names that already start with the prefix (case-insensitive) are now left as-is to prevent double-prefixing (e.g. `MYAPP_API_KEY` stays `MYAPP_API_KEY` when `-Prefix 'MYAPP_'` is supplied). Pass `-ForcePrefix` to restore unconditional prepending. Centralized in a new `PSInfisicalAPI.Common.InfisicalPrefix.Apply` helper. + +## 2026.06.07.1421 + +- Build produced from commit b5575222eb36. + +## Unreleased (carried forward) + +## 2026.06.07.1350 + +- Build produced from commit 1aa51b8cbf9c. + +## Unreleased (carried forward) + ## 2026.06.07.0017 - Build produced from commit 77cb03ec9845. -## Unreleased (carried forward) +## Unreleased (carried forward) - Added Organization CRUD cmdlets: `Get-InfisicalOrganization`, `New-InfisicalOrganization`, `Update-InfisicalOrganization`, `Remove-InfisicalOrganization`. `Get` lists every organization the active session can see (List parameter set, default) and returns a single record when `-OrganizationId` is supplied (Single parameter set). `New`/`Update`/`Remove` honor `-WhatIf`/`-Confirm`; `Remove` defaults to High `ConfirmImpact` and supports `-PassThru`. No project context required. Backed by new `InfisicalOrganization` model, DTO, mapper, and client wired into `InfisicalEndpointRegistry` (`ListOrganizations`, `RetrieveOrganization`, `CreateOrganization`, `UpdateOrganization`, `DeleteOrganization`). - Added Sub-Organization CRUD cmdlets: `Get-InfisicalSubOrganization`, `New-InfisicalSubOrganization`, `Update-InfisicalSubOrganization`, `Remove-InfisicalSubOrganization`, targeting the `/api/v1/sub-organizations` Beta endpoints. `Get` lists by default and accepts optional `-Limit`, `-Offset`, `-Search`, `-OrderBy`, `-OrderDirection`, and `-IsAccessible` query parameters; supplying `-SubOrganizationId` returns a single record. `New`/`Update`/`Remove` honor `-WhatIf`/`-Confirm`; `Remove` defaults to High `ConfirmImpact` and supports `-PassThru`. No project context required. Backed by new `InfisicalSubOrganization` model, DTO, mapper, and client wired into `InfisicalEndpointRegistry` (`ListSubOrganizations`, `RetrieveSubOrganization`, `CreateSubOrganization`, `UpdateSubOrganization`, `DeleteSubOrganization`). diff --git a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 index e8b4746..92f4aaa 100644 --- a/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 +++ b/Module/PSInfisicalAPI/PSInfisicalAPI.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'PSInfisicalAPI.psm1' - ModuleVersion = '2026.06.07.0017' + ModuleVersion = '2026.06.07.1435' GUID = 'b8a2f3d4-7c51-4d2f-9e6a-1f0c8b3d4e51' Author = 'Grace Solutions' CompanyName = 'Grace Solutions' @@ -19,6 +19,7 @@ 'Copy-InfisicalSecret', 'ConvertTo-InfisicalSecretDictionary', 'Export-InfisicalSecrets', + 'Import-InfisicalSecret', 'Get-InfisicalProject', 'New-InfisicalProject', 'Update-InfisicalProject', @@ -60,6 +61,7 @@ 'Export-InfisicalScepMdmProfile', 'Write-InfisicalScepMdmProfileToWmi', 'Start-InfisicalProcess', + 'Get-InfisicalEnvironmentVariable', 'Get-InfisicalSANList' ) AliasesToExport = @() @@ -72,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 = '77cb03ec9845' + CommitHash = '97193d46f2ff' } } } \ No newline at end of file diff --git a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll index 9376fcb..f6b3142 100644 Binary files a/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll and b/Module/PSInfisicalAPI/bin/PSInfisicalAPI.dll differ diff --git a/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml index 24f3026..cb37781 100644 --- a/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml +++ b/Module/PSInfisicalAPI/bin/en-US/PSInfisicalAPI.dll-Help.xml @@ -295,7 +295,7 @@ $CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters 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). + 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). -Prefix prepends a string to every dictionary key (e.g. SecretName 'API_KEY' with -Prefix 'MYAPP_' becomes key 'MYAPP_API_KEY'); the underlying InfisicalSecret objects are not mutated. A SecretName that already starts with -Prefix (case-insensitive) is left as-is to avoid double-prefixing; pass -ForcePrefix to always prepend. Notes @@ -322,6 +322,11 @@ $ConvertToInfisicalSecretDictionaryParameters.Verbose = $True $ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions. + + EXAMPLE 3 + Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | ConvertTo-InfisicalSecretDictionary -Prefix 'MYAPP_' -AsPlainText + Builds a plain-text dictionary whose keys are namespaced with 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY); the source InfisicalSecret objects are unchanged. + @@ -333,7 +338,7 @@ $ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary 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. -Prefix prepends a string to every emitted variable name regardless of format. + 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. -Prefix prepends a string to every emitted variable name regardless of format; names that already start with -Prefix (case-insensitive) are left as-is to avoid double-prefixing. Pass -ForcePrefix to always prepend. Notes @@ -369,6 +374,44 @@ $ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsP + + + Import-InfisicalSecret + Reads a previously exported secrets file (Json, Yaml, Env, or Xml) back into a name-keyed Dictionary. + Import + InfisicalSecret + + + Loads the file at -Path (which must exist) using the parser matching -Format and returns a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText for a plain string dictionary. -Prefix prepends to every emitted key; keys already starting with -Prefix (case-insensitive) are left as-is to avoid double-prefixing, and -ForcePrefix overrides that gate. -DuplicateKeyBehavior controls collision handling (Error, FirstWins, LastWins). The EnvironmentVariables format is intentionally not supported here; use [Environment]::GetEnvironmentVariable or Get-InfisicalEnvironmentVariable for environment-backed values. + + + Notes + + Importers expect the same schema that Export-InfisicalSecrets produces (Json/Yaml = array or 'Secrets' root list of {SecretName, SecretValue}; Xml = <Secrets><Secret><SecretName/><SecretValue/>; Env = KEY=VALUE per line, '#' comments allowed). JSON and YAML additionally accept a flat key/value object as a convenience. + + + + + EXAMPLE 1 + $Secrets = Import-InfisicalSecret -Path '.\secrets.json' -Format Json + Reads a JSON export back into a Dictionary<string, SecureString> keyed by the original SecretName values. + + + EXAMPLE 2 + $ImportInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ImportInfisicalSecretParameters.Path = [System.IO.FileInfo]'.\secrets.env' +$ImportInfisicalSecretParameters.Format = 'Env' +$ImportInfisicalSecretParameters.Prefix = 'MYAPP_' +$ImportInfisicalSecretParameters.DuplicateKeyBehavior = 'LastWins' +$ImportInfisicalSecretParameters.AsPlainText = $True + +$ImportInfisicalSecretResult = Import-InfisicalSecret @ImportInfisicalSecretParameters + Loads a .env file into a plain-text dictionary, namespacing every key with 'MYAPP_' and letting the last occurrence win on duplicates. + + + + + Get-InfisicalProject @@ -1662,7 +1705,7 @@ $WriteInfisicalScepMdmProfileToWmiResult = Write-InfisicalScepMdmProfileToWmi @W InfisicalProcess - Launches the executable specified by -FilePath, captures stdout/stderr, validates the exit code against -AcceptableExitCodeList, and optionally parses output with -ParsingExpression. InfisicalSecret objects supplied via -Secret (pipeline or by name) are decrypted into the ProcessStartInfo.Environment dictionary only, never written to the user or machine scope; -Prefix prepends a string to each injected variable name. -EnvironmentVariables adds additional non-secret values. -ExecutionTimeout, -NoWait, -CreateNoWindow, -WindowStyle, -Priority, -StandardInputObjectList, -SecureArgumentList, -LogOutput, and -ContinueOnError mirror the semantics of the upstream Start-ProcessWithOutput helper. Honors -WhatIf and -Confirm. + Launches the executable specified by -FilePath, captures stdout/stderr, validates the exit code against -AcceptableExitCodeList, and optionally parses output with -ParsingExpression. InfisicalSecret objects supplied via -Secret (pipeline or by name) are decrypted into the ProcessStartInfo.Environment dictionary only, never written to the user or machine scope; -Prefix prepends a string to each injected variable name, skipping names that already start with -Prefix (case-insensitive) unless -ForcePrefix is supplied. -EnvironmentVariables adds additional non-secret values. -ExecutionTimeout, -NoWait, -CreateNoWindow, -WindowStyle, -Priority, -StandardInputObjectList, -SecureArgumentList, -LogOutput, and -ContinueOnError mirror the semantics of the upstream Start-ProcessWithOutput helper. Honors -WhatIf and -Confirm. Notes @@ -1701,6 +1744,37 @@ $StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessPara + + + Get-InfisicalEnvironmentVariable + Reads an environment variable from the first scope that has a non-empty value (Process > User > Machine). + Get + InfisicalEnvironmentVariable + + + Returns the value of -Name from the first scope that contains a non-empty value, checking Process, then User, then Machine in that order. Emits nothing when the variable is missing or blank in every scope, so an assignment yields $null without writing errors or warnings. Platform-unsupported scopes (User and Machine on non-Windows) are silently skipped. + + + Notes + + Designed for the same discovery semantics the rest of PSInfisicalAPI uses when resolving connection inputs from the environment. Pipe-friendly: accepts -Name from the pipeline by value and by property name. + + + + + EXAMPLE 1 + $Value = Get-InfisicalEnvironmentVariable -Name 'INFISICAL_CLIENT_ID' + Returns the first non-empty value of INFISICAL_CLIENT_ID across Process, User, and Machine scopes; assigns $null when the variable is unset everywhere. + + + EXAMPLE 2 + @('INFISICAL_BASE_URI','INFISICAL_PROJECT_ID') | Get-InfisicalEnvironmentVariable + Pipes a list of variable names through the cmdlet and emits one value per name that is set in any scope. + + + + + Get-InfisicalOrganization diff --git a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml index 24f3026..cb37781 100644 --- a/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml +++ b/Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml @@ -295,7 +295,7 @@ $CopyInfisicalSecretResult = Copy-InfisicalSecret @CopyInfisicalSecretParameters 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). + 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). -Prefix prepends a string to every dictionary key (e.g. SecretName 'API_KEY' with -Prefix 'MYAPP_' becomes key 'MYAPP_API_KEY'); the underlying InfisicalSecret objects are not mutated. A SecretName that already starts with -Prefix (case-insensitive) is left as-is to avoid double-prefixing; pass -ForcePrefix to always prepend. Notes @@ -322,6 +322,11 @@ $ConvertToInfisicalSecretDictionaryParameters.Verbose = $True $ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary @ConvertToInfisicalSecretDictionaryParameters Aggregates recursive secret results into a plain-text dictionary, with the last value winning on key collisions. + + EXAMPLE 3 + Get-InfisicalSecret -ProjectId $ProjectId -Environment 'dev' | ConvertTo-InfisicalSecretDictionary -Prefix 'MYAPP_' -AsPlainText + Builds a plain-text dictionary whose keys are namespaced with 'MYAPP_' (e.g. API_KEY becomes MYAPP_API_KEY); the source InfisicalSecret objects are unchanged. + @@ -333,7 +338,7 @@ $ConvertToInfisicalSecretDictionaryResult = ConvertTo-InfisicalSecretDictionary 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. -Prefix prepends a string to every emitted variable name regardless of format. + 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. -Prefix prepends a string to every emitted variable name regardless of format; names that already start with -Prefix (case-insensitive) are left as-is to avoid double-prefixing. Pass -ForcePrefix to always prepend. Notes @@ -369,6 +374,44 @@ $ExportInfisicalSecretsResult = Export-InfisicalSecrets @ExportInfisicalSecretsP + + + Import-InfisicalSecret + Reads a previously exported secrets file (Json, Yaml, Env, or Xml) back into a name-keyed Dictionary. + Import + InfisicalSecret + + + Loads the file at -Path (which must exist) using the parser matching -Format and returns a case-insensitive Dictionary keyed by SecretName. By default values are SecureString; pass -AsPlainText for a plain string dictionary. -Prefix prepends to every emitted key; keys already starting with -Prefix (case-insensitive) are left as-is to avoid double-prefixing, and -ForcePrefix overrides that gate. -DuplicateKeyBehavior controls collision handling (Error, FirstWins, LastWins). The EnvironmentVariables format is intentionally not supported here; use [Environment]::GetEnvironmentVariable or Get-InfisicalEnvironmentVariable for environment-backed values. + + + Notes + + Importers expect the same schema that Export-InfisicalSecrets produces (Json/Yaml = array or 'Secrets' root list of {SecretName, SecretValue}; Xml = <Secrets><Secret><SecretName/><SecretValue/>; Env = KEY=VALUE per line, '#' comments allowed). JSON and YAML additionally accept a flat key/value object as a convenience. + + + + + EXAMPLE 1 + $Secrets = Import-InfisicalSecret -Path '.\secrets.json' -Format Json + Reads a JSON export back into a Dictionary<string, SecureString> keyed by the original SecretName values. + + + EXAMPLE 2 + $ImportInfisicalSecretParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase) +$ImportInfisicalSecretParameters.Path = [System.IO.FileInfo]'.\secrets.env' +$ImportInfisicalSecretParameters.Format = 'Env' +$ImportInfisicalSecretParameters.Prefix = 'MYAPP_' +$ImportInfisicalSecretParameters.DuplicateKeyBehavior = 'LastWins' +$ImportInfisicalSecretParameters.AsPlainText = $True + +$ImportInfisicalSecretResult = Import-InfisicalSecret @ImportInfisicalSecretParameters + Loads a .env file into a plain-text dictionary, namespacing every key with 'MYAPP_' and letting the last occurrence win on duplicates. + + + + + Get-InfisicalProject @@ -1662,7 +1705,7 @@ $WriteInfisicalScepMdmProfileToWmiResult = Write-InfisicalScepMdmProfileToWmi @W InfisicalProcess - Launches the executable specified by -FilePath, captures stdout/stderr, validates the exit code against -AcceptableExitCodeList, and optionally parses output with -ParsingExpression. InfisicalSecret objects supplied via -Secret (pipeline or by name) are decrypted into the ProcessStartInfo.Environment dictionary only, never written to the user or machine scope; -Prefix prepends a string to each injected variable name. -EnvironmentVariables adds additional non-secret values. -ExecutionTimeout, -NoWait, -CreateNoWindow, -WindowStyle, -Priority, -StandardInputObjectList, -SecureArgumentList, -LogOutput, and -ContinueOnError mirror the semantics of the upstream Start-ProcessWithOutput helper. Honors -WhatIf and -Confirm. + Launches the executable specified by -FilePath, captures stdout/stderr, validates the exit code against -AcceptableExitCodeList, and optionally parses output with -ParsingExpression. InfisicalSecret objects supplied via -Secret (pipeline or by name) are decrypted into the ProcessStartInfo.Environment dictionary only, never written to the user or machine scope; -Prefix prepends a string to each injected variable name, skipping names that already start with -Prefix (case-insensitive) unless -ForcePrefix is supplied. -EnvironmentVariables adds additional non-secret values. -ExecutionTimeout, -NoWait, -CreateNoWindow, -WindowStyle, -Priority, -StandardInputObjectList, -SecureArgumentList, -LogOutput, and -ContinueOnError mirror the semantics of the upstream Start-ProcessWithOutput helper. Honors -WhatIf and -Confirm. Notes @@ -1701,6 +1744,37 @@ $StartInfisicalProcessResult = Start-InfisicalProcess @StartInfisicalProcessPara + + + Get-InfisicalEnvironmentVariable + Reads an environment variable from the first scope that has a non-empty value (Process > User > Machine). + Get + InfisicalEnvironmentVariable + + + Returns the value of -Name from the first scope that contains a non-empty value, checking Process, then User, then Machine in that order. Emits nothing when the variable is missing or blank in every scope, so an assignment yields $null without writing errors or warnings. Platform-unsupported scopes (User and Machine on non-Windows) are silently skipped. + + + Notes + + Designed for the same discovery semantics the rest of PSInfisicalAPI uses when resolving connection inputs from the environment. Pipe-friendly: accepts -Name from the pipeline by value and by property name. + + + + + EXAMPLE 1 + $Value = Get-InfisicalEnvironmentVariable -Name 'INFISICAL_CLIENT_ID' + Returns the first non-empty value of INFISICAL_CLIENT_ID across Process, User, and Machine scopes; assigns $null when the variable is unset everywhere. + + + EXAMPLE 2 + @('INFISICAL_BASE_URI','INFISICAL_PROJECT_ID') | Get-InfisicalEnvironmentVariable + Pipes a list of variable names through the cmdlet and emits one value per name that is set in any scope. + + + + + Get-InfisicalOrganization diff --git a/build.ps1 b/build.ps1 index 66299a1..d0b2edb 100644 --- a/build.ps1 +++ b/build.ps1 @@ -113,6 +113,7 @@ function Write-Manifest { 'Copy-InfisicalSecret', 'ConvertTo-InfisicalSecretDictionary', 'Export-InfisicalSecrets', + 'Import-InfisicalSecret', 'Get-InfisicalProject', 'New-InfisicalProject', 'Update-InfisicalProject', @@ -154,6 +155,7 @@ function Write-Manifest { 'Export-InfisicalScepMdmProfile', 'Write-InfisicalScepMdmProfileToWmi', 'Start-InfisicalProcess', + 'Get-InfisicalEnvironmentVariable', 'Get-InfisicalSANList' ) AliasesToExport = @() @@ -219,7 +221,7 @@ if (`$cmds.Count -eq 0) { throw "No cmdlets were exported by the PSInfisicalAPI module." } -`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalOrganization','New-InfisicalOrganization','Update-InfisicalOrganization','Remove-InfisicalOrganization','Get-InfisicalSubOrganization','New-InfisicalSubOrganization','Update-InfisicalSubOrganization','Remove-InfisicalSubOrganization','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess','Get-InfisicalSANList') +`$expectedCmds = @('Connect-Infisical','Disconnect-Infisical','Get-InfisicalSecret','New-InfisicalSecret','Update-InfisicalSecret','Remove-InfisicalSecret','Copy-InfisicalSecret','ConvertTo-InfisicalSecretDictionary','Export-InfisicalSecrets','Import-InfisicalSecret','Get-InfisicalProject','New-InfisicalProject','Update-InfisicalProject','Remove-InfisicalProject','Get-InfisicalEnvironment','New-InfisicalEnvironment','Update-InfisicalEnvironment','Remove-InfisicalEnvironment','Get-InfisicalFolder','New-InfisicalFolder','Update-InfisicalFolder','Remove-InfisicalFolder','Get-InfisicalTag','New-InfisicalTag','Update-InfisicalTag','Remove-InfisicalTag','Get-InfisicalOrganization','New-InfisicalOrganization','Update-InfisicalOrganization','Remove-InfisicalOrganization','Get-InfisicalSubOrganization','New-InfisicalSubOrganization','Update-InfisicalSubOrganization','Remove-InfisicalSubOrganization','Get-InfisicalCertificateAuthority','Get-InfisicalPkiSubscriber','Get-InfisicalCertificateProfile','Get-InfisicalCertificatePolicy','Get-InfisicalCertificate','Request-InfisicalCertificate','ConvertTo-InfisicalCertificate','Install-InfisicalCertificate','Uninstall-InfisicalCertificate','Export-InfisicalCertificate','Get-InfisicalCertificateApplication','Get-InfisicalCertificateApplicationEnrollment','New-InfisicalScepDynamicChallenge','Get-InfisicalScepMdmProfile','Export-InfisicalScepMdmProfile','Write-InfisicalScepMdmProfileToWmi','Start-InfisicalProcess','Get-InfisicalEnvironmentVariable','Get-InfisicalSANList') foreach (`$expected in `$expectedCmds) { if (-not (Get-Command -Name `$expected -Module PSInfisicalAPI -ErrorAction SilentlyContinue)) { throw "Cmdlet not found: `$expected" diff --git a/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs index 25925f4..68768fb 100644 --- a/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/ConvertToInfisicalSecretDictionaryCmdlet.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Management.Automation; using System.Security; +using PSInfisicalAPI.Common; using PSInfisicalAPI.Errors; using PSInfisicalAPI.Models; @@ -21,6 +22,12 @@ namespace PSInfisicalAPI.Cmdlets [Parameter] public SwitchParameter AsPlainText { get; set; } + [Parameter] + public string Prefix { get; set; } + + [Parameter] + public SwitchParameter ForcePrefix { get; set; } + private readonly List _buffer = new List(); protected override void ProcessRecord() @@ -63,7 +70,7 @@ namespace PSInfisicalAPI.Cmdlets foreach (InfisicalSecret secret in _buffer) { - string key = secret.SecretName ?? string.Empty; + string key = InfisicalPrefix.Apply(secret.SecretName ?? string.Empty, Prefix, ForcePrefix.IsPresent); if (dictionary.ContainsKey(key)) { diff --git a/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs index 59bb694..21860dd 100644 --- a/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/ExportInfisicalSecretsCmdlet.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Management.Automation; using System.Text; +using PSInfisicalAPI.Common; using PSInfisicalAPI.Errors; using PSInfisicalAPI.Exports; using PSInfisicalAPI.Models; @@ -40,6 +41,9 @@ namespace PSInfisicalAPI.Cmdlets [Parameter] public string Prefix { get; set; } + [Parameter] + public SwitchParameter ForcePrefix { get; set; } + private readonly List _buffer = new List(); protected override void ProcessRecord() @@ -71,7 +75,7 @@ namespace PSInfisicalAPI.Cmdlets InfisicalExportRequest request = new InfisicalExportRequest { - Secrets = ApplyPrefix(_buffer, Prefix), + Secrets = ApplyPrefix(_buffer, Prefix, ForcePrefix.IsPresent), Format = Format, Path = Path, Scope = Scope, @@ -88,7 +92,7 @@ namespace PSInfisicalAPI.Cmdlets } } - private static InfisicalSecret[] ApplyPrefix(List source, string prefix) + private static InfisicalSecret[] ApplyPrefix(List source, string prefix, bool force) { if (string.IsNullOrEmpty(prefix)) { return source.ToArray(); } @@ -104,7 +108,7 @@ namespace PSInfisicalAPI.Cmdlets Environment = original.Environment, Version = original.Version, Type = original.Type, - SecretName = string.Concat(prefix, original.SecretName), + SecretName = InfisicalPrefix.Apply(original.SecretName, prefix, force), SecretValue = original.SecretValue, SecretValueHidden = original.SecretValueHidden, SecretPath = original.SecretPath, diff --git a/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentVariableCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentVariableCmdlet.cs new file mode 100644 index 0000000..472c423 --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/GetInfisicalEnvironmentVariableCmdlet.cs @@ -0,0 +1,43 @@ +using System; +using System.Management.Automation; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsCommon.Get, "InfisicalEnvironmentVariable")] + [OutputType(typeof(string))] + public sealed class GetInfisicalEnvironmentVariableCmdlet : PSCmdlet + { + private static readonly EnvironmentVariableTarget[] TargetOrder = new[] + { + EnvironmentVariableTarget.Process, + EnvironmentVariableTarget.User, + EnvironmentVariableTarget.Machine + }; + + [Parameter(Mandatory = true, Position = 0, ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)] + [ValidateNotNullOrEmpty] + public string Name { get; set; } + + protected override void ProcessRecord() + { + foreach (EnvironmentVariableTarget target in TargetOrder) + { + string value; + try + { + value = Environment.GetEnvironmentVariable(Name, target); + } + catch + { + continue; + } + + if (!string.IsNullOrEmpty(value)) + { + WriteObject(value); + return; + } + } + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/ImportInfisicalSecretCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ImportInfisicalSecretCmdlet.cs new file mode 100644 index 0000000..87a63ab --- /dev/null +++ b/src/PSInfisicalAPI/Cmdlets/ImportInfisicalSecretCmdlet.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Security; +using PSInfisicalAPI.Common; +using PSInfisicalAPI.Errors; +using PSInfisicalAPI.Imports; +using PSInfisicalAPI.Models; +using PSInfisicalAPI.Security; + +namespace PSInfisicalAPI.Cmdlets +{ + [Cmdlet(VerbsData.Import, "InfisicalSecret")] + [OutputType(typeof(Dictionary))] + [OutputType(typeof(Dictionary))] + public sealed class ImportInfisicalSecretCmdlet : InfisicalCmdletBase + { + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNull] + public FileInfo Path { get; set; } + + [Parameter(Mandatory = true)] + public InfisicalImportFormat Format { get; set; } + + [Parameter] + public InfisicalDuplicateKeyBehavior DuplicateKeyBehavior { get; set; } = InfisicalDuplicateKeyBehavior.Error; + + [Parameter] + public SwitchParameter AsPlainText { get; set; } + + [Parameter] + public string Prefix { get; set; } + + [Parameter] + public SwitchParameter ForcePrefix { get; set; } + + protected override void EndProcessing() + { + try + { + Path.Refresh(); + if (!Path.Exists) + { + throw new InfisicalImportException(string.Concat("Import path does not exist: ", Path.FullName)); + } + + IInfisicalImporter importer = InfisicalImporterFactory.Create(Format); + IList> pairs = importer.Import(Path); + + if (AsPlainText.IsPresent) + { + Dictionary plain = BuildDictionary(pairs, value => value ?? string.Empty); + WriteObject(plain); + } + else + { + Dictionary secure = BuildDictionary(pairs, value => SecureStringUtility.ToReadOnlySecureString(value ?? string.Empty)); + WriteObject(secure); + } + } + catch (Exception exception) + { + ThrowTerminatingForException("ImportInfisicalSecretCmdlet", "ImportSecret", exception); + } + } + + private Dictionary BuildDictionary( + IList> pairs, + Func valueSelector) + { + Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair pair in pairs) + { + if (pair.Key == null) { continue; } + string key = InfisicalPrefix.Apply(pair.Key, Prefix, ForcePrefix.IsPresent); + + if (dictionary.ContainsKey(key)) + { + if (DuplicateKeyBehavior == InfisicalDuplicateKeyBehavior.Error) + { + throw new InfisicalConfigurationException(string.Concat("Duplicate secret name encountered: ", key)); + } + + if (DuplicateKeyBehavior == InfisicalDuplicateKeyBehavior.LastWins) + { + dictionary[key] = valueSelector(pair.Value); + } + + continue; + } + + dictionary[key] = valueSelector(pair.Value); + } + + return dictionary; + } + } +} diff --git a/src/PSInfisicalAPI/Cmdlets/StartInfisicalProcessCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/StartInfisicalProcessCmdlet.cs index c129a22..2cd5c18 100644 --- a/src/PSInfisicalAPI/Cmdlets/StartInfisicalProcessCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/StartInfisicalProcessCmdlet.cs @@ -95,6 +95,9 @@ namespace PSInfisicalAPI.Cmdlets [Parameter] public string Prefix { get; set; } + [Parameter] + public SwitchParameter ForcePrefix { get; set; } + private readonly List _secretBuffer = new List(); protected override void ProcessRecord() @@ -135,7 +138,8 @@ namespace PSInfisicalAPI.Cmdlets LogOutput = LogOutput.IsPresent, ContinueOnError = ContinueOnError.IsPresent, Secrets = _secretBuffer.ToArray(), - Prefix = Prefix + Prefix = Prefix, + ForcePrefix = ForcePrefix.IsPresent }; InfisicalProcessResult result = InfisicalProcessRunner.Run(options, Logger); diff --git a/src/PSInfisicalAPI/Common/InfisicalPrefix.cs b/src/PSInfisicalAPI/Common/InfisicalPrefix.cs new file mode 100644 index 0000000..d00e208 --- /dev/null +++ b/src/PSInfisicalAPI/Common/InfisicalPrefix.cs @@ -0,0 +1,15 @@ +using System; + +namespace PSInfisicalAPI.Common +{ + public static class InfisicalPrefix + { + public static string Apply(string original, string prefix, bool force) + { + if (string.IsNullOrEmpty(prefix)) { return original ?? string.Empty; } + if (original == null) { return prefix; } + if (!force && original.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { return original; } + return string.Concat(prefix, original); + } + } +} diff --git a/src/PSInfisicalAPI/Errors/InfisicalException.cs b/src/PSInfisicalAPI/Errors/InfisicalException.cs index 98f2c1f..bd29745 100644 --- a/src/PSInfisicalAPI/Errors/InfisicalException.cs +++ b/src/PSInfisicalAPI/Errors/InfisicalException.cs @@ -75,6 +75,13 @@ namespace PSInfisicalAPI.Errors public InfisicalExportException(string message, Exception innerException) : base(message, innerException) { } } + public class InfisicalImportException : InfisicalException + { + public InfisicalImportException() { } + public InfisicalImportException(string message) : base(message) { } + public InfisicalImportException(string message, Exception innerException) : base(message, innerException) { } + } + public class InfisicalConfigurationException : InfisicalException { public InfisicalConfigurationException() { } diff --git a/src/PSInfisicalAPI/Imports/EnvInfisicalImporter.cs b/src/PSInfisicalAPI/Imports/EnvInfisicalImporter.cs new file mode 100644 index 0000000..54f9c02 --- /dev/null +++ b/src/PSInfisicalAPI/Imports/EnvInfisicalImporter.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.IO; +using PSInfisicalAPI.Errors; + +namespace PSInfisicalAPI.Imports +{ + public sealed class EnvInfisicalImporter : IInfisicalImporter + { + public IList> Import(FileInfo path) + { + if (path == null) { throw new InfisicalImportException("Path is required for ENV import."); } + + List> result = new List>(); + string[] lines = File.ReadAllLines(path.FullName); + + foreach (string raw in lines) + { + if (raw == null) { continue; } + string line = raw.Trim(); + if (line.Length == 0) { continue; } + if (line[0] == '#') { continue; } + + int idx = line.IndexOf('='); + if (idx <= 0) { continue; } + + string key = line.Substring(0, idx).Trim(); + string value = line.Substring(idx + 1); + if (key.Length == 0) { continue; } + + result.Add(new KeyValuePair(key, value)); + } + + return result; + } + } +} diff --git a/src/PSInfisicalAPI/Imports/IInfisicalImporter.cs b/src/PSInfisicalAPI/Imports/IInfisicalImporter.cs new file mode 100644 index 0000000..26d5d84 --- /dev/null +++ b/src/PSInfisicalAPI/Imports/IInfisicalImporter.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using System.IO; + +namespace PSInfisicalAPI.Imports +{ + public interface IInfisicalImporter + { + IList> Import(FileInfo path); + } +} diff --git a/src/PSInfisicalAPI/Imports/InfisicalImporterFactory.cs b/src/PSInfisicalAPI/Imports/InfisicalImporterFactory.cs new file mode 100644 index 0000000..5ace46c --- /dev/null +++ b/src/PSInfisicalAPI/Imports/InfisicalImporterFactory.cs @@ -0,0 +1,20 @@ +using PSInfisicalAPI.Errors; +using PSInfisicalAPI.Models; + +namespace PSInfisicalAPI.Imports +{ + public static class InfisicalImporterFactory + { + public static IInfisicalImporter Create(InfisicalImportFormat format) + { + switch (format) + { + case InfisicalImportFormat.Json: return new JsonInfisicalImporter(); + case InfisicalImportFormat.Yaml: return new YamlInfisicalImporter(); + case InfisicalImportFormat.Env: return new EnvInfisicalImporter(); + case InfisicalImportFormat.Xml: return new XmlInfisicalImporter(); + default: throw new InfisicalImportException(string.Concat("Unsupported import format: ", format.ToString())); + } + } + } +} diff --git a/src/PSInfisicalAPI/Imports/JsonInfisicalImporter.cs b/src/PSInfisicalAPI/Imports/JsonInfisicalImporter.cs new file mode 100644 index 0000000..c507296 --- /dev/null +++ b/src/PSInfisicalAPI/Imports/JsonInfisicalImporter.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json.Linq; +using PSInfisicalAPI.Errors; + +namespace PSInfisicalAPI.Imports +{ + public sealed class JsonInfisicalImporter : IInfisicalImporter + { + public IList> Import(FileInfo path) + { + if (path == null) { throw new InfisicalImportException("Path is required for JSON import."); } + + List> result = new List>(); + string text = File.ReadAllText(path.FullName); + JToken root = JToken.Parse(text); + + if (root.Type == JTokenType.Array) + { + foreach (JToken item in (JArray)root) + { + if (item == null || item.Type != JTokenType.Object) { continue; } + string key = ReadString((JObject)item, "SecretName") ?? ReadString((JObject)item, "secretName"); + string value = ReadString((JObject)item, "SecretValue") ?? ReadString((JObject)item, "secretValue"); + if (string.IsNullOrEmpty(key)) { continue; } + result.Add(new KeyValuePair(key, value ?? string.Empty)); + } + } + else if (root.Type == JTokenType.Object) + { + foreach (JProperty prop in ((JObject)root).Properties()) + { + if (prop == null || string.IsNullOrEmpty(prop.Name)) { continue; } + string value = prop.Value != null && prop.Value.Type != JTokenType.Null ? prop.Value.ToString() : string.Empty; + result.Add(new KeyValuePair(prop.Name, value)); + } + } + else + { + throw new InfisicalImportException("JSON import expects an array of secret objects or a flat key/value object."); + } + + return result; + } + + private static string ReadString(JObject obj, string name) + { + JToken token; + if (!obj.TryGetValue(name, out token) || token == null || token.Type == JTokenType.Null) { return null; } + return token.ToString(); + } + } +} diff --git a/src/PSInfisicalAPI/Imports/XmlInfisicalImporter.cs b/src/PSInfisicalAPI/Imports/XmlInfisicalImporter.cs new file mode 100644 index 0000000..61f768c --- /dev/null +++ b/src/PSInfisicalAPI/Imports/XmlInfisicalImporter.cs @@ -0,0 +1,51 @@ +using System.Collections.Generic; +using System.IO; +using System.Xml; +using PSInfisicalAPI.Errors; + +namespace PSInfisicalAPI.Imports +{ + public sealed class XmlInfisicalImporter : IInfisicalImporter + { + public IList> Import(FileInfo path) + { + if (path == null) { throw new InfisicalImportException("Path is required for XML import."); } + + List> result = new List>(); + XmlDocument document = new XmlDocument(); + document.Load(path.FullName); + + XmlNode root = document.DocumentElement; + if (root == null || !string.Equals(root.LocalName, "Secrets", System.StringComparison.Ordinal)) + { + throw new InfisicalImportException("XML import expects a root element."); + } + + foreach (XmlNode node in root.ChildNodes) + { + if (node == null || node.NodeType != XmlNodeType.Element) { continue; } + if (!string.Equals(node.LocalName, "Secret", System.StringComparison.Ordinal)) { continue; } + + string key = ReadChild(node, "SecretName"); + string value = ReadChild(node, "SecretValue"); + if (string.IsNullOrEmpty(key)) { continue; } + result.Add(new KeyValuePair(key, value ?? string.Empty)); + } + + return result; + } + + private static string ReadChild(XmlNode parent, string name) + { + foreach (XmlNode child in parent.ChildNodes) + { + if (child == null || child.NodeType != XmlNodeType.Element) { continue; } + if (string.Equals(child.LocalName, name, System.StringComparison.Ordinal)) + { + return child.InnerText; + } + } + return null; + } + } +} diff --git a/src/PSInfisicalAPI/Imports/YamlInfisicalImporter.cs b/src/PSInfisicalAPI/Imports/YamlInfisicalImporter.cs new file mode 100644 index 0000000..a8af0eb --- /dev/null +++ b/src/PSInfisicalAPI/Imports/YamlInfisicalImporter.cs @@ -0,0 +1,61 @@ +using System.Collections; +using System.Collections.Generic; +using System.IO; +using PSInfisicalAPI.Errors; +using YamlDotNet.Serialization; + +namespace PSInfisicalAPI.Imports +{ + public sealed class YamlInfisicalImporter : IInfisicalImporter + { + public IList> Import(FileInfo path) + { + if (path == null) { throw new InfisicalImportException("Path is required for YAML import."); } + + List> result = new List>(); + string text = File.ReadAllText(path.FullName); + + IDeserializer deserializer = new DeserializerBuilder().Build(); + object root = deserializer.Deserialize(text); + if (root == null) { return result; } + + IDictionary rootMap = root as IDictionary; + if (rootMap != null && rootMap.Contains("Secrets")) + { + IList entries = rootMap["Secrets"] as IList; + if (entries != null) + { + foreach (object entry in entries) + { + IDictionary map = entry as IDictionary; + if (map == null) { continue; } + string key = AsString(map["SecretName"]); + string value = AsString(map["SecretValue"]); + if (string.IsNullOrEmpty(key)) { continue; } + result.Add(new KeyValuePair(key, value ?? string.Empty)); + } + return result; + } + } + + if (rootMap != null) + { + foreach (DictionaryEntry kvp in rootMap) + { + string key = AsString(kvp.Key); + if (string.IsNullOrEmpty(key)) { continue; } + result.Add(new KeyValuePair(key, AsString(kvp.Value) ?? string.Empty)); + } + return result; + } + + throw new InfisicalImportException("YAML import expects a 'Secrets' root list or a flat key/value mapping."); + } + + private static string AsString(object value) + { + if (value == null) { return null; } + return value.ToString(); + } + } +} diff --git a/src/PSInfisicalAPI/Models/InfisicalImportFormat.cs b/src/PSInfisicalAPI/Models/InfisicalImportFormat.cs new file mode 100644 index 0000000..a70849c --- /dev/null +++ b/src/PSInfisicalAPI/Models/InfisicalImportFormat.cs @@ -0,0 +1,10 @@ +namespace PSInfisicalAPI.Models +{ + public enum InfisicalImportFormat + { + Json, + Yaml, + Env, + Xml + } +} diff --git a/src/PSInfisicalAPI/Process/InfisicalProcessOptions.cs b/src/PSInfisicalAPI/Process/InfisicalProcessOptions.cs index fc72ead..e081740 100644 --- a/src/PSInfisicalAPI/Process/InfisicalProcessOptions.cs +++ b/src/PSInfisicalAPI/Process/InfisicalProcessOptions.cs @@ -26,5 +26,6 @@ namespace PSInfisicalAPI.Process public bool ContinueOnError { get; set; } public InfisicalSecret[] Secrets { get; set; } public string Prefix { get; set; } + public bool ForcePrefix { get; set; } } } diff --git a/src/PSInfisicalAPI/Process/InfisicalProcessRunnerHelpers.cs b/src/PSInfisicalAPI/Process/InfisicalProcessRunnerHelpers.cs index 239f382..4326f31 100644 --- a/src/PSInfisicalAPI/Process/InfisicalProcessRunnerHelpers.cs +++ b/src/PSInfisicalAPI/Process/InfisicalProcessRunnerHelpers.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading; +using PSInfisicalAPI.Common; using PSInfisicalAPI.Logging; using PSInfisicalAPI.Models; using PSInfisicalAPI.Security; @@ -39,7 +40,7 @@ namespace PSInfisicalAPI.Process foreach (InfisicalSecret secret in options.Secrets) { if (secret == null || string.IsNullOrEmpty(secret.SecretName) || secret.SecretValue == null) { continue; } - string name = string.IsNullOrEmpty(options.Prefix) ? secret.SecretName : string.Concat(options.Prefix, secret.SecretName); + string name = InfisicalPrefix.Apply(secret.SecretName, options.Prefix, options.ForcePrefix); SecureStringUtility.UsePlainText(secret.SecretValue, plain => { processEnv[name] = plain;