Adding new analysis - incoming (not yet including outgoing) packets inter-arrival...
[pingpong.git] / base_gefx_generator.py
index 4031e0ca3eed82c89a1b8e8702cecf4bb0d7aaa4..d33c9bc36ff58310d4b10331dbb715adcad558b3 100644 (file)
@@ -23,15 +23,18 @@ from decimal import *
 
 import parser.parse_dns
 
+# List of devices
 DEVICE_MAC_LIST = "devicelist.dat"
+EXCLUSION_MAC_LIST = "exclusion.dat"
 COLUMN_MAC = "MAC_address"
 COLUMN_DEVICE_NAME = "device_name"
-
-
+# Fields
 JSON_KEY_SOURCE = "_source"
 JSON_KEY_LAYERS = "layers"
 JSON_KEY_FRAME = "frame"
+JSON_KEY_FRAME_PROTOCOLS = "frame.protocols"
 JSON_KEY_FRAME_TIME_EPOCH = "frame.time_epoch"
+JSON_KEY_FRAME_LENGTH = "frame.len"
 JSON_KEY_ETH = "eth"
 JSON_KEY_ETH_SRC = "eth.src"
 JSON_KEY_ETH_DST = "eth.dst"
@@ -42,7 +45,8 @@ JSON_KEY_IP_DST = "ip.dst"
 JSON_KEY_UDP = "udp"
 JSON_KEY_TCP = "tcp"
 # List of checked protocols
