scythe-ttp 0.16.0__tar.gz → 0.17.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.
Files changed (59) hide show
  1. {scythe_ttp-0.16.0/scythe_ttp.egg-info → scythe_ttp-0.17.1}/PKG-INFO +2 -1
  2. scythe_ttp-0.17.1/VERSION +1 -0
  3. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/pyproject.toml +3 -2
  4. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/cli/main.py +148 -5
  5. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/core/executor.py +18 -0
  6. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/journeys/executor.py +6 -0
  7. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1/scythe_ttp.egg-info}/PKG-INFO +2 -1
  8. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe_ttp.egg-info/requires.txt +1 -0
  9. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/tests/test_expected_results.py +57 -0
  10. scythe_ttp-0.16.0/VERSION +0 -1
  11. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/LICENSE +0 -0
  12. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/MANIFEST.in +0 -0
  13. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/README.md +0 -0
  14. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/requirements.txt +0 -0
  15. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/__init__.py +0 -0
  16. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/auth/__init__.py +0 -0
  17. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/auth/base.py +0 -0
  18. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/auth/basic.py +0 -0
  19. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/auth/bearer.py +0 -0
  20. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/auth/cookie_jwt.py +0 -0
  21. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/behaviors/__init__.py +0 -0
  22. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/behaviors/base.py +0 -0
  23. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/behaviors/default.py +0 -0
  24. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/behaviors/human.py +0 -0
  25. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/behaviors/machine.py +0 -0
  26. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/behaviors/stealth.py +0 -0
  27. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/cli/__init__.py +0 -0
  28. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/core/__init__.py +0 -0
  29. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/core/headers.py +0 -0
  30. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/core/ttp.py +0 -0
  31. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/journeys/__init__.py +0 -0
  32. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/journeys/actions.py +0 -0
  33. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/journeys/base.py +0 -0
  34. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/orchestrators/__init__.py +0 -0
  35. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/orchestrators/base.py +0 -0
  36. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/orchestrators/batch.py +0 -0
  37. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/orchestrators/distributed.py +0 -0
  38. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/orchestrators/scale.py +0 -0
  39. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/payloads/__init__.py +0 -0
  40. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/payloads/generators.py +0 -0
  41. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/ttps/__init__.py +0 -0
  42. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/ttps/web/__init__.py +0 -0
  43. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/ttps/web/login_bruteforce.py +0 -0
  44. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/ttps/web/sql_injection.py +0 -0
  45. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe/ttps/web/uuid_guessing.py +0 -0
  46. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe_ttp.egg-info/SOURCES.txt +0 -0
  47. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe_ttp.egg-info/dependency_links.txt +0 -0
  48. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe_ttp.egg-info/entry_points.txt +0 -0
  49. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/scythe_ttp.egg-info/top_level.txt +0 -0
  50. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/setup.cfg +0 -0
  51. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/tests/test_api_models.py +0 -0
  52. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/tests/test_authentication.py +0 -0
  53. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/tests/test_behaviors.py +0 -0
  54. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/tests/test_cli.py +0 -0
  55. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/tests/test_cookie_jwt_auth.py +0 -0
  56. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/tests/test_feature_completeness.py +0 -0
  57. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/tests/test_header_extraction.py +0 -0
  58. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/tests/test_journeys.py +0 -0
  59. {scythe_ttp-0.16.0 → scythe_ttp-0.17.1}/tests/test_orchestrators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scythe-ttp
3
- Version: 0.16.0
3
+ Version: 0.17.1
4
4
  Summary: An extensible framework for emulating attacker TTPs with Selenium.
5
5
  Author-email: EpykLab <cyber@epyklab.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -35,6 +35,7 @@ Requires-Dist: urllib3==2.4.0
35
35
  Requires-Dist: websocket-client==1.8.0
36
36
  Requires-Dist: wsproto==1.2.0
37
37
  Requires-Dist: typer
38
+ Requires-Dist: shellingham
38
39
  Dynamic: license-file
39
40
 
40
41
  <h1 align="center">Scythe</h1>
@@ -0,0 +1 @@
1
+ 0.17.1
@@ -40,7 +40,8 @@ dependencies = [
40
40
  "urllib3==2.4.0",
41
41
  "websocket-client==1.8.0",
42
42
  "wsproto==1.2.0",
43
- "typer"
43
+ "typer",
44
+ "shellingham"
44
45
  ]
45
46
  # Remove the static version line entirely
