PcapHTTPExtractor

Allows to parse and extract http flows (requests & responses) from a pcap/pcapng file.

python · Common Scripts

Source

import re
import sys
import traceback
import zlib
from datetime import datetime
from io import StringIO

import pyshark
from CommonServerPython import *

from CommonServerUserPython import *

serr = sys.stderr
sys.stderr = StringIO()

""" GLOBAL VARIABLES """
LIMIT = ""
START = ""
LIMIT_DATA = 0
ALLOWED_CONTENT_TYPES = ()

# Used to convert pyshark keys to Demisto's conventions
# Also used as a whitelist of relevant keys for outputs.
PYSHARK_RES_TO_DEMISTO = {
    # Request
    "http.chat": "HttpChat",
    "http.request.method": "HttpRequestMethod",
    "http.request.uri": "HttpRequestURI",
    "http.request.version": "HttpRequestVersion",
    "http.host": "HttpHost",
    "http.x_forwarded_for": "HttpXForwardedFor",
    "http.connection": "HttpConnection",
    "http.user_agent": "HttpUserAgent",
    "http.accept": "HttpAccept",
    "http.referer": "HttpReferer",
    "http.accept_encoding": "HttpAcceptEncoding",
    "http.accept_language": "HttpAcceptLanguage",
    "http.request.full_uri": "HttpRequestFullURI",
    "http.cookie": "HttpCookie",
    # Response
    "http.response.version": "HttpResponseVersion",
    "http.response.code": "HttpResponseCode",
    "http.server": "HttpServer",
    "http.date": "HttpDate",
    "http.content_type": "HttpContentType",
    "http.content_length": "HttpContentLength",
    "http.last_modified": "HttpLastModified",
    "http.cache_control": "HttpCacheControl",
    "http.file_data": "HttpFileData",
    "http.transfer_encoding": "HttpTransferEncoding",
    "http.content_encoding": "HttpContentEncoding",
}


def _file_has_extension(file_name, extensions):
    """
    Check if a file name has an extension.
    Returns true/false

    :param file_name: the file name to check against.
    :param extensions: extensions to test if exists in file_name.
    :return: True if one of the extensions is in the file_name
    """
    return any(file_name.endswith(ext) for ext in extensions)


def _find_entry_id_by_name(file_name, extensions=None):
    """
    Scan all entries and find corresponding entry id by file name
    extensions, an array used to furthur filter the entries.

    :param file_name: find by file name.
    :param extensions:  filter more by the file extension.
    :return: the found entryID
    """
    entries = demisto.executeCommand("getEntries", {})
    found_entry_id = None
    for entry in entries:
        entry_file_name = demisto.get(entry, "File")
        is_correct_file = file_name.lower() == entry_file_name.lower()
        has_correct_extension = _file_has_extension(file_name, extensions) if extensions else True

        if is_correct_file and has_correct_extension:
            found_entry_id = entry["ID"]
            break

    if not found_entry_id:
        demisto.results(
            {
                "Type": entryTypes["note"],
                "ContentsFormat": formats["markdown"],
                "Contents": "### No file found",
                "EntryContext": {"PcapHTTPExtractor.Flows": []},
            }
        )
        sys.exit(0)

    return found_entry_id


def get_entry_from_args():
    """
    Handle finding the file entry using the user supplied arguments
    return the entry or quits script entirely.

    :rtype: object containing the entry of the found file and the entry_id or error & exit
    """
    # Get the pcap file from arguments
    entry_id = None
    if "pcapFileName" in demisto.args() and "entryID" not in demisto.args():
        PCAP_FILE = demisto.args()["pcapFileName"]
        entry_id = _find_entry_id_by_name(PCAP_FILE, [".pcap", ".cap", ".pcapng"])
    elif "entryID" in demisto.args():
        entry_id = demisto.args()["entryID"]
    else:
        return_error("You must set pcapFileName or entryID when executing the PcapHTTPExtract script.")

    res = demisto.executeCommand("getFilePath", {"id": entry_id})

    if len(res) > 0 and res[0]["Type"] == entryTypes["error"]:
        return_error(f"Failed to get the file path for entry: {entry_id}")

    return res, entry_id


