PcapMinerV2

PcapMIner V2 allows to parse PCAP files by displaying the all of the relevant data within including ip addresses, ports, flows, specific protocol breakdown, searching by regex, decrypting encrypted traffic and more. This automation takes about a minute to process 20,000 packets (which is approximately 10MB). If you want to mine large files you can either: a) Use the `pcap_filter` parameter to filter your PCAP file and thus make is smaller. b) Copy the automation and change the `default timeout` parameter to match your needs.

python · PCAP Analysis

Source

import re
import traceback
from typing import Any

import demistomock as demisto
import pyshark
from CommonServerPython import *

"""GLOBAL VARS"""
BAD_CHARS = ["[", "]", ">", "<", "'", " Layer", " ", "{", "}"]
EMAIL_REGEX = r"\b[A-Za-z0-9._%=+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"
IP_REGEX = r"\b(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\b"
URL_REGEX = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
PRAGMA_REGEX = r"Pragma: ([^\\]+)"
TYPE_REGEX = r"Type: (.+)"
CLASS_REGEX = r"Class: (.+)"
COMMAND_REGEX = r"Command: (.+)"
SNAME_REGEX = r"SNameString: (.+)"
MESSAGE_CODE = r"Message Code: (.+)"
RESPONSE_CODE = r"Response code: (.+)"
ALL_SUPPORTED_PROTOCOLS = [
    "HTTP",
    "DNS",
    "LLMNR",
    "SYSLOG",
    "SMTP",
    "NETBIOS",
    "ICMP",
    "KERBEROS",
    "TELNET",
    "SSH",
    "IRC",
    "FTP",
    "SMB2",
]


