IdentityCommand.SCA.psm1

function ConvertFrom-SCAApiValue {
    <#
    .SYNOPSIS
    Translates a numeric value returned by the SCA API to its name and meaning.
 
    .DESCRIPTION
    The reverse of ConvertTo-SCAApiValue. Given a numeric value from an API response, returns the
    friendly name and the description of what the value means, as defined by Get-SCAApiValueMap.
 
    A value the module has no definition for is not an error - the returned object reports a null name
    and description, so an enumeration extended by the API does not break the calling command.
 
    .PARAMETER Name
    The enumeration to look the value up in.
 
    .PARAMETER Value
    The numeric value returned by the API.
 
    .EXAMPLE
    ConvertFrom-SCAApiValue -Name PolicyStatus -Value 1
 
    Returns an object reporting Value 1, Name 'Active' and Description 'The policy is active'.
    #>

    [OutputType([pscustomobject])]
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            Position = 0
        )]
        [ValidateSet('CloudProvider', 'PolicyStatus')]
        [string]$Name,

        [parameter(
            Mandatory = $true,
            Position = 1,
            ValueFromPipeline = $true
        )]
        [int]$Value
    )

    begin {

        $Map = Get-SCAApiValueMap -Name $Name

    }#begin

    process {

        $MemberName = $Map.Keys | Where-Object { $Map[$PSItem]['Value'] -eq $Value } | Select-Object -First 1

        if ($null -eq $MemberName) {

            Write-Verbose "$Value is not a $Name value defined by this module"

        }

        [pscustomobject]@{
            Value       = $Value
            Name        = $MemberName
            Description = if ($null -ne $MemberName) { $Map[$MemberName]['Description'] } else { $null }
        }

    }#process

}

function ConvertTo-SCAApiValue {
    <#
    .SYNOPSIS
    Translates a friendly value to the numeric value the SCA API expects.
 
    .DESCRIPTION
    A handful of SCA fields are numeric enumerations - the cloud provider a policy or scan applies to,
    and the status a policy query filters on. Commands take the readable name from the caller and
    convert it here, against the enumeration defined by Get-SCAApiValueMap.
 
    .PARAMETER Name
    The enumeration to look the value up in.
 
    CloudProvider - AWS = 0, GCP = 1, AZURE = 2, AWS_IDC = 3, AZURE_ENTRA_ID = 4
    PolicyStatus - Active = 1, Expired = 3, Error = 4, Warning = 5, Validating = 6
 
    .PARAMETER Value
    The friendly value to translate. Matched case-insensitively.
 
    .EXAMPLE
    ConvertTo-SCAApiValue -Name CloudProvider -Value AWS
 
    Returns 0
 
    .EXAMPLE
    ConvertTo-SCAApiValue -Name PolicyStatus -Value Validating
 
    Returns 6
    #>

    [OutputType([int])]
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            Position = 0
        )]
        [ValidateSet('CloudProvider', 'PolicyStatus')]
        [string]$Name,

        [parameter(
            Mandatory = $true,
            Position = 1,
            ValueFromPipeline = $true
        )]
        [string]$Value
    )

    begin {

        $Map = Get-SCAApiValueMap -Name $Name

    }#begin

    process {

        $Member = $Map[$Value]

        if ($null -eq $Member) {

            throw "'$Value' is not a supported $Name value. Expected one of: $(($Map.Keys | Sort-Object) -join ', ')"

        }

        $Member['Value']

    }#process

}

function Get-SCAApiValueMap {
    <#
    .SYNOPSIS
    Returns the definition of an SCA API numeric enumeration.
 
    .DESCRIPTION
    A handful of SCA fields are numeric enumerations - the cloud provider a policy or scan applies to, and
    the status of a policy. This helper is the single definition of those enumerations: the friendly name
    of each member, the numeric value the API uses for it, and a description of what it means.
 
    ConvertTo-SCAApiValue translates a friendly name to the numeric value for a request; and
    ConvertFrom-SCAApiValue translates a numeric value from a response back to its name and description.
 
    .PARAMETER Name
    The enumeration to return.
 
    .EXAMPLE
    (Get-SCAApiValueMap -Name PolicyStatus)['Active'].Value
 
    Returns 1
 
    .EXAMPLE
    (Get-SCAApiValueMap -Name CloudProvider).Keys
 
    Returns the supported cloud provider names.
    #>

    [OutputType([System.Collections.Specialized.OrderedDictionary])]
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            Position = 0
        )]
        [ValidateSet('CloudProvider', 'PolicyStatus')]
        [string]$Name
    )

    $Map = @{

        'CloudProvider' = [ordered]@{
            'AWS'            = [ordered]@{ Value = 0; Description = 'Amazon Web Services (IAM)' }
            'GCP'            = [ordered]@{ Value = 1; Description = 'Google Cloud' }
            'AZURE'          = [ordered]@{ Value = 2; Description = 'Azure (resource)' }
            'AWS_IDC'        = [ordered]@{ Value = 3; Description = 'Amazon Web Services (IAM Identity Center)' }
            'AZURE_ENTRA_ID' = [ordered]@{ Value = 4; Description = 'Azure (Microsoft Entra ID)' }
        }

        'PolicyStatus'  = [ordered]@{
            'Active'     = [ordered]@{ Value = 1; Description = 'The policy is active' }
            'Expired'    = [ordered]@{ Value = 3; Description = 'The policy has expired' }
            'Error'      = [ordered]@{ Value = 4; Description = 'There is an error in the policy' }
            'Warning'    = [ordered]@{ Value = 5; Description = 'The policy is active, with a warning' }
            'Validating' = [ordered]@{ Value = 6; Description = 'The policy is currently being validated' }
        }

    }

    $Map[$Name]

}

