eed37c050b2fdf8319cd677feeeb2623f9d1daed
[pingpong.git] / base_gexf_generator.py
1 #!/usr/bin/python
2
3 """
4 Script that constructs a graph in which hosts are nodes.
5 An edge between two hosts indicate that the hosts communicate.
6 Hosts are labeled and identified by their IPs.
7 The graph is written to a file in Graph Exchange XML format for later import and visual inspection in Gephi.
8
9 Update per February 2, 2018:
10 Extension of base_gefx_generator.py.
11 This script constructs a bipartite graph with IoT devices on one side and Internet hosts on the other side.
12 As a result, this graph does NOT show inter IoT device communication.
13
14 The input to this script is the JSON output by extract_from_tshark.py by Anastasia Shuba.
15
16 This script is a simplification of Milad Asgari's parser_data_to_gephi.py script.
17 It serves as a baseline for future scripts that want to include more information in the graph.
18 """
19
20 import socket
21 import json
22 import tldextract
23 import networkx as nx
24 import sys
25 import csv
26 import re
27 import parser.parse_dns
28 from decimal import *
29 from networkx.algorithms import bipartite
30
31 # List of devices
32 DEVICE_MAC_LIST = "devicelist.dat"
33 EXCLUSION_MAC_LIST = "exclusion.dat"
34 COLUMN_MAC = "MAC_address"
35 COLUMN_DEVICE_NAME = "device_name"
36 # Fields
37 JSON_KEY_SOURCE = "_source"
38 JSON_KEY_LAYERS = "layers"
39 JSON_KEY_FRAME = "frame"
40 JSON_KEY_FRAME_PROTOCOLS = "frame.protocols"
41 JSON_KEY_FRAME_TIME_EPOCH = "frame.time_epoch"
42 JSON_KEY_FRAME_LENGTH = "frame.len"
43 JSON_KEY_ETH = "eth"
44 JSON_KEY_ETH_SRC = "eth.src"
45 JSON_KEY_ETH_DST = "eth.dst"
46 JSON_KEY_IP = "ip"
47 JSON_KEY_IP_SRC = "ip.src"
48 JSON_KEY_IP_DST = "ip.dst"
49 # Checked protocols
50 JSON_KEY_UDP = "udp"
51 JSON_KEY_TCP = "tcp"
52 # List of checked protocols
53 listchkprot = [ "arp",
54                 "bootp",
55                 "dhcpv6",
56                 "dns",
57                 "llmnr",
58                 "mdns",
59                 "ssdp" ]
60
61 # Switch to generate graph that only shows local communication
62 ONLY_INCLUDE_LOCAL_COMMUNICATION = False
63
64
65 def create_device_list(dev_list_file):
66     """ Create list for smart home devices from a CSV file
67         Args:
68             dev_list_file: CSV file path that contains list of device MAC addresses
69     """
70     # Open the device MAC list file
71     with open(dev_list_file) as csvfile:
72         mac_list = csv.DictReader(csvfile, (COLUMN_MAC, COLUMN_DEVICE_NAME))
73         crude_list = list()
74         for item in mac_list:
75             crude_list.append(item)
76     # Create key-value dictionary
77     dev_list = dict()
78     for item in crude_list:
79         dev_list[item[COLUMN_MAC]] = item[COLUMN_DEVICE_NAME]
80         #print item["MAC_address"] + " => " + item["device_name"]
81     #for key, value in devlist.iteritems():
82     #    print key + " => " + value
83
84     return dev_list
85
86
87 def traverse_and_merge_nodes(G, dev_list_file):
88     """ Merge nodes that have similar properties, e.g. same protocols
89         But, we only do this for leaves (outer nodes), and not for
90         nodes that are in the middle/have many neighbors.
91         The pre-condition is that the node:
92         (1) only has one neighbor, and
93         (2) not a smarthome device.
94         then we compare the edges, whether they use the same protocols
95         or not. If yes, then we collapse that node and we attach
96         it to the very first node that uses that set of protocols.
97         Args:
98             G: a complete networkx graph
99             dev_list_file: CSV file path that contains list of device MAC addresses
100     """
101     nodes = G.nodes()
102     #print "Nodes: ", nodes
103     node_to_merge = dict()
104     # Create list of smarthome devices
105     dev_list = create_device_list(DEVICE_MAC_LIST)
106     # Traverse every node
107     # Check that the node is not a smarthome device
108     for node in nodes:
109         neighbors = G[node] #G.neighbors(node)
110         #print "Neighbors: ", neighbors, "\n"
111         # Skip if the node is a smarthome device
112         if node in dev_list:
113             continue
114         # Skip if the node has many neighbors (non-leaf) or no neighbor at all
115         if len(neighbors) is not 1:
116             continue
117         #print "Node: ", node
118         neighbor = neighbors.keys()[0] #neighbors[0]
119         #print "Neighbor: ", neighbors
120         protocols = G[node][neighbor]['Protocol']
121         #print "Protocol: ", protocols
122         # Store neighbor-protocol as key in dictionary
123         neigh_proto = neighbor + "-" + protocols
124         if neigh_proto not in node_to_merge:
125             node_to_merge[neigh_proto] = node
126         else:
127         # Merge this node if there is already an entry
128             # First delete
129             G.remove_node(node)
130             node_to_merge_with = node_to_merge[neigh_proto]
131             merged_nodes = G.node[node_to_merge_with]['Merged']
132             # Check if this is the first node
133             if merged_nodes is '':
134                 merged_nodes = node
135             else:
136             # Put comma if there is already one or more nodes
137                 merged_nodes += ", " + node
138             # Then attach as attribute
139             G.node[node_to_merge_with]['Merged'] = merged_nodes
140
141     return G
142
143
144 def place_in_graph(G, eth_src, eth_dst, device_dns_mappings, dev_list, layers, 
145         edge_to_prot, edge_to_vol):
146     """ Place nodes and edges on the graph
147         Args:
148             G: the complete graph
149             eth_src: MAC address of source
150             eth_dst: MAC address of destination
151             device_dns_mappings: device to DNS mappings (data structure)
152             dev_list: list of existing smarthome devices
153             layers: layers of JSON file structure
154             edge_to_prot: edge to protocols mappings
155             edge_to_vol: edge to traffic volume mappings
156     """
157     # Get timestamp of packet (router's timestamp)
158     timestamp = Decimal(layers[JSON_KEY_FRAME][JSON_KEY_FRAME_TIME_EPOCH])
159     # Get packet length
160     packet_len = Decimal(layers[JSON_KEY_FRAME][JSON_KEY_FRAME_LENGTH])
161     # Get the protocol and strip just the name of it
162     long_protocol = layers[JSON_KEY_FRAME][JSON_KEY_FRAME_PROTOCOLS]
163     # Split once starting from the end of the string and get it
164     split_protocol = long_protocol.split(':')
165     protocol = None
166     if len(split_protocol) < 5:
167         last_index = len(split_protocol) - 1
168         protocol = split_protocol[last_index]
169     else:
170         protocol = split_protocol[3] + ":" + split_protocol[4]
171     #print "timestamp: ", timestamp, " - new protocol added: ", protocol, "\n"
172     # Store protocol into the set (source)
173     protocols = None
174     # Key to search in the dictionary is <src-mac-address>-<dst-mac_address>
175     dict_key = eth_src + "-" + eth_dst
176     if dict_key not in edge_to_prot:
177         edge_to_prot[dict_key] = set()
178     protocols = edge_to_prot[dict_key]
179     protocols.add(protocol)
180     protocols_str = ', '.join(protocols)
181     #print "protocols: ", protocols_str, "\n"
182     # Check packet length and accumulate to get traffic volume
183     if dict_key not in edge_to_vol:
184         edge_to_vol[dict_key] = 0;
185     edge_to_vol[dict_key] = edge_to_vol[dict_key] + packet_len
186     volume = str(edge_to_vol[dict_key])
187     # And source and destination IPs
188     ip_src = layers[JSON_KEY_IP][JSON_KEY_IP_SRC]
189     ip_dst = layers[JSON_KEY_IP][JSON_KEY_IP_DST]
190     # Categorize source and destination IP addresses: local vs. non-local
191     ip_re = re.compile(r'\b192.168.[0-9.]+')
192     src_is_local = ip_re.search(ip_src) 
193     dst_is_local = ip_re.search(ip_dst)
194
195     # Skip device to cloud communication if we are interested in the local graph.
196     # TODO should this go before the protocol dict is changed?
197     if ONLY_INCLUDE_LOCAL_COMMUNICATION and not (src_is_local and dst_is_local):
198         return
199
200     #print "ip.src =", ip_src, "ip.dst =", ip_dst, "\n"
201     # Place nodes and edges
202     src_node = None
203     dst_node = None
204     # Integer values used for tagging nodes, indicating to Gephi if they are local IoT devices or web servers.
205     remote_node = 0
206     local_node = 1
207     # Values for the 'bipartite' attribute of a node when constructing the bipartite graph
208     bipartite_iot = 0
209     bipartite_web_server = 1
210     if src_is_local:
211         G.add_node(eth_src, Name=dev_list[eth_src], islocal=local_node, bipartite=bipartite_iot)
212         src_node = eth_src
213     else:
214         hostname = None
215         # Check first if the key (eth_dst) exists in the dictionary
216         if eth_dst in device_dns_mappings:
217             # If the source is not local, then it's inbound traffic, and hence the eth_dst is the MAC of the IoT device.
218             hostname = device_dns_mappings[eth_dst].hostname_for_ip_at_time(ip_src, timestamp)                   
219         if hostname is None:
220             # Use IP if no hostname mapping
221             hostname = ip_src
222         # Non-smarthome devices can be merged later
223         G.add_node(hostname, Merged='', islocal=remote_node, bipartite=bipartite_web_server)
224         src_node = hostname
225
226     if dst_is_local:
227         G.add_node(eth_dst, Name=dev_list[eth_dst], islocal=local_node, bipartite=bipartite_iot)
228         dst_node = eth_dst
229     else:
230         hostname = None
231         # Check first if the key (eth_dst) exists in the dictionary
232         if eth_src in device_dns_mappings:
233             # If the destination is not local, then it's outbound traffic, and hence the eth_src is the MAC of the IoT device.
234             hostname = device_dns_mappings[eth_src].hostname_for_ip_at_time(ip_dst, timestamp)
235         if hostname is None:
236             # Use IP if no hostname mapping
237             hostname = ip_dst
238         # Non-smarthome devices can be merged later
239         G.add_node(hostname, Merged='', islocal=remote_node, bipartite=bipartite_web_server)
240         dst_node = hostname
241     G.add_edge(src_node, dst_node, Protocol=protocols_str, Volume=volume)
242
243
244 def parse_json(file_path):
245     """ Parse JSON file and create graph
246         Args:
247             file_path: path to the JSON file
248     """
249     # Create a smart home device list
250     dev_list = create_device_list(DEVICE_MAC_LIST)
251     # Create an exclusion list
252     exc_list = create_device_list(EXCLUSION_MAC_LIST)
253     # First parse the file once, constructing a map that contains information about individual devices' DNS resolutions.
254     device_dns_mappings = parser.parse_dns.parse_json_dns(file_path) # "./json/eth1.dump.json"
255     # Init empty graph
256     G = nx.DiGraph()
257     # Mapping from edge to a set of protocols
258     edge_to_prot = dict()
259     # Mapping from edge to traffic volume
260     edge_to_vol = dict()
261     # Parse file again, this time constructing a graph of device<->server and device<->device communication.
262     with open(file_path) as jf:
263         # Read JSON; data becomes reference to root JSON object (or in our case json array)
264         data = json.load(jf)
265         # Loop through json objects (packets) in data
266         for p in data:
267             # p is a JSON object, not an index - drill down to object containing data from the different layers
268             layers = p[JSON_KEY_SOURCE][JSON_KEY_LAYERS]
269
270             iscontinue = False
271             for prot in listchkprot:
272                 if prot in layers:
273                     iscontinue = True
274             if iscontinue:
275                 continue            
276
277             # Skip any non udp/non tcp traffic
278             if JSON_KEY_UDP not in layers and JSON_KEY_TCP not in layers:
279                 continue
280
281             # Fetch source and destination MACs
282             eth = layers.get(JSON_KEY_ETH, None)
283             if eth is None:
284                 print "[ WARNING: eth data not found ]"
285                 continue
286             eth_src = eth.get(JSON_KEY_ETH_SRC, None)
287             eth_dst = eth.get(JSON_KEY_ETH_DST, None)
288             # Exclude devices in the exclusion list
289             if eth_src in exc_list:
290                 print "[ WARNING: Source ", eth_src, " is excluded from graph! ]"
291                 continue
292             if eth_dst in exc_list:
293                 print "[ WARNING: Destination ", eth_dst, " is excluded from graph! ]"
294                 continue
295            
296             # Place nodes and edges in graph
297             place_in_graph(G, eth_src, eth_dst, device_dns_mappings, dev_list, layers, 
298                 edge_to_prot, edge_to_vol)
299
300     # Print DNS mapping for reference
301         #for mac in device_dns_mappings:
302         #       ddm = device_dns_mappings[mac]
303         #       ddm.print_mappings()
304     
305     return G
306
307
308 # ------------------------------------------------------
309 # Not currently used.
310 # Might be useful later on if we wish to resolve IPs.
311 def get_domain(host):
312     ext_result = tldextract.extract(str(host))
313     # Be consistent with ReCon and keep suffix
314     domain = ext_result.domain + "." + ext_result.suffix
315     return domain
316
317 def is_IP(addr):
318     try:
319         socket.inet_aton(addr)
320         return True
321     except socket.error:
322         return False
323 # ------------------------------------------------------
324
325
326 if __name__ == '__main__':
327     if len(sys.argv) < 3:
328         print "Usage:", sys.argv[0], "input_file output_file"
329         print "outfile_file should end in .gexf"
330         sys.exit(0)
331     # Input file: Path to JSON file generated from tshark JSON output using Anastasia's script (extract_from_tshark.py).
332     input_file = sys.argv[1]
333     print "[ input_file  =", input_file, "]"
334     # Output file: Path to file where the Gephi XML should be written.
335     output_file = sys.argv[2]
336     print "[ output_file =", output_file, "]"
337     # Construct graph from JSON
338     G = parse_json(input_file)
339     # Contract nodes that have the same properties, i.e. same protocols
340     G = traverse_and_merge_nodes(G, DEVICE_MAC_LIST)
341     # Write Graph in Graph Exchange XML format
342     nx.write_gexf(G, output_file)