scythe-ttp 0.15.10__tar.gz → 0.17.0__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.

Potentially problematic release.


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

Files changed (59) hide show
  1. {scythe_ttp-0.15.10/scythe_ttp.egg-info → scythe_ttp-0.17.0}/PKG-INFO +2 -1
  2. scythe_ttp-0.17.0/VERSION +1 -0
  3. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/pyproject.toml +3 -2
  4. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/cli/main.py +135 -5
  5. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/journeys/actions.py +29 -7
  6. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0/scythe_ttp.egg-info}/PKG-INFO +2 -1
  7. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe_ttp.egg-info/requires.txt +1 -0
  8. scythe_ttp-0.15.10/VERSION +0 -1
  9. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/LICENSE +0 -0
  10. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/MANIFEST.in +0 -0
  11. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/README.md +0 -0
  12. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/requirements.txt +0 -0
  13. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/__init__.py +0 -0
  14. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/auth/__init__.py +0 -0
  15. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/auth/base.py +0 -0
  16. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/auth/basic.py +0 -0
  17. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/auth/bearer.py +0 -0
  18. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/auth/cookie_jwt.py +0 -0
  19. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/behaviors/__init__.py +0 -0
  20. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/behaviors/base.py +0 -0
  21. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/behaviors/default.py +0 -0
  22. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/behaviors/human.py +0 -0
  23. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/behaviors/machine.py +0 -0
  24. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/behaviors/stealth.py +0 -0
  25. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/cli/__init__.py +0 -0
  26. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/core/__init__.py +0 -0
  27. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/core/executor.py +0 -0
  28. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/core/headers.py +0 -0
  29. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/core/ttp.py +0 -0
  30. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/journeys/__init__.py +0 -0
  31. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/journeys/base.py +0 -0
  32. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/journeys/executor.py +0 -0
  33. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/orchestrators/__init__.py +0 -0
  34. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/orchestrators/base.py +0 -0
  35. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/orchestrators/batch.py +0 -0
  36. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/orchestrators/distributed.py +0 -0
  37. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/orchestrators/scale.py +0 -0
  38. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/payloads/__init__.py +0 -0
  39. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/payloads/generators.py +0 -0
  40. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/ttps/__init__.py +0 -0
  41. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/ttps/web/__init__.py +0 -0
  42. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/ttps/web/login_bruteforce.py +0 -0
  43. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/ttps/web/sql_injection.py +0 -0
  44. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe/ttps/web/uuid_guessing.py +0 -0
  45. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe_ttp.egg-info/SOURCES.txt +0 -0
  46. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe_ttp.egg-info/dependency_links.txt +0 -0
  47. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe_ttp.egg-info/entry_points.txt +0 -0
  48. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/scythe_ttp.egg-info/top_level.txt +0 -0
  49. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/setup.cfg +0 -0
  50. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/tests/test_api_models.py +0 -0
  51. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/tests/test_authentication.py +0 -0
  52. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/tests/test_behaviors.py +0 -0
  53. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/tests/test_cli.py +0 -0
  54. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/tests/test_cookie_jwt_auth.py +0 -0
  55. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/tests/test_expected_results.py +0 -0
  56. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/tests/test_feature_completeness.py +0 -0
  57. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/tests/test_header_extraction.py +0 -0
  58. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/tests/test_journeys.py +0 -0
  59. {scythe_ttp-0.15.10 → scythe_ttp-0.17.0}/tests/test_orchestrators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scythe-ttp
3
- Version: 0.15.10
3
+ Version: 0.17.0
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.0
@@ -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
@@ -83,6 +83,138 @@ def main():
83
83
  dest='gate_versions',
84
84
  help='Gate versions to test against')
85
85
 