def _chunker(seq, size):
    """
    Function that groups items in a sequence for easier iteration of multiple items at once.

    :param seq: the sequence (list/generator)
    :param size: the size of the group (chunk)
    :return: an iterator that outputs groups of items (chunks) from the sequence
    """
    ret = []
    for i in range(0, len(seq), size):
        chunk = seq[i : i + size]

        # Make sure the chunks are with a specified size
        padding = ["" for j in range(size - len(chunk))]
        ret.append(chunk + padding)

    return iter(ret)


def decode_gzip(str_compressed):
    """
    Decode a hex string with gz decompression

    :type str_compressed: hex string gz compressed
    :return: gz decompressed string
    """
    decoded = zlib.decompress(bytearray.fromhex(str_compressed), 47)
    return decoded


def _date_to_ISO(strdate):
    """
    Return date in ISO format, from pyshark date output

    :type strdate: string of date (pyshark's output) that is not in ISO format
    :return: an ISO formatted date
    """
    return datetime.strptime(strdate, "%a, %d %b %Y %H:%M:%S %Z").isoformat()


def get_next_matching_index(http_packets, tcp_src, tcp_dst, ip_src, ip_dst):
    for i, p in enumerate(http_packets):
        if p["TCP"].srcport == tcp_dst and p["TCP"].dstport == tcp_src and p["IP"].src == ip_dst and p["IP"].dst == ip_src:
            return i
    return None


def fix_order(http_packets):
    """
    Re-organizes the http packets to be request-response pairs.
    Sometimes pyshark doesn't put the packets in the order they are HTTP-wise.
    So we are re-organizing them according to the tcp and ip information of the packets.
    :param http_packets:
    :return: http_packets
    """
    new_order = []

    # For each packet find the next one that matches the tcp,ip tuple.
    while len(http_packets) > 0:
        packet = http_packets[0]
        tcp_src = packet["TCP"].srcport
        tcp_dst = packet["TCP"].dstport
        ip_src = packet["IP"].src
        ip_dst = packet["IP"].dst
        new_order.append(packet)
        del http_packets[0]

        next_matching_index = get_next_matching_index(http_packets, tcp_src, tcp_dst, ip_src, ip_dst)
        if next_matching_index is None:
            new_order.append(None)
        else:
            new_order.append(http_packets[next_matching_index])
            del http_packets[next_matching_index]

    return new_order


def get_http_flows(pcap_file_path):
    """
    Return a list of HTTP requests/responses from pcap file

    :param pcap_file_path:
    :return: list of requests/response pairs.
    """
    capture_object = pyshark.FileCapture(pcap_file_path, display_filter="tcp")

    # Filter all non HTTP packets
    http_packets = [p for p in capture_object if "HTTP" in p]
    http_packets = fix_order(http_packets)

    # Construct request <> response dicts
    http_flows = []

    for req, res in _chunker(http_packets, 2):
        sanitized_req = req
        sanitized_res = res

        if req:
            req_fields = req["HTTP"]._all_fields.keys()

            # if the file contains only a response, the response will falsely appear in the 'req' variable
            if "http.response" in req_fields:
                sanitized_req = None
                sanitized_res = req

        http_flows.append({"Request": sanitized_req, "Response": sanitized_res})
    return http_flows


def get_flow_info(http_flow):
    """
    Get the TCP, IP and Meta information of the flow.

    :param http_flow: http flow object
    :return: tcp (src,dst ports), ip (src, dst addresses)
    """
    return {
        "TcpSrcPort": http_flow["TCP"].srcport,
        "TcpDstPort": http_flow["TCP"].dstport,
        "IpSrc": http_flow["IP"].src,
        "IpDst": http_flow["IP"].dst,
        "MetaSniffTimeStamp": http_flow.sniff_timestamp,
        "ResultIndex": http_flow.frame_info.number,
    }


