CyAPI.ps1

<#
.NAME
    Cylance-API
 
.SYNOPSIS
    A collection of verbs to work with the Cylance Console API v2
 
.DESCRIPTION
    Allows retrieval and manipulation of configuration objects in the Cylance console using API v2.
 
.LINK
    Blog: http://tietze.io/
    Jan Tietze
#>


<#
    Represents the API handle returned by API after authentication
#>

Class CylanceAPIHandle {
    [string]$AccessToken
    [string]$BaseUrl
}

<#
.TODO
    At some point In the future, artifacts for the API will be classes
#>


Class CylanceDevice {
    [string]$Id
}
Class CylanceZone {
    [string]$Id
}
Class CylanceThreat {
    [string]$Id
}

<#
.SYNOPSIS
    Gets an API access token for the authenticated access to the Console API, valid for 30 minutes.
 
.PARAMETER APIId
    Optional. API ID
 
.PARAMETER APISecret
    Optional. API Secret
 
.PARAMETER APITenantId
    Optional. API Tenant ID
 
.PARAMETER APIAuthUrl
    Optional. URL to obtain token, e.g. "https://protectapi<-region>.cylance.com/auth/v2/token". Defaults to EUC1 region.
    Use the value obtained from the API documentation for your console shard.
 
.PARAMETER Scope
    Optional. If you need to access multiple tenants in parallel, use "None" as scope and collect the API object returned.
 
.PARAMETER Console
    Optional. The console ID in your consoles.json file. See the README for CyCLI module.
#>

function Get-CyAPI {
    Param (
        [parameter(Mandatory=$true, ParameterSetName="Direct")]
        [String]$APIId,
        [parameter(Mandatory=$true, ParameterSetName="Direct")]
        [SecureString]$APISecret,
        [parameter(Mandatory=$true, ParameterSetName="Direct")]
        [String]$APITenantId,
        [parameter(Mandatory=$false, ParameterSetName="Direct")]
        [String]$APIAuthUrl = "https://protectapi-euc1.cylance.com/auth/v2/token",
        [parameter(Mandatory=$false)]
        [ValidateSet ("Session", "None")]
        [String]$Scope = "Session"
        )
    DynamicParam {
        Get-CyConsoleArgumentAutoCompleter -Mandatory -ParameterName "Console" -ParameterSetName "ByReference"
    }

    Begin {
        switch ($PSCmdlet.ParameterSetName)
        {
            "Direct"
            {
                # decrypt DPAPI protected string into SecureString
                # https://social.technet.microsoft.com/wiki/contents/articles/4546.working-with-passwords-secure-strings-and-credentials-in-windows-powershell.aspx
                $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($APISecret)
                $pw = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)

                $claims = @{}

                $jwtBearerToken = Get-JWTToken `
                    -claims $claims `
                    -expirationSeconds 1800 `
                    -secret $pw `
                    -iss "http://cylance.com" `
                    -tid $APITenantId `
                    -APIid $APIId

                $payload = @{ "auth_token" = $jwtBearerToken } | ConvertTo-Json

                try {
                   $headers = @{
                        "Accept" = "*/*"
                        }
                    $result = Invoke-RestMethod -Method Post -Uri $APIAuthUrl -ContentType "application/json; charset=utf-8" -UserAgent "" -Body $payload -Headers $headers
                }
                catch {
                    $result = $_.Exception.Response.GetResponseStream()
                    $reader = New-Object System.IO.StreamReader($result)
                    $reader.BaseStream.Position = 0
                    $reader.DiscardBufferedData()
                    Write-Error "Could not obtain valid API token. This may mean that (a) your API credentials are incorrect or (b) your API auth URL is incorrect (use Get-Help Get-CyAPI to get a list of URLs), and your code is attempting to authenticate to the wrong global API instance."
                    if ($Scope -eq "None") {
                        $script:GlobalCyAPIHandle = $null
                    }
                    throw $_.Exception
                    return
                }

                $baseUrl = ([System.Uri]$APIAuthUrl).Scheme + "://" + ([System.Uri]$APIAuthUrl).Host

                [CylanceAPIHandle]$r = New-Object CylanceAPIHandle
                $r.AccessToken = $result.access_token
                $r.BaseUrl = $baseUrl

                if ($Scope -eq "Session") {
                    $script:GlobalCyAPIHandle = $r
                } else {
                    $r
                }
            }
        "ByReference" 
            {
                $ConsoleDetails = (Get-CyConsoleConfig) | Where-Object ConsoleId -eq $PSBoundParameters.Console

                # decrypt DPAPI protected string into SecureString
                # https://social.technet.microsoft.com/wiki/contents/articles/4546.working-with-passwords-secure-strings-and-credentials-in-windows-powershell.aspx
                $SecureStringPw = $ConsoleDetails.APISecret | ConvertTo-SecureString

                if ($null -ne $ConsoleDetails.APIUrl) { 
                    $APIAuthUrl = $ConsoleDetails.APIUrl
                }
                $args = @{
                    APIId = $ConsoleDetails.APIId
                    APISecret = $SecureStringPw
                    APITenantId = $ConsoleDetails.APITenantId
                    Scope = $Scope
                    APIAuthUrl = $APIAuthUrl
                }
                Get-CyAPI @args
            }
        }
    }

    Process {
    }
}

