PcapExtractStreams
Extract payloads of each stream from a pcap. The payloads will be retrieved with an array of dictionaries of these keys: - protocol - client_ip - client_port - server_ip - server_port - stream_size - stream_text - stream_base64 - outgoing_size - outgoing_text - outgoing_base64 - incoming_size - incoming_text - incoming_base64.
python · PCAP Analysis
Source
import base64 import codecs import logging import os import unicodedata from collections.abc import Generator from tempfile import NamedTemporaryFile from typing import Any import demistomock as demisto # noqa: F401 import pyshark from CommonServerPython import * # noqa: F401 TCP_FLAG_FIN = 0x01 TCP_FLAG_SYN = 0x02 TCP_FLAG_ACK = 0x10 codecs.register_error("replace_with_space", lambda x: (" ", x.start + 1)) # type: ignore[attr-defined] def from_bytes_to_text(mode: str, binary: bytes) -> str: """ Make a text from a binary. :param mode: How to convert the binary to text. :return: A text converted from the binary. """ if mode == "text-based-protocol": # Keep all the characters used in text based protocols # * The unicodedata category names of control code start with C return "".join( " " if c == "\ufffd" or (c not in ("\n", "\r", "\t") and unicodedata.category(c)[0] == "C") else c for c in binary.decode("utf-8", errors="replace") ) elif mode == "human-readable": return binary.decode("utf-8", errors="replace_with_space") else: raise ValueError(f"Unknown text conversion mode: {mode}") class Streams: def __init__(self, server_ports: list[tuple[int, int]]): self.__tcp_streams: dict[tuple, Any] = {} self.__udp_streams: dict[tuple, Any] = {} self.__server_ports = server_ports def __make_item( self, bin2txt_mode: str, filter_keys: list[str] | None, protocol: str, sender_ip: str, sender_port: int, recipient_ip: str, recipient_port: int, stream_bytes: bytes, outgoing_bytes: bytes, incoming_bytes: bytes, ) -> dict[str, Any]: return { k: v for k, v in { "protocol": protocol, "client_ip": sender_ip, "client_port": sender_port, "server_ip": recipient_ip, "server_port": recipient_port, "stream_size": len(stream_bytes), "stream_text": from_bytes_to_text(bin2txt_mode, stream_bytes), "stream_base64": base64.b64encode(stream_bytes).decode(), "outgoing_size": len(outgoing_bytes), "outgoing_text": from_bytes_to_text(bin2txt_mode, outgoing_bytes), "outgoing_base64": base64.b64encode(outgoing_bytes).decode(), "incoming_size": len(incoming_bytes), "incoming_text": from_bytes_to_text(bin2txt_mode, incoming_bytes), "incoming_base64": base64.b64encode(incoming_bytes).decode(), }.items() if not filter_keys or (k in filter_keys) } def add_tcp( self, stream_index: int, tcp_flags: int, sender_ip: str, sender_port: int, recipient_ip: str, recipient_port: int, payload: bytes | None, ) -> None: """ Add a tcp packet to the streams. :param stream_index: The index of TCP streams. :param tcp_flags: The TCP flags. :param sender_ip: The IP address of a sender. :param sender_port: The port number of a sender. :param recipient_ip: The IP address of a recipient. :param recipient_port: The port number of a recipient. :param payload: The payload transmitted. """ key1 = (sender_ip, sender_port, recipient_ip, recipient_port, stream_index) key2 = (recipient_ip, recipient_port, sender_ip, sender_port, stream_index) if key1 in self.__tcp_streams: if payload: self.__tcp_streams[key1]["outgoing"] += payload self.__tcp_streams[key1]["stream"] += payload elif key2 in self.__tcp_streams: if payload: self.__tcp_streams[key2]["incoming"] += payload self.__tcp_streams[key2]["stream"] += payload else: mask = TCP_FLAG_SYN | TCP_FLAG_FIN | TCP_FLAG_ACK payload = payload if payload else b"" outgoing = None if (tcp_flags & mask) == TCP_FLAG_SYN: outgoing = True elif (tcp_flags & mask) == (TCP_FLAG_SYN | TCP_FLAG_ACK): outgoing = False else: matchs = any(port_range[0] < sender_port < port_range[1] for port_range in self.__server_ports) matchr = any(port_range[0] < recipient_port < port_range[1] for port_range in self.__server_ports) if matchs and (not matchr or (sender_port < recipient_port)): outgoing = False elif matchr and (not matchs or (recipient_port < sender_port)): outgoing = True if outgoing is None or outgoing is True: self.__tcp_streams[key1] = {"outgoing": payload, "incoming": b"", "stream": payload} else: self.__tcp_streams[key2] = {"outgoing": b"", "incoming": payload, "stream": payload} def add_udp(self, sender_ip: str, sender_port: int, recipient_ip: str, recipient_port: int, payload: bytes | None) -> None: """ Add a udp packet to the streams. :param sender_ip: The IP address of a sender. :param sender_port: The port number of a sender. :param recipient_ip: The IP address of a recipient. :param recipient_port: The port number of a recipient. :param payload: The payload transmitted. """ key1 = (sender_ip, sender_port, recipient_ip, recipient_port) key2 = (recipient_ip, recipient_port, sender_ip, sender_port) if key1 in self.__udp_streams: if payload: self.__udp_streams[key1]["outgoing"] += payload self.__udp_streams[key1]["stream"] += payload elif key2 in self.__udp_streams: if payload: self.__udp_streams[key2]["incoming"] += payload self.__udp_streams[key2]["stream"] += payload else: payload = payload if payload else b"" self.__udp_streams[key1] = {"outgoing": payload, "incoming": b"", "stream": payload} def build(self, bin2txt_mode: str, filter_keys: list[str] | None = None) -> list[dict[str, Any]]: """ Build the streams :param bin2txt_mode: How to convert the binary to text. :param filter_keys: Keys of the values to filter the stream information. :return: List of the streams. """ output = [] for key, stream in self.__tcp_streams.items(): sender_ip, sender_port, recipient_ip, recipient_port, stream_index = key output.append( self.__make_item( bin2txt_mode=bin2txt_mode, filter_keys=filter_keys, protocol="tcp", sender_ip=sender_ip, sender_port=sender_port, recipient_ip=recipient_ip, recipient_port=recipient_port, stream_bytes=stream.get("stream", b""), outgoing_bytes=stream.get("outgoing", b""), incoming_bytes=stream.get("incoming", b""), ) ) for key, stream in self.__udp_streams.items(): sender_ip, sender_port, recipient_ip, recipient_port = key output.append( self.__make_item( bin2txt_mode=bin2txt_mode, filter_keys=filter_keys, protocol="udp", sender_ip=sender_ip, sender_port=sender_port, recipient_ip=recipient_ip, recipient_port=recipient_port, stream_bytes=stream.get("stream", b""), outgoing_bytes=stream.get("outgoing", b""), incoming_bytes=stream.get("incoming", b""), ) ) return output class PcapParser: def __init__(self, server_ports: list[tuple[int, int]]): self.__server_ports = server_ports def __parse(self, pcap: pyshark.capture.capture.Capture) -> Streams: """ Parse a pcap :param pcap: The packet capture instance. :return: The stream data extracted. """ streams = Streams(server_ports=self.__server_ports) for packet in pcap: ip_layer = packet.get_multiple_layers("ip") if not ip_layer: continue sender_ip = ip_layer[0].get("src_host") recipient_ip = ip_layer[0].get("dst_host") if tcp_layer := packet.get_multiple_layers("tcp"): tcp = tcp_layer[0] payload = tcp.get("payload") streams.add_tcp( stream_index=int(tcp.get("stream", 0)), tcp_flags=int(tcp.flags, 0), sender_ip=str(sender_ip), sender_port=int(tcp.get("srcport")), recipient_ip=str(recipient_ip), recipient_port=int(tcp.get("dstport")), payload=payload.binary_value if payload else None, ) if udp_layer := packet.get_multiple_layers("udp"): udp = udp_layer[0] payload = udp.get("payload") streams.add_udp( sender_ip=str(sender_ip), sender_port=int(udp.get("srcport")), recipient_ip=str(recipient_ip), recipient_port=int(udp.get("dstport")), payload=payload.binary_value if payload else None, ) return streams def parse_file(self, pcap_file_path: str, wpa_password: str, rsa_key_file_path: str | None, pcap_filter: str) -> Streams: """ Parse a pcap file :param pcap_file_path: The file of the pcap to parse. :param wpa_password: The wpa password for the decryption :param rsa_key_file_path: The file of the decryption key. :param pcap_filter: A filter to apply on the PCAP. Same filter syntax as in Wireshark :return: The stream data extracted. """ # Create custom parameters custom_parameters = None if rsa_key_file_path: custom_parameters = {"-o": f'uat:rsa_keys:"{rsa_key_file_path}",""'} # Parse the pcap logging.getLogger("asyncio").setLevel(logging.ERROR) with open(os.devnull, "w") as devnull: sys.stderr = devnull try: with pyshark.FileCapture( pcap_file_path, display_filter=pcap_filter, decryption_key=wpa_password, encryption_type="WPA-PWD", keep_packets=False, custom_parameters=custom_parameters, ) as cap: return self.__parse(cap) except pyshark.capture.capture.TSharkCrashException: raise ValueError( "Could not find packets. Make sure that the file is a .cap/.pcap/.pcapng file, " "the filter is of the correct syntax and that the rsa key is added correctly." ) finally: sys.stderr = sys.__stderr__ def parse_bytes(self, pcap: bytes, wpa_password: str, rsa_decrypt_key: bytes | None, pcap_filter: str) -> Streams: """ Parse a pcap bytes :param pcap: The data of a PCAP :param wpa_password: The wpa password for the decryption :param rsa_decrypt_key: The decryption key. :param pcap_filter: A filter to apply on the PCAP. Same filter syntax as in Wireshark :return: The stream data extracted. """ with NamedTemporaryFile(mode="wb") as pcap_file: pcap_file.write(pcap) pcap_file.flush() os.fsync(pcap_file.fileno()) if rsa_decrypt_key: with NamedTemporaryFile(mode="wb") as rsa_key_file: rsa_key_file.write(rsa_decrypt_key) rsa_key_file.flush() os.fsync(rsa_key_file.fileno()) return self.parse_file(pcap_file.name, wpa_password, rsa_key_file.name, pcap_filter) else: return self.parse_file(pcap_file.name, wpa_password, None, pcap_filter) def make_pcap_by_type(pcap_type: str, pcap_bytes: bytes) -> bytes: """ Build a pcap bytes by type :param pcap_type: The data type of the PCAP. :param pcap_bytes: The data of a PCAP. :return: The data of the PCAP which can be parsed by pyshark. """ if pcap_type in ("auto", "cdl-pcap"): while True: if pcap_bytes[0:1] not in (b"\x01", b"\x00") or pcap_bytes[1:2] not in (b"\x01", b"\x00"): break POS_FIRST_PACKET = 0x24 packet_time_be32 = pcap_bytes[16 : 16 + 4] packet_size_be16 = pcap_bytes[2 : 2 + 2] packet_size = int.from_bytes(packet_size_be16, byteorder="big") if len(pcap_bytes) < packet_size + POS_FIRST_PACKET: break # pcap header pcap = b"\xa1\xb2\xc3\xd4" # .magic pcap += b"\x00\x02" # .major pcap += b"\x00\x04" # .minor pcap += b"\x00\x00\x00\x00" # .thiszone pcap += b"\x00\x00\x00\x00" # .sigfigs pcap += b"\x00\x00\xff\xff" # .snaplen pcap += b"\x00\x00\x00\x01" # .linktype # packet header pcap += packet_time_be32 # .ts.tv_sec pcap += b"\x00\x00\x00\x00" # .ts.tv_usec pcap += b"\x00\x00" + packet_size_be16 # .caplen: length of portion present pcap += b"\x00\x00" + packet_size_be16 # .len: length this packet (off wire) # packet payload pcap += pcap_bytes[POS_FIRST_PACKET : POS_FIRST_PACKET + packet_size] return pcap if pcap_type != "auto": raise ValueError("Invalid packet in CDL format") if pcap_type in ("auto", "libpcap"): return pcap_bytes else: raise ValueError(f"Unknown pcap type: {pcap_type}") def split_context_path(path: str) -> tuple[list[str], str]: """ Split a context path separated by a dot with a replacement name following a comma into the key tree the replacement name. :param path: The context path (with an optional replacement name) :return: The key tree and the replacement name. """ key_tree = [] key = [] itr = iter(path) for c in itr: if c == "\\": try: key.append(next(itr)) except StopIteration: key.append("\\") elif c == ".": key_tree.append("".join(key)) key = [] else: key.append(c) names = "".join(key).rsplit(",", 1) if len(names) == 2: key_tree.append(names[0]) return key_tree, names[1] elif len(names) == 1: key_tree.append(names[0]) return key_tree, names[0] else: raise ValueError(f"Invalid path: {path}") class MainProcess: def __init__( self, pcap_type: str, rsa_decrypt_key: bytes | None, wpa_password: str, pcap_filter: str, bin2txt_mode: str, filter_keys: list[str], error_action: str, server_ports: list[tuple[int, int]], ): self.__pcap_type = pcap_type self.__rsa_decrypt_key = rsa_decrypt_key self.__wpa_password = wpa_password self.__pcap_filter = pcap_filter self.__bin2txt_mode = bin2txt_mode self.__filter_keys = filter_keys self.__error_action = error_action self.__server_ports = server_ports def __make_streams(self, node: Any) -> Generator[Any, None, None]: """ Create stream instances from a pcap node. :param node: The pcap node. :return: The streams extracted from pcaps in the node. """ if isinstance(node, list): for v in node: yield from self.__make_streams(v) else: try: if not isinstance((pcap_value := node), str): raise ValueError(f"A pcap value is not str: {type(pcap_value)}") if pcap_bytes := base64.b64decode(pcap_value.encode()): parser = PcapParser(server_ports=self.__server_ports) streams = parser.parse_bytes( pcap=make_pcap_by_type(self.__pcap_type, pcap_bytes), wpa_password=self.__wpa_password, rsa_decrypt_key=self.__rsa_decrypt_key, pcap_filter=self.__pcap_filter, ).build(bin2txt_mode=self.__bin2txt_mode, filter_keys=self.__filter_keys) yield from streams except Exception: if self.__error_action == "abort": raise elif self.__error_action == "keep": yield node elif self.__error_action == "ignore": pass else: raise ValueError(f"Invalid error action: {self.__error_action}") def extract_and_replace(self, node: Any, key_tree: list[str], repl_name: str) -> None: """ Extract streams from pcaps specified with 'node' and 'key_tree', and set them into the node specified with 'repl_name' :param node: The node :param key_tree: The list of the keys to the node where the pcap exists. :param repl_name: The name of the key to set the streams. """ for i, key in enumerate(key_tree): if isinstance(node, list): for v in node: self.extract_and_replace(v, key_tree[i:], repl_name) return if not isinstance(node, dict) or key not in node: # The key is not found in the node return if i != len(key_tree) - 1: node = node[key] else: # Extract pcap streams and set them into the node node[repl_name] = self.make_streams(node[key]) def make_streams(self, node: Any) -> Any: """ Create streams from the pcap node. :param node: The pcap node. :return: The streams extracted from pcaps in the node. """ streams = list(self.__make_streams(node)) if isinstance(node, list) or len(streams) != 1: return streams else: return streams[0] """MAIN""" def main(): args = demisto.args() value = args.get("value", "") path = args.get("path") error_action = args.get("error_action") or "abort" pcap_type = args.get("pcap_type") or "auto" rsa_decrypt_key_base64 = args.get("rsa_decrypt_key") or "" wpa_password = args.get("wpa_password") or "" pcap_filter = args.get("pcap_filter") or "" bin2txt_mode = args.get("bin2txt_mode") or "text-based-protocol" filter_keys = argToList(args.get("filter_keys")) server_ports: list[tuple[int, int]] = [] for range_str in argToList(args.get("server_ports") or "1-49151"): port_range = range_str.split("-") if len(port_range) == 1: server_ports.append(((int(port_range[0].strip())), (int(port_range[0].strip())))) elif len(port_range) == 2: server_ports.append(((int(port_range[0].strip())), (int(port_range[1].strip())))) else: raise ValueError(f"Invalid port range: {range_str}") proc = MainProcess( pcap_type=pcap_type, rsa_decrypt_key=base64.b64decode(rsa_decrypt_key_base64.encode()), wpa_password=wpa_password, pcap_filter=pcap_filter, bin2txt_mode=bin2txt_mode, filter_keys=filter_keys, error_action=error_action, server_ports=server_ports, ) if path: key_tree, repl_name = split_context_path(path) proc.extract_and_replace(value, key_tree, repl_name) else: value = proc.make_streams(value) return_results(value) if __name__ in ("__main__", "builtin", "builtins"): main()
README
Extract payloads of each stream from a pcap.
The payloads will be retrieved with an array of dictionaries of these keys:
- protocol
- client_ip
- client_port
- server_ip
- server_port
- stream_size
- stream_text
- stream_base64
- outgoing_size
- outgoing_text
- outgoing_base64
- incoming_size
- incoming_text
- incoming_base64
Script Data
| Name | Description |
|---|---|
| Script Type | python3 |
| Tags | pcap, Utility, transformer |
Inputs
| Argument Name | Description |
|---|---|
| value | The data of a pcap in base64 from which to extract streams. |
| path | The context path to the pcap (e.g., PcapData.pcap). If you add a comma plus a node name after the path, the output will be set to the node (e.g., PcapData.pcap,out). |
| pcap_type | The data type of the pcap data. |
| bin2txt_mode | The mode of how to convert the binary to text |
| pcap_filter | Filter to apply on PCAP. Wireshark syntax as can be found here: https://www.wireshark.org/docs/man-pages/wireshark-filter.html |
| rsa_decrypt_key | The RSA decryption key in base64. |
| wpa_password | The WPA password. By providing the password you will be able to decrypt encrypted traffic data. |
| filter_keys | Keys of output items by which to filter them. |
| error_action | The action on error to parsing pcap. Possible values are abort (default), ignore, and keep. |
| server_ports | Default server port numbers by which to decide the direction. |
Outputs
There are no outputs for this script.