Private/Invoke-S1Query.ps1
function Invoke-S1Query { <# .SYNOPSIS Handles the request/response aspect of interacting with the SentinelOne API. .DESCRIPTION Handles the request/response aspect of interacting with the SentinelOne API, including pagination and error handling .EXAMPLE Invoke-S1Query -URI "/web/api/v2.1/agents" -Parameters @{computerName__contains = "hostname"} -Method GET #> [CmdletBinding(DefaultParameterSetName="Default")] Param( # The API URI from the SentinelOne API Documentation, i.e. "/web/api/v2.1/agents" [Parameter(Mandatory=$True)] [String] $URI, # Hashtable containing the query string parameters used for filtering the results [Parameter(Mandatory=$False)] [Hashtable] $Parameters, # Content type of the body, if necessary, i.e. "application/json" [Parameter(Mandatory=$False)] [String] $ContentType, # Rest method for the query. [Parameter(Mandatory=$False)] [ValidateSet("Get", "Post", "Put", "Delete")] [String] $Method = "Get", # Used to limit the number of results in the response, if supported by the specific API [Parameter(Mandatory=$False,ParameterSetName="Count")] [Uint32] $Count, # Specify the maximum number of results allowed by the API. This value differs between APIs with a default of 100 [Parameter(Mandatory=$False)] [Uint32] $MaxCount=100, # Used to follow the cursor in paginated requests to retrieve all possible results [Parameter(Mandatory=$False,ParameterSetName="Recurse")] [Switch] $Recurse, # The body value for a POST or PUT request [Parameter(Mandatory=$False)] $Body ) if ($URI -notlike "*/users/login") { # Log the function and parameters being executed $InitializationLog = $MyInvocation.MyCommand.Name $MyInvocation.BoundParameters.GetEnumerator() | ForEach-Object { $InitializationLog = $InitializationLog + " -$($_.Key) $($_.Value)" } Write-Log -Message $InitializationLog -Level Verbose # Attempt to retrieve cached configuration if ((-not $Script:PSSentinelOne.ApiToken -and -not $Script:PSSentinelOne.TemporaryToken) -or -not $Script:PSSentinelOne.ManagementURL) { Write-Log -Message "PS-SentinelOne Module Configuration not cached. Loading information from disk." -Level Verbose Get-S1ModuleConfiguration -Persisted -Cache } } # If no management URL is known, notify the user and exit if (-not $Script:PSSentinelOne.ManagementURL) { Write-Log -Message "Please use Set-S1ModuleConfiguration to save your management URL." -Level Error return } # If no token is present and not authenticating, notify the user and exit if (-not $Script:PSSentinelOne.ApiToken -and -not $Script:PSSentinelOne.TemporaryToken -and -not $URI -eq "/web/api/v2.0/users/login") { Write-Log -Message "Please use Set-S1ModuleConfiguration to save your APIToken, or use Get-S1Token to authenticate and generate a temporary token." -Level Error return } # Start building request $Request = @{} $Request.Add("Method", $Method) $Request.Add("ErrorVariable", "RestError") if ($Script:PSSentinelOne.Proxy) { $Request.Add("Proxy", $Script:PSSentinelOne.Proxy) } if ($ContentType) { $Request.Add("ContentType", $ContentType) } if ($Body) { $Request.Add("Body", $Body) } # Build request headers and add to request $Headers = @{} if ($Script:PSSentinelOne.ApiToken) { $ApiToken = Unprotect-S1Token -String $Script:PSSentinelOne.ApiToken $Headers.Add("Authorization", "ApiToken $ApiToken") } elseif ($Script:PSSentinelOne.TemporaryToken) { $TemporaryToken = Unprotect-S1Token -String $Script:PSSentinelOne.TemporaryToken $Headers.Add("Authorization", "Token $TemporaryToken") } $Request.Add("Headers", $Headers) # Start building request URI $URIBuilder = [System.UriBuilder]"$($Script:PSSentinelOne.ManagementURL.Trim("/"), $URI.Trim("/") -join "/")" $QueryString = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) # Add any parameters supplied with -Parameters switch to Query String if ($Parameters.Count -gt 0) { $Parameters.GetEnumerator() | ForEach-Object { $QueryString.Add($_.Name, $_.Value) } } # Process result limit if ($Count) { if ($Count -lt $MaxCount) { $QueryString.Add("limit", $Count) $Count = $Count - $Count } else { $QueryString.Add("limit", $MaxCount) $Count = $Count - $MaxCount } } if ($Recurse -and $QueryString -notcontains "limit") { $QueryString.Add("limit", $MaxCount) } # Add querystring to URI if ($QueryString.Count -gt 0) { $URIBuilder.Query = $QueryString.ToString() } # Add URI to request $Request.Add("URI", $URIBuilder.Uri.OriginalString) # Send request Try { Write-Log -Message "[$Method] $($URIBuilder.Uri.OriginalString)" -Level Verbose $Response = Invoke-RestMethod @Request } Catch { Write-Log -Message $RestError.InnerException.Message -Level Warning Write-Log -Message $RestError.Message -Level Warning Throw } if ($Parameters.countOnly) { return $Response.pagination.totalItems } else { Write-Output $Response } # Recurse through all results using the pagination cursor if ($Recurse) { while ($Response.pagination.nextCursor) { $URIBuilder = [System.UriBuilder]"$($Script:PSSentinelOne.ManagementURL.Trim("/"), $URI.Trim("/") -join "/")" $QueryString.Add("cursor", $Response.pagination.nextCursor) $URIBuilder.Query = $QueryString.ToString() $Request.URI = $URIBuilder.Uri.OriginalString Write-Log -Message "[$Method] $($URIBuilder.Uri.OriginalString)" -Level Verbose $Response = Invoke-RestMethod @Request Write-Output $Response $QueryString.Remove("cursor") } } # Recurse through results until requested count is met. This could result in too many results, the commandlets should deal with returning exact numbers if ($Count) { while ($Count -gt 0 -and $Response.pagination.nextCursor) { $URIBuilder = [System.UriBuilder]"$($Script:PSSentinelOne.ManagementURL.Trim("/"), $URI.Trim("/") -join "/")" $QueryString.Add("cursor", $Response.pagination.nextCursor) $URIBuilder.Query = $QueryString.ToString() $Request.URI = $URIBuilder.Uri.OriginalString Write-Log -Message "[$Method] $($URIBuilder.Uri.OriginalString)" -Level Verbose $Response = Invoke-RestMethod @Request Write-Output $Response if ($Count -lt $MaxCount) { $Count = $Count - $Count } else { $Count = $Count - $MaxCount } $QueryString.Remove("cursor") } } } |