Adding protocols as the property of edges; simplifying protocol tracking through...
[pingpong.git] / base_gefx_generator.py
index af39ffc142814c3b706f8df599fed2a303481ace..bf72c368a9da485dbd8071ec0fca0066332801f6 100644 (file)
@@ -17,81 +17,194 @@ import json
 import tldextract
 import networkx as nx
 import sys
+import csv
+import re
 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_ETH = "eth"
 JSON_KEY_ETH_SRC = "eth.src"
 JSON_KEY_ETH_DST = "eth.dst"
+JSON_KEY_IP = "ip"
+JSON_KEY_IP_SRC = "ip.src"
+JSON_KEY_IP_DST = "ip.dst"
+# Checked protocols
+JSON_KEY_UDP = "udp"
+JSON_KEY_TCP = "tcp"
+# List of checked protocols
+listchkprot = [ "arp",
+                "bootp",
+                "dhcpv6",
+                "dns",
+                "llmnr",
+                "mdns",
+                "ssdp" ]
+
+
+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(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
+    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 parse_json(file_path):
 
-    device_dns_mappings = parser.parse_dns.parse_json_dns("./json/dns.json")
+    # 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 node to a set of protocols
+    edge_to_prot = 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)
         data = json.load(jf)
-        # Loop through json objects in data
-        for k in data:
-            # Fetch timestamp of packet
-            packet_timestamp = Decimal(data[k]["ts"])
-            # Fetch eth source and destination info
-            eth_src = data[k][JSON_KEY_ETH_SRC]
-            eth_dst = data[k][JSON_KEY_ETH_DST]
-            # Traffic can be both outbound and inbound.
-            # Determine which one of the two by looking up device MAC in DNS map.
-            iot_device = None
-            if eth_src in device_dns_mappings:
-                iot_device = eth_src
-            elif eth_dst in device_dns_mappings:
-                iot_device = eth_dst
-            else:
-                print "[ WARNING: DNS mapping not found for device with MAC", eth_src, "OR", eth_dst, "]"
-                # This must be local communication between two IoT devices OR an IoT device talking to a hardcoded IP.
-                # For now let's assume local communication.
-                # Add a node for each device and an edge between them.
-                G.add_node(eth_src)
-                G.add_node(eth_dst)
-                G.add_edge(eth_src, eth_dst)
-                # TODO add regex check on src+dst IP to figure out if hardcoded server IP (e.g. check if one of the two are NOT a 192.168.x.y IP)
+
+        # 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
+            layers = p[JSON_KEY_SOURCE][JSON_KEY_LAYERS]
+
+            iscontinue = False
+            for prot in listchkprot:
+                if prot in layers:
+                    iscontinue = True
+            if iscontinue:
+                continue            
+
+            # Skip any non udp/non tcp traffic
+            if JSON_KEY_UDP not in layers and JSON_KEY_TCP not in layers:
                 continue
-            # It is outbound traffic if iot_device matches src, otherwise it must be inbound traffic.
-            outbound_traffic = iot_device == eth_src
 
-            ''' Graph construction '''
-            # No need to check if the Nodes and/or Edges we add already exist:
-            # NetworkX won't add already existing nodes/edges (except in the case of a MultiGraph or MultiDiGraph (see NetworkX doc)).
-            
-            # Add a node for each host.
-            # First add node for IoT device.
-            G.add_node(iot_device)
-            # Then add node for the server.
-            # For this we need to distinguish between outbound and inbound traffic so that we look up the proper IP in our DNS map.
-            # For outbound traffic, the server's IP is the destination IP.
-            # For inbound traffic, the server's IP is the source IP.
-            server_ip = data[k]["dst_ip"] if outbound_traffic else data[k]["src_ip"]
-            hostname = device_dns_mappings[iot_device].hostname_for_ip_at_time(server_ip, packet_timestamp)
-            if hostname is None:
-                # TODO this can occur when two local devices communicate OR if IoT device has hardcoded server IP.
-                # However, we only get here for the DNS that have not performed any DNS lookups
-                # We should use a regex check early in the loop to see if it is two local devices communicating.
-                # This way we would not have to consider these corner cases later on.
-                print "[ WARNING: no ip-hostname mapping found for ip", server_ip, " -- adding eth.src->eth.dst edge, but note that this may be incorrect if IoT device has hardcoded server IP ]"
-                G.add_node(eth_src)
-                G.add_node(eth_dst)
-                G.add_edge(eth_src, eth_dst)
+            # Fetch source and destination MACs
+            eth = layers.get(JSON_KEY_ETH, None)
+            if eth is None:
+                print "[ WARNING: eth data not found ]"
+                continue
+            eth_src = eth.get(JSON_KEY_ETH_SRC, None)
+            eth_dst = eth.get(JSON_KEY_ETH_DST, None)
+            # Exclude devices in the exclusion list
+            if eth_src in exc_list:
+                print "[ WARNING: Source ", eth_src, " is excluded from graph! ]"
                 continue
-            G.add_node(hostname)
-            # Connect the two nodes we just added.
-            if outbound_traffic:
-                G.add_edge(iot_device, hostname)
+            if eth_dst in exc_list:
+                print "[ WARNING: Destination ", eth_dst, " is excluded from graph! ]"
+                continue
+
+            # Fetch timestamp of packet (router's timestamp)
+            timestamp = Decimal(layers[JSON_KEY_FRAME][JSON_KEY_FRAME_TIME_EPOCH])
+            # 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
+            #protocol = long_protocol.rsplit(':', 1)[1]
+            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 for protocol list in the dictionary is
+            #   <src-mac-address>-<dst-mac_address>
+            protocol_key = eth_src + "-" + eth_dst
+            if protocol_key not in edge_to_prot:
+                edge_to_prot[protocol_key] = set()
+            protocols = edge_to_prot[protocol_key]
+            protocols.add(protocol)
+            protocols_str = ', '.join(protocols)
+            print "protocols: ", protocols_str, "\n"
+            # 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, "\n"
+            
+            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:
-                G.add_edge(hostname, iot_device)
+                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
+                G.add_node(hostname)
+                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
+                G.add_node(hostname)
+                dst_node = hostname
+            G.add_edge(src_node, dst_node, Protocol=protocols_str)
+
+    # Print DNS mapping for reference
+       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.
@@ -109,6 +222,7 @@ def is_IP(addr):
         return False
 # ------------------------------------------------------
 
+
 if __name__ == '__main__':
     if len(sys.argv) < 3:
         print "Usage:", sys.argv[0], "input_file output_file"