-listchkprot = [ "bootp",
+listchkprot = [ "arp",
+                "bootp",
                 "dhcpv6",
                 "dns",
                 "llmnr",
@@ -50,38 +54,197 @@ listchkprot = [ "bootp",
                 "ssdp" ]
 
 
-def parse_json(file_path):
-
+def create_device_list(dev_list_file):
+    """ Create list for smart home devices from a CSV file
+        Args:
+            dev_list_file: CSV file path that contains list of device MAC addresses
+    """
     # Open the device MAC list file
-    with open(DEVICE_MAC_LIST) as csvfile:
-        maclist = csv.DictReader(csvfile, (COLUMN_MAC, COLUMN_DEVICE_NAME))
-        crudelist = list()
-        for item in maclist:
-            crudelist.append(item)
-            #print(item)
+    with open(dev_list_file) as csvfile:
+        mac_list = csv.DictReader(csvfile, (COLUMN_MAC, COLUMN_DEVICE_NAME))
+        crude_list = list()
+        for item in mac_list:
+            crude_list.append(item)
     # Create key-value dictionary
-    devlist = dict()
-    for item in crudelist:
-        devlist[item[COLUMN_MAC]] = item[COLUMN_DEVICE_NAME]
+    dev_list = dict()
+    for item in crude_list:
+        dev_list[item[COLUMN_MAC]] = item[COLUMN_DEVICE_NAME]
         #print item["MAC_address"] + " => " + item["device_name"]
     #for key, value in devlist.iteritems():
     #    print key + " => " + value
 
+    return dev_list
+
+
+def traverse_and_merge_nodes(G, dev_list_file):
+    """ Merge nodes that have similar properties, e.g. same protocols
+        But, we only do this for leaves (outer nodes), and not for
+        nodes that are in the middle/have many neighbors.
+        The pre-condition is that the node:
+        (1) only has one neighbor, and
+        (2) not a smarthome device.
+        then we compare the edges, whether they use the same protocols
+        or not. If yes, then we collapse that node and we attach
+        it to the very first node that uses that set of protocols.
+        Args:
+            G: a complete networkx graph
+            dev_list_file: CSV file path that contains list of device MAC addresses
+    """
+    nodes = G.nodes()
+    #print "Nodes: ", nodes
+    node_to_merge = dict()
+    # Create list of smarthome devices
+    dev_list = create_device_list(DEVICE_MAC_LIST)
+    # Traverse every node
+    # Check that the node is not a smarthome device
+    for node in nodes:
+        neighbors = G.neighbors(node)
+        #print "Neighbors: ", neighbors, "\n"
+        # Skip if the node is a smarthome device
+        if node in dev_list:
+            continue
+        # Skip if the node has many neighbors (non-leaf) or no neighbor at all
+        if len(neighbors) is not 1:
+            continue
+        #print "Node: ", node
+        neighbor = neighbors[0]
+        #print "Neighbor: ", neighbors
+        protocols = G[node][neighbor]['Protocol']
+        #print "Protocol: ", protocols
+        # Store neighbor-protocol as key in dictionary
+        neigh_proto = neighbor + "-" + protocols
+        if neigh_proto not in node_to_merge:
+            node_to_merge[neigh_proto] = node
+        else:
+        # Merge this node if there is already an entry
+            # First delete
+            G.remove_node(node)
+            node_to_merge_with = node_to_merge[neigh_proto]
+            merged_nodes = G.node[node_to_merge_with]['Merged']
+            # Check if this is the first node
+            if merged_nodes is '':
+                merged_nodes = node
+            else:
+            # Put comma if there is already one or more nodes
+                merged_nodes += ", " + node
+            # Then attach as attribute
+            G.node[node_to_merge_with]['Merged'] = merged_nodes
+
+    return G
+
+
+def place_in_graph(G, eth_src, eth_dst, device_dns_mappings, dev_list, layers, 
+        edge_to_prot, edge_to_vol):
+    """ Place nodes and edges on the graph
+        Args:
+            G: the complete graph
+            eth_src: MAC address of source
+            eth_dst: MAC address of destination
+            device_dns_mappings: device to DNS mappings (data structure)
+            dev_list: list of existing smarthome devices
+            layers: layers of JSON file structure
+            edge_to_prot: edge to protocols mappings
+            edge_to_vol: edge to traffic volume mappings
+    """
+    # Get timestamp of packet (router's timestamp)
+    timestamp = Decimal(layers[JSON_KEY_FRAME][JSON_KEY_FRAME_TIME_EPOCH])
+    # Get packet length
+    packet_len = Decimal(layers[JSON_KEY_FRAME][JSON_KEY_FRAME_LENGTH])
+    # Get the protocol and strip just the name of it
+    long_protocol = layers[JSON_KEY_FRAME][JSON_KEY_FRAME_PROTOCOLS]
+    # Split once starting from the end of the string and get it
+    split_protocol = long_protocol.split(':')
+    protocol = None
+    if len(split_protocol) < 5:
+        last_index = len(split_protocol) - 1
+        protocol = split_protocol[last_index]
+    else:
+        protocol = split_protocol[3] + ":" + split_protocol[4]
+    print "timestamp: ", timestamp, " - new protocol added: ", protocol, "\n"
+    # Store protocol into the set (source)
+    protocols = None
+    # Key to search in the dictionary is <src-mac-address>-<dst-mac_address>
+    dict_key = eth_src + "-" + eth_dst
+    if dict_key not in edge_to_prot:
+        edge_to_prot[dict_key] = set()
+    protocols = edge_to_prot[dict_key]
+    protocols.add(protocol)
+    protocols_str = ', '.join(protocols)
+    print "protocols: ", protocols_str, "\n"
+    # Check packet length and accumulate to get traffic volume
+    if dict_key not in edge_to_vol:
+        edge_to_vol[dict_key] = 0;
+    edge_to_vol[dict_key] = edge_to_vol[dict_key] + packet_len
+    volume = str(edge_to_vol[dict_key])
+    # And source and destination IPs
+    ip_src = layers[JSON_KEY_IP][JSON_KEY_IP_SRC]
+    ip_dst = layers[JSON_KEY_IP][JSON_KEY_IP_DST]
+    # Categorize source and destination IP addresses: local vs. non-local
+    ip_re = re.compile(r'\b192.168.[0-9.]+')
+    src_is_local = ip_re.search(ip_src) 
+    dst_is_local = ip_re.search(ip_dst)
+    print "ip.src =", ip_src, "ip.dst =", ip_dst, "\n"
+    # Place nodes and edges
+    src_node = None
+    dst_node = None
+    if src_is_local:
+        G.add_node(eth_src, Name=dev_list[eth_src])
+        src_node = eth_src
+    else:
+        hostname = None
+        # Check first if the key (eth_dst) exists in the dictionary
+        if eth_dst in device_dns_mappings:
+            # If the source is not local, then it's inbound traffic, and hence the eth_dst is the MAC of the IoT device.
+            hostname = device_dns_mappings[eth_dst].hostname_for_ip_at_time(ip_src, timestamp)                   
+        if hostname is None:
+            # Use IP if no hostname mapping
+            hostname = ip_src
+        # Non-smarthome devices can be merged later
+        G.add_node(hostname, Merged='')
+        src_node = hostname
+
+    if dst_is_local:
+        G.add_node(eth_dst, Name=dev_list[eth_dst])
+        dst_node = eth_dst
+    else:
+        hostname = None
+        # Check first if the key (eth_dst) exists in the dictionary
+        if eth_src in device_dns_mappings:
+            # If the destination is not local, then it's outbound traffic, and hence the eth_src is the MAC of the IoT device.
+            hostname = device_dns_mappings[eth_src].hostname_for_ip_at_time(ip_dst, timestamp)
+        if hostname is None:
+            # Use IP if no hostname mapping
+            hostname = ip_dst
+        # Non-smarthome devices can be merged later
+        G.add_node(hostname, Merged='')
+        dst_node = hostname
+    G.add_edge(src_node, dst_node, Protocol=protocols_str, Volume=volume)
+
+
+def parse_json(file_path):
+    """ Parse JSON file and create graph
+        Args:
+            file_path: path to the JSON file
+    """
+    # Create a smart home device list
+    dev_list = create_device_list(DEVICE_MAC_LIST)
+    # Create an exclusion list
+    exc_list = create_device_list(EXCLUSION_MAC_LIST)
     # First parse the file once, constructing a map that contains information about individual devices' DNS resolutions.
     device_dns_mappings = parser.parse_dns.parse_json_dns(file_path) # "./json/eth1.dump.json"
-    
     # Init empty graph
-    G = nx.DiGraph() 
+    G = nx.DiGraph()
+    # Mapping from edge to a set of protocols
+    edge_to_prot = dict()
+    # Mapping from edge to traffic volume
+    edge_to_vol = dict()
     # Parse file again, this time constructing a graph of device<->server and device<->device communication.
     with open(file_path) as jf:
-        # Read JSON.
-        # data becomes reference to root JSON object (or in our case json array)
+        # Read JSON; data becomes reference to root JSON object (or in our case json array)
         data = json.load(jf)
-
         # Loop through json objects (packets) in data
         for p in data:
-            # p is a JSON object, not an index
-            # Drill down to object containing data from the different layers
+            # p is a JSON object, not an index - drill down to object containing data from the different layers
             layers = p[JSON_KEY_SOURCE][JSON_KEY_LAYERS]
 
             iscontinue = False
@@ -95,9 +258,6 @@ def parse_json(file_path):
             if JSON_KEY_UDP not in layers and JSON_KEY_TCP not in layers:
                 continue
 
-            # Fetch timestamp of packet (router's timestamp)
-            packet_timestamp = Decimal(layers[JSON_KEY_FRAME][JSON_KEY_FRAME_TIME_EPOCH])
-            print "timestamp", packet_timestamp
             # Fetch source and destination MACs
             eth = layers.get(JSON_KEY_ETH, None)
             if eth is None:
@@ -105,55 +265,26 @@ def parse_json(file_path):
                 continue
             eth_src = eth.get(JSON_KEY_ETH_SRC, None)
             eth_dst = eth.get(JSON_KEY_ETH_DST, None)
-            # And source and destination IPs
-            ip_src = layers[JSON_KEY_IP][JSON_KEY_IP_SRC]
-            ip_dst = layers[JSON_KEY_IP][JSON_KEY_IP_DST]
-
-            # Categorize source and destination IP addresses: local vs. non-local
-            ipre = re.compile(r'\b192.168.[0-9.]+')
-            src_is_local = ipre.search(ip_src) 
-            dst_is_local = ipre.search(ip_dst)
-            print "ip.src =", ip_src, "ip.dst =", ip_dst
-
-            src_node = None
-            dst_node = None
-            if src_is_local:
-                G.add_node(eth_src, Name=devlist[eth_src])
-                src_node = eth_src
-            else:
-                hostname = None
-                # Check first if the key (eth_dst) exists in the dictionary
-                if eth_dst in device_dns_mappings:
-                    # If the source is not local, then it's inbound traffic, and hence the eth_dst is the MAC of the IoT device.
-                    hostname = device_dns_mappings[eth_dst].hostname_for_ip_at_time(ip_src, packet_timestamp)                   
-                if hostname is None:
-                    # Use IP if no hostname mapping
-                    hostname = ip_src
-                G.add_node(hostname)
-                src_node = hostname
-            if dst_is_local:
-                G.add_node(eth_dst, Name=devlist[eth_src])
-                dst_node = eth_dst
-            else:
-                hostname = None
-                # Check first if the key (eth_dst) exists in the dictionary
-                if eth_src in device_dns_mappings:
-                    # If the destination is not local, then it's outbound traffic, and hence the eth_src is the MAC of the IoT device.
-                    hostname = device_dns_mappings[eth_src].hostname_for_ip_at_time(ip_dst, packet_timestamp)
-                if hostname is None:
-                    # Use IP if no hostname mapping
-                    hostname = ip_dst
-                G.add_node(hostname)
-                dst_node = hostname
-            G.add_edge(src_node, dst_node)
+            # Exclude devices in the exclusion list
+            if eth_src in exc_list:
+                print "[ WARNING: Source ", eth_src, " is excluded from graph! ]"
+                continue
+            if eth_dst in exc_list:
+                print "[ WARNING: Destination ", eth_dst, " is excluded from graph! ]"
+                continue
+           
+            # Place nodes and edges in graph
+            place_in_graph(G, eth_src, eth_dst, device_dns_mappings, dev_list, layers, 
+                edge_to_prot, edge_to_vol)
 
     # Print DNS mapping for reference
-       for mac in device_dns_mappings:
-               ddm = device_dns_mappings[mac]
-               ddm.print_mappings()
+       #for mac in device_dns_mappings:
+       #       ddm = device_dns_mappings[mac]
+       #       ddm.print_mappings()
     
     return G
 
+
 # ------------------------------------------------------
 # Not currently used.
 # Might be useful later on if we wish to resolve IPs.
@@ -171,6 +302,7 @@ def is_IP(addr):
         return False
 # ------------------------------------------------------
 
+
 if __name__ == '__main__':
     if len(sys.argv) < 3:
         print "Usage:", sys.argv[0], "input_file output_file"
@@ -184,5 +316,7 @@ if __name__ == '__main__':
     print "[ output_file =", output_file, "]"
     # Construct graph from JSON
     G = parse_json(input_file)
+    # Contract nodes that have the same properties, i.e. same protocols
+    G = traverse_and_merge_nodes(G, DEVICE_MAC_LIST)
     # Write Graph in Graph Exchange XML format
     nx.write_gexf(G, output_file)