class PCAP:
    @logger
    def __init__(self, is_reg_extract: bool, extracted_protocols: list, homemade_regex: str, unique_ips: bool, entry_id: str):
        """
        PCAP class. This class will contain all data and functions in order to mine the pcap.

        Args:
            is_reg_extract: Whether to extract regular expressions from the PCAP. URL, IP, Email
            extracted_protocols: A list of protocols to extract
            homemade_regex: A regex to extract from the PCAP
            unique_ips: Whether to export to context the unique source and dest IPs from the PCAP.
        """

        # setup data structures
        self.hierarchy: dict[str, int] = {}
        self.num_of_packets = 0
        self.tcp_streams = 0
        self.udp_streams = 0
        self.last_packet = 0
        self.bytes_transmitted = 0
        self.min_time = float("inf")
        self.max_time = -float("inf")
        self.conversations: dict[tuple, Any] = {}
        self.flows: dict[tuple, Any] = {}
        self.unique_source_ip: set = set()
        self.unique_dest_ip: set = set()
        self.ips_extracted: set = set()
        self.urls_extracted: set = set()
        self.emails_extracted: set = set()
        self.homemade_extracted: set = set()
        self.last_layer: set = set()
        self.irc_data: list = []
        self.protocol_data: dict[str, Any] = {}
        self.entry_id = entry_id
        self.extracted_protocols = extracted_protocols
        self.homemade_regex = homemade_regex
        self.unique_ips = unique_ips
        for protocol in extracted_protocols:
            self.protocol_data[protocol] = {}

        # Regex compilation
        if "LLMNR" in extracted_protocols:
            self.llmnr_type = re.compile("Type: (.*)\n")
            self.llmnr_class = re.compile("Class: (.*)\n")
            self.llmnr_dict: dict = {}

        if is_reg_extract:
            self.reg_ip = re.compile(IP_REGEX)
            self.reg_email = re.compile(EMAIL_REGEX)
            self.reg_url = re.compile(URL_REGEX)

        if "HTTP" in extracted_protocols:
            self.reg_pragma = re.compile(PRAGMA_REGEX)

        if "ICMP" in extracted_protocols:
            self.icmp_data: set = set()
        if "DNS" in extracted_protocols or "NETBIOS" in extracted_protocols or "ICMP" in extracted_protocols:
            self.reg_type = re.compile(TYPE_REGEX)
        if "NETBIOS" in extracted_protocols:
            self.reg_class = re.compile(CLASS_REGEX)
        if "SMB2" in extracted_protocols:
            self.reg_cmd = re.compile(COMMAND_REGEX)
        if "KERBEROS" in extracted_protocols:
            self.reg_sname = re.compile(SNAME_REGEX)
            self.kerb_data: list = []
        if "SSH" in extracted_protocols:
            self.ssh_data = {
                "EntryID": entry_id,
                "ClientProtocols": set(),
                "ServerProtocols": set(),
                "KeyExchangeMessageCode": set(),
            }
            self.reg_message_code = re.compile(MESSAGE_CODE)
        if "FTP" in extracted_protocols:
            self.reg_res_code = re.compile(RESPONSE_CODE)
        if "TELNET" in extracted_protocols:
            self.telnet_data: set = set()
            self.telnet_commands: set = set()
        if homemade_regex:
            self.reg_homemade = re.compile(self.homemade_regex)

    @logger
    def extract_dns(self, packet):
        dns_layer = packet.dns
        temp_dns = {
            "EntryID": self.entry_id,
            "ID": dns_layer.get("id"),
            "Request": dns_layer.get("qry_name"),
            "Response": dns_layer.get("a"),
            "Type": self.reg_type.findall(str(dns_layer))[0] if self.reg_type.findall(str(dns_layer)) else None,
        }
        add_to_data(self.protocol_data["DNS"], temp_dns)

    @logger
    def extract_kerberos(self, packet):
        kerb_layer = packet.kerberos
        sname_results = self.reg_sname.findall(str(kerb_layer))
        self.kerb_data.append(
            {
                "EntryID": self.entry_id,
                "Realm": kerb_layer.get("realm"),
                "CName": kerb_layer.get("CNameString"),
                "SName": sname_results if sname_results else None,
            }
        )

    @logger
    def extract_telnet(self, packet):
        telnet_layer = packet.telnet
        for message in telnet_layer._get_all_field_lines():
            if "Data:" in message:
                self.telnet_data.add(message.lstrip("Data: "))  # noqa: B005
                continue
            if ":" not in message:
                self.telnet_commands.add(message.lstrip("\t").rstrip("\n"))

    @logger
    def extract_llmnr(self, packet):
        llmnr_layer = packet.llmnr
        llmnr_layer_string = str(llmnr_layer)
        query_type_results = self.llmnr_type.findall(llmnr_layer_string)
        query_class_results = self.llmnr_class.findall(llmnr_layer_string)
        llmnr_data = {
            "EntryID": self.entry_id,
            "ID": llmnr_layer.get("dns_id"),
            "QueryType": None if len(query_type_results) == 0 else query_type_results[0],
            "QueryClass": None if len(query_class_results) == 0 else query_class_results[0],
            "QueryName": str(llmnr_layer.get("dns_qry_name")),
            "Questions": int(llmnr_layer.get("dns_count_queries")),
        }
        add_to_data(self.protocol_data["LLMNR"], llmnr_data)

    @logger
    def extract_syslog(self, packet):
        syslog_layer = packet.syslog
        syslog_data = {
            "EntryID": self.entry_id,
            "ID": syslog_layer.get("msgid"),
            "Message": syslog_layer.get("msg"),
            "Hostname": syslog_layer.get("hostname"),
            "Timestamp": syslog_layer.get("timestamp"),
        }
        add_to_data(self.protocol_data["SYSLOG"], syslog_data)

    @logger
    def extract_imf(self, packet):
        imf_layer = packet.imf
        imf_data = {
            "EntryID": self.entry_id,
            "ID": imf_layer.get("Message-ID", -1),
            "To": strip(imf_layer.get("to"), ["<", ">"]),
            "From": strip(imf_layer.get("from"), ["<", ">"]),
            "Subject": imf_layer.get("subject"),
            "MimeVersion": imf_layer.get("mime-version"),
        }
        add_to_data(self.protocol_data["SMTP"], imf_data)

    @logger
    def extract_smtp(self, packet):
        smtp_layer = packet.smtp

        if smtp_layer.get_field("req_parameter") is None:
            parameters = []
        else:
            parameters = smtp_layer.req_parameter.split(":")

        smtp_data = {"EntryID": self.entry_id, "ID": packet.tcp.seq}
        if len(parameters) == 2:
            smtp_data[parameters[0].title()] = strip(parameters[1], ["<", ">"])
        add_to_data(self.protocol_data["SMTP"], smtp_data, packet.tcp.nxtseq)

    @logger
    def extract_smb(self, packet):
        smb_layer = packet.smb2
        command_results = self.reg_cmd.findall(str(smb_layer))
        smb_data = {
            "EntryID": self.entry_id,
            "ID": smb_layer.get("sesid", -1),
            "UserName": smb_layer.get("ntlmssp_auth_username"),
            "Domain": smb_layer.get("ntlmssp_auth_domain"),
            "HostName": smb_layer.get("ntlmssp_auth_hostname"),
            "Command": command_results[0] if command_results else None,
            "FileName": smb_layer.get("smb2.filename"),
            "Tree": smb_layer.get("tree"),
        }
        add_to_data(self.protocol_data["SMB2"], smb_data)

    @logger
    def extract_netbios(self, packet):
        netbios_layer = packet.nbns
        type_results = self.reg_type.findall(str(netbios_layer))
        class_results = self.reg_class.findall(str(netbios_layer))
        netbios_data = {
            "EntryID": self.entry_id,
            "ID": netbios_layer.get("id", -1),
            "Name": netbios_layer.get("name"),
            "Type": type_results if type_results else None,
            "Class": class_results if class_results else None,
        }
        add_to_data(self.protocol_data["NETBIOS"], netbios_data)

    @logger
    def extract_icmp(self, packet):
        icmp_layer = packet.icmp
        type_results = self.reg_type.findall(str(icmp_layer))
        for result in type_results:
            self.icmp_data.add(result)

    @logger
    def extract_ssh(self, packet):
        ssh_layer = packet.ssh
        protocol = ssh_layer.get("protocol")
        message_code_results = self.reg_message_code.findall(str(ssh_layer))
        if protocol and ssh_layer.get("direction") == 1:
            # direction is server to client
            self.ssh_data["ServerProtocols"].add(protocol)  # type: ignore[attr-defined]
        if protocol and ssh_layer.get("direction") == 0:
            # direction is client to server
            self.ssh_data["ClientProtocols"].add(protocol)  # type: ignore[attr-defined]
        if message_code_results and message_code_results:
            self.ssh_data["KeyExchangeMessageCode"].add(message_code_results[0])  # type: ignore[attr-defined]

    @logger
    def extract_irc(self, packet):
        irc_layer = packet.irc
        if irc_layer.get("request"):
            command = irc_layer.get("request_command")
            trailer = irc_layer.get("request_trailer", "")
            prefix = irc_layer.get("request_prefix", "")
            parameters = irc_layer.get("request").replace(command, "").replace(trailer, "").replace(prefix, "").split(" ")
            parameters.remove(" ")
            irc_data = {
                "EntryID": self.entry_id,
                "ID": packet.tcp.get("ack"),
                "RequestCommand": command,
                "RequestTrailer": trailer,
                "RequestPrefix": prefix,
                "RequestParameters": parameters,
            }
        else:
            command = irc_layer.get("response_command")
            trailer = irc_layer.get("response_trailer", "")
            prefix = irc_layer.get("response_prefix", "")
            parameters = irc_layer.get("response").replace(command, "").replace(trailer, "").replace(prefix, "").split(" ")
            parameters.remove(" ")
            irc_data = {
                "EntryID": self.entry_id,
                "ID": packet.tcp.get("seq"),
                "ResponseCommand": command,
                "ResponseTrailer": trailer,
                "ResponsePrefix": prefix,
                "ResponseParameters": parameters,
            }
        add_to_data(self.protocol_data["IRC"], irc_data, next_id=packet.tcp.nxtseq)

    @logger
    def extract_ftp(self, packet):
        ftp_layer = packet.ftp
        res_code_results = self.reg_res_code.findall(str(ftp_layer))
        ftp_data = {
            "EntryID": self.entry_id,
            "ID": packet.tcp.get("seq"),
            "RequestCommand": ftp_layer.get("request_command"),
            "ResponseArgs": ftp_layer.get("response_arg"),
            "ResponseCode": res_code_results[0] if res_code_results else None,
        }
        if ftp_data["ResponseCode"]:
            # if packet is a response, don't update ID. This is because FTP may have a lot of requests and
            # responses in the same conversation
            add_to_data(self.protocol_data["FTP"], ftp_data)
        else:
            add_to_data(self.protocol_data["FTP"], ftp_data, next_id=packet.tcp.ack)

    @logger
    def extract_context_from_packet(self, packet, layers_str: str, is_reg_extract: bool = False) -> None:
        """
        Get a packet and extract context from it for the specified protocols specified in `self.extracted_protocols`
        Args:
            packet: The packet itself.
            layers_str: A comma-seperated string of the layers in the packet.
            is_reg_extract: Whether to export to context the unique source and dest IPs from the PCAP.
        """

        layers = layers_str.split(",")
        if is_reg_extract:
            self.ips_extracted.update(self.reg_ip.findall(str(packet)))
            self.emails_extracted.update(self.reg_email.findall(str(packet)))
            self.urls_extracted.update(self.reg_url.findall(str(packet)))

        if self.homemade_regex:
            self.homemade_extracted.update(self.reg_homemade.findall(str(packet)))

        if "DNS" in self.extracted_protocols and "DNS" in layers:
            return self.extract_dns(packet)

        if "KERBEROS" in self.extracted_protocols and "KERBEROS" in layers:
            return self.extract_kerberos(packet)

        if "TELNET" in self.extracted_protocols and "TELNET" in layers:
            return self.extract_telnet(packet)

        if "LLMNR" in self.extracted_protocols and "LLMNR" in layers:
            return self.extract_llmnr(packet)

        if "SYSLOG" in self.extracted_protocols and "SYSLOG" in layers:
            return self.extract_syslog(packet)

        if "SMB2" in self.extracted_protocols and "SMB2" in layers:
            return self.extract_smb(packet)

        if "NETBIOS" in self.extracted_protocols and "NETBIOS" in layers:
            return self.extract_netbios(packet)

        if "ICMP" in self.extracted_protocols and "ICMP" in layers:
            return self.extract_icmp(packet)

        if "SSH" in self.extracted_protocols and "SSH" in layers:
            return self.extract_ssh(packet)

        if "IRC" in self.extracted_protocols and "IRC" in layers:
            return self.extract_irc(packet)

        if "FTP" in self.extracted_protocols and "FTP" in layers:
            return self.extract_ftp(packet)

        if "SMTP" in self.extracted_protocols:  # noqa: RET503
            if "IMF" in layers:
                return self.extract_imf(packet)

            if "SMTP" in layers:  # noqa: RET503
                return self.extract_smtp(packet)

    @logger
    def get_outputs(self, conversation_number_to_display=15, is_flows=False, is_reg_extract=False):
        if self.num_of_packets == 0:
            return "## No packets found.\nTry changing the filter, if applied.", {}, {}
        md = (
            f"## PCAP Info:\n"
            f"Between {formatEpochDate(self.min_time)} and {formatEpochDate(self.max_time)} "
            f"there were {self.num_of_packets} "
            f"packets transmitted in {self.tcp_streams + self.udp_streams} streams.\n"
        )
        md += "#### Protocol Breakdown\n"
        md += hierarchy_to_md(self.hierarchy)
        md += f"#### Top {conversation_number_to_display} Conversations\n"
        md += conversations_to_md(self.conversations, conversation_number_to_display)
        if is_flows:
            md += f"#### Top {conversation_number_to_display} Flows\n"
            md += flows_to_md(self.flows, conversation_number_to_display)

        # Entry Context
        general_context = {
            "EntryID": self.entry_id,
            "Bytes": self.bytes_transmitted,
            "Packets": self.num_of_packets,
            "StreamCount": self.tcp_streams + self.udp_streams,
            "UniqueSourceIP": len(self.unique_source_ip),
            "UniqueDestIP": len(self.unique_dest_ip),
            "StartTime": formatEpochDate(self.min_time),
            "EndTime": formatEpochDate(self.max_time),
            "Protocols": list(self.last_layer),
        }
        ec = {"PCAPResults(val.EntryID == obj.EntryID)": general_context}
        for protocol in self.extracted_protocols:
            if self.protocol_data[protocol]:
                ec[f"PCAPResults{protocol}"] = list(self.protocol_data[protocol].values())  # type: ignore[assignment]
        if "ICMP" in self.extracted_protocols and self.icmp_data:
            ec["PcapResultsICMP"] = list(self.icmp_data)  # type: ignore[assignment]
        if "KERBEROS" in self.extracted_protocols and self.kerb_data:
            ec["PCAPResultsKERBEROS"] = self.kerb_data  # type: ignore[assignment]
        if "SSH" in self.extracted_protocols:
            temp = {
                "EntryID": self.entry_id,
                "ClientProtocols": list(self.ssh_data["ClientProtocols"]),
                "ServerProtocols": list(self.ssh_data["ServerProtocols"]),
                "KeyExchangeMessageCode": list(self.ssh_data["KeyExchangeMessageCode"]),
            }
            ec["PCAPResultsSSH"] = assign_params(**temp)
        if "TELNET" in self.extracted_protocols:
            ec["PCAPResultsTelnet"] = {
                "Commands": list(self.telnet_commands),
                "Data": list(self.telnet_data),
                "EntryID": self.entry_id,
            }
        if is_flows:
            ec["PCAPResultsFlow"] = flows_to_ec(self.flows)
        if is_reg_extract:
            general_context["IP"] = list(self.ips_extracted)
            general_context["URL"] = list(self.urls_extracted)
            general_context["Email"] = list(self.emails_extracted)
        if self.homemade_regex:
            general_context["Regex"] = list(self.homemade_extracted)
        if self.unique_ips:
            all_ips = self.unique_source_ip.copy()
            all_ips.update(self.unique_dest_ip)
            if general_context.get("IP"):
                general_context.get("IP").append(list(all_ips))  # type: ignore[union-attr]
            else:
                general_context["IP"] = list(all_ips)

            general_context["SourceIP"] = list(self.unique_source_ip)
            general_context["DestIP"] = list(self.unique_dest_ip)
        return md, ec, general_context

    @logger
    def mine(
        self,
        file_path: str,
        wpa_password: str,
        rsa_key_file_path: str,
        is_flows: bool,
        is_reg_extract: bool,
        pcap_filter: str,
        pcap_filter_new_file_path: str,
    ) -> None:
        """
        The main function of the script. Mines the PCAP.

        Args:
            file_path: The PCAP's file path.
            wpa_password: The wpa password for the decryption
            is_flows: Whether to extract flows.
            is_reg_extract: Whether to extract regexes from the PCAP.
            pcap_filter: A filter to apply on the PCAP. Same filter syntax as in Wireshark
            pcap_filter_new_file_path: The new path to save the filtered PCAP in

        """
        cap = None
        try:
            custom_parameters = None
            if rsa_key_file_path:
                custom_parameters = {"-o": f'uat:rsa_keys:"{rsa_key_file_path}",""'}
            cap = pyshark.FileCapture(
                file_path,
                display_filter=pcap_filter,
                output_file=pcap_filter_new_file_path,
                decryption_key=wpa_password,
                encryption_type="WPA-PWD",
                keep_packets=False,
                custom_parameters=custom_parameters,
            )
            for packet in cap:
                self.last_packet = int(packet.number)
                self.last_layer.add(packet.layers[-1].layer_name.upper())

                layers_string = str(packet.layers)
                # remove duplicate layer names such as [ETH,DATA,DATA] -> # [ETH, DATA]
                layers: list = list(dict.fromkeys(layers_string.split(",")))
                layers = strip(str(layers))
                self.hierarchy[layers] = self.hierarchy.get(layers, 0) + 1  # type: ignore[index, call-overload]

                # update times
                packet_epoch_time = float(packet.frame_info.get("time_epoch"))
                self.max_time = max(self.max_time, packet_epoch_time)
                self.min_time = min(self.min_time, packet_epoch_time)

                # count packets
                self.num_of_packets += 1

                # count bytes
                self.bytes_transmitted += int(packet.length)

                # count num of streams + get src/dest ports
                tcp = packet.get_multiple_layers("tcp")
                tcp_or_udp = "Not TCP and not UDP"
                if tcp:
                    self.tcp_streams = max(int(tcp[0].get("stream", 0)) + 1, self.tcp_streams)
                    src_port = int(tcp[0].get("srcport", 0))
                    dest_port = int(tcp[0].get("dstport", 0))
                    tcp_or_udp = "TCP"

                udp = packet.get_multiple_layers("udp")
                if udp:
                    self.udp_streams = max(int(udp[0].get("stream", 0)) + 1, self.udp_streams)
                    src_port = int(udp[0].get("srcport", 0))
                    dest_port = int(udp[0].get("dstport", 0))
                    tcp_or_udp = "UDP"

                # add conversations
                ip_layer = packet.get_multiple_layers("ip")
                if ip_layer:
                    a = ip_layer[0].get("src_host", "")
                    b = ip_layer[0].get("dst_host")
                    self.unique_source_ip.add(a)
                    self.unique_dest_ip.add(b)

                    # generate flow data
                    if is_flows:
                        if "src_port" not in locals():
                            continue
                        if (b, dest_port, a, src_port) in self.flows:
                            b, a, src_port, dest_port = a, b, dest_port, src_port
                        flow = (a, src_port, b, dest_port)
                        flow_data = self.flows.get(
                            flow,
                            {
                                "EntryID": self.entry_id,
                                "Transport": tcp_or_udp,
                                "min_time": float("inf"),
                                "max_time": -float("inf"),
                                "bytes": 0,
                                "counter": 0,
                            },
                        )
                        flow_data["min_time"] = min(flow_data["min_time"], packet_epoch_time)
                        flow_data["max_time"] = max(flow_data["min_time"], packet_epoch_time)
                        flow_data["bytes"] += int(packet.length)
                        flow_data["counter"] += 1
                        self.flows[flow] = flow_data

                    # gather http data
                    if "HTTP" in self.extracted_protocols:
                        http_layer = packet.get_multiple_layers("http")
                        if http_layer:
                            http_layer = http_layer[0]
                            all_fields = http_layer._all_fields
                            temp_http = {
                                "EntryID": self.entry_id,
                                "ID": http_layer.get("request_in", packet.number),
                                "RequestAgent": all_fields.get("http.user_agent"),
                                "RequestHost": all_fields.get("http.host"),
                                "RequestSourceIP": a,
                                "RequestURI": http_layer.get("request_full_uri"),
                                "RequestMethod": http_layer.get("request_method"),
                                "RequestVersion": http_layer.get("request_version"),
                                "RequestAcceptEncoding": http_layer.get("accept_encoding"),
                                "RequestPragma": self.reg_pragma.findall(str(http_layer))[0]
                                if self.reg_pragma.findall(str(http_layer))
                                else None,
                                "RequestAcceptLanguage": http_layer.get("accept_language"),
                                "RequestCacheControl": http_layer.get("cache_control"),
                            }
                            # if the packet is a response
                            if all_fields.get("http.response"):
                                temp_http.update(
                                    {
                                        "EntryID": self.entry_id,
                                        "ResponseStatusCode": http_layer.get("response_code"),
                                        "ResponseVersion": all_fields.get("http.response.version"),
                                        "ResponseCodeDesc": http_layer.get("response_code_desc"),
                                        "ResponseContentLength": http_layer.get("content_length"),
                                        "ResponseContentType": http_layer.get("content_type"),
                                        "ResponseDate": formatEpochDate(packet_epoch_time),
                                    }
                                )
                            add_to_data(self.protocol_data["HTTP"], temp_http)
                    if (b, a) in self.conversations:
                        a, b = b, a
                    hosts = (a, b)
                    self.conversations[hosts] = self.conversations.get(hosts, 0) + 1

                self.extract_context_from_packet(packet, layers, is_reg_extract)

        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:
            if cap:
                cap.close()


