traffic-taffy 0.4__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.
- traffic_taffy/tools/__init__.py +0 -0
- traffic_taffy/tools/cache_info.py +65 -0
- traffic_taffy/tools/compare.py +110 -0
- traffic_taffy/tools/dissect.py +77 -0
- traffic_taffy/tools/explore.py +686 -0
- traffic_taffy/tools/graph.py +85 -0
- {traffic_taffy-0.4.dist-info → traffic_taffy-0.4.1.dist-info}/METADATA +1 -1
- {traffic_taffy-0.4.dist-info → traffic_taffy-0.4.1.dist-info}/RECORD +11 -5
- {traffic_taffy-0.4.dist-info → traffic_taffy-0.4.1.dist-info}/WHEEL +0 -0
- {traffic_taffy-0.4.dist-info → traffic_taffy-0.4.1.dist-info}/entry_points.txt +0 -0
- {traffic_taffy-0.4.dist-info → traffic_taffy-0.4.1.dist-info}/top_level.txt +0 -0
File without changes
|
@@ -0,0 +1,65 @@
|
|
1
|
+
"""Loads the cached data for a file to display the results about it"""
|
2
|
+
|
3
|
+
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
|
4
|
+
from rich import print
|
5
|
+
import logging
|
6
|
+
import msgpack
|
7
|
+
|
8
|
+
|
9
|
+
def parse_args():
|
10
|
+
"Parse the command line arguments."
|
11
|
+
parser = ArgumentParser(
|
12
|
+
formatter_class=ArgumentDefaultsHelpFormatter,
|
13
|
+
description=__doc__,
|
14
|
+
epilog="Exmaple Usage: ",
|
15
|
+
)
|
16
|
+
|
17
|
+
parser.add_argument(
|
18
|
+
"--log-level",
|
19
|
+
"--ll",
|
20
|
+
default="info",
|
21
|
+
help="Define the logging verbosity level (debug, info, warning, error, fotal, critical).",
|
22
|
+
)
|
23
|
+
|
24
|
+
parser.add_argument(
|
25
|
+
"cache_file",
|
26
|
+
type=str,
|
27
|
+
nargs="+",
|
28
|
+
help="The cache file (or pcap file) to load and display information about",
|
29
|
+
)
|
30
|
+
|
31
|
+
args = parser.parse_args()
|
32
|
+
log_level = args.log_level.upper()
|
33
|
+
logging.basicConfig(level=log_level, format="%(levelname)-10s:\t%(message)s")
|
34
|
+
return args
|
35
|
+
|
36
|
+
|
37
|
+
def main():
|
38
|
+
args = parse_args()
|
39
|
+
|
40
|
+
for cache_file in args.cache_file:
|
41
|
+
print(f"===== {cache_file} ======")
|
42
|
+
contents = msgpack.load(open(cache_file, "rb"), strict_map_key=False)
|
43
|
+
|
44
|
+
# play the major keys
|
45
|
+
for key in contents.keys():
|
46
|
+
if key != "dissection" and key != "parameters":
|
47
|
+
print(f"{key:<20} {contents[key]}")
|
48
|
+
|
49
|
+
# then the minors
|
50
|
+
print("parameters:")
|
51
|
+
for key in contents["parameters"]:
|
52
|
+
print(f" {key:<16} {contents['parameters'][key]}")
|
53
|
+
|
54
|
+
print("data info:")
|
55
|
+
timestamps = list(contents["dissection"].keys())
|
56
|
+
print(f" timestamps: {len(timestamps)}")
|
57
|
+
if len(timestamps) > 1:
|
58
|
+
print(f" first: {timestamps[1]}") # skips 0 = global
|
59
|
+
print(f" last: {timestamps[-1]}")
|
60
|
+
else:
|
61
|
+
print(" (only the entire summary timestamp)")
|
62
|
+
|
63
|
+
|
64
|
+
if __name__ == "__main__":
|
65
|
+
main()
|
@@ -0,0 +1,110 @@
|
|
1
|
+
"""Takes a set of pcap files to compare and creates a report"""
|
2
|
+
|
3
|
+
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
|
4
|
+
import logging
|
5
|
+
from traffic_taffy.output.console import Console
|
6
|
+
from traffic_taffy.output.fsdb import Fsdb
|
7
|
+
|
8
|
+
from traffic_taffy.compare import compare_add_parseargs, get_comparison_args
|
9
|
+
from traffic_taffy.dissector import (
|
10
|
+
dissector_add_parseargs,
|
11
|
+
limitor_add_parseargs,
|
12
|
+
check_dissector_level,
|
13
|
+
)
|
14
|
+
from traffic_taffy.compare import PcapCompare
|
15
|
+
|
16
|
+
|
17
|
+
def parse_args():
|
18
|
+
"Parse the command line arguments."
|
19
|
+
parser = ArgumentParser(
|
20
|
+
formatter_class=ArgumentDefaultsHelpFormatter,
|
21
|
+
description=__doc__,
|
22
|
+
epilog="Exmaple Usage: ",
|
23
|
+
)
|
24
|
+
|
25
|
+
output_options = parser.add_argument_group("Output format")
|
26
|
+
output_options.add_argument(
|
27
|
+
"-f",
|
28
|
+
"--fsdb",
|
29
|
+
action="store_true",
|
30
|
+
help="Print results in an FSDB formatted output",
|
31
|
+
)
|
32
|
+
|
33
|
+
limitor_parser = limitor_add_parseargs(parser)
|
34
|
+
compare_add_parseargs(limitor_parser, False)
|
35
|
+
dissector_add_parseargs(parser)
|
36
|
+
|
37
|
+
debugging_group = parser.add_argument_group("Debugging options")
|
38
|
+
|
39
|
+
debugging_group.add_argument(
|
40
|
+
"--log-level",
|
41
|
+
"--ll",
|
42
|
+
default="info",
|
43
|
+
help="Define the logging verbosity level (debug, info, warning, error, ...).",
|
44
|
+
)
|
45
|
+
|
46
|
+
parser.add_argument("pcap_files", type=str, nargs="*", help="PCAP files to analyze")
|
47
|
+
|
48
|
+
args = parser.parse_args()
|
49
|
+
log_level = args.log_level.upper()
|
50
|
+
logging.basicConfig(level=log_level, format="%(levelname)-10s:\t%(message)s")
|
51
|
+
|
52
|
+
check_dissector_level(args.dissection_level)
|
53
|
+
|
54
|
+
return args
|
55
|
+
|
56
|
+
|
57
|
+
def main():
|
58
|
+
args = parse_args()
|
59
|
+
|
60
|
+
# setup output options
|
61
|
+
printing_arguments = get_comparison_args(args)
|
62
|
+
|
63
|
+
# get our files to compare (maybe just one)
|
64
|
+
left = args.pcap_files.pop(0)
|
65
|
+
right = None
|
66
|
+
more_than_one = False
|
67
|
+
|
68
|
+
if len(args.pcap_files) > 0:
|
69
|
+
right = args.pcap_files.pop(0)
|
70
|
+
more_than_one = True
|
71
|
+
|
72
|
+
while left:
|
73
|
+
files = [left]
|
74
|
+
if right:
|
75
|
+
files.append(right)
|
76
|
+
|
77
|
+
pc = PcapCompare(
|
78
|
+
files,
|
79
|
+
cache_results=args.cache_pcap_results,
|
80
|
+
cache_file_suffix=args.cache_file_suffix,
|
81
|
+
maximum_count=printing_arguments["maximum_count"],
|
82
|
+
dissection_level=args.dissection_level,
|
83
|
+
between_times=args.between_times,
|
84
|
+
bin_size=args.bin_size,
|
85
|
+
ignore_list=args.ignore_list,
|
86
|
+
)
|
87
|
+
|
88
|
+
# compare the pcaps
|
89
|
+
reports = pc.compare()
|
90
|
+
|
91
|
+
if args.fsdb:
|
92
|
+
output = Fsdb(None, printing_arguments)
|
93
|
+
else:
|
94
|
+
output = Console(None, printing_arguments)
|
95
|
+
|
96
|
+
for report in reports:
|
97
|
+
# output results to the console
|
98
|
+
output.output(report)
|
99
|
+
|
100
|
+
left = right
|
101
|
+
right = None
|
102
|
+
if len(args.pcap_files) > 0:
|
103
|
+
right = args.pcap_files.pop(0)
|
104
|
+
|
105
|
+
if left and not right and more_than_one:
|
106
|
+
left = None
|
107
|
+
|
108
|
+
|
109
|
+
if __name__ == "__main__":
|
110
|
+
main()
|
@@ -0,0 +1,77 @@
|
|
1
|
+
from traffic_taffy.dissector import (
|
2
|
+
PCAPDissector,
|
3
|
+
dissector_add_parseargs,
|
4
|
+
limitor_add_parseargs,
|
5
|
+
check_dissector_level,
|
6
|
+
)
|
7
|
+
|
8
|
+
|
9
|
+
def main():
|
10
|
+
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
|
11
|
+
import logging
|
12
|
+
|
13
|
+
def parse_args():
|
14
|
+
"Parse the command line arguments."
|
15
|
+
parser = ArgumentParser(
|
16
|
+
formatter_class=ArgumentDefaultsHelpFormatter,
|
17
|
+
description=__doc__,
|
18
|
+
epilog="Exmaple Usage: ",
|
19
|
+
)
|
20
|
+
|
21
|
+
parser.add_argument(
|
22
|
+
"--log-level",
|
23
|
+
"--ll",
|
24
|
+
default="info",
|
25
|
+
help="Define the logging verbosity level (debug, info, warning, error, fotal, critical).",
|
26
|
+
)
|
27
|
+
|
28
|
+
parser.add_argument(
|
29
|
+
"-f",
|
30
|
+
"--fsdb",
|
31
|
+
action="store_true",
|
32
|
+
help="Print results in an FSDB formatted output",
|
33
|
+
)
|
34
|
+
|
35
|
+
dissector_add_parseargs(parser)
|
36
|
+
limitor_add_parseargs(parser)
|
37
|
+
|
38
|
+
parser.add_argument("input_file", type=str, help="input pcap file")
|
39
|
+
|
40
|
+
args = parser.parse_args()
|
41
|
+
log_level = args.log_level.upper()
|
42
|
+
logging.basicConfig(level=log_level, format="%(levelname)-10s:\t%(message)s")
|
43
|
+
return args
|
44
|
+
|
45
|
+
args = parse_args()
|
46
|
+
|
47
|
+
check_dissector_level(args.dissection_level)
|
48
|
+
|
49
|
+
pd = PCAPDissector(
|
50
|
+
args.input_file,
|
51
|
+
bin_size=args.bin_size,
|
52
|
+
dissector_level=args.dissection_level,
|
53
|
+
maximum_count=args.packet_count,
|
54
|
+
cache_results=args.cache_pcap_results,
|
55
|
+
cache_file_suffix=args.cache_file_suffix,
|
56
|
+
ignore_list=args.ignore_list,
|
57
|
+
)
|
58
|
+
pd.load(force=args.force)
|
59
|
+
|
60
|
+
if args.fsdb:
|
61
|
+
pd.print_to_fsdb(
|
62
|
+
timestamps=[0],
|
63
|
+
match_string=args.match_string,
|
64
|
+
match_value=args.match_value,
|
65
|
+
minimum_count=args.minimum_count,
|
66
|
+
)
|
67
|
+
else:
|
68
|
+
pd.print(
|
69
|
+
timestamps=[0],
|
70
|
+
match_string=args.match_string,
|
71
|
+
match_value=args.match_value,
|
72
|
+
minimum_count=args.minimum_count,
|
73
|
+
)
|
74
|
+
|
75
|
+
|
76
|
+
if __name__ == "__main__":
|
77
|
+
main()
|
@@ -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()
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""Read a PCAP file and graph it or parts of it"""
|
2
|
+
|
3
|
+
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter
|
4
|
+
from traffic_taffy.graph import PcapGraph
|
5
|
+
from traffic_taffy.dissector import (
|
6
|
+
dissector_add_parseargs,
|
7
|
+
limitor_add_parseargs,
|
8
|
+
check_dissector_level,
|
9
|
+
)
|
10
|
+
import logging
|
11
|
+
|
12
|
+
|
13
|
+
def parse_args():
|
14
|
+
"Parse the command line arguments."
|
15
|
+
parser = ArgumentParser(
|
16
|
+
formatter_class=ArgumentDefaultsHelpFormatter,
|
17
|
+
description=__doc__,
|
18
|
+
epilog="Exmaple Usage: ",
|
19
|
+
)
|
20
|
+
|
21
|
+
parser.add_argument(
|
22
|
+
"-o",
|
23
|
+
"--output-file",
|
24
|
+
default=None,
|
25
|
+
type=str,
|
26
|
+
help="Where to save the output (png)",
|
27
|
+
)
|
28
|
+
|
29
|
+
parser.add_argument(
|
30
|
+
"-p",
|
31
|
+
"--by-percentage",
|
32
|
+
action="store_true",
|
33
|
+
help="Graph by percentage of traffic rather than by value",
|
34
|
+
)
|
35
|
+
|
36
|
+
parser.add_argument(
|
37
|
+
"-i",
|
38
|
+
"--interactive",
|
39
|
+
action="store_true",
|
40
|
+
help="Prompt repeatedly for graph data to create",
|
41
|
+
)
|
42
|
+
|
43
|
+
parser.add_argument(
|
44
|
+
"--log-level",
|
45
|
+
"--ll",
|
46
|
+
default="info",
|
47
|
+
help="Define verbosity level (debug, info, warning, error, fotal, critical).",
|
48
|
+
)
|
49
|
+
|
50
|
+
dissector_add_parseargs(parser)
|
51
|
+
limitor_add_parseargs(parser)
|
52
|
+
|
53
|
+
parser.add_argument("input_file", type=str, help="PCAP file to graph", nargs="+")
|
54
|
+
|
55
|
+
args = parser.parse_args()
|
56
|
+
log_level = args.log_level.upper()
|
57
|
+
logging.basicConfig(level=log_level, format="%(levelname)-10s:\t%(message)s")
|
58
|
+
logging.getLogger("matplotlib.font_manager").setLevel(logging.ERROR)
|
59
|
+
return args
|
60
|
+
|
61
|
+
|
62
|
+
def main():
|
63
|
+
args = parse_args()
|
64
|
+
|
65
|
+
check_dissector_level(args.dissection_level)
|
66
|
+
|
67
|
+
pc = PcapGraph(
|
68
|
+
args.input_file,
|
69
|
+
args.output_file,
|
70
|
+
maximum_count=args.packet_count,
|
71
|
+
minimum_count=args.minimum_count,
|
72
|
+
bin_size=args.bin_size,
|
73
|
+
match_string=args.match_string,
|
74
|
+
match_value=args.match_value,
|
75
|
+
cache_pcap_results=args.cache_pcap_results,
|
76
|
+
dissector_level=args.dissection_level,
|
77
|
+
interactive=args.interactive,
|
78
|
+
by_percentage=args.by_percentage,
|
79
|
+
ignore_list=args.ignore_list,
|
80
|
+
)
|
81
|
+
pc.graph_it()
|
82
|
+
|
83
|
+
|
84
|
+
if __name__ == "__main__":
|
85
|
+
main()
|
@@ -16,8 +16,14 @@ traffic_taffy/output/__init__.py,sha256=0laqOfA1LvqlgL0IVDMp-qnwDIQuPLDvrpT3qV-R
|
|
16
16
|
traffic_taffy/output/console.py,sha256=me-BPAHwgqvl5tS-EvHmEPUEt6ryn0Rcj76lfGJazSc,2315
|
17
17
|
traffic_taffy/output/fsdb.py,sha256=c_XwjPdLcl85MZwBY7VgbPymqDZje8St_HmyJl_9LFU,1449
|
18
18
|
traffic_taffy/output/memory.py,sha256=McYpPbg4kux_U21OabE4lUhrgT60V0eKUiey0Udzc9s,1502
|
19
|
-
traffic_taffy
|
20
|
-
traffic_taffy
|
21
|
-
traffic_taffy
|
22
|
-
traffic_taffy
|
23
|
-
traffic_taffy
|
19
|
+
traffic_taffy/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
20
|
+
traffic_taffy/tools/cache_info.py,sha256=4SZVdcwA_36YLyIYN8F5AbKaM-xCLj8P089wE2fKXCA,1933
|
21
|
+
traffic_taffy/tools/compare.py,sha256=f2A1bDmMZc_TZkAGWCEpcFcQVXkO83aVYpMi013kFbg,3016
|
22
|
+
traffic_taffy/tools/dissect.py,sha256=fVfZ_fHOkiHaa51QF7uhTJlknkphw-AWssLF2zWYWCs,2089
|
23
|
+
traffic_taffy/tools/explore.py,sha256=Y_fjBDwDkghvK_bvrXevKSBc1qqpfQqkuRngr33T7PY,24888
|
24
|
+
traffic_taffy/tools/graph.py,sha256=8o3jxtRDgoXmb9vJXndc4agNzBlYQlNrC3jW5Cn-uAY,2215
|
25
|
+
traffic_taffy-0.4.1.dist-info/METADATA,sha256=BEdOspLVEQkIXc1YObuDSyIq9gnUz3rDgJGIXnH85uU,1014
|
26
|
+
traffic_taffy-0.4.1.dist-info/WHEEL,sha256=pkctZYzUS4AYVn6dJ-7367OJZivF2e8RA9b_ZBjif18,92
|
27
|
+
traffic_taffy-0.4.1.dist-info/entry_points.txt,sha256=pl_Acah6y3vbCuS52Thx21WJ91YMzTs8Cdftz9740dk,267
|
28
|
+
traffic_taffy-0.4.1.dist-info/top_level.txt,sha256=wjeQaxXSKqUzrRtfbUlbSn8SoaL4VC73yudFAEtnIuo,14
|
29
|
+
traffic_taffy-0.4.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|