HttpV2

Sends a HTTP request with advanced capabilities.

python · Common Scripts

Source

import json

import demistomock as demisto
from CommonServerPython import *

from CommonServerUserPython import *

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: tuple, 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 requests.exceptions.ConnectTimeout as exception:
            err_msg = (
                "Connection Timeout Error - potential reasons might be that the Server URL parameter"
                " is incorrect or that the Server is not accessible from your host."
            )
            raise DemistoException(err_msg, exception)
        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 exception:
        raise DemistoException(f"Failed to parse json object from response: {res.content}", exception)


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


""" MAIN FUNCTION """


def main():
    try:
        args = demisto.args()
        method = args.get("method", "")
        full_url = args.get("url", "")
        body = args.get("body", "")
        request_content_type = args.get("request_content_type", "")
        response_content_type = args.get("response_content_type", "")
        parse_response_as = args.get("parse_response_as", RAW_RESPONSE)
        params = args.get("params", {})
        headers = args.get("headers", {})
        if isinstance(headers, str):
            headers = parse_headers(headers)
        headers = create_headers(headers, request_content_type, response_content_type)
        auth = tuple(argToList(args.get("auth_credentials", None)))
        save_as_file = args.get("save_as_file", "no")
        file_name = args.get("filename", "http-file")
        timeout = arg_to_number(args.get("timeout", 10))
        timeout_between_retries = arg_to_number(args.get("timeout_between_retries", 5))
        retry_count = arg_to_number(args.get("retry_count", 3))
        proxy = argToBoolean(args.get("proxy", False))
        verify = argToBoolean(not args.get("unsecure", False))

        client = Client(base_url=full_url, auth=auth, verify=verify, proxy=proxy)
        kwargs = {
            "method": method,
            "full_url": full_url,
            "headers": headers,
            "data": body,
            "timeout": timeout,
            "params": params,
            "backoff_factor": timeout_between_retries,
        }

        retry_on_status = 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(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 == "yes":
            return fileResult(file_name, res.content)

        outputs = build_outputs(parsed_res, res)

        return CommandResults(
            readable_output=f"Sent a {method} request to {full_url}",
            outputs_prefix="HttpRequest.Response",
            outputs=outputs,
            raw_response={"data": parsed_res},
        )
    except Exception as e:
        return_error(f"Failed to execute HttpV2 script. Error: {e!s}")


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

README

Sends a HTTP request with advanced capabilities

Script Data


Name Description
Script Type python3
Tags basescript
Cortex XSOAR Version 6.5.0

Inputs


Argument Name Description
url Specify where the request should be sent. Include the URI scheme (‘http’ or ‘https’).
method Specify the HTTP method to use.
headers Specify a hash of headers to send with the request.
Headers can be of string type but need to be formatted in the following ways:
`{“key1”: “value1”, “key2”: “value2”}` or `“key1”: “value1”, “key2”: “value2”`
body Specify the body of the request.
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
response_content_type Specify the Accept header for the request - the response content type.
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
parse_response_as Specify how you would like to parse the response.
auth_credentials Basic authorization. Please set values in the format: username,password. For Bearer token please use the headers.
params URL parameters to specify the query.
timeout Specify the timeout of the HTTP request in seconds. Defaults to 10 seconds.
enable_redirect The request will be called again with the new URL.
retry_on_status Specify the array of status codes that should cause a retry. For example: 301-303,400,402.
retry_count Specify the number or retries to be made in case of a failure. Defaults to 3.
timeout_between_retries Specify the timeout between each retry in seconds. Defaults to 5.
save_as_file Save the response in a file.
filename filename
unsecure Trust any certificate (not secure)
proxy Use system proxy settings

Outputs


Path Description Type
HttpRequest.Response.StatusCode The number that indicates the status of the request. String
HttpRequest.Response.StatusText The text corresponding to the status code String
HttpRequest.Response.URL The URL of the response String
HttpRequest.Response.ParsedBody The parsed response, according to `parse_response_as` argument. String
HttpRequest.Response.Headers The response headers. String
HttpRequest.Response.Body The response data. Unknown

Script Examples

Example command

!HttpV2 method=GET url="https://test.jamfcloud.com/JSSResource/computers/id/1/subset/General" response_content_type=json request_content_type=json auth_credentials=myUser,myPass parse_response_as=json

Context Example

{
    "HttpRequest": {
        "Response": {
            "Body": "{\"computer\":{\"general\":{\"id\":1,\"name\":\"Computer 1\",\"network_adapter_type\":\"\",\"mac_address\":\"11:5B:35:CA:12:12\",\"alt_network_adapter_type\":\"\",\"alt_mac_address\":\"A1:34:95:EC:97:C1\",\"ip_address\":\"123.243.192.11\",\"last_reported_ip\":\"192.168.1.11\",\"serial_number\":\"AA40F81C60A3\",\"udid\":\"AA40F812-60A3-11E4-90B8-12DF261F2C7E\",\"jamf_version\":\"9.6.29507.c\",\"platform\":\"Mac\",\"barcode_1\":\"\",\"barcode_2\":\"\",\"asset_tag\":\"\",\"remote_management\":{\"managed\":false,\"management_username\":\"\",\"management_password_sha256\":\"1230c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b811\"},\"supervised\":false,\"mdm_capable\":false,\"mdm_capable_users\":{},\"report_date\":\"2021-03-29 12:44:12\",\"report_date_epoch\":1617021852595,\"report_date_utc\":\"2021-03-29T12:44:12.595+0000\",\"last_contact_time\":\"2014-10-24 10:26:55\",\"last_contact_time_epoch\":1414146415335,\"last_contact_time_utc\":\"2014-10-24T10:26:55.335+0000\",\"initial_entry_date\":\"2021-03-29\",\"initial_entry_date_epoch\":1617021852322,\"initial_entry_date_utc\":\"2021-03-29T12:44:12.322+0000\",\"last_cloud_backup_date_epoch\":0,\"last_cloud_backup_date_utc\":\"\",\"last_enrolled_date_epoch\":1414146339607,\"last_enrolled_date_utc\":\"2014-10-24T10:25:39.607+0000\",\"mdm_profile_expiration_epoch\":0,\"mdm_profile_expiration_utc\":\"\",\"distribution_point\":\"\",\"sus\":\"\",\"site\":{\"id\":-1,\"name\":\"None\"},\"itunes_store_account_is_active\":false}}}",
            "Headers": {
                "Accept-Ranges": "bytes",
                "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0, post-check=0, pre-check=0",
                "Connection": "keep-alive",
                "Content-Encoding": "gzip",
                "Content-Length": "641",
                "Content-Type": "text/plain;charset=UTF-8",
                "Date": "Wed, 19 Jan 2022 12:12:16 GMT",
                "Server": "Jamf Cloud Node",
                "Set-Cookie": "AASALB=rbRVEXaRgAKa6+YhXAr2E4JbbP5PSS3eZpm9G1AeWQBNVG/vQ3SOmsNV4tYA5N0v7sEUQd+QYMkuUTOmF/7USLyVnQVz/yW7wQh4dWrbGOY/gAU7rL30IvQwfdut; Expires=Wed, 26 Jan 2022 12:12:16 GMT; Path=/, AWSALBCORS=rbRVEXaRgAKa6+YhXAr2E4JbbP5PSS3eZpm9G1AeWQBNVG/vQ3SOmsNV4tYA5N0v7sEUQd+QYMkuUTOmF/7USLyVnQVz/yW7wQh4dWrbGOY/gAU7rL30IvQwfdut; Expires=Wed, 26 Jan 2022 12:12:16 GMT; Path=/; SameSite=None; Secure, APBALANCEID=aws.usw2-std-ellison2-tc-4; path=/;HttpOnly;Secure;",
                "Strict-Transport-Security": "max-age=31536000; includeSubdomains;, max-age=31536000 ; includeSubDomains",
                "Vary": "Accept-Charset,Accept-Encoding,Accept-Language,Accept",
                "X-FRAME-OPTIONS": "DENY",
                "X-XSS-Protection": "1; mode=block"
            },
            "ParsedBody": {
                "computer": {
                    "general": {
                        "alt_mac_address": "A1:34:95:EC:97:C1",
                        "alt_network_adapter_type": "",
                        "asset_tag": "",
                        "barcode_1": "",
                        "barcode_2": "",
                        "distribution_point": "",
                        "id": 1,
                        "initial_entry_date": "2021-03-29",
                        "initial_entry_date_epoch": 1617021852322,
                        "initial_entry_date_utc": "2021-03-29T12:44:12.322+0000",
                        "ip_address": "200.200.200.200",
                        "itunes_store_account_is_active": false,
                        "jamf_version": "9.6.29507.c",
                        "last_cloud_backup_date_epoch": 0,
                        "last_cloud_backup_date_utc": "",
                        "last_contact_time": "2014-10-24 10:26:55",
                        "last_contact_time_epoch": 1414146415335,
                        "last_contact_time_utc": "2014-10-24T10:26:55.335+0000",
                        "last_enrolled_date_epoch": 1414146339607,
                        "last_enrolled_date_utc": "2014-10-24T10:25:39.607+0000",
                        "last_reported_ip": "192.168.1.10",
                        "mac_address": "11:5B:35:CA:12:12",
                        "mdm_capable": false,
                        "mdm_capable_users": {},
                        "mdm_profile_expiration_epoch": 0,
                        "mdm_profile_expiration_utc": "",
                        "name": "Computer 1",
                        "network_adapter_type": "",
                        "platform": "Mac",
                        "remote_management": {
                            "managed": false,
                            "management_password_sha256": "abc0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                            "management_username": ""
                        },
                        "report_date": "2021-03-29 12:44:12",
                        "report_date_epoch": 1617021852595,
                        "report_date_utc": "2021-03-29T12:44:12.595+0000",
                        "serial_number": "AA40F81C60A3",
                        "site": {
                            "id": -1,
                            "name": "None"
                        },
                        "supervised": false,
                        "sus": "",
                        "udid": "AA40F812-60A3-11E4-90B8-12DF261F2C7E"
                    }
                }
            },
            "StatusCode": 200,
            "StatusText": "",
            "URL": "https://test.jamfcloud.com/JSSResource/computers/id/1/subset/General"
        }
    }
}

Human Readable Output

Sent a GET request to https://test.jamfcloud.com/JSSResource/computers/id/1/subset/General