function Get-SCAPolicyApiHeader {
    <#
    .SYNOPSIS
    Returns the header which selects the version of the SCA Policies API to call.
 
    .DESCRIPTION
    The SCA Policies API exposes two request/response shapes, selected by the X-API-Version request
    header. This module works exclusively with version 2.0 - the version CyberArk recommends - so every
    policy command sends the same header, formed here.
 
    .EXAMPLE
    Invoke-IDRestMethod -Uri $URI -Method GET -Headers (Get-SCAPolicyApiHeader)
    #>

    [OutputType([hashtable])]
    [CmdletBinding()]
    param()

    @{ 'X-API-Version' = '2.0' }

}

function Get-SCAPolicyTextPattern {
    <#
    .SYNOPSIS
    Returns the pattern the SCA API accepts for a policy name or description.
 
    .DESCRIPTION
    The API constrains the characters allowed in a policy name and description, and publishes a
    regular expression for each. Both are reproduced here verbatim, as the single definition used by
    Test-SCAPolicyText.
 
    Each pattern contains a character range rather than the list of literal characters its layout
    suggests, so the accepted set is wider than it appears. Neither accepts tab, newline or any
    non-ASCII character, and both reject the quote, bracket and ampersand characters commonly used
    in free text.
 
    .PARAMETER Name
    The policy field to return the pattern for.
 
    .EXAMPLE
    Get-SCAPolicyTextPattern -Name Name
 
    Returns the pattern a policy name must match.
    #>

    [OutputType([string])]
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            Position = 0
        )]
        [ValidateSet('Name', 'Description')]
        [string]$Name
    )

    $Pattern = @{
        #Accepts a space and ! + , - . / 0-9 : ; < = > ? @ A-Z [ \ _ a-z
        'Name'        = '^[A-Za-z0-9-@ _!.+-\\s]{1,200}$'
        #As Name, and additionally $ ] ^ { }
        'Description' = '^[A-Za-z0-9-@ _!^[{}.+-\]$\\s]{0,200}$'
    }

    $Pattern[$Name]

}

function Resolve-SCAJobStatus {
    <#
    .SYNOPSIS
    Reports the status of the job started by an asynchronous request.
 
    .DESCRIPTION
    Some SCA operations are asynchronous, and their response carries nothing except the identifier of the
    job which was started. The commands calling those endpoints pass the response here, and the status of
    that job is returned in its place.
 
    The API is inconsistent about which field carries the identifier - 'jobId' from the policy endpoints,
    'job_id' from web app creation - so both are accepted.
 
    A response carrying neither field is returned unaltered, so a response which reports more than the job
    it started does not have that detail discarded.
 
    .PARAMETER Result
    The response from the asynchronous request.
 
    .EXAMPLE
    Invoke-IDRestMethod -Uri $URI -Method POST -Body $body | Resolve-SCAJobStatus
 
    .EXAMPLE
    Resolve-SCAJobStatus -Result $result
    #>

    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            Position = 0,
            ValueFromPipeline = $true
        )]
        [psobject]$Result
    )

    process {

        $JobId = $null
        $Fields = $Result.PSObject.Properties.Name

        foreach ($Field in 'jobId', 'job_id') {

            if (($Fields -contains $Field) -and (-not [string]::IsNullOrEmpty($Result.$Field))) {

                $JobId = $Result.$Field
                break

            }

        }

        if ([string]::IsNullOrEmpty($JobId)) {

            Write-Verbose 'Response contains no job identifier - returning it unaltered'
            return $Result

        }

        Get-SCAJobStatus -jobId $JobId

    }#process

}