<#
.SYNOPSIS
    Gets ALL pages for paged query results with maximum page size.
 
.PARAMETER API
    Optional. API Handle (use only when not using session scope).
 
.PARAMETER QueryParams
    Optional. If you need to add any query parameters, supply them in a Hashtable.
#>

function Read-CyData {
    Param (
        [parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [CylanceAPIHandle]$API,
        [parameter(Mandatory=$true)]
        [string]$Uri,
        [parameter(Mandatory=$false)]
        [Hashtable]$QueryParams = @{}
        )

    $auth = "Bearer " + $API.AccessToken

    $headers = @{
        "Accept" = "application/json"
        "Authorization" = $auth
    }

    $page = 1
    do {
        $params = @{
            "page" = $page
            "page_size" = 200
        }
        foreach ($key in $QueryParams.Keys) {
            $params.$key = $QueryParams.$key
        }

        foreach ($key in $params.Keys) {
            Write-Verbose "Read-CyData: GET ${Uri} | $($key) = $($params.$key)"
        }
        $resp = Invoke-RestMethod `
            -Method GET `
            -Uri $Uri `
            -Header $headers `
            -UserAgent "" -Body $params `

        $resp.page_items | foreach-object {
            $_ | Convert-CyObject
        }
        Write-Verbose "Response was page $($resp.page_number) of $($resp.total_pages) pages"

        $page++

    } while ($resp.page_number -lt $resp.total_pages)
}

<#
.SYNOPSIS
    Returns the currently active global CyAPIHandle, if one is set
#>

Function Get-CyAPIHandle {
    $GlobalCyAPIHandle
}


<#
.SYNOPSIS
    Converts a date string as returned from the API to a DateTime object
 
.PARAMETER Date
    The date string as returned by the API
#>

function Get-CyDateFromString {
    Param (
        [Parameter(Mandatory=$true, Position=1)]
        [String]$Date
    )
    # convert e.g. 2018-03-07T13:21:07 to Date
    # convert e.g. 2018-03-07T13:21:07.123 to Date (date_offline uses fractional seconds)
    Write-Verbose "Converting date $($Date) to [DateTime]"
    $dt = [DateTime]::ParseExact($Date, "yyyy-MM-ddTHH:mm:ss.FFFFFFF", [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal)
    Write-Verbose "Conversion result: $($dt)"
    return $dt
}

<#
.SYNOPSIS
    Converts all "date" strings received through the JSON API and turns them into "date" objects.
#>

function Convert-CyObject {
    Param (
        [Parameter(Mandatory=$true, Position=1, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [PSCustomObject]$CyObject
        )
    Begin {
        $fields = @("date_first_registered", "date_offline", "date_last_modified", "date_found", "cert_timestamp", "date_last_login", "date_email_confirmed", "date_created", "date_modified")
    }
    Process {
        foreach ($f in $fields) {
            try {
                if (($null -ne $CyObject.$f) -and ($CyObject.$f -isnot [DateTime])) {
                    Write-Verbose "Converting field $($f) (value: $($CyObject.$f)) to date time value"
                    $newval = Get-CyDateFromString $CyObject.$f
                    # I think we hit a bug in PowerShell. Previous code was $CyObject.$f = $newval.
                    # This would break on date conversion with a PropertyAssignmentException when called from Get-CyDeviceDetailByMac, but not when called by Get-CyDeviceDetail
                    $CyObject | Add-Member $f $newval -Force 
                    Write-Verbose "Conversion result for field $($f): $($CyObject.$f) $($newval)"
                }
            } catch [FormatException] {
                Write-Error "Problem converting field $($f) to date time (value: $($CyObject.$f))"
            }
        }

        $CyObject
    }
}