scythe-ttp 0.17.6__tar.gz → 0.18.1__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.
Files changed (63) hide show
  1. {scythe_ttp-0.17.6/scythe_ttp.egg-info → scythe_ttp-0.18.1}/PKG-INFO +1 -1
  2. scythe_ttp-0.18.1/VERSION +1 -0
  3. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/cli/main.py +28 -16
  4. scythe_ttp-0.18.1/scythe/ttps/web/__init__.py +12 -0
  5. scythe_ttp-0.18.1/scythe/ttps/web/request_flooding.py +503 -0
  6. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1/scythe_ttp.egg-info}/PKG-INFO +1 -1
  7. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/SOURCES.txt +1 -0
  8. scythe_ttp-0.17.6/VERSION +0 -1
  9. scythe_ttp-0.17.6/scythe/ttps/web/__init__.py +0 -0
  10. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/LICENSE +0 -0
  11. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/MANIFEST.in +0 -0
  12. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/README.md +0 -0
  13. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/pyproject.toml +0 -0
  14. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/requirements.txt +0 -0
  15. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/__init__.py +0 -0
  16. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/auth/__init__.py +0 -0
  17. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/auth/base.py +0 -0
  18. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/auth/basic.py +0 -0
  19. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/auth/bearer.py +0 -0
  20. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/auth/cookie_jwt.py +0 -0
  21. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/__init__.py +0 -0
  22. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/base.py +0 -0
  23. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/default.py +0 -0
  24. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/human.py +0 -0
  25. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/machine.py +0 -0
  26. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/behaviors/stealth.py +0 -0
  27. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/cli/__init__.py +0 -0
  28. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/core/__init__.py +0 -0
  29. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/core/executor.py +0 -0
  30. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/core/headers.py +0 -0
  31. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/core/ttp.py +0 -0
  32. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/journeys/__init__.py +0 -0
  33. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/journeys/actions.py +0 -0
  34. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/journeys/base.py +0 -0
  35. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/journeys/executor.py +0 -0
  36. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/orchestrators/__init__.py +0 -0
  37. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/orchestrators/base.py +0 -0
  38. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/orchestrators/batch.py +0 -0
  39. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/orchestrators/distributed.py +0 -0
  40. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/orchestrators/scale.py +0 -0
  41. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/payloads/__init__.py +0 -0
  42. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/payloads/generators.py +0 -0
  43. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/ttps/__init__.py +0 -0
  44. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/ttps/web/login_bruteforce.py +0 -0
  45. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/ttps/web/sql_injection.py +0 -0
  46. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe/ttps/web/uuid_guessing.py +0 -0
  47. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/dependency_links.txt +0 -0
  48. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/entry_points.txt +0 -0
  49. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/requires.txt +0 -0
  50. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/scythe_ttp.egg-info/top_level.txt +0 -0
  51. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/setup.cfg +0 -0
  52. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_api_models.py +0 -0
  53. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_authentication.py +0 -0
  54. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_behaviors.py +0 -0
  55. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_cli.py +0 -0
  56. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_cookie_jwt_auth.py +0 -0
  57. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_executor_modes.py +0 -0
  58. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_expected_results.py +0 -0
  59. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_feature_completeness.py +0 -0
  60. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_header_extraction.py +0 -0
  61. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_journeys.py +0 -0
  62. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_orchestrators.py +0 -0
  63. {scythe_ttp-0.17.6 → scythe_ttp-0.18.1}/tests/test_ttp_api_mode.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scythe-ttp
3
- Version: 0.17.6
3
+ Version: 0.18.1
4
4
  Summary: An extensible framework for emulating attacker TTPs with Selenium.
5
5
  Author-email: EpykLab <cyber@epyklab.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -0,0 +1 @@
1
+ 0.18.1
@@ -65,14 +65,14 @@ def check_version_in_response_header(args) -> bool:
65
65
  return False
66
66
  return True
67
67
 
68
- def scythe_test_definition(args) -> bool:
68
+ def scythe_test_definition(args) -> int:
69
69
  # TODO: implement your test using Scythe primitives.
70
70
  # Example placeholder that simply passes.
71
-
71
+
72
72
  # Example usage with TTPExecutor:
73
73
  # from scythe.core.executor import TTPExecutor
74
74
  # from scythe.ttps.web.login_bruteforce import LoginBruteforceTTP
75
- #
75
+ #
76
76
  # ttp = LoginBruteforceTTP(
