GenericAPICall

Integration version of the httpv2 automation used to send HTTP requests for API calls to endpoints not associated with an existing XSOAR integration.

Utilities · Generic API Call

Details

IDGenericAPICall
ProviderOpen Source
CategoryUtilities
From Version5.5.0
Docker Imagedemisto/python3:3.12.13.10404775
Supported ModulesAgentix XSIAM Cloud Posture Security

README

GenericAPICall Integration

This integration provides a method for executing HTTP API calls to specific API endpoints used in one-off scenarios not covered by other XSOAR integrations. This integration supports unauthenticated and authenticated API calls over HTTPS, using API key and basic HTTP authentication methods.

Configuration Parameters

  • Name - Integration instance name
  • Base Server URL - The base server URL for the API endpoint without the trailing slash (/)
  • Username - Username for HTTP basic authentication.
    • Leave blank when using an API key
  • Password/API Key - The password (when using HTTP basic authentication) or API key to use for API calls with this integration instance
  • API call is authenticated - Check this box if API calls to this endpoint require authentication (Default: False)
  • API key supplied in header - Check this box if API key or authentication credentials are provided as part of the HTTP header (Default: False)
    • Unchecking this box will pass the API key as a parameter in the URL (&apiKey=KEY_HERE)
  • Authentication Header - The value to use to identify the API key field as part of the HTTP header or API Key parameter in the URL
    • Header example: { ‘AUTHENICATION_HEADER_HERE’: ‘API_KEY_HERE’ }
    • Parameterized example: &AUTHENTICATION_HEADER=API_KEY_HERE
  • Use system proxy settings - Leverage the proxy settings configured on the XSOAR server
  • Trust any certificate (not secure) - Bypass SSL certificate verification for these connections

Commands

  1. Execute a HTTP-based API call to the configured API endpoint

generic-api-call

Performs a HTTP-based API call to the server configured server and url path.
NOTE: Because of the potentially large amount of data returned per specific API call, no content is output to the war room except a message confirming call execution.

Base Command

!generic-api-call

Input


Argument Name Description Required
method The HTTP method to use for the API call. (Default: POST) GET, POST, PUT, PATCH, DELETE, HEAD Required
urlpath The URL path to the API endpoint to be appended to the base_url parameter. (Default: /) Required
parse_response_as Specify the type of data expected in the API response

Defined types:
- json
- raw_response
- text
- xml
Required
body The body to be used for the API call Optional
request_content_type Specify the Content-Type header for the request.Shorthands are provided for the following common content types:json (application/json)xml (text/xml)form (application/x-www-form-urlencoded)data (multipart/form-data).

If you choose to define a different type, please include the full type name, e.g: application/pdf

Predefined types:
- application/json
- application/pdf
- application/x-www-form-urlencoded
- application/xml
- multipart/form-data
- text/csv
- text/plain
- text/xml
Optional
response_content_type Specify the Accept header for the request.Shorthands are provided for the following common content types:json (application/json)xml (text/xml)form (application/x-www-form-urlencoded)data (multipart/form-data).

If you choose to define a different type, please include the full type name, e.g: application/pdf

Predefined types:
- application/json
- application/pdf
- application/x-www-form-urlencoded
- application/xml
- multipart/form-data
- text/csv
- text/plain
- text/xml
Optional
params User-defined parameters for the API call Optional
save_as_file Save the API response as a file to context in XSOAR (Default: False) Optional
file_name The filename to save the returned API response as in XSOAR (Default: http-file) Optional
timeout Specify the timeout of the HTTP request in seconds. (Default: 10 seconds) Optional
timeout_between_retries Specify the timeout between each retry in seconds. (Default: 5 seconds) Optional
retry_count Specify the number or retries to be made in case of a call failure. (Default: 3 attempts) Optional
retry_on_status Specify a list of HTTP status codes that a retry should be attempted on. For example: 301-303,400,402.” Optional
enable_redirect The request will be called again with the new URL (Default: false) Optional

Context Output

The response data

Path Type Description
APICall.StatusCode String The HTTP status code returned from the request
APICall.StatusText String The text corresponding to the returned HTTP status code
APICall.URL String The URL of the API call
APICall.ParsedBody String The parsed response, formatted according to parse_response_as argument
APICall.Headers String The headers from the API call response
APICall.Body String The raw data returned from the API call

Command examples

All examples in this documentation are based off the following endpoints:

JSON: https[:]//api[.]apis[.]guru/v2/metrics.json
XML: https[:]//boardgamegeek[.]com/xmlapi/search?search=civilizations
JSON Request Body: https[:]//reqres[.]in/

Command example - JSON

API call with output to context - JSON Return

!generic-api-call urlpath=/metrics.json request_content_type=application/json response_content_type=application/json method=GET parse_response_as=json

Context Example

JSON Context Output - 1
JSON Context Output - 2

Human Readable Output

Sent a GET request to https://api.apis.guru/v2

Command example - XML

API call with output to context - XML Return

!generic-api-call urlpath=/xmlapi/search?search=civilizations method=GET request_content_type=application/xml response_content_type=application/xml parse_response_as=xml

Context Example

XML Context Output - 1
XML Context Output - 2
XML Context Output - 3

Human Readable Output

Sent a GET request to https://boardgamegeek.com/xmlapi

Command example - Saving output to file in war room/context

Saving output to file in context/war room

!generic-api-call urlpath=/metrics.json request_content_type=application/json response_content_type=application/json method=GET parse_response_as=json save_as_file=true

Context Example

File output - Context

Human Readable Output

File output - War Room

Command example - API Call with JSON Request Body

API call with output to context - JSON specified in request body

!generic-api-call response_content_type=application/json method=POST urlpath=/api/users parse_response_as=json body=`{"name": "xsoar", "job": "XSOAR Ninja"}` request_content_type=application/json

Output Example

JSON Request Body Output Example

Human Readable Output

Sent a POST request to https://reqres.in

Command example - ElasticSearch Cases API Call

As the current ElasticSearch integration does not support pulling case data, the Generic API Call integration can be used to perform such a data pull. In the below example, a generic API call is issued to pull back a list of cases in ElasticSearch tied to the Elastic Cases API. Below are screenshots showing how the integration is configured to authenticate to ElasticSearch.

Please make note of the way the API key and authorization header are specified. For the API key, Elastic uses the “Authorization” header with an API key formatted as “ApiKey ENCODED_KEY_HERE”. The “ApiKey ENCODED_KEY_HERE” text goes in the password field for the integration, while “Authorization” is specified in the “Authentication Header” integration instance configuration field. The USERNAME field is left blank in the integration configuration for a call to the ElasticSearch API endpoint.

GenericAPICall Integration Configuration for ElasticSearch Cases

API call with output to context - ElasticSearch Cases API

!generic-api-call method=GET parse_response_as=json enable_redirect=true urlpath=/_find"

Context Example

Elastic Generic API Call Context Output

Human Readable Output

Sent a GET request to https://X.X.X.X/api/cases

Configuration parameters

  • base_url — Base API Endpoint URL (https:// with no trailing slash) (required)
  • credentials — Username (Blank when using API key)
  • is_auth — API call is authenticated (Default: False)
  • apikey_in_header — API Key supplied in header
  • api_call_key — Authentication Header
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)

Commands (1)

  • generic-api-call

    Generic HTTPS request to a specified API endpoint.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401
import urllib3
import requests
import json
from typing import Any

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR

""" CLIENT CLASS """

CONTENT_TYPE_MAPPER = {
    "json": "application/json",
    "xml": "text/xml",
    "form": "application/x-www-form-urlencoded",
    "data": "multipart/form-data",
}

RAW_RESPONSE = "raw_response"


class Client(BaseClient):
    def __init__(self, base_url: str, auth, verify: bool, proxy: bool):
        super().__init__(base_url=base_url, auth=auth, verify=verify, proxy=proxy)

    def http_request(
        self,
        method: str,
        full_url: str = "",
        headers: dict = None,
        resp_type: str = RAW_RESPONSE,
        params: dict = None,
        data: str = None,
        timeout: int = 10,
        retries: int = 0,
        status_list_to_retry: list = None,
        raise_on_status: bool = False,
        allow_redirects: bool = True,
        backoff_factor: int = 5,
    ):
        try:
            res = self._http_request(
                method=method,
                full_url=full_url,
                headers=headers,
                params=params,
                timeout=timeout,
                resp_type=resp_type,
                status_list_to_retry=status_list_to_retry,
                raise_on_status=raise_on_status,
                retries=retries,
                data=data,
                error_handler=self._generic_error_handler,
                allow_redirects=allow_redirects,
                backoff_factor=backoff_factor,
            )
        except Exception as e:
            return_error(f"Failed to execute API call. Error: {str(e)}")
        return res

    @staticmethod
    def _generic_error_handler(res: requests.Response):
        status_code = res.status_code
        if status_code == 400:
            raise DemistoException(f"Bad request. Status code: {status_code}. Origin response from server: {res.text}")

        if status_code == 401:
            raise DemistoException(f"Unauthorized. Status code: {status_code}. Origin response from server: {res.text}")

        if status_code == 403:
            raise DemistoException(f"Invalid permissions. Status code: {status_code}. Origin response from server: {res.text}")

        if status_code == 404:
            raise DemistoException(
                f"The server has not found anything matching the request URI. Status code:"
                f" {status_code}. Origin response from server: {res.text}"
            )
        if status_code == 500:
            raise DemistoException(f"Internal server error. Status code: {status_code}. Origin response from server: {res.text}")

        if status_code == 502:
            raise DemistoException(f"Bad gateway. Status code: {status_code}. Origin response from server: {res.text}")


def create_headers(headers: Dict, request_content_type_header: str, response_content_type_header: str) -> Dict[str, str]:
    """
    Create a dictionary of headers. It will map the header if it exists in the CONTENT_TYPE_MAPPER.
    Args:
        headers: The headers the user insert.
        request_content_type_header: The content type header.
        response_content_type_header: The response type header.

    Returns:
        A dictionary of headers to send in the request.
    """
    if request_content_type_header in CONTENT_TYPE_MAPPER:
        request_content_type_header = CONTENT_TYPE_MAPPER[request_content_type_header]
    if response_content_type_header in CONTENT_TYPE_MAPPER:
        response_content_type_header = CONTENT_TYPE_MAPPER[response_content_type_header]
    if request_content_type_header and not headers.get("Content-Type"):
        headers["Content-Type"] = request_content_type_header
    if response_content_type_header and not headers.get("Accept"):
        headers["Accept"] = response_content_type_header

    return headers


def get_parsed_response(res, resp_type: str) -> Any:
    try:
        resp_type = resp_type.lower()
        if resp_type == "json":
            res = res.json()
        elif resp_type == "xml":
            res = json.loads(xml2json(res.content))
        else:
            res = res.text
        return res
    except ValueError as e:
        raise DemistoException(f"Failed to parse json object from response: {res.content}\n\nError Message: {e}")


def format_status_list(status_list: list) -> List[int]:
    """
    Get a status list and format it to a range of status numbers.
    Example:
        given: ['400-404',500,501]
        return: [400,401,402,403,500,501]
    Args:
        status_list: The given status list.
    Returns:
        A list of statuses to retry.
    """
    final_status_list = []
    for status in status_list:
        # Checks if the status is a range of statuses
        if "-" in status:
            range_numbers = status.split("-")
            status_range = list(range(int(range_numbers[0]), int(range_numbers[1]) + 1))
            final_status_list.extend(status_range)
        elif status.isdigit():
            final_status_list.append(int(status))
    return final_status_list


def build_outputs(parsed_res, res: requests.Response) -> Dict:
    return {
        "ParsedBody": parsed_res,
        "Body": res.text,
        "StatusCode": res.status_code,
        "StatusText": res.reason,
        "URL": res.url,
        "Headers": dict(res.headers),
    }


def parse_headers(headers: str) -> Dict:
    """
    Parsing headers from str type to dict.
    The allowed format are:
    1. {"key": "value"}
    2. "key": "value"
    """
    if not headers.startswith("{") and not headers.endswith("}"):
        headers = "{" + headers + "}"
    try:
        headers_dict = json.loads(headers)
    except json.decoder.JSONDecodeError:
        raise DemistoException("Make sure the headers are in one of the allowed formats.")
    return headers_dict


