KeyControl.psm1

function ConvertFrom-Box {
    <#
    .SYNOPSIS
        Converts Box API responses into something presentable.
     
    .DESCRIPTION
        Converts Box API responses into something presentable.
     
    .PARAMETER InputObject
        The Box API response object to make pretty.
     
    .EXAMPLE
        PS C:\> (Invoke-KeyControlRequest @param).boxes | ConvertFrom-Box
 
        Searches for Boxes and makes them presentable.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject
    )
    process {
        if (-not $InputObject) { return }

        [PSCustomObject]@{
            PSTypeName        = 'KeyControl.Box'
            BoxID             = $InputObject.box_id
            Name              = $InputObject.name
            Description       = $InputObject.Description
            Created           = $InputObject.created_at -as [datetime]
            Updated           = $InputObject.updated_at -as [datetime]
            Tags              = $InputObject.tags
            Revision          = $InputObject.revision
            MaxSecretVersions = $InputObject.max_secret_versions
            Rotation          = $InputObject.rotation
            ExclusiveCheckout = $InputObject.exclusive_checkout

            Object            = $InputObject
        }
    }
}

function ConvertFrom-Secret {
    <#
    .SYNOPSIS
        Converts Secret API response objects into presentable data.
     
    .DESCRIPTION
        Converts Secret API response objects into presentable data.
     
    .PARAMETER InputObject
        The data to make pretty.
     
    .PARAMETER BoxID
        The ID of the box the secret is from.
        Added as data to the processed API response.
     
    .EXAMPLE
        PS C:\> (Invoke-KeyControlRequest @param).secrets | ConvertFrom-Secret -BoxID $BoxID
 
        Search for secrets abd make them pretty.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)]
        $InputObject,

        [string]
        $BoxID
    )
    process {
        if (-not $InputObject) { return }

        [PSCustomObject]@{
            PSTypeName     = 'KeyControl.Secret'
            BoxID          = $BoxID
            SecretID       = $InputObject.secret_id
            Revision       = $InputObject.revision
            Name           = $InputObject.name
            Description    = $InputObject.desc
            BoxName        = $InputObject.box_name
            Tags           = $InputObject.tags
            Expired        = $InputObject.expired
            CanAccess      = $InputObject.checkout_allowed

            CurrentVersion = $InputObject.current_version
            VersionCount   = $InputObject.version_count

            VersionCreated = $InputObject.current_version_creation_time -as [datetime]
            FirstCreated   = $InputObject.created_at -as [datetime]
            Updated        = $InputObject.updated_at -as [datetime]
            Accessed       = $InputObject.last_accessed -as [datetime]

            Owner          = $InputObject.owner_name
            OwnerMail      = $InputObject.owner_email

            Type           = $InputObject.secret_type.type
            SubType        = $InputObject.secret_subtype_info
            Info           = $InputObject.secret_info

            Secret         = $null

            Object         = $InputObject
        }
    }
}

function Assert-KeyControlConnection {
    <#
    .SYNOPSIS
        Ensure there exists a working connection the an entrust Key Control secrets Vault.
     
    .DESCRIPTION
        Ensure there exists a working connection the an entrust Key Control secrets Vault.
        This function is mostly used internally to ensure commands fail early when not connected.
 
        Use the "Connect-KeyControl" command to establish a connection.
     
    .PARAMETER Cmdlet
        The $PSCmdlet variable of the command calling this command.
        By providing this parameter, the error thrown in case of a missing connection happens within the context of the calling command.
        In essence, this hides this function - Assert-KeyControlConnection - from the user and instead only shows the calling command.
     
    .EXAMPLE
        PS C:\> Assert-KeyControlConnection -Cmdlet $PSCmdlet
         
        Will do nothing if already connected or throw a terminating exception in the context of the calling command if not so.
        This function will be fully invisible to the end user.
    #>

    [CmdletBinding()]
    param (
        $Cmdlet = $PSCmdlet
    )
    process {
        if ($script:_KeyControlSession) { return }

        $Cmdlet.ThrowTerminatingError(
            [System.Management.Automation.ErrorRecord]::new(
                [System.Exception]::new("Not connected yet! Use Connect-KeyControl to connect to a KeyControl server first."),
                "NotAuthenticated",
                [System.Management.Automation.ErrorCategory]::AuthenticationError,
                $null
            )
        )
    }
}

