scythe-ttp 0.17.2__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.
- {scythe_ttp-0.17.2/scythe_ttp.egg-info → scythe_ttp-0.17.3}/PKG-INFO +1 -1
- scythe_ttp-0.17.3/VERSION +1 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/core/executor.py +116 -2
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3/scythe_ttp.egg-info}/PKG-INFO +1 -1
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe_ttp.egg-info/SOURCES.txt +1 -0
- scythe_ttp-0.17.3/tests/test_executor_modes.py +481 -0
- scythe_ttp-0.17.2/VERSION +0 -1
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/LICENSE +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/MANIFEST.in +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/README.md +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/pyproject.toml +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/requirements.txt +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/__init__.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/auth/__init__.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/auth/base.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/auth/basic.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/auth/bearer.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/auth/cookie_jwt.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/behaviors/__init__.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/behaviors/base.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/behaviors/default.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/behaviors/human.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/behaviors/machine.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/behaviors/stealth.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/cli/__init__.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/cli/main.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/core/__init__.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/core/headers.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/core/ttp.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/journeys/__init__.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/journeys/actions.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/journeys/base.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/journeys/executor.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/orchestrators/__init__.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/orchestrators/base.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/orchestrators/batch.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/orchestrators/distributed.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/orchestrators/scale.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/payloads/__init__.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/payloads/generators.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/ttps/__init__.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/ttps/web/__init__.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/ttps/web/login_bruteforce.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/ttps/web/sql_injection.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe/ttps/web/uuid_guessing.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe_ttp.egg-info/dependency_links.txt +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe_ttp.egg-info/entry_points.txt +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe_ttp.egg-info/requires.txt +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/scythe_ttp.egg-info/top_level.txt +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/setup.cfg +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_api_models.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_authentication.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_behaviors.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_cli.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_cookie_jwt_auth.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_expected_results.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_feature_completeness.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_header_extraction.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_journeys.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_orchestrators.py +0 -0
- {scythe_ttp-0.17.2 → scythe_ttp-0.17.3}/tests/test_ttp_api_mode.py +0 -0
|
@@ -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
|
-
|
|
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:
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive tests for TTPExecutor in both UI and API modes.
|
|
3
|
+
|
|
4
|
+
This test file ensures that TTPExecutor correctly handles both execution modes
|
|
5
|
+
and would have caught the bug where API mode was not properly implemented.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
# Add the scythe package to the path for testing
|
|
14
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|
15
|
+
|
|
16
|
+
from scythe.core.ttp import TTP
|
|
17
|
+
from scythe.core.executor import TTPExecutor
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MockTTPDualMode(TTP):
|
|
21
|
+
"""Mock TTP that supports both UI and API modes."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, execution_mode='ui', expected_result=True):
|
|
24
|
+
super().__init__(
|
|
25
|
+
name="Mock Dual Mode TTP",
|
|
26
|
+
description="Test TTP supporting both modes",
|
|
27
|
+
expected_result=expected_result,
|
|
28
|
+
execution_mode=execution_mode
|
|
29
|
+
)
|
|
30
|
+
self.ui_execute_called = False
|
|
31
|
+
self.ui_verify_called = False
|
|
32
|
+
self.api_execute_called = False
|
|
33
|
+
self.api_verify_called = False
|
|
34
|
+
self.payloads_list = ['payload1', 'payload2', 'payload3']
|
|
35
|
+
|
|
36
|
+
def get_payloads(self):
|
|
37
|
+
"""Yield test payloads."""
|
|
38
|
+
yield from self.payloads_list
|
|
39
|
+
|
|
40
|
+
def execute_step(self, driver, payload):
|
|
41
|
+
"""Mock UI execution step."""
|
|
42
|
+
self.ui_execute_called = True
|
|
43
|
+
|
|
44
|
+
def verify_result(self, driver):
|
|
45
|
+
"""Mock UI result verification."""
|
|
46
|
+
self.ui_verify_called = True
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
def execute_step_api(self, session, payload, context):
|
|
50
|
+
"""Mock API execution step."""
|
|
51
|
+
self.api_execute_called = True
|
|
52
|
+
|
|
53
|
+
# Create a mock response
|
|
54
|
+
mock_response = Mock()
|
|
55
|
+
mock_response.status_code = 200
|
|
56
|
+
mock_response.text = '{"success": true}'
|
|
57
|
+
mock_response.url = context.get('target_url', 'http://test.com')
|
|
58
|
+
mock_response.headers = {'X-SCYTHE-TARGET-VERSION': '1.0.0'}
|
|
59
|
+
|
|
60
|
+
return mock_response
|
|
61
|
+
|
|
62
|
+
def verify_result_api(self, response, context):
|
|
63
|
+
"""Mock API result verification."""
|
|
64
|
+
self.api_verify_called = True
|
|
65
|
+
return response.status_code == 200
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class MockTTPUIOnly(TTP):
|
|
69
|
+
"""Mock TTP that only supports UI mode (legacy behavior)."""
|
|
70
|
+
|
|
71
|
+
def __init__(self):
|
|
72
|
+
super().__init__(
|
|
73
|
+
name="Mock UI Only TTP",
|
|
74
|
+
description="Test TTP without API support",
|
|
75
|
+
execution_mode='ui'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def get_payloads(self):
|
|
79
|
+
yield 'payload1'
|
|
80
|
+
|
|
81
|
+
def execute_step(self, driver, payload):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
def verify_result(self, driver):
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestTTPExecutorUIMode(unittest.TestCase):
|
|
89
|
+
"""Test cases for TTPExecutor in UI mode."""
|
|
90
|
+
|
|
91
|
+
def setUp(self):
|
|
92
|
+
"""Set up test fixtures."""
|
|
93
|
+
self.mock_driver = Mock()
|
|
94
|
+
self.mock_driver.current_url = "http://test.com"
|
|
95
|
+
self.mock_driver.quit = Mock()
|
|
96
|
+
|
|
97
|
+
@patch('scythe.core.executor.webdriver.Chrome')
|
|
98
|
+
def test_ui_mode_initializes_webdriver(self, mock_webdriver):
|
|
99
|
+
"""Test that UI mode initializes WebDriver."""
|
|
100
|
+
mock_webdriver.return_value = self.mock_driver
|
|
101
|
+
|
|
102
|
+
ttp = MockTTPDualMode(execution_mode='ui')
|
|
103
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
104
|
+
|
|
105
|
+
with patch.object(executor, 'logger'):
|
|
106
|
+
executor.run()
|
|
107
|
+
|
|
108
|
+
# WebDriver should be initialized
|
|
109
|
+
mock_webdriver.assert_called_once()
|
|
110
|
+
|
|
111
|
+
@patch('scythe.core.executor.webdriver.Chrome')
|
|
112
|
+
def test_ui_mode_calls_ui_methods(self, mock_webdriver):
|
|
113
|
+
"""Test that UI mode calls execute_step and verify_result."""
|
|
114
|
+
mock_webdriver.return_value = self.mock_driver
|
|
115
|
+
|
|
116
|
+
ttp = MockTTPDualMode(execution_mode='ui')
|
|
117
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
118
|
+
|
|
119
|
+
with patch.object(executor, 'logger'):
|
|
120
|
+
executor.run()
|
|
121
|
+
|
|
122
|
+
# UI methods should be called
|
|
123
|
+
self.assertTrue(ttp.ui_execute_called)
|
|
124
|
+
self.assertTrue(ttp.ui_verify_called)
|
|
125
|
+
|
|
126
|
+
# API methods should NOT be called
|
|
127
|
+
self.assertFalse(ttp.api_execute_called)
|
|
128
|
+
self.assertFalse(ttp.api_verify_called)
|
|
129
|
+
|
|
130
|
+
@patch('scythe.core.executor.webdriver.Chrome')
|
|
131
|
+
def test_ui_mode_driver_cleanup(self, mock_webdriver):
|
|
132
|
+
"""Test that WebDriver is properly cleaned up in UI mode."""
|
|
133
|
+
mock_webdriver.return_value = self.mock_driver
|
|
134
|
+
|
|
135
|
+
ttp = MockTTPDualMode(execution_mode='ui')
|
|
136
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
137
|
+
|
|
138
|
+
with patch.object(executor, 'logger'):
|
|
139
|
+
executor.run()
|
|
140
|
+
|
|
141
|
+
# Driver should be quit
|
|
142
|
+
self.mock_driver.quit.assert_called_once()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class TestTTPExecutorAPIMode(unittest.TestCase):
|
|
146
|
+
"""Test cases for TTPExecutor in API mode."""
|
|
147
|
+
|
|
148
|
+
def test_api_mode_does_not_initialize_webdriver(self):
|
|
149
|
+
"""Test that API mode does NOT initialize WebDriver."""
|
|
150
|
+
ttp = MockTTPDualMode(execution_mode='api')
|
|
151
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
152
|
+
|
|
153
|
+
with patch('scythe.core.executor.webdriver.Chrome') as mock_webdriver:
|
|
154
|
+
with patch.object(executor, 'logger'):
|
|
155
|
+
executor.run()
|
|
156
|
+
|
|
157
|
+
# WebDriver should NOT be initialized in API mode
|
|
158
|
+
mock_webdriver.assert_not_called()
|
|
159
|
+
|
|
160
|
+
def test_api_mode_calls_api_methods(self):
|
|
161
|
+
"""Test that API mode calls execute_step_api and verify_result_api."""
|
|
162
|
+
ttp = MockTTPDualMode(execution_mode='api')
|
|
163
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
164
|
+
|
|
165
|
+
with patch.object(executor, 'logger'):
|
|
166
|
+
executor.run()
|
|
167
|
+
|
|
168
|
+
# API methods should be called
|
|
169
|
+
self.assertTrue(ttp.api_execute_called)
|
|
170
|
+
self.assertTrue(ttp.api_verify_called)
|
|
171
|
+
|
|
172
|
+
# UI methods should NOT be called
|
|
173
|
+
self.assertFalse(ttp.ui_execute_called)
|
|
174
|
+
self.assertFalse(ttp.ui_verify_called)
|
|
175
|
+
|
|
176
|
+
def test_api_mode_creates_session(self):
|
|
177
|
+
"""Test that API mode creates a requests.Session."""
|
|
178
|
+
ttp = MockTTPDualMode(execution_mode='api')
|
|
179
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
180
|
+
|
|
181
|
+
with patch('scythe.core.executor.requests.Session') as mock_session_class:
|
|
182
|
+
mock_session = Mock()
|
|
183
|
+
mock_session_class.return_value = mock_session
|
|
184
|
+
|
|
185
|
+
with patch.object(executor, 'logger'):
|
|
186
|
+
executor.run()
|
|
187
|
+
|
|
188
|
+
# Session should be created
|
|
189
|
+
mock_session_class.assert_called_once()
|
|
190
|
+
|
|
191
|
+
# Session should be closed
|
|
192
|
+
mock_session.close.assert_called_once()
|
|
193
|
+
|
|
194
|
+
def test_api_mode_processes_payloads(self):
|
|
195
|
+
"""Test that API mode processes all payloads."""
|
|
196
|
+
ttp = MockTTPDualMode(execution_mode='api')
|
|
197
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
198
|
+
|
|
199
|
+
with patch.object(executor, 'logger'):
|
|
200
|
+
executor.run()
|
|
201
|
+
|
|
202
|
+
# Should have processed all payloads
|
|
203
|
+
self.assertEqual(len(executor.results), 3)
|
|
204
|
+
|
|
205
|
+
def test_api_mode_context_setup(self):
|
|
206
|
+
"""Test that API mode sets up context correctly."""
|
|
207
|
+
ttp = MockTTPDualMode(execution_mode='api')
|
|
208
|
+
|
|
209
|
+
# Track the context passed to execute_step_api
|
|
210
|
+
captured_context = {}
|
|
211
|
+
|
|
212
|
+
def capture_context(session, payload, context):
|
|
213
|
+
captured_context.update(context)
|
|
214
|
+
return ttp.execute_step_api(session, payload, context)
|
|
215
|
+
|
|
216
|
+
with patch.object(ttp, 'execute_step_api', side_effect=capture_context):
|
|
217
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
218
|
+
|
|
219
|
+
with patch.object(executor, 'logger'):
|
|
220
|
+
executor.run()
|
|
221
|
+
|
|
222
|
+
# Context should have required keys
|
|
223
|
+
self.assertIn('target_url', captured_context)
|
|
224
|
+
self.assertEqual(captured_context['target_url'], 'http://test.com')
|
|
225
|
+
self.assertIn('auth_headers', captured_context)
|
|
226
|
+
self.assertIn('rate_limit_resume_at', captured_context)
|
|
227
|
+
|
|
228
|
+
def test_api_mode_extracts_version_header(self):
|
|
229
|
+
"""Test that API mode extracts X-SCYTHE-TARGET-VERSION header."""
|
|
230
|
+
ttp = MockTTPDualMode(execution_mode='api')
|
|
231
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
232
|
+
|
|
233
|
+
with patch.object(executor, 'logger'):
|
|
234
|
+
executor.run()
|
|
235
|
+
|
|
236
|
+
# Results should have version info
|
|
237
|
+
for result in executor.results:
|
|
238
|
+
self.assertIn('target_version', result)
|
|
239
|
+
self.assertEqual(result['target_version'], '1.0.0')
|
|
240
|
+
|
|
241
|
+
def test_api_mode_with_expected_results_true(self):
|
|
242
|
+
"""Test API mode with expected_result=True."""
|
|
243
|
+
ttp = MockTTPDualMode(execution_mode='api', expected_result=True)
|
|
244
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
245
|
+
|
|
246
|
+
with patch.object(executor, 'logger') as mock_logger:
|
|
247
|
+
executor.run()
|
|
248
|
+
|
|
249
|
+
# Should log expected successes
|
|
250
|
+
self.assertEqual(len(executor.results), 3)
|
|
251
|
+
for result in executor.results:
|
|
252
|
+
self.assertTrue(result['expected'])
|
|
253
|
+
self.assertTrue(result['actual'])
|
|
254
|
+
|
|
255
|
+
def test_api_mode_with_expected_results_false(self):
|
|
256
|
+
"""Test API mode with expected_result=False."""
|
|
257
|
+
ttp = MockTTPDualMode(execution_mode='api', expected_result=False)
|
|
258
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
259
|
+
|
|
260
|
+
with patch.object(executor, 'logger') as mock_logger:
|
|
261
|
+
executor.run()
|
|
262
|
+
|
|
263
|
+
# Should log unexpected successes
|
|
264
|
+
self.assertEqual(len(executor.results), 3)
|
|
265
|
+
for result in executor.results:
|
|
266
|
+
self.assertFalse(result['expected'])
|
|
267
|
+
self.assertTrue(result['actual'])
|
|
268
|
+
|
|
269
|
+
# Should mark as test failure
|
|
270
|
+
self.assertTrue(executor.has_test_failures)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class TestTTPExecutorModeSelection(unittest.TestCase):
|
|
274
|
+
"""Test cases for TTPExecutor mode selection logic."""
|
|
275
|
+
|
|
276
|
+
def test_executor_detects_ui_mode(self):
|
|
277
|
+
"""Test that executor detects UI mode from TTP."""
|
|
278
|
+
ttp = MockTTPDualMode(execution_mode='ui')
|
|
279
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
280
|
+
|
|
281
|
+
self.assertEqual(executor.ttp.execution_mode, 'ui')
|
|
282
|
+
|
|
283
|
+
def test_executor_detects_api_mode(self):
|
|
284
|
+
"""Test that executor detects API mode from TTP."""
|
|
285
|
+
ttp = MockTTPDualMode(execution_mode='api')
|
|
286
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
287
|
+
|
|
288
|
+
self.assertEqual(executor.ttp.execution_mode, 'api')
|
|
289
|
+
|
|
290
|
+
def test_executor_logs_execution_mode(self):
|
|
291
|
+
"""Test that executor logs the execution mode."""
|
|
292
|
+
ttp_ui = MockTTPDualMode(execution_mode='ui')
|
|
293
|
+
executor_ui = TTPExecutor(ttp=ttp_ui, target_url="http://test.com", headless=True)
|
|
294
|
+
|
|
295
|
+
with patch('scythe.core.executor.webdriver.Chrome') as mock_webdriver:
|
|
296
|
+
mock_driver = Mock()
|
|
297
|
+
mock_driver.current_url = "http://test.com"
|
|
298
|
+
mock_driver.quit = Mock()
|
|
299
|
+
mock_webdriver.return_value = mock_driver
|
|
300
|
+
|
|
301
|
+
with patch.object(executor_ui, 'logger') as mock_logger:
|
|
302
|
+
executor_ui.run()
|
|
303
|
+
|
|
304
|
+
# Should log UI mode
|
|
305
|
+
mock_logger.info.assert_any_call("Execution mode: UI")
|
|
306
|
+
|
|
307
|
+
ttp_api = MockTTPDualMode(execution_mode='api')
|
|
308
|
+
executor_api = TTPExecutor(ttp=ttp_api, target_url="http://test.com", headless=True)
|
|
309
|
+
|
|
310
|
+
with patch.object(executor_api, 'logger') as mock_logger:
|
|
311
|
+
executor_api.run()
|
|
312
|
+
|
|
313
|
+
# Should log API mode
|
|
314
|
+
mock_logger.info.assert_any_call("Execution mode: API")
|
|
315
|
+
|
|
316
|
+
def test_executor_default_mode_is_ui(self):
|
|
317
|
+
"""Test that default execution mode is UI."""
|
|
318
|
+
# TTP with no explicit mode should default to UI
|
|
319
|
+
ttp = MockTTPUIOnly()
|
|
320
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
321
|
+
|
|
322
|
+
self.assertEqual(executor.ttp.execution_mode, 'ui')
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class TestTTPExecutorAPIErrorHandling(unittest.TestCase):
|
|
326
|
+
"""Test cases for error handling in API mode."""
|
|
327
|
+
|
|
328
|
+
def test_api_mode_handles_request_exception(self):
|
|
329
|
+
"""Test that API mode handles request exceptions gracefully."""
|
|
330
|
+
ttp = MockTTPDualMode(execution_mode='api')
|
|
331
|
+
|
|
332
|
+
# Make execute_step_api raise an exception
|
|
333
|
+
def raise_exception(session, payload, context):
|
|
334
|
+
raise Exception("Network error")
|
|
335
|
+
|
|
336
|
+
with patch.object(ttp, 'execute_step_api', side_effect=raise_exception):
|
|
337
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
338
|
+
|
|
339
|
+
with patch.object(executor, 'logger') as mock_logger:
|
|
340
|
+
executor.run()
|
|
341
|
+
|
|
342
|
+
# Should log errors but not crash
|
|
343
|
+
self.assertTrue(any('Error during step' in str(call) for call in mock_logger.error.call_args_list))
|
|
344
|
+
|
|
345
|
+
def test_api_mode_continues_after_error(self):
|
|
346
|
+
"""Test that API mode continues processing after an error."""
|
|
347
|
+
ttp = MockTTPDualMode(execution_mode='api')
|
|
348
|
+
|
|
349
|
+
call_count = [0]
|
|
350
|
+
|
|
351
|
+
def fail_first_then_succeed(session, payload, context):
|
|
352
|
+
call_count[0] += 1
|
|
353
|
+
if call_count[0] == 1:
|
|
354
|
+
raise Exception("First call fails")
|
|
355
|
+
return ttp.execute_step_api(session, payload, context)
|
|
356
|
+
|
|
357
|
+
with patch.object(ttp, 'execute_step_api', side_effect=fail_first_then_succeed):
|
|
358
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
359
|
+
|
|
360
|
+
with patch.object(executor, 'logger'):
|
|
361
|
+
executor.run()
|
|
362
|
+
|
|
363
|
+
# Should have processed remaining payloads after error
|
|
364
|
+
self.assertEqual(len(executor.results), 2) # 3 payloads - 1 error = 2 results
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
class TestTTPExecutorAuthenticationAPIMode(unittest.TestCase):
|
|
368
|
+
"""Test cases for authentication in API mode."""
|
|
369
|
+
|
|
370
|
+
def test_api_mode_applies_auth_headers(self):
|
|
371
|
+
"""Test that API mode applies authentication headers."""
|
|
372
|
+
from scythe.auth.base import Authentication
|
|
373
|
+
|
|
374
|
+
# Create mock authentication
|
|
375
|
+
mock_auth = Mock(spec=Authentication)
|
|
376
|
+
mock_auth.name = "Test Auth"
|
|
377
|
+
mock_auth.get_auth_headers.return_value = {'Authorization': 'Bearer token123'}
|
|
378
|
+
|
|
379
|
+
ttp = MockTTPDualMode(execution_mode='api')
|
|
380
|
+
ttp.authentication = mock_auth
|
|
381
|
+
|
|
382
|
+
captured_session = None
|
|
383
|
+
|
|
384
|
+
def capture_session(session, payload, context):
|
|
385
|
+
nonlocal captured_session
|
|
386
|
+
captured_session = session
|
|
387
|
+
return ttp.execute_step_api(session, payload, context)
|
|
388
|
+
|
|
389
|
+
with patch.object(ttp, 'execute_step_api', side_effect=capture_session):
|
|
390
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
391
|
+
|
|
392
|
+
with patch.object(executor, 'logger'):
|
|
393
|
+
executor.run()
|
|
394
|
+
|
|
395
|
+
# Auth headers should be called
|
|
396
|
+
mock_auth.get_auth_headers.assert_called()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
class TestTTPExecutorWasSuccessfulAPIMode(unittest.TestCase):
|
|
400
|
+
"""Test was_successful() method in API mode."""
|
|
401
|
+
|
|
402
|
+
def test_was_successful_true_when_results_match_expectations(self):
|
|
403
|
+
"""Test was_successful returns True when API results match expectations."""
|
|
404
|
+
ttp = MockTTPDualMode(execution_mode='api', expected_result=True)
|
|
405
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
406
|
+
|
|
407
|
+
with patch.object(executor, 'logger'):
|
|
408
|
+
executor.run()
|
|
409
|
+
|
|
410
|
+
# Should be successful since results matched expectations
|
|
411
|
+
self.assertTrue(executor.was_successful())
|
|
412
|
+
|
|
413
|
+
def test_was_successful_false_when_unexpected_success(self):
|
|
414
|
+
"""Test was_successful returns False with unexpected successes in API mode."""
|
|
415
|
+
ttp = MockTTPDualMode(execution_mode='api', expected_result=False)
|
|
416
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://test.com", headless=True)
|
|
417
|
+
|
|
418
|
+
with patch.object(executor, 'logger'):
|
|
419
|
+
executor.run()
|
|
420
|
+
|
|
421
|
+
# Should NOT be successful since we got unexpected successes
|
|
422
|
+
self.assertFalse(executor.was_successful())
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class TestTTPExecutorBothModes(unittest.TestCase):
|
|
426
|
+
"""Test cases comparing UI and API mode behavior."""
|
|
427
|
+
|
|
428
|
+
@patch('scythe.core.executor.webdriver.Chrome')
|
|
429
|
+
def test_both_modes_process_same_payloads(self, mock_webdriver):
|
|
430
|
+
"""Test that both modes process the same payloads."""
|
|
431
|
+
mock_driver = Mock()
|
|
432
|
+
mock_driver.current_url = "http://test.com"
|
|
433
|
+
mock_driver.quit = Mock()
|
|
434
|
+
mock_webdriver.return_value = mock_driver
|
|
435
|
+
|
|
436
|
+
# UI mode
|
|
437
|
+
ttp_ui = MockTTPDualMode(execution_mode='ui')
|
|
438
|
+
executor_ui = TTPExecutor(ttp=ttp_ui, target_url="http://test.com", headless=True)
|
|
439
|
+
|
|
440
|
+
with patch.object(executor_ui, 'logger'):
|
|
441
|
+
executor_ui.run()
|
|
442
|
+
|
|
443
|
+
# API mode
|
|
444
|
+
ttp_api = MockTTPDualMode(execution_mode='api')
|
|
445
|
+
executor_api = TTPExecutor(ttp=ttp_api, target_url="http://test.com", headless=True)
|
|
446
|
+
|
|
447
|
+
with patch.object(executor_api, 'logger'):
|
|
448
|
+
executor_api.run()
|
|
449
|
+
|
|
450
|
+
# Both should process same number of results
|
|
451
|
+
self.assertEqual(len(executor_ui.results), len(executor_api.results))
|
|
452
|
+
|
|
453
|
+
@patch('scythe.core.executor.webdriver.Chrome')
|
|
454
|
+
def test_both_modes_respect_expected_result(self, mock_webdriver):
|
|
455
|
+
"""Test that both modes respect expected_result setting."""
|
|
456
|
+
mock_driver = Mock()
|
|
457
|
+
mock_driver.current_url = "http://test.com"
|
|
458
|
+
mock_driver.quit = Mock()
|
|
459
|
+
mock_webdriver.return_value = mock_driver
|
|
460
|
+
|
|
461
|
+
# UI mode with expected_result=False
|
|
462
|
+
ttp_ui = MockTTPDualMode(execution_mode='ui', expected_result=False)
|
|
463
|
+
executor_ui = TTPExecutor(ttp=ttp_ui, target_url="http://test.com", headless=True)
|
|
464
|
+
|
|
465
|
+
with patch.object(executor_ui, 'logger'):
|
|
466
|
+
executor_ui.run()
|
|
467
|
+
|
|
468
|
+
# API mode with expected_result=False
|
|
469
|
+
ttp_api = MockTTPDualMode(execution_mode='api', expected_result=False)
|
|
470
|
+
executor_api = TTPExecutor(ttp=ttp_api, target_url="http://test.com", headless=True)
|
|
471
|
+
|
|
472
|
+
with patch.object(executor_api, 'logger'):
|
|
473
|
+
executor_api.run()
|
|
474
|
+
|
|
475
|
+
# Both should have test failures (unexpected successes)
|
|
476
|
+
self.assertTrue(executor_ui.has_test_failures)
|
|
477
|
+
self.assertTrue(executor_api.has_test_failures)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
if __name__ == '__main__':
|
|
481
|
+
unittest.main()
|
scythe_ttp-0.17.2/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.17.2
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|