In [ ]:
import struct, socket
import numpy as np
import linecache, bisect
import csv
import operator
import json
import os
import pandas as pd
try:
import ipywidgets as widgets # For jupyter/ipython >= 1.4
except ImportError:
from IPython.html import widgets
from IPython.display import display, Javascript, clear_output
with open('/etc/spot.conf') as conf:
for line in conf.readlines():
if "DBNAME=" in line: DBNAME = line.split("=")[1].strip('\n').replace("'","");
elif "IMPALA_DEM=" in line: IMPALA_DEM = line.split("=")[1].strip('\n').replace("'","");
spath = os.getcwd()
path = spath.split("/")
date = path[len(path)-1]
dpath = '/'.join(['data' if var == 'ipynb' else var for var in path]) + '/'
cpath = '/'.join(['context' if var == 'ipynb' else var for var in path][:len(path)-2]) + '/'
sconnect = dpath + 'flow_scores.csv'
threats_file = dpath + 'threats.csv'
iploc = cpath + 'iploc.csv'
nwloc = cpath + 'networkcontext_1.csv'
anchor = ''
ir_f = ''
threat_name = ''
iplist = ''
top_results = 20
details_limit = 1000
if os.path.isfile(iploc):
iplist = np.loadtxt(iploc,dtype=np.uint32,delimiter=',',usecols={0}, converters={0: lambda s: np.uint32(s.replace('"',''))})
else:
print "No iploc.csv file was found, Map View map won't be created"
In [ ]:
# Widget styles and initialization
topBox = widgets.Box()
bottomBox = widgets.Box()
mainBoxes_css = (
(None, 'width', '90%'),
(None, 'margin', '0 auto'),
)
topBox._css = mainBoxes_css
bottomBox._css = mainBoxes_css
separator = widgets.HBox(width='100%', height='20px')
threatBox = widgets.HBox(width='100%', height='auto')
threat_title = widgets.HTML(height='25px', width='100%')
threat_list_container = widgets.Box(width='80%', height='100%')
threat_button_container = widgets.Box(width='20%', height='100%')
susp_select = widgets.Select(height='100%', width='99%')
search_btn = widgets.Button(description='Search',height='100%', width='65px')
search_btn.button_style = 'primary'
susp_select._css = (
(None, 'height', '90%'),
(None, 'width', '95%'),
('select', 'overflow-x', 'auto'),
('select', 'margin', 0)
)
resultSummaryBox = widgets.Box()
result_title = widgets.HTML(width='100%')
result_summary_box = widgets.HBox(width='100%')
result_summary_container = widgets.Box(width='80%')
result_button_container = widgets.Box(width='20%')
result_summary_box.children = [result_title, result_summary_container, result_button_container]
resultTableBox = widgets.HBox()
result_box_css = (
(None, 'overflow', 'hidden'),
(None, 'width', '100%'),
)
resultSummaryBox._css = result_box_css
resultTableBox._css = result_box_css
result_content_box_css = (
(None, 'overflow','auto'),
(None, 'width', '50%'),
)
threat_button_container._css = (
(None, 'padding-top', '30px'),
)
topBox.children = [threatBox]
bottomBox.children = [resultSummaryBox,resultTableBox]
Functions Definition
In [ ]:
top_inbound_b=''
top_outbound_b=''
top_twoway_b=''
inbound=''
outbound=''
twoway=''
global src_per_conns
global src_per_bytes
global dst_per_conns
global dst_per_bytes
def start_investigation():
display(Javascript("$('.widget-area > .widget-subarea > *').remove();"))
external_ips = []
c_ips=[]
clear_output()
if os.path.isfile(threats_file) and not file_is_empty(threats_file):
with open(threats_file, 'r') as th:
t_read = csv.DictReader(th, delimiter='|')
for row in t_read:
if row['ip'] != '' : c_ips.append(row['ip'])
with open(sconnect, 'r') as f:
reader = csv.DictReader(f, delimiter=',')
#Internal Netflows use case:
for row in reader:
if row['sev'] == '1':
srcIP = ''
dstIP = ''
if row['srcIP'] not in external_ips and row['srcIP'] not in c_ips:
external_ips.append(row['srcIP'])
if row['dstIP'] not in external_ips and row['dstIP'] not in c_ips:
external_ips.append(row['dstIP'])
if len(external_ips) == 0:
display(widgets.Box((widgets.HTML(value="There are no connections scored as High risk.\
You can score some connections at the 'Suspicious' panel.", width='100%'),)))
else:
sorted_dict = sorted(external_ips, key=operator.itemgetter(0))
display_controls(sorted_dict)
def display_controls(threat_list):
threat_title.value ="<h4>Suspicious Connections</h4>"
susp_select.options = threat_list
susp_select.height=150
susp_select.selected_label = threat_list[0]
threat_list_container.children = [threat_title,susp_select]
threat_button_container.children = [search_btn]
threatBox.children = [threat_list_container, threat_button_container]
display(topBox)
def search_ip(b):
global anchor
global top_inbound_b
anchor = susp_select.value
if anchor != "":
clear_output()
removeWidget(1)
print "Searching for ip: " + anchor
global ir_f
ir_f = dpath + "ir-" + anchor + ".tsv"
if not os.path.isfile(ir_f) or (os.path.isfile(ir_f) and file_is_empty(ir_f)):
imp_query = (" \"SELECT min(treceived) as firstSeen, max(treceived) as lastSeen, sip as srcIP, dip as dstIP, " +
"sport as SPort, dport AS Dport, count(sip) as conns, max(ipkt) as maxPkts, avg(ipkt) " +
"as avgPkts, max(ibyt) as maxBytes, avg(ibyt) as avgBytes FROM "+DBNAME+".flow WHERE " +
"y="+ date[0:4] +" AND m="+ date[4:6] +" AND d="+ date[6:] +" " +
" AND (sip =\'" + anchor + "\' OR dip=\'" + anchor + "\') GROUP BY sip, dip,sport,dport\" ")
!impala-shell -i $IMPALA_DEM --quiet -q "INVALIDATE METADATA"
!impala-shell -i $IMPALA_DEM --quiet --print_header -B --output_delimiter='\t' -q $imp_query -o $ir_f
clear_output()
if not file_is_empty(ir_f):
print "\n Looking for additional details..."
display_threat_box(anchor)
get_in_out_and_twoway_conns()
add_geospatial_info()
add_network_context()
display(bottomBox)
else:
display(widgets.Box((widgets.HTML(value="Something went wrong. \
The expanded search couldn't be performed", width='100%'),)))
search_btn.on_click(search_ip)
def display_threat_box(ip):
clear_output()
result_title.value="<h4 class='spot-text-wrapper spot-text-xlg' data-toggle='tooltip'>Threat summary for " + anchor +"</h4>"
tc_txt_title = widgets.Text(value='', placeholder='Threat Title', width='100%')
tc_txa_summary = widgets.Textarea(value='', height=100, width='95%')
tc_btn_save = widgets.Button(description='Save', width='65px', layout='width:100%')
tc_btn_save.button_style = 'primary'
tc_txt_title._css = (
(None, 'width', '95%'),
)
result_summary_container.children = [tc_txt_title, tc_txa_summary]
result_button_container.children=[tc_btn_save]
result_summary_box.children = [result_summary_container, result_button_container]
resultSummaryBox.children = [result_title,result_summary_box]
def save_threat_summary(b):
clear_output()
removeWidget(1)
response = ""
response += generate_attack_map_file(anchor, top_inbound_b, top_outbound_b, top_twoway_b)
response += generate_stats(anchor, inbound, outbound, twoway, threat_name)
response += generate_dendro(anchor, inbound, outbound, twoway, date)
response += details_inbound(anchor, top_inbound_b, top_outbound_b, top_twoway_b)
response += add_threat(anchor, tc_txt_title.value, tc_txa_summary.value.replace('\n', '\\n'))
response += "Story board successfully created for {0}".format(anchor)
start_investigation()
display(widgets.Box((widgets.HTML(value=response, width='100%'),)))
tc_btn_save.on_click(save_threat_summary)
In [ ]:
def details_inbound(anchor, inbound, outbound, twoway):
top_keys = []
if len(twoway) > 0: top_keys.extend(twoway.keys())
if len(outbound) > 0: top_keys.extend(outbound.keys())
if len(inbound) > 0: top_keys.extend(inbound.keys())
sbdet_f = dpath + "sbdet-" + anchor + ".tsv"
if not os.path.isfile(sbdet_f):
imp_query = ("\"SELECT min(treceived) as tstart, max(treceived) as tend, sip as srcIP, "
+ "dip as dstIP, proto as Proto, sport as SPort, dport AS Dport,ipkt as "
+ "Pkts, ibyt as Bytes FROM "+DBNAME+".flow WHERE "
+ "y="+ date[0:4] +" AND m="+ date[4:6] +" AND d="+ date[6:]
+ " AND ((dip IN({0}) AND sip ='{1}') OR "
+ "(sip IN({0}) "
+ "AND dip ='{1}')) GROUP BY sip, dip, proto, sport, dport, ipkt, ibyt ORDER BY tstart "
+ "LIMIT {2}\" ")
ips = "'" + "','".join(top_keys) + "'"
imp_query = imp_query.format(ips,anchor,details_limit)
!impala-shell -i $IMPALA_DEM --quiet -q "INVALIDATE METADATA"
!impala-shell -i $IMPALA_DEM --quiet --print_header -B --output_delimiter='\t' -q $imp_query -o $sbdet_f
clear_output()
return "Timeline successfully created <br/>"
else:
return "Timeline file already existed <br/>"
def generate_dendro(ip, inbound, outbound, twoway, date):
dendro_fpath = dpath + 'threat-dendro-' + anchor + ".json"
obj = {
'name':ip,
'children': [],
'time': date
}
#----- Add Inbound Connections-------#
if len(inbound) > 0:
obj["children"].append({'name': 'Inbound Only', 'children': [], 'impact': 0})
in_ctxs = {}
for ip in inbound:
if 'nwloc' in inbound[ip] and len(inbound[ip]['nwloc']) > 0:
ctx = inbound[ip]['nwloc'][2] # get the machine type Only for vast Data
if ctx not in in_ctxs:
in_ctxs[ctx] = 1
else:
in_ctxs[ctx] += 1
for ctx in in_ctxs:
obj["children"][0]['children'].append({
'name': ctx,
'impact': in_ctxs[ctx]
})
#------ Add Outbound ----------------#
if len(outbound) > 0:
obj["children"].append({'name': 'Outbound Only', 'children': [], 'impact': 0})
out_ctxs = {}
for ip in outbound:
if 'nwloc' in outbound[ip] and len(outbound[ip]['nwloc']) > 0:
ctx = outbound[ip]['nwloc'][2] # get the machine type Only for vast Data
if ctx not in out_ctxs:
out_ctxs[ctx] = 1
else:
out_ctxs[ctx] += 1
for ctx in out_ctxs:
obj["children"][1]['children'].append({
'name': ctx,
'impact': out_ctxs[ctx]
})
#------ Add TwoWay ----------------#
if len(twoway) > 0:
obj["children"].append({'name': 'two way', 'children': [], 'impact': 0})
tw_ctxs = {}
for ip in twoway:
if 'nwloc' in twoway[ip] and len(twoway[ip]['nwloc']) > 0:
ctx = twoway[ip]['nwloc'][2] # get the machine type Only for vast Data
if ctx not in tw_ctxs:
tw_ctxs[ctx] = 1
else:
tw_ctxs[ctx] += 1
for ctx in tw_ctxs:
obj["children"][2]['children'].append({
'name': ctx,
'impact': tw_ctxs[ctx]
})
with open(dendro_fpath, 'w') as dendro_f:
dendro_f.write(json.dumps(obj))
return "Incident progression successfully created <br/>"
def generate_stats(ip, inbound, outbound, twoway, threat_name):
stats_fpath = dpath + 'stats-' + anchor + ".json"
obj = {
'name':threat_name,
'children': [],
'size': len(inbound) + len(outbound) + len(twoway)
}
#----- Add Inbound Connections-------#
obj["children"].append({'name': 'Inbound Only', 'children': [], 'size': len(inbound)})
in_ctxs = {}
for ip in inbound:
full_ctx = ''
if 'nwloc' in inbound[ip] and len(inbound[ip]['nwloc']) > 0:
full_ctx = inbound[ip]['nwloc'][2].split('.')[0]
ctx = get_ctx_name(full_ctx) # get the machine type Only for vast Data
if ctx not in in_ctxs:
in_ctxs[ctx] = 1
else:
in_ctxs[ctx] += 1
for ctx in in_ctxs:
obj["children"][0]['children'].append({
'name': ctx,
'size': in_ctxs[ctx]
})
#------ Add Outbound ----------------#
obj["children"].append({'name': 'Outbound Only', 'children': [], 'size': len(outbound)})
out_ctxs = {}
for ip in outbound:
full_ctx = ''
if 'nwloc' in outbound[ip] and len(outbound[ip]['nwloc']) > 0:
full_ctx = outbound[ip]['nwloc'][2].split('.')[0]
ctx = get_ctx_name(full_ctx) # get the machine type Only for vast Data
if ctx not in out_ctxs:
out_ctxs[ctx] = 1
else:
out_ctxs[ctx] += 1
for ctx in out_ctxs:
obj["children"][1]['children'].append({
'name': ctx,
'size': out_ctxs[ctx]
})
#------ Add Twoway ----------------#
obj["children"].append({'name': 'two way', 'children': [], 'size': len(twoway)})
tw_ctxs = {}
for ip in twoway:
full_ctx = ''
if 'nwloc' in twoway[ip] and len(twoway[ip]['nwloc']) > 0:
full_ctx = twoway[ip]['nwloc'][2].split('.')[0]
ctx = get_ctx_name(full_ctx) # get the machine type Only for vast Data
if ctx not in tw_ctxs:
tw_ctxs[ctx] = 1
else:
tw_ctxs[ctx] += 1
for ctx in tw_ctxs:
obj["children"][2]['children'].append({
'name': ctx,
'size': tw_ctxs[ctx]
})
json_str = json.dumps(obj)
with open(stats_fpath, 'w') as stats_f:
stats_f.write(json_str)
return "Stats file successfully created <br/>"
def get_ctx_name(full_context):
ctx= 'DMZ'
if "VPN" in full_context:
ctx = "VPN"
elif "DMZ" in full_context:
ctx = "DMZ"
elif "Proxy" in full_context:
ctx = "Proxy"
elif "FW" in full_context:
ctx = "FW"
return ctx
# calculate number of inbound only, two-way, and outbound only
# build dict of IP addresses
# firstSeen,lastSeen,srcIP, dstIP, sport,dport,conns, maxPkts, avgPkts,maxBytes, avgBytes
def get_in_out_and_twoway_conns():
global inbound
inbound = {}
global outbound
outbound = {}
global twoway
twoway = {}
srcdict = {}
dstdict = {}
conns_dict= {}
rowct = 0
if os.path.isfile(ir_f):
df = pd.read_csv(ir_f,sep='\t')
with open(ir_f, 'r') as f:
reader = csv.reader(f,delimiter='\t')
reader.next() #skip headers
for row in reader:
if row != []:
srcdict[row[2]] = {
'ip_int': struct.unpack("!L", socket.inet_aton(row[2]))[0],
'dst_ip': row[3],
'dst_ip_int': struct.unpack("!L", socket.inet_aton(row[3]))[0],
'conns': int(row[6]),
'maxbytes': int(row[9])
}
dstdict[row[3]] = {
'ip_int': struct.unpack("!L", socket.inet_aton(row[3]))[0],
'src_ip': row[2],
'src_ip_int': struct.unpack("!L", socket.inet_aton(row[2]))[0],
'conns': int(row[6]),
'maxbytes': int(row[9])
}
rowct +=1
src = df.loc[df['dstip'] == anchor]
src_per_conns = src.sort_values('conns',0,False)
src_per_bytes = src.sort_values('maxbytes',0,False)
dst = df.loc[df['srcip'] == anchor]
dst_per_conns = dst.sort_values('conns',0,False)
dst_per_bytes = dst.sort_values('maxbytes',0,False)
children = []
children += (display_results(['srcip','conns','sport','dport'], src_per_conns,
top_results),)
children += (display_results(['dstip','conns','sport','dport'], dst_per_conns,
top_results),)
children += (display_results(['srcip','maxbytes','sport','dport'], src_per_bytes,
top_results),)
children += (display_results(['dstip','maxbytes','sport','dport'], dst_per_bytes,
top_results),)
result_tabs = widgets.Accordion(children=children, width='100%', selected_index=-1)
result_tabs.set_title(0,"Top source IP per connections")
result_tabs.set_title(1,"Top destination IP per connections")
result_tabs.set_title(2,"Top source IP per bytes transferred")
result_tabs.set_title(3,"Top destination IP per bytes transferred")
result_tabs._css = (
(None, 'margin-top', '10px'),
(None, 'margin-bottom', '10px'),
)
resultTableBox.children = [result_tabs,]
if rowct > 0:
for result in srcdict:
if result in dstdict:
twoway[result] = srcdict[result]
else:
outbound[result] = srcdict[result]
for result in dstdict:
if result not in srcdict:
inbound[result] = dstdict[result]
global top_inbound_b
global top_outbound_b
global top_twoway_b
if len(inbound) > 0:
top_inbound_b = get_top_bytes(inbound,top_results)
top_inbound_conns = get_top_conns(inbound,top_results)
top_inbound_b.update(top_inbound_conns) # merge the two dictionaries
if len(outbound) > 0:
top_outbound_b = get_top_bytes(outbound,top_results)
top_outbound_conns = get_top_conns(outbound,top_results)
top_outbound_b.update(top_outbound_conns) # merge the two dictionaries
if len(twoway) > 0:
top_twoway_b = get_top_bytes(twoway,top_results)
top_twoway_conns = get_top_conns(twoway,top_results)
top_twoway_b.update(top_twoway_conns) # merge the two dictionaries
def display_results(cols, dataframe, top):
table = dataframe[:top].to_html(classes='table table-striped table-bordered table-hover', columns=cols, index=False)
return widgets.HTML(value=table, width='100%')
#=========== Adds GEO IP information to the outbound, inbound and twoway connections==============================#
def add_geospatial_info():
# get geospatial info, only when iplocation file is available
if iplist != '':
for srcip in outbound:
reader = csv.reader([linecache.getline(iploc, bisect.bisect(iplist,outbound[srcip]['ip_int'])).replace('\n','')])
outbound[srcip]['geo'] = reader.next()
reader = csv.reader([linecache.getline(iploc, bisect.bisect(iplist,outbound[srcip]['dst_ip_int'])).replace('\n','')])
outbound[srcip]['geo_dst'] = reader.next()
for dstip in twoway:
reader = csv.reader([linecache.getline(iploc, bisect.bisect(iplist,twoway[dstip]['ip_int'])).replace('\n','')])
twoway[dstip]['geo'] = reader.next()
for srcip in inbound:
reader = csv.reader([linecache.getline(iploc, bisect.bisect(iplist,inbound[srcip]['ip_int'])).replace('\n','')])
inbound[srcip]['geo'] = reader.next()
reader = csv.reader([linecache.getline(iploc, bisect.bisect(iplist,inbound[srcip]['src_ip_int'])).replace('\n','')])
inbound[srcip]['geo_src'] = reader.next()
# need some way to combine timelines of outbound and two-way with big picture inbound only
# get network context - get start and end ranges
def add_network_context():
nwdict = {}
if os.path.isfile(nwloc) :
with open(nwloc, 'r') as f:
reader = csv.reader(f,delimiter=',')
reader.next()
#address range, description
for row in reader:
if '/' in row[0]:
#Range in subnet
iprange = row[0].split('/')
if len(iprange) < 2:
ipend = 0
else:
ipend = int(iprange[1])
nwdict[row[0]] = [struct.unpack("!L", socket.inet_aton(iprange[0]))[0],
struct.unpack("!L", socket.inet_aton(iprange[0]))[0]+2**(32-ipend)-1, row[1]]
elif '-' in row[0]:
#IP Range
iprange = row[0].split('-')
nwdict[row[0]] = [struct.unpack("!L", socket.inet_aton(iprange[0].replace(" ", "")))[0],
struct.unpack("!L", socket.inet_aton(iprange[1].replace(" ", "")))[0], row[1]]
else:
#Exact match
nwdict[row[0]] = [struct.unpack("!L", socket.inet_aton(row[0]))[0],
struct.unpack("!L", socket.inet_aton(row[0]))[0], row[1]]
for srcip in outbound:
temp_ip = struct.unpack("!L", socket.inet_aton(srcip))[0]
if srcip in nwdict:
inbound[srcip]['nwloc'] = nwdict[srcip]
else:
matchingVals = [x for x in nwdict if nwdict[x][1] >= temp_ip and nwdict[x][0] <= temp_ip]
outbound[srcip]['nwloc'] = nwdict[matchingVals[0]] if len(matchingVals) > 0 else ''
for dstip in twoway:
temp_ip = struct.unpack("!L", socket.inet_aton(dstip))[0]
if dstip in nwdict:
twoway[dstip]['nwloc'] = nwdict[dstip]
else:
matchingVals = [x for x in nwdict if nwdict[x][1] >= temp_ip and nwdict[x][0] <= temp_ip]
twoway[dstip]['nwloc'] = nwdict[matchingVals[0]] if len(matchingVals) > 0 else ''
for srcip in inbound:
temp_ip = struct.unpack("!L", socket.inet_aton(srcip))[0]
if srcip in nwdict:
inbound[srcip]['nwloc'] = nwdict[srcip]
else:
matchingVals = [x for x in nwdict if nwdict[x][1] >= temp_ip and nwdict[x][0] <= temp_ip]
inbound[srcip]['nwloc'] = nwdict[matchingVals[0]] if len(matchingVals) > 0 else ''
def generate_attack_map_file(ip, inbound, outbound, twoway):
if iplist != '':
globe_fpath = dpath + 'globe-' + ip + ".json"
globe_json = {}
globe_json['type'] = "FeatureCollection"
globe_json['sourceips'] = []
globe_json['destips'] = []
for srcip in twoway:
try:
row = twoway[srcip]['geo']
globe_json['destips'].append({
'type': 'Feature',
'properties': {
'location':row[8],
'ip':srcip,
'type':1
},
'geometry': {
'type': 'Point',
'coordinates': [float(row[7]), float(row[6])]
}
})
except ValueError:
pass
for dstip in outbound:
try:
row = outbound[dstip]['geo']
dst_geo = outbound[dstip]['geo_dst']
globe_json['sourceips'].append({
'type': 'Feature',
'properties': {
'location':row[8],
'ip':dstip,
'type':3
},
'geometry': {
'type': 'Point',
'coordinates': [float(row[7]), float(row[6])]
}
})
globe_json['destips'].append({
'type': 'Feature',
'properties': {
'location':row[8],
'ip':outbound[dstip]['dst_ip'],
'type':3
},
'geometry': {
'type': 'Point',
'coordinates': [float(dst_geo[7]), float(dst_geo[6])]
}
})
except ValueError:
pass
for dstip in inbound:
try:
row = inbound[dstip]['geo']
dst_geo = inbound[dstip]['geo_src']
globe_json['sourceips'].append({
'type': 'Feature',
'properties': {
'location':row[8],
'ip':dstip,
'type':2
},
'geometry': {
'type': 'Point',
'coordinates': [float(row[7]), float(row[6])]
}
})
globe_json['destips'].append({
'type': 'Feature',
'properties': {
'location':row[8],
'ip':inbound[dstip]['src_ip'],
'type':2
},
'geometry': {
'type': 'Point',
'coordinates': [float(dst_geo[7]), float(dst_geo[6])]
}
})
except ValueError:
pass
json_str = json.dumps(globe_json)
with open(globe_fpath, 'w') as globe_f:
globe_f.write(json_str)
response = "Geolocation map successfully created <br/>"
else:
response = "The map can't be created without an iploc file <br/>"
return response
def add_threat(ip,threat_title, threat_comment):
content = ''
try:
threat_f = open(threats_file, 'r')
content = threat_f.read()
if '{0}|{1}|{2}\n'.format(ip,threat_title,threat_comment) not in content:
content += '{0}|{1}|{2}\n'.format(ip,threat_title,threat_comment)
threat_f.close()
except:
content = 'ip|title|summary\n'
content += '{0}|{1}|{2}\n'.format(ip,threat_title,threat_comment)
threat_fw = open(threats_file, 'w')
threat_fw.write(content)
threat_fw.close()
return ""
def get_top_bytes(conns_dict, top):
topbytes = sorted(conns_dict.iteritems(), key=lambda (x,y): y['maxbytes'], reverse=True)
topbytes = topbytes[0:top]
return dict(topbytes)
def get_top_conns(conns_dict, top):
topconns = sorted(conns_dict.iteritems(), key=lambda (x,y): y['conns'], reverse=True)
topconns = topconns[0:top]
return dict(topconns)
def file_is_empty(path):
return os.stat(path).st_size==0
def removeWidget(index):
js_command = "$('.widget-area > .widget-subarea > .widget-box:eq({0})').remove();".format(index)
display(Javascript(js_command))
In [ ]:
start_investigation()