"""HELPER FUNCTIONS"""


@logger
def strip(s: str, bad_chars=None):
    """

    Args:
        s: string to strip
        bad_chars: all characters to remove from string.

    Returns:
        The input s without the bad_chars
    """
    if bad_chars is None:
        bad_chars = BAD_CHARS
    temp = s
    for char in bad_chars:
        temp = temp.replace(char, "")
    return temp


@logger
def hierarchy_to_md(hierarchy: dict) -> str:
    """

    Args:
        hierarchy: a dictionary of layer hierarchy for all packets

    Returns:
        A markdown string for displaying the hierarchy in a nice view. The script also counts the number of occurrences
        each hierarchy.
    """
    final_dict: dict[str, Any] = {}
    num_of_all_packets = 0
    for k in hierarchy:
        layer_heir = ""
        for layer in k.split(","):
            layer_heir += " -> " + layer
            final_dict[layer_heir] = final_dict.get(layer_heir, 0) + hierarchy[k]
        num_of_all_packets += hierarchy[k]
    md = "|Layer| # of Packets|% of Packets|\n|---|----|---|\n"
    for key in sorted(final_dict):
        md += f"|{key}|{final_dict[key]}|{round(final_dict[key] / num_of_all_packets, 3) * 100}%|\n"
    return md


