traffic-taffy 0.6.4__tar.gz → 0.8.1__tar.gz
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-0.6.4 → traffic_taffy-0.8.1}/PKG-INFO +3 -1
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/pyproject.toml +2 -0
- traffic_taffy-0.8.1/traffic_taffy/__init__.py +1 -0
- traffic_taffy-0.8.1/traffic_taffy/algorithms/__init__.py +14 -0
- traffic_taffy-0.8.1/traffic_taffy/algorithms/statistical.py +100 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/compare.py +30 -111
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/dissection.py +13 -6
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/dissectmany.py +11 -1
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/dissector.py +39 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/graph.py +3 -0
- traffic_taffy-0.8.1/traffic_taffy/hooks/__init__.py +38 -0
- traffic_taffy-0.8.1/traffic_taffy/hooks/ip2asn.py +49 -0
- traffic_taffy-0.8.1/traffic_taffy/hooks/psl.py +42 -0
- traffic_taffy-0.8.1/traffic_taffy/report.py +12 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tests/test_compare_results.py +5 -4
- traffic_taffy-0.8.1/traffic_taffy/tests/test_dpkt_engine.py +15 -0
- traffic_taffy-0.8.1/traffic_taffy/tests/test_hooks.py +46 -0
- traffic_taffy-0.8.1/traffic_taffy/tests/test_splitter.py +45 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tests/test_value_printing.py +1 -1
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tools/compare.py +6 -3
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tools/dissect.py +10 -3
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tools/explore.py +3 -2
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tools/export.py +3 -2
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tools/graph.py +3 -2
- traffic_taffy-0.6.4/traffic_taffy/__init__.py +0 -1
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/.gitignore +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/LICENSE.txt +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/README.md +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/comparison.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/dissector_engine/__init__.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/dissector_engine/dnstap.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/dissector_engine/dpkt.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/dissector_engine/scapy.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/graphdata.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/output/__init__.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/output/console.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/output/fsdb.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/output/memory.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tests/test_dict_merge.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tests/test_normalize.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tests/test_pcap_dissector.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tests/test_pcap_splitter.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tools/__init__.py +0 -0
- {traffic_taffy-0.6.4 → traffic_taffy-0.8.1}/traffic_taffy/tools/cache_info.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: traffic-taffy
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.8.1
|
4
4
|
Summary: A tool for doing differential analysis of pcap files
|
5
5
|
Project-URL: Homepage, https://traffic-taffy.github.io/
|
6
6
|
Author-email: Wes Hardaker <opensource@hardakers.net>
|
@@ -9,7 +9,9 @@ Classifier: Operating System :: OS Independent
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
10
10
|
Requires-Python: >=3.7
|
11
11
|
Requires-Dist: cryptography
|
12
|
+
Requires-Dist: dnssplitter
|
12
13
|
Requires-Dist: dpkt
|
14
|
+
Requires-Dist: ip2asn
|
13
15
|
Requires-Dist: msgpack
|
14
16
|
Requires-Dist: pandas
|
15
17
|
Requires-Dist: pcap-parallel
|
@@ -0,0 +1 @@
|
|
1
|
+
__VERSION__ = "0.8.1"
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from typing import TYPE_CHECKING
|
2
|
+
|
3
|
+
if TYPE_CHECKING:
|
4
|
+
pass
|
5
|
+
|
6
|
+
|
7
|
+
class ComparisonAlgorithm:
|
8
|
+
def __init__(self):
|
9
|
+
pass
|
10
|
+
|
11
|
+
def compare_dissections(left_side: dict, right_side: dict) -> dict:
|
12
|
+
raise ValueError(
|
13
|
+
"code failure: base class compare_dissections should never be called"
|
14
|
+
)
|
@@ -0,0 +1,100 @@
|
|
1
|
+
from traffic_taffy.algorithms import ComparisonAlgorithm
|
2
|
+
from traffic_taffy.comparison import Comparison
|
3
|
+
from traffic_taffy.dissection import Dissection
|
4
|
+
from traffic_taffy.report import Report
|
5
|
+
|
6
|
+
|
7
|
+
class ComparisonStatistical(ComparisonAlgorithm):
|
8
|
+
def __init__(self):
|
9
|
+
super().__init__()
|
10
|
+
|
11
|
+
def compare_dissections(self, left_side: dict, right_side: dict) -> Comparison:
|
12
|
+
"""Compare two dissections."""
|
13
|
+
report = {}
|
14
|
+
|
15
|
+
keys = set(left_side.keys())
|
16
|
+
keys = keys.union(right_side.keys())
|
17
|
+
for key in keys:
|
18
|
+
report[key] = {}
|
19
|
+
|
20
|
+
if key not in left_side:
|
21
|
+
left_side[key] = {}
|
22
|
+
left_side_total = sum(left_side[key].values())
|
23
|
+
|
24
|
+
if key not in right_side:
|
25
|
+
right_side[key] = {}
|
26
|
+
right_side_total = sum(right_side[key].values())
|
27
|
+
|
28
|
+
new_left_count = 0
|
29
|
+
for subkey in left_side[key]:
|
30
|
+
delta_percentage = 0.0
|
31
|
+
total = 0
|
32
|
+
if subkey in right_side[key]:
|
33
|
+
left_percentage = left_side[key][subkey] / left_side_total
|
34
|
+
right_percentage = right_side[key][subkey] / right_side_total
|
35
|
+
delta_percentage = right_percentage - left_percentage
|
36
|
+
total = right_side[key][subkey] + left_side[key][subkey]
|
37
|
+
left_count = left_side[key][subkey]
|
38
|
+
right_count = right_side[key][subkey]
|
39
|
+
else:
|
40
|
+
delta_percentage = -1.0
|
41
|
+
left_percentage = left_side[key][subkey] / left_side_total
|
42
|
+
right_percentage = 0.0
|
43
|
+
total = -left_side[key][subkey]
|
44
|
+
left_count = left_side[key][subkey]
|
45
|
+
right_count = 0
|
46
|
+
new_left_count += 1
|
47
|
+
|
48
|
+
delta_absolute = right_count - left_count
|
49
|
+
report[key][subkey] = Report(
|
50
|
+
delta_percentage=delta_percentage,
|
51
|
+
delta_absolute=delta_absolute,
|
52
|
+
total=total,
|
53
|
+
left_count=left_count,
|
54
|
+
right_count=right_count,
|
55
|
+
left_percentage=left_percentage,
|
56
|
+
right_percentage=right_percentage,
|
57
|
+
)
|
58
|
+
|
59
|
+
new_right_count = 0
|
60
|
+
for subkey in right_side[key]:
|
61
|
+
if subkey not in report[key]:
|
62
|
+
delta_percentage = 1.0
|
63
|
+
total = right_side[key][subkey]
|
64
|
+
left_count = 0
|
65
|
+
right_count = right_side[key][subkey]
|
66
|
+
left_percentage = 0.0
|
67
|
+
right_percentage = right_side[key][subkey] / right_side_total
|
68
|
+
new_right_count += 1 # this value wasn't in the left
|
69
|
+
|
70
|
+
report[key][subkey] = Report(
|
71
|
+
delta_percentage=delta_percentage,
|
72
|
+
delta_absolute=right_count,
|
73
|
+
total=total,
|
74
|
+
left_count=left_count,
|
75
|
+
right_count=right_count,
|
76
|
+
left_percentage=left_percentage,
|
77
|
+
right_percentage=right_percentage,
|
78
|
+
)
|
79
|
+
|
80
|
+
if right_side_total == 0:
|
81
|
+
right_percent = 100
|
82
|
+
else:
|
83
|
+
right_percent = new_right_count / right_side_total
|
84
|
+
|
85
|
+
if left_side_total == 0:
|
86
|
+
left_percent = 100
|
87
|
+
else:
|
88
|
+
left_percent = new_left_count / left_side_total
|
89
|
+
|
90
|
+
report[key][Dissection.NEW_RIGHT_SUBKEY] = Report(
|
91
|
+
delta_absolute=new_right_count - new_left_count,
|
92
|
+
total=new_left_count + new_right_count,
|
93
|
+
left_count=new_left_count,
|
94
|
+
right_count=new_right_count,
|
95
|
+
left_percentage=left_percent,
|
96
|
+
right_percentage=right_percent,
|
97
|
+
delta_percentage=right_percent - left_percent,
|
98
|
+
)
|
99
|
+
|
100
|
+
return Comparison(report)
|
@@ -3,8 +3,9 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
from logging import debug, error
|
5
5
|
from typing import List, TYPE_CHECKING
|
6
|
-
import datetime as dt
|
7
6
|
from datetime import datetime
|
7
|
+
import datetime as dt
|
8
|
+
import itertools
|
8
9
|
|
9
10
|
if TYPE_CHECKING:
|
10
11
|
from argparse import ArgumentParser, Namespace
|
@@ -13,19 +14,7 @@ from traffic_taffy.comparison import Comparison
|
|
13
14
|
from traffic_taffy.dissectmany import PCAPDissectMany
|
14
15
|
from traffic_taffy.dissector import PCAPDissectorLevel
|
15
16
|
from traffic_taffy.dissection import Dissection
|
16
|
-
|
17
|
-
from dataclasses import dataclass
|
18
|
-
|
19
|
-
|
20
|
-
@dataclass
|
21
|
-
class Report:
|
22
|
-
delta_percentage: float
|
23
|
-
delta_absolute: int
|
24
|
-
total: int
|
25
|
-
left_count: int
|
26
|
-
right_count: int
|
27
|
-
left_percentage: float
|
28
|
-
right_percentage: float
|
17
|
+
from traffic_taffy.algorithms.statistical import ComparisonStatistical
|
29
18
|
|
30
19
|
|
31
20
|
class PcapCompare:
|
@@ -48,6 +37,7 @@ class PcapCompare:
|
|
48
37
|
layers: List[str] | None = None,
|
49
38
|
force_load: bool = False,
|
50
39
|
force_overwrite: bool = False,
|
40
|
+
merge_files: bool = False,
|
51
41
|
) -> None:
|
52
42
|
"""Create a compare object."""
|
53
43
|
self.pcap_files = pcap_files
|
@@ -63,6 +53,9 @@ class PcapCompare:
|
|
63
53
|
self.layers = layers
|
64
54
|
self.force_overwrite = force_overwrite
|
65
55
|
self.force_load = force_load
|
56
|
+
self.merge_files = merge_files
|
57
|
+
|
58
|
+
self.algorithm = ComparisonStatistical()
|
66
59
|
|
67
60
|
@property
|
68
61
|
def pcap_files(self) -> List[str]:
|
@@ -82,97 +75,6 @@ class PcapCompare:
|
|
82
75
|
def reports(self, newvalue: List[dict]) -> None:
|
83
76
|
self._reports = newvalue
|
84
77
|
|
85
|
-
def compare_dissections(self, left_side: dict, right_side: dict) -> dict:
|
86
|
-
"""Compare two dissections."""
|
87
|
-
report = {}
|
88
|
-
|
89
|
-
keys = set(left_side.keys())
|
90
|
-
keys = keys.union(right_side.keys())
|
91
|
-
for key in keys:
|
92
|
-
report[key] = {}
|
93
|
-
|
94
|
-
if key not in left_side:
|
95
|
-
left_side[key] = {}
|
96
|
-
left_side_total = sum(left_side[key].values())
|
97
|
-
|
98
|
-
if key not in right_side:
|
99
|
-
right_side[key] = {}
|
100
|
-
right_side_total = sum(right_side[key].values())
|
101
|
-
|
102
|
-
new_left_count = 0
|
103
|
-
for subkey in left_side[key]:
|
104
|
-
delta_percentage = 0.0
|
105
|
-
total = 0
|
106
|
-
if subkey in right_side[key]:
|
107
|
-
left_percentage = left_side[key][subkey] / left_side_total
|
108
|
-
right_percentage = right_side[key][subkey] / right_side_total
|
109
|
-
delta_percentage = right_percentage - left_percentage
|
110
|
-
total = right_side[key][subkey] + left_side[key][subkey]
|
111
|
-
left_count = left_side[key][subkey]
|
112
|
-
right_count = right_side[key][subkey]
|
113
|
-
else:
|
114
|
-
delta_percentage = -1.0
|
115
|
-
left_percentage = left_side[key][subkey] / left_side_total
|
116
|
-
right_percentage = 0.0
|
117
|
-
total = -left_side[key][subkey]
|
118
|
-
left_count = left_side[key][subkey]
|
119
|
-
right_count = 0
|
120
|
-
new_left_count += 1
|
121
|
-
|
122
|
-
delta_absolute = right_count - left_count
|
123
|
-
report[key][subkey] = Report(
|
124
|
-
delta_percentage=delta_percentage,
|
125
|
-
delta_absolute=delta_absolute,
|
126
|
-
total=total,
|
127
|
-
left_count=left_count,
|
128
|
-
right_count=right_count,
|
129
|
-
left_percentage=left_percentage,
|
130
|
-
right_percentage=right_percentage,
|
131
|
-
)
|
132
|
-
|
133
|
-
new_right_count = 0
|
134
|
-
for subkey in right_side[key]:
|
135
|
-
if subkey not in report[key]:
|
136
|
-
delta_percentage = 1.0
|
137
|
-
total = right_side[key][subkey]
|
138
|
-
left_count = 0
|
139
|
-
right_count = right_side[key][subkey]
|
140
|
-
left_percentage = 0.0
|
141
|
-
right_percentage = right_side[key][subkey] / right_side_total
|
142
|
-
new_right_count += 1 # this value wasn't in the left
|
143
|
-
|
144
|
-
report[key][subkey] = Report(
|
145
|
-
delta_percentage=delta_percentage,
|
146
|
-
delta_absolute=right_count,
|
147
|
-
total=total,
|
148
|
-
left_count=left_count,
|
149
|
-
right_count=right_count,
|
150
|
-
left_percentage=left_percentage,
|
151
|
-
right_percentage=right_percentage,
|
152
|
-
)
|
153
|
-
|
154
|
-
if right_side_total == 0:
|
155
|
-
right_percent = 100
|
156
|
-
else:
|
157
|
-
right_percent = new_right_count / right_side_total
|
158
|
-
|
159
|
-
if left_side_total == 0:
|
160
|
-
left_percent = 100
|
161
|
-
else:
|
162
|
-
left_percent = new_left_count / left_side_total
|
163
|
-
|
164
|
-
report[key][Dissection.NEW_RIGHT_SUBKEY] = Report(
|
165
|
-
delta_absolute=new_right_count - new_left_count,
|
166
|
-
total=new_left_count + new_right_count,
|
167
|
-
left_count=new_left_count,
|
168
|
-
right_count=new_right_count,
|
169
|
-
left_percentage=left_percent,
|
170
|
-
right_percentage=right_percent,
|
171
|
-
delta_percentage=right_percent - left_percent,
|
172
|
-
)
|
173
|
-
|
174
|
-
return Comparison(report)
|
175
|
-
|
176
78
|
def load_pcaps(self) -> None:
|
177
79
|
"""Load all pcaps into memory and dissect them."""
|
178
80
|
# load the first as a reference pcap
|
@@ -188,6 +90,7 @@ class PcapCompare:
|
|
188
90
|
layers=self.layers,
|
189
91
|
force_load=self.force_load,
|
190
92
|
force_overwrite=self.force_overwrite,
|
93
|
+
merge_files=self.merge_files,
|
191
94
|
)
|
192
95
|
return pdm.load_all()
|
193
96
|
|
@@ -200,19 +103,34 @@ class PcapCompare:
|
|
200
103
|
def compare_all(self, dissections: List[Dissection]) -> List[Comparison]:
|
201
104
|
"""Compare all loaded pcaps."""
|
202
105
|
reports = []
|
203
|
-
|
106
|
+
|
107
|
+
# hack to figure out if there is at least two instances of a generator
|
108
|
+
# without actually extracting them all
|
109
|
+
# (since it could be memory expensive)
|
110
|
+
reference = next(dissections)
|
111
|
+
other = None
|
112
|
+
multiple = True
|
113
|
+
try:
|
114
|
+
other = next(dissections)
|
115
|
+
dissections = itertools.chain([other], dissections)
|
116
|
+
except Exception as e:
|
117
|
+
print(e)
|
118
|
+
multiple = False
|
119
|
+
|
120
|
+
if multiple:
|
204
121
|
# multiple file comparison
|
205
|
-
reference = next(dissections)
|
206
122
|
for other in dissections:
|
207
123
|
# compare the two global summaries
|
208
124
|
|
209
|
-
report = self.compare_dissections(
|
125
|
+
report = self.algorithm.compare_dissections(
|
126
|
+
reference.data[0], other.data[0]
|
127
|
+
)
|
210
128
|
report.title = f"{reference.pcap_file} vs {other.pcap_file}"
|
211
129
|
|
212
130
|
reports.append(report)
|
213
131
|
else:
|
214
132
|
# deal with timestamps within a single file
|
215
|
-
reference =
|
133
|
+
reference = reference.data
|
216
134
|
timestamps = list(reference.keys())
|
217
135
|
if len(timestamps) <= 2: # just 0-summary plus a single stamp
|
218
136
|
error(
|
@@ -240,7 +158,7 @@ class PcapCompare:
|
|
240
158
|
|
241
159
|
debug(f"comparing timestamps {time_left} and {time_right}")
|
242
160
|
|
243
|
-
report = self.compare_dissections(
|
161
|
+
report = self.algorithm.compare_dissections(
|
244
162
|
reference[time_left],
|
245
163
|
reference[time_right],
|
246
164
|
)
|
@@ -293,7 +211,7 @@ def compare_add_parseargs(
|
|
293
211
|
)
|
294
212
|
|
295
213
|
compare_parser.add_argument(
|
296
|
-
"-
|
214
|
+
"-R",
|
297
215
|
"--top-records",
|
298
216
|
default=None,
|
299
217
|
type=int,
|
@@ -338,4 +256,5 @@ def get_comparison_args(args: Namespace) -> dict:
|
|
338
256
|
"top_records": args.top_records,
|
339
257
|
"reverse_sort": args.reverse_sort,
|
340
258
|
"sort_by": args.sort_by,
|
259
|
+
"merge_files": args.merge,
|
341
260
|
}
|
@@ -416,7 +416,9 @@ class Dissection:
|
|
416
416
|
try:
|
417
417
|
if isinstance(value, bytes):
|
418
418
|
if value_type in Dissection.DISPLAY_TRANSFORMERS:
|
419
|
-
value = str(
|
419
|
+
value = str(
|
420
|
+
Dissection.DISPLAY_TRANSFORMERS[value_type](value_type, value)
|
421
|
+
)
|
420
422
|
else:
|
421
423
|
value = "0x" + value.hex()
|
422
424
|
else:
|
@@ -432,7 +434,7 @@ class Dissection:
|
|
432
434
|
return value
|
433
435
|
|
434
436
|
@staticmethod
|
435
|
-
def print_mac_address(value: bytes) -> str:
|
437
|
+
def print_mac_address(value_type: str, value: bytes) -> str:
|
436
438
|
"""Convert bytes to ethernet mac style address."""
|
437
439
|
|
438
440
|
# TODO(hardaker): certainly inefficient
|
@@ -441,12 +443,17 @@ class Dissection:
|
|
441
443
|
|
442
444
|
return ":".join(map(two_hex, value))
|
443
445
|
|
446
|
+
@staticmethod
|
447
|
+
def print_ip_address(value_type: str, value: bytes) -> str:
|
448
|
+
"""Convert binary bytes to IP addresses (v4 and v6)."""
|
449
|
+
return ipaddress.ip_address(value)
|
450
|
+
|
444
451
|
# has to go at the end to pick up the above function names
|
445
452
|
DISPLAY_TRANSFORMERS: ClassVar[Dict[str, callable]] = {
|
446
|
-
"Ethernet_IP_src":
|
447
|
-
"Ethernet_IP_dst":
|
448
|
-
"Ethernet_IP6_src":
|
449
|
-
"Ethernet_IP6_dst":
|
453
|
+
"Ethernet_IP_src": print_ip_address,
|
454
|
+
"Ethernet_IP_dst": print_ip_address,
|
455
|
+
"Ethernet_IP6_src": print_ip_address,
|
456
|
+
"Ethernet_IP6_dst": print_ip_address,
|
450
457
|
"Ethernet_src": print_mac_address,
|
451
458
|
"Ethernet_dst": print_mac_address,
|
452
459
|
}
|
@@ -120,6 +120,16 @@ class PCAPDissectMany:
|
|
120
120
|
# use all available resources
|
121
121
|
with ProcessPoolExecutor() as executor:
|
122
122
|
dissections = executor.map(self.load_pcap, self.pcap_files)
|
123
|
-
|
123
|
+
|
124
|
+
# all loaded files should be merged as if they are one
|
125
|
+
if self.kwargs["merge_files"]:
|
126
|
+
dissection = next(dissections)
|
127
|
+
for to_be_merged in dissections:
|
128
|
+
dissection.merge(to_be_merged)
|
129
|
+
|
130
|
+
dissections = [dissection]
|
131
|
+
|
132
|
+
elif return_as_list: # convert from generator
|
124
133
|
dissections = list(dissections)
|
134
|
+
|
125
135
|
return dissections
|
@@ -6,10 +6,14 @@ import sys
|
|
6
6
|
from collections import Counter, defaultdict
|
7
7
|
from logging import error, warning
|
8
8
|
from typing import List
|
9
|
+
import importlib
|
9
10
|
|
10
11
|
from rich import print
|
11
12
|
|
12
13
|
from traffic_taffy.dissection import Dissection, PCAPDissectorLevel
|
14
|
+
from traffic_taffy.hooks import call_hooks
|
15
|
+
|
16
|
+
POST_DISSECT_HOOK: str = "post_dissect"
|
13
17
|
|
14
18
|
|
15
19
|
class PCAPDissector:
|
@@ -28,6 +32,7 @@ class PCAPDissector:
|
|
28
32
|
layers: List[str] | None = None,
|
29
33
|
force_overwrite: bool = False,
|
30
34
|
force_load: bool = False,
|
35
|
+
merge_files: bool = False, # Note: unused for a single load
|
31
36
|
) -> None:
|
32
37
|
"""Create a dissector object."""
|
33
38
|
if ignore_list is None:
|
@@ -119,6 +124,8 @@ class PCAPDissector:
|
|
119
124
|
engine = DissectionEngineDpkt(*args)
|
120
125
|
|
121
126
|
self.dissection = engine.load()
|
127
|
+
call_hooks(POST_DISSECT_HOOK, dissection=self.dissection)
|
128
|
+
|
122
129
|
if self.cache_results:
|
123
130
|
self.dissection.save_to_cache()
|
124
131
|
return self.dissection
|
@@ -256,6 +263,22 @@ def dissector_add_parseargs(parser, add_subgroup: bool = True):
|
|
256
263
|
help="List of extra layers to load (eg: tls, http, etc)",
|
257
264
|
)
|
258
265
|
|
266
|
+
parser.add_argument(
|
267
|
+
"-x",
|
268
|
+
"--modules",
|
269
|
+
default=None,
|
270
|
+
type=str,
|
271
|
+
nargs="*",
|
272
|
+
help="Extra processing modules to load (currently: psl) ",
|
273
|
+
)
|
274
|
+
|
275
|
+
parser.add_argument(
|
276
|
+
"--merge",
|
277
|
+
"--merge-files",
|
278
|
+
action="store_true",
|
279
|
+
help="Dissect multiple files as one. (compare by time)",
|
280
|
+
)
|
281
|
+
|
259
282
|
parser.add_argument(
|
260
283
|
"-C",
|
261
284
|
"--cache-pcap-results",
|
@@ -318,6 +341,22 @@ def limitor_add_parseargs(parser, add_subgroup: bool = True):
|
|
318
341
|
return parser
|
319
342
|
|
320
343
|
|
344
|
+
def dissector_handle_arguments(args) -> None:
|
345
|
+
check_dissector_level(args.dissection_level)
|
346
|
+
dissector_load_extra_modules(args.modules)
|
347
|
+
|
348
|
+
|
349
|
+
def dissector_load_extra_modules(modules: List[str]) -> None:
|
350
|
+
"""Loads extra modules"""
|
351
|
+
if not modules:
|
352
|
+
return
|
353
|
+
for module in modules:
|
354
|
+
try:
|
355
|
+
importlib.import_module(f"traffic_taffy.hooks.{module}")
|
356
|
+
except Exception as exp:
|
357
|
+
error(f"failed to load module {module}: {exp}")
|
358
|
+
|
359
|
+
|
321
360
|
def check_dissector_level(level: int):
|
322
361
|
"""Check that the dissector level is legal."""
|
323
362
|
current_dissection_levels = [
|
@@ -34,6 +34,7 @@ class PcapGraph(PcapGraphData):
|
|
34
34
|
layers: List[str] | None = None,
|
35
35
|
force_overwrite: bool = False,
|
36
36
|
force_load: bool = False,
|
37
|
+
merge_files: bool = False, # unused
|
37
38
|
):
|
38
39
|
"""Create an instance of a graphing object."""
|
39
40
|
self.pcap_files = pcap_files
|
@@ -55,6 +56,7 @@ class PcapGraph(PcapGraphData):
|
|
55
56
|
self.layers = layers
|
56
57
|
self.force_overwrite = force_overwrite
|
57
58
|
self.force_load = force_load
|
59
|
+
self.merge_files = merge_files
|
58
60
|
|
59
61
|
super().__init__()
|
60
62
|
|
@@ -75,6 +77,7 @@ class PcapGraph(PcapGraphData):
|
|
75
77
|
layers=self.layers,
|
76
78
|
force_overwrite=self.force_overwrite,
|
77
79
|
force_load=self.force_load,
|
80
|
+
merge_files=self.merge_files,
|
78
81
|
)
|
79
82
|
self.dissections = pdm.load_all()
|
80
83
|
info("done reading pcap files")
|
@@ -0,0 +1,38 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
|
3
|
+
# __path__ = extend_path(__path__, __name__)
|
4
|
+
|
5
|
+
from functools import wraps
|
6
|
+
|
7
|
+
hooks = defaultdict(list)
|
8
|
+
|
9
|
+
|
10
|
+
def register_hook(hook):
|
11
|
+
def decorator(function):
|
12
|
+
hooks[hook].append(function)
|
13
|
+
|
14
|
+
@wraps(function)
|
15
|
+
def _wrap(*args, **kwargs):
|
16
|
+
return function(*args, **kwargs)
|
17
|
+
|
18
|
+
return _wrap
|
19
|
+
|
20
|
+
return decorator
|
21
|
+
|
22
|
+
|
23
|
+
def call_hooks(spot, *args, **kwargs):
|
24
|
+
for hook in hooks[spot]:
|
25
|
+
hook(*args, **kwargs)
|
26
|
+
|
27
|
+
|
28
|
+
def main():
|
29
|
+
@register_hook("hookspot")
|
30
|
+
def test_hook():
|
31
|
+
print("world!!!")
|
32
|
+
|
33
|
+
print("hello")
|
34
|
+
call_hooks("hookspot")
|
35
|
+
|
36
|
+
|
37
|
+
if __name__ == "__main__":
|
38
|
+
main()
|
@@ -0,0 +1,49 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from logging import error, info, debug
|
3
|
+
import ip2asn
|
4
|
+
|
5
|
+
from traffic_taffy.hooks import register_hook
|
6
|
+
from traffic_taffy.dissector import POST_DISSECT_HOOK
|
7
|
+
from traffic_taffy.dissection import Dissection
|
8
|
+
|
9
|
+
if not Path("ip2asn-combined.tsv").exists():
|
10
|
+
error("The ip2asn plugin requires a ip2asn-combined.tsv in this directory")
|
11
|
+
error("Please download it from https://iptoasn.com/")
|
12
|
+
|
13
|
+
info("loading ip2asn-combined.tsv")
|
14
|
+
i2a = ip2asn.IP2ASN("ip2asn-combined.tsv")
|
15
|
+
info(" ... loaded")
|
16
|
+
|
17
|
+
|
18
|
+
@register_hook(POST_DISSECT_HOOK)
|
19
|
+
def ip_to_asn(dissection: Dissection, **kwargs):
|
20
|
+
timestamps = dissection.data.keys()
|
21
|
+
|
22
|
+
for timestamp in timestamps:
|
23
|
+
keys = list(dissection.data[timestamp].keys())
|
24
|
+
|
25
|
+
for key in keys:
|
26
|
+
key = str(key)
|
27
|
+
if (
|
28
|
+
key.endswith("IP_src")
|
29
|
+
or key.endswith("IP_dst")
|
30
|
+
or key.endswith("IPv6_src")
|
31
|
+
or key.endswith("IPv6_dst")
|
32
|
+
):
|
33
|
+
for value in dissection.data[timestamp][key]:
|
34
|
+
count = dissection.data[timestamp][key][value]
|
35
|
+
details = None
|
36
|
+
try:
|
37
|
+
details = i2a.lookup_address(value)
|
38
|
+
except Exception:
|
39
|
+
debug("failed to parse address: {value}")
|
40
|
+
if not details:
|
41
|
+
continue
|
42
|
+
|
43
|
+
dissection.data[timestamp][key + "_ASN"][details["ASN"]] += count
|
44
|
+
dissection.data[timestamp][key + "_country"][
|
45
|
+
details["country"]
|
46
|
+
] += count
|
47
|
+
dissection.data[timestamp][key + "_owner"][
|
48
|
+
details["owner"]
|
49
|
+
] += count
|
@@ -0,0 +1,42 @@
|
|
1
|
+
from traffic_taffy.hooks import register_hook
|
2
|
+
from traffic_taffy.dissector import POST_DISSECT_HOOK
|
3
|
+
from traffic_taffy.dissection import Dissection
|
4
|
+
|
5
|
+
import dnssplitter
|
6
|
+
|
7
|
+
splitter = dnssplitter.DNSSplitter()
|
8
|
+
splitter.init_tree()
|
9
|
+
|
10
|
+
|
11
|
+
@register_hook(POST_DISSECT_HOOK)
|
12
|
+
def split_dns_names(dissection: Dissection, **kwargs):
|
13
|
+
timestamps = dissection.data.keys()
|
14
|
+
|
15
|
+
for timestamp in timestamps:
|
16
|
+
keys = list(dissection.data[timestamp].keys())
|
17
|
+
|
18
|
+
for key in keys:
|
19
|
+
key = str(key)
|
20
|
+
if (
|
21
|
+
key.endswith("_qname")
|
22
|
+
or key.endswith("_mname")
|
23
|
+
or key.endswith("_rrname")
|
24
|
+
):
|
25
|
+
for value in dissection.data[timestamp][key]:
|
26
|
+
count = dissection.data[timestamp][key][value]
|
27
|
+
results = splitter.search_tree(value)
|
28
|
+
if not results or not results[2]:
|
29
|
+
continue
|
30
|
+
(
|
31
|
+
prefix,
|
32
|
+
registered_domain,
|
33
|
+
registration_point,
|
34
|
+
) = results
|
35
|
+
if registration_point:
|
36
|
+
dissection.data[timestamp][key + "_prefix"][prefix] += count
|
37
|
+
dissection.data[timestamp][key + "_domain"][
|
38
|
+
registered_domain
|
39
|
+
] += count
|
40
|
+
dissection.data[timestamp][key + "_psl"][
|
41
|
+
registration_point
|
42
|
+
] += count
|
@@ -1,8 +1,9 @@
|
|
1
1
|
from collections import Counter
|
2
|
-
from traffic_taffy.
|
2
|
+
from traffic_taffy.report import Report
|
3
|
+
from traffic_taffy.algorithms.statistical import ComparisonStatistical
|
3
4
|
|
4
5
|
|
5
|
-
def
|
6
|
+
def test_compare_statistical_algorithm():
|
6
7
|
left_data = {0: {"src": Counter({"a": 5, "b": 10})}} # total = 15
|
7
8
|
right_data = {0: {"src": Counter({"a": 15, "c": 15})}} # total = 30
|
8
9
|
|
@@ -48,7 +49,7 @@ def test_compare_results():
|
|
48
49
|
}
|
49
50
|
}
|
50
51
|
|
51
|
-
|
52
|
-
report =
|
52
|
+
algorithm = ComparisonStatistical() # bogus file names
|
53
|
+
report = algorithm.compare_dissections(left_data[0], right_data[0])
|
53
54
|
|
54
55
|
assert report.contents == expected
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import os
|
2
|
+
from traffic_taffy.dissection import PCAPDissectorLevel
|
3
|
+
from traffic_taffy.dissector_engine.dpkt import DissectionEngineDpkt
|
4
|
+
|
5
|
+
def test_dpkt_engine():
|
6
|
+
test_pcap = "dns.pcap"
|
7
|
+
test_pcap = "port53-2023-30-31_20.pcap"
|
8
|
+
test_pcap = "airplane-wireless.pcap"
|
9
|
+
if not os.path.exists(test_pcap):
|
10
|
+
return
|
11
|
+
|
12
|
+
engine = DissectionEngineDpkt(test_pcap,
|
13
|
+
dissector_level = PCAPDissectorLevel.COMMON_LAYERS)
|
14
|
+
dissection = engine.load()
|
15
|
+
|
@@ -0,0 +1,46 @@
|
|
1
|
+
from traffic_taffy.hooks import register_hook, call_hooks
|
2
|
+
|
3
|
+
into_hook = 1
|
4
|
+
|
5
|
+
|
6
|
+
def test_register_and_call_hook():
|
7
|
+
@register_hook("testhook")
|
8
|
+
def hook_callback():
|
9
|
+
global into_hook
|
10
|
+
into_hook += 1
|
11
|
+
|
12
|
+
call_hooks("testhookDNE")
|
13
|
+
assert into_hook == 1
|
14
|
+
|
15
|
+
call_hooks("testhook")
|
16
|
+
assert into_hook == 2
|
17
|
+
|
18
|
+
|
19
|
+
def test_register_and_call_hook_with_args():
|
20
|
+
@register_hook("testhook_storage")
|
21
|
+
def hook_callback(storage, key, value):
|
22
|
+
storage[key] = value
|
23
|
+
|
24
|
+
the_storage = {}
|
25
|
+
|
26
|
+
call_hooks("testhook_storage", the_storage, "testkey", "testvalue")
|
27
|
+
assert the_storage == {"testkey": "testvalue"}
|
28
|
+
|
29
|
+
call_hooks("testhook_storage", the_storage, "otherkey", 4)
|
30
|
+
assert the_storage == {"testkey": "testvalue", "otherkey": 4}
|
31
|
+
|
32
|
+
|
33
|
+
def test_register_and_call_hook_with_kwargs():
|
34
|
+
@register_hook("testhook_storage")
|
35
|
+
def hook_callback(storage={}, key=None, value=None):
|
36
|
+
storage[key] = value
|
37
|
+
|
38
|
+
the_storage = {}
|
39
|
+
|
40
|
+
call_hooks(
|
41
|
+
"testhook_storage", storage=the_storage, key="testkey", value="testvalue"
|
42
|
+
)
|
43
|
+
assert the_storage == {"testkey": "testvalue"}
|
44
|
+
|
45
|
+
call_hooks("testhook_storage", key="otherkey", value=4, storage=the_storage)
|
46
|
+
assert the_storage == {"testkey": "testvalue", "otherkey": 4}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
from traffic_taffy.dissection import Dissection
|
2
|
+
from traffic_taffy.dissector import POST_DISSECT_HOOK
|
3
|
+
from traffic_taffy.hooks import call_hooks
|
4
|
+
import traffic_taffy.hooks.psl
|
5
|
+
|
6
|
+
|
7
|
+
def test_splitter_module():
|
8
|
+
dissection = Dissection("bogus")
|
9
|
+
dissection.incr("foo", "bar")
|
10
|
+
dissection.incr("foo_qname", "www.example.com")
|
11
|
+
dissection.incr("foo_qname", "www.example.net")
|
12
|
+
dissection.incr("foo_mname", "www.example.co.uk")
|
13
|
+
dissection.incr("foo_qname", "bogus.__doesntexist")
|
14
|
+
|
15
|
+
# bogus to avoid ruff removing the import
|
16
|
+
traffic_taffy.hooks.psl.splitter = traffic_taffy.hooks.psl.splitter
|
17
|
+
|
18
|
+
assert dissection.data[0] == {
|
19
|
+
"foo": {"bar": 1},
|
20
|
+
"foo_qname": {
|
21
|
+
"www.example.com": 1,
|
22
|
+
"www.example.net": 1,
|
23
|
+
"bogus.__doesntexist": 1,
|
24
|
+
},
|
25
|
+
"foo_mname": {"www.example.co.uk": 1},
|
26
|
+
}
|
27
|
+
|
28
|
+
call_hooks(POST_DISSECT_HOOK, dissection)
|
29
|
+
|
30
|
+
test_result = {
|
31
|
+
"foo": {"bar": 1},
|
32
|
+
"foo_mname": {"www.example.co.uk": 1},
|
33
|
+
"foo_mname_prefix": {"www": 1},
|
34
|
+
"foo_mname_domain": {"example.co.uk": 1},
|
35
|
+
"foo_mname_psl": {"co.uk": 1},
|
36
|
+
"foo_qname": {
|
37
|
+
"www.example.com": 1,
|
38
|
+
"www.example.net": 1,
|
39
|
+
"bogus.__doesntexist": 1,
|
40
|
+
},
|
41
|
+
"foo_qname_prefix": {"www": 2},
|
42
|
+
"foo_qname_domain": {"example.com": 1, "example.net": 1},
|
43
|
+
"foo_qname_psl": {"com": 1, "net": 1},
|
44
|
+
}
|
45
|
+
assert dissection.data[0] == test_result
|
@@ -3,7 +3,7 @@ from traffic_taffy.dissection import Dissection
|
|
3
3
|
|
4
4
|
def test_printable():
|
5
5
|
assert (
|
6
|
-
Dissection.make_printable("
|
6
|
+
Dissection.make_printable("Ethernet_IP_dst", b"\x7f\x00\x00\x01") == "127.0.0.1"
|
7
7
|
)
|
8
8
|
|
9
9
|
assert Dissection.make_printable("badtype", b"\x7f\x00\x00\x01") == "0x7f000001"
|
@@ -3,6 +3,7 @@
|
|
3
3
|
import sys
|
4
4
|
from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, Namespace
|
5
5
|
import logging
|
6
|
+
from logging import error
|
6
7
|
from traffic_taffy.output.console import Console
|
7
8
|
from traffic_taffy.output.fsdb import Fsdb
|
8
9
|
|
@@ -10,7 +11,7 @@ from traffic_taffy.compare import compare_add_parseargs, get_comparison_args
|
|
10
11
|
from traffic_taffy.dissector import (
|
11
12
|
dissector_add_parseargs,
|
12
13
|
limitor_add_parseargs,
|
13
|
-
|
14
|
+
dissector_handle_arguments,
|
14
15
|
)
|
15
16
|
from traffic_taffy.compare import PcapCompare
|
16
17
|
|
@@ -50,7 +51,7 @@ def parse_args() -> Namespace:
|
|
50
51
|
log_level = args.log_level.upper()
|
51
52
|
logging.basicConfig(level=log_level, format="%(levelname)-10s:\t%(message)s")
|
52
53
|
|
53
|
-
|
54
|
+
dissector_handle_arguments(args)
|
54
55
|
|
55
56
|
return args
|
56
57
|
|
@@ -89,12 +90,14 @@ def main() -> None:
|
|
89
90
|
layers=args.layers,
|
90
91
|
force_load=args.force_load,
|
91
92
|
force_overwrite=args.force_overwrite,
|
93
|
+
merge_files=args.merge,
|
92
94
|
)
|
93
95
|
|
94
96
|
# compare the pcaps
|
95
97
|
try:
|
96
98
|
reports = pc.compare()
|
97
|
-
except ValueError:
|
99
|
+
except ValueError as e:
|
100
|
+
error(e)
|
98
101
|
sys.exit()
|
99
102
|
|
100
103
|
if args.fsdb:
|
@@ -1,9 +1,11 @@
|
|
1
1
|
"""Performs generic dissection of a PCAP file."""
|
2
|
+
import sys
|
2
3
|
import logging
|
4
|
+
from logging import error
|
3
5
|
from traffic_taffy.dissector import (
|
4
6
|
dissector_add_parseargs,
|
5
7
|
limitor_add_parseargs,
|
6
|
-
|
8
|
+
dissector_handle_arguments,
|
7
9
|
PCAPDissector,
|
8
10
|
)
|
9
11
|
from traffic_taffy.dissectmany import PCAPDissectMany
|
@@ -53,7 +55,7 @@ def main() -> None:
|
|
53
55
|
|
54
56
|
args = parse_args()
|
55
57
|
|
56
|
-
|
58
|
+
dissector_handle_arguments(args)
|
57
59
|
|
58
60
|
# load all the files
|
59
61
|
pdm = PCAPDissectMany(
|
@@ -68,8 +70,13 @@ def main() -> None:
|
|
68
70
|
layers=args.layers,
|
69
71
|
force_overwrite=args.force_overwrite,
|
70
72
|
force_load=args.force_load,
|
73
|
+
merge_files=args.merge,
|
71
74
|
)
|
72
|
-
|
75
|
+
try:
|
76
|
+
dissections = pdm.load_all(return_as_list=True, dont_fork=args.dont_fork)
|
77
|
+
except ValueError as e:
|
78
|
+
error(e)
|
79
|
+
sys.exit()
|
73
80
|
|
74
81
|
# merge them into a single dissection
|
75
82
|
dissection = dissections.pop(0)
|
@@ -10,7 +10,7 @@ from argparse import ArgumentParser, ArgumentDefaultsHelpFormatter, Namespace
|
|
10
10
|
from traffic_taffy.dissector import (
|
11
11
|
dissector_add_parseargs,
|
12
12
|
limitor_add_parseargs,
|
13
|
-
|
13
|
+
dissector_handle_arguments,
|
14
14
|
)
|
15
15
|
from traffic_taffy.dissection import Dissection
|
16
16
|
from traffic_taffy.graphdata import PcapGraphData
|
@@ -180,6 +180,7 @@ class TaffyExplorer(QDialog, PcapGraphData):
|
|
180
180
|
layers=self.args.layers,
|
181
181
|
force_load=self.args.force_load,
|
182
182
|
force_overwrite=self.args.force_overwrite,
|
183
|
+
merge_files=self.args.merge,
|
183
184
|
)
|
184
185
|
|
185
186
|
# create the graph data storage
|
@@ -638,7 +639,7 @@ def parse_args() -> Namespace:
|
|
638
639
|
log_level = args.log_level.upper()
|
639
640
|
logging.basicConfig(level=log_level, format="%(levelname)-10s:\t%(message)s")
|
640
641
|
|
641
|
-
|
642
|
+
dissector_handle_arguments(args)
|
642
643
|
|
643
644
|
return args
|
644
645
|
|
@@ -8,9 +8,9 @@ import pyfsdb
|
|
8
8
|
|
9
9
|
from traffic_taffy.dissectmany import PCAPDissectMany
|
10
10
|
from traffic_taffy.dissector import (
|
11
|
-
check_dissector_level,
|
12
11
|
dissector_add_parseargs,
|
13
12
|
limitor_add_parseargs,
|
13
|
+
dissector_handle_arguments,
|
14
14
|
)
|
15
15
|
|
16
16
|
|
@@ -52,7 +52,7 @@ def main() -> None:
|
|
52
52
|
"""Export traffic-taffy data into an FSDB file."""
|
53
53
|
args = parse_args()
|
54
54
|
|
55
|
-
|
55
|
+
dissector_handle_arguments(args)
|
56
56
|
|
57
57
|
pdm = PCAPDissectMany(
|
58
58
|
args.input_pcaps,
|
@@ -66,6 +66,7 @@ def main() -> None:
|
|
66
66
|
layers=args.layers,
|
67
67
|
force_load=args.force_load,
|
68
68
|
force_overwrite=args.force_overwrite,
|
69
|
+
merge_files=args.merge,
|
69
70
|
)
|
70
71
|
|
71
72
|
dissections = pdm.load_all(return_as_list=True)
|
@@ -5,7 +5,7 @@ from traffic_taffy.graph import PcapGraph
|
|
5
5
|
from traffic_taffy.dissector import (
|
6
6
|
dissector_add_parseargs,
|
7
7
|
limitor_add_parseargs,
|
8
|
-
|
8
|
+
dissector_handle_arguments,
|
9
9
|
)
|
10
10
|
import logging
|
11
11
|
|
@@ -63,7 +63,7 @@ def main() -> None:
|
|
63
63
|
"""Run taffy-graph."""
|
64
64
|
args = parse_args()
|
65
65
|
|
66
|
-
|
66
|
+
dissector_handle_arguments(args)
|
67
67
|
|
68
68
|
pc = PcapGraph(
|
69
69
|
args.input_pcaps,
|
@@ -83,6 +83,7 @@ def main() -> None:
|
|
83
83
|
layers=args.layers,
|
84
84
|
force_overwrite=args.force_overwrite,
|
85
85
|
force_load=args.force_load,
|
86
|
+
merge_files=args.merge,
|
86
87
|
)
|
87
88
|
pc.graph_it()
|
88
89
|
|
@@ -1 +0,0 @@
|
|
1
|
-
__VERSION__ = "0.6.4"
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|