function Connect-KeyControl {
    <#
    .SYNOPSIS
        Connects to a entrust Key Control secrets Vault.
     
    .DESCRIPTION
        Connects to a entrust Key Control secrets Vault.
 
        This module assumes regular account authentication settings (local or ldap).
     
    .PARAMETER ComputerName
        The computer hosting the vault.
     
    .PARAMETER Vault
        The ID of the vault to connect to.
     
    .PARAMETER Credential
        The credentials of the account used for authentication.
     
    .EXAMPLE
        PS C:\> Connect-KeyControl -ComputerName vault.contoso.com -Credential $cred -Vault $vaultID
     
        Connects to the entrust Key Control secrets Vault hosted on "vault.contoso.com", using the credentials provided in $cred.
        It specifically connects to the vault in $vaultID
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]
        $ComputerName,

        [Parameter(Mandatory = $true)]
        [string]
        $Vault,

        [Parameter(Mandatory = $true)]
        [PSCredential]
        $Credential
    )
    process {
        $session = [PSCustomObject]@{
            ComputerName = $ComputerName
            Credential   = $Credential
            Vault        = $Vault
            Token        = ''
            Expires      = (Get-Date).AddMinutes(30)
            BasePath     = "https://$ComputerName/vault/1.0"
        }

        $body = @{
            username = $Credential.UserName
            password = $Credential.GetNetworkCredential().Password
        }
        $response = Invoke-RestMethod -Method POST -Uri "$($session.BasePath)/Login/$Vault/" -Body ($body | ConvertTo-Json) -ContentType 'application/json'

        if ($response.access_token) {
            $session.Token = $response.access_token

            $script:_KeyControlSession = $session
        }
        else {
            throw "Failed to connect: $($response | ConvertTo-Json)"
        }
    }
}

function Disconnect-KeyControl {
    <#
    .SYNOPSIS
        Disconnects from the previously connected entrust Key Control secrets Vault.
     
    .DESCRIPTION
        Disconnects from the previously connected entrust Key Control secrets Vault.
        Use "Connect-KeyControl" to first establish a connection.
 
        Does not act at all, when not already connected.
     
    .EXAMPLE
        PS C:\> Disconnect-KeyControl
 
        Disconnects from the previously connected entrust Key Control secrets Vault.
    #>

    [CmdletBinding()]
    param ()
    process {
        if (-not $script:_KeyControlSession) { return }
        
        Invoke-KeyControlRequest -Path 'logout/'
        $script:_KeyControlSession = $null
    }
}

function Get-KeyControlBox {
    <#
    .SYNOPSIS
        Searches for boxes in the connected Key Control Vault.
     
    .DESCRIPTION
        Searches for boxes in the connected Key Control Vault.
 
        Requires an already established connection via "Connect-KeyControl".
     
    .PARAMETER Name
        The name to search by.
     
    .PARAMETER BoxID
        The specific ID of the box to retrieve.
     
    .PARAMETER Filter
        A custom filter condition to search by.
        For when the builtin parameters do not cover your need.
        Filter reference: https://docs.hytrust.com/DataControl/Online/Content/Books/Secrets-Vault-Programmers-Reference/API/API-Filters.html
     
    .EXAMPLE
        PS C:\> Get-KeyControlBox
         
        Lists all boxes in the connected vault.
 
    .EXAMPLE
        PS C:\> Get-KeyControlBox -Name d12*
 
        Lists all boxes in the connected vault whose name starts with "d12"
 
    .EXAMPLE
        PS C:\> Get-KeyControlBox -BoxID $boxID
 
        Retrieves the specifically requested box.
    #>

    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName = 'ByName')]
        [string]
        $Name = '*',

        [Parameter(Mandatory = $true, ParameterSetName = 'ByID')]
        [Alias('ID')]
        [string]
        $BoxID,

        [Parameter(Mandatory = $true, ParameterSetName = 'ByFilter')]
        [string]
        $Filter
    )
    begin {
        Assert-KeyControlConnection -Cmdlet $PSCmdlet
    }
    process {
        if ($BoxID) {
            $body = @{
                'box_id' = $BoxID
            }
            Invoke-KeyControlRequest -Path 'GetBox/' -Body $body | ConvertFrom-Box
            return
        }

        $param = @{
            Path = 'ListBoxes/'
        }

        if ($Name) {
            $filterString = "/name eq '$Name'"
            if ($Name -match '^\*') { $filterString = "endswith(/name, '$($Name.Trim('*'))')" }
            if ($Name -match '\*$') { $filterString = "startswith(/name, '$($Name.Trim('*'))')"}
    
            $body = @{
                filters = $filterString
            }
            $param.Body = $body
        }
        if ($Filter) {
            $body = @{
                filters = $Filter
            }
            $param.Body = $body
        }
        (Invoke-KeyControlRequest @param).boxes | ConvertFrom-Box
    }
}

function Get-KeyControlSecret {
    <#
    .SYNOPSIS
        Searches for secrets in a Key Control Vault's specified box.
         
    .DESCRIPTION
        Searches for secrets in a Key Control Vault's specified box.
        Secret Data is only included when asking for a specific secret by ID!
     
        Requires an already established connection via "Connect-KeyControl".
     
    .PARAMETER BoxID
        The ID of the box to search in.
     
    .PARAMETER SecretID
        The secret ID of the specific secret to retrieve.
     
    .PARAMETER Name
        The name to search by.
        Supports wildcards at the beginning or the end, but not in the middle of the name.
     
    .PARAMETER Tags
        Tags to search of (including their value).
     
    .PARAMETER Filter
        A custom filter condition to search by.
        For when the builtin parameters do not cover your need.
        Filter reference: https://docs.hytrust.com/DataControl/Online/Content/Books/Secrets-Vault-Programmers-Reference/API/API-Filters.html
     
    .PARAMETER Version
        The specific version of the secret to retrieve.
 
    .PARAMETER NameProperty
        The property on the info object to use for a credential name.
        If Specified, this command will return a PSCredential object, with the value of that property as Username and the secret as password.
     
    .EXAMPLE
        PS C:\> Get-KeyControlSecret -BoxID $boxID
         
        Lists all secrets' info from within the specified box.
 
    .EXAMPLE
        PS C:\> Get-KeyControlSecret -BoxID $boxID SecretID $secret.secret_id
 
        Retrieve the specified secret, including both metadata and the actual secret data.
 
    .EXAMPLE
        PS C:\> Get-KeyControlSecret -BoxID $boxID SecretID $secret.secret_id -NameProperty name
 
        Retrieve the specified secret, returning a PSCredential object with the secret name as username and secret as password.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")]
    [CmdletBinding(DefaultParameterSetName = 'ByCondition')]
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $BoxID,

        [Parameter(ParameterSetName = 'ByID', Mandatory = $true, ValueFromPipelineByPropertyName = $true)]
        [string]
        $SecretID,

        [Parameter(ParameterSetName = 'ByCondition')]
        [string]
        $Name,

        [Parameter(ParameterSetName = 'ByCondition')]
        [hashtable]
        $Tags,

        [Parameter(ParameterSetName = 'ByFilter')]
        [string]
        $Filter,

        [Parameter(ParameterSetName = 'ByID', ValueFromPipelineByPropertyName = $true)]
        [Alias('CurrentVersion')]
        [string]
        $Version,

        [Parameter(ParameterSetName = 'ByID')]
        [string]
        $NameProperty
    )
    process {
        if ($SecretID) {
            $body = @{
                box_id = $BoxID
                secret_id = $SecretID
            }
            if ($Version) {
                $body['version'] = $Version
            }

            $secretInfo = Invoke-KeyControlRequest -Path 'GetSecret/' -Body $body | ConvertFrom-Secret -BoxID $BoxID
            $secret = Invoke-KeyControlRequest -Path 'CheckoutSecret/' -Body $body | Add-Member -MemberType NoteProperty -Name BoxID -Value $BoxID -PassThru
            if ($secret.secret_data -is [string]) { $secretInfo.Secret = $secret.secret_data | ConvertTo-SecureString -AsPlainText -Force }
            else { $secretInfo.Secret = $secret.secret_data | ConvertTo-Json -Depth 99 | ConvertTo-SecureString -AsPlainText -Force }

            if (-not $NameProperty) { return $secretInfo }
            
            [PSCredential]::new($secret.$NameProperty, $secret.Secret)
            return
        }

        $body = @{
            box_id = $BoxID
        }
        $conditions = @()
        if ($Name) {
            if ($Name -notmatch '^\*|\*$') { $conditions += "/name eq '$Name'" }
            if ($Name -match '^\*') { $conditions += "endswith(/name, '$($Name.Trim('*'))')" }
            if ($Name -match '\*$') { $conditions += "startswith(/name, '$($Name.Trim('*'))')"}
        }
        if ($Tags) {
            foreach ($pair in $Tags.GetEnumerator()) {
                $conditions += "/tags/{0} eq '{1}'" -f $pair.Key, $pair.Value
            }
        }
        if ($Filter) { $conditions = @($Filter) }
        
        if ($conditions.Count -gt 0) {
            $body['filters'] = $conditions -join ' and '
        }

        $param = @{
            Path = 'ListSecrets/'
            Body = $body
        }

        (Invoke-KeyControlRequest @param).secrets | ConvertFrom-Secret -BoxID $BoxID
    }
}

function Invoke-KeyControlRequest {
    <#
    .SYNOPSIS
        Executes an API request against the entrust Key Control secrets Vault API.
     
    .DESCRIPTION
        Executes an API request against the entrust Key Control secrets Vault API.
        This tool is optimized to make request execution simple, handling authentication, headers & body formatting, while making path selection easy.
        Rather than specifying the full URL, with this helper you can provide simply the final endpoint you are executing against - e.g. "ListBoxes/".
                 
        Will automatically refresh the session if it is about to expire (or already has expired).
 
        Requires an already established connection via "Connect-KeyControl".
 
        This command is mostly intended for internal use, but exposed for custom scenarios or non-implemented endpoints.
 
        Documentation:
        - KeyControl API Reference: https://docs.hytrust.com/DataControl/Online/Content/Books/Secrets-Vault-Programmers-Reference/API/Accessing-the-SV-API.html
        - Examples (in GO) for endpoints: https://github.com/EntrustCorporation/pasmcli/tree/master/pasmcli/cmd
     
    .PARAMETER Path
        Relative path to the base URI of the service.
        Usually expects something like "ListBoxes/" or "GetSecret/".
        Most paths expect a trailing "/"
     
    .PARAMETER Body
        The json payload to send.
        Will convert to json if not already a string.
     
    .EXAMPLE
        PS C:\> Invoke-KeyControlRequest -Path 'GetBox/' -Body @{ box_id = $id }
 
        Retrieves the specified box from the connected vault.
    #>

    [CmdletBinding()]
    param (
        [string]
        $Path,

        $Body = @()
    )
    begin {
        Assert-KeyControlConnection -Cmdlet $PSCmdlet

        if ($script:_KeyControlSession.Expires -lt (Get-Date).AddMinutes(2)) {
            Connect-KeyControl -ComputerName $script:_KeyControlSession.ComputerName -Credential $script:_KeyControlSession.Credential -Vault $script:_KeyControlSession.Vault
        }
    }
    process {
        $param = @{
            Method = 'POST'
            Uri = "$($script:_KeyControlSession.BasePath.Trim('/\'))/$($Path.TrimStart('/\'))"
            Headers = @{
                'content-type' = 'application/json'
                'x-vault-auth' = $script:_KeyControlSession.Token
            }
        }
        if ($Body) {
            $bodyJson = $Body
            if ($Body -isnot [string]) { $bodyJson = ConvertTo-Json -InputObject $Body }
            $param.Body = $bodyJson
        }

        Invoke-RestMethod @param -ErrorAction Stop
    }
}

# The currently connected Key Control session
$script:_KeyControlSession = $null