traffic-taffy 0.6.4__py3-none-any.whl → 0.8.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/__init__.py CHANGED
@@ -1 +1 @@
1
- __VERSION__ = "0.6.4"
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)
traffic_taffy/compare.py CHANGED
@@ -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
- if len(self.pcap_files) > 1:
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(reference.data[0], other.data[0])
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 = list(dissections)[0].data
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
- "-x",
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(Dissection.DISPLAY_TRANSFORMERS[value_type](value))
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": ipaddress.ip_address,
447
- "Ethernet_IP_dst": ipaddress.ip_address,
448
- "Ethernet_IP6_src": ipaddress.ip_address,
449
- "Ethernet_IP6_dst": ipaddress.ip_address,
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
- if return_as_list: # convert from generator
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 = [
traffic_taffy/graph.py CHANGED
@@ -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
@@ -0,0 +1,12 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Report:
6
+ delta_percentage: float
7
+ delta_absolute: int
8
+ total: int
9
+ left_count: int
10
+ right_count: int
11
+ left_percentage: float
12
+ right_percentage: float
@@ -1,8 +1,9 @@
1
1
  from collections import Counter
2
- from traffic_taffy.compare import PcapCompare, Report
2
+ from traffic_taffy.report import Report
3
+ from traffic_taffy.algorithms.statistical import ComparisonStatistical
3
4
 
4
5
 
5
- def test_compare_results():
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
- pc = PcapCompare([1, 2]) # bogus file names
52
- report = pc.compare_dissections(left_data[0], right_data[0])
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("Ethernet.IP.dst", b"\x7f\x00\x00\x01") == "127.0.0.1"
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
- check_dissector_level,
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
- check_dissector_level(args.dissection_level)
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
- check_dissector_level,
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
- check_dissector_level(args.dissection_level)
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
- dissections = pdm.load_all(return_as_list=True, dont_fork=args.dont_fork)
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
- check_dissector_level,
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
- check_dissector_level(args.dissection_level)
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
- check_dissector_level(args.dissection_level)
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
- check_dissector_level,
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
- check_dissector_level(args.dissection_level)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: traffic-taffy
3
- Version: 0.6.4
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,43 @@
1
+ traffic_taffy/__init__.py,sha256=cGUURt3LgcflHlV_5CG6SItz7kLJVNNBZFU8RU7RSJY,22
2
+ traffic_taffy/compare.py,sha256=4T25Ygp6rQw7J3syn4Tam6tEw3ieSzl-lELPARvNnVw,8641
3
+ traffic_taffy/comparison.py,sha256=MmCxK7E3RbVLS8gJ_JJOdqa2MHQ7np1Pq_VB0Scir8U,933
4
+ traffic_taffy/dissection.py,sha256=-jXt4PYCi_lvPh86KC_84VywRZqhZ1cGgLdV2glCQC8,17390
5
+ traffic_taffy/dissectmany.py,sha256=kJO2XF8QzOmLDsKDjZIkQBmxC97wFpi-IisCURLOmfU,4700
6
+ traffic_taffy/dissector.py,sha256=JHewf3vhvYjE6CGuRzO6_SnR5QuHZjzx-hZjpkAPHl4,11685
7
+ traffic_taffy/graph.py,sha256=lH20RjnY3rMU8g2zyOkpS5pfvgCcYa4GCtO22uArzs8,4510
8
+ traffic_taffy/graphdata.py,sha256=lHpLuzzypEPuz3QPfkvs0IAbplTLzKcqHaLyoRidmiE,2971
9
+ traffic_taffy/report.py,sha256=Yzb27hUWcWL-RxWpSQmRyM8NyWxQGT0l0jUCGHoYDSY,224
10
+ traffic_taffy/algorithms/__init__.py,sha256=wfzoujmw_Huwi69UnDgAcBQyXeKqz2tPW4RFSqTr3iU,318
11
+ traffic_taffy/algorithms/statistical.py,sha256=J9eI2a4S3GXezWySjjXZQ_sxNEnSc6ePiDSL3cp-UjQ,4023
12
+ traffic_taffy/dissector_engine/__init__.py,sha256=Hu-UQtz7yhivmQLUP5b8tFQLEhy2bfvrRV3Q4aZp6vg,2202
13
+ traffic_taffy/dissector_engine/dnstap.py,sha256=rBzVlB0D3YVhHOsr17cbnCIZU13g20srgR4sE7ZfNUE,4810
14
+ traffic_taffy/dissector_engine/dpkt.py,sha256=YgFceo_6cy1VN-ODIijSsOfH3w8OzHPbpUS463is3YI,10949
15
+ traffic_taffy/dissector_engine/scapy.py,sha256=5lE6y4wzULfULTFcTAo6qINazqoBVwzbRzP0alP4ATc,3730
16
+ traffic_taffy/hooks/__init__.py,sha256=Bvhl6RnyBqQkWuCU6TS0O_ZHe4qCQsC4HE8FELigWPw,661
17
+ traffic_taffy/hooks/ip2asn.py,sha256=8KyqwKTY3YK9GZEDW_3PM_6zuvrzkDAzIceIodNAepY,1765
18
+ traffic_taffy/hooks/psl.py,sha256=6Xmbl2OigDIe17ChumU35iuE0B6Q7ttBp2iBruxWFg4,1529
19
+ traffic_taffy/output/__init__.py,sha256=dSpW9xDno3nKaYOwThS2fVkcIHJVAJE9DlEiLfdHXfg,4555
20
+ traffic_taffy/output/console.py,sha256=Ziq4MKtzSdP9oVaZrWHiw0Bpsm2Dj9QFLJtDK38mxaY,2911
21
+ traffic_taffy/output/fsdb.py,sha256=po--ldHeRPTXVTkP68qtI_BYV2cEOab-kFVWv0ymj5M,1704
22
+ traffic_taffy/output/memory.py,sha256=86tgJ-jMt3UVX31eP6U02YbbYRoqbYhhR4kXJQmYzO4,1870
23
+ traffic_taffy/tests/test_compare_results.py,sha256=LRAAsel2vwSLZmSGxzdGurga77dhBsLkTCMRjybb10A,1921
24
+ traffic_taffy/tests/test_dict_merge.py,sha256=t3rZSQQ0AlBxRKfLborx9SxYN53cCAQQzZ2w-__WT2Y,1429
25
+ traffic_taffy/tests/test_dpkt_engine.py,sha256=512Wfq7D1qVkfhGwf1u2QSgZooWqZQWV9L4OhpAr4AE,489
26
+ traffic_taffy/tests/test_hooks.py,sha256=amjEbtMwOZZCg_RCJ0wQR7aOqNfwz3IG3WY-9CwjSF4,1260
27
+ traffic_taffy/tests/test_normalize.py,sha256=k5y3XmYitnF1aTkB-9dZ7WZPQB__l_iEzC6atKKywfw,2601
28
+ traffic_taffy/tests/test_pcap_dissector.py,sha256=sxJJ3seO9pjzilM0b6iu_ggQbUI4E7WbMFdwm4m9Gz0,2027
29
+ traffic_taffy/tests/test_pcap_splitter.py,sha256=Pjv2CS_WXqK1wQ1rdsxLU-M1Z5fuHsciEhiDJpujFnI,2568
30
+ traffic_taffy/tests/test_splitter.py,sha256=bSb84QOCxRuRXy4BGLFDs5YYRBDgZrx9InhtE3DZsFg,1481
31
+ traffic_taffy/tests/test_value_printing.py,sha256=rhmCUqnh1Lk1TTZvZi7ksvUWm4XDB4g2aUihjLpoqHI,266
32
+ traffic_taffy/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ traffic_taffy/tools/cache_info.py,sha256=N4L-xjO3i-p3QD5DIytSt3GBnT4JsJh3V2dl5Tktumk,2084
34
+ traffic_taffy/tools/compare.py,sha256=BkcWza0lN6KeBb_8oyr88eh0WoVqp5LM3yK8nRY16bc,3476
35
+ traffic_taffy/tools/dissect.py,sha256=Z_5JJ92E4168_GZM_9Iu-m6lDfETBt8Gv-vD_Mu3Cfg,3559
36
+ traffic_taffy/tools/explore.py,sha256=Hb4x9HZkFoYnR8BJKF5OLwZWKGR3RX3MdZhX4Oo-NDU,24180
37
+ traffic_taffy/tools/export.py,sha256=BuAnZcOszj9ZpMxHmRjDqptR15dMRigOhp2-BFJHgWA,2707
38
+ traffic_taffy/tools/graph.py,sha256=gARv5-7N5MUBSJJ8Uj5XLx4xEonXgIMADKwN573jxyk,2555
39
+ traffic_taffy-0.8.1.dist-info/METADATA,sha256=MSgtWi6_e2R7wxBETCrzuPu_hI5QMXJa_n5ATjmJR6c,1933
40
+ traffic_taffy-0.8.1.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
41
+ traffic_taffy-0.8.1.dist-info/entry_points.txt,sha256=ySz30b1Cu03CtCGMRqqg0NZJpzrVI91T5HjbEaTNnpQ,314
42
+ traffic_taffy-0.8.1.dist-info/licenses/LICENSE.txt,sha256=hiV1DJgDQeSM1r7P-ez5oxily11S5nsCedU0jKzKKzo,11338
43
+ traffic_taffy-0.8.1.dist-info/RECORD,,
@@ -1,34 +0,0 @@
1
- traffic_taffy/__init__.py,sha256=nWnFwYowE5aRF4-qDmemCNsKthCCZqKs1fEUqJ-LX9E,22
2
- traffic_taffy/compare.py,sha256=VwwxfJUYMXSVByX4HTAk3IoQz5yxBUzcWYw36f5bj4s,11855
3
- traffic_taffy/comparison.py,sha256=MmCxK7E3RbVLS8gJ_JJOdqa2MHQ7np1Pq_VB0Scir8U,933
4
- traffic_taffy/dissection.py,sha256=Y1uiMHrD4TWQqu85dptI3joGOX6O4Dmn0MRnaH3gIPI,17141
5
- traffic_taffy/dissectmany.py,sha256=M3myhPiatVLJrINAuHkX4bx9X7rhpJy37T_H7p1TI8w,4394
6
- traffic_taffy/dissector.py,sha256=ysUVcsdQyx4LMFabSvln1vS7FFGShj67wBQfvnrSpAs,10609
7
- traffic_taffy/graph.py,sha256=Jj5Ga43I-zbetGjAIT-rzuEHr_ZZwFhGP76vkEgwNmg,4384
8
- traffic_taffy/graphdata.py,sha256=lHpLuzzypEPuz3QPfkvs0IAbplTLzKcqHaLyoRidmiE,2971
9
- traffic_taffy/dissector_engine/__init__.py,sha256=Hu-UQtz7yhivmQLUP5b8tFQLEhy2bfvrRV3Q4aZp6vg,2202
10
- traffic_taffy/dissector_engine/dnstap.py,sha256=rBzVlB0D3YVhHOsr17cbnCIZU13g20srgR4sE7ZfNUE,4810
11
- traffic_taffy/dissector_engine/dpkt.py,sha256=YgFceo_6cy1VN-ODIijSsOfH3w8OzHPbpUS463is3YI,10949
12
- traffic_taffy/dissector_engine/scapy.py,sha256=5lE6y4wzULfULTFcTAo6qINazqoBVwzbRzP0alP4ATc,3730
13
- traffic_taffy/output/__init__.py,sha256=dSpW9xDno3nKaYOwThS2fVkcIHJVAJE9DlEiLfdHXfg,4555
14
- traffic_taffy/output/console.py,sha256=Ziq4MKtzSdP9oVaZrWHiw0Bpsm2Dj9QFLJtDK38mxaY,2911
15
- traffic_taffy/output/fsdb.py,sha256=po--ldHeRPTXVTkP68qtI_BYV2cEOab-kFVWv0ymj5M,1704
16
- traffic_taffy/output/memory.py,sha256=86tgJ-jMt3UVX31eP6U02YbbYRoqbYhhR4kXJQmYzO4,1870
17
- traffic_taffy/tests/test_compare_results.py,sha256=aUxy6UdtGhWJYNuQn8_UzkPRQpbEfzKXWz7DShpmRG4,1832
18
- traffic_taffy/tests/test_dict_merge.py,sha256=t3rZSQQ0AlBxRKfLborx9SxYN53cCAQQzZ2w-__WT2Y,1429
19
- traffic_taffy/tests/test_normalize.py,sha256=k5y3XmYitnF1aTkB-9dZ7WZPQB__l_iEzC6atKKywfw,2601
20
- traffic_taffy/tests/test_pcap_dissector.py,sha256=sxJJ3seO9pjzilM0b6iu_ggQbUI4E7WbMFdwm4m9Gz0,2027
21
- traffic_taffy/tests/test_pcap_splitter.py,sha256=Pjv2CS_WXqK1wQ1rdsxLU-M1Z5fuHsciEhiDJpujFnI,2568
22
- traffic_taffy/tests/test_value_printing.py,sha256=3k-d1bpa6tD8qcrgNVoZhhG7kwylI2AMVNFPbRE2vds,266
23
- traffic_taffy/tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- traffic_taffy/tools/cache_info.py,sha256=N4L-xjO3i-p3QD5DIytSt3GBnT4JsJh3V2dl5Tktumk,2084
25
- traffic_taffy/tools/compare.py,sha256=mVChkw56pzU6HCY67pNSI7qrlaU5N65BdLoP8upmsXU,3395
26
- traffic_taffy/tools/dissect.py,sha256=1-b6fnIE3DPJ2xvsImPrVynKPwerQU0PrYvaMJC85ZA,3420
27
- traffic_taffy/tools/explore.py,sha256=TEUqfiknqKwEq0zswfgwvKPLMSOlTUWqC8NhcwPfQO8,24146
28
- traffic_taffy/tools/export.py,sha256=jk5ck-K9m9_Rno72fDWgfS67WKUumDL1KQpMG_j0-G8,2682
29
- traffic_taffy/tools/graph.py,sha256=5pcO_FsASddi_zEYB9h24syuqLCaDzByIPAiR4aq6gg,2530
30
- traffic_taffy-0.6.4.dist-info/METADATA,sha256=m9iXCaqdmC3SC9hb24ZIUnjdWESXR2cp6UdjHIpOOSs,1884
31
- traffic_taffy-0.6.4.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
32
- traffic_taffy-0.6.4.dist-info/entry_points.txt,sha256=ySz30b1Cu03CtCGMRqqg0NZJpzrVI91T5HjbEaTNnpQ,314
33
- traffic_taffy-0.6.4.dist-info/licenses/LICENSE.txt,sha256=hiV1DJgDQeSM1r7P-ez5oxily11S5nsCedU0jKzKKzo,11338
34
- traffic_taffy-0.6.4.dist-info/RECORD,,