def api_call_command(client: Client):
    dmst_params = demisto.params()
    apikey_in_header = dmst_params.get("apikey_in_header", True)
    api_call_key = dmst_params.get("api_call_key", "")
    is_auth = argToBoolean(dmst_params.get("is_auth", "False"))

    cmd_args = demisto.args()
    method = cmd_args.get("method", "")
    body = cmd_args.get("body", "")
    request_content_type = cmd_args.get("request_content_type", "")
    response_content_type = cmd_args.get("response_content_type", "")
    parse_response_as = cmd_args.get("parse_response_as", RAW_RESPONSE)
    params = cmd_args.get("params", {})
    headers = cmd_args.get("headers", {})
    if not api_call_key and is_auth:
        demisto.error("Parameter/Header key used for API call must be specified")
    elif is_auth:
        if apikey_in_header:
            headers.update({api_call_key: demisto.getParam("credentials")["password"]})
        else:
            params.update({api_call_key: demisto.getParam("credentials")["password"]})
    url_path = cmd_args.get("urlpath", "/")
    if isinstance(headers, str):
        headers = parse_headers(headers)
    headers = create_headers(headers, request_content_type, response_content_type)
    save_as_file = argToBoolean(cmd_args.get("save_as_file", False))
    file_name = cmd_args.get("filename", "http-file")
    timeout = arg_to_number(cmd_args.get("timeout", 10))
    timeout_between_retries = cmd_args.get("timeout_between_retries", 5)
    retry_count = arg_to_number(cmd_args.get("retry_count", 3))

    kwargs = {
        "method": method,
        "full_url": client._base_url + url_path,
        "headers": headers,
        "data": body,
        "timeout": timeout,
        "params": params,
        "backoff_factor": timeout_between_retries,
    }

    retry_on_status = cmd_args.get("retry_on_status", None)
    raise_on_status = bool(retry_on_status)
    retry_status_list = format_status_list(argToList(retry_on_status))

    if raise_on_status:
        kwargs.update({"retries": retry_count, "status_list_to_retry": retry_status_list, "raise_on_status": raise_on_status})

    enable_redirect = argToBoolean(cmd_args.get("enable_redirect", True))

    if not enable_redirect:
        kwargs.update({"allow_redirects": enable_redirect})

    res = client.http_request(**kwargs)
    parsed_res = get_parsed_response(res, parse_response_as)

    if save_as_file:
        return fileResult(file_name, res.content)

    outputs = build_outputs(parsed_res, res)

    return CommandResults(
        readable_output=f"Sent a {method} request to {client._base_url}",
        outputs_prefix="APICall",
        outputs=outputs,
        raw_response={"data": parsed_res},
    )


def test_module(client):
    # Basic test logic to validate the connection or configuration
    return """Test-module is not implemented in this integration due to the number
            of possible API endpoints that may be configured."""


""" MAIN FUNCTION """


def main():
    try:
        params = demisto.params()
        results = ""
        auth: Optional[tuple[str, str]] = None
        apikey_in_header = argToBoolean(params.get("apikey_in_header", False))
        base_url = params.get("base_url", "")
        is_auth = params.get("is_auth", True)
        creds = params.get("credentials", "")
        proxy = params.get("proxy", False)
        verify = not params.get("insecure", True)

        command = demisto.command()

        if command == "generic-api-call":
            if is_auth and not apikey_in_header:
                # Credential object - API Key or HTTP Basic Auth
                if "credentials" in creds and creds["credentials"]["name"]:
                    auth = (creds["credentials"]["user"], creds["credentials"]["password"])
                # Creds configured in integration instance
                else:
                    auth = (creds["identifier"], creds["password"])
            else:
                auth = None

            client = Client(base_url, auth=auth, verify=verify, proxy=proxy)

            demisto.debug(f"Command being called is {command}")
            results = api_call_command(client)
        elif command == "test-module":
            return_results(test_module(client))
        return_results(results)

    except Exception as e:
        return_error(f"Failed to execute generic API call. Error: {str(e)}")


if __name__ in ("__main__", "__builtin__", "builtins"):
    main()