@logger
def conversations_to_md(conversations: dict, disp_num: int) -> str:
    """

    Args:
        conversations: a raw dictionary of conversations.
        disp_num: The limit of conversations to display.

    Returns:
        A mardkown of <=disp_num of conversations, ordered in descending order.
    """
    md = "|A|B|# of Packets|\n|---|---|---|\n"
    ordered_conv_list = sorted(conversations.items(), key=lambda x: x[1], reverse=True)
    disp_num = min(disp_num, len(ordered_conv_list))
    for conv in ordered_conv_list[:disp_num]:
        (ipA, ipB), data = conv
        md += f"|{ipA}|{ipB}|{data}|\n"
    return md


@logger
def flows_to_md(flows: dict, disp_num: int) -> str:
    """

    Args:
        flows: a raw dictionary of flows.
        disp_num: The limit of flows to display.

    Returns:
        A mardkown of <=disp_num of flows, ordered in descending order.

    """
    md = "|A|port|B|port|# of Packets|\n|---|---|---|---|---|\n"
    ordered_flow_list = sorted(flows.items(), key=lambda x: x[1].get("counter"), reverse=True)
    disp_num = min(disp_num, len(ordered_flow_list))
    for flow in ordered_flow_list[:disp_num]:
        (ipA, portA, ipB, portB), data = flow
        md += f'|{ipA}|{portA}|{ipB}|{portB}|{data.get("counter", 0)}|\n'
    return md