function Test-SCAPolicyText {
    <#
    .SYNOPSIS
    Validates a policy name or description against the characters the SCA API accepts.
 
    .DESCRIPTION
    Used as the ValidateScript for the policy -name and -description parameters. The API rejects a
    value containing an unsupported character, so this catches it before the request is sent, and
    reports which characters were at fault - a plain ValidatePattern would show the caller nothing
    but the raw expression.
 
    Returns $true when the value is acceptable, and throws otherwise, as ValidateScript requires.
 
    .PARAMETER Value
    The value supplied for the parameter.
 
    .PARAMETER ParameterName
    The name of the parameter being validated, used in the error message.
 
    .PARAMETER Pattern
    The pattern the value must match, from Get-SCAPolicyTextPattern.
 
    .EXAMPLE
    Test-SCAPolicyText -Value 'End of year' -ParameterName name -Pattern (Get-SCAPolicyTextPattern -Name Name)
 
    Returns $true.
    #>

    [OutputType([bool])]
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            Position = 0
        )]
        [AllowEmptyString()]
        [string]$Value,

        [parameter(
            Mandatory = $true,
            Position = 1
        )]
        [string]$ParameterName,

        [parameter(
            Mandatory = $true,
            Position = 2
        )]
        [string]$Pattern
    )

    if ($Value -cmatch $Pattern) { return $true }

    #Report the offending characters rather than the expression which rejected them
    $Unsupported = ([char[]]$Value | Where-Object { "$PSItem" -cnotmatch $Pattern } | Select-Object -Unique) -join ''

    if ([string]::IsNullOrEmpty($Unsupported)) {

        throw "The policy $ParameterName is not an accepted length."

    }

    throw "The policy $ParameterName contains characters the SCA API does not accept: $Unsupported"

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Connect-SCATenant {

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Subdomain')]
    param(

        #subdomain
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Subdomain')]
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'SubdomainCredential')]
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'SubdomainSAML')]
        [ValidateNotNullOrEmpty()]
        [Alias('subdomain')]
        [String]$tenant_subdomain,

        #tenant_url
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'URL')]
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'URLCredential')]
        [parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'URLSAML')]
        [ValidateNotNullOrEmpty()]
        [Alias('SCA_url')]
        [String]$tenant_url,

        #Credential used to authenticate to CyberArk Identity. Authentication is performed even if an active IdentityCommand session is found, replacing it
        [parameter(Mandatory = $true, ParameterSetName = 'SubdomainCredential')]
        [parameter(Mandatory = $true, ParameterSetName = 'URLCredential')]
        [ValidateNotNullOrEmpty()]
        [PSCredential]$Credential,

        #Authenticate as a service user via New-IDPlatformToken (OAuth client_credentials) instead of the interactive New-IDSession
        [parameter(ParameterSetName = 'SubdomainCredential')]
        [parameter(ParameterSetName = 'URLCredential')]
        [Switch]$PlatformToken,

        #SAML assertion used to authenticate to CyberArk Identity. Authentication is performed even if an active IdentityCommand session is found, replacing it
        [parameter(Mandatory = $true, ParameterSetName = 'SubdomainSAML')]
        [parameter(Mandatory = $true, ParameterSetName = 'URLSAML')]
        [ValidateNotNullOrEmpty()]
        [String]$SAMLResponse

    )

    begin {

        $IDSession = Get-IDSession
        $HaveSession = $null -ne $IDSession.tenant_url
        $AuthRequested = $PSBoundParameters.ContainsKey('Credential') -or $PSBoundParameters.ContainsKey('SAMLResponse')

        if ($HaveSession -and $AuthRequested) {
            Write-Verbose 'Authentication parameters were supplied; authenticating and replacing the existing IdentityCommand session'
        }

    }#begin

    process {

        $UsingSubdomain = $PSCmdlet.ParameterSetName -like 'Subdomain*'

        #Resolve service URLs from platform discovery when a subdomain was supplied, or when
        #authentication is required and the CyberArk Identity URL must be discovered.
        $ServiceUrl = $null

        if ($UsingSubdomain) {

            $ServiceUrl = Resolve-ServiceUrl -Service sca -Subdomain $tenant_subdomain
            $tenant_url = $ServiceUrl.ServiceUrl

        } else {

            #Ensure URL is in expected format - remove trailing slash if provided in Url
            $tenant_url = $tenant_url -replace '/$', ''

            if ($AuthRequested) {
                $ServiceUrl = Resolve-ServiceUrl -Service sca -Url $tenant_url
            }

        }

        if ($AuthRequested -or (-not $HaveSession)) {

            if (-not $AuthRequested) {
                throw 'Authenticate with New-IDSession or New-IDPlatformToken, or supply -Credential, and try again'
            }

            $IdentityUrl = $ServiceUrl.IdentityUrl

            if ($PSCmdlet.ShouldProcess($IdentityUrl, 'Authenticate to CyberArk Identity')) {

                if ($PSCmdlet.ParameterSetName -like '*SAML') {
                    $null = New-IDSession -tenant_url $IdentityUrl -SAMLResponse $SAMLResponse
                } elseif ($PlatformToken) {
                    $null = New-IDPlatformToken -tenant_url $IdentityUrl -Credential $Credential
                } else {
                    $null = New-IDSession -tenant_url $IdentityUrl -Credential $Credential
                }

                $IDSession = Get-IDSession

            }

        }

        #Make the CyberArk Identity Session available in the IdentityCommand.SCA scope
        foreach ($key in $IDSession.keys) {
            if ($null -ne $IDSession[$key]) {
                $ISPSSSession[$key] = $IDSession[$key]
            }
        }

        #Set the SCA URL in the session data
        $ISPSSSession.tenant_url = $tenant_url

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Get-SCAEligibleGroup {
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AZURE')]
        [String]$csp = 'AZURE',

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateRange(1, 100)]
        [int]$limit
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/access/$csp/eligibility/groups"

        $URI = Add-QueryString -URI $URI -Parameter ($PSBoundParameters | Get-Parameter -ParametersToRemove csp)

        #Send Request
        $result = Invoke-IDRestMethod -Uri $URI -Method GET

        if ($null -ne $result) {

            Get-PagedResult -InitialResult $result -URI $URI -Style Cursor -ResultProperty response -CursorRequestKey nextToken -CursorResponseKey nextToken

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Get-SCAEligibleTarget {
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AWS', 'AZURE', 'GCP')]
        [String]$csp,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateRange(1, 100)]
        [int]$limit
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/access/$csp/eligibility"

        $URI = Add-QueryString -URI $URI -Parameter ($PSBoundParameters | Get-Parameter -ParametersToRemove csp)

        #Send Request
        $result = Invoke-IDRestMethod -Uri $URI -Method GET

        if ($null -ne $result) {

            Get-PagedResult -InitialResult $result -URI $URI -Style Cursor -ResultProperty response -CursorRequestKey nextToken -CursorResponseKey nextToken

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Get-SCAJobStatus {
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [Alias('job_id')]
        [String]$jobId
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/integrations/status"

        $URI = Add-QueryString -URI $URI -Parameter ($PSBoundParameters | Get-Parameter) -SupportsDebug

        #Send Request
        $result = Invoke-IDRestMethod -Uri $URI -Method GET

        if ($null -ne $result) {

            $result

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Get-SCAModuleData {
    [CmdletBinding()]
    param()

    begin { }#begin

    process {

        #Calculate the time elapsed since the start of the session and include in return data
        if ($null -ne $ISPSSSession.StartTime) {
            $ISPSSSession.ElapsedTime = '{0:HH:mm:ss}' -f ([datetime]$($(Get-Date) - $($ISPSSSession.StartTime)).Ticks)
        } else { $ISPSSSession.ElapsedTime = $null }

        #Deep Copy the $ISPSSSession session object and return as IdCmd Session type.
        Get-SessionClone -InputObject $ISPSSSession | Add-CustomType -Type IdCmd.Session

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Get-SCAOnDemandConfig {
    [CmdletBinding()]
    param()

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/on-demand/config"

        #Send Request
        $result = Invoke-IDRestMethod -Uri $URI -Method GET

        if ($null -ne $result) {

            $result

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Get-SCAPolicy {
    [CmdletBinding(DefaultParameterSetName = 'List')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'ById'
        )]
        [Alias('policyId')]
        [String]$policy_id,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'List'
        )]
        [ValidateSet('Active', 'Expired', 'Error', 'Validating')]
        [String]$status,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'List'
        )]
        [String]$free_text,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'List'
        )]
        [ValidateSet('AWS', 'GCP', 'AZURE', 'AWS_IDC', 'AZURE_ENTRA_ID')]
        [String]$cloud_provider
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/policies"

        if ($PSCmdlet.ParameterSetName -eq 'ById') {

            $URI = Add-QueryString -URI "$URI/$policy_id" -SupportsDebug

        } else {

            $boundParameters = $PSBoundParameters | Get-Parameter

            #The API filters on the numeric value of these fields
            if ($PSBoundParameters.ContainsKey('status')) {
                $boundParameters['status'] = ConvertTo-SCAApiValue -Name PolicyStatus -Value $status
            }

            if ($PSBoundParameters.ContainsKey('cloud_provider')) {
                $boundParameters['cloud_provider'] = ConvertTo-SCAApiValue -Name CloudProvider -Value $cloud_provider
            }

            $URI = Add-QueryString -URI $URI -Parameter $boundParameters -SupportsDebug

        }

        #Send Request
        $result = Invoke-IDRestMethod -Uri $URI -Method GET -Headers $(Get-SCAPolicyApiHeader)

        if ($null -ne $result) {

            if ($PSCmdlet.ParameterSetName -eq 'ById') { $result }
            else { $result.hits }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Get-SCAPolicyStatus {
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [Alias('policyId')]
        [String]$policy_id
    )

    begin { }#begin

    process {

        #The API defines policy_id as both a path and a query parameter for this operation
        $URI = Add-QueryString -URI "$($ISPSSSession.tenant_url)/api/policies/$policy_id/status" -Parameter @{ 'policy_id' = $policy_id }

        #Send Request
        $result = Invoke-IDRestMethod -Uri $URI -Method GET

        if ($null -ne $result) {

            #The API returns the status as a bare integer - report what it means alongside it
            $Status = ConvertFrom-SCAApiValue -Name PolicyStatus -Value $result

            [pscustomobject]@{
                policyId    = $policy_id
                status      = $Status.Value
                statusName  = $Status.Name
                description = $Status.Description
            } | Add-CustomType -Type IdCmd.SCA.Policy.Status

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Get-SCASession {
    [CmdletBinding(DefaultParameterSetName = 'All')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'ByUser'
        )]
        [String]$userId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AWS', 'AZURE', 'GCP')]
        [String]$csp,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateRange(1, 50)]
        [int]$limit
    )

    begin { }#begin

    process {

        $URI = if ($PSCmdlet.ParameterSetName -eq 'ByUser') {
            "$($ISPSSSession.tenant_url)/api/access/users/$userId/sessions"
        } else {
            "$($ISPSSSession.tenant_url)/api/access/sessions"
        }

        $URI = Add-QueryString -URI $URI -Parameter ($PSBoundParameters | Get-Parameter -ParametersToRemove userId)

        #Send Request
        $result = Invoke-IDRestMethod -Uri $URI -Method GET

        if ($null -ne $result) {

            Get-PagedResult -InitialResult $result -URI $URI -Style Cursor -ResultProperty response -CursorRequestKey nextToken -CursorResponseKey nextToken

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function New-SCAAccessTargetDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change state')]
    [OutputType('IdCmd.SCA.Definition.Access.Target')]
    [CmdletBinding(DefaultParameterSetName = 'ByRoleId')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'ByRoleId'
        )]
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'ByRoleName'
        )]
        [String]$workspaceId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'ByRoleId'
        )]
        [String]$roleId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'ByRoleName'
        )]
        [String]$roleName,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $false,
            HelpMessage = 'Accepts previous output from New-SCAAccessTargetDefinition, to which this target is appended.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Access.Target')]
        [psobject[]]$Definition
    )

    begin {

        $ExpectedProperties = @('workspaceId', 'roleId', 'roleName')

    }#begin

    process {

        $boundParameters = $PSBoundParameters | Get-Parameter -ParametersToRemove Definition

        $Target = Select-RequestProperty -Property $ExpectedProperties -BoundParameter $boundParameters

        if ($PSBoundParameters.ContainsKey('Definition')) { $Definition }

        $Target | Add-CustomType -Type IdCmd.SCA.Definition.Access.Target

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function New-SCAPolicy {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AWS', 'GCP', 'AZURE')]
        [String]$csp,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 200)]
        [ValidateScript({ Test-SCAPolicyText -Value $PSItem -ParameterName name -Pattern $(Get-SCAPolicyTextPattern -Name Name) })]
        [String]$name,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(0, 200)]
        [ValidateScript({ Test-SCAPolicyText -Value $PSItem -ParameterName description -Pattern $(Get-SCAPolicyTextPattern -Name Description) })]
        [String]$description,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [datetime]$startDate,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [datetime]$endDate,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            HelpMessage = 'Accepts object output from New-SCAPolicyRoleDefinition.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Policy.Role')]
        [psobject[]]$roles,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            HelpMessage = 'Accepts object output from New-SCAPolicyIdentityDefinition.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Policy.Identity')]
        [psobject[]]$identities,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            HelpMessage = 'Accepts object output from New-SCAPolicyAccessRuleDefinition.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Policy.AccessRule')]
        [psobject]$accessRules
    )

    begin {

        $ExpectedProperties = @('csp', 'name', 'description', 'startDate', 'endDate', 'roles', 'identities', 'accessRules')

    }#begin

    process {

        $URI = Add-QueryString -URI "$($ISPSSSession.tenant_url)/api/policies/create-policy" -SupportsDebug

        #Get request parameters
        $boundParameters = $PSBoundParameters | Get-Parameter

        #The API expects ISO format dates
        foreach ($dateParam in 'startDate', 'endDate') {
            if ($PSBoundParameters.ContainsKey($dateParam)) {
                $boundParameters[$dateParam] = ConvertTo-DateString -Date $PSBoundParameters[$dateParam]
            }
        }

        #Project supplied parameters onto the expected request properties
        $Properties = Select-RequestProperty -Property $ExpectedProperties -BoundParameter $boundParameters

        #Create Request Body
        $body = ConvertTo-JsonBody -Body $Properties

        if ($PSCmdlet.ShouldProcess($name, 'Create New SCA Policy')) {

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method POST -Body $body -Headers $(Get-SCAPolicyApiHeader)

            if ($null -ne $result) {

                #Report the status of the job started by the request
                $result | Resolve-SCAJobStatus

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function New-SCAPolicyAccessRuleDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change state')]
    [OutputType('IdCmd.SCA.Definition.Policy.AccessRule')]
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday')]
        [String[]]$days,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidatePattern('^([01]\d|2[0-3]):([0-5]\d)(:([0-5]\d))?$')]
        [String]$fromTime,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidatePattern('^([01]\d|2[0-3]):([0-5]\d)(:([0-5]\d))?$')]
        [String]$toTime,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateRange(1, 24)]
        [int]$maxSessionDuration,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$timeZone
    )

    begin {

        $ExpectedProperties = @('days', 'fromTime', 'toTime', 'maxSessionDuration', 'timeZone')

    }#begin

    process {

        $boundParameters = $PSBoundParameters | Get-Parameter

        $AccessRule = Select-RequestProperty -Property $ExpectedProperties -BoundParameter $boundParameters

        #The API expects an array of day names, which a single supplied day must not collapse to a string.
        $AccessRule['days'] = @($days)

        $AccessRule | Add-CustomType -Type IdCmd.SCA.Definition.Policy.AccessRule

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function New-SCAPolicyIdentityDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change state')]
    [OutputType('IdCmd.SCA.Definition.Policy.Identity')]
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$entityName,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$entitySourceId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('user', 'role', 'group')]
        [String]$entityClass,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $false,
            HelpMessage = 'Accepts previous output from New-SCAPolicyIdentityDefinition, to which this identity is appended.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Policy.Identity')]
        [psobject[]]$Definition
    )

    begin {

        $ExpectedProperties = @('entityName', 'entitySourceId', 'entityClass')

    }#begin

    process {

        $boundParameters = $PSBoundParameters | Get-Parameter -ParametersToRemove Definition

        $Identity = Select-RequestProperty -Property $ExpectedProperties -BoundParameter $boundParameters

        if ($PSBoundParameters.ContainsKey('Definition')) { $Definition }

        $Identity | Add-CustomType -Type IdCmd.SCA.Definition.Policy.Identity

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function New-SCAPolicyRoleDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change state')]
    [OutputType('IdCmd.SCA.Definition.Policy.Role')]
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$entityId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$entitySourceId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$workspaceType,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$organizationId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $false,
            HelpMessage = 'Accepts previous output from New-SCAPolicyRoleDefinition, to which this role is appended.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Policy.Role')]
        [psobject[]]$Definition
    )

    begin {

        $ExpectedProperties = @('entityId', 'workspaceType', 'entitySourceId', 'organizationId')

    }#begin

    process {

        $boundParameters = $PSBoundParameters | Get-Parameter -ParametersToRemove Definition

        $Role = Select-RequestProperty -Property $ExpectedProperties -BoundParameter $boundParameters

        if ($PSBoundParameters.ContainsKey('Definition')) { $Definition }

        $Role | Add-CustomType -Type IdCmd.SCA.Definition.Policy.Role

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function New-SCAScanEntityDefinition {
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Function does not change state')]
    [OutputType('IdCmd.SCA.Definition.Scan.EntityId')]
    [CmdletBinding()]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$account_id,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$org_id,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $false,
            HelpMessage = 'Accepts previous output from New-SCAScanEntityDefinition, to which this entity is appended.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Scan.EntityId')]
        [psobject[]]$Definition
    )

    begin {

        $ExpectedProperties = @('org_id', 'account_id')

    }#begin

    process {

        $boundParameters = $PSBoundParameters | Get-Parameter -ParametersToRemove Definition

        $Entity = Select-RequestProperty -Property $ExpectedProperties -BoundParameter $boundParameters

        if ($PSBoundParameters.ContainsKey('Definition')) { $Definition }

        $Entity | Add-CustomType -Type IdCmd.SCA.Definition.Scan.EntityId

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function New-SCAWebApp {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AWS IAM', 'AWS IdC', 'Azure', 'GCP')]
        [String]$appType,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$appName,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$workspaceId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [bool]$establishAutoTrust,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [bool]$useIdentityAsIdp
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/onboarding/apps"

        $boundParameters = $PSBoundParameters | Get-Parameter

        #The workspace details are sent as a nested appMetadata object
        $Properties = Select-RequestProperty -Property @('appType', 'appName') -BoundParameter $boundParameters
        $Properties['appMetadata'] = Select-RequestProperty -Property @('workspaceId', 'establishAutoTrust', 'useIdentityAsIdp') -BoundParameter $boundParameters

        #Create Request Body
        $body = ConvertTo-JsonBody -Body $Properties

        if ($PSCmdlet.ShouldProcess($appName, 'Create SCA Web App')) {

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method POST -Body $body

            if ($null -ne $result) {

                #Report the status of the job started by the request
                $result | Resolve-SCAJobStatus

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Remove-SCAPolicy {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [Alias('policyId')]
        [String]$policy_id
    )

    begin { }#begin

    process {

        $URI = Add-QueryString -URI "$($ISPSSSession.tenant_url)/api/policies/$policy_id" -SupportsDebug

        if ($PSCmdlet.ShouldProcess($policy_id, 'Remove SCA Policy')) {

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method DELETE -Headers $(Get-SCAPolicyApiHeader)

            if ($null -ne $result) {

                $result

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Request-SCAAccess {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AWS', 'AZURE', 'GCP')]
        [String]$csp,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            HelpMessage = 'Accepts object output from New-SCAAccessTargetDefinition.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Access.Target')]
        [ValidateCount(1, 5)]
        [psobject[]]$targets,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$organizationId
    )

    begin {

        $ExpectedProperties = @('organizationId', 'csp', 'targets')

    }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/access/elevate"

        #Project supplied parameters onto the expected request properties
        $Properties = Select-RequestProperty -Property $ExpectedProperties -BoundParameter ($PSBoundParameters | Get-Parameter)

        #Create Request Body
        $body = ConvertTo-JsonBody -Body $Properties

        if ($PSCmdlet.ShouldProcess("$csp targets", 'Elevate Access')) {

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method POST -Body $body

            if ($null -ne $result) {

                $result.response

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Request-SCAGroupMembership {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$directoryId,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateCount(1, 5)]
        [String[]]$groupId,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AZURE')]
        [String]$csp = 'AZURE'
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/access/elevate/groups"

        $Properties = [ordered]@{
            'directoryId' = $directoryId
            'csp'         = $csp
            'targets'     = @($groupId | ForEach-Object { @{ 'groupId' = $PSItem } })
        }

        #Create Request Body
        $body = ConvertTo-JsonBody -Body $Properties

        if ($PSCmdlet.ShouldProcess($($groupId -join ', '), 'Request Just-In-Time Group Membership')) {

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method POST -Body $body

            if ($null -ne $result) {

                $result

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Revoke-SCASession {
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'BySessionId')]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'BySessionId'
        )]
        [Alias('sessionId')]
        [ValidateCount(1, 100)]
        [String[]]$sessionIds,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true,
            ParameterSetName = 'ByUser'
        )]
        [String]$userId
    )

    begin { }#begin

    process {

        if ($PSCmdlet.ParameterSetName -eq 'ByUser') {

            $URI = "$($ISPSSSession.tenant_url)/api/access/users/$userId/sessions/revoke"
            $Target = $userId
            $body = $null

        } else {

            $URI = "$($ISPSSSession.tenant_url)/api/access/sessions/revoke"
            $Target = $sessionIds -join ', '

            #Create Request Body
            $body = ConvertTo-JsonBody -Body @{ 'sessionIds' = @($sessionIds) }

        }

        if ($PSCmdlet.ShouldProcess($Target, 'Revoke SCA Session')) {

            #Send Request
            $result = if ($null -eq $body) {
                Invoke-IDRestMethod -Uri $URI -Method POST
            } else {
                Invoke-IDRestMethod -Uri $URI -Method POST -Body $body
            }

            if ($null -ne $result) {

                $result.response

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Set-SCAOnDemandConfig {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('In-platform', 'External')]
        [String]$ApprovalChannel
    )

    begin { }#begin

    process {

        $URI = "$($ISPSSSession.tenant_url)/api/on-demand/config"

        #Create Request Body
        $body = ConvertTo-JsonBody -Body ($PSBoundParameters | Get-Parameter)

        if ($PSCmdlet.ShouldProcess($ApprovalChannel, 'Set SCA On-Demand Approval Channel')) {

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method PATCH -Body $body

            if ($null -ne $result) {

                $result

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Set-SCAPolicy {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [Alias('policyId')]
        [String]$policy_id,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(1, 200)]
        [ValidateScript({ Test-SCAPolicyText -Value $PSItem -ParameterName name -Pattern $(Get-SCAPolicyTextPattern -Name Name) })]
        [String]$name,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateLength(0, 200)]
        [ValidateScript({ Test-SCAPolicyText -Value $PSItem -ParameterName description -Pattern $(Get-SCAPolicyTextPattern -Name Description) })]
        [String]$description,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [datetime]$startDate,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [datetime]$endDate,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            HelpMessage = 'Accepts object output from New-SCAPolicyRoleDefinition.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Policy.Role')]
        [psobject[]]$roles,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            HelpMessage = 'Accepts object output from New-SCAPolicyIdentityDefinition.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Policy.Identity')]
        [psobject[]]$identities,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            HelpMessage = 'Accepts object output from New-SCAPolicyAccessRuleDefinition.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Policy.AccessRule')]
        [psobject]$accessRules
    )

    begin {

        $OrderedProperties = [ordered]@{
            'name'        = $null
            'description' = $null
            'startDate'   = $null
            'endDate'     = $null
            'roles'       = @()
            'identities'  = @()
            'accessRules' = $null
        }

    }#begin

    process {

        $URI = Add-QueryString -URI "$($ISPSSSession.tenant_url)/api/policies/$policy_id" -SupportsDebug

        #Get existing policy settings
        $PolicySettings = Get-SCAPolicy -policy_id $policy_id

        #Get request parameters
        $boundParameters = $PSBoundParameters | Get-Parameter -ParametersToRemove policy_id

        #The API expects ISO format dates
        foreach ($dateParam in 'startDate', 'endDate') {
            if ($PSBoundParameters.ContainsKey($dateParam)) {
                $boundParameters[$dateParam] = ConvertTo-DateString -Date $PSBoundParameters[$dateParam]
            }
        }

        #Project supplied parameters onto the request template, falling back to the existing policy
        $Properties = Merge-Parameter -Template $OrderedProperties -BoundParameter $boundParameters -Fallback $PolicySettings

        #Create Request Body
        $body = ConvertTo-JsonBody -Body $Properties

        if ($PSCmdlet.ShouldProcess($policy_id, 'Update SCA Policy')) {

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method PUT -Body $body -Headers $(Get-SCAPolicyApiHeader)

            if ($null -ne $result) {

                #Report the status of the job started by the request
                $result | Resolve-SCAJobStatus

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Start-SCADiscovery {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AWS', 'GCP', 'AZURE')]
        [String]$csp,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$organization_id,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [String]$id,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true
        )]
        [bool]$new_account
    )

    begin { }#begin

    process {

        $URI = Add-QueryString -URI "$($ISPSSSession.tenant_url)/api/policies/discovery" -SupportsDebug

        $boundParameters = $PSBoundParameters | Get-Parameter

        #The workspace to discover is sent as a nested account_info object
        $Properties = Select-RequestProperty -Property @('csp', 'organization_id') -BoundParameter $boundParameters
        $Properties['account_info'] = Select-RequestProperty -Property @('id', 'new_account') -BoundParameter $boundParameters

        #Create Request Body
        $body = ConvertTo-JsonBody -Body $Properties

        if ($PSCmdlet.ShouldProcess($id, 'Start SCA Workspace Discovery')) {

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method POST -Body $body

            if ($null -ne $result) {

                $result

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Start-SCAScan {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('AWS', 'GCP', 'AZURE')]
        [String]$cloudProvider,

        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [ValidateSet('All', 'Specific')]
        [String]$accountType,

        [parameter(
            Mandatory = $false,
            ValueFromPipelinebyPropertyName = $true,
            HelpMessage = 'Accepts object output from New-SCAScanEntityDefinition.'
        )]
        [PSTypeName('IdCmd.SCA.Definition.Scan.EntityId')]
        [psobject[]]$entityIds
    )

    begin {

        $ExpectedProperties = @('cloudProvider', 'accountType', 'entityIds')

    }#begin

    process {

        $URI = Add-QueryString -URI "$($ISPSSSession.tenant_url)/api/cloud/rescan" -SupportsDebug

        $boundParameters = $PSBoundParameters | Get-Parameter

        #The API expects the numeric value of the cloud provider
        $boundParameters['cloudProvider'] = ConvertTo-SCAApiValue -Name CloudProvider -Value $cloudProvider

        #Project supplied parameters onto the expected request properties
        $Properties = Select-RequestProperty -Property $ExpectedProperties -BoundParameter $boundParameters

        #Create Request Body
        $body = ConvertTo-JsonBody -Body $Properties

        if ($PSCmdlet.ShouldProcess("$cloudProvider ($accountType)", 'Start SCA Cloud Scan')) {

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method POST -Body $body

            if ($null -ne $result) {

                $result

            }

        }

    }#process

    end { }#end

}

# .ExternalHelp IdentityCommand.SCA-help.xml
function Test-SCAPolicy {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(
            Mandatory = $true,
            ValueFromPipelinebyPropertyName = $true
        )]
        [Alias('policy_id')]
        [String]$policyId
    )

    begin { }#begin

    process {

        $URI = Add-QueryString -URI "$($ISPSSSession.tenant_url)/api/policies/validate" -SupportsDebug

        #Create Request Body
        $body = ConvertTo-JsonBody -Body @{ 'policyId' = $policyId }

        if ($PSCmdlet.ShouldProcess($policyId, 'Validate SCA Policy')) {

            #Send Request
            $result = Invoke-IDRestMethod -Uri $URI -Method POST -Body $body

            if ($null -ne $result) {

                #Report the status of the job started by the request
                $result | Resolve-SCAJobStatus

            }

        }

    }#process

    end { }#end

}