46
47
 
@@ -52,4 +53,4 @@ packages = {find = {exclude = ["tests*", "examples*"]}}
52
53
 
53
54
  [tool.setuptools.dynamic]
54
55
  version = {file = "VERSION"} # Fix this to point to your actual VERSION file
55
- dependencies = {file = "requirements.txt"} # Keep if using; otherwise remove if sticking with static deps list
56
+ dependencies = {file = "requirements.txt"} # Keep if using; otherwise remove if sticking with static deps list
@@ -68,6 +68,19 @@ def check_version_in_response_header(args) -> bool:
68
68
  def scythe_test_definition(args) -> bool:
69
69
  # TODO: implement your test using Scythe primitives.
70
70
  # Example placeholder that simply passes.
71
+
72
+ # Example usage with TTPExecutor:
73
+ # from scythe.core.executor import TTPExecutor
74
+ # executor = TTPExecutor(ttp=my_ttp, target_url=args.url)
75
+ # executor.run()
76
+ # return executor.was_successful() # Returns True if all results matched expectations
77
+
78
+ # Example usage with JourneyExecutor:
79
+ # from scythe.journeys.executor import JourneyExecutor
80
+ # executor = JourneyExecutor(journey=my_journey, target_url=args.url)
81
+ # executor.run()
82
+ # return executor.was_successful() # Returns True if journey succeeded as expected
83
+
71
84
  return True
72
85
 
73
86
 
@@ -83,6 +96,138 @@ def main():
83
96
  dest='gate_versions',
84
97
  help='Gate versions to test against')
85
98
 
99
+ # Core Application Parameters
100
+ parser.add_argument(
101
+ '--protocol',
102
+ default='https',
103
+ choices=['http', 'https'],
104
+ help='Protocol to use (http/https, default: https)')
105
+ parser.add_argument(
106
+ '--port',
107
+ type=int,
108
+ help='Port number for the target application')
109
+
110
+ # Authentication Parameters
111
+ parser.add_argument(
112
+ '--username',
113
+ help='Username for authentication')
114
+ parser.add_argument(
115
+ '--password',
116
+ help='Password for authentication')
117
+ parser.add_argument(
118
+ '--token',
119
+ help='Bearer token or API key')
120
+ parser.add_argument(
121
+ '--auth-type',
122
+ choices=['basic', 'bearer', 'form'],
123
+ help='Authentication method (basic, bearer, form, etc.)')
124
+ parser.add_argument(
125
+ '--credentials-file',
126
+ help='Path to file containing multiple user credentials')
127
+
128
+ # Test Data Parameters
129
+ parser.add_argument(
130
+ '--users-file',
131
+ help='Path to CSV file containing user data')
132
+ parser.add_argument(
133
+ '--emails-file',
134
+ help='Path to text file containing email addresses')
135
+ parser.add_argument(
136
+ '--payload-file',
137
+ help='Path to file containing test payloads')
138
+ parser.add_argument(
139
+ '--data-file',
140
+ help='Generic path to test data file')
141
+
142
+ # Execution Control Parameters
143
+ parser.add_argument(
144
+ '--batch-size',
145
+ type=int,
146
+ default=10,
147
+ help='Number of operations per batch (default: 10)')
148
+ parser.add_argument(
149
+ '--max-batches',
150
+ type=int,
151
+ help='Maximum number of batches to run')
152
+ parser.add_argument(
153
+ '--workers',
154
+ type=int,
155
+ help='Number of concurrent workers/threads')
156
+ parser.add_argument(
157
+ '--replications',
158
+ type=int,
159
+ help='Number of test replications for load testing')
160
+ parser.add_argument(
161
+ '--timeout',
162
+ type=int,
163
+ help='Request timeout in seconds')
164
+ parser.add_argument(
165
+ '--delay',
166
+ type=float,
167
+ help='Delay between requests in seconds')
168
+
169
+ # Browser/Execution Parameters
170
+ parser.add_argument(
171
+ '--headless',
172
+ action='store_true',
173
+ help='Run browser in headless mode (flag)')
174
+ parser.add_argument(
175
+ '--browser',
176
+ choices=['chrome', 'firefox', 'safari', 'edge'],
177
+ help='Browser type (chrome, firefox, etc.)')
178
+ parser.add_argument(
179
+ '--user-agent',
180
+ help='Custom user agent string')
181
+ parser.add_argument(
182
+ '--proxy',
183
+ help='Proxy server URL')
184
+ parser.add_argument(
185
+ '--proxy-file',
186
+ help='Path to file containing proxy list')
187
+
188
+ # Output and Reporting Parameters
189
+ parser.add_argument(
190
+ '--output-dir',
191
+ help='Directory for output files')
192
+ parser.add_argument(
193
+ '--report-format',
194
+ choices=['json', 'csv', 'html'],
195
+ help='Report format (json, csv, html)')
196
+ parser.add_argument(
197
+ '--log-level',
198
+ choices=['debug', 'info', 'warning', 'error'],
199
+ help='Logging level (debug, info, warning, error)')
200
+ parser.add_argument(
201
+ '--verbose',
202
+ action='store_true',
203
+ help='Enable verbose output (flag)')
204
+ parser.add_argument(
205
+ '--silent',
206
+ action='store_true',
207
+ help='Suppress output except errors (flag)')
208
+
209
+ # Test Control Parameters
210
+ parser.add_argument(
211
+ '--fail-fast',
212
+ action='store_true',
213
+ help='Stop immediately on first failure (flag)')
214
+ parser.add_argument(
215
+ '--dry-run',
216
+ action='store_true',
217
+ help='Validate configuration without executing tests (flag)')
218
+ parser.add_argument(
219
+ '--test-type',
220
+ choices=['load', 'security', 'functional'],
221
+ help='Type of test to run (load, security, functional)')
222
+ parser.add_argument(
223
+ '--iterations',
224
+ type=int,
225
+ help='Number of test iterations')
226
+ parser.add_argument(
227
+ '--duration',
228
+ type=int,
229
+ help='Test duration in seconds')
230
+
86
231
  args = parser.parse_args()
