scythe-ttp 0.17.2__py3-none-any.whl → 0.17.4__py3-none-any.whl
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/auth/cookie_jwt.py +12 -5
- scythe/core/executor.py +116 -2
- {scythe_ttp-0.17.2.dist-info → scythe_ttp-0.17.4.dist-info}/METADATA +1 -1
- {scythe_ttp-0.17.2.dist-info → scythe_ttp-0.17.4.dist-info}/RECORD +8 -8
- {scythe_ttp-0.17.2.dist-info → scythe_ttp-0.17.4.dist-info}/WHEEL +0 -0
- {scythe_ttp-0.17.2.dist-info → scythe_ttp-0.17.4.dist-info}/entry_points.txt +0 -0
- {scythe_ttp-0.17.2.dist-info → scythe_ttp-0.17.4.dist-info}/licenses/LICENSE +0 -0
- {scythe_ttp-0.17.2.dist-info → scythe_ttp-0.17.4.dist-info}/top_level.txt +0 -0
scythe/auth/cookie_jwt.py
CHANGED
|
@@ -53,6 +53,10 @@ class CookieJWTAuth(Authentication):
|
|
|
53
53
|
token via jwt_json_path, and 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.
|
|
56
60
|
"""
|
|
57
61
|
|
|
58
62
|
def __init__(self,
|
|
@@ -64,6 +68,7 @@ class CookieJWTAuth(Authentication):
|
|
|
64
68
|
extra_fields: Optional[Dict[str, Any]] = None,
|
|
65
69
|
jwt_json_path: str = "token",
|
|
66
70
|
cookie_name: str = "stellarbridge",
|
|
71
|
+
content_type: str = "json",
|
|
67
72
|
session: Optional[requests.Session] = None,
|
|
68
73
|
description: str = "Authenticate via API and set JWT cookie"):
|
|
69
74
|
super().__init__(
|
|
@@ -78,18 +83,20 @@ class CookieJWTAuth(Authentication):
|
|
|
78
83
|
self.extra_fields = extra_fields or {}
|
|
79
84
|
self.jwt_json_path = jwt_json_path
|
|
80
85
|
self.cookie_name = cookie_name
|
|
86
|
+
self.content_type = content_type
|
|
81
87
|
# Avoid importing requests in test environments; allow injected session
|
|
82
88
|
self._session = session or (requests.Session() if requests is not None else None)
|
|
83
89
|
self.token: Optional[str] = None
|
|
84
90
|
|
|
85
91
|
def _login_and_get_token(self) -> str:
|
|
86
92
|
payload: Dict[str, Any] = dict(self.extra_fields)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if self.password is not None:
|
|
90
|
-
payload[self.password_field] = self.password
|
|
93
|
+
payload[self.username_field] = self.username
|
|
94
|
+
payload[self.password_field] = self.password
|
|
91
95
|
try:
|
|
92
|
-
|
|
96
|
+
if self.content_type == "form":
|
|
97
|
+
resp = self._session.post(self.login_url, data=payload, timeout=15)
|
|
98
|
+
else:
|
|
99
|
+
resp = self._session.post(self.login_url, json=payload, timeout=15)
|
|
93
100
|
# try json; raise on non-2xx to surface errors
|
|
94
101
|
resp.raise_for_status()
|
|
95
102
|
data = resp.json()
|
scythe/core/executor.py
CHANGED
|
@@ -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:
|
|
@@ -3,7 +3,7 @@ scythe/auth/__init__.py,sha256=InEANqWEIAULFyzH9IyxWDPs_gJd3m_JYmzoaBk_37M,420
|
|
|
3
3
|
scythe/auth/base.py,sha256=DllKaPGj0MRyRh4PQgQ2TUFgeAXjgXOT2h6zUz2ZAag,3807
|
|
4
4
|
scythe/auth/basic.py,sha256=H4IG9-Y7wFe7ZQCNHmmqhre-Pp9CnBxlT23h2uvOPWo,14354
|
|
5
5
|
scythe/auth/bearer.py,sha256=ngOL-sS6FcfB8XAKMR-CZbpqyySu2MaKxUl10SyBmmI,12687
|
|
6
|
-
scythe/auth/cookie_jwt.py,sha256=
|
|
6
|
+
scythe/auth/cookie_jwt.py,sha256=YJP40A74hYrKSFXDcMSfmWlouOc_tPI4321UAswvRa4,6652
|
|
7
7
|
scythe/behaviors/__init__.py,sha256=w-WRBGRgna5a1N8oHP2aXSQnkQUHyOXiujpwEVf_ZyM,291
|
|
8
8
|
scythe/behaviors/base.py,sha256=INvIYKVIWzEi5w_4njOwKZ3X9IvySvqiMJnYX7_2Lns,3955
|
|
9
9
|
scythe/behaviors/default.py,sha256=MDx4N-KwC23pPLGu1-ZIkGiTRNUG3Lxjbvo7SJ3UwMc,2117
|
|
@@ -13,7 +13,7 @@ scythe/behaviors/stealth.py,sha256=xv7MrPQgRCdCUJyYTcXV2aasWZoAw8rAQWg-AuQVb7U,1
|
|
|
13
13
|
scythe/cli/__init__.py,sha256=9EVxmFiWsAoqWJ6br1bc3BxlA71JyOQP28fUHhX2k7E,43
|
|
14
14
|
scythe/cli/main.py,sha256=DFvOB39tX4FeiOxitJwXfq28J13GjewBZ9gOA_0HOjI,26003
|
|
15
15
|
scythe/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
-
scythe/core/executor.py,sha256=
|
|
16
|
+
scythe/core/executor.py,sha256=FTuhvWiNOsN1LZ4scEsVGDXx9D3yF3tiAoDt-HW2QRI,16038
|
|
17
17
|
scythe/core/headers.py,sha256=AokCQ3F5QGUcfoK7iO57hA1HHL4IznZeWV464_MqYcE,16670
|
|
18
18
|
scythe/core/ttp.py,sha256=tEYIhDdr8kcwQrlcfVmdeLFiAfOvc0BhPOVxPh8TiWo,5676
|
|
19
19
|
scythe/journeys/__init__.py,sha256=Odi8NhRg7Hefmo1EJj1guakrCSPhsuus4i-_62uUUjs,654
|
|
@@ -32,9 +32,9 @@ scythe/ttps/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
32
32
|
scythe/ttps/web/login_bruteforce.py,sha256=ybmN2Vl9-p58YbOchirY1193GvlaTmUTW1qlluN_l3I,7816
|
|
33
33
|
scythe/ttps/web/sql_injection.py,sha256=rIRHaRUilSrMA5q5MO1RqR6-TM_fRIiCanPaFz5wKKs,11712
|
|
34
34
|
scythe/ttps/web/uuid_guessing.py,sha256=JwNt_9HVynMWFPPU6UGJFcpxvMVDsvc_wAnJVtcYbps,1235
|
|
35
|
-
scythe_ttp-0.17.
|
|
36
|
-
scythe_ttp-0.17.
|
|
37
|
-
scythe_ttp-0.17.
|
|
38
|
-
scythe_ttp-0.17.
|
|
39
|
-
scythe_ttp-0.17.
|
|
40
|
-
scythe_ttp-0.17.
|
|
35
|
+
scythe_ttp-0.17.4.dist-info/licenses/LICENSE,sha256=B7iB4Fv6zDQolC7IgqNF8F4GEp_DLe2jrPPuR_MYMOM,1064
|
|
36
|
+
scythe_ttp-0.17.4.dist-info/METADATA,sha256=GxFEimDo4r_WDY2KHy26MvpRJZJmM07DKV9q2wfioCw,30188
|
|
37
|
+
scythe_ttp-0.17.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
38
|
+
scythe_ttp-0.17.4.dist-info/entry_points.txt,sha256=rAAsFBcCm0OX3I4uRyclfx4YJGoTuumZKY43HN7R5Ro,48
|
|
39
|
+
scythe_ttp-0.17.4.dist-info/top_level.txt,sha256=BCKTrPuVvmLyhOR07C1ggOh6sU7g2LoVvwDMn46O55Y,7
|
|
40
|
+
scythe_ttp-0.17.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|