scythe-ttp 0.10.0__py3-none-any.whl → 0.11.0__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.

Potentially problematic release.


This version of scythe-ttp might be problematic. Click here for more details.

scythe/core/executor.py CHANGED
@@ -5,6 +5,7 @@ from selenium.webdriver.chrome.options import Options
5
5
  from .ttp import TTP
6
6
  from typing import Optional
7
7
  from ..behaviors.base import Behavior
8
+ from .headers import HeaderExtractor
8
9
 
9
10
  # Configure logging
10
11
  logging.basicConfig(
@@ -33,8 +34,12 @@ class TTPExecutor:
33
34
  self.chrome_options.add_argument("--no-sandbox")
34
35
  self.chrome_options.add_argument("--disable-dev-shm-usage")
35
36
 
37
+ # Enable header extraction capabilities
38
+ HeaderExtractor.enable_logging_for_driver(self.chrome_options)
39
+
36
40
  self.driver = None
37
41
  self.results = []
42
+ self.header_extractor = HeaderExtractor()
38
43
 
39
44
  def _setup_driver(self):
40
45
  """Initializes the WebDriver."""
@@ -108,13 +113,27 @@ class TTPExecutor:
108
113
  if success:
109
114
  consecutive_failures = 0
110
115
  current_url = self.driver.current_url if self.driver else "unknown"
111
- result_entry = {'payload': payload, 'url': current_url, 'expected': self.ttp.expected_result, 'actual': True}
116
+
117
+ # Extract target version header
118
+ target_version = None
119
+ if self.driver:
120
+ target_version = self.header_extractor.extract_target_version(self.driver, self.target_url)
121
+
122
+ result_entry = {
123
+ 'payload': payload,
124
+ 'url': current_url,
125
+ 'expected': self.ttp.expected_result,
126
+ 'actual': True,
127
+ 'target_version': target_version
128
+ }
112
129
  self.results.append(result_entry)
113
130
 
114
131
  if self.ttp.expected_result:
115
- self.logger.info(f"EXPECTED SUCCESS: '{payload}'")
132
+ version_info = f" | Version: {target_version}" if target_version else ""
133
+ self.logger.info(f"EXPECTED SUCCESS: '{payload}'{version_info}")
116
134
  else:
117
- self.logger.warning(f"UNEXPECTED SUCCESS: '{payload}' (expected to fail)")
135
+ version_info = f" | Version: {target_version}" if target_version else ""
136
+ self.logger.warning(f"UNEXPECTED SUCCESS: '{payload}' (expected to fail){version_info}")
118
137
  else:
119
138
  consecutive_failures += 1
120
139
  if self.ttp.expected_result:
@@ -168,12 +187,26 @@ class TTPExecutor:
168
187
  if expected_successes:
169
188
  self.logger.info(f"Expected successes: {len(expected_successes)}")
170
189
  for result in expected_successes:
171
- self.logger.info(f" Payload: {result['payload']} | URL: {result['url']}")
190
+ version_info = f" | Version: {result['target_version']}" if result.get('target_version') else ""
191
+ self.logger.info(f" ✓ Payload: {result['payload']} | URL: {result['url']}{version_info}")
172
192
 
173
193
  if unexpected_successes:
174
194
  self.logger.warning(f"Unexpected successes: {len(unexpected_successes)}")
175
195
  for result in unexpected_successes:
176
- self.logger.warning(f" Payload: {result['payload']} | URL: {result['url']}")
196
+ version_info = f" | Version: {result['target_version']}" if result.get('target_version') else ""
197
+ self.logger.warning(f" ✗ Payload: {result['payload']} | URL: {result['url']}{version_info}")
198
+
199
+ # Display version summary
200
+ version_summary = self.header_extractor.get_version_summary(self.results)
201
+ if version_summary['results_with_version'] > 0:
202
+ self.logger.info("\nTarget Version Summary:")
203
+ self.logger.info(f" Results with version info: {version_summary['results_with_version']}/{version_summary['total_results']}")
204
+ if version_summary['unique_versions']:
205
+ for version in version_summary['unique_versions']:
206
+ count = version_summary['version_counts'][version]
207
+ self.logger.info(f" Version {version}: {count} result(s)")
208
+ else:
209
+ self.logger.info("\nNo X-SCYTHE-TARGET-VERSION headers detected in responses.")
177
210
  else:
178
211
  if self.ttp.expected_result:
179
212
  self.logger.info("No successes detected (expected to find vulnerabilities).")
scythe/core/headers.py ADDED
@@ -0,0 +1,194 @@
1
+ import json
2
+ import logging
3
+ from typing import Optional, Dict, Any
4
+ from selenium.webdriver.remote.webdriver import WebDriver
5
+ from selenium.webdriver.chrome.options import Options
6
+
7
+
8
+ class HeaderExtractor:
9
+ """
10
+ Utility class for extracting HTTP response headers from WebDriver sessions.
11
+
12
+ Specifically designed to capture the X-SCYTHE-TARGET-VERSION header
13
+ that indicates the version of the web application being tested.
14
+ """
15
+
16
+ SCYTHE_VERSION_HEADER = "X-SCYTHE-TARGET-VERSION"
17
+
18
+ def __init__(self):
19
+ self.logger = logging.getLogger("HeaderExtractor")
20
+
21
+ @staticmethod
22
+ def enable_logging_for_driver(chrome_options: Options) -> None:
23
+ """
24
+ Enable performance logging capabilities for Chrome WebDriver.
25
+
26
+ This must be called during WebDriver setup to capture network logs.
27
+
28
+ Args:
29
+ chrome_options: Chrome options object to modify
30
+ """
31
+ # Enable performance logging to capture network events
32
+ chrome_options.add_argument("--enable-logging")
33
+ chrome_options.add_argument("--log-level=0")
34
+ chrome_options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
35
+
36
+ def extract_target_version(self, driver: WebDriver, target_url: Optional[str] = None) -> Optional[str]:
37
+ """
38
+ Extract the X-SCYTHE-TARGET-VERSION header from the most recent HTTP response.
39
+
40
+ Args:
41
+ driver: WebDriver instance with performance logging enabled
42
+ target_url: Optional URL to filter responses for (if None, uses any response)
43
+
44
+ Returns:
45
+ Version string if header found, None otherwise
46
+ """
47
+ try:
48
+ # Get performance logs - using getattr to handle type checking
49
+ if not hasattr(driver, 'get_log'):
50
+ self.logger.warning("WebDriver does not support get_log method")
51
+ return None
52
+
53
+ logs = getattr(driver, 'get_log')('performance')
54
+
55
+ # Look for Network.responseReceived events
56
+ for log_entry in reversed(logs): # Start with most recent
57
+ try:
58
+ message = log_entry.get('message', {})
59
+ if isinstance(message, str):
60
+ message = json.loads(message)
61
+
62
+ method = message.get('message', {}).get('method', '')
63
+ params = message.get('message', {}).get('params', {})
64
+
65
+ if method == 'Network.responseReceived':
66
+ response = params.get('response', {})
67
+ headers = response.get('headers', {})
68
+ response_url = response.get('url', '')
69
+
70
+ # Filter by target URL if specified
71
+ if target_url and target_url not in response_url:
72
+ continue
73
+
74
+ # Look for the version header (case-insensitive)
75
+ version = self._find_version_header(headers)
76
+ if version:
77
+ self.logger.debug(f"Found target version '{version}' in response from {response_url}")
78
+ return version
79
+
80
+ except (json.JSONDecodeError, KeyError, AttributeError) as e:
81
+ self.logger.debug(f"Error parsing log entry: {e}")
82
+ continue
83
+
84
+ self.logger.debug("No X-SCYTHE-TARGET-VERSION header found in network logs")
85
+ return None
86
+
87
+ except Exception as e:
88
+ self.logger.warning(f"Failed to extract target version from logs: {e}")
89
+ return None
90
+
91
+ def _find_version_header(self, headers: Dict[str, Any]) -> Optional[str]:
92
+ """
93
+ Find the version header in a case-insensitive manner.
94
+
95
+ Args:
96
+ headers: Dictionary of HTTP headers
97
+
98
+ Returns:
99
+ Version string if found, None otherwise
100
+ """
101
+ # Check for exact case match first
102
+ if self.SCYTHE_VERSION_HEADER in headers:
103
+ return str(headers[self.SCYTHE_VERSION_HEADER]).strip()
104
+
105
+ # Check case-insensitive
106
+ header_lower = self.SCYTHE_VERSION_HEADER.lower()
107
+ for header_name, header_value in headers.items():
108
+ if header_name.lower() == header_lower:
109
+ return str(header_value).strip()
110
+
111
+ return None
112
+
113
+ def extract_all_headers(self, driver: WebDriver, target_url: Optional[str] = None) -> Dict[str, str]:
114
+ """
115
+ Extract all headers from the most recent HTTP response.
116
+
117
+ Useful for debugging or when additional headers might be needed.
118
+
119
+ Args:
120
+ driver: WebDriver instance with performance logging enabled
121
+ target_url: Optional URL to filter responses for
122
+
123
+ Returns:
124
+ Dictionary of headers from the most recent response
125
+ """
126
+ try:
127
+ # Get performance logs - using getattr to handle type checking
128
+ if not hasattr(driver, 'get_log'):
129
+ self.logger.warning("WebDriver does not support get_log method")
130
+ return {}
131
+
132
+ logs = getattr(driver, 'get_log')('performance')
133
+
134
+ for log_entry in reversed(logs):
135
+ try:
136
+ message = log_entry.get('message', {})
137
+ if isinstance(message, str):
138
+ message = json.loads(message)
139
+
140
+ method = message.get('message', {}).get('method', '')
141
+ params = message.get('message', {}).get('params', {})
142
+
143
+ if method == 'Network.responseReceived':
144
+ response = params.get('response', {})
145
+ headers = response.get('headers', {})
146
+ response_url = response.get('url', '')
147
+
148
+ # Filter by target URL if specified
149
+ if target_url and target_url not in response_url:
150
+ continue
151
+
152
+ # Convert all header values to strings
153
+ return {k: str(v) for k, v in headers.items()}
154
+
155
+ except (json.JSONDecodeError, KeyError, AttributeError):
156
+ continue
157
+
158
+ return {}
159
+
160
+ except Exception as e:
161
+ self.logger.warning(f"Failed to extract headers from logs: {e}")
162
+ return {}
163
+
164
+ def get_version_summary(self, results: list) -> Dict[str, Any]:
165
+ """
166
+ Analyze version information across multiple test results.
167
+
168
+ Args:
169
+ results: List of result dictionaries containing version information
170
+
171
+ Returns:
172
+ Dictionary with version analysis summary
173
+ """
174
+ versions = []
175
+ results_with_version = 0
176
+
177
+ for result in results:
178
+ version = result.get('target_version')
179
+ if version:
180
+ versions.append(version)
181
+ results_with_version += 1
182
+
183
+ summary = {
184
+ 'total_results': len(results),
185
+ 'results_with_version': results_with_version,
186
+ 'unique_versions': list(set(versions)) if versions else [],
187
+ 'version_counts': {}
188
+ }
189
+
190
+ # Count occurrences of each version
191
+ for version in versions:
192
+ summary['version_counts'][version] = summary['version_counts'].get(version, 0) + 1
193
+
194
+ return summary
scythe/journeys/base.py CHANGED
@@ -276,6 +276,10 @@ class Journey:
276
276
  logger.info(f"Description: {self.description}")
277
277
  logger.info(f"Steps: {len(self.steps)}")
278
278
 
279
+ # Import here to avoid circular imports
280
+ from ..core.headers import HeaderExtractor
281
+ header_extractor = HeaderExtractor()
282
+
279
283
  start_time = time.time()
280
284
 
281
285
  # Set initial context
@@ -297,7 +301,9 @@ class Journey:
297
301
  'overall_success': False,
298
302
  'execution_time': 0,
299
303
  'step_results': [],
300
- 'errors': []
304
+ 'errors': [],
305
+ 'target_versions': [],
306
+ 'version_summary': {}
301
307
  }
