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 |