scythe-ttp 0.10.0__py3-none-any.whl → 0.11.0__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.11.0.dist-info}/METADATA +70 -1
- {scythe_ttp-0.10.0.dist-info → scythe_ttp-0.11.0.dist-info}/RECORD +9 -8
- {scythe_ttp-0.10.0.dist-info → scythe_ttp-0.11.0.dist-info}/WHEEL +0 -0
- {scythe_ttp-0.10.0.dist-info → scythe_ttp-0.11.0.dist-info}/licenses/LICENSE +0 -0
- {scythe_ttp-0.10.0.dist-info → scythe_ttp-0.11.0.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.11.0
|
|
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
|
|
|
@@ -443,6 +444,74 @@ executor = TTPExecutor(
|
|
|
443
444
|
)
|
|
444
445
|
```
|
|
445
446
|
|
|
447
|
+
### Version Detection
|
|
448
|
+
|
|
449
|
+
Scythe automatically captures the `X-SCYTHE-TARGET-VERSION` header from HTTP responses to track which version of your web application is being tested:
|
|
450
|
+
|
|
451
|
+
```python
|
|
452
|
+
from scythe.core.ttp import TTP
|
|
453
|
+
from scythe.core.executor import TTPExecutor
|
|
454
|
+
|
|
455
|
+
# Your web application should set this header:
|
|
456
|
+
# X-SCYTHE-TARGET-VERSION: 1.3.2
|
|
457
|
+
|
|
458
|
+
class MyTTP(TTP):
|
|
459
|
+
def get_payloads(self):
|
|
460
|
+
yield "test_payload"
|
|
461
|
+
|
|
462
|
+
def execute_step(self, driver, payload):
|
|
463
|
+
driver.get("http://your-app.com/login")
|
|
464
|
+
# ... test logic ...
|
|
465
|
+
|
|
466
|
+
def verify_result(self, driver):
|
|
467
|
+
return "welcome" in driver.page_source
|
|
468
|
+
|
|
469
|
+
# Run the test
|
|
470
|
+
ttp = MyTTP("Version Test", "Test with version detection")
|
|
471
|
+
executor = TTPExecutor(ttp=ttp, target_url="http://your-app.com")
|
|
472
|
+
executor.run()
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
**Output includes version information:**
|
|
476
|
+
```
|
|
477
|
+
✓ EXPECTED SUCCESS: 'test_payload' | Version: 1.3.2
|
|
478
|
+
Target Version Summary:
|
|
479
|
+
Results with version info: 1/1
|
|
480
|
+
Version 1.3.2: 1 result(s)
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Server-side implementation examples:**
|
|
484
|
+
```python
|
|
485
|
+
# Python/Flask
|
|
486
|
+
@app.after_request
|
|
487
|
+
def add_version_header(response):
|
|
488
|
+
response.headers['X-SCYTHE-TARGET-VERSION'] = '1.3.2'
|
|
489
|
+
return response
|
|
490
|
+
|
|
491
|
+
# Node.js/Express
|
|
492
|
+
app.use((req, res, next) => {
|
|
493
|
+
res.set('X-SCYTHE-TARGET-VERSION', '1.3.2');
|
|
494
|
+
next();
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
# Java/Spring Boot
|
|
498
|
+
@Component
|
|
499
|
+
public class VersionHeaderFilter implements Filter {
|
|
500
|
+
@Override
|
|
501
|
+
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
|
|
502
|
+
HttpServletResponse httpResponse = (HttpServletResponse) response;
|
|
503
|
+
httpResponse.setHeader("X-SCYTHE-TARGET-VERSION", "1.3.2");
|
|
504
|
+
chain.doFilter(request, response);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
This feature helps you:
|
|
510
|
+
- **Track test results** by application version
|
|
511
|
+
- **Verify deployment status** during testing
|
|
512
|
+
- **Correlate issues** with specific software versions
|
|
513
|
+
- **Ensure consistency** across test environments
|
|
514
|
+
|
|
446
515
|
### Custom Test Creation
|
|
447
516
|
|
|
448
517
|
Extend Scythe for specific testing needs:
|
|
@@ -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=U-IXYs1g0q1BB2W7fxfvuVwd3tPCRkBdF4_-Dj787w8,7833
|
|
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.11.0.dist-info/licenses/LICENSE,sha256=B7iB4Fv6zDQolC7IgqNF8F4GEp_DLe2jrPPuR_MYMOM,1064
|
|
33
|
+
scythe_ttp-0.11.0.dist-info/METADATA,sha256=6HnepGVg2VYQweeSJthYxiKQjZYZhbMY3KUgA5Zshmk,27473
|
|
34
|
+
scythe_ttp-0.11.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
35
|
+
scythe_ttp-0.11.0.dist-info/top_level.txt,sha256=BCKTrPuVvmLyhOR07C1ggOh6sU7g2LoVvwDMn46O55Y,7
|
|
36
|
+
scythe_ttp-0.11.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|