|
@@ -0,0 +1,446 @@
|
|
1
|
+import dash
|
|
2
|
+from dash import dcc, html, dash_table
|
|
3
|
+from dash.dependencies import Input, Output, State, ALL
|
|
4
|
+import pandas as pd
|
|
5
|
+from datetime import datetime as dt
|
|
6
|
+import plotly.express as px
|
|
7
|
+import plotly.graph_objs as go
|
|
8
|
+import os
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+DATA_DIR = os.path.abspath(os.path.join(BASE_DIR, "..", "data"))
|
|
14
|
+
|
|
15
|
+detailed_scan_csv = os.path.join(DATA_DIR, "detailed_scan_results.csv")
|
|
16
|
+openvas_csv = os.path.join(DATA_DIR, "openvasscan.csv")
|
|
17
|
+
|
|
18
|
+# Load and prepare the dataset
|
|
19
|
+df = pd.read_csv(detailed_scan_csv)
|
|
20
|
+vulnerability_data = pd.read_csv(openvas_csv)
|
|
21
|
+
|
|
22
|
+# Preparing grouped data
|
|
23
|
+grouped_data = vulnerability_data.groupby(['IP', 'NVT Name', 'Severity']).first().reset_index()
|
|
24
|
+grouped_data['Details'] = grouped_data.apply(lambda row: f"CVSS: {row['CVSS']}\nSeverity: {row['Severity']}\nSummary: {row['Summary']}\nSolution Type: {row['Solution Type']}", axis=1)
|
|
25
|
+
|
|
26
|
+# List of unique IPs for the dropdown
|
|
27
|
+unique_ips = vulnerability_data['IP'].unique().tolist()
|
|
28
|
+unique_ips.insert(0, 'All')
|
|
29
|
+
|
|
30
|
+# Convert Timestamp to datetime and sort
|
|
31
|
+df['Timestamp'] = pd.to_datetime(df['Timestamp'])
|
|
32
|
+df.sort_values('Timestamp', inplace=True)
|
|
33
|
+
|
|
34
|
+# Extract unique timestamps
|
|
35
|
+unique_timestamps = df['Timestamp'].unique()
|
|
36
|
+
|
|
37
|
+# Prepare data for the timeline graph, grouped by day
|
|
38
|
+df['Date'] = df['Timestamp'].dt.date
|
|
39
|
+ip_count_over_time = df.groupby('Date')['IP'].nunique().reset_index()
|
|
40
|
+ip_count_over_time.columns = ['Date', 'IP_Count']
|
|
41
|
+
|
|
42
|
+# Create the Plotly graph
|
|
43
|
+timeline_fig = px.line(ip_count_over_time, x='Date', y='IP_Count', title='Number of IPs Over Time')
|
|
44
|
+timeline_fig.update_layout(
|
|
45
|
+ xaxis_title="Date",
|
|
46
|
+ yaxis_title="IP Count"
|
|
47
|
+)
|
|
48
|
+
|
|
49
|
+# Initialize the Dash app
|
|
50
|
+app = dash.Dash(__name__)
|
|
51
|
+
|
|
52
|
+# Convert timestamps to strings for slider display
|
|
53
|
+timestamp_options = [{'label': str(ts), 'value': ts} for ts in df['Timestamp'].unique()]
|
|
54
|
+timestamp_values = [ts.value for ts in df['Timestamp']]
|
|
55
|
+
|
|
56
|
+def style_status_badge(status):
|
|
57
|
+ emoji_map = {
|
|
58
|
+ 'Added': '🟩',
|
|
59
|
+ 'Removed': '🟥',
|
|
60
|
+ 'Still Active': '⚪'
|
|
61
|
+ }
|
|
62
|
+ return f"{emoji_map.get(status, '⬜')} {status}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+app.layout = html.Div([
|
|
66
|
+ dcc.Tabs(id="tabs", children=[
|
|
67
|
+ dcc.Tab(label='Overview', children=[
|
|
68
|
+ html.Div([
|
|
69
|
+ dcc.RangeSlider(
|
|
70
|
+ id='time-range-slider',
|
|
71
|
+ min=0,
|
|
72
|
+ max=len(unique_timestamps) - 1,
|
|
73
|
+ value=[0, len(unique_timestamps) - 1],
|
|
74
|
+ marks={i: {'label': str(ts)[:10]} for i, ts in enumerate(unique_timestamps)},
|
|
75
|
+ step=1,
|
|
76
|
+ allowCross=False
|
|
77
|
+ ),
|
|
78
|
+ dash_table.DataTable(
|
|
79
|
+ id='table',
|
|
80
|
+ columns=[{"name": i, "id": i, "presentation": "markdown"} if i == "Status" else {"name": i, "id": i}
|
|
81
|
+ for i in df.columns
|
|
82
|
+ ] + [{"name": "Status", "id": "Status", "presentation": "markdown"}],
|
|
83
|
+ sort_action='native',
|
|
84
|
+ filter_action='native',
|
|
85
|
+ style_table={'overflowX': 'auto'},
|
|
86
|
+ style_data_conditional=[{'if': {'column_id': 'Status'}, 'textAlign': 'center', 'width': '120px'}]
|
|
87
|
+ ),
|
|
88
|
+ html.Div([
|
|
89
|
+ dcc.Graph(
|
|
90
|
+ id='timeline-graph',
|
|
91
|
+ figure=timeline_fig
|
|
92
|
+ ),
|
|
93
|
+ dcc.Graph(id='open-ports-bar-chart')
|
|
94
|
+ ], style={'display': 'flex', 'flex-direction': 'row'}),
|
|
95
|
+ html.Div([
|
|
96
|
+ dcc.Graph(id='severity-pie-chart')
|
|
97
|
+
|
|
98
|
+ ], style={'display': 'flex', 'flex-direction': 'row'}),
|
|
99
|
+ html.Div([
|
|
100
|
+ dcc.Graph(id='ip-change-bar-chart'),
|
|
101
|
+ dash_table.DataTable(
|
|
102
|
+ id='ip-change-table',
|
|
103
|
+ columns=[
|
|
104
|
+ {"name": "IP", "id": "IP"},
|
|
105
|
+ {"name": "Status", "id": "Status"}
|
|
106
|
+ ],
|
|
107
|
+ sort_action='native',
|
|
108
|
+ filter_action='native',
|
|
109
|
+ style_table={'overflowX': 'auto'}
|
|
110
|
+ )
|
|
111
|
+ ], style={'display': 'flex', 'flex-direction': 'row'}),
|
|
112
|
+ html.Div(id='summary-section', style={'padding': '20px'})
|
|
113
|
+ ])
|
|
114
|
+ ]),
|
|
115
|
+ dcc.Tab(label='Vulnerability Analysis', children=[
|
|
116
|
+ html.Div([
|
|
117
|
+ dcc.Dropdown(
|
|
118
|
+ id='severity-dropdown',
|
|
119
|
+ options=[{'label': s, 'value': s} for s in ['All', 'High', 'Medium', 'Low']],
|
|
120
|
+ value='All'
|
|
121
|
+ ),
|
|
122
|
+ dcc.Dropdown(
|
|
123
|
+ id='ip-dropdown',
|
|
124
|
+ options=[{'label': ip, 'value': ip} for ip in unique_ips],
|
|
125
|
+ value='All'
|
|
126
|
+ ),
|
|
127
|
+ dcc.Graph(id='vulnerability-treemap'),
|
|
128
|
+ html.Div(id='details-and-ip-output'),
|
|
129
|
+ html.Div(id='clicked-ip', style={'display': 'none'})
|
|
130
|
+ ])
|
|
131
|
+ ]),
|
|
132
|
+ dcc.Tab(label='Port Heatmap', children=[
|
|
133
|
+ html.Div([
|
|
134
|
+ dcc.Graph(id='ip-port-heatmap', style={'height': '700px', 'width': '100%'}),
|
|
135
|
+ html.Div([
|
|
136
|
+ html.P("🟦 = Port is Open"),
|
|
137
|
+ html.P("⬜ = Port is Closed"),
|
|
138
|
+ html.P("Each row represents a Host (IP), and each column is a Port."),
|
|
139
|
+ html.P("This heatmap shows which ports are open on each host at the selected time.")
|
|
140
|
+ ], style={
|
|
141
|
+ 'padding': '10px',
|
|
142
|
+ 'backgroundColor': '#f9f9f9',
|
|
143
|
+ 'border': '1px solid #ccc',
|
|
144
|
+ 'marginTop': '10px',
|
|
145
|
+ 'borderRadius': '5px'
|
|
146
|
+ })
|
|
147
|
+ ])
|
|
148
|
+
|
|
149
|
+ ])
|
|
150
|
+ ])
|
|
151
|
+])
|
|
152
|
+
|
|
153
|
+@app.callback(
|
|
154
|
+ [Output('table', 'data'),
|
|
155
|
+ Output('table', 'style_data_conditional'),
|
|
156
|
+ Output('timeline-graph', 'figure'),
|
|
157
|
+ Output('open-ports-bar-chart', 'figure'),
|
|
158
|
+ Output('severity-pie-chart', 'figure'),
|
|
159
|
+ Output('ip-port-heatmap', 'figure'),
|
|
160
|
+ Output('ip-change-bar-chart', 'figure'),
|
|
161
|
+ Output('ip-change-table', 'data'),
|
|
162
|
+ Output('summary-section', 'children')],
|
|
163
|
+ [Input('time-range-slider', 'value')]
|
|
164
|
+)
|
|
165
|
+def update_overview_tab(time_range):
|
|
166
|
+ start_index, end_index = time_range
|
|
167
|
+ start_timestamp = unique_timestamps[start_index]
|
|
168
|
+ end_timestamp = unique_timestamps[end_index]
|
|
169
|
+
|
|
170
|
+ # Filter data within the selected time range
|
|
171
|
+ filtered_df = df[(df['Timestamp'] >= start_timestamp) & (df['Timestamp'] <= end_timestamp)].copy()
|
|
172
|
+
|
|
173
|
+ # Update table
|
|
174
|
+ filtered_df_selected = filtered_df.copy()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+ # Determine IPs in the time range
|
|
178
|
+ #all_ips = set(filtered_df['IP'])
|
|
179
|
+ #status_dict = {ip: 'Within Range' for ip in all_ips}
|
|
180
|
+
|
|
181
|
+ # Assign badge-style labels using style_status_badge
|
|
182
|
+ #filtered_df_selected['Status'] = filtered_df_selected['IP'].map(status_dict).fillna('Unknown')
|
|
183
|
+ # filtered_df_selected['Status'] = filtered_df_selected['Status'].apply(style_status_badge)
|
|
184
|
+ # Determine IPs in the time range
|
|
185
|
+ all_ips = set(filtered_df['IP'])
|
|
186
|
+
|
|
187
|
+ # Get previous IP set
|
|
188
|
+ if start_index > 0:
|
|
189
|
+ prev_timestamp = unique_timestamps[start_index - 1]
|
|
190
|
+ else:
|
|
191
|
+ prev_timestamp = start_timestamp
|
|
192
|
+
|
|
193
|
+ prev_ips = set(df[df['Timestamp'] == prev_timestamp]['IP'])
|
|
194
|
+ new_ips = all_ips - prev_ips
|
|
195
|
+ removed_ips = prev_ips - all_ips
|
|
196
|
+ existing_ips = all_ips.intersection(prev_ips)
|
|
197
|
+
|
|
198
|
+ # Add dummy rows for removed IPs (with NaNs or placeholders)
|
|
199
|
+ removed_rows = pd.DataFrame({
|
|
200
|
+ "IP": list(removed_ips),
|
|
201
|
+ "Hostname": "", "MAC Address": "", "Protocol": "", "Port": "", "Name": "",
|
|
202
|
+ "State": "", "Product": "", "Version": "", "Extra Info": "",
|
|
203
|
+ "Timestamp": pd.NaT, "Date": None
|
|
204
|
+ })
|
|
205
|
+ filtered_df_selected = pd.concat([filtered_df_selected, removed_rows], ignore_index=True)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+ # Build status dictionary for badges
|
|
210
|
+ status_dict = {}
|
|
211
|
+ for ip in new_ips:
|
|
212
|
+ status_dict[ip] = 'Added'
|
|
213
|
+ for ip in removed_ips:
|
|
214
|
+ status_dict[ip] = 'Removed'
|
|
215
|
+ for ip in existing_ips:
|
|
216
|
+ status_dict[ip] = 'Still Active'
|
|
217
|
+
|
|
218
|
+ # Assign and badge
|
|
219
|
+ filtered_df_selected['Status'] = filtered_df_selected['IP'].map(status_dict).fillna('Unknown')
|
|
220
|
+ filtered_df_selected['Status'] = filtered_df_selected['Status'].apply(style_status_badge)
|
|
221
|
+
|
|
222
|
+ # Apply conditional formatting based on the 'Status' column
|
|
223
|
+ style = [
|
|
224
|
+ {
|
|
225
|
+ 'if': {
|
|
226
|
+ 'filter_query': '{Status} = "Added"',
|
|
227
|
+ },
|
|
228
|
+ 'borderLeft': '4px solid green',
|
|
229
|
+ 'backgroundColor': '#eaf7ea' # very light green background
|
|
230
|
+ },
|
|
231
|
+ {
|
|
232
|
+ 'if': {
|
|
233
|
+ 'filter_query': '{Status} = "Removed"',
|
|
234
|
+ },
|
|
235
|
+ 'borderLeft': '4px solid red',
|
|
236
|
+ 'backgroundColor': '#fcebea' # very light red background
|
|
237
|
+ },
|
|
238
|
+ {
|
|
239
|
+ 'if': {
|
|
240
|
+ 'filter_query': '{Status} = "Still Active"',
|
|
241
|
+ },
|
|
242
|
+ 'borderLeft': '4px solid lightgray'
|
|
243
|
+ }
|
|
244
|
+]
|
|
245
|
+
|
|
246
|
+ # Update timeline graph, grouped by day
|
|
247
|
+ filtered_df['Date'] = filtered_df['Timestamp'].dt.date
|
|
248
|
+ ip_count_over_time = filtered_df.groupby('Date')['IP'].nunique().reset_index()
|
|
249
|
+ ip_count_over_time.columns = ['Date', 'IP_Count']
|
|
250
|
+ timeline_fig = px.line(ip_count_over_time, x='Date', y='IP_Count', title='Number of IPs Over Time')
|
|
251
|
+ timeline_fig.update_layout(
|
|
252
|
+ xaxis_title="Date",
|
|
253
|
+ yaxis_title="IP Count"
|
|
254
|
+ )
|
|
255
|
+
|
|
256
|
+ # Open ports bar chart
|
|
257
|
+ open_ports_count = filtered_df['Port'].value_counts().reset_index()
|
|
258
|
+ open_ports_count.columns = ['Port', 'Count']
|
|
259
|
+ open_ports_bar_chart = px.bar(open_ports_count, x='Port', y='Count', title='Distribution of Open Ports')
|
|
260
|
+ open_ports_bar_chart.update_layout(
|
|
261
|
+ xaxis_title="Port",
|
|
262
|
+ yaxis_title="Count"
|
|
263
|
+ )
|
|
264
|
+ open_ports_bar_chart.update_traces(marker_color='blue', marker_line_color='darkblue', marker_line_width=1.5, opacity=0.8)
|
|
265
|
+
|
|
266
|
+ # Severity pie chart
|
|
267
|
+ severity_count = vulnerability_data['Severity'].value_counts().reset_index()
|
|
268
|
+ severity_count.columns = ['Severity', 'Count']
|
|
269
|
+ severity_pie_chart = px.pie(severity_count, names='Severity', values='Count', title='Severity Distribution')
|
|
270
|
+
|
|
271
|
+ # IP-Port Heatmap with Fixed Port Range and Binary Open/Closed
|
|
272
|
+
|
|
273
|
+ # Define all possible ports you want to show (e.g. top 1024)
|
|
274
|
+ # Only include ports that were actually scanned, but sorted
|
|
275
|
+ all_ports = sorted(filtered_df['Port'].dropna().astype(int).unique().tolist())
|
|
276
|
+
|
|
277
|
+ all_ips = set(filtered_df['IP'])
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+ heatmap_df = (
|
|
282
|
+ filtered_df[["IP", "Port"]]
|
|
283
|
+ .dropna()
|
|
284
|
+ .assign(value=1)
|
|
285
|
+ .pivot_table(index="IP", columns="Port", values="value", fill_value=0)
|
|
286
|
+ )
|
|
287
|
+ heatmap_df.columns = heatmap_df.columns.astype(int)
|
|
288
|
+ heatmap_df = heatmap_df.sort_index(axis=1)
|
|
289
|
+
|
|
290
|
+ hover_text = [
|
|
291
|
+ [f"IP: {ip}<br>Port: {port}<br>Status: {'Open' if val == 1 else 'Closed'}"
|
|
292
|
+ for port, val in zip(heatmap_df.columns, row)]
|
|
293
|
+ for ip, row in zip(heatmap_df.index, heatmap_df.values)
|
|
294
|
+ ]
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+ # Generate heatmap
|
|
298
|
+ ip_port_heatmap = go.Figure(data=go.Heatmap(
|
|
299
|
+ z=heatmap_df.values,
|
|
300
|
+ x=heatmap_df.columns,
|
|
301
|
+ y=heatmap_df.index,
|
|
302
|
+ text=hover_text,
|
|
303
|
+ hoverinfo='text',
|
|
304
|
+ colorscale=[[0, 'white'], [1, 'darkblue']],
|
|
305
|
+ zmin=0,
|
|
306
|
+ zmax=1,
|
|
307
|
+ zsmooth=False,
|
|
308
|
+ colorbar=dict(
|
|
309
|
+ title='Port Status',
|
|
310
|
+ tickvals=[0, 1],
|
|
311
|
+ ticktext=['Closed (White)', 'Open (Blue)']
|
|
312
|
+ )
|
|
313
|
+ ))
|
|
314
|
+ ip_port_heatmap.update_layout(
|
|
315
|
+ title='Binary Heatmap - Which Ports Are Open on Which Hosts',
|
|
316
|
+ xaxis_title='Port',
|
|
317
|
+ yaxis_title='IP',
|
|
318
|
+ height=600
|
|
319
|
+ )
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+ # Determine IPs added and removed
|
|
323
|
+ if start_index > 0:
|
|
324
|
+ prev_timestamp = unique_timestamps[start_index - 1]
|
|
325
|
+ else:
|
|
326
|
+ prev_timestamp = start_timestamp
|
|
327
|
+
|
|
328
|
+ prev_ips = set(df[df['Timestamp'] == prev_timestamp]['IP'])
|
|
329
|
+ new_ips = all_ips - prev_ips
|
|
330
|
+ removed_ips = prev_ips - all_ips
|
|
331
|
+ existing_ips = all_ips.intersection(prev_ips)
|
|
332
|
+
|
|
333
|
+ # IP change table
|
|
334
|
+ ip_change_data = []
|
|
335
|
+ for ip in new_ips:
|
|
336
|
+ ip_change_data.append({"IP": ip, "Status": "Added"})
|
|
337
|
+ for ip in removed_ips:
|
|
338
|
+ ip_change_data.append({"IP": ip, "Status": "Removed"})
|
|
339
|
+ for ip in existing_ips:
|
|
340
|
+ ip_change_data.append({"IP": ip, "Status": "Still Active"})
|
|
341
|
+
|
|
342
|
+ # IP change bar chart
|
|
343
|
+ ip_change_summary = {
|
|
344
|
+ "Added": len(new_ips),
|
|
345
|
+ "Removed": len(removed_ips),
|
|
346
|
+ "Still Active": len(existing_ips)
|
|
347
|
+ }
|
|
348
|
+ ip_change_bar_chart = px.bar(
|
|
349
|
+ x=list(ip_change_summary.keys()),
|
|
350
|
+ y=list(ip_change_summary.values()),
|
|
351
|
+ title="IP Changes Summary"
|
|
352
|
+ )
|
|
353
|
+ ip_change_bar_chart.update_layout(
|
|
354
|
+ xaxis_title="Change Type",
|
|
355
|
+ yaxis_title="Count"
|
|
356
|
+ )
|
|
357
|
+ ip_change_bar_chart.update_traces(marker_color='purple', marker_line_color='darkblue', marker_line_width=1.5, opacity=0.8)
|
|
358
|
+
|
|
359
|
+ # Summary section
|
|
360
|
+ total_unique_ips = len(df['IP'].unique())
|
|
361
|
+ total_vulnerabilities = len(vulnerability_data)
|
|
362
|
+ most_common_ports = filtered_df['Port'].value_counts().head(5).to_dict()
|
|
363
|
+ most_dangerous_vulnerability = vulnerability_data.loc[vulnerability_data['CVSS'].idxmax()]
|
|
364
|
+ most_common_vulnerability = vulnerability_data['NVT Name'].value_counts().idxmax()
|
|
365
|
+ most_common_ip = df['IP'].value_counts().idxmax()
|
|
366
|
+ average_cvss_score = vulnerability_data['CVSS'].mean()
|
|
367
|
+ ips_with_most_vulnerabilities = vulnerability_data['IP'].value_counts().head(5).to_dict()
|
|
368
|
+
|
|
369
|
+ summary_content = html.Div([
|
|
370
|
+ html.H3("Summary of Interesting Data"),
|
|
371
|
+ html.P(f"Total unique IPs: {total_unique_ips}"),
|
|
372
|
+ html.P(f"Total vulnerabilities recorded: {total_vulnerabilities}"),
|
|
373
|
+ html.P(f"Most dangerous vulnerability (highest CVSS score): {most_dangerous_vulnerability['NVT Name']} with CVSS score {most_dangerous_vulnerability['CVSS']}"),
|
|
374
|
+ html.P(f"Most common vulnerability: {most_common_vulnerability}"),
|
|
375
|
+ html.P(f"Most common IP: {most_common_ip}"),
|
|
376
|
+ html.P(f"Average CVSS score: {average_cvss_score:.2f}"),
|
|
377
|
+ html.H4("Most Common Ports:"),
|
|
378
|
+ html.Ul([html.Li(f"Port {port}: {count} times") for port, count in most_common_ports.items()]),
|
|
379
|
+ html.H4("IPs with the Most Vulnerabilities:"),
|
|
380
|
+ html.Ul([html.Li(f"IP {ip}: {count} vulnerabilities") for ip, count in ips_with_most_vulnerabilities.items()])
|
|
381
|
+ ])
|
|
382
|
+
|
|
383
|
+ return (filtered_df_selected.to_dict('records'), style, timeline_fig, open_ports_bar_chart, severity_pie_chart,
|
|
384
|
+ ip_port_heatmap, ip_change_bar_chart, ip_change_data, summary_content)
|
|
385
|
+
|
|
386
|
+@app.callback(
|
|
387
|
+ [Output('vulnerability-treemap', 'figure'),
|
|
388
|
+ Output('clicked-ip', 'children')],
|
|
389
|
+ [Input('severity-dropdown', 'value'),
|
|
390
|
+ Input('ip-dropdown', 'value'),
|
|
391
|
+ Input({'type': 'dynamic-ip', 'index': ALL}, 'n_clicks')],
|
|
392
|
+ [State({'type': 'dynamic-ip', 'index': ALL}, 'index')]
|
|
393
|
+)
|
|
394
|
+def update_treemap(selected_severity, selected_ip, n_clicks, ip_indices):
|
|
395
|
+ ctx = dash.callback_context
|
|
396
|
+ triggered_id = ctx.triggered[0]['prop_id'] if ctx.triggered else None
|
|
397
|
+ # Determine if the callback was triggered by a related IP link click
|
|
398
|
+ if ctx.triggered and 'dynamic-ip' in ctx.triggered[0]['prop_id']:
|
|
399
|
+ # Extract clicked IP
|
|
400
|
+ triggered_info = ctx.triggered[0]
|
|
401
|
+ button_id = triggered_info['prop_id'].split('}.')[0] + '}'
|
|
402
|
+ clicked_ip = json.loads(button_id)['index']
|
|
403
|
+ else:
|
|
404
|
+ clicked_ip = None
|
|
405
|
+
|
|
406
|
+ # Filter data based on severity, dropdown IP, or clicked related IP
|
|
407
|
+ filtered_data = grouped_data.copy()
|
|
408
|
+ filtered_data['CVSS'] = filtered_data['CVSS'].fillna(0)
|
|
409
|
+ if selected_severity != 'All':
|
|
410
|
+ filtered_data = filtered_data[filtered_data['Severity'] == selected_severity]
|
|
411
|
+ if selected_ip != 'All':
|
|
412
|
+ filtered_data = filtered_data[filtered_data['IP'] == selected_ip]
|
|
413
|
+ if clicked_ip:
|
|
414
|
+ filtered_data = filtered_data[filtered_data['IP'] == clicked_ip]
|
|
415
|
+ filtered_data = filtered_data[filtered_data['CVSS'] > 0]
|
|
416
|
+
|
|
417
|
+ fig = px.treemap(
|
|
418
|
+ filtered_data,
|
|
419
|
+ path=['IP', 'NVT Name'],
|
|
420
|
+ values='CVSS',
|
|
421
|
+ color='CVSS',
|
|
422
|
+ color_continuous_scale='reds',
|
|
423
|
+ hover_data=['Details']
|
|
424
|
+ )
|
|
425
|
+ return fig, "" # Reset clicked-ip because of bug
|
|
426
|
+
|
|
427
|
+# Callback to display details and related IPs
|
|
428
|
+@app.callback(
|
|
429
|
+ Output('details-and-ip-output', 'children'),
|
|
430
|
+ [Input('vulnerability-treemap', 'clickData')]
|
|
431
|
+)
|
|
432
|
+def display_details_and_ips(clickData):
|
|
433
|
+ if clickData is not None:
|
|
434
|
+ clicked_vuln = clickData['points'][0]['label'].split('<br>')[0]
|
|
435
|
+ details = clickData['points'][0]['customdata'][0]
|
|
436
|
+ matching_ips = vulnerability_data[vulnerability_data['NVT Name'] == clicked_vuln]['IP'].unique()
|
|
437
|
+
|
|
438
|
+ return html.Div([
|
|
439
|
+ html.Pre(f'Details of Selected Vulnerability:\n{details}'),
|
|
440
|
+ html.H4("Related IPs with the same vulnerability:"),
|
|
441
|
+ html.Div([html.A(ip, href='#', id={'type': 'dynamic-ip', 'index': ip}, style={'marginRight': '10px', 'cursor': 'pointer'}) for ip in matching_ips])
|
|
442
|
+ ])
|
|
443
|
+ return 'Click on a vulnerability to see details and related IPs.'
|
|
444
|
+
|
|
445
|
+if __name__ == '__main__':
|
|
446
|
+ app.run(debug=True)
|