traffic-taffy 0.3.6__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. traffic_taffy/cache_info.py +0 -6
  2. traffic_taffy/compare.py +154 -250
  3. traffic_taffy/comparison.py +26 -0
  4. traffic_taffy/dissection.py +383 -0
  5. traffic_taffy/dissectmany.py +20 -18
  6. traffic_taffy/dissector.py +128 -476
  7. traffic_taffy/dissector_engine/__init__.py +35 -0
  8. traffic_taffy/dissector_engine/dpkt.py +98 -0
  9. traffic_taffy/dissector_engine/scapy.py +98 -0
  10. traffic_taffy/graph.py +23 -90
  11. traffic_taffy/graphdata.py +35 -20
  12. traffic_taffy/output/__init__.py +118 -0
  13. traffic_taffy/output/console.py +72 -0
  14. traffic_taffy/output/fsdb.py +50 -0
  15. traffic_taffy/output/memory.py +51 -0
  16. traffic_taffy/pcap_splitter.py +17 -36
  17. traffic_taffy/tools/cache_info.py +65 -0
  18. traffic_taffy/tools/compare.py +110 -0
  19. traffic_taffy/tools/dissect.py +77 -0
  20. traffic_taffy/tools/explore.py +686 -0
  21. traffic_taffy/tools/graph.py +85 -0
  22. {traffic_taffy-0.3.6.dist-info → traffic_taffy-0.4.1.dist-info}/METADATA +1 -1
  23. traffic_taffy-0.4.1.dist-info/RECORD +29 -0
  24. traffic_taffy-0.4.1.dist-info/entry_points.txt +6 -0
  25. pcap_compare/cache_info.py +0 -46
  26. pcap_compare/compare.py +0 -288
  27. pcap_compare/dissectmany.py +0 -21
  28. pcap_compare/dissector.py +0 -512
  29. pcap_compare/dissectorresults.py +0 -21
  30. pcap_compare/graph.py +0 -210
  31. traffic_taffy/explore.py +0 -221
  32. traffic_taffy-0.3.6.dist-info/RECORD +0 -22
  33. traffic_taffy-0.3.6.dist-info/entry_points.txt +0 -5
  34. {pcap_compare → traffic_taffy/tools}/__init__.py +0 -0
  35. {traffic_taffy-0.3.6.dist-info → traffic_taffy-0.4.1.dist-info}/WHEEL +0 -0
  36. {traffic_taffy-0.3.6.dist-info → traffic_taffy-0.4.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,686 @@
1
+ import sys
2
+ from os.path import basename
3
+ import logging
4
+ from logging import debug
5
+ from datetime import datetime
6
+ import datetime as dt
7
+ from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
8
+ from traffic_taffy.dissector import (
9
+ dissector_add_parseargs,
10
+ limitor_add_parseargs,
11
+ check_dissector_level,
12
+ )
13
+ from traffic_taffy.dissection import Dissection
14
+ from traffic_taffy.graphdata import PcapGraphData
15
+ from traffic_taffy.compare import (
16
+ PcapCompare,
17
+ get_comparison_args,
18
+ compare_add_parseargs,
19
+ )
20
+ from traffic_taffy.output.memory import Memory
21
+
22
+ from PyQt6.QtCharts import QLineSeries, QChart, QChartView, QDateTimeAxis, QValueAxis
23
+ from PyQt6.QtCore import Qt, QTimer
24
+ from PyQt6.QtGui import QImage, QColor
25
+
26
+ from PyQt6.QtWidgets import (
27
+ QPushButton,
28
+ QDialog,
29
+ QGridLayout,
30
+ QVBoxLayout,
31
+ QHBoxLayout,
32
+ QApplication,
33
+ QWidget,
34
+ QLabel,
35
+ QScrollArea,
36
+ QSpinBox,
37
+ QToolButton,
38
+ QMenu,
39
+ QCheckBox,
40
+ )
41
+
42
+
43
+ class CallWithParameter:
44
+ def __init__(self, function, *args):
45
+ self.parameters = args
46
+ self.function = function
47
+
48
+ def __call__(self):
49
+ self.function(*self.parameters)
50
+
51
+
52
+ class TaffyExplorer(QDialog, PcapGraphData):
53
+ """Explore PCAP files by comparison slices"""
54
+
55
+ def testplot(self, area):
56
+ print(area)
57
+ # self.traffic_graph.setPlotArea(area)
58
+ # self.detail_graph.setPlotArea(area)
59
+ self.traffic_graph.zoomIn(area)
60
+
61
+ def __init__(self, args):
62
+ super().__init__()
63
+
64
+ self.mainLayout = QVBoxLayout()
65
+ self.setLayout(self.mainLayout)
66
+
67
+ # create the graph at the top
68
+ self.detail_graph = QChart()
69
+ self.detail_graph_view = QChartView(self.detail_graph)
70
+ self.detail_graph_view.setRubberBand(QChartView.RubberBand.RectangleRubberBand)
71
+ self.detail_graph.setMinimumSize(1000, 400)
72
+ # this is the screen space not the zoom setting
73
+ # self.detail_graph.plotAreaChanged.connect(self.testplot)
74
+ self.mainLayout.addWidget(self.detail_graph_view)
75
+
76
+ # create the mini graph next
77
+ self.traffic_graph = QChart()
78
+ self.traffic_graph.legend().hide()
79
+ self.traffic_graph.setTitle("All Traffic")
80
+ self.traffic_graph_view = QChartView(self.traffic_graph)
81
+ self.traffic_graph_view.setRubberBand(QChartView.RubberBand.RectangleRubberBand)
82
+ self.traffic_graph.setMinimumSize(1000, 200)
83
+ self.mainLayout.addWidget(self.traffic_graph_view)
84
+
85
+ # create the traffic source menu bar
86
+ self.source_menus = QHBoxLayout()
87
+ self.source_menus_w = QWidget()
88
+ self.source_menus_w.setLayout(self.source_menus)
89
+ self.mainLayout.addWidget(self.source_menus_w)
90
+
91
+ self.control_menus = QHBoxLayout()
92
+ self.control_menus_w = QWidget()
93
+ self.control_menus_w.setLayout(self.control_menus)
94
+ self.mainLayout.addWidget(self.control_menus_w)
95
+
96
+ self.comparison_panel_w = None # place holder for update_report()
97
+
98
+ # the comparison panel contains deltas between them
99
+ self.scroll_area = QScrollArea()
100
+ self.scroll_area.setMinimumSize(1000, 200)
101
+ self.scroll_area.setWidgetResizable(True)
102
+
103
+ self.mainLayout.addWidget(self.scroll_area)
104
+
105
+ self.quit_button = QPushButton("Quit")
106
+ self.mainLayout.addWidget(self.quit_button)
107
+ self.quit_button.clicked.connect(self.quit)
108
+
109
+ # self.tree = QTreeWidget()
110
+ # self.tree.setHeaderHidden(True)
111
+ # self.tree.setIndentation(0)
112
+
113
+ self.args = args
114
+
115
+ self.only_positive = args.only_positive
116
+ self.only_negative = args.only_negative
117
+ self.print_threshold = args.print_threshold
118
+ self.minimum_count = args.minimum_count
119
+ self.minimum_graph_count = args.minimum_count
120
+ self.top_records = args.top_records
121
+
122
+ # other needed itmes
123
+ self.min_count_changed_timer = QTimer(self)
124
+ self.min_count_changed_timer.setSingleShot(True)
125
+ self.min_count_changed_timer.setInterval(1000)
126
+ self.min_count_changed_timer.timeout.connect(self.min_count_changed_actual)
127
+
128
+ self.min_graph_changed_timer = QTimer(self)
129
+ self.min_graph_changed_timer.setSingleShot(True)
130
+ self.min_graph_changed_timer.setInterval(1000)
131
+ self.min_graph_changed_timer.timeout.connect(
132
+ self.min_graph_count_changed_actual
133
+ )
134
+
135
+ self.top_records_changed_timer = QTimer(self)
136
+ self.top_records_changed_timer.setSingleShot(True)
137
+ self.top_records_changed_timer.setInterval(1000)
138
+ self.top_records_changed_timer.timeout.connect(self.top_records_changed_actual)
139
+
140
+ self.axisX = None
141
+ self.axisY = None
142
+
143
+ self.printing_arguments = get_comparison_args(self.args)
144
+
145
+ self.chart_column = "count"
146
+
147
+ def quit(self):
148
+ exit()
149
+
150
+ def create_initial_comparison_report_arguments(self):
151
+ if len(self.dissections) == 1:
152
+ self.dissection1 = self.dissections[0]
153
+ self.dissection2 = self.dissection1
154
+
155
+ keys = list(self.dissection1.data.keys())
156
+
157
+ # skipping key 0 which is the full timestamp
158
+ self.dissection_key1 = keys[1]
159
+ self.dissection_key2 = keys[2]
160
+
161
+ else:
162
+ self.dissection1 = self.dissections[0]
163
+ self.dissection2 = self.dissections[1]
164
+
165
+ # comparing the full times
166
+ self.dissection_key1 = 0
167
+ self.dissection_key2 = 0
168
+
169
+ def create_comparison(self):
170
+ self.pc = PcapCompare(
171
+ self.args.pcap_files,
172
+ maximum_count=self.args.packet_count,
173
+ cache_results=self.args.cache_pcap_results,
174
+ cache_file_suffix=self.args.cache_file_suffix,
175
+ dissection_level=self.args.dissection_level,
176
+ between_times=self.args.between_times,
177
+ bin_size=self.args.bin_size,
178
+ )
179
+
180
+ # create the graph data storage
181
+ # and load everything in
182
+ self.dissections = list(self.pc.load_pcaps())
183
+
184
+ self.create_initial_comparison_report_arguments()
185
+ self.compare_two()
186
+
187
+ def compare_two(self):
188
+ self.comparison = self.pc.compare_dissections(
189
+ self.dissection1.data[self.dissection_key1],
190
+ self.dissection2.data[self.dissection_key2],
191
+ )
192
+
193
+ def update_chart(
194
+ self,
195
+ chart: QChart,
196
+ match_string: str,
197
+ match_value: str | None = None,
198
+ chart_column: str = None,
199
+ ):
200
+ self.match_string = match_string
201
+ self.match_value = match_value
202
+
203
+ if chart_column is None:
204
+ chart_column = self.chart_column
205
+
206
+ # for matching on a single value, don't do a minimum count at all
207
+ tmpv = self.minimum_count
208
+ self.minimum_count = self.minimum_graph_count
209
+ if match_value is not None:
210
+ self.minimum_count = 0
211
+
212
+ df = self.get_dataframe(calculate_load_fraction=True)
213
+
214
+ # TODO: there must be a better way! (key is duplicated)
215
+ series_set = []
216
+ maxv = -100
217
+ for key in df.key.unique():
218
+ series = QLineSeries()
219
+
220
+ for index in df[df["key"] == key].index:
221
+ series.append(
222
+ df["time"][index].to_pydatetime().timestamp() * 1000,
223
+ df[chart_column][index],
224
+ )
225
+
226
+ height = df["count"][index]
227
+ maxv = max(maxv, height)
228
+
229
+ series.setName(df["subkey"][index])
230
+ series.setOpacity(0.5)
231
+ series_set.append(series)
232
+ # axisx = QDateTimeAxis()
233
+ # chart.setAxisX()
234
+
235
+ if len(df) == 0:
236
+ return # TODO: handle displaying an error
237
+
238
+ df["time"].min().to_pydatetime().timestamp()
239
+ df["time"].max().to_pydatetime().timestamp()
240
+
241
+ grey = QColor("grey")
242
+
243
+ # add another series for file ovelays
244
+ for dissection in self.dissections:
245
+ timestamps = list(dissection.data.keys())
246
+ first_time = timestamps[1] # skip the leading 0 timestamp
247
+ last_time = timestamps[-1]
248
+
249
+ # maxv = max(dict(dissection.data.values()))
250
+
251
+ # tick-height:
252
+ tick_height = int(0.01 * maxv)
253
+
254
+ # time range with up/down markers
255
+ series = QLineSeries()
256
+ for timestamp in timestamps[1:]:
257
+ series.append(timestamp * 1000, maxv + tick_height)
258
+ series.append(timestamp * 1000, maxv + 1)
259
+ series.append(timestamp * 1000, maxv + tick_height)
260
+ series.setName(dissection.pcap_file)
261
+ series.setColor(grey)
262
+ series_set.append(series)
263
+ # chart.addSeries(series)
264
+ # series.attachAxis(axisX)
265
+ # series.attachAxis(axisY)
266
+
267
+ # beginning end markers
268
+ series = QLineSeries()
269
+ series.append(first_time * 1000, maxv + tick_height)
270
+ series.append(last_time * 1000, maxv + tick_height)
271
+
272
+ series.setMarkerSize(20)
273
+ triangle = QImage("images/grey_triangle.png").scaled(10, 10)
274
+ series.setLightMarker(triangle)
275
+ # series.setColor(grey)
276
+ series_set.append(series)
277
+ # chart.addSeries(series)
278
+ # series.attachAxis(axisX)
279
+ # series.attachAxis(axisY)
280
+
281
+ # we always add the real data last to keep file name coloring consistent
282
+
283
+ if self.axisX:
284
+ chart.removeAxis(self.axisX)
285
+ self.axisX = QDateTimeAxis()
286
+ self.axisX.setTickCount(5)
287
+ self.axisX.setFormat("yyyy-MM-dd\nhh:mm")
288
+ # self.axisX.setLabelsAngle(-45)
289
+ chart.addAxis(self.axisX, Qt.AlignmentFlag.AlignBottom)
290
+
291
+ if self.axisY:
292
+ chart.removeAxis(self.axisY)
293
+ self.axisY = QValueAxis()
294
+ self.axisY.setLabelFormat("%i")
295
+ chart.addAxis(self.axisY, Qt.AlignmentFlag.AlignLeft)
296
+
297
+ # if these aren't all added at the very end then the axis are
298
+ # all incorrectly zoomed.
299
+ for series in series_set:
300
+ chart.addSeries(series)
301
+ series.attachAxis(self.axisX)
302
+ series.attachAxis(self.axisY)
303
+
304
+ # series = QLineSeries()
305
+ # series.append(first_time, 0)
306
+ # series.append(first_time, maxv)
307
+ # series.attachAxis(axisX)
308
+ # series.attachAxis(axisY)
309
+ # chart.addSeries(series)
310
+
311
+ # chart.createDefaultAxes()
312
+ # chart.zoomIn(QRectF(QPointF(first_time/1000.0, maxv), QPointF(last_time/1000.0, 0)))
313
+
314
+ self.saved_df = df
315
+ self.minimum_count = tmpv
316
+
317
+ def update_detail_chart(
318
+ self, match_string: str = "__TOTAL__", match_value: str | None = None
319
+ ):
320
+ self.detail_graph.setTitle(match_string)
321
+ self.detail_graph.removeAllSeries()
322
+ self.update_chart(self.detail_graph, match_string, match_value)
323
+
324
+ def update_traffic_chart(self):
325
+ self.update_chart(self.traffic_graph, "__TOTAL__", chart_column="count")
326
+
327
+ # def show_comparison(self, pcap_one, timestamp_one, pcap_two, timestamp_two):
328
+
329
+ def graph_type_changed(self, value):
330
+ if value == 0:
331
+ self.chart_column = "count"
332
+ else:
333
+ self.chart_column = "load_fraction"
334
+ self.update_detail_chart(self.match_string, self.match_value)
335
+
336
+ def header_clicked(self, key):
337
+ self.update_detail_chart(key, None)
338
+
339
+ def min_count_changed_actual(self):
340
+ self.printing_arguments["minimum_count"] = self.minimum_count
341
+ self.update_report()
342
+ self.update_detail_chart(self.match_string, self.match_value)
343
+ debug(f"updating table with minimum count of {self.minimum_count}")
344
+
345
+ def min_count_changed(self, value):
346
+ self.minimum_count = value
347
+ # in case we're running already, stop it first
348
+ self.min_count_changed_timer.stop()
349
+ self.min_count_changed_timer.start()
350
+ debug(f"changed minimum count to {self.minimum_count}")
351
+
352
+ def min_graph_count_changed_actual(self):
353
+ self.update_detail_chart(self.match_string, self.match_value)
354
+ debug(f"updating graph with minimum count of {self.minimum_graph_count}")
355
+
356
+ def min_graph_count_changed(self, value):
357
+ self.minimum_graph_count = value
358
+ # in case we're running already, stop it first
359
+ self.min_graph_changed_timer.stop()
360
+ self.min_graph_changed_timer.start()
361
+ debug(f"changed minimum count to {self.minimum_graph_count}")
362
+
363
+ def top_records_changed_actual(self):
364
+ self.printing_arguments["top_records"] = self.top_records
365
+ self.update_report()
366
+ self.update_detail_chart(self.match_string, self.match_value)
367
+ debug(f"updating top report count with {self.top_records}")
368
+
369
+ def top_records_changed(self, value):
370
+ self.top_records = value
371
+ # in case we're running already, stop it first
372
+ self.top_records_changed_timer.stop()
373
+ self.top_records_changed_timer.start()
374
+
375
+ # def clearGridLayout(layout, deleteWidgets: bool = True):
376
+
377
+ # for widget in layout.something():
378
+ # layout.removeWidget(widget)
379
+ # widget.deletLater()
380
+
381
+ # while (QLayoutItem* item = layout->takeAt(0))
382
+
383
+ # if (deleteWidgets)
384
+ # {
385
+ # if (QWidget* widget = item->widget())
386
+ # widget->deleteLater();
387
+ # }
388
+ # if (QLayout* childLayout = item->layout())
389
+ # clearLayout(childLayout, deleteWidgets);
390
+ # delete item;
391
+ # }
392
+
393
+ def set_left_dissection(self, action):
394
+ self.left_w.setText(basename(action.text()))
395
+ selection = action.data()
396
+ if isinstance(selection, tuple):
397
+ (filenum, timestamp) = selection
398
+ self.dissection1 = self.dissections[filenum]
399
+ self.dissection1_key = timestamp
400
+ else:
401
+ self.dissection1 = self.dissections[selection]
402
+ self.dissection1_key = 0
403
+ self.compare_two()
404
+ self.update_report()
405
+
406
+ def set_right_dissection(self, action):
407
+ self.right_w.setText(basename(action.text()))
408
+
409
+ selection = action.data()
410
+ if isinstance(selection, tuple):
411
+ (filenum, timestamp) = selection
412
+ self.dissection2 = self.dissections[filenum]
413
+ self.dissection2_key = timestamp
414
+ else:
415
+ self.dissection2 = self.dissections[selection]
416
+ self.dissection2_key = 0
417
+
418
+ self.compare_two()
419
+ self.update_report()
420
+
421
+ def update_left_right_sources(self):
422
+ self.left_menu = QMenu(basename(self.dissection1.pcap_file))
423
+ for n, item in enumerate(self.dissections):
424
+ # TODO: this should warn or be a configurable limit or something...
425
+ if len(item.data) < 20:
426
+ time_menu = self.left_menu.addMenu(item.pcap_file)
427
+ for timestamp in item.data:
428
+ if timestamp == 0:
429
+ menu_name = item.pcap_file + " ALL"
430
+ else:
431
+ menu_name = datetime.fromtimestamp(timestamp, dt.UTC).strftime(
432
+ "%Y-%m-%d %H:%M:%S"
433
+ )
434
+ submenu_action = time_menu.addAction(menu_name)
435
+ submenu_action.setData((n, timestamp))
436
+ else:
437
+ action = self.left_menu.addAction(item.pcap_file)
438
+ action.setData(n)
439
+
440
+ self.left_w.setMenu(self.left_menu)
441
+ self.left_w.setText(basename(self.dissection1.pcap_file))
442
+
443
+ self.right_menu = QMenu(basename(self.dissection2.pcap_file))
444
+ for n, item in enumerate(self.dissections):
445
+ # TODO: this should warn or be a configurable limit or something...
446
+ if len(item.data) < 20:
447
+ time_menu = self.right_menu.addMenu(item.pcap_file)
448
+ for timestamp in item.data:
449
+ if timestamp == 0:
450
+ menu_name = basename(item.pcap_file) + " ALL"
451
+ else:
452
+ menu_name = datetime.fromtimestamp(timestamp, dt.UTC).strftime(
453
+ "%Y-%m-%d %H:%M:%S"
454
+ )
455
+ submenu_action = time_menu.addAction(menu_name)
456
+ submenu_action.setData((n, timestamp))
457
+ else:
458
+ action = self.right_menu.addAction(item.pcap_file)
459
+ action.setData(n)
460
+
461
+ self.right_w.setMenu(self.right_menu)
462
+ self.right_w.setText(basename(self.dissection2.pcap_file))
463
+
464
+ def add_control_widgets(self):
465
+ self.source_menus.addWidget(QLabel("Left:"))
466
+ self.left_w = QToolButton(
467
+ autoRaise=True, popupMode=QToolButton.ToolButtonPopupMode.InstantPopup
468
+ )
469
+ self.left_w.triggered.connect(self.set_left_dissection)
470
+ self.source_menus.addWidget(self.left_w)
471
+
472
+ self.source_menus.addStretch()
473
+
474
+ self.source_menus.addWidget(QLabel("Right:"))
475
+ self.right_w = QToolButton(
476
+ autoRaise=True, popupMode=QToolButton.ToolButtonPopupMode.InstantPopup
477
+ )
478
+ self.right_w.triggered.connect(self.set_right_dissection)
479
+ self.source_menus.addWidget(self.right_w)
480
+
481
+ self.update_left_right_sources()
482
+
483
+ self.control_menus.addWidget(QLabel("Minimum report count:"))
484
+ self.minimum_count_w = QSpinBox()
485
+ self.minimum_count_w.setMinimum(0)
486
+ self.minimum_count_w.setMaximum(1000000) # TODO: inf
487
+ self.minimum_count_w.setValue(int(self.minimum_count))
488
+ self.minimum_count_w.setSingleStep(5)
489
+
490
+ self.minimum_count_w.valueChanged.connect(self.min_count_changed)
491
+ self.control_menus.addWidget(self.minimum_count_w)
492
+
493
+ self.control_menus.addWidget(QLabel("Report at most:"))
494
+ self.top_records_w = QSpinBox()
495
+ self.top_records_w.setMinimum(0)
496
+ self.top_records_w.setMaximum(1000000) # TODO: inf
497
+ self.top_records_w.setValue(int(self.top_records or 0))
498
+ self.top_records_w.setSingleStep(1)
499
+
500
+ self.top_records_w.valueChanged.connect(self.top_records_changed)
501
+ self.control_menus.addWidget(self.top_records_w)
502
+
503
+ self.control_menus.addWidget(QLabel("Minimum graph count:"))
504
+ self.minimum_graph_count_w = QSpinBox()
505
+ self.minimum_graph_count_w.setMinimum(0)
506
+ self.minimum_graph_count_w.setMaximum(1000000) # TODO: inf
507
+ self.minimum_graph_count_w.setValue(int(self.minimum_graph_count))
508
+ self.minimum_graph_count_w.setSingleStep(5)
509
+
510
+ self.minimum_graph_count_w.valueChanged.connect(self.min_graph_count_changed)
511
+ self.control_menus.addWidget(self.minimum_graph_count_w)
512
+
513
+ self.show_as_percent_w = QCheckBox("Percent")
514
+ self.control_menus.addWidget(self.show_as_percent_w)
515
+ self.show_as_percent_w.stateChanged.connect(self.graph_type_changed)
516
+
517
+ def update_report(self):
518
+ # TODO: less duplication with this and compare:print_report()
519
+ "fills in the grid table showing the differences from a saved report"
520
+
521
+ old_widget = self.comparison_panel_w
522
+
523
+ # add a new one
524
+ self.comparison_panel = QGridLayout()
525
+ self.comparison_panel_w = QWidget()
526
+ self.comparison_panel_w.setLayout(self.comparison_panel)
527
+ self.scroll_area.setWidget(self.comparison_panel_w)
528
+
529
+ del old_widget
530
+
531
+ # we need to store the key/match values to reset
532
+ (tmp_key, tmp_value) = (self.match_string, self.match_value)
533
+ self.match_string = None
534
+ self.match_value = None
535
+
536
+ # add the header in row 0
537
+ headers = [
538
+ "Value",
539
+ "Left Count",
540
+ "Right Count",
541
+ "Delta",
542
+ "Left %",
543
+ "Right %",
544
+ "Delta %",
545
+ ]
546
+ for n, header in enumerate(headers):
547
+ header = header.replace(" ", "**\n\n**")
548
+ label = QLabel("**" + header + "**")
549
+ label.setAlignment(Qt.AlignmentFlag.AlignRight)
550
+ label.setTextFormat(Qt.TextFormat.MarkdownText)
551
+ self.comparison_panel.addWidget(label, 0, n)
552
+
553
+ current_grid_row = 1
554
+
555
+ printing_arguments = self.printing_arguments
556
+ memory_report = Memory(self.comparison.title, printing_arguments)
557
+ memory_report.output(self.comparison)
558
+
559
+ for key in memory_report.memory:
560
+ reported = False
561
+ for record in memory_report.memory[key]:
562
+ # add the header
563
+ if not reported:
564
+ debug(f"reporting on {key}")
565
+ report_button = QPushButton(key)
566
+ report_button.clicked.connect(
567
+ CallWithParameter(self.update_detail_chart, key)
568
+ )
569
+ self.comparison_panel.addWidget(
570
+ report_button, current_grid_row, 0, 1, 6
571
+ )
572
+ current_grid_row += 1
573
+ reported = True
574
+
575
+ subkey = record["subkey"]
576
+ delta_percentage: float = record["delta_percentage"]
577
+
578
+ # apply some fancy styling
579
+ style = ""
580
+ if delta_percentage < -0.5:
581
+ style = "color: red" # TODO bold
582
+ elif delta_percentage < 0.0:
583
+ style = "color: red"
584
+ elif delta_percentage > 0.5:
585
+ style = "color: lightgreen" # TODO bold
586
+ elif delta_percentage > 0.0:
587
+ style = "color: lightgreen"
588
+
589
+ # construct the output line with styling
590
+ subkey = Dissection.make_printable(key, subkey)
591
+ debug(f" adding {subkey}")
592
+
593
+ subkey_button = QPushButton(" " + subkey)
594
+ # subkey_button.setAlignment(Qt.AlignmentFlag.AlignLeft)
595
+ subkey_button.clicked.connect(
596
+ CallWithParameter(self.update_detail_chart, key, subkey)
597
+ )
598
+ subkey_button.setStyleSheet(style)
599
+ self.comparison_panel.addWidget(subkey_button, current_grid_row, 0)
600
+
601
+ column = 0
602
+
603
+ column += 1
604
+ label = QLabel(f"{record['left_count']:>8}")
605
+ label.setAlignment(Qt.AlignmentFlag.AlignRight)
606
+ self.comparison_panel.addWidget(label, current_grid_row, column)
607
+
608
+ column += 1
609
+ label = QLabel(f"{record['right_count']:>8}")
610
+ label.setAlignment(Qt.AlignmentFlag.AlignRight)
611
+ self.comparison_panel.addWidget(label, current_grid_row, column)
612
+
613
+ column += 1
614
+ label = QLabel(f"{record['delta_absolute']:>8}")
615
+ label.setAlignment(Qt.AlignmentFlag.AlignRight)
616
+ self.comparison_panel.addWidget(label, current_grid_row, column)
617
+
618
+ column += 1
619
+ label = QLabel(f"{record['left_percentage']:>7.2f}")
620
+ label.setAlignment(Qt.AlignmentFlag.AlignRight)
621
+ self.comparison_panel.addWidget(label, current_grid_row, column)
622
+
623
+ column += 1
624
+ label = QLabel(f"{record['right_percentage']:>7.2f}")
625
+ label.setAlignment(Qt.AlignmentFlag.AlignRight)
626
+ self.comparison_panel.addWidget(label, current_grid_row, column)
627
+
628
+ column += 1
629
+ label = QLabel(f"{100*delta_percentage:>7.2f}")
630
+ label.setAlignment(Qt.AlignmentFlag.AlignRight)
631
+ self.comparison_panel.addWidget(label, current_grid_row, column)
632
+
633
+ current_grid_row += 1
634
+
635
+ (self.match_string, self.match_value) = (tmp_key, tmp_value)
636
+
637
+
638
+ def parse_args():
639
+ "Parse the command line arguments."
640
+ parser = ArgumentParser(
641
+ formatter_class=ArgumentDefaultsHelpFormatter,
642
+ description=__doc__,
643
+ epilog="Exmaple Usage: ",
644
+ )
645
+
646
+ limitor_parser = limitor_add_parseargs(parser)
647
+ compare_add_parseargs(limitor_parser, False)
648
+
649
+ dissector_add_parseargs(parser)
650
+
651
+ debugging_group = parser.add_argument_group("Debugging options")
652
+
653
+ debugging_group.add_argument(
654
+ "--log-level",
655
+ "--ll",
656
+ default="info",
657
+ help="Define the logging verbosity level (debug, info, warning, error, ...).",
658
+ )
659
+
660
+ parser.add_argument("pcap_files", type=str, nargs="+", help="PCAP files to analyze")
661
+
662
+ args = parser.parse_args()
663
+ log_level = args.log_level.upper()
664
+ logging.basicConfig(level=log_level, format="%(levelname)-10s:\t%(message)s")
665
+
666
+ check_dissector_level(args.dissection_level)
667
+
668
+ return args
669
+
670
+
671
+ def main():
672
+ args = parse_args()
673
+
674
+ app = QApplication(sys.argv)
675
+ window = TaffyExplorer(args)
676
+ window.create_comparison()
677
+ window.add_control_widgets()
678
+ window.update_traffic_chart()
679
+ window.update_detail_chart()
680
+ window.update_report()
681
+ window.show()
682
+ sys.exit(app.exec())
683
+
684
+
685
+ if __name__ == "__main__":
686
+ main()