87
232
 
88
233
  if check_url_available(args.url):
@@ -226,10 +371,8 @@ def _create_test(project_root: str, name: str) -> str:
226
371
 
227
372
  return filepath
228
373
 
229
-
230
- _VERSION_RE = re.compile(r"X-SCYTHE-TARGET-VERSION\s*[:=]\s*([\w\.-]+)")
231
- _DETECTED_LIST_RE = re.compile(r"Detected target versions: \[?([^\]]*)\]?")
232
-
374
+ _VERSION_RE = re.compile(r"['\"]?X-Scythe-Target-Version['\"]?\s*:\s*['\"]?([\w.-]+)['\"]?")
375
+ _DETECTED_LIST_RE = re.compile(r"Target versions detected:\s*\[?([^]]*)\]?")
233
376
 
234
377
  def _parse_version_from_output(output: str) -> Optional[str]:
235
378
  m = _VERSION_RE.search(output)
@@ -240,7 +383,7 @@ def _parse_version_from_output(output: str) -> Optional[str]:
240
383
  if m:
241
384
  inner = m.group(1)
242
385
  # extract first version-like token
243
- mv = re.search(r"[\d]+(?:\.[\w\-]+)+", inner)
386
+ mv = re.search(r"\d+(?:\.[\w\-]+)+", inner)
244
387
  if mv:
245
388
  return mv.group(0)
246
389
  return None
@@ -40,6 +40,7 @@ class TTPExecutor:
40
40
  self.driver = None
41
41
  self.results = []
42
42
  self.header_extractor = HeaderExtractor()
43
+ self.has_test_failures = False # Track if any test had unexpected results
43
44
 
44
45
  def _setup_driver(self):
45
46
  """Initializes the WebDriver."""
@@ -134,10 +135,12 @@ class TTPExecutor:
134
135
  else:
135
136
  version_info = f" | Version: {target_version}" if target_version else ""
136
137
  self.logger.warning(f"UNEXPECTED SUCCESS: '{payload}' (expected to fail){version_info}")
138
+ self.has_test_failures = True # Mark as failure when result differs from expected
137
139
  else:
138
140
  consecutive_failures += 1
139
141
  if self.ttp.expected_result:
140
142
  self.logger.info(f"EXPECTED FAILURE: '{payload}' (security control working)")
143
+ self.has_test_failures = True # Mark as failure when result differs from expected
141
144
  else:
142
145
  self.logger.info(f"EXPECTED FAILURE: '{payload}'")
143
146
 
@@ -212,3 +215,18 @@ class TTPExecutor:
212
215
  self.logger.info("No successes detected (expected to find vulnerabilities).")
213
216
  else:
214
217
  self.logger.info("No successes detected (security controls working as expected).")
218
+
219
+ # Log overall test status
220
+ if self.has_test_failures:
221
+ self.logger.error("\n✗ TEST FAILED: One or more test results differed from expected")
222
+ else:
223
+ self.logger.info("\n✓ TEST PASSED: All test results matched expectations")
224
+
225
+ def was_successful(self) -> bool:
226
+ """
227
+ Check if all test results matched expectations.
228
+
229
+ Returns:
230
+ True if all test results matched expectations, False otherwise
231
+ """
232
+ return not self.has_test_failures
@@ -406,6 +406,12 @@ class JourneyExecutor:
406
406
  else:
407
407
  self.logger.info("\nNo X-SCYTHE-TARGET-VERSION headers detected in responses.")
408
408
 
409
+ # Log overall test status (similar to TTPExecutor)
410
+ if self.was_successful():
411
+ self.logger.info("\n✓ TEST PASSED: Journey results matched expectations")
412
+ else:
413
+ self.logger.error("\n✗ TEST FAILED: Journey results differed from expected")
414
+
409
415
  self.logger.info("="*60)
410
416
 
411
417
  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.16.0
3
+ Version: 0.17.1
4
4
  Summary: An extensible framework for emulating attacker TTPs with Selenium.
5
5
  Author-email: EpykLab <cyber@epyklab.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -35,6 +35,7 @@ Requires-Dist: urllib3==2.4.0
35
35
  Requires-Dist: websocket-client==1.8.0
36
36
  Requires-Dist: wsproto==1.2.0
37
37
  Requires-Dist: typer
38
+ Requires-Dist: shellingham
38
39
  Dynamic: license-file
39
40
 
40
41
  <h1 align="center">Scythe</h1>
@@ -19,3 +19,4 @@ urllib3==2.4.0
19
19
  websocket-client==1.8.0
20
20
  wsproto==1.2.0
21
21
  typer
22
+ shellingham
@@ -296,6 +296,63 @@ class TestExpectedResults(unittest.TestCase):
296
296
 
297
297
  self.assertTrue(ttp_pass.expected_result)
298
298
  self.assertFalse(ttp_fail.expected_result)
299
+
300
+ @patch('scythe.core.executor.webdriver.Chrome')
301
+ def test_was_successful_with_expected_results(self, mock_webdriver):
302
+ """Test was_successful() returns True when all results match expectations."""
303
+ mock_webdriver.return_value = self.mock_driver
304
+
305
+ # Test with expected successes
306
+ ttp = MockTTP(
307
+ name="Test TTP",
308
+ description="Test description",
309
+ expected_result=True,
310
+ success_results=[True, True]
311
+ )
312
+
313
+ executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
314
+ executor.run()
315
+
316
+ # Should return True since results matched expectations
317
+ self.assertTrue(executor.was_successful())
318
+
319
+ @patch('scythe.core.executor.webdriver.Chrome')
320
+ def test_was_successful_with_unexpected_results(self, mock_webdriver):
321
+ """Test was_successful() returns False when results don't match expectations."""
322
+ mock_webdriver.return_value = self.mock_driver
323
+
324
+ # Test with unexpected successes (expected to fail but succeeded)
325
+ ttp = MockTTP(
326
+ name="Test TTP",
327
+ description="Test description",
328
+ expected_result=False,
329
+ success_results=[True]
330
+ )
331
+
332
+ executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
333
+ executor.run()
334
+
335
+ # Should return False since we got unexpected success
336
+ self.assertFalse(executor.was_successful())
337
+
338
+ @patch('scythe.core.executor.webdriver.Chrome')
339
+ def test_was_successful_with_unexpected_failures(self, mock_webdriver):
340
+ """Test was_successful() returns False when expected success but got failure."""
341
+ mock_webdriver.return_value = self.mock_driver
342
+
343
+ # Test expecting success but getting failure
344
+ ttp = MockTTP(
345
+ name="Test TTP",
346
+ description="Test description",
347
+ expected_result=True,
348
+ success_results=[False, False]
349
+ )
350
+
351
+ executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
352
+ executor.run()
353
+
354
+ # Should return False since we expected success but got failures
355
+ self.assertFalse(executor.was_successful())
299
356
 
300
357
 
301
358
  if __name__ == '__main__':
scythe_ttp-0.16.0/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.16.0
File without changes
File without changes
File without changes
File without changes