@logger
def flows_to_ec(flows: dict) -> list:
    """

    Args:
        flows: A dictionary that hold the flows data

    Returns:
        flows data in ec format.
    """
    flows_ec = []
    for flow in flows:
        flow_data = flows[flow]
        flow_ec = {
            "SourceIP": flow[0],
            "SourcePort": flow[1],
            "DestIP": flow[2],
            "DestPort": flow[3],
            "Duration": round(flow_data.get("max_time", 0) - flow_data.get("min_time", 0)),
            "StartTime": formatEpochDate(flow_data.get("min_time", 0)),
            "EndTime": formatEpochDate(flow_data.get("max_time", 0)),
            "Bytes": flow_data.get("bytes", 0),
            "EntryID": flow_data.get("EntryID"),
            "Transport": flow_data.get("Transport"),
        }
        flows_ec.append(flow_ec)
    return flows_ec


@logger
def add_to_data(d: dict, data: dict, next_id: int = None) -> None:
    """
    updates dictionary d to include/update the data. Also removes None values.
    Args:
        d: a Dictionary of ID: data to which we want to update the data according to ID
        data: the data to update. data must have an "ID" field.
        prev_id: when there's no ID in a packet, we'd like to keep link the data by the next sequence number from TCP
            protocol. In order to keep this up to date we'd like to know what the next sequence ID and then update
            it to be the current acknowledge number.

    Returns:
        updates dictionary d to include/update the data. Also removes None values.
    """
    data_id = data.get("ID")
    if not data_id:
        return
    else:
        if not d.get(data_id):
            if list(data.keys()) == ["ID"] or list(data.keys()) == ["ID", "EntryID"]:
                # The dictionary doesn't exist and is empty (except ID/EntryID)
                return
            if next_id:
                d[next_id] = assign_params(**data)
            else:
                d[data_id] = assign_params(**data)
        else:
            if next_id and next_id > data_id:
                # id exists but we want to keep its next seq as ID (and also next seq id is larger therefore
                # there's another packet in the future)
                temp = d.pop(data_id)
                temp.update(assign_params(**data))
                d[next_id] = temp
            else:
                # ID exists, we just want to update it.
                d[data_id].update(assign_params(**data))