86
+ # Core Application Parameters
87
+ parser.add_argument(
88
+ '--protocol',
89
+ default='https',
90
+ choices=['http', 'https'],
91
+ help='Protocol to use (http/https, default: https)')
92
+ parser.add_argument(
93
+ '--port',
94
+ type=int,
95
+ help='Port number for the target application')
96
+
97
+ # Authentication Parameters
98
+ parser.add_argument(
99
+ '--username',
100
+ help='Username for authentication')
101
+ parser.add_argument(
102
+ '--password',
103
+ help='Password for authentication')
104
+ parser.add_argument(
105
+ '--token',
106
+ help='Bearer token or API key')
107
+ parser.add_argument(
108
+ '--auth-type',
109
+ choices=['basic', 'bearer', 'form'],
110
+ help='Authentication method (basic, bearer, form, etc.)')
111
+ parser.add_argument(
112
+ '--credentials-file',
113
+ help='Path to file containing multiple user credentials')
114
+
115
+ # Test Data Parameters
116
+ parser.add_argument(
117
+ '--users-file',
118
+ help='Path to CSV file containing user data')
119
+ parser.add_argument(
120
+ '--emails-file',
121
+ help='Path to text file containing email addresses')
122
+ parser.add_argument(
123
+ '--payload-file',
124
+ help='Path to file containing test payloads')
125
+ parser.add_argument(
126
+ '--data-file',
127
+ help='Generic path to test data file')
128
+
129
+ # Execution Control Parameters
130
+ parser.add_argument(
131
+ '--batch-size',
132
+ type=int,
133
+ default=10,
134
+ help='Number of operations per batch (default: 10)')
135
+ parser.add_argument(
136
+ '--max-batches',
137
+ type=int,
138
+ help='Maximum number of batches to run')
139
+ parser.add_argument(
140
+ '--workers',
141
+ type=int,
142
+ help='Number of concurrent workers/threads')
143
+ parser.add_argument(
144
+ '--replications',
145
+ type=int,
146
+ help='Number of test replications for load testing')
147
+ parser.add_argument(
148
+ '--timeout',
149
+ type=int,
150
+ help='Request timeout in seconds')
151
+ parser.add_argument(
152
+ '--delay',
153
+ type=float,
154
+ help='Delay between requests in seconds')
155
+
156
+ # Browser/Execution Parameters
157
+ parser.add_argument(
158
+ '--headless',
159
+ action='store_true',
160
+ help='Run browser in headless mode (flag)')
161
+ parser.add_argument(
162
+ '--browser',
163
+ choices=['chrome', 'firefox', 'safari', 'edge'],
164
+ help='Browser type (chrome, firefox, etc.)')
165
+ parser.add_argument(
166
+ '--user-agent',
167
+ help='Custom user agent string')
168
+ parser.add_argument(
169
+ '--proxy',
170
+ help='Proxy server URL')
171
+ parser.add_argument(
172
+ '--proxy-file',
173
+ help='Path to file containing proxy list')
174
+
175
+ # Output and Reporting Parameters
176
+ parser.add_argument(
177
+ '--output-dir',
178
+ help='Directory for output files')
179
+ parser.add_argument(
180
+ '--report-format',
181
+ choices=['json', 'csv', 'html'],
182
+ help='Report format (json, csv, html)')
183
+ parser.add_argument(
184
+ '--log-level',
185
+ choices=['debug', 'info', 'warning', 'error'],
186
+ help='Logging level (debug, info, warning, error)')
187
+ parser.add_argument(
188
+ '--verbose',
189
+ action='store_true',
190
+ help='Enable verbose output (flag)')
191
+ parser.add_argument(
192
+ '--silent',
193
+ action='store_true',
194
+ help='Suppress output except errors (flag)')
195
+
196
+ # Test Control Parameters
197
+ parser.add_argument(
198
+ '--fail-fast',
199
+ action='store_true',
200
+ help='Stop immediately on first failure (flag)')
201
+ parser.add_argument(
202
+ '--dry-run',
203
+ action='store_true',
204
+ help='Validate configuration without executing tests (flag)')
205
+ parser.add_argument(
206
+ '--test-type',
207
+ choices=['load', 'security', 'functional'],
208
+ help='Type of test to run (load, security, functional)')
209
+ parser.add_argument(
210
+ '--iterations',
211
+ type=int,
212
+ help='Number of test iterations')
213
+ parser.add_argument(
214
+ '--duration',
215
+ type=int,
216
+ help='Test duration in seconds')
217
+
86
218
  args = parser.parse_args()
87
219
 
88
220
  if check_url_available(args.url):
@@ -226,10 +358,8 @@ def _create_test(project_root: str, name: str) -> str:
226
358
 
227
359
  return filepath
228
360
 
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
-
361
+ _VERSION_RE = re.compile(r"['\"]?X-Scythe-Target-Version['\"]?\s*:\s*['\"]?([\w.-]+)['\"]?")
362
+ _DETECTED_LIST_RE = re.compile(r"Target versions detected:\s*\[?([^]]*)\]?")
233
363
 
234
364
  def _parse_version_from_output(output: str) -> Optional[str]:
235
365
  m = _VERSION_RE.search(output)
@@ -240,7 +370,7 @@ def _parse_version_from_output(output: str) -> Optional[str]:
240
370
  if m:
241
371
  inner = m.group(1)
242
372
  # extract first version-like token
243
- mv = re.search(r"[\d]+(?:\.[\w\-]+)+", inner)
373
+ mv = re.search(r"\d+(?:\.[\w\-]+)+", inner)
244
374
  if mv:
245
375
  return mv.group(0)
246
376
  return None
@@ -659,6 +659,7 @@ class ApiRequestAction(Action):
659
659
  def __init__(self,
660
660
  method: str,
661
661
  url: str,
662
+ flush: bool = False,
662
663
  params: Optional[Dict[str, Any]] = None,
663
664
  body_json: Optional[Dict[str, Any]] = None,
664
665
  data: Optional[Dict[str, Any]] = None,
@@ -673,6 +674,7 @@ class ApiRequestAction(Action):
673
674
  fail_on_validation_error: bool = False):
674
675
  self.method = method.upper()
675
676
  self.url = url
677
+ self.flush = flush
676
678
  self.params = params or {}
677
679
  self.body_json = body_json
678
680
  self.data = data
@@ -692,15 +694,15 @@ class ApiRequestAction(Action):
692
694
  if session is None:
693
695
  session = requests.Session()
694
696
  context['requests_session'] = session
695
-
696
- # Build headers: auth headers from context + action headers (action overrides)
697
+
698
+ # Build headers: auth headers from context and action headers (action overrides)
697
699
  final_headers = {}
698
700
  auth_headers = context.get('auth_headers', {}) or {}
699
701
  if auth_headers:
700
702
  final_headers.update(auth_headers)
701
703
  if self.headers:
702
704
  final_headers.update(self.headers)
703
-
705
+
704
706
  # Simple masking for sensitive headers
705
707
  def _mask_headers(headers: Dict[str, Any]) -> Dict[str, Any]:
706
708
  masked = {}
@@ -713,7 +715,7 @@ class ApiRequestAction(Action):
713
715
  else:
714
716
  masked[k] = v
715
717
  return masked
716
-
718
+
717
719
  # Resolve URL: absolute or join with target_url from context
718
720
  from urllib.parse import urljoin
719
721
  from ..core.headers import HeaderExtractor
@@ -725,7 +727,7 @@ class ApiRequestAction(Action):
725
727
  resolved_url = self.url
726
728
  else:
727
729
  resolved_url = urljoin(base_url, self.url)
728
-
730
+
729
731
  # Store request details early
730
732
  self.store_result('request_method', self.method)
731
733
  self.store_result('url', resolved_url)
@@ -736,7 +738,7 @@ class ApiRequestAction(Action):
736
738
  if self.data is not None:
737
739
  self.store_result('request_data', self.data)
738
740
  self.store_result('request_headers', _mask_headers(final_headers))
739
-
741
+
740
742
  logger = logging.getLogger("Journey.ApiRequestAction")
741
743
  # Honor any pending rate-limit resume time set by previous actions/steps
742
744
  try:
@@ -861,14 +863,25 @@ class ApiRequestAction(Action):
861
863
 
862
864
  # Determine success (status-based by default)
863
865
  if self.expected_status is not None:
864
- http_ok = (getattr(response, 'status_code', None) == self.expected_status)
866
+ http_ok = (status_code == self.expected_status)
867
+ if not http_ok:
868
+ self.store_result('status_mismatch', f"Expected status {self.expected_status}, got {status_code}")
869
+ logger.warning(f"API request status mismatch: expected {self.expected_status}, got {status_code}")
865
870
  else:
866
871
  http_ok = bool(getattr(response, 'ok', False))
867
872
 
873
+ # Store the final status check result
874
+ self.store_result('http_status_ok', http_ok)
875
+
868
876
  # Optionally fail on validation error
869
877
  if self.response_model is not None and self.fail_on_validation_error and self.get_result('response_validation_error'):
870
878
  return False
871
879
 
880
+ if self.flush:
881
+ clear_requests_session(context)
882
+
883
+ # Return False if status doesn't match expected, regardless of expected_result
884
+ # The framework will then compare this with expected_result to determine test outcome
872
885
  return http_ok
873
886
  except Exception as e:
874
887
  last_exception = e
@@ -878,3 +891,12 @@ class ApiRequestAction(Action):
878
891
 
879
892
  # If we got here and had an exception or no return, fail
880
893
  return False
894
+
895
+ def clear_requests_session(context: Dict[str, Any]):
896
+ """Clear the request session from the context."""
897
+ logger = logging.getLogger("Journey.ApiRequestAction")
898
+ session = context.get('requests_session')
899
+ if session is not None:
900
+ session.close()
901
+ context['requests_session'] = None
902
+ logger.info("Cleared requests session from context")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scythe-ttp
3
- Version: 0.15.10
3
+ Version: 0.17.0
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
@@ -1 +0,0 @@
1
- 0.15.10
File without changes
File without changes
File without changes
File without changes