scythe-ttp 0.17.1__tar.gz → 0.17.3__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 (63) hide show
  1. {scythe_ttp-0.17.1/scythe_ttp.egg-info → scythe_ttp-0.17.3}/PKG-INFO +1 -1
  2. scythe_ttp-0.17.3/VERSION +1 -0
  3. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/core/executor.py +116 -2
  4. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/core/ttp.py +61 -3
  5. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/journeys/actions.py +151 -69
  6. scythe_ttp-0.17.3/scythe/ttps/web/login_bruteforce.py +195 -0
  7. scythe_ttp-0.17.3/scythe/ttps/web/sql_injection.py +297 -0
  8. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3/scythe_ttp.egg-info}/PKG-INFO +1 -1
  9. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe_ttp.egg-info/SOURCES.txt +3 -1
  10. scythe_ttp-0.17.3/tests/test_executor_modes.py +481 -0
  11. scythe_ttp-0.17.3/tests/test_ttp_api_mode.py +591 -0
  12. scythe_ttp-0.17.1/VERSION +0 -1
  13. scythe_ttp-0.17.1/scythe/ttps/web/login_bruteforce.py +0 -64
  14. scythe_ttp-0.17.1/scythe/ttps/web/sql_injection.py +0 -80
  15. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/LICENSE +0 -0
  16. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/MANIFEST.in +0 -0
  17. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/README.md +0 -0
  18. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/pyproject.toml +0 -0
  19. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/requirements.txt +0 -0
  20. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/__init__.py +0 -0
  21. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/auth/__init__.py +0 -0
  22. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/auth/base.py +0 -0
  23. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/auth/basic.py +0 -0
  24. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/auth/bearer.py +0 -0
  25. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/auth/cookie_jwt.py +0 -0
  26. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/behaviors/__init__.py +0 -0
  27. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/behaviors/base.py +0 -0
  28. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/behaviors/default.py +0 -0
  29. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/behaviors/human.py +0 -0
  30. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/behaviors/machine.py +0 -0
  31. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/behaviors/stealth.py +0 -0
  32. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/cli/__init__.py +0 -0
  33. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/cli/main.py +0 -0
  34. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/core/__init__.py +0 -0
  35. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/core/headers.py +0 -0
  36. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/journeys/__init__.py +0 -0
  37. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/journeys/base.py +0 -0
  38. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/journeys/executor.py +0 -0
  39. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/orchestrators/__init__.py +0 -0
  40. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/orchestrators/base.py +0 -0
  41. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/orchestrators/batch.py +0 -0
  42. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/orchestrators/distributed.py +0 -0
  43. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/orchestrators/scale.py +0 -0
  44. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/payloads/__init__.py +0 -0
  45. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/payloads/generators.py +0 -0
  46. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/ttps/__init__.py +0 -0
  47. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/ttps/web/__init__.py +0 -0
  48. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe/ttps/web/uuid_guessing.py +0 -0
  49. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe_ttp.egg-info/dependency_links.txt +0 -0
  50. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe_ttp.egg-info/entry_points.txt +0 -0
  51. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe_ttp.egg-info/requires.txt +0 -0
  52. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/scythe_ttp.egg-info/top_level.txt +0 -0
  53. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/setup.cfg +0 -0
  54. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/tests/test_api_models.py +0 -0
  55. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/tests/test_authentication.py +0 -0
  56. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/tests/test_behaviors.py +0 -0
  57. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/tests/test_cli.py +0 -0
  58. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/tests/test_cookie_jwt_auth.py +0 -0
  59. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/tests/test_expected_results.py +0 -0
  60. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/tests/test_feature_completeness.py +0 -0
  61. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/tests/test_header_extraction.py +0 -0
  62. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/tests/test_journeys.py +0 -0
  63. {scythe_ttp-0.17.1 → scythe_ttp-0.17.3}/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.17.3
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.17.3
@@ -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:
@@ -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
@@ -439,81 +439,163 @@ class TTPAction(Action):
439
439
  True if TTP execution matches expected result, False otherwise
440
440
  """
441
441
  try:
442
- # Determine target URL
443
- if self.target_url:
444
- url = self.target_url
445
- elif 'current_url' in context:
446
- url = context['current_url']
442
+ # Check execution mode
443
+ if self.ttp.execution_mode == 'api':
444
+ return self._execute_api_mode(driver, context)
447
445
  else:
448
- url = driver.current_url
449
-
450
- # Navigate to URL if needed
451
- if url != driver.current_url:
452
- driver.get(url)
453
-
454
- # Execute TTP authentication if required
455
- if self.ttp.requires_authentication():
456
- auth_success = self.ttp.authenticate(driver, url)
457
- if not auth_success:
458
- self.store_result('error', 'TTP authentication failed')
459
- return False
460
-
461
- # Execute TTP payloads
462
- ttp_results = []
463
- success_count = 0
464
- total_count = 0
446
+ return self._execute_ui_mode(driver, context)
447
+
448
+ except Exception as e:
449
+ self.store_result('error', str(e))
450
+ return False
451
+
452
+ def _execute_ui_mode(self, driver: WebDriver, context: Dict[str, Any]) -> bool:
453
+ """Execute TTP in UI mode using Selenium."""
454
+ # Determine target URL
455
+ if self.target_url:
456
+ url = self.target_url
457
+ elif 'current_url' in context:
458
+ url = context['current_url']
459
+ else:
460
+ url = driver.current_url
461
+
462
+ # Navigate to URL if needed
463
+ if url != driver.current_url:
464
+ driver.get(url)
465
+
466
+ # Execute TTP authentication if required
467
+ if self.ttp.requires_authentication():
468
+ auth_success = self.ttp.authenticate(driver, url)
469
+ if not auth_success:
470
+ self.store_result('error', 'TTP authentication failed')
471
+ return False
472
+
473
+ # Execute TTP payloads
474
+ ttp_results = []
475
+ success_count = 0
476
+ total_count = 0
477
+
478
+ for payload in self.ttp.get_payloads():
479
+ total_count += 1
465
480
 
466
- for payload in self.ttp.get_payloads():
467
- total_count += 1
481
+ try:
482
+ # Execute step
483
+ self.ttp.execute_step(driver, payload)
468
484
 
469
- try:
470
- # Execute step
471
- self.ttp.execute_step(driver, payload)
472
-
473
- # Verify result
474
- result = self.ttp.verify_result(driver)
475
-
476
- ttp_results.append({
477
- 'payload': str(payload),
478
- 'success': result,
479
- 'url': driver.current_url
480
- })
485
+ # Verify result
486
+ result = self.ttp.verify_result(driver)
487
+
488
+ ttp_results.append({
489
+ 'payload': str(payload),
490
+ 'success': result,
491
+ 'url': driver.current_url
492
+ })
493
+
494
+ if result:
495
+ success_count += 1
481
496
 
482
- if result:
483
- success_count += 1
484
-
485
- except Exception as e:
486
- ttp_results.append({
487
- 'payload': str(payload),
488
- 'success': False,
489
- 'error': str(e),
490
- 'url': driver.current_url
491
- })
492
-
493
- # Store results
494
- self.store_result('ttp_name', self.ttp.name)
495
- self.store_result('total_payloads', total_count)
496
- self.store_result('successful_payloads', success_count)
497
- self.store_result('ttp_results', ttp_results)
498
- self.store_result('success_rate', success_count / total_count if total_count > 0 else 0)
499
-
500
- # Update context
501
- context[f'ttp_results_{self.ttp.name}'] = ttp_results
502
- context['last_ttp_success_count'] = success_count
503
-
504
- # Determine action success based on expected result
505
- has_successes = success_count > 0
497
+ except Exception as e:
498
+ ttp_results.append({
499
+ 'payload': str(payload),
500
+ 'success': False,
501
+ 'error': str(e),
502
+ 'url': driver.current_url
503
+ })
504
+
505
+ # Store results
506
+ self.store_result('ttp_name', self.ttp.name)
507
+ self.store_result('execution_mode', 'ui')
508
+ self.store_result('total_payloads', total_count)
509
+ self.store_result('successful_payloads', success_count)
510
+ self.store_result('ttp_results', ttp_results)
511
+ self.store_result('success_rate', success_count / total_count if total_count > 0 else 0)
512
+
513
+ # Update context
514
+ context[f'ttp_results_{self.ttp.name}'] = ttp_results
515
+ context['last_ttp_success_count'] = success_count
516
+
517
+ # Determine action success based on expected result
518
+ has_successes = success_count > 0
519
+
520
+ if self.expected_result:
521
+ # Expecting TTP to find vulnerabilities/succeed
522
+ return has_successes
523
+ else:
524
+ # Expecting TTP to fail (security controls working)
525
+ return not has_successes
526
+
527
+ def _execute_api_mode(self, driver: WebDriver, context: Dict[str, Any]) -> bool:
528
+ """Execute TTP in API mode using requests library."""
529
+ import requests
530
+
531
+ # Get or create requests session
532
+ session = context.get('requests_session')
533
+ if session is None:
534
+ session = requests.Session()
535
+ context['requests_session'] = session
536
+
537
+ # Set target URL in context if provided
538
+ if self.target_url:
539
+ context['target_url'] = self.target_url
540
+
541
+ # Verify TTP supports API mode
542
+ if not self.ttp.supports_api_mode():
543
+ self.store_result('error', f'TTP {self.ttp.name} does not support API execution mode')
544
+ return False
545
+
546
+ # Execute TTP payloads via API
547
+ ttp_results = []
548
+ success_count = 0
549
+ total_count = 0
550
+
551
+ for payload in self.ttp.get_payloads():
552
+ total_count += 1
506
553
 
507
- if self.expected_result:
508
- # Expecting TTP to find vulnerabilities/succeed
509
- return has_successes
510
- else:
511
- # Expecting TTP to fail (security controls working)
512
- return not has_successes
554
+ try:
555
+ # Execute step via API
556
+ response = self.ttp.execute_step_api(session, payload, context)
513
557
 
514
- except Exception as e:
515
- self.store_result('error', str(e))
516
- return False
558
+ # Verify result
559
+ result = self.ttp.verify_result_api(response, context)
560
+
561
+ ttp_results.append({
562
+ 'payload': str(payload),
563
+ 'success': result,
564
+ 'status_code': response.status_code,
565
+ 'url': response.url
566
+ })
567
+
568
+ if result:
569
+ success_count += 1
570
+
571
+ except Exception as e:
572
+ ttp_results.append({
573
+ 'payload': str(payload),
574
+ 'success': False,
575
+ 'error': str(e)
576
+ })
577
+
578
+ # Store results
579
+ self.store_result('ttp_name', self.ttp.name)
580
+ self.store_result('execution_mode', 'api')
581
+ self.store_result('total_payloads', total_count)
582
+ self.store_result('successful_payloads', success_count)
583
+ self.store_result('ttp_results', ttp_results)
584
+ self.store_result('success_rate', success_count / total_count if total_count > 0 else 0)
585
+
586
+ # Update context
587
+ context[f'ttp_results_{self.ttp.name}'] = ttp_results
588
+ context['last_ttp_success_count'] = success_count
589
+
590
+ # Determine action success based on expected result
591
+ has_successes = success_count > 0
592
+
593
+ if self.expected_result:
594
+ # Expecting TTP to find vulnerabilities/succeed
595
+ return has_successes
596
+ else:
597
+ # Expecting TTP to fail (security controls working)
598
+ return not has_successes
517
599
 
518
600
 
519
601
  class AssertAction(Action):