"""MAIN"""


def main():
    args = demisto.args()
    entry_id = args.get("entry_id", "")
    file_path = demisto.getFilePath(entry_id).get("path")

    wpa_password = args.get("wpa_password", "")
    rsa_decrypt_key_entry_id = args.get("rsa_decrypt_key_entry_id", "")
    rsa_key_file_path = None
    if rsa_decrypt_key_entry_id:
        rsa_key_file_path = demisto.getFilePath(rsa_decrypt_key_entry_id).get("path")
    conversation_number_to_display = int(args.get("convs_to_display", "15"))
    extracted_protocols = argToList(args.get("protocol_output", ""))
    if "All" in extracted_protocols:
        extracted_protocols = ALL_SUPPORTED_PROTOCOLS
    is_flows = True
    is_reg_extract = args.get("extract_strings", "False") == "True"
    pcap_filter = args.get("pcap_filter", "")
    homemade_regex = args.get("custom_regex", "")  # 'Layer (.+):'
    pcap_filter_new_file_path = ""
    pcap_filter_new_file_name = args.get("filtered_file_name", "")
    unique_ips = args.get("extract_ips", "False") == "True"

    if pcap_filter_new_file_name:
        temp = demisto.uniqueFile()
        pcap_filter_new_file_path = demisto.investigation()["id"] + "_" + temp

    try:
        pcap = PCAP(is_reg_extract, extracted_protocols, homemade_regex, unique_ips, entry_id)
        pcap.mine(file_path, wpa_password, rsa_key_file_path, is_flows, is_reg_extract, pcap_filter, pcap_filter_new_file_path)
        hr, ec, raw = pcap.get_outputs(conversation_number_to_display, is_flows, is_reg_extract)
        return_outputs(hr, ec, raw)

    except Exception as e:
        return_error(f"Unexpected error: {e!s}", error=traceback.format_exc())

    if pcap_filter_new_file_name:
        demisto.results(
            {"Contents": "", "ContentsFormat": formats["text"], "Type": 3, "File": pcap_filter_new_file_name, "FileID": temp}
        )


if __name__ in ["__main__", "builtin", "builtins"]:
    main()

README

PcapMIner V2 allows to parse PCAP files by displaying the all of the relevant data within including ip addresses, ports, flows, specific protocol breakdown, searching by regex, decrypting encrypted traffic and more.
This automation takes about a minute to process 20,000 packets (which is approximately 10MB). If you want to mine large files you can either:
a) Use the pcap_filter parameter to filter your PCAP file and thus make is smaller.
b) Copy the automation and change the default timeout parameter to match your needs.

Script Data


Name Description
Script Type python3
Tags pcap, mine, file, Utility
Cortex XSOAR Version 5.0.0

Inputs


Argument Name Description
entry_id The entry_id of the PCAP file to mine.
protocol_output A comma-separated list of protocols to output as context. If empty, will not output any protocols to context.
extract_strings Whether to extract IP, URL, and Email from PCAP file. Can be “True” or “False”.
pcap_filter Filter to apply on PCAP. Wireshark syntax as can be found here: https://www.wireshark.org/docs/man-pages/wireshark-filter.html
custom_regex Your own regular expression to extract from the PCAP.
filtered_file_name The name of the PCAP file to save to the War Room after applying the `pcap_filter` (i.e. `filtered_file.pcap`).
rsa_decrypt_key_entry_id The entry ID for the RSA decryption key.
convs_to_display Number of conversations to display. The default is 15.
wpa_password The WPA password. By providing the password you will be able to decrypt encrypted traffic data.
extract_ips Output to context the source and destination IPs in the PCAP file. Can be “True” or “False”. The default is “False”.

Outputs


Path Description Type
PCAPResults.Bytes The number of bytes transmitted in the PCAP file. Number
PCAPResults.Packets The number of packets transmitted in the PCAP file. Number
PCAPResults.EntryID The entryID of the PCAP file. String
PCAPResults.StreamCount The number of streams in the PCAP file. String
PCAPResults.StartTime The date and time of the first packet in the PCAP file. Date
PCAPResults.EndTime The date and time of the last packet in the PCAP file. String
PCAPResults.UniqueSourceIP The number of unique IPs from which packets were transmitted. Number
PCAPResults.UniqueDestIP The number of unique IPs from to packets were transmitted. Number
PCAPResultsFlow.Bytes The number of bytes transmitted in the flow. String
PCAPResultsFlow.DestIP The destination IP of the flow. String
PCAPResultsFlow.SourceIP The source IP of the flow. String
PCAPResultsFlow.Transport The transport protocol of the flow. String
PCAPResultsFlow.SourcePort The source port of the flow. String
PCAPResultsFlow.DestPort The destination port of the flow. String
PCAPResultsFlow.Duration The duration of the flow (in seconds). String
PCAPResultsFlow.EndTime The date/time the flow ended. Date
PCAPResultsFlow.StartTime The date/time the flow started. Date
PCAPResults.URL The URLs extracted from the file. String
PCAPResults.IP The IPs extracted from the file. String
PCAPResults.Email The emails extracted from the file. String
PCAPResults.Regex The regular expressions specified in `extract_regex` extracted from the file. String
PCAPResultsHTTP.ResponseStatusCode The response code. String
PCAPResultsHTTP.RequestVersion The request version. String
PCAPResultsHTTP.RequestCacheControl The cache control of the request. String
PCAPResultsHTTP.ResponseDate The date/time of the response. Date
PCAPResultsHTTP.RequestMethod The request method. String
PCAPResultsHTTP.RequestSourceIP The source IP of the request. String
PCAPResultsHTTP.ResponseContentType The response content type. String
PCAPResultsHTTP.RequestAgent The request agent. String
PCAPResultsHTTP.RequestHost The request host. String
PCAPResultsHTTP.ResponseVersion The response version. String
PCAPResultsHTTP.ID The ID of the HTTP interaction. String
PCAPResultsHTTP.EntryID The PCAP entry ID. String
PCAPResultsHTTP.RequestURI The request URI. String
PCAPResultsHTTP.ResponseContentLength The length of the response content. String
PCAPResultsHTTP.ResponseCodeDesc The code description of the response. String
PCAPResultsDNS.ID The ID of the DNS request. String
PCAPResultsDNS.Request The DNS request. String
PCAPResultsDNS.Response The DNS response. String
PCAPResultsDNS.Type The type of the DNS request. String
PCAPResultsDNS.ID The DNS packet ID. String
PCAPResultsDNS.EntryID The PCAP entry ID. String
PCAPResults.Protocols List of protocols found in the PCAP. String
PCAPResultsSMTP.From The mail sender. String
PCAPResultsSMTP.To The mail recipients. String
PCAPResultsSMTP.Subject The mail subject. String
PCAPResultsSMTP.MimeVersion The mime version. String
PCAPResultsSMTP.ID The SMTP packet’s ID. String
PCAPResultsSMTP.EntryID The PCAP entry ID. String
PCAPResultsKERBEROS.EntryID The PCAP entry ID. String
PCAPResultsKERBEROS.Realm The KERBEROS realm. String
PCAPResultsKERBEROS.SName The KERBEROS SName. String
PCAPResultsKERBEROS.CName The KERBEROS CName. String
PCAPResultsTelnet.Data The telnet data. String
PCAPResultsTelnet.Commands The telnet commands. String
PCAPResultsTelnet.EntryID The PCAP entry ID. String
PCAPResultsLLMNR.EntryID The PCAP entry ID. String
PCAPResultsLLMNR.QueryClass The LLMNR query class. String
PCAPResultsLLMNR.QueryName The LLMNR query name. String
PCAPResultsLLMNR.Questions The LLMNR questions. String
PCAPResultsLLMNR.ID The LLMNR packet ID. String
PCAPResultsLLMNR.QueryType The LLMNR query type. String
PCAPResultsSYSLOG.EntryID The PCAP entry ID. String
PCAPResultsSYSLOG.ID The SYSLOGS packet ID. String
PCAPResultsSYSLOG.Message The SYSLOGS message. String
PCAPResultsSYSLOG.Hostname The SYSLOGS host name. String
PCAPResultsSYSLOG.Timestamp The SYSLOGS time stamp. String
PCAPResultsSMB2.EntryID The PCAP entry ID. String
PCAPResultsSMB2.ID The SMB2 packet ID. String
PCAPResultsSMB2.UserName The SMB2 user name. String
PCAPResultsSMB2.Domain The SMB2 domain. String
PCAPResultsSMB2.HostName The SMB2 host name. String
PCAPResultsSMB2.Command The SMB2 command. String
PCAPResultsSMB2.FileName The SMB2 file name. String
PCAPResultsSMB2.Tree The SMB2 tree. String
PCAPResultsNETBIOS.EntryID The PCAP entry ID. String
PCAPResultsNETBIOS.ID The NETIOS packet ID. String
PCAPResultsNETBIOS.Name The NETIOS name. String
PCAPResultsNETBIOS.Type The NETIOS type. String
PCAPResultsNETBIOS.Class The NETIOS class. String
PCAPResultsIRC.EntryID The PCAP entry ID. String
PCAPResultsIRC.ID The IRC packet ID. String
PCAPResultsIRC.RequestCommand The IRC request command. String
PCAPResultsIRC.RequestTrailer The IRC request trailer. String
PCAPResultsIRC.RequestPrefix The IRC request prefix. String
PCAPResultsIRC.RequestParameters The IRC request parameters. String
PCAPResultsIRC.ResponseCommand The IRC response command. String
PCAPResultsIRC.ResponseTrailer The IRC response trailers. String
PCAPResultsIRC.ResponsePrefix The IRC response prefix. String
PCAPResultsIRC.ResponseParameters The IRC response parameters. String
PCAPResultsFTP.EntryID The PCAP entry ID. String
PCAPResultsFTP.ID The FTP packet ID. String
PCAPResultsFTP.RequestCommand The FTP request command. String
PCAPResultsFTP.ResponseArgs The FTP response arguments. String
PCAPResultsFTP.ResponseCode The FTP response code. String
PCAPResultsICMP ICMP data. String
PCAPResultsSSH.EntryID The PCAP’s entry ID. String
PCAPResultsSSH.ClientProtocols The SSH client protocols in the PCAP. String
PCAPResultsSSH.ServerProtocols The SSH server protocols in the PCAP. String
PCAPResultsSSH.KeyExchangeMessageCode The SSH key exchange message codes. String