scythe-ttp 0.10.0__py3-none-any.whl → 0.12.1__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/core/executor.py +38 -5
- scythe/core/headers.py +194 -0
- scythe/journeys/base.py +21 -2
- scythe/journeys/executor.py +22 -1
- {scythe_ttp-0.10.0.dist-info → scythe_ttp-0.12.1.dist-info}/METADATA +103 -14
- {scythe_ttp-0.10.0.dist-info → scythe_ttp-0.12.1.dist-info}/RECORD +9 -8
- {scythe_ttp-0.10.0.dist-info → scythe_ttp-0.12.1.dist-info}/WHEEL +0 -0
- {scythe_ttp-0.10.0.dist-info → scythe_ttp-0.12.1.dist-info}/licenses/LICENSE +0 -0
- {scythe_ttp-0.10.0.dist-info → scythe_ttp-0.12.1.dist-info}/top_level.txt +0 -0
scythe/core/executor.py
CHANGED
|
@@ -5,6 +5,7 @@ from selenium.webdriver.chrome.options import Options
|
|
|
5
5
|
from .ttp import TTP
|
|
6
6
|
from typing import Optional
|
|
7
7
|
from ..behaviors.base import Behavior
|
|
8
|
+
from .headers import HeaderExtractor
|
|
8
9
|
|
|
9
10
|
# Configure logging
|
|
10
11
|
logging.basicConfig(
|
|
@@ -33,8 +34,12 @@ class TTPExecutor:
|
|
|
33
34
|
self.chrome_options.add_argument("--no-sandbox")
|
|
34
35
|
self.chrome_options.add_argument("--disable-dev-shm-usage")
|
|
35
36
|
|
|
37
|
+
# Enable header extraction capabilities
|
|
38
|
+
HeaderExtractor.enable_logging_for_driver(self.chrome_options)
|
|
39
|
+
|
|
36
40
|
self.driver = None
|
|
37
41
|
self.results = []
|
|
42
|
+
self.header_extractor = HeaderExtractor()
|
|
38
43
|
|
|
39
44
|
def _setup_driver(self):
|
|
40
45
|
"""Initializes the WebDriver."""
|
|
@@ -108,13 +113,27 @@ class TTPExecutor:
|
|
|
108
113
|
if success:
|
|
109
114
|
consecutive_failures = 0
|
|
110
115
|
current_url = self.driver.current_url if self.driver else "unknown"
|
|
111
|
-
|
|
116
|
+
|
|
117
|
+
# Extract target version header
|
|
118
|
+
target_version = None
|
|
119
|
+
if self.driver:
|
|
120
|
+
target_version = self.header_extractor.extract_target_version(self.driver, self.target_url)
|
|
121
|
+
|
|
122
|
+
result_entry = {
|
|
123
|
+
'payload': payload,
|
|
124
|
+
'url': current_url,
|
|
125
|
+
'expected': self.ttp.expected_result,
|
|
126
|
+
'actual': True,
|
|
127
|
+
'target_version': target_version
|
|
128
|
+
}
|
|
112
129
|
self.results.append(result_entry)
|
|
113
130
|
|
|
114
131
|
if self.ttp.expected_result:
|
|
115
|
-
|
|
132
|
+
version_info = f" | Version: {target_version}" if target_version else ""
|
|
133
|
+
self.logger.info(f"EXPECTED SUCCESS: '{payload}'{version_info}")
|
|
116
134
|
else:
|
|
117
|
-
|
|
135
|
+
version_info = f" | Version: {target_version}" if target_version else ""
|
|
136
|
+
self.logger.warning(f"UNEXPECTED SUCCESS: '{payload}' (expected to fail){version_info}")
|
|
118
137
|
else:
|
|
119
138
|
consecutive_failures += 1
|
|
120
139
|
if self.ttp.expected_result:
|
|
@@ -168,12 +187,26 @@ class TTPExecutor:
|
|
|
168
187
|
if expected_successes:
|
|
169
188
|
self.logger.info(f"Expected successes: {len(expected_successes)}")
|
|
170
189
|
for result in expected_successes:
|
|
171
|
-
|
|
190
|
+
version_info = f" | Version: {result['target_version']}" if result.get('target_version') else ""
|
|
191
|
+
self.logger.info(f" ✓ Payload: {result['payload']} | URL: {result['url']}{version_info}")
|
|
172
192
|
|
|
173
193
|
if unexpected_successes:
|
|
174
194
|
self.logger.warning(f"Unexpected successes: {len(unexpected_successes)}")
|
|
175
195
|
for result in unexpected_successes:
|
|
176
|
-
|
|
196
|
+
version_info = f" | Version: {result['target_version']}" if result.get('target_version') else ""
|
|
197
|
+
self.logger.warning(f" ✗ Payload: {result['payload']} | URL: {result['url']}{version_info}")
|
|
198
|
+
|
|
199
|
+
# Display version summary
|
|
200
|
+
version_summary = self.header_extractor.get_version_summary(self.results)
|
|
201
|
+
if version_summary['results_with_version'] > 0:
|
|
202
|
+
self.logger.info("\nTarget Version Summary:")
|
|
203
|
+
self.logger.info(f" Results with version info: {version_summary['results_with_version']}/{version_summary['total_results']}")
|
|
204
|
+
if version_summary['unique_versions']:
|
|
205
|
+
for version in version_summary['unique_versions']:
|
|
206
|
+
count = version_summary['version_counts'][version]
|
|
207
|
+
self.logger.info(f" Version {version}: {count} result(s)")
|
|
208
|
+
else:
|
|
209
|
+
self.logger.info("\nNo X-SCYTHE-TARGET-VERSION headers detected in responses.")
|
|
177
210
|
else:
|
|
178
211
|
if self.ttp.expected_result:
|
|
179
212
|
self.logger.info("No successes detected (expected to find vulnerabilities).")
|
scythe/core/headers.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional, Dict, Any
|
|
4
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
|
5
|
+
from selenium.webdriver.chrome.options import Options
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HeaderExtractor:
|
|
9
|
+
"""
|
|
10
|
+
Utility class for extracting HTTP response headers from WebDriver sessions.
|
|
11
|
+
|
|
12
|
+
Specifically designed to capture the X-SCYTHE-TARGET-VERSION header
|
|
13
|
+
that indicates the version of the web application being tested.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
SCYTHE_VERSION_HEADER = "X-Scythe-Target-Version"
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.logger = logging.getLogger("HeaderExtractor")
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def enable_logging_for_driver(chrome_options: Options) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Enable performance logging capabilities for Chrome WebDriver.
|
|
25
|
+
|
|
26
|
+
This must be called during WebDriver setup to capture network logs.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
chrome_options: Chrome options object to modify
|
|
30
|
+
"""
|
|
31
|
+
# Enable performance logging to capture network events
|
|
32
|
+
chrome_options.add_argument("--enable-logging")
|
|
33
|
+
chrome_options.add_argument("--log-level=0")
|
|
34
|
+
chrome_options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
|
|
35
|
+
|
|
36
|
+
def extract_target_version(self, driver: WebDriver, target_url: Optional[str] = None) -> Optional[str]:
|
|
37
|
+
"""
|
|
38
|
+
Extract the X-SCYTHE-TARGET-VERSION header from the most recent HTTP response.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
driver: WebDriver instance with performance logging enabled
|
|
42
|
+
target_url: Optional URL to filter responses for (if None, uses any response)
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Version string if header found, None otherwise
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
# Get performance logs - using getattr to handle type checking
|
|
49
|
+
if not hasattr(driver, 'get_log'):
|
|
50
|
+
self.logger.warning("WebDriver does not support get_log method")
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
logs = getattr(driver, 'get_log')('performance')
|
|
54
|
+
|
|
55
|
+
# Look for Network.responseReceived events
|
|
56
|
+
for log_entry in reversed(logs): # Start with most recent
|
|
57
|
+
try:
|
|
58
|
+
message = log_entry.get('message', {})
|
|
59
|
+
if isinstance(message, str):
|
|
60
|
+
message = json.loads(message)
|
|
61
|
+
|
|
62
|
+
method = message.get('message', {}).get('method', '')
|
|
63
|
+
params = message.get('message', {}).get('params', {})
|
|
64
|
+
|
|
65
|
+
if method == 'Network.responseReceived':
|
|
66
|
+
response = params.get('response', {})
|
|
67
|
+
headers = response.get('headers', {})
|
|
68
|
+
response_url = response.get('url', '')
|
|
69
|
+
|
|
70
|
+
# Filter by target URL if specified
|
|
71
|
+
if target_url and target_url not in response_url:
|
|
72
|
+
continue
|
|
73
|
+
|
|
74
|
+
# Look for the version header (case-insensitive)
|
|
75
|
+
version = self._find_version_header(headers)
|
|
76
|
+
if version:
|
|
77
|
+
self.logger.debug(f"Found target version '{version}' in response from {response_url}")
|
|
78
|
+
return version
|
|
79
|
+
|
|
80
|
+
except (json.JSONDecodeError, KeyError, AttributeError) as e:
|
|
81
|
+
self.logger.debug(f"Error parsing log entry: {e}")
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
self.logger.debug("No X-SCYTHE-TARGET-VERSION header found in network logs")
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
self.logger.warning(f"Failed to extract target version from logs: {e}")
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def _find_version_header(self, headers: Dict[str, Any]) -> Optional[str]:
|
|
92
|
+
"""
|
|
93
|
+
Find the version header in a case-insensitive manner.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
headers: Dictionary of HTTP headers
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Version string if found, None otherwise
|
|
100
|
+
"""
|
|
101
|
+
# Check for exact case match first
|
|
102
|
+
if self.SCYTHE_VERSION_HEADER in headers:
|
|
103
|
+
return str(headers[self.SCYTHE_VERSION_HEADER]).strip()
|
|
104
|
+
|
|
105
|
+
# Check case-insensitive
|
|
106
|
+
header_lower = self.SCYTHE_VERSION_HEADER.lower()
|
|
107
|
+
for header_name, header_value in headers.items():
|
|
108
|
+
if header_name.lower() == header_lower:
|
|
109
|
+
return str(header_value).strip()
|
|
110
|
+
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def extract_all_headers(self, driver: WebDriver, target_url: Optional[str] = None) -> Dict[str, str]:
|
|
114
|
+
"""
|
|
115
|
+
Extract all headers from the most recent HTTP response.
|
|
116
|
+
|
|
117
|
+
Useful for debugging or when additional headers might be needed.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
driver: WebDriver instance with performance logging enabled
|
|
121
|
+
target_url: Optional URL to filter responses for
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Dictionary of headers from the most recent response
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
# Get performance logs - using getattr to handle type checking
|
|
128
|
+
if not hasattr(driver, 'get_log'):
|
|
129
|
+
self.logger.warning("WebDriver does not support get_log method")
|
|
130
|
+
return {}
|
|
131
|
+
|
|
132
|
+
logs = getattr(driver, 'get_log')('performance')
|
|
133
|
+
|
|
134
|
+
for log_entry in reversed(logs):
|
|
135
|
+
try:
|
|
136
|
+
message = log_entry.get('message', {})
|
|
137
|
+
if isinstance(message, str):
|
|
138
|
+
message = json.loads(message)
|
|
139
|
+
|
|
140
|
+
method = message.get('message', {}).get('method', '')
|
|
141
|
+
params = message.get('message', {}).get('params', {})
|
|
142
|
+
|
|
143
|
+
if method == 'Network.responseReceived':
|
|
144
|
+
response = params.get('response', {})
|
|
145
|
+
headers = response.get('headers', {})
|
|
146
|
+
response_url = response.get('url', '')
|
|
147
|
+
|
|
148
|
+
# Filter by target URL if specified
|
|
149
|
+
if target_url and target_url not in response_url:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Convert all header values to strings
|
|
153
|
+
return {k: str(v) for k, v in headers.items()}
|
|
154
|
+
|
|
155
|
+
except (json.JSONDecodeError, KeyError, AttributeError):
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
return {}
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
self.logger.warning(f"Failed to extract headers from logs: {e}")
|
|
162
|
+
return {}
|
|
163
|
+
|
|
164
|
+
def get_version_summary(self, results: list) -> Dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
Analyze version information across multiple test results.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
results: List of result dictionaries containing version information
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Dictionary with version analysis summary
|
|
173
|
+
"""
|
|
174
|
+
versions = []
|
|
175
|
+
results_with_version = 0
|
|
176
|
+
|
|
177
|
+
for result in results:
|
|
178
|
+
version = result.get('target_version')
|
|
179
|
+
if version:
|
|
180
|
+
versions.append(version)
|
|
181
|
+
results_with_version += 1
|
|
182
|
+
|
|
183
|
+
summary = {
|
|
184
|
+
'total_results': len(results),
|
|
185
|
+
'results_with_version': results_with_version,
|
|
186
|
+
'unique_versions': list(set(versions)) if versions else [],
|
|
187
|
+
'version_counts': {}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Count occurrences of each version
|
|
191
|
+
for version in versions:
|
|
192
|
+
summary['version_counts'][version] = summary['version_counts'].get(version, 0) + 1
|
|
193
|
+
|
|
194
|
+
return summary
|
scythe/journeys/base.py
CHANGED
|
@@ -276,6 +276,10 @@ class Journey:
|
|
|
276
276
|
logger.info(f"Description: {self.description}")
|
|
277
277
|
logger.info(f"Steps: {len(self.steps)}")
|
|
278
278
|
|
|
279
|
+
# Import here to avoid circular imports
|
|
280
|
+
from ..core.headers import HeaderExtractor
|
|
281
|
+
header_extractor = HeaderExtractor()
|
|
282
|
+
|
|
279
283
|
start_time = time.time()
|
|
280
284
|
|
|
281
285
|
# Set initial context
|
|
@@ -297,7 +301,9 @@ class Journey:
|
|
|
297
301
|
'overall_success': False,
|
|
298
302
|
'execution_time': 0,
|
|
299
303
|
'step_results': [],
|
|
300
|
-
'errors': []
|
|
304
|
+
'errors': [],
|
|
305
|
+
'target_versions': [],
|
|
306
|
+
'version_summary': {}
|
|
301
307
|
}
|
|
302
308
|
|
|
303
309
|
try:
|
|
@@ -335,6 +341,12 @@ class Journey:
|
|
|
335
341
|
else:
|
|
336
342
|
results['actions_failed'] += 1
|
|
337
343
|
|
|
344
|
+
# Extract target version header after step execution
|
|
345
|
+
target_version = header_extractor.extract_target_version(driver, target_url)
|
|
346
|
+
if target_version:
|
|
347
|
+
results['target_versions'].append(target_version)
|
|
348
|
+
logger.info(f"Target version detected: {target_version}")
|
|
349
|
+
|
|
338
350
|
# Store step results
|
|
339
351
|
step_result = {
|
|
340
352
|
'step_name': step.name,
|
|
@@ -342,7 +354,8 @@ class Journey:
|
|
|
342
354
|
'expected': step.expected_result,
|
|
343
355
|
'actual': step_success,
|
|
344
356
|
'actions': step.execution_results.copy(),
|
|
345
|
-
'step_data': step.step_data.copy()
|
|
357
|
+
'step_data': step.step_data.copy(),
|
|
358
|
+
'target_version': target_version
|
|
346
359
|
}
|
|
347
360
|
results['step_results'].append(step_result)
|
|
348
361
|
|
|
@@ -368,6 +381,12 @@ class Journey:
|
|
|
368
381
|
results['end_time'] = end_time
|
|
369
382
|
results['execution_time'] = end_time - start_time
|
|
370
383
|
|
|
384
|
+
# Generate version summary
|
|
385
|
+
if results['target_versions']:
|
|
386
|
+
version_summary = header_extractor.get_version_summary([{'target_version': v} for v in results['target_versions']])
|
|
387
|
+
results['version_summary'] = version_summary
|
|
388
|
+
logger.info(f"Target versions detected: {list(set(results['target_versions']))}")
|
|
389
|
+
|
|
371
390
|
logger.info(f"Journey completed in {results['execution_time']:.2f} seconds")
|
|
372
391
|
logger.info(f"Overall success: {results['overall_success']}")
|
|
373
392
|
logger.info(f"Steps: {results['steps_succeeded']}/{results['steps_executed']} succeeded")
|
scythe/journeys/executor.py
CHANGED
|
@@ -5,6 +5,7 @@ from selenium.webdriver.chrome.options import Options
|
|
|
5
5
|
from typing import Optional, Dict, Any, List
|
|
6
6
|
from ..behaviors.base import Behavior
|
|
7
7
|
from .base import Journey
|
|
8
|
+
from ..core.headers import HeaderExtractor
|
|
8
9
|
|
|
9
10
|
# Configure logging
|
|
10
11
|
logging.basicConfig(
|
|
@@ -64,8 +65,12 @@ class JourneyExecutor:
|
|
|
64
65
|
else:
|
|
65
66
|
self.chrome_options.add_argument(f"--{key}={value}")
|
|
66
67
|
|
|
68
|
+
# Enable header extraction capabilities
|
|
69
|
+
HeaderExtractor.enable_logging_for_driver(self.chrome_options)
|
|
70
|
+
|
|
67
71
|
self.driver = None
|
|
68
72
|
self.execution_results = None
|
|
73
|
+
self.header_extractor = HeaderExtractor()
|
|
69
74
|
|
|
70
75
|
def _setup_driver(self):
|
|
71
76
|
"""Initialize the WebDriver."""
|
|
@@ -297,14 +302,30 @@ class JourneyExecutor:
|
|
|
297
302
|
step_success = step_result.get('actual', False)
|
|
298
303
|
step_expected = step_result.get('expected', True)
|
|
299
304
|
actions = step_result.get('actions', [])
|
|
305
|
+
target_version = step_result.get('target_version')
|
|
300
306
|
|
|
301
307
|
status = "✓" if step_success == step_expected else "✗"
|
|
302
308
|
result_text = "SUCCESS" if step_success else "FAILURE"
|
|
303
309
|
expected_text = "expected" if step_success == step_expected else "unexpected"
|
|
310
|
+
version_info = f" | Version: {target_version}" if target_version else ""
|
|
304
311
|
|
|
305
|
-
self.logger.info(f" {status} Step {i}: {step_name} - {result_text} ({expected_text})")
|
|
312
|
+
self.logger.info(f" {status} Step {i}: {step_name} - {result_text} ({expected_text}){version_info}")
|
|
306
313
|
self.logger.info(f" Actions: {len([a for a in actions if a.get('actual', False)])}/{len(actions)} succeeded")
|
|
307
314
|
|
|
315
|
+
# Version summary
|
|
316
|
+
target_versions = self.execution_results.get('target_versions', [])
|
|
317
|
+
|
|
318
|
+
if target_versions:
|
|
319
|
+
self.logger.info("\nTarget Version Summary:")
|
|
320
|
+
self.logger.info(f" Steps with version info: {len(target_versions)}/{len(step_results) if step_results else 0}")
|
|
321
|
+
unique_versions = list(set(target_versions))
|
|
322
|
+
if unique_versions:
|
|
323
|
+
for version in unique_versions:
|
|
324
|
+
count = target_versions.count(version)
|
|
325
|
+
self.logger.info(f" Version {version}: {count} step(s)")
|
|
326
|
+
else:
|
|
327
|
+
self.logger.info("\nNo X-SCYTHE-TARGET-VERSION headers detected in responses.")
|
|
328
|
+
|
|
308
329
|
self.logger.info("="*60)
|
|
309
330
|
|
|
310
331
|
def get_results(self) -> Optional[Dict[str, Any]]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scythe-ttp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.1
|
|
4
4
|
Summary: An extensible framework for emulating attacker TTPs with Selenium.
|
|
5
5
|
Home-page: https://github.com/EpykLab/scythe
|
|
6
6
|
Author: EpykLab
|
|
@@ -96,6 +96,7 @@ Scythe operates on the principle that robust systems must be tested under advers
|
|
|
96
96
|
### 📊 **Professional Reporting**
|
|
97
97
|
* **Clear Result Indicators**: ✓ Expected outcomes, ✗ Unexpected results
|
|
98
98
|
* **Comprehensive Logging**: Detailed execution tracking and analysis
|
|
99
|
+
* **Version Detection**: Automatic extraction of X-SCYTHE-TARGET-VERSION headers
|
|
99
100
|
* **Performance Metrics**: Timing, success rates, and resource utilization
|
|
100
101
|
* **Execution Statistics**: Detailed reporting across all test types
|
|
101
102
|
|
|
@@ -168,6 +169,26 @@ file_upload_ttp = FileUploadTTP(
|
|
|
168
169
|
|
|
169
170
|
### Installation
|
|
170
171
|
|
|
172
|
+
#### If you would like to use as a library:
|
|
173
|
+
|
|
174
|
+
setup the virtual environment
|
|
175
|
+
```bash
|
|
176
|
+
python3 -m venv venv
|
|
177
|
+
|
|
178
|
+
# source the venv
|
|
179
|
+
# bash,zsh: source venv/bin/activate
|
|
180
|
+
# fish: source venv/bin/activate.fish
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
install the package
|
|
184
|
+
```bash
|
|
185
|
+
# in an activated venv
|
|
186
|
+
|
|
187
|
+
pip3 install scythe-ttp
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
#### If you would like like to contribute:
|
|
191
|
+
|
|
171
192
|
1. Clone the repository:
|
|
172
193
|
```bash
|
|
173
194
|
git clone https://github.com/EpykLab/scythe.git
|
|
@@ -443,6 +464,74 @@ executor = TTPExecutor(
|
|
|
443
464
|
)
|
|
444
465
|
```
|
|
445
466
|
|
|
467
|
+
### Version Detection
|
|
468
|
+
|
|
469
|
+
Scythe automatically captures the `X-SCYTHE-TARGET-VERSION` header from HTTP responses to track which version of your web application is being tested:
|
|
470
|
+
|
|
471
|
+
```python
|
|
472
|
+
from scythe.core.ttp import TTP
|
|
473
|
+
from scythe.core.executor import TTPExecutor
|
|
474
|
+
|
|
475
|
+
# Your web application should set this header:
|
|
476
|
+
# X-SCYTHE-TARGET-VERSION: 1.3.2
|
|
477
|
+
|
|
478
|
+
class MyTTP(TTP):
|
|
479
|
+
def get_payloads(self):
|
|
480
|
+
yield "test_payload"
|
|
481
|
+
|
|
482
|
+
def execute_step(self, driver, payload):
|
|
483
|
+
driver.get("http://your-app.com/login")
|
|
484
|
+
# ... test logic ...
|
|
485
|
+
|
|
486
|
+
def verify_result(self, driver):
|
|
487
|
+
return "welcome" in driver.page_source
|
|
488
|
+
|
|
489
|
+
# Run the test
|
|
490
|
+
ttp = MyTTP("Version Test", "Test with version detection")
|
|
491
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://your-app.com")
|
|
492
|
+
executor.run()
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
**Output includes version information:**
|
|
496
|
+
```
|
|
497
|
+
✓ EXPECTED SUCCESS: 'test_payload' | Version: 1.3.2
|
|
498
|
+
Target Version Summary:
|
|
499
|
+
Results with version info: 1/1
|
|
500
|
+
Version 1.3.2: 1 result(s)
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Server-side implementation examples:**
|
|
504
|
+
```python
|
|
505
|
+
# Python/Flask
|
|
506
|
+
@app.after_request
|
|
507
|
+
def add_version_header(response):
|
|
508
|
+
response.headers['X-SCYTHE-TARGET-VERSION'] = '1.3.2'
|
|
509
|
+
return response
|
|
510
|
+
|
|
511
|
+
# Node.js/Express
|
|
512
|
+
app.use((req, res, next) => {
|
|
513
|
+
res.set('X-SCYTHE-TARGET-VERSION', '1.3.2');
|
|
514
|
+
next();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
# Java/Spring Boot
|
|
518
|
+
@Component
|
|
519
|
+
public class VersionHeaderFilter implements Filter {
|
|
520
|
+
@Override
|
|
521
|
+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
|
|
522
|
+
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
|
523
|
+
httpResponse.setHeader("X-SCYTHE-TARGET-VERSION", "1.3.2");
|
|
524
|
+
chain.doFilter(request, response);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
This feature helps you:
|
|
530
|
+
- **Track test results** by application version
|
|
531
|
+
- **Verify deployment status** during testing
|
|
532
|
+
- **Correlate issues** with specific software versions
|
|
533
|
+
- **Ensure consistency** across test environments
|
|
534
|
+
|
|
446
535
|
### Custom Test Creation
|
|
447
536
|
|
|
448
537
|
Extend Scythe for specific testing needs:
|
|
@@ -454,7 +543,7 @@ from typing import Generator, Any
|
|
|
454
543
|
|
|
455
544
|
class CustomBusinessLogicTTP(TTP):
|
|
456
545
|
"""Test specific business logic under adverse conditions."""
|
|
457
|
-
|
|
546
|
+
|
|
458
547
|
def __init__(self, business_scenarios: list, expected_result: bool = True):
|
|
459
548
|
super().__init__(
|
|
460
549
|
name="Business Logic Test",
|
|
@@ -462,28 +551,28 @@ class CustomBusinessLogicTTP(TTP):
|
|
|
462
551
|
expected_result=expected_result
|
|
463
552
|
)
|
|
464
553
|
self.scenarios = business_scenarios
|
|
465
|
-
|
|
554
|
+
|
|
466
555
|
def get_payloads(self) -> Generator[Any, None, None]:
|
|
467
556
|
for scenario in self.scenarios:
|
|
468
557
|
yield scenario
|
|
469
|
-
|
|
558
|
+
|
|
470
559
|
def execute_step(self, driver, payload):
|
|
471
560
|
# Implement your specific business logic testing
|
|
472
561
|
# This could involve API calls, database interactions, etc.
|
|
473
562
|
pass
|
|
474
|
-
|
|
563
|
+
|
|
475
564
|
def verify_result(self, driver) -> bool:
|
|
476
565
|
# Verify the business logic behaved correctly
|
|
477
566
|
return self.check_business_rules(driver)
|
|
478
567
|
|
|
479
568
|
class CustomWorkflowAction(Action):
|
|
480
569
|
"""Custom action for specific workflow steps."""
|
|
481
|
-
|
|
570
|
+
|
|
482
571
|
def __init__(self, workflow_step: str, parameters: dict):
|
|
483
572
|
super().__init__(f"Custom {workflow_step}", f"Execute {workflow_step}")
|
|
484
573
|
self.workflow_step = workflow_step
|
|
485
574
|
self.parameters = parameters
|
|
486
|
-
|
|
575
|
+
|
|
487
576
|
def execute(self, driver, context):
|
|
488
577
|
# Implement custom workflow logic
|
|
489
578
|
return self.perform_workflow_step(driver, context)
|
|
@@ -500,12 +589,12 @@ ecommerce_suite = [
|
|
|
500
589
|
payment_security_test, # Test payment form security
|
|
501
590
|
user_data_protection_test, # Test PII protection
|
|
502
591
|
session_management_test, # Test session security
|
|
503
|
-
|
|
592
|
+
|
|
504
593
|
# Load testing
|
|
505
594
|
product_catalog_load_test, # High-traffic product browsing
|
|
506
595
|
checkout_process_load_test, # Concurrent checkout processes
|
|
507
596
|
search_functionality_test, # Search under load
|
|
508
|
-
|
|
597
|
+
|
|
509
598
|
# Workflow testing
|
|
510
599
|
complete_purchase_journey, # End-to-end purchase flow
|
|
511
600
|
return_process_journey, # Product return workflow
|
|
@@ -578,30 +667,30 @@ def analyze_test_results(orchestration_result):
|
|
|
578
667
|
print("="*60)
|
|
579
668
|
print("COMPREHENSIVE TEST ANALYSIS")
|
|
580
669
|
print("="*60)
|
|
581
|
-
|
|
670
|
+
|
|
582
671
|
print(f"Total Executions: {orchestration_result.total_executions}")
|
|
583
672
|
print(f"Success Rate: {orchestration_result.success_rate:.1f}%")
|
|
584
673
|
print(f"Average Execution Time: {orchestration_result.average_execution_time:.2f}s")
|
|
585
|
-
|
|
674
|
+
|
|
586
675
|
# Performance metrics
|
|
587
676
|
if orchestration_result.metadata.get('performance_stats'):
|
|
588
677
|
stats = orchestration_result.metadata['performance_stats']
|
|
589
678
|
print(f"Peak Response Time: {stats.get('peak_response_time', 'N/A')}")
|
|
590
679
|
print(f"95th Percentile: {stats.get('p95_response_time', 'N/A')}")
|
|
591
|
-
|
|
680
|
+
|
|
592
681
|
# Geographic distribution (if applicable)
|
|
593
682
|
if orchestration_result.metadata.get('distribution_stats'):
|
|
594
683
|
dist = orchestration_result.metadata['distribution_stats']
|
|
595
684
|
print("Geographic Distribution:")
|
|
596
685
|
for location, count in dist.get('location_usage', {}).items():
|
|
597
686
|
print(f" {location}: {count} executions")
|
|
598
|
-
|
|
687
|
+
|
|
599
688
|
# Error analysis
|
|
600
689
|
if orchestration_result.errors:
|
|
601
690
|
print(f"\nErrors Encountered: {len(orchestration_result.errors)}")
|
|
602
691
|
for i, error in enumerate(orchestration_result.errors[:5], 1):
|
|
603
692
|
print(f" {i}. {error}")
|
|
604
|
-
|
|
693
|
+
|
|
605
694
|
print("="*60)
|
|
606
695
|
|
|
607
696
|
# Use with any orchestration result
|
|
@@ -10,12 +10,13 @@ scythe/behaviors/human.py,sha256=1PqYvE7cnxlj-KDmDIr3uzfWHvDAbbxQxJ0V0iTl9yo,102
|
|
|
10
10
|
scythe/behaviors/machine.py,sha256=NDMUq3mDhpCvujzAFxhn2eSVq78-J-LSBhIcvHkzKXo,4624
|
|
11
11
|
scythe/behaviors/stealth.py,sha256=xv7MrPQgRCdCUJyYTcXV2aasWZoAw8rAQWg-AuQVb7U,15278
|
|
12
12
|
scythe/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
-
scythe/core/executor.py,sha256=
|
|
13
|
+
scythe/core/executor.py,sha256=x1w2nByVu2G70sh7t0kOh6urlrTm_r_pbk0S7v1Ov28,9736
|
|
14
|
+
scythe/core/headers.py,sha256=1LznVigcyLepho4EL_Z5_EWjEcn9sJRSdHmROpf6_90,7326
|
|
14
15
|
scythe/core/ttp.py,sha256=Xw9GgptYsjZ-pMLdyPv64bhiwGKobrXHdF32pjIY7OU,3102
|
|
15
16
|
scythe/journeys/__init__.py,sha256=-8AIpCmkeWtQ656yU3omj_guMG4v4i1koIpD6NZhUGM,612
|
|
16
17
|
scythe/journeys/actions.py,sha256=Ez6Bpzs2VHzXMl6GtPve85XxzQV09rDscmDuzSs3VBE,25229
|
|
17
|
-
scythe/journeys/base.py,sha256=
|
|
18
|
-
scythe/journeys/executor.py,sha256=
|
|
18
|
+
scythe/journeys/base.py,sha256=BWf35Ee3N9qy76Awh-r04-waUTDfLyxssvDmwYToXgY,15461
|
|
19
|
+
scythe/journeys/executor.py,sha256=1D_HUzvi4Z7a5uE7QbIDWH7HTGz5DoxcQffr-05bi_0,19978
|
|
19
20
|
scythe/orchestrators/__init__.py,sha256=_vemcXjKbB1jI0F2dPA0F1zNsyUekjcXImLDUDhWDN0,560
|
|
20
21
|
scythe/orchestrators/base.py,sha256=YOZV0ewlzJ49H08P_LKnimutUms8NnDrQprFpSKhOeM,13595
|
|
21
22
|
scythe/orchestrators/batch.py,sha256=FpK501kk-earJzz6v7dcuw2y708rTvt_IMH_5qjKdrc,26635
|
|
@@ -28,8 +29,8 @@ scythe/ttps/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
28
29
|
scythe/ttps/web/login_bruteforce.py,sha256=D4G8zB_nU9LD5w3Vv2ABTuOl4XTeg2BgZwYMObt4JJw,2488
|
|
29
30
|
scythe/ttps/web/sql_injection.py,sha256=aWk4DFePbtFDsieOOj03Ux-5OiykyOs2_d_3SvWMOVE,2910
|
|
30
31
|
scythe/ttps/web/uuid_guessing.py,sha256=WwCIQPLIixd5U2EY4bhnj7YP2AQDaPfQy7Yhj84UHy8,1245
|
|
31
|
-
scythe_ttp-0.
|
|
32
|
-
scythe_ttp-0.
|
|
33
|
-
scythe_ttp-0.
|
|
34
|
-
scythe_ttp-0.
|
|
35
|
-
scythe_ttp-0.
|
|
32
|
+
scythe_ttp-0.12.1.dist-info/licenses/LICENSE,sha256=B7iB4Fv6zDQolC7IgqNF8F4GEp_DLe2jrPPuR_MYMOM,1064
|
|
33
|
+
scythe_ttp-0.12.1.dist-info/METADATA,sha256=hU3I4EUC7C1MdaPbNY6tSiDUSke2CXOmAMoZPVmLFfI,27741
|
|
34
|
+
scythe_ttp-0.12.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
35
|
+
scythe_ttp-0.12.1.dist-info/top_level.txt,sha256=BCKTrPuVvmLyhOR07C1ggOh6sU7g2LoVvwDMn46O55Y,7
|
|
36
|
+
scythe_ttp-0.12.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|