302
308
 
303
309
  try:
@@ -335,6 +341,12 @@ class Journey:
335
341
  else:
336
342
  results['actions_failed'] += 1
337
343
 
344
+ # Extract target version header after step execution
345
+ target_version = header_extractor.extract_target_version(driver, target_url)
346
+ if target_version:
347
+ results['target_versions'].append(target_version)
348
+ logger.info(f"Target version detected: {target_version}")
349
+
338
350
  # Store step results
339
351
  step_result = {
340
352
  'step_name': step.name,
@@ -342,7 +354,8 @@ class Journey:
342
354
  'expected': step.expected_result,
343
355
  'actual': step_success,
344
356
  'actions': step.execution_results.copy(),
345
- 'step_data': step.step_data.copy()
357
+ 'step_data': step.step_data.copy(),
358
+ 'target_version': target_version
346
359
  }
347
360
  results['step_results'].append(step_result)
348
361
 
@@ -368,6 +381,12 @@ class Journey:
368
381
  results['end_time'] = end_time
369
382
  results['execution_time'] = end_time - start_time
370
383
 
384
+ # Generate version summary
385
+ if results['target_versions']:
386
+ version_summary = header_extractor.get_version_summary([{'target_version': v} for v in results['target_versions']])
387
+ results['version_summary'] = version_summary
388
+ logger.info(f"Target versions detected: {list(set(results['target_versions']))}")
389
+
371
390
  logger.info(f"Journey completed in {results['execution_time']:.2f} seconds")