#Copy IdentityCommand's private helpers into this module: this module's functions call them, and
#the argument completer registrations below do so at import time.
#Each copy is created from the function definition, so it runs in this module's scope and uses this
#module's $ISPSSSession, whether IdentityCommand loaded from source or from its combined psm1.
#Resolve a single IdentityCommand module: with more than one version loaded, Get-Module returns
#an array.
$Module = Get-Module -Name IdentityCommand | Sort-Object Version -Descending | Select-Object -First 1

if ($null -eq $Module) {
    throw 'The IdentityCommand module is not loaded. Import IdentityCommand and try again.'
}

& $Module { Get-ChildItem -Path Function: } |

    Where-Object { $_.ModuleName -eq $Module.Name -and -not $Module.ExportedFunctions.ContainsKey($_.Name) } |

    ForEach-Object {

        . ([scriptblock]::Create("function $($_.Name) {$($_.Definition)}"))

    }

#region Registration

$SCAPolicyIdCompleter = Get-ArgumentCompleter -RetrievalCommand 'Get-SCAPolicy' -ValueProperty 'policyId' -LabelProperty 'name'

Register-ArgumentCompleter -ParameterName 'policy_id' -ScriptBlock $SCAPolicyIdCompleter -CommandName @(
    'Get-SCAPolicy'
    'Set-SCAPolicy'
    'Remove-SCAPolicy'
    'Get-SCAPolicyStatus'
)

