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
Details
| ID | PcapExtractStreams |
|---|---|
| Language | python |
| From Version | 6.2.0 |
| Docker Image | demisto/pcap-miner:1.0.0.10133006 |
| Tags | pcap Utility transformer |
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.
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()