# PSInfisicalAPI

A C# binary PowerShell module for interacting with the [Infisical](https://infisical.com/) REST API. It provides cmdlets for authentication, secret retrieval, structured export, and includes automatic environment-variable discovery so connections can be established with little or no inline configuration.

- License: AGPL-3.0
- Author: Grace Solutions
- Target framework: .NET Standard 2.0 (compatible with Windows PowerShell 5.1 and PowerShell 7+)

## Installation

### From the PowerShell Gallery

```powershell
Install-Module -Name PSInfisicalAPI -Scope CurrentUser
Import-Module -Name PSInfisicalAPI
```

### From source

```powershell
git clone https://prod.git.gracesolution.info/gsadmin/PSInfisicalAPI.git
cd PSInfisicalAPI
pwsh -NoProfile -ExecutionPolicy Bypass -File .\build.ps1 -RunTests
Import-Module -Name .\Module\PSInfisicalAPI
```

## Cmdlets

The module exports 51 cmdlets. Discovery cmdlets (`Get-Infisical*`) use a `List` (default) / single-record parameter-set pair: invoking without the identity parameter returns the collection, supplying the identity parameter returns one record.

### Session

| Cmdlet                 | Purpose                                                                                            |
| ---------------------- | -------------------------------------------------------------------------------------------------- |
| `Connect-Infisical`    | Establishes an authenticated session with an Infisical server and stores it for use by subsequent cmdlets. |
| `Disconnect-Infisical` | Clears the current Infisical session from the module-level session manager.                        |

### Secrets

| Cmdlet                                | Purpose                                                                                            |
| ------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `Get-InfisicalSecret`                 | Lists or retrieves Infisical secrets within a project, environment, and optional folder path.      |
| `New-InfisicalSecret`                 | Creates a new Infisical secret, with support for SecureString values and bulk creation.            |
| `Update-InfisicalSecret`              | Updates an existing Infisical secret value, comment, name, or tags.                                |
| `Remove-InfisicalSecret`              | Deletes one or many Infisical secrets by name.                                                     |
| `Copy-InfisicalSecret`                | Duplicates one or more secrets into a different environment or secret path.                        |
| `ConvertTo-InfisicalSecretDictionary` | Converts a stream of InfisicalSecret objects into a name-keyed Dictionary of SecureString or plain text values. |
| `Export-InfisicalSecrets`             | Exports InfisicalSecret objects to disk or environment variables in a chosen file format.          |

### Organizations

| Cmdlet                         | Purpose                                                                                            |
| ------------------------------ | -------------------------------------------------------------------------------------------------- |
| `Get-InfisicalOrganization`    | Lists or retrieves Infisical organizations accessible to the current identity.                     |
| `New-InfisicalOrganization`    | Creates a new Infisical organization.                                                              |
| `Update-InfisicalOrganization` | Updates the name or slug of an existing Infisical organization.                                    |
| `Remove-InfisicalOrganization` | Deletes an Infisical organization.                                                                 |

### Sub-Organizations

| Cmdlet                            | Purpose                                                                                            |
| --------------------------------- | -------------------------------------------------------------------------------------------------- |
| `Get-InfisicalSubOrganization`    | Lists or retrieves Infisical sub-organizations, with optional search, paging, and ordering filters. |
| `New-InfisicalSubOrganization`    | Creates a new Infisical sub-organization.                                                          |
| `Update-InfisicalSubOrganization` | Updates the name or slug of an existing Infisical sub-organization.                                |
| `Remove-InfisicalSubOrganization` | Deletes an Infisical sub-organization.                                                             |

### Projects

| Cmdlet                    | Purpose                                                                                            |
| ------------------------- | -------------------------------------------------------------------------------------------------- |
| `Get-InfisicalProject`    | Lists or retrieves Infisical projects accessible to the current identity.                          |
| `New-InfisicalProject`    | Creates a new Infisical project in the active organization.                                        |
| `Update-InfisicalProject` | Updates the name, description, or auto-capitalization flag on an existing project.                 |
| `Remove-InfisicalProject` | Deletes an Infisical project.                                                                      |