def map_demisto_keys(pyshark_results, keys_transform_map):
    """
    Map all of the whitelisted keys from the transform map dict to the pyshark result

    :param pyshark_results: object from pyshark, use values and transform his keys
    :param keys_transform_map: map of key transformation
    :return: new dict with transformed keys.
    """
    new_keys = {}
    for k, v in pyshark_results.items():
        if k not in keys_transform_map:
            continue
        new_keys[keys_transform_map[k]] = v

    return new_keys


def create_flow_object(flow, keys_transform_map, trim_file_data_size, allowed_content_types):
    """
    Create a flow object constructed from HTTP,TCP,IP and Meta data of the flow.
    Fix the PyShark keys to enforce the Demisto standards. Trim the http data if too big.
    Strip the http data if it isn't allowed.

    :param flow: a http flow.
    :param keys_transform_map: a map of pyshark keys to Demisto standard keys.
    :param trim_file_data_size: the byte siez of max file_data_size
    :param allowed_content_types: allowed content types to display
    :return: an http flow
    """

    if flow is None:
        return {"Not found": "No response found"}

    # Get the HTTP and TCP, IP and Meta fields.
    r = flow["HTTP"]._all_fields
    flow_info = get_flow_info(flow)

    # Map the keys to the conventions
    r = map_demisto_keys(r, keys_transform_map)

    # Merge the HTTP and TCP, IP fields.
    r.update(flow_info)

    # Remove the \r\n from the HttpChat
    if "HttpChat" in r:
        r["HttpChat"] = r["HttpChat"].replace("\\r\\n", "")

    # Trim a file data too big
    if "HttpFileData" in r:
        # Remove the \xef\xbf\xbd and \xa hex from the file data
        if "\\xef\\xbf\\xbd" in r["HttpFileData"]:
            r["HttpFileData"] = re.sub("(\\\\xef\\\\xbf\\\\xbd)+", "[UNICODE]", r["HttpFileData"])
        r["HttpFileData"] = r["HttpFileData"].replace("\\xa", "")

        if "HttpContentType" not in r:
            r["HttpFileData"] = "Couldn't find response content type, assuming not allowed."
        elif not r["HttpContentType"].startswith(allowed_content_types):
            r["HttpFileData"] = "[{} is not in allowedContentTypes arg/default]".format(r["HttpContentType"])
        elif len(r["HttpFileData"]) > trim_file_data_size:
            r["HttpFileData"] = r["HttpFileData"][:trim_file_data_size] + "... [TRIMMED by limitData arg]"

    # Fix ISO dates
    if "HttpDate" in r:
        r["HttpDate"] = _date_to_ISO(r["HttpDate"])
    if "HttpLastModified" in r:
        r["HttpLastModified"] = _date_to_ISO(r["HttpLastModified"])

    return r


def format_http_flows(http_flows, keys_transform_map, trim_file_data_size, allowed_content_types):
    """
    Merge the HTTP, IP and TCP fields together and create a usable object for further proceeding.

    :param http_flows:
    :param keys_transform_map:
    :return: http_flows object
    """
    formatted_http_flows = []

    for flow in http_flows:
        req = create_flow_object(flow["Request"], keys_transform_map, trim_file_data_size, allowed_content_types)
        res = create_flow_object(flow["Response"], keys_transform_map, trim_file_data_size, allowed_content_types)

        formatted_http_flows.append({"Request": req, "Response": res})

    return formatted_http_flows


def get_markdown_output(http_flows):
    """
    Convert a list of http flows into a markdown table

    :param http_flows: a list of http packets
    :return: a string of markdown
    """
    result_template = "---\n{req}\n{res}\n---"
    markdown_result = ""

    for i, flow in enumerate(http_flows):
        row = result_template.format(
            req=tableToMarkdown(f"HTTPRequest #{i + 1}", flow["Request"], flow["Request"].keys()),
            res=tableToMarkdown(f"HTTPResponse #{i + 1}", flow["Response"], flow["Response"].keys()),
        )
        markdown_result += row

    return markdown_result


