6c5c62ed9897158a9866e0126c85e366327dae88
[pingpong.git] / bipartite_iot_web_gexf_generator.py
1 #!/usr/bin/python
2
3 """
4 Extension of base_gefx_generator.py.
5 This script constructs a bipartite graph with IoT devices on one side and Internet hosts on the other side.
6 As a result, this graph does NOT show inter IoT device communication.
7
8 The input to this script is the Wirshark's/tshark's JSON representation of a packet trace.
9
10 """
11
12 import socket
13 import json
14 import tldextract
15 import networkx as nx
16
17 from networkx.algorithms import bipartite 
18
19 import sys
20 import csv
21 import re
22 from decimal import *
23
24 import parser.parse_dns
25
26 DEVICE_MAC_LIST = "devicelist.dat"
27 COLUMN_MAC = "MAC_address"
28 COLUMN_DEVICE_NAME = "device_name"
29
30
31 JSON_KEY_SOURCE = "_source"
32 JSON_KEY_LAYERS = "layers"
33 JSON_KEY_FRAME = "frame"
34 JSON_KEY_FRAME_TIME_EPOCH = "frame.time_epoch"
35 JSON_KEY_ETH = "eth"
36 JSON_KEY_ETH_SRC = "eth.src"
37 JSON_KEY_ETH_DST = "eth.dst"
38 JSON_KEY_IP = "ip"
39 JSON_KEY_IP_SRC = "ip.src"
40 JSON_KEY_IP_DST = "ip.dst"
41 JSON_KEY_UDP = "udp"
42 JSON_KEY_TCP = "tcp"
43 JSON_KEY_MDNS = "mdns"
44 JSON_KEY_BOOTP = "bootp"
45 JSON_KEY_SSDP = "ssdp"
46 JSON_KEY_DHCPV6 = "dhcpv6"
47 JSON_KEY_LLMNR = "llmnr"
48
49
50 def parse_json(file_path):
51
52     # Open the device MAC list file
53     with open(DEVICE_MAC_LIST) as csvfile:
54         maclist = csv.DictReader(csvfile, (COLUMN_MAC, COLUMN_DEVICE_NAME))
55         crudelist = list()
56         for item in maclist:
57             crudelist.append(item)
58             #print(item)
59     # Create key-value dictionary
60     devlist = dict()
61     for item in crudelist:
62         devlist[item[COLUMN_MAC]] = item[COLUMN_DEVICE_NAME]
63
64     # First parse the file once, constructing a map that contains information about individual devices' DNS resolutions.
65     device_dns_mappings = parser.parse_dns.parse_json_dns(file_path) # "./json/eth1.dump.json"
66
67     # Init empty graph
68     G = nx.DiGraph() 
69     # Parse file again, this time constructing a graph of device<->server and device<->device communication.
70     with open(file_path) as jf:
71         # Read JSON.
72         # data becomes reference to root JSON object (or in our case json array)
73         data = json.load(jf)
74         # Loop through json objects (packets) in data
75         for p in data:
76             # p is a JSON object, not an index
77             # Drill down to object containing data from the different layers
78             layers = p[JSON_KEY_SOURCE][JSON_KEY_LAYERS]
79
80             # Skip all MDNS traffic.
81             if JSON_KEY_MDNS in layers:
82                 continue
83
84             # Skip all LLMNR traffic.
85             if JSON_KEY_LLMNR in layers:
86                 continue
87
88             # Skip all SSDP traffic - we don't care about disovery, only the actual communication.
89             if JSON_KEY_SSDP in layers:
90                 continue
91
92             # Skip all bootp traffic (DHCP related)
93             if JSON_KEY_BOOTP in layers:
94                 continue
95
96             # Skip DHCPv6 for now.
97             if JSON_KEY_DHCPV6 in layers:
98                 continue
99
100             # Skip any non udp/non tcp traffic
101             if JSON_KEY_UDP not in layers and JSON_KEY_TCP not in layers:
102                 continue
103
104             # Skip any non IP traffic
105             if JSON_KEY_IP not in layers:
106                 continue
107             
108             # Fetch timestamp of packet (router's timestamp)
109             packet_timestamp = Decimal(layers[JSON_KEY_FRAME][JSON_KEY_FRAME_TIME_EPOCH])
110             # Fetch source and destination MACs
111             eth = layers.get(JSON_KEY_ETH, None)
112             if eth is None:
113                 print "[ WARNING: eth data not found ]"
114                 continue
115             eth_src = eth.get(JSON_KEY_ETH_SRC, None)
116             eth_dst = eth.get(JSON_KEY_ETH_DST, None)
117             # And source and destination IPs
118             ip_src = layers[JSON_KEY_IP][JSON_KEY_IP_SRC]
119             ip_dst = layers[JSON_KEY_IP][JSON_KEY_IP_DST]
120
121             src_is_local = ip_src.startswith("192.168.") 
122             dst_is_local = ip_dst.startswith("192.168.")
123
124             src_node = None
125             dst_node = None
126
127             # Values for the 'bipartite' attribute of a node when constructing the bipartite graph
128             bipartite_iot = 0
129             bipartite_web_server = 1
130
131             # Skip inter-IoT device communication.
132             if src_is_local and dst_is_local:
133                 continue
134
135             if src_is_local:
136                 G.add_node(eth_src, Name=devlist[eth_src], bipartite=bipartite_iot)
137                 src_node = eth_src
138             else:
139                 # If the source is not local, then it's inbound traffic, and hence the eth_dst is the MAC of the IoT device.
140                 hostname = None
141                 # Guard against cases where a device does not perform DNS lookups (or the lookups occur before data collection starts)
142                 if eth_dst in device_dns_mappings:
143                     hostname = device_dns_mappings[eth_dst].hostname_for_ip_at_time(ip_src, packet_timestamp)
144                 else:
145                     print "[ WARNING: No entry for", eth_dst, "in DNS query map ]"
146
147                 if hostname is None:
148                     # Use IP if no hostname mapping
149                     hostname = ip_src
150                 G.add_node(hostname, bipartite=bipartite_web_server)
151                 src_node = hostname
152             if dst_is_local:
153                 G.add_node(eth_dst, Name=devlist[eth_src], bipartite=bipartite_iot)
154                 dst_node = eth_dst
155             else:
156                 # If the destination is not local, then it's outbound traffic, and hence the eth_src is the MAC of the IoT device.
157                 hostname = None
158                 # Guard against cases where a device does not perform DNS lookups (or the lookups occur before data collection starts)
159                 if eth_src in device_dns_mappings:
160                     hostname = device_dns_mappings[eth_src].hostname_for_ip_at_time(ip_dst, packet_timestamp)
161                 else:
162                     print "[ WARNING: No entry for", eth_src, "in DNS query map ]"
163                 if hostname is None:
164                     # Use IP if no hostname mapping
165                     hostname = ip_dst
166                 G.add_node(hostname, bipartite=bipartite_web_server)
167                 dst_node = hostname
168             G.add_edge(src_node, dst_node)
169     return G
170
171 if __name__ == '__main__':
172     if len(sys.argv) < 3:
173         print "Usage:", sys.argv[0], "input_file output_file"
174         print "outfile_file should end in .gexf"
175         sys.exit(0)
176     # Input file: Path to Wireshark/tshark JSON file.
177     input_file = sys.argv[1]
178     print "[ input_file  =", input_file, "]"
179     # Output file: Path to file where the Gephi XML should be written.
180     output_file = sys.argv[2]
181     print "[ output_file =", output_file, "]"
182     # Construct graph from JSON
183     G = parse_json(input_file)
184     # Write Graph in Graph Exchange XML format
185     nx.write_gexf(G, output_file)