#Test-SCAPolicy takes the policy id under the API's body field name.
Register-ArgumentCompleter -ParameterName 'policyId' -ScriptBlock $SCAPolicyIdCompleter -CommandName 'Test-SCAPolicy'

Register-ArgumentCompleter -ParameterName 'sessionIds' -ScriptBlock (
    Get-ArgumentCompleter -RetrievalCommand 'Get-SCASession' -ValueProperty 'sessionId' -LabelProperty 'userId'
) -CommandName 'Revoke-SCASession'

#The workspace types the API supports differ by cloud provider, so these are offered as completions
#rather than enforced with a ValidateSet.
$SCAWorkspaceType = [ordered]@{
    'account'          = 'AWS - required only for IAM Identity Center'
    'directory'        = 'Azure - Microsoft Entra ID directory'
    'management_group' = 'Azure - management group'
    'subscription'     = 'Azure - subscription'
    'resource_group'   = 'Azure - resource group'
    'resource'         = 'Azure - resource'
    'gcp_organization' = 'Google Cloud - organization'
    'folder'           = 'Google Cloud - folder'
    'project'          = 'Google Cloud - project'
}

Register-ArgumentCompleter -ParameterName 'workspaceType' -CommandName 'New-SCAPolicyRoleDefinition' -ScriptBlock {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    #Standard ArgumentCompleter parameters that are not otherwise referenced.
    $null = $commandName, $parameterName, $commandAst, $fakeBoundParameters

    $Word = "$wordToComplete".Trim("'`"")

    $SCAWorkspaceType.Keys | Where-Object { $PSItem -like "$Word*" } | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($PSItem, $PSItem, 'ParameterValue', $SCAWorkspaceType[$PSItem])
    }

}.GetNewClosure()

#endregion

# Script scope session object for session data
$ISPSSSession = [ordered]@{
    tenant_url         = $null
    User               = $null
    TenantId           = $null
    SessionId          = $null
    WebSession         = $null
    StartTime          = $null
    ElapsedTime        = $null
    LastCommand        = $null
    LastCommandTime    = $null
    LastCommandResults = $null
    LastError          = $null
    LastErrorTime      = $null
} | Add-CustomType -Type IdCmd.Session

New-Variable -Name ISPSSSession -Value $ISPSSSession -Scope Script -Force