def main():
    try:
        """ GLOBAL VARIABLES """
        global LIMIT, START, LIMIT_DATA, ALLOWED_CONTENT_TYPES
        LIMIT = demisto.args().get("limit")
        START = demisto.args().get("start")
        LIMIT_DATA = int(demisto.args().get("limitData"))
        if "allowedContentTypes" not in demisto.args():
            ALLOWED_CONTENT_TYPES = (
                "text",
                "application/json",
                "multipart/form-data",  # type: ignore
                "application/xml",
                "application/xhtml+xml",
                "application/ld+json",
                "application/javascript",
                "multipart/alternative",
                "application/x-www-form-urlencoded",
            )
        else:
            ALLOWED_CONTENT_TYPES = tuple(demisto.args()["allowedContentTypes"].split(","))  # type: ignore

        # Parse the arguments
        pcap_file_path_in_container, pcap_entry_id = get_entry_from_args()
        pcap_file_path_in_container = pcap_file_path_in_container[0]["Contents"]["path"]

        # Work on the pcap file and return a result
        http_flows = get_http_flows(pcap_file_path_in_container)

        # Cut results according to the user args (times 2, because we are working on pairs of requests and responses).
        if START:
            http_flows = http_flows[int(START) :]
        if LIMIT:
            http_flows = http_flows[: int(LIMIT)]

        # Format and get output representation of the flows
        formatted_http_flows = format_http_flows(http_flows, PYSHARK_RES_TO_DEMISTO, LIMIT_DATA, ALLOWED_CONTENT_TYPES)
        markdown_output = get_markdown_output(formatted_http_flows)
        context_output = formatted_http_flows

        # Profit, send the output
        demisto.results(
            {
                "Type": entryTypes["note"],
                "ContentsFormat": formats["markdown"],
                "Contents": markdown_output,
                "EntryContext": {"PcapHTTPFlows": context_output},
            }
        )
    except Exception as e:
        demisto.error(traceback.format_exc())  # print the traceback
        return_error(f"Failed to execute PcapHTTPExtractor Script. Error: {e!s}")
    finally:
        sys.stderr = serr


# python2 uses __builtin__ python3 uses builtins
if __name__ in ("__builtin__", "builtins", "__main__"):
    main()

README

Allows to parse and extract http flows (requests & responses) from a pcap/pcapng file.

Script Data


Name Description
Script Type python3
Tags pcap, http
Cortex XSOAR Version 5.0.0

Inputs


Argument Name Description
pcapFileName get file entry from file name
entryID File entry from the WarRoom
limit Limit the output of the capture file output’s flows (starts from 0).
start Index of where to output flows (starts from 0).
limitData Limit the HttpFileData field (in bytes)
allowedContentTypes The allowed content types to display, separated with comma, uses startswith to find a match (ie text,image will display text\html, and image\png).

Outputs


Path Description Type
PcapHTTPFlows Flows extracted from the pcap file. String
PcapHttpFlows.ResultIndex The index of the http packet in the pcap file. String
PcapHttpFlows.HttpContentType Http content type of the response. String
PcapHttpFlows.HttpResponseVersion Http version used in the response. String
PcapHttpFlows.HttpResponseCode Http response code from the server String
PcapHttpFlows.HttpDate Http date returned from the sever String
PcapHttpFlows.HttpRequestMethod Http request method used. String
PcapHttpFlows.HttpRequestUri Http request URI (path) String
PcapHttpFlows.HttpFileData Http content of the response String
PcapHttpFlows.HttpServer The server signature in the response String
PcapHttpFlows.HttpUserAgent Http user agent sent in the request String
PcapHttpFlows.HttpAccept Http request accept type String
PcapHttpFlows.MetaSniffTimeStamp Time the packet was sniffed (unixtime). String