feat: add Import-InfisicalSecret + Get-InfisicalEnvironmentVariable + -Prefix on ConvertTo-InfisicalSecretDictionary

This commit is contained in:
GraceSolutions
2026-06-07 10:16:20 -04:00
parent 1aa51b8cbf
commit b5575222eb
17 changed files with 556 additions and 6 deletions
@@ -21,6 +21,9 @@ namespace PSInfisicalAPI.Cmdlets
[Parameter]
public SwitchParameter AsPlainText { get; set; }
[Parameter]
public string Prefix { get; set; }
private readonly List<InfisicalSecret> _buffer = new List<InfisicalSecret>();
protected override void ProcessRecord()
@@ -60,10 +63,11 @@ namespace PSInfisicalAPI.Cmdlets
private Dictionary<string, TValue> BuildDictionary<TValue>(Func<InfisicalSecret, TValue> valueSelector)
{
Dictionary<string, TValue> dictionary = new Dictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
string prefix = Prefix ?? string.Empty;
foreach (InfisicalSecret secret in _buffer)
{
string key = secret.SecretName ?? string.Empty;
string key = string.Concat(prefix, secret.SecretName ?? string.Empty);
if (dictionary.ContainsKey(key))
{
@@ -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;
}
}
}
}
}
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Management.Automation;
using System.Security;
using PSInfisicalAPI.Errors;
using PSInfisicalAPI.Imports;
using PSInfisicalAPI.Models;
using PSInfisicalAPI.Security;
namespace PSInfisicalAPI.Cmdlets
{
[Cmdlet(VerbsData.Import, "InfisicalSecret")]
[OutputType(typeof(Dictionary<string, SecureString>))]
[OutputType(typeof(Dictionary<string, string>))]
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; }
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<KeyValuePair<string, string>> pairs = importer.Import(Path);
if (AsPlainText.IsPresent)
{
Dictionary<string, string> plain = BuildDictionary<string>(pairs, value => value ?? string.Empty);
WriteObject(plain);
}
else
{
Dictionary<string, SecureString> secure = BuildDictionary<SecureString>(pairs, value => SecureStringUtility.ToReadOnlySecureString(value ?? string.Empty));
WriteObject(secure);
}
}
catch (Exception exception)
{
ThrowTerminatingForException("ImportInfisicalSecretCmdlet", "ImportSecret", exception);
}
}
private Dictionary<string, TValue> BuildDictionary<TValue>(
IList<KeyValuePair<string, string>> pairs,
Func<string, TValue> valueSelector)
{
Dictionary<string, TValue> dictionary = new Dictionary<string, TValue>(StringComparer.OrdinalIgnoreCase);
string prefix = Prefix ?? string.Empty;
foreach (KeyValuePair<string, string> pair in pairs)
{
if (pair.Key == null) { continue; }
string key = string.Concat(prefix, pair.Key);
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;
}
}
}
@@ -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() { }
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using System.IO;
using PSInfisicalAPI.Errors;
namespace PSInfisicalAPI.Imports
{
public sealed class EnvInfisicalImporter : IInfisicalImporter
{
public IList<KeyValuePair<string, string>> Import(FileInfo path)
{
if (path == null) { throw new InfisicalImportException("Path is required for ENV import."); }
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
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<string, string>(key, value));
}
return result;
}
}
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using System.IO;
namespace PSInfisicalAPI.Imports
{
public interface IInfisicalImporter
{
IList<KeyValuePair<string, string>> Import(FileInfo path);
}
}
@@ -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()));
}
}
}
}
@@ -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<KeyValuePair<string, string>> Import(FileInfo path)
{
if (path == null) { throw new InfisicalImportException("Path is required for JSON import."); }
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
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<string, string>(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<string, string>(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();
}
}
}
@@ -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<KeyValuePair<string, string>> Import(FileInfo path)
{
if (path == null) { throw new InfisicalImportException("Path is required for XML import."); }
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
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 <Secrets> 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<string, string>(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;
}
}
}
@@ -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<KeyValuePair<string, string>> Import(FileInfo path)
{
if (path == null) { throw new InfisicalImportException("Path is required for YAML import."); }
List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
string text = File.ReadAllText(path.FullName);
IDeserializer deserializer = new DeserializerBuilder().Build();
object root = deserializer.Deserialize<object>(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<string, string>(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<string, string>(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();
}
}
}
@@ -0,0 +1,10 @@
namespace PSInfisicalAPI.Models
{
public enum InfisicalImportFormat
{
Json,
Yaml,
Env,
Xml
}
}