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

IDPcapExtractStreams
Languagepython
From Version6.2.0
Docker Imagedemisto/pcap-miner:1.0.0.10133006
Tagspcap 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()