scythe-ttp 0.17.1__tar.gz → 0.18.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 (65) hide show
  1. {scythe_ttp-0.17.1/scythe_ttp.egg-info → scythe_ttp-0.18.1}/PKG-INFO +1 -1
  2. scythe_ttp-0.18.1/VERSION +1 -0
  3. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/auth/cookie_jwt.py +43 -14
  4. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/cli/main.py +52 -14
  5. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/core/executor.py +125 -2
  6. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/core/ttp.py +61 -3
  7. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/journeys/actions.py +151 -69
  8. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/journeys/executor.py +9 -0
  9. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/orchestrators/base.py +18 -0
  10. scythe_ttp-0.18.1/scythe/ttps/web/__init__.py +12 -0
  11. scythe_ttp-0.18.1/scythe/ttps/web/login_bruteforce.py +195 -0
  12. scythe_ttp-0.18.1/scythe/ttps/web/request_flooding.py +503 -0
  13. scythe_ttp-0.18.1/scythe/ttps/web/sql_injection.py +297 -0
  14. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1/scythe_ttp.egg-info}/PKG-INFO +1 -1
  15. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/SOURCES.txt +4 -1
  16. scythe_ttp-0.18.1/tests/test_executor_modes.py +481 -0
  17. scythe_ttp-0.18.1/tests/test_ttp_api_mode.py +591 -0
  18. scythe_ttp-0.17.1/VERSION +0 -1
  19. scythe_ttp-0.17.1/scythe/ttps/web/__init__.py +0 -0
  20. scythe_ttp-0.17.1/scythe/ttps/web/login_bruteforce.py +0 -64
  21. scythe_ttp-0.17.1/scythe/ttps/web/sql_injection.py +0 -80
  22. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/LICENSE +0 -0
  23. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/MANIFEST.in +0 -0
  24. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/README.md +0 -0
  25. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/pyproject.toml +0 -0
  26. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/requirements.txt +0 -0
  27. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/__init__.py +0 -0
  28. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/auth/__init__.py +0 -0
  29. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/auth/base.py +0 -0
  30. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/auth/basic.py +0 -0
  31. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/auth/bearer.py +0 -0
  32. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/behaviors/__init__.py +0 -0
  33. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/behaviors/base.py +0 -0
  34. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/behaviors/default.py +0 -0
  35. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/behaviors/human.py +0 -0
  36. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/behaviors/machine.py +0 -0
  37. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/behaviors/stealth.py +0 -0
  38. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/cli/__init__.py +0 -0
  39. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/core/__init__.py +0 -0
  40. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/core/headers.py +0 -0
  41. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/journeys/__init__.py +0 -0
  42. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/journeys/base.py +0 -0
  43. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/orchestrators/__init__.py +0 -0
  44. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/orchestrators/batch.py +0 -0
  45. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/orchestrators/distributed.py +0 -0
  46. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/orchestrators/scale.py +0 -0
  47. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/payloads/__init__.py +0 -0
  48. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/payloads/generators.py +0 -0
  49. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/ttps/__init__.py +0 -0
  50. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe/ttps/web/uuid_guessing.py +0 -0
  51. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/dependency_links.txt +0 -0
  52. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/entry_points.txt +0 -0
  53. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/requires.txt +0 -0
  54. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/top_level.txt +0 -0
  55. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/setup.cfg +0 -0
  56. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/tests/test_api_models.py +0 -0
  57. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/tests/test_authentication.py +0 -0
  58. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/tests/test_behaviors.py +0 -0
  59. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/tests/test_cli.py +0 -0
  60. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/tests/test_cookie_jwt_auth.py +0 -0
  61. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/tests/test_expected_results.py +0 -0
  62. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/tests/test_feature_completeness.py +0 -0
  63. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/tests/test_header_extraction.py +0 -0
  64. {scythe_ttp-0.17.1 → scythe_ttp-0.18.1}/tests/test_journeys.py +0 -0
  65. {scythe_ttp-0.17.1 → scythe_ttp-0.18.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.17.1
3
+ Version: 0.18.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
@@ -0,0 +1 @@
1
+ 0.18.1
@@ -49,10 +49,17 @@ class CookieJWTAuth(Authentication):
49
49
 
50
50
  Behavior:
51
51
  - In API mode: JourneyExecutor will call get_auth_cookies(); this class will
52
- perform a POST to login_url (if token not cached), parse JSON, extract the
53
- token via jwt_json_path, and return {cookie_name: token}.
52
+ perform a POST to login_url (if token not cached), extract the token, and
53
+ return {cookie_name: token}.
54
54
  - In UI mode: authenticate() will ensure the browser has the cookie set for
55
55
  the target domain.
56
+
57
+ Parameters:
58
+ - content_type: Either "json" (default) to send payload as JSON, or "form"
59
+ to send as application/x-www-form-urlencoded form data.
60
+ - jwt_source: Either "json" (default) to extract JWT from the JSON response body
61
+ using jwt_json_path, or "cookie" to extract it from the Set-Cookie response header
62
+ using cookie_name.
56
63
  """
57
64
 
58
65
  def __init__(self,
@@ -64,6 +71,8 @@ class CookieJWTAuth(Authentication):
64
71
  extra_fields: Optional[Dict[str, Any]] = None,
65
72
  jwt_json_path: str = "token",
66
73
  cookie_name: str = "stellarbridge",
74
+ content_type: str = "json",
75
+ jwt_source: str = "json",
67
76
  session: Optional[requests.Session] = None,
68
77
  description: str = "Authenticate via API and set JWT cookie"):
69
78
  super().__init__(
@@ -78,29 +87,49 @@ class CookieJWTAuth(Authentication):
78
87
  self.extra_fields = extra_fields or {}
79
88
  self.jwt_json_path = jwt_json_path
80
89
  self.cookie_name = cookie_name
90
+ self.content_type = content_type
91
+ self.jwt_source = jwt_source
81
92
  # Avoid importing requests in test environments; allow injected session
82
93
  self._session = session or (requests.Session() if requests is not None else None)
83
94
  self.token: Optional[str] = None
84
95
 
85
96
  def _login_and_get_token(self) -> str:
86
97
  payload: Dict[str, Any] = dict(self.extra_fields)
87
- if self.username is not None:
88
- payload[self.username_field] = self.username
89
- if self.password is not None:
90
- payload[self.password_field] = self.password
98
+ payload[self.username_field] = self.username
99
+ payload[self.password_field] = self.password
91
100
  try:
92
- resp = self._session.post(self.login_url, json=payload, timeout=15)
101
+ if self.content_type == "form":
102
+ resp = self._session.post(self.login_url, data=payload, timeout=15)
103
+ else:
104
+ resp = self._session.post(self.login_url, json=payload, timeout=15)
93
105
  # try json; raise on non-2xx to surface errors
94
106
  resp.raise_for_status()
95
- data = resp.json()
96
107
  except Exception as e:
97
108
  raise AuthenticationError(f"Login request failed: {e}", self.name)
98
- token = _extract_by_dot_path(data, self.jwt_json_path)
99
- if not token or not isinstance(token, str):
100
- raise AuthenticationError(
101
- f"JWT not found at path '{self.jwt_json_path}' in login response",
102
- self.name,
103
- )
109
+
110
+ # Extract token from either response cookies or JSON body
111
+ token = None
112
+ if self.jwt_source == "cookie":
113
+ # Extract from response cookies
114
+ token = resp.cookies.get(self.cookie_name)
115
+ if not token or not isinstance(token, str):
116
+ raise AuthenticationError(
117
+ f"JWT cookie '{self.cookie_name}' not found in login response",
118
+ self.name,
119
+ )
120
+ else:
121
+ # Extract from JSON response body
122
+ try:
123
+ data = resp.json()
124
+ except Exception as e:
125
+ raise AuthenticationError(f"Failed to parse JSON response: {e}", self.name)
126
+ token = _extract_by_dot_path(data, self.jwt_json_path)
127
+ if not token or not isinstance(token, str):
128
+ raise AuthenticationError(
129
+ f"JWT not found at path '{self.jwt_json_path}' in login response",
130
+ self.name,
131
+ )
132
+
104
133
  self.token = token
105
134
  self.store_auth_data('jwt', token)
106
135
  self.store_auth_data('login_time', time.time())
@@ -65,23 +65,49 @@ def check_version_in_response_header(args) -> bool:
65
65
  return False
66
66
  return True
67
67
 
68
- def scythe_test_definition(args) -> bool:
68
+ def scythe_test_definition(args) -> int:
69
69
  # TODO: implement your test using Scythe primitives.
70
70
  # Example placeholder that simply passes.
71
-
71
+
72
72
  # Example usage with TTPExecutor:
73
73
  # from scythe.core.executor import TTPExecutor
74
- # executor = TTPExecutor(ttp=my_ttp, target_url=args.url)
74
+ # from scythe.ttps.web.login_bruteforce import LoginBruteforceTTP
75
+ #
76
+ # ttp = LoginBruteforceTTP(
77
+ # payloads=['admin', 'root', 'test'],
78
+ # expected_result=False # Expect security controls to block attempts
79
+ # )
80
+ # executor = TTPExecutor(ttp=ttp, target_url=args.url)
75
81
  # executor.run()
76
82
  # return executor.was_successful() # Returns True if all results matched expectations
77
-
83
+
78
84
  # Example usage with JourneyExecutor:
79
85
  # from scythe.journeys.executor import JourneyExecutor
80
- # executor = JourneyExecutor(journey=my_journey, target_url=args.url)
86
+ # from scythe.journeys.base import Journey, Step
87
+ # from scythe.journeys.actions import NavigateAction, FillFormAction, ClickAction
88
+ #
89
+ # journey = Journey(
90
+ # name="Login Journey",
91
+ # description="Test user login flow",
92
+ # expected_result=True # Expect journey to succeed
93
+ # )
94
+ # journey.add_step(Step("Navigate").add_action(NavigateAction(url=args.url)))
95
+ # executor = JourneyExecutor(journey=journey, target_url=args.url)
81
96
  # executor.run()
82
97
  # return executor.was_successful() # Returns True if journey succeeded as expected
83
-
84
- return True
98
+
99
+ # Example usage with Orchestrators:
100
+ # from scythe.orchestrators.scale import ScaleOrchestrator
101
+ # from scythe.orchestrators.base import OrchestrationStrategy
102
+ #
103
+ # orchestrator = ScaleOrchestrator(
104
+ # strategy=OrchestrationStrategy.PARALLEL,
105
+ # max_workers=10
106
+ # )
107
+ # result = orchestrator.orchestrate_ttp(ttp=my_ttp, target_url=args.url, replications=100)
108
+ # return orchestrator.exit_code(result) == 0 # Returns True if all executions succeeded
109
+
110
+ return executor.exit_code() # assumes executor var
85
111
 
86
112
 
87
113
  def main():
@@ -233,14 +259,14 @@ def main():
233
259
  if check_url_available(args.url):
234
260
  if args.gate_versions:
235
261
  if check_version_in_response_header(args):
236
- ok = scythe_test_definition(args)
237
- sys.exit(0 if ok else 1)
262
+ exit_code = scythe_test_definition(args)
263
+ sys.exit(exit_code)
238
264
  else:
239
265
  print("No compatible version found in response header.")
240
266
  sys.exit(1)
241
267
  else:
242
- ok = scythe_test_definition(args)
243
- sys.exit(0 if ok else 1)
268
+ exit_code = scythe_test_definition(args)
269
+ sys.exit(exit_code)
244
270
  else:
245
271
  print("URL not available.")
246
272
  sys.exit(1)
@@ -254,6 +280,13 @@ class ScytheCLIError(Exception):
254
280
  pass
255
281
 
256
282
 
283
+ class ExitWithCode(Exception):
284
+ """Exception to exit with a specific code from within Typer commands."""
285
+ def __init__(self, code: int):
286
+ self.code = code
287
+ super().__init__()
288
+
289
+
257
290
  def _find_project_root(start: Optional[str] = None) -> Optional[str]:
258
291
  """Walk upwards from start (or cwd) to find a directory containing .scythe."""
259
292
  cur = os.path.abspath(start or os.getcwd())
@@ -702,7 +735,10 @@ def main(argv: Optional[List[str]] = None) -> int:
702
735
  code, output, version = _run_test(project_root, name, extra)
703
736
  _record_run(project_root, name, code, output, version)
704
737
  print(output)
705
- return code
738
+ # Raise exception to propagate exit code through Typer
739
+ if code != 0:
740
+ raise ExitWithCode(code)
741
+ return 0
706
742
 
707
743
  db_app = typer.Typer(
708
744
  no_args_is_help=True,
@@ -737,8 +773,10 @@ def main(argv: Optional[List[str]] = None) -> int:
737
773
  app.add_typer(db_app, name="db")
738
774
 
739
775
  try:
740
- rv = app()
741
- return int(rv) if isinstance(rv, int) else 0
776
+ app()
777
+ return 0
778
+ except ExitWithCode as e:
779
+ return e.code
742
780
  except ScytheCLIError as e:
743
781
  print(f"Error: {e}", file=sys.stderr)
744
782
  return 2
@@ -3,9 +3,10 @@ import logging
3
3
  from selenium import webdriver
4
4
  from selenium.webdriver.chrome.options import Options
5
5
  from .ttp import TTP
6
- from typing import Optional
6
+ from typing import Optional, Dict, Any
7
7
  from ..behaviors.base import Behavior
8
8
  from .headers import HeaderExtractor
9
+ import requests
9
10
 
10
11
  # Configure logging
11
12
  logging.basicConfig(
@@ -60,7 +61,18 @@ class TTPExecutor:
60
61
  self.logger.info(f"Using behavior: {self.behavior.name}")
61
62
  self.logger.info(f"Behavior description: {self.behavior.description}")
62
63
 
63
- self._setup_driver()
64
+ # Check execution mode
65
+ if self.ttp.execution_mode == 'api':
66
+ self.logger.info("Execution mode: API")
67
+ self._run_api_mode()
68
+ return
69
+ else:
70
+ self.logger.info("Execution mode: UI")
71
+ self._setup_driver()
72
+ self._run_ui_mode()
73
+
74
+ def _run_ui_mode(self):
75
+ """Execute TTP in UI mode using Selenium."""
64
76
 
65
77
  try:
66
78
  # Handle authentication if required
@@ -172,6 +184,108 @@ class TTPExecutor:
172
184
  finally:
173
185
  self._cleanup()
174
186
 
187
+ def _run_api_mode(self):
188
+ """Execute TTP in API mode using requests."""
189
+ session = requests.Session()
190
+ context: Dict[str, Any] = {
191
+ 'target_url': self.target_url,
192
+ 'auth_headers': {},
193
+ 'rate_limit_resume_at': None
194
+ }
195
+
196
+ try:
197
+ # Handle authentication if required (API mode)
198
+ if self.ttp.requires_authentication():
199
+ auth_name = self.ttp.authentication.name if self.ttp.authentication else "Unknown"
200
+ self.logger.info(f"Authentication required for TTP: {auth_name}")
201
+
202
+ # Try to get auth headers directly
203
+ try:
204
+ if hasattr(self.ttp.authentication, 'get_auth_headers'):
205
+ auth_headers = self.ttp.authentication.get_auth_headers() or {}
206
+ context['auth_headers'] = auth_headers
207
+ session.headers.update(auth_headers)
208
+ self.logger.info("Authentication headers applied")
209
+ except Exception as e:
210
+ self.logger.warning(f"Failed to get auth headers: {e}")
211
+
212
+ consecutive_failures = 0
213
+
214
+ for i, payload in enumerate(self.ttp.get_payloads(), 1):
215
+ # Check if behavior wants to continue
216
+ if self.behavior and not self.behavior.should_continue(i, consecutive_failures):
217
+ self.logger.info("Behavior requested to stop execution")
218
+ break
219
+
220
+ self.logger.info(f"Attempt {i}: Executing with payload -> '{payload}'")
221
+
222
+ try:
223
+ # Execute API request
224
+ response = self.ttp.execute_step_api(session, payload, context)
225
+
226
+ # Use behavior delay if available, otherwise use default
227
+ if self.behavior:
228
+ step_delay = self.behavior.get_step_delay(i)
229
+ else:
230
+ step_delay = self.delay
231
+
232
+ time.sleep(step_delay)
233
+
234
+ # Verify result
235
+ success = self.ttp.verify_result_api(response, context)
236
+
237
+ # Compare actual result with expected result
238
+ if success:
239
+ consecutive_failures = 0
240
+
241
+ # Extract target version from response headers
242
+ target_version = response.headers.get('X-SCYTHE-TARGET-VERSION') or response.headers.get('x-scythe-target-version')
243
+
244
+ result_entry = {
245
+ 'payload': payload,
246
+ 'url': response.url if hasattr(response, 'url') else self.target_url,
247
+ 'expected': self.ttp.expected_result,
248
+ 'actual': True,
249
+ 'target_version': target_version
250
+ }
251
+ self.results.append(result_entry)
252
+
253
+ if self.ttp.expected_result:
254
+ version_info = f" | Version: {target_version}" if target_version else ""
255
+ self.logger.info(f"EXPECTED SUCCESS: '{payload}'{version_info}")
256
+ else:
257
+ version_info = f" | Version: {target_version}" if target_version else ""
258
+ self.logger.warning(f"UNEXPECTED SUCCESS: '{payload}' (expected to fail){version_info}")
259
+ self.has_test_failures = True
260
+ else:
261
+ consecutive_failures += 1
262
+ if self.ttp.expected_result:
263
+ self.logger.info(f"EXPECTED FAILURE: '{payload}' (security control working)")
264
+ self.has_test_failures = True
265
+ else:
266
+ self.logger.info(f"EXPECTED FAILURE: '{payload}'")
267
+
268
+ except Exception as step_error:
269
+ consecutive_failures += 1
270
+ self.logger.error(f"Error during step {i}: {step_error}")
271
+
272
+ # Let behavior handle the error
273
+ if self.behavior:
274
+ if not self.behavior.on_error(step_error, i):
275
+ self.logger.info("Behavior requested to stop due to error")
276
+ break
277
+ else:
278
+ # Default behavior: continue on most errors
279
+ continue
280
+
281
+ except KeyboardInterrupt:
282
+ self.logger.info("Test interrupted by user.")
283
+ except Exception as e:
284
+ self.logger.error(f"An unexpected error occurred: {e}", exc_info=True)
285
+ finally:
286
+ session.close()
287
+ self._cleanup()
288
+
175
289
  def _cleanup(self):
176
290
  """Closes the WebDriver and prints a summary."""
177
291
  if self.driver:
@@ -230,3 +344,12 @@ class TTPExecutor:
230
344
  True if all test results matched expectations, False otherwise
231
345
  """
232
346
  return not self.has_test_failures
347
+
348
+ def exit_code(self) -> int:
349
+ """
350
+ Get the exit code for this test execution.
351
+
352
+ Returns:
353
+ 0 if test was successful (results matched expectations), 1 otherwise
354
+ """
355
+ return 0 if self.was_successful() else 1
@@ -1,9 +1,10 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Generator, Any, Optional, TYPE_CHECKING
2
+ from typing import Generator, Any, Optional, TYPE_CHECKING, Dict
3
3
  from selenium.webdriver.remote.webdriver import WebDriver
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from ..auth.base import Authentication
7
+ import requests
7
8
 
8
9
  class TTP(ABC):
9
10
  """
@@ -11,9 +12,14 @@ class TTP(ABC):
11
12
 
12
13
  Each TTP implementation must define how to generate payloads, how to
13
14
  execute a test step with a given payload, and how to verify the outcome.
15
+
16
+ TTPs can operate in two modes:
17
+ - 'ui': Uses Selenium WebDriver to interact with web UI (default)
18
+ - 'api': Uses requests library to interact directly with backend APIs
14
19
  """
15
20
 
16
- def __init__(self, name: str, description: str, expected_result: bool = True, authentication: Optional['Authentication'] = None):
21
+ def __init__(self, name: str, description: str, expected_result: bool = True,
22
+ authentication: Optional['Authentication'] = None, execution_mode: str = 'ui'):
17
23
  """
18
24
  Initialize a TTP.
19
25
 
@@ -24,11 +30,13 @@ class TTP(ABC):
24
30
  True means we expect to find vulnerabilities/success conditions.
25
31
  False means we expect the security controls to prevent success.
26
32
  authentication: Optional authentication mechanism to use before executing TTP
33
+ execution_mode: Execution mode - 'ui' for Selenium-based UI testing or 'api' for direct API testing
27
34
  """
28
35
  self.name = name
29
36
  self.description = description
30
37
  self.expected_result = expected_result
31
38
  self.authentication = authentication
39
+ self.execution_mode = execution_mode.lower()
32
40
 
33
41
  @abstractmethod
34
42
  def get_payloads(self) -> Generator[Any, None, None]:
@@ -80,9 +88,59 @@ class TTP(ABC):
80
88
  @abstractmethod
81
89
  def verify_result(self, driver: WebDriver) -> bool:
82
90
  """
83
- Verifies the outcome of the executed step.
91
+ Verifies the outcome of the executed step in UI mode.
84
92
 
85
93
  Returns:
86
94
  True if the test indicates a potential success/vulnerability, False otherwise.
87
95
  """
88
96
  pass
97
+
98
+ def execute_step_api(self, session: 'requests.Session', payload: Any, context: Dict[str, Any]) -> 'requests.Response':
99
+ """
100
+ Executes a single test action using the provided payload via API request.
101
+ This method should be overridden by TTPs that support API mode.
102
+
103
+ Args:
104
+ session: requests.Session instance for making HTTP requests
105
+ payload: The payload to use for this test iteration
106
+ context: Shared context dictionary for storing state and auth headers
107
+
108
+ Returns:
109
+ requests.Response object from the API call
110
+
111
+ Raises:
112
+ NotImplementedError: If the TTP does not support API mode
113
+ """
114
+ raise NotImplementedError(f"{self.name} does not support API execution mode")
115
+
116
+ def verify_result_api(self, response: 'requests.Response', context: Dict[str, Any]) -> bool:
117
+ """
118
+ Verifies the outcome of the executed step in API mode.
119
+ This method should be overridden by TTPs that support API mode.
120
+
121
+ Args:
122
+ response: The requests.Response object from execute_step_api
123
+ context: Shared context dictionary for accessing state
124
+
125
+ Returns:
126
+ True if the test indicates a potential success/vulnerability, False otherwise
127
+
128
+ Raises:
129
+ NotImplementedError: If the TTP does not support API mode
130
+ """
131
+ raise NotImplementedError(f"{self.name} does not support API result verification in API mode")
132
+
133
+ def supports_api_mode(self) -> bool:
134
+ """
135
+ Check if this TTP implementation supports API execution mode.
136
+
137
+ Returns:
138
+ True if API mode is supported, False otherwise
139
+ """
140
+ # Check if the TTP has overridden the API methods
141
+ try:
142
+ # Try to call the method on the class to see if it's been overridden
143
+ return (type(self).execute_step_api != TTP.execute_step_api or
144
+ type(self).verify_result_api != TTP.verify_result_api)
145
+ except Exception:
146
+ return False