### Environments

| Cmdlet                        | Purpose                                                                                            |
| ----------------------------- | -------------------------------------------------------------------------------------------------- |
| `Get-InfisicalEnvironment`    | Lists or retrieves Infisical environments defined on a project.                                    |
| `New-InfisicalEnvironment`    | Creates a new environment on an Infisical project.                                                 |
| `Update-InfisicalEnvironment` | Updates the name, slug, or sort order of an existing Infisical environment.                        |
| `Remove-InfisicalEnvironment` | Deletes an Infisical environment from a project.                                                   |

### Folders

| Cmdlet                   | Purpose                                                                                            |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| `Get-InfisicalFolder`    | Lists or retrieves Infisical folders at a given secret path.                                       |
| `New-InfisicalFolder`    | Creates a new Infisical folder under the supplied parent path.                                     |
| `Update-InfisicalFolder` | Renames an existing Infisical folder.                                                              |
| `Remove-InfisicalFolder` | Deletes an Infisical folder and all secrets it contains.                                           |

### Tags

| Cmdlet                | Purpose                                                                                            |
| --------------------- | -------------------------------------------------------------------------------------------------- |
| `Get-InfisicalTag`    | Lists or retrieves Infisical tags defined on a project.                                            |
| `New-InfisicalTag`    | Creates a new Infisical tag on a project.                                                          |
| `Update-InfisicalTag` | Updates the slug, name, or color of an existing Infisical tag.                                     |
| `Remove-InfisicalTag` | Deletes an Infisical tag from a project.                                                           |

### PKI

| Cmdlet                              | Purpose                                                                                            |
| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
| `Get-InfisicalCertificateAuthority` | Lists or retrieves Infisical internal Certificate Authorities.                                     |
| `Get-InfisicalPkiSubscriber`        | Lists or retrieves Infisical PKI subscribers in a project.                                         |
| `Get-InfisicalCertificate`          | Lists or retrieves Infisical certificates in a project, with optional filters and automatic paging. |
| `Request-InfisicalCertificate`      | Requests a new Infisical certificate (local CSR + sign) or reuses a still-valid existing one.      |
| `ConvertTo-InfisicalCertificate`    | Materializes an X509Certificate2 from an Infisical certificate record, bundle, or serial number.   |
| `Install-InfisicalCertificate`      | Installs an Infisical certificate (and optional chain) into a Windows certificate store.           |
| `Uninstall-InfisicalCertificate`    | Removes a certificate from a Windows certificate store by thumbprint, subject, or pipeline input.  |
| `Export-InfisicalCertificate`       | Exports an Infisical certificate to disk in PEM, PFX, or CER format.                               |
| `Get-InfisicalScepMdmProfile`       | Projects an Infisical certificate profile into a Windows SCEP MDM profile model.                   |
| `Export-InfisicalScepMdmProfile`    | Writes a SCEP MDM profile to disk as a SyncML payload suitable for MDM delivery.                   |
| `Write-InfisicalScepMdmProfileToWmi`| Submits a SCEP MDM profile to the local MDM Bridge WMI provider to trigger enrollment.             |
| `Get-InfisicalSANList`              | Builds a SAN candidate list (device name, `<device>.<suffix>` per adapter DNS suffix, RFC 1918 + CGNAT IPv4 addresses, IPv4/IPv6 loopback) for `Request-InfisicalCertificate -DnsName`. |

### Process

| Cmdlet                   | Purpose                                                                                            |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| `Start-InfisicalProcess` | Launches a child process with Infisical secrets injected directly into its environment block, capturing stdout/stderr and validating the exit code. |

Use `Get-Help <Cmdlet> -Full` for parameter details and `Get-Help about_PSInfisicalAPI` for the module overview.

## Quick start

```powershell
$secureSecret = Read-Host -AsSecureString 'Client Secret'

$connection = Connect-Infisical `
    -BaseUri        'https://app.infisical.com' `
    -OrganizationId '00000000-0000-0000-0000-000000000000' `
    -ProjectId      '11111111-1111-1111-1111-111111111111' `
    -Environment    'dev' `
    -ClientId       'machine-identity-client-id' `
    -ClientSecret   $secureSecret `
    -PassThru

Get-InfisicalSecret -SecretPath '/'
Disconnect-Infisical
```

## End-to-end: request and install a chained certificate

Connects, selects a project by name, sources SANs from `Get-InfisicalSANList`, picks the first available internal CA, requests a certificate, installs it (and its chain) into the current-user store, and disconnects. Each call uses a splatted `OrderedDictionary` constructed with `OrdinalIgnoreCase` so parameter names round-trip case-insensitively.

```powershell
$ConnectInfisicalParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
    $ConnectInfisicalParameters.BaseUri        = 'https://app.infisical.com'
    $ConnectInfisicalParameters.OrganizationId = '00000000-0000-0000-0000-000000000000'
    $ConnectInfisicalParameters.ClientId       = 'machine-identity-client-id'
    $ConnectInfisicalParameters.ClientSecret   = ConvertTo-SecureString -String 'ClientSecret' -AsPlainText -Force
    $ConnectInfisicalParameters.PassThru       = $True
    $ConnectInfisicalParameters.Verbose        = $True

$Connection = Connect-Infisical @ConnectInfisicalParameters

$Project = Get-InfisicalProject | Where-Object {($_.Name -eq 'Platform')} | Select-Object -First 1
$Ca      = Get-InfisicalCertificateAuthority -ProjectId ($Project.Id) | Select-Object -First 1
$SanList = Get-InfisicalSANList

$RequestInfisicalCertificateParameters = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
    $RequestInfisicalCertificateParameters.ProjectId              = $Project.Id
    $RequestInfisicalCertificateParameters.CertificateAuthorityId = $Ca.Id
    $RequestInfisicalCertificateParameters.CommonName             = "CN=$($Env:ComputerName.ToUpper())"
    $RequestInfisicalCertificateParameters.DnsName                = New-Object -TypeName 'System.Collections.Generic.List[System.String]'
        $RequestInfisicalCertificateParameters.DnsName.AddRange($SanList)
        $RequestInfisicalCertificateParameters.DnsName.Add('myrecord.mydomain.com')
    $RequestInfisicalCertificateParameters.Ttl                    = '90d'
    $RequestInfisicalCertificateParameters.Install                = $True
    $RequestInfisicalCertificateParameters.InstallChain           = $True
    $Certificate = Request-InfisicalCertificate @RequestInfisicalCertificateParameters

$Null = Disconnect-Infisical -Verbose
```

## Automatic environment-variable discovery

When `Connect-Infisical` is invoked with one or more parameters missing (or set to whitespace/empty), the cmdlet searches environment variables and uses the first value it finds. This makes invocation as simple as `Connect-Infisical` when variables are set up in advance.

### Scope precedence

Scopes are searched in order; the first matching variable with a non-blank value wins:

1. `Process`
2. `User`
3. `Machine`

### Patterns

The resolver matches case-insensitively against patterns aligned with Infisical's CLI defaults plus common variants such as `CLOUDINIT_INFISICAL_*` and custom-prefixed names (e.g., `myapp_infisical_client_id`).

| Parameter         | Example variable names matched                                                       |
| ----------------- | ------------------------------------------------------------------------------------ |
| `BaseUri`         | `INFISICAL_API_URL`, `INFISICAL_BASE_URL`, `INFISICAL_HOST`                          |
| `OrganizationId`  | `INFISICAL_ORG_ID`, `INFISICAL_ORGANIZATION_ID`                                      |
| `ProjectId`       | `INFISICAL_PROJECT_ID`, `INFISICAL_WORKSPACE_ID`                                     |
| `Environment`     | `INFISICAL_ENVIRONMENT`, `INFISICAL_ENV`, `INFISICAL_ENV_SLUG`                       |
| `ClientId`        | `INFISICAL_CLIENT_ID`, `INFISICAL_UNIVERSAL_AUTH_CLIENT_ID`                          |
| `ClientSecret`    | `INFISICAL_CLIENT_SECRET`, `INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET`                  |
| `AccessToken`     | `INFISICAL_TOKEN`, `INFISICAL_ACCESS_TOKEN`, `INFISICAL_AUTH_TOKEN`                  |
| `SecretPath`      | `INFISICAL_SECRET_PATH`, `INFISICAL_DEFAULT_SECRET_PATH`                             |
| `ApiVersion`      | `INFISICAL_API_VERSION`                                                              |

Sensitive values (`ClientSecret`, `AccessToken`) are read directly into a read-only `SecureString` and never logged.

### Zero-configuration example

```powershell
[Environment]::SetEnvironmentVariable('INFISICAL_API_URL',       'https://app.infisical.com',          'User')
[Environment]::SetEnvironmentVariable('INFISICAL_ORG_ID',        '00000000-0000-0000-0000-000000000000', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_PROJECT_ID',    '11111111-1111-1111-1111-111111111111', 'User')
[Environment]::SetEnvironmentVariable('INFISICAL_ENVIRONMENT',   'dev',                                  'User')
[Environment]::SetEnvironmentVariable('INFISICAL_CLIENT_ID',     'machine-identity-client-id',           'User')
[Environment]::SetEnvironmentVariable('INFISICAL_CLIENT_SECRET', 'super-secret-value',                   'User')

Connect-Infisical
Get-InfisicalSecret
```

### Mixed example (explicit values override discovery)

Explicit parameters always win over discovered values; blank/whitespace explicit values trigger discovery.

```powershell
Connect-Infisical -Environment 'prod'   # everything else discovered from environment
```

### Logging

The resolver emits a single verbose line announcing the scan and one informational line per discovered variable (variable name and scope; values are never logged). Use `-Verbose` to see the scan announcement.

## Building

```powershell
pwsh -NoProfile -ExecutionPolicy Bypass -File .\build.ps1 -RunTests
```

The script builds the binary, runs unit tests, publishes binaries into `Module/PSInfisicalAPI/bin/`, regenerates the manifest, and validates that the module imports.

## Extending the module

### Adding a new API endpoint

All HTTP routes live in two files under `src/PSInfisicalAPI/Endpoints/`:

- `InfisicalEndpointNames.cs` declares a `const string` identifier for each endpoint.
- `InfisicalEndpointRegistry.cs` maps each identifier to one or more `InfisicalEndpointDefinition` records grouped by resource (`RegisterAuthentication`, `RegisterSecrets`, `RegisterPki`, etc.).

To add a route:

1. Add a constant in `InfisicalEndpointNames.cs` (e.g., `public const string ListPkiSubscribers = "ListPkiSubscribers";`).
2. In the matching `Register<Resource>` method, call `Add(map, new InfisicalEndpointDefinition { ... })` with `Name`, `Resource`, `Version`, `Method`, `Template`, and the `RequiresAuthorization` / `ContainsSecretMaterialInRequest` / `ContainsSecretMaterialInResponse` flags. Use `{placeholder}` tokens in `Template`; they are substituted from the `pathParameters` dictionary passed by the caller.
3. If the same logical operation has more than one upstream path (legacy + current), register both definitions under the same `Name` — `InvokeWithCandidateFallback` tries each in order until one succeeds.
4. Invoke the endpoint from the appropriate client (`InfisicalPkiClient`, `InfisicalSecretsClient`, etc.) via `_invoker.InvokeWithCandidateFallback(connection, InfisicalEndpointNames.XYZ, "XYZ", pathParameters, query, body)`.

### Adding a new cmdlet

Cmdlets live in `src/PSInfisicalAPI/Cmdlets/` and derive from `InfisicalCmdletBase`, which exposes `HttpClient`, `Logger`, `ResolveProjectId`, and `ThrowTerminatingForException`. Follow the consolidated discovery pattern when the cmdlet supports both list and single-record retrieval:

```csharp
[Cmdlet(VerbsCommon.Get, "InfisicalPkiSubscriber", DefaultParameterSetName = "List")]
[OutputType(typeof(InfisicalPkiSubscriber))]
public sealed class GetInfisicalPkiSubscriberCmdlet : InfisicalCmdletBase
{
    [Parameter(ParameterSetName = "ByName", Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
    [Alias("SubscriberName", "Slug")]
    public string Name { get; set; }

    [Parameter] public string ProjectId { get; set; }

    protected override void ProcessRecord() { /* dispatch on ParameterSetName */ }
}
```

After adding (or removing) a cmdlet:

1. Update `build.ps1` in **two** places — the `CmdletsToExport` array inside the generated manifest block, and the `$expectedCmds` array used by `Test-ModuleImports`. Both must list the same cmdlets; the build fails fast if they drift.
2. Add a `<command:command>` entry in `Module/PSInfisicalAPI/en-US/PSInfisicalAPI.dll-Help.xml`. Each entry must include a non-empty `<maml:description>` synopsis (do not let it start with the cmdlet name — the validation gate rejects PowerShell's auto-generated fallback), a non-empty `<maml:description>` body, and at least one `<command:example>` with a non-empty `<dev:code>` block.
3. For consolidated `List` / single-record cmdlets, ship **three examples**: two straight-line invocations (one per parameter set) and one `OrderedDictionary` splat. The splat must construct the dictionary with `OrdinalIgnoreCase` so parameter names round-trip case-insensitively:

   ```powershell
   $Params = New-Object -TypeName 'System.Collections.Specialized.OrderedDictionary' -ArgumentList ([System.StringComparer]::OrdinalIgnoreCase)
   $Params.ProjectId = (Get-InfisicalProject | Select-Object -First 1).Id
   $Result = Get-InfisicalPkiSubscriber @Params
   ```
4. Add a `## Unreleased` entry to `CHANGELOG.md` describing the change (mark removals of public cmdlets or parameters as **BREAKING**).
5. Run `./build.ps1 -RunTests`. The script enforces the cmdlet list, runs the xUnit suite, and verifies that every exported cmdlet has a valid synopsis, description, and at least one non-empty example.

### Committing source and build artifacts in lockstep

The embedded `BuildCommitHash` in `Module/PSInfisicalAPI/PSInfisicalAPI.psd1` and the bundled DLL is captured from `git rev-parse HEAD` at build time. To keep the embedded hash truthful, commit source and build artifacts as two ordered commits:

1. Stage and commit your source changes first. Suppose this produces commit `S`.
2. Run `./build.ps1 -RunTests -CommitArtifacts`. The build picks up `S` as `HEAD`, embeds it as `BuildCommitHash`, then stages and commits **only** the build outputs (`Module/PSInfisicalAPI/bin/**`, `Module/PSInfisicalAPI/PSInfisicalAPI.psd1`, and the `CHANGELOG.md` build-stamp insertion). The commit message references `S` so the binary commit always traces back to its source.
3. `git push`.

`-CommitArtifacts` only touches the three artifact paths above; any other dirty files in your working tree are left alone. Use the older `-CommitOnSuccess` switch only when you intentionally want a single commit covering everything (`git add -A` + `git commit -m "Build <version>"`); the two switches are mutually exclusive.

## Continuous integration

`.gitea/workflows/publish-psgallery.yml` publishes the module to the PowerShell Gallery whenever a pull request is merged into `main`. The workflow expects a repository secret named `PSGALLERY_API_KEY` containing a valid Gallery API key.

## License

Distributed under the GNU Affero General Public License v3.0. See [LICENSE](LICENSE).