372
391
  logger.info(f"Overall success: {results['overall_success']}")
373
392
  logger.info(f"Steps: {results['steps_succeeded']}/{results['steps_executed']} succeeded")
@@ -5,6 +5,7 @@ from selenium.webdriver.chrome.options import Options
5
5
  from typing import Optional, Dict, Any, List
6
6
  from ..behaviors.base import Behavior
7
7
  from .base import Journey
8
+ from ..core.headers import HeaderExtractor
8
9
 
9
10
  # Configure logging
10
11
  logging.basicConfig(
@@ -64,8 +65,12 @@ class JourneyExecutor:
64
65
  else:
65
66
  self.chrome_options.add_argument(f"--{key}={value}")
66
67
 
68
+ # Enable header extraction capabilities
69
+ HeaderExtractor.enable_logging_for_driver(self.chrome_options)
70
+
67
71
  self.driver = None
68
72
  self.execution_results = None
73
+ self.header_extractor = HeaderExtractor()
69
74
 
70
75
  def _setup_driver(self):
71
76
  """Initialize the WebDriver."""
@@ -297,14 +302,30 @@ class JourneyExecutor:
297
302
  step_success = step_result.get('actual', False)
298
303
  step_expected = step_result.get('expected', True)
299
304
  actions = step_result.get('actions', [])
305
+ target_version = step_result.get('target_version')
300
306
 
301
307
  status = "✓" if step_success == step_expected else "✗"
302
308
  result_text = "SUCCESS" if step_success else "FAILURE"
303
309
  expected_text = "expected" if step_success == step_expected else "unexpected"
310
+ version_info = f" | Version: {target_version}" if target_version else ""
304
311
 
305
- self.logger.info(f" {status} Step {i}: {step_name} - {result_text} ({expected_text})")
312
+ self.logger.info(f" {status} Step {i}: {step_name} - {result_text} ({expected_text}){version_info}")
306
313
  self.logger.info(f" Actions: {len([a for a in actions if a.get('actual', False)])}/{len(actions)} succeeded")
307
314
 
315
+ # Version summary
316
+ target_versions = self.execution_results.get('target_versions', [])
317
+
318
+ if target_versions:
319
+ self.logger.info("\nTarget Version Summary:")
320
+ self.logger.info(f" Steps with version info: {len(target_versions)}/{len(step_results) if step_results else 0}")
321
+ unique_versions = list(set(target_versions))
322
+ if unique_versions:
323
+ for version in unique_versions:
324
+ count = target_versions.count(version)
325
+ self.logger.info(f" Version {version}: {count} step(s)")
326
+ else:
327
+ self.logger.info("\nNo X-SCYTHE-TARGET-VERSION headers detected in responses.")
328
+
308
329
  self.logger.info("="*60)
309
330
 
310
331
  def get_results(self) -> Optional[Dict[str, Any]]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scythe-ttp
3
- Version: 0.10.0
3
+ Version: 0.11.0
4
4
  Summary: An extensible framework for emulating attacker TTPs with Selenium.
5
5
  Home-page: https://github.com/EpykLab/scythe
6
6
  Author: EpykLab
@@ -96,6 +96,7 @@ Scythe operates on the principle that robust systems must be tested under advers
96
96
  ### 📊 **Professional Reporting**
97
97
  * **Clear Result Indicators**: ✓ Expected outcomes, ✗ Unexpected results
98
98
  * **Comprehensive Logging**: Detailed execution tracking and analysis
99
+ * **Version Detection**: Automatic extraction of X-SCYTHE-TARGET-VERSION headers
99
100
  * **Performance Metrics**: Timing, success rates, and resource utilization
100
101
  * **Execution Statistics**: Detailed reporting across all test types
101
102
 
@@ -443,6 +444,74 @@ executor = TTPExecutor(
443
444
  )
444
445
  ```
445
446
 
447
+ ### Version Detection
448
+
449
+ Scythe automatically captures the `X-SCYTHE-TARGET-VERSION` header from HTTP responses to track which version of your web application is being tested:
450
+
451
+ ```python
452
+ from scythe.core.ttp import TTP
453
+ from scythe.core.executor import TTPExecutor
454
+
455
+ # Your web application should set this header:
456
+ # X-SCYTHE-TARGET-VERSION: 1.3.2
457
+
458
+ class MyTTP(TTP):
459
+ def get_payloads(self):
460
+ yield "test_payload"
461
+
462
+ def execute_step(self, driver, payload):
463
+ driver.get("http://your-app.com/login")
464
+ # ... test logic ...
465
+
466
+ def verify_result(self, driver):
467
+ return "welcome" in driver.page_source
468
+
469
+ # Run the test
470
+ ttp = MyTTP("Version Test", "Test with version detection")
471
+ executor = TTPExecutor(ttp=ttp, target_url="http://your-app.com")
472
+ executor.run()
473
+ ```
474
+
475
+ **Output includes version information:**
476
+ ```
477
+ ✓ EXPECTED SUCCESS: 'test_payload' | Version: 1.3.2
478
+ Target Version Summary:
479
+ Results with version info: 1/1
480
+ Version 1.3.2: 1 result(s)
481
+ ```
482
+
483
+ **Server-side implementation examples:**
484
+ ```python
485
+ # Python/Flask
486
+ @app.after_request
487
+ def add_version_header(response):
488
+ response.headers['X-SCYTHE-TARGET-VERSION'] = '1.3.2'
489
+ return response
490
+
491
+ # Node.js/Express
492
+ app.use((req, res, next) => {
493
+ res.set('X-SCYTHE-TARGET-VERSION', '1.3.2');
494
+ next();
495
+ });
496
+
497
+ # Java/Spring Boot
498
+ @Component
499
+ public class VersionHeaderFilter implements Filter {
500
+ @Override
501
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
502
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
503
+ httpResponse.setHeader("X-SCYTHE-TARGET-VERSION", "1.3.2");
504
+ chain.doFilter(request, response);
505
+ }
506
+ }
507
+ ```
508
+
509
+ This feature helps you:
510
+ - **Track test results** by application version
511
+ - **Verify deployment status** during testing
512
+ - **Correlate issues** with specific software versions
513
+ - **Ensure consistency** across test environments
514
+
446
515
  ### Custom Test Creation
447
516
 
448
517
  Extend Scythe for specific testing needs:
@@ -10,12 +10,13 @@ scythe/behaviors/human.py,sha256=1PqYvE7cnxlj-KDmDIr3uzfWHvDAbbxQxJ0V0iTl9yo,102
10
10
  scythe/behaviors/machine.py,sha256=NDMUq3mDhpCvujzAFxhn2eSVq78-J-LSBhIcvHkzKXo,4624
11
11
  scythe/behaviors/stealth.py,sha256=xv7MrPQgRCdCUJyYTcXV2aasWZoAw8rAQWg-AuQVb7U,15278
12
12
  scythe/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- scythe/core/executor.py,sha256=KyfnOnYK5mtfAE9dNYFeay7me8dEA5g0ICLgbWPtscg,7721
13
+ scythe/core/executor.py,sha256=x1w2nByVu2G70sh7t0kOh6urlrTm_r_pbk0S7v1Ov28,9736
14
+ scythe/core/headers.py,sha256=U-IXYs1g0q1BB2W7fxfvuVwd3tPCRkBdF4_-Dj787w8,7833
14
15
  scythe/core/ttp.py,sha256=Xw9GgptYsjZ-pMLdyPv64bhiwGKobrXHdF32pjIY7OU,3102
15
16
  scythe/journeys/__init__.py,sha256=-8AIpCmkeWtQ656yU3omj_guMG4v4i1koIpD6NZhUGM,612
16
17
  scythe/journeys/actions.py,sha256=Ez6Bpzs2VHzXMl6GtPve85XxzQV09rDscmDuzSs3VBE,25229
17
- scythe/journeys/base.py,sha256=jSP-qy_xquoCH5UBFO8e52EdnF-2nugSoJHNeLLuXSE,14408
18
- scythe/journeys/executor.py,sha256=uarNHeAfRJMxC343Lgh8z9i0pZj9F-pNU8YX-R2ov5E,18875
18
+ scythe/journeys/base.py,sha256=BWf35Ee3N9qy76Awh-r04-waUTDfLyxssvDmwYToXgY,15461
19
+ scythe/journeys/executor.py,sha256=1D_HUzvi4Z7a5uE7QbIDWH7HTGz5DoxcQffr-05bi_0,19978
19
20
  scythe/orchestrators/__init__.py,sha256=_vemcXjKbB1jI0F2dPA0F1zNsyUekjcXImLDUDhWDN0,560
20
21
  scythe/orchestrators/base.py,sha256=YOZV0ewlzJ49H08P_LKnimutUms8NnDrQprFpSKhOeM,13595
21
22
  scythe/orchestrators/batch.py,sha256=FpK501kk-earJzz6v7dcuw2y708rTvt_IMH_5qjKdrc,26635
@@ -28,8 +29,8 @@ scythe/ttps/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
29
  scythe/ttps/web/login_bruteforce.py,sha256=D4G8zB_nU9LD5w3Vv2ABTuOl4XTeg2BgZwYMObt4JJw,2488
29
30
  scythe/ttps/web/sql_injection.py,sha256=aWk4DFePbtFDsieOOj03Ux-5OiykyOs2_d_3SvWMOVE,2910
30
31
  scythe/ttps/web/uuid_guessing.py,sha256=WwCIQPLIixd5U2EY4bhnj7YP2AQDaPfQy7Yhj84UHy8,1245
31
- scythe_ttp-0.10.0.dist-info/licenses/LICENSE,sha256=B7iB4Fv6zDQolC7IgqNF8F4GEp_DLe2jrPPuR_MYMOM,1064
32
- scythe_ttp-0.10.0.dist-info/METADATA,sha256=YsBI0wg0fungTybVjt4mrE3PwD728C3meTcQgfaUywg,25478
33
- scythe_ttp-0.10.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
34
- scythe_ttp-0.10.0.dist-info/top_level.txt,sha256=BCKTrPuVvmLyhOR07C1ggOh6sU7g2LoVvwDMn46O55Y,7
35
- scythe_ttp-0.10.0.dist-info/RECORD,,
32
+ scythe_ttp-0.11.0.dist-info/licenses/LICENSE,sha256=B7iB4Fv6zDQolC7IgqNF8F4GEp_DLe2jrPPuR_MYMOM,1064
33
+ scythe_ttp-0.11.0.dist-info/METADATA,sha256=6HnepGVg2VYQweeSJthYxiKQjZYZhbMY3KUgA5Zshmk,27473
34
+ scythe_ttp-0.11.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ scythe_ttp-0.11.0.dist-info/top_level.txt,sha256=BCKTrPuVvmLyhOR07C1ggOh6sU7g2LoVvwDMn46O55Y,7
36
+ scythe_ttp-0.11.0.dist-info/RECORD,,