77
77
  # payloads=['admin', 'root', 'test'],
78
78
  # expected_result=False # Expect security controls to block attempts
@@ -80,12 +80,12 @@ def scythe_test_definition(args) -> bool:
80
80
  # executor = TTPExecutor(ttp=ttp, target_url=args.url)
81
81
  # executor.run()
82
82
  # return executor.was_successful() # Returns True if all results matched expectations
83
-
83
+
84
84
  # Example usage with JourneyExecutor:
85
85
  # from scythe.journeys.executor import JourneyExecutor
86
86
  # from scythe.journeys.base import Journey, Step
87
87
  # from scythe.journeys.actions import NavigateAction, FillFormAction, ClickAction
88
- #
88
+ #
89
89
  # journey = Journey(
90
90
  # name="Login Journey",
91
91
  # description="Test user login flow",
@@ -95,19 +95,19 @@ def scythe_test_definition(args) -> bool:
95
95
  # executor = JourneyExecutor(journey=journey, target_url=args.url)
96
96
  # executor.run()
97
97
  # return executor.was_successful() # Returns True if journey succeeded as expected
98
-
98
+
99
99
  # Example usage with Orchestrators:
100
100
  # from scythe.orchestrators.scale import ScaleOrchestrator
101
101
  # from scythe.orchestrators.base import OrchestrationStrategy
102
- #
102
+ #
103
103
  # orchestrator = ScaleOrchestrator(
104
104
  # strategy=OrchestrationStrategy.PARALLEL,
105
105
  # max_workers=10
106
106
  # )
107
107
  # result = orchestrator.orchestrate_ttp(ttp=my_ttp, target_url=args.url, replications=100)
108
108
  # return orchestrator.exit_code(result) == 0 # Returns True if all executions succeeded
109
-
110
- return True
109
+
110
+ return executor.exit_code() # assumes executor var
111
111
 
112
112
 
113
113
  def main():
@@ -259,14 +259,14 @@ def main():
259
259
  if check_url_available(args.url):
260
260
  if args.gate_versions:
261
261
  if check_version_in_response_header(args):
262
- ok = scythe_test_definition(args)
263
- sys.exit(0 if ok else 1)
262
+ exit_code = scythe_test_definition(args)
263
+ sys.exit(exit_code)
264
264
  else:
265
265
  print("No compatible version found in response header.")
266
266
  sys.exit(1)
267
267
  else:
268
- ok = scythe_test_definition(args)
269
- sys.exit(0 if ok else 1)
268
+ exit_code = scythe_test_definition(args)
269
+ sys.exit(exit_code)
270
270
  else:
271
271
  print("URL not available.")
272
272
  sys.exit(1)
@@ -280,6 +280,13 @@ class ScytheCLIError(Exception):
280
280
  pass
281
281
 
282
282
 
283
+ class ExitWithCode(Exception):
284
+ """Exception to exit with a specific code from within Typer commands."""
285
+ def __init__(self, code: int):
286
+ self.code = code
287
+ super().__init__()
288
+
289
+
283
290
  def _find_project_root(start: Optional[str] = None) -> Optional[str]:
284
291
  """Walk upwards from start (or cwd) to find a directory containing .scythe."""
285
292
  cur = os.path.abspath(start or os.getcwd())
@@ -728,7 +735,10 @@ def main(argv: Optional[List[str]] = None) -> int:
728
735
  code, output, version = _run_test(project_root, name, extra)
729
736
  _record_run(project_root, name, code, output, version)
730
737
  print(output)
731
- return code
738
+ # Raise exception to propagate exit code through Typer
739
+ if code != 0:
740
+ raise ExitWithCode(code)
741
+ return 0
732
742
 
733
743
  db_app = typer.Typer(
734
744
  no_args_is_help=True,
@@ -763,8 +773,10 @@ def main(argv: Optional[List[str]] = None) -> int:
763
773
  app.add_typer(db_app, name="db")
764
774
 
765
775
  try:
766
- rv = app()
767
- return int(rv) if isinstance(rv, int) else 0
776
+ app()
777
+ return 0
778
+ except ExitWithCode as e:
779
+ return e.code
768
780
  except ScytheCLIError as e:
769
781
  print(f"Error: {e}", file=sys.stderr)
770
782
  return 2
@@ -0,0 +1,12 @@
1
+ from .login_bruteforce import LoginBruteforceTTP
2
+ from .sql_injection import InputFieldInjector, URLManipulation
3
+ from .uuid_guessing import GuessUUIDInURL
4
+ from .request_flooding import RequestFloodingTTP
5
+
6
+ __all__ = [
7
+ 'LoginBruteforceTTP',
8
+ 'InputFieldInjector',
9
+ 'URLManipulation',
10
+ 'GuessUUIDInURL',
11
+ 'RequestFloodingTTP'
12
+ ]
@@ -0,0 +1,503 @@
1
+ from selenium.webdriver.common.by import By
2
+ from selenium.webdriver.remote.webdriver import WebDriver
3
+ from selenium.common.exceptions import NoSuchElementException, TimeoutException
4
+ from typing import Dict, Any, Optional, List, Generator
5
+ import requests
6
+ import time
7
+ import threading
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ import random
10
+
11
+ from ...core.ttp import TTP
12
+
13
+
14
+ class RequestFloodingTTP(TTP):
15
+ """
16
+ A TTP that emulates DDoS and request flooding attacks to test application resilience.
17
+
18
+ This TTP tests an application's ability to withstand high-volume request attacks
19
+ and rate limiting mechanisms by sending multiple rapid requests to target endpoints.
20
+
21
+ Supports two execution modes:
22
+ - UI mode: Uses Selenium to repeatedly interact with web pages/forms
23
+ - API mode: Makes rapid HTTP requests to API endpoints
24
+
25
+ Attack patterns include:
26
+ - Volume flooding: High number of requests in short time
27
+ - Slowloris-style: Slow, prolonged connections
28
+ - Burst flooding: Intermittent bursts of high traffic
29
+ - Resource exhaustion: Targeting expensive operations
30
+ """
31
+
32
+ def __init__(self,
33
+ target_endpoints: List[str] = None,
34
+ request_count: int = 100,
35
+ requests_per_second: float = 10.0,
36
+ attack_pattern: str = 'volume',
37
+ concurrent_threads: int = 5,
38
+ payload_data: Optional[Dict[str, Any]] = None,
39
+ http_method: str = 'GET',
40
+ form_selector: str = None,
41
+ submit_selector: str = None,
42
+ expected_result: bool = False,
43
+ authentication=None,
44
+ execution_mode: str = 'api',
45
+ success_indicators: Optional[Dict[str, Any]] = None,
46
+ user_agents: Optional[List[str]] = None,
47
+ randomize_timing: bool = True):
48
+ """
49
+ Initialize the Request Flooding TTP.
50
+
51
+ Args:
52
+ target_endpoints: List of endpoint paths to target (e.g., ['/api/search', '/login'])
53
+ request_count: Total number of requests to send per endpoint
54
+ requests_per_second: Target rate of requests (used for timing calculations)
55
+ attack_pattern: Type of attack - 'volume', 'slowloris', 'burst', 'resource_exhaustion'
56
+ concurrent_threads: Number of concurrent threads to use for requests
57
+ payload_data: Data to send in request body (API mode) or form fields (UI mode)
58
+ http_method: HTTP method to use ('GET', 'POST', 'PUT', 'DELETE')
59
+ form_selector: CSS selector for form to repeatedly submit (UI mode)
60
+ submit_selector: CSS selector for submit button (UI mode)
61
+ expected_result: False = expect app to resist/rate-limit, True = expect success
62
+ authentication: Optional authentication mechanism
63
+ execution_mode: 'ui' or 'api'
64
+ success_indicators: Dict defining what constitutes successful flooding detection
65
+ user_agents: List of user agents to rotate through (helps bypass simple filtering)
66
+ randomize_timing: Whether to randomize request timing to appear more natural
67
+ """
68
+ super().__init__(
69
+ name="Request Flooding / DDoS Test",
70
+ description=f"Tests application resilience against {attack_pattern} flooding attacks with {request_count} requests",
71
+ expected_result=expected_result,
72
+ authentication=authentication,
73
+ execution_mode=execution_mode
74
+ )
75
+
76
+ # Core configuration
77
+ self.target_endpoints = target_endpoints or ['/']
78
+ self.request_count = request_count
79
+ self.requests_per_second = requests_per_second
80
+ self.attack_pattern = attack_pattern.lower()
81
+ self.concurrent_threads = min(concurrent_threads, 20) # Cap to prevent system overload
82
+ self.payload_data = payload_data or {}
83
+ self.http_method = http_method.upper()
84
+
85
+ # UI mode configuration
86
+ self.form_selector = form_selector
87
+ self.submit_selector = submit_selector
88
+
89
+ # Attack sophistication
90
+ self.user_agents = user_agents or [
91
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
92
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36',
93
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
94
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101'
95
+ ]
96
+ self.randomize_timing = randomize_timing
97
+
98
+ # Success/failure detection
99
+ self.success_indicators = success_indicators or {
100
+ 'rate_limit_status_codes': [429, 503, 502],
101
+ 'error_keywords': ['rate limit', 'too many requests', 'service unavailable'],
102
+ 'max_response_time': 30.0, # Consider slow responses as potential DoS impact
103
+ 'expected_success_rate': 0.1 # Expect most requests to be blocked if defenses work
104
+ }
105
+
106
+ # Attack results tracking
107
+ self.attack_results = {
108
+ 'total_requests': 0,
109
+ 'successful_requests': 0,
110
+ 'failed_requests': 0,
111
+ 'rate_limited_requests': 0,
112
+ 'error_responses': 0,
113
+ 'avg_response_time': 0.0,
114
+ 'max_response_time': 0.0,
115
+ 'responses_by_status': {},
116
+ 'attack_effectiveness': 0.0
117
+ }
118
+
119
+ def get_payloads(self) -> Generator[Dict[str, Any], None, None]:
120
+ """
121
+ Generates attack payloads based on the configured attack pattern.
122
+ Each payload contains timing and configuration data for the attack.
123
+ """
124
+ base_delay = 1.0 / self.requests_per_second if self.requests_per_second > 0 else 0.1
125
+
126
+ for i in range(self.request_count):
127
+ payload = {
128
+ 'request_id': i,
129
+ 'endpoint': self.target_endpoints[i % len(self.target_endpoints)],
130
+ 'data': self.payload_data.copy(),
131
+ 'user_agent': random.choice(self.user_agents),
132
+ 'delay': self._calculate_delay(i, base_delay),
133
+ 'timeout': self._calculate_timeout(i)
134
+ }
135
+
136
+ # Add attack-pattern specific modifications
137
+ if self.attack_pattern == 'burst':
138
+ # Create bursts every 10 requests
139
+ if i % 10 == 0:
140
+ payload['delay'] = 0.05 # Very fast burst
141
+ else:
142
+ payload['delay'] = base_delay * 3 # Slower between bursts
143
+
144
+ elif self.attack_pattern == 'slowloris':
145
+ payload['timeout'] = 60.0 # Very long timeout
146
+ payload['delay'] = base_delay * 2 # Slower rate but longer connections
147
+
148
+ elif self.attack_pattern == 'resource_exhaustion':
149
+ # Add resource-intensive parameters
150
+ payload['data'].update({
151
+ 'limit': 10000, # Request large datasets
152
+ 'search': '*', # Broad search terms
153
+ 'recursive': True
154
+ })
155
+
156
+ yield payload
157
+
158
+ def _calculate_delay(self, request_index: int, base_delay: float) -> float:
159
+ """Calculate delay between requests based on pattern and randomization."""
160
+ if not self.randomize_timing:
161
+ return base_delay
162
+
163
+ # Add randomization (±25% of base delay)
164
+ jitter = base_delay * 0.25 * (random.random() - 0.5) * 2
165
+ return max(0.01, base_delay + jitter)
166
+
167
+ def _calculate_timeout(self, request_index: int) -> float:
168
+ """Calculate request timeout based on attack pattern."""
169
+ if self.attack_pattern == 'slowloris':
170
+ return 60.0
171
+ elif self.attack_pattern == 'resource_exhaustion':
172
+ return 30.0
173
+ else:
174
+ return 10.0
175
+
176
+ def execute_step(self, driver: WebDriver, payload: Dict[str, Any]) -> None:
177
+ """
178
+ Executes a single flooding request in UI mode.
179
+ For UI mode, this repeatedly submits forms or navigates to pages.
180
+ """
181
+ try:
182
+ endpoint = payload['endpoint']
183
+ delay = payload['delay']
184
+
185
+ # Wait for the calculated delay
186
+ if delay > 0:
187
+ time.sleep(delay)
188
+
189
+ # Navigate to the target endpoint
190
+ current_url = driver.current_url
191
+ base_url = current_url.split('?')[0].rstrip('/')
192
+ target_url = f"{base_url}{endpoint}"
193
+
194
+ start_time = time.time()
195
+ driver.get(target_url)
196
+
197
+ # If we have form selectors, submit the form
198
+ if self.form_selector and self.submit_selector:
199
+ try:
200
+ # Fill form with payload data
201
+ for field_name, field_value in payload['data'].items():
202
+ try:
203
+ field = driver.find_element(By.NAME, field_name)
204
+ field.clear()
205
+ field.send_keys(str(field_value))
206
+ except NoSuchElementException:
207
+ # Try by ID if name doesn't work
208
+ try:
209
+ field = driver.find_element(By.ID, field_name)
210
+ field.clear()
211
+ field.send_keys(str(field_value))
212
+ except NoSuchElementException:
213
+ continue # Skip this field if not found
214
+
215
+ # Submit the form
216
+ submit_btn = driver.find_element(By.CSS_SELECTOR, self.submit_selector)
217
+ submit_btn.click()
218
+
219
+ except NoSuchElementException:
220
+ pass # Continue even if form submission fails
221
+
222
+ # Record timing
223
+ response_time = time.time() - start_time
224
+ self._record_ui_result(response_time, driver.current_url)
225
+
226
+ except TimeoutException:
227
+ self._record_ui_result(payload['timeout'], None, timeout=True)
228
+ except Exception as e:
229
+ self._record_ui_result(0.0, None, error=str(e))
230
+
231
+ def verify_result(self, driver: WebDriver) -> bool:
232
+ """
233
+ Verifies the outcome of the flooding attack in UI mode.
234
+ Checks for rate limiting, error pages, or performance degradation.
235
+ """
236
+ try:
237
+ page_source = driver.page_source.lower()
238
+ current_url = driver.current_url.lower()
239
+
240
+ # Check for rate limiting indicators
241
+ rate_limit_indicators = [
242
+ 'rate limit', 'too many requests', 'service unavailable',
243
+ 'temporarily unavailable', 'error 429', 'error 503',
244
+ 'please wait', 'slow down', 'blocked'
245
+ ]
246
+
247
+ for indicator in rate_limit_indicators:
248
+ if indicator in page_source or indicator in current_url:
249
+ return not self.expected_result # Rate limiting found
250
+
251
+ # If no rate limiting found and we expected defenses, that's a failure
252
+ return self.expected_result
253
+
254
+ except Exception:
255
+ return False
256
+
257
+ def execute_step_api(self, session: requests.Session, payload: Dict[str, Any], context: Dict[str, Any]) -> requests.Response:
258
+ """
259
+ Executes a single flooding request in API mode.
260
+ This is where the actual HTTP flooding happens.
261
+ """
262
+ from urllib.parse import urljoin
263
+
264
+ # Build the full URL
265
+ base_url = context.get('target_url', '')
266
+ if not base_url:
267
+ raise ValueError("target_url must be set in context for API mode")
268
+
269
+ endpoint = payload['endpoint']
270
+ url = urljoin(base_url, endpoint)
271
+
272
+ # Prepare headers
273
+ headers = {
274
+ 'User-Agent': payload['user_agent']
275
+ }
276
+
277
+ # Merge auth headers from context
278
+ auth_headers = context.get('auth_headers', {})
279
+ if auth_headers:
280
+ headers.update(auth_headers)
281
+
282
+ # Wait for the calculated delay
283
+ delay = payload['delay']
284
+ if delay > 0:
285
+ time.sleep(delay)
286
+
287
+ # Honor existing rate limiting from previous requests
288
+ resume_at = context.get('rate_limit_resume_at')
289
+ if resume_at and time.time() < resume_at:
290
+ # Skip this request due to rate limiting
291
+ raise requests.exceptions.RequestException("Rate limited")
292
+
293
+ # Make the request
294
+ start_time = time.time()
295
+ try:
296
+ if self.http_method == 'GET':
297
+ response = session.get(
298
+ url,
299
+ params=payload['data'],
300
+ headers=headers,
301
+ timeout=payload['timeout']
302
+ )
303
+ else:
304
+ response = session.request(
305
+ self.http_method,
306
+ url,
307
+ json=payload['data'],
308
+ headers=headers,
309
+ timeout=payload['timeout']
310
+ )
311
+
312
+ # Record the result
313
+ response_time = time.time() - start_time
314
+ self._record_api_result(response, response_time, context)
315
+
316
+ return response
317
+
318
+ except requests.exceptions.Timeout:
319
+ response_time = payload['timeout']
320
+ self._record_api_result(None, response_time, context, timeout=True)
321
+ raise
322
+ except Exception as e:
323
+ response_time = time.time() - start_time
324
+ self._record_api_result(None, response_time, context, error=str(e))
325
+ raise
326
+
327
+ def verify_result_api(self, response: requests.Response, context: Dict[str, Any]) -> bool:
328
+ """
329
+ Verifies if the flooding attack was effective or if defenses kicked in.
330
+
331
+ Args:
332
+ response: The response from execute_step_api
333
+ context: Shared context dictionary
334
+
335
+ Returns:
336
+ True if attack behavior detected (rate limiting, errors),
337
+ False if requests succeeded without defensive measures
338
+ """
339
+ # Check if we have accumulated enough results to make a determination
340
+ total_requests = self.attack_results['total_requests']
341
+
342
+ # Early determination if we have enough data
343
+ if total_requests >= min(20, self.request_count // 2):
344
+ success_rate = self.attack_results['successful_requests'] / total_requests
345
+ rate_limit_rate = self.attack_results['rate_limited_requests'] / total_requests
346
+
347
+ # If we expected defenses (expected_result=False)
348
+ if not self.expected_result:
349
+ # Good defense: high rate limiting, low success rate
350
+ if rate_limit_rate > 0.3 or success_rate < self.success_indicators['expected_success_rate']:
351
+ return True
352
+
353
+ # If we expected success (expected_result=True)
354
+ else:
355
+ # Attack successful: high success rate, low rate limiting
356
+ if success_rate > 0.7 and rate_limit_rate < 0.2:
357
+ return True
358
+
359
+ # Check immediate response indicators
360
+ if response:
361
+ # Rate limiting detected
362
+ if response.status_code in self.success_indicators['rate_limit_status_codes']:
363
+ return not self.expected_result
364
+
365
+ # Check response content for defensive indicators
366
+ try:
367
+ response_text = response.text.lower()
368
+ for keyword in self.success_indicators['error_keywords']:
369
+ if keyword in response_text:
370
+ return not self.expected_result
371
+ except Exception:
372
+ pass
373
+
374
+ # Default to continuing the test
375
+ return False
376
+
377
+ def _record_ui_result(self, response_time: float, url: str = None, timeout: bool = False, error: str = None):
378
+ """Record results from UI mode execution."""
379
+ self.attack_results['total_requests'] += 1
380
+
381
+ if timeout:
382
+ self.attack_results['failed_requests'] += 1
383
+ elif error:
384
+ self.attack_results['error_responses'] += 1
385
+ elif url and any(indicator in url for indicator in ['error', 'limit', '429', '503']):
386
+ self.attack_results['rate_limited_requests'] += 1
387
+ else:
388
+ self.attack_results['successful_requests'] += 1
389
+
390
+ # Update timing stats
391
+ self.attack_results['max_response_time'] = max(
392
+ self.attack_results['max_response_time'], response_time
393
+ )
394
+
395
+ # Calculate rolling average
396
+ total = self.attack_results['total_requests']
397
+ current_avg = self.attack_results['avg_response_time']
398
+ self.attack_results['avg_response_time'] = (
399
+ (current_avg * (total - 1) + response_time) / total
400
+ )
401
+
402
+ def _record_api_result(self, response: requests.Response = None, response_time: float = 0.0,
403
+ context: Dict[str, Any] = None, timeout: bool = False, error: str = None):
404
+ """Record results from API mode execution."""
405
+ self.attack_results['total_requests'] += 1
406
+
407
+ if timeout:
408
+ self.attack_results['failed_requests'] += 1
409
+ elif error:
410
+ self.attack_results['error_responses'] += 1
411
+ elif response:
412
+ status_code = response.status_code
413
+
414
+ # Track status code distribution
415
+ if status_code not in self.attack_results['responses_by_status']:
416
+ self.attack_results['responses_by_status'][status_code] = 0
417
+ self.attack_results['responses_by_status'][status_code] += 1
418
+
419
+ # Categorize the response
420
+ if status_code in self.success_indicators['rate_limit_status_codes']:
421
+ self.attack_results['rate_limited_requests'] += 1
422
+
423
+ # Update rate limiting in context
424
+ if context and response.headers.get('Retry-After'):
425
+ try:
426
+ retry_after = int(response.headers['Retry-After'])
427
+ context['rate_limit_resume_at'] = time.time() + min(retry_after, 60)
428
+ except (ValueError, TypeError):
429
+ context['rate_limit_resume_at'] = time.time() + 5
430
+
431
+ elif 200 <= status_code < 300:
432
+ self.attack_results['successful_requests'] += 1
433
+ else:
434
+ self.attack_results['error_responses'] += 1
435
+
436
+ # Update timing statistics
437
+ self.attack_results['max_response_time'] = max(
438
+ self.attack_results['max_response_time'], response_time
439
+ )
440
+
441
+ # Calculate rolling average response time
442
+ total = self.attack_results['total_requests']
443
+ current_avg = self.attack_results['avg_response_time']
444
+ self.attack_results['avg_response_time'] = (
445
+ (current_avg * (total - 1) + response_time) / total
446
+ )
447
+
448
+ # Calculate attack effectiveness score
449
+ if total > 0:
450
+ success_rate = self.attack_results['successful_requests'] / total
451
+ rate_limit_rate = self.attack_results['rate_limited_requests'] / total
452
+
453
+ if self.expected_result:
454
+ # Higher success rate = more effective attack
455
+ self.attack_results['attack_effectiveness'] = success_rate * 100
456
+ else:
457
+ # Higher rate limiting = more effective defenses (which we want to detect)
458
+ self.attack_results['attack_effectiveness'] = rate_limit_rate * 100
459
+
460
+ def get_attack_summary(self) -> Dict[str, Any]:
461
+ """
462
+ Returns a comprehensive summary of the attack results.
463
+ Useful for detailed analysis and reporting.
464
+ """
465
+ total = self.attack_results['total_requests']
466
+ if total == 0:
467
+ return {"error": "No requests completed"}
468
+
469
+ summary = {
470
+ "attack_pattern": self.attack_pattern,
471
+ "total_requests": total,
472
+ "success_rate": (self.attack_results['successful_requests'] / total) * 100,
473
+ "rate_limit_rate": (self.attack_results['rate_limited_requests'] / total) * 100,
474
+ "error_rate": (self.attack_results['error_responses'] / total) * 100,
475
+ "avg_response_time": round(self.attack_results['avg_response_time'], 3),
476
+ "max_response_time": round(self.attack_results['max_response_time'], 3),
477
+ "attack_effectiveness": round(self.attack_results['attack_effectiveness'], 1),
478
+ "status_code_distribution": self.attack_results['responses_by_status'],
479
+ "defense_assessment": self._assess_defenses()
480
+ }
481
+
482
+ return summary
483
+
484
+ def _assess_defenses(self) -> str:
485
+ """Assess the effectiveness of the target's defensive measures."""
486
+ total = self.attack_results['total_requests']
487
+ if total == 0:
488
+ return "Insufficient data"
489
+
490
+ success_rate = self.attack_results['successful_requests'] / total
491
+ rate_limit_rate = self.attack_results['rate_limited_requests'] / total
492
+ avg_response_time = self.attack_results['avg_response_time']
493
+
494
+ if rate_limit_rate > 0.5:
495
+ return "Strong rate limiting detected - Good defenses"
496
+ elif rate_limit_rate > 0.2:
497
+ return "Moderate rate limiting detected - Basic defenses"
498
+ elif avg_response_time > 10.0:
499
+ return "Performance degradation detected - Possible DoS impact"
500
+ elif success_rate > 0.8:
501
+ return "High success rate - Weak or no defenses detected"
502
+ else:
503
+ return "Mixed results - Some defensive measures present"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scythe-ttp
3
- Version: 0.17.6
3
+ Version: 0.18.1
4
4
  Summary: An extensible framework for emulating attacker TTPs with Selenium.
5
5
  Author-email: EpykLab <cyber@epyklab.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -36,6 +36,7 @@ scythe/payloads/generators.py
36
36
  scythe/ttps/__init__.py
37
37
  scythe/ttps/web/__init__.py
38
38
  scythe/ttps/web/login_bruteforce.py
39
+ scythe/ttps/web/request_flooding.py
39
40
  scythe/ttps/web/sql_injection.py
40
41
  scythe/ttps/web/uuid_guessing.py
41
42
  scythe_ttp.egg-info/PKG-INFO
scythe_ttp-0.17.6/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.17.6
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes