scythe-ttp 0.11.0__tar.gz → 0.12.4__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 (52) hide show
  1. {scythe_ttp-0.11.0/scythe_ttp.egg-info → scythe_ttp-0.12.4}/PKG-INFO +36 -16
  2. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/README.md +36 -16
  3. scythe_ttp-0.12.4/VERSION +1 -0
  4. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/core/headers.py +197 -45
  5. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4/scythe_ttp.egg-info}/PKG-INFO +36 -16
  6. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/setup.py +1 -1
  7. scythe_ttp-0.11.0/VERSION +0 -1
  8. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/LICENSE +0 -0
  9. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/MANIFEST.in +0 -0
  10. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/requirements.txt +0 -0
  11. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/__init__.py +0 -0
  12. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/auth/__init__.py +0 -0
  13. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/auth/base.py +0 -0
  14. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/auth/basic.py +0 -0
  15. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/auth/bearer.py +0 -0
  16. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/__init__.py +0 -0
  17. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/base.py +0 -0
  18. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/default.py +0 -0
  19. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/human.py +0 -0
  20. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/machine.py +0 -0
  21. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/stealth.py +0 -0
  22. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/core/__init__.py +0 -0
  23. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/core/executor.py +0 -0
  24. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/core/ttp.py +0 -0
  25. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/journeys/__init__.py +0 -0
  26. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/journeys/actions.py +0 -0
  27. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/journeys/base.py +0 -0
  28. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/journeys/executor.py +0 -0
  29. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/orchestrators/__init__.py +0 -0
  30. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/orchestrators/base.py +0 -0
  31. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/orchestrators/batch.py +0 -0
  32. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/orchestrators/distributed.py +0 -0
  33. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/orchestrators/scale.py +0 -0
  34. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/payloads/__init__.py +0 -0
  35. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/payloads/generators.py +0 -0
  36. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/ttps/__init__.py +0 -0
  37. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/ttps/web/__init__.py +0 -0
  38. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/ttps/web/login_bruteforce.py +0 -0
  39. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/ttps/web/sql_injection.py +0 -0
  40. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/ttps/web/uuid_guessing.py +0 -0
  41. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe_ttp.egg-info/SOURCES.txt +0 -0
  42. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe_ttp.egg-info/dependency_links.txt +0 -0
  43. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe_ttp.egg-info/requires.txt +0 -0
  44. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe_ttp.egg-info/top_level.txt +0 -0
  45. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/setup.cfg +0 -0
  46. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_authentication.py +0 -0
  47. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_behaviors.py +0 -0
  48. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_expected_results.py +0 -0
  49. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_feature_completeness.py +0 -0
  50. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_header_extraction.py +0 -0
  51. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_journeys.py +0 -0
  52. {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_orchestrators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scythe-ttp
3
- Version: 0.11.0
3
+ Version: 0.12.4
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
@@ -169,6 +169,26 @@ file_upload_ttp = FileUploadTTP(
169
169
 
170
170
  ### Installation
171
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
+
172
192
  1. Clone the repository:
173
193
  ```bash
174
194
  git clone https://github.com/EpykLab/scythe.git
@@ -458,11 +478,11 @@ from scythe.core.executor import TTPExecutor
458
478
  class MyTTP(TTP):
459
479
  def get_payloads(self):
460
480
  yield "test_payload"
461
-
481
+
462
482
  def execute_step(self, driver, payload):
463
483
  driver.get("http://your-app.com/login")
464
484
  # ... test logic ...
465
-
485
+
466
486
  def verify_result(self, driver):
467
487
  return "welcome" in driver.page_source
468
488
 
@@ -523,7 +543,7 @@ from typing import Generator, Any
523
543
 
524
544
  class CustomBusinessLogicTTP(TTP):
525
545
  """Test specific business logic under adverse conditions."""
526
-
546
+
527
547
  def __init__(self, business_scenarios: list, expected_result: bool = True):
528
548
  super().__init__(
529
549
  name="Business Logic Test",
@@ -531,28 +551,28 @@ class CustomBusinessLogicTTP(TTP):
531
551
  expected_result=expected_result
532
552
  )
533
553
  self.scenarios = business_scenarios
534
-
554
+
535
555
  def get_payloads(self) -> Generator[Any, None, None]:
536
556
  for scenario in self.scenarios:
537
557
  yield scenario
538
-
558
+
539
559
  def execute_step(self, driver, payload):
540
560
  # Implement your specific business logic testing
541
561
  # This could involve API calls, database interactions, etc.
542
562
  pass
543
-
563
+
544
564
  def verify_result(self, driver) -> bool:
545
565
  # Verify the business logic behaved correctly
546
566
  return self.check_business_rules(driver)
547
567
 
548
568
  class CustomWorkflowAction(Action):
549
569
  """Custom action for specific workflow steps."""
550
-
570
+
551
571
  def __init__(self, workflow_step: str, parameters: dict):
552
572
  super().__init__(f"Custom {workflow_step}", f"Execute {workflow_step}")
553
573
  self.workflow_step = workflow_step
554
574
  self.parameters = parameters
555
-
575
+
556
576
  def execute(self, driver, context):
557
577
  # Implement custom workflow logic
558
578
  return self.perform_workflow_step(driver, context)
@@ -569,12 +589,12 @@ ecommerce_suite = [
569
589
  payment_security_test, # Test payment form security
570
590
  user_data_protection_test, # Test PII protection
571
591
  session_management_test, # Test session security
572
-
592
+
573
593
  # Load testing
574
594
  product_catalog_load_test, # High-traffic product browsing
575
595
  checkout_process_load_test, # Concurrent checkout processes
576
596
  search_functionality_test, # Search under load
577
-
597
+
578
598
  # Workflow testing
579
599
  complete_purchase_journey, # End-to-end purchase flow
580
600
  return_process_journey, # Product return workflow
@@ -647,30 +667,30 @@ def analyze_test_results(orchestration_result):
647
667
  print("="*60)
648
668
  print("COMPREHENSIVE TEST ANALYSIS")
649
669
  print("="*60)
650
-
670
+
651
671
  print(f"Total Executions: {orchestration_result.total_executions}")
652
672
  print(f"Success Rate: {orchestration_result.success_rate:.1f}%")
653
673
  print(f"Average Execution Time: {orchestration_result.average_execution_time:.2f}s")
654
-
674
+
655
675
  # Performance metrics
656
676
  if orchestration_result.metadata.get('performance_stats'):
657
677
  stats = orchestration_result.metadata['performance_stats']
658
678
  print(f"Peak Response Time: {stats.get('peak_response_time', 'N/A')}")
659
679
  print(f"95th Percentile: {stats.get('p95_response_time', 'N/A')}")
660
-
680
+
661
681
  # Geographic distribution (if applicable)
662
682
  if orchestration_result.metadata.get('distribution_stats'):
663
683
  dist = orchestration_result.metadata['distribution_stats']
664
684
  print("Geographic Distribution:")
665
685
  for location, count in dist.get('location_usage', {}).items():
666
686
  print(f" {location}: {count} executions")
667
-
687
+
668
688
  # Error analysis
669
689
  if orchestration_result.errors:
670
690
  print(f"\nErrors Encountered: {len(orchestration_result.errors)}")
671
691
  for i, error in enumerate(orchestration_result.errors[:5], 1):
672
692
  print(f" {i}. {error}")
673
-
693
+
674
694
  print("="*60)
675
695
 
676
696
  # Use with any orchestration result
@@ -122,6 +122,26 @@ file_upload_ttp = FileUploadTTP(
122
122
 
123
123
  ### Installation
124
124
 
125
+ #### If you would like to use as a library:
126
+
127
+ setup the virtual environment
128
+ ```bash
129
+ python3 -m venv venv
130
+
131
+ # source the venv
132
+ # bash,zsh: source venv/bin/activate
133
+ # fish: source venv/bin/activate.fish
134
+ ```
135
+
136
+ install the package
137
+ ```bash
138
+ # in an activated venv
139
+
140
+ pip3 install scythe-ttp
141
+ ```
142
+
143
+ #### If you would like like to contribute:
144
+
125
145
  1. Clone the repository:
126
146
  ```bash
127
147
  git clone https://github.com/EpykLab/scythe.git
@@ -411,11 +431,11 @@ from scythe.core.executor import TTPExecutor
411
431
  class MyTTP(TTP):
412
432
  def get_payloads(self):
413
433
  yield "test_payload"
414
-
434
+
415
435
  def execute_step(self, driver, payload):
416
436
  driver.get("http://your-app.com/login")
417
437
  # ... test logic ...
418
-
438
+
419
439
  def verify_result(self, driver):
420
440
  return "welcome" in driver.page_source
421
441
 
@@ -476,7 +496,7 @@ from typing import Generator, Any
476
496
 
477
497
  class CustomBusinessLogicTTP(TTP):
478
498
  """Test specific business logic under adverse conditions."""
479
-
499
+
480
500
  def __init__(self, business_scenarios: list, expected_result: bool = True):
481
501
  super().__init__(
482
502
  name="Business Logic Test",
@@ -484,28 +504,28 @@ class CustomBusinessLogicTTP(TTP):
484
504
  expected_result=expected_result
485
505
  )
486
506
  self.scenarios = business_scenarios
487
-
507
+
488
508
  def get_payloads(self) -> Generator[Any, None, None]:
489
509
  for scenario in self.scenarios:
490
510
  yield scenario
491
-
511
+
492
512
  def execute_step(self, driver, payload):
493
513
  # Implement your specific business logic testing
494
514
  # This could involve API calls, database interactions, etc.
495
515
  pass
496
-
516
+
497
517
  def verify_result(self, driver) -> bool:
498
518
  # Verify the business logic behaved correctly
499
519
  return self.check_business_rules(driver)
500
520
 
501
521
  class CustomWorkflowAction(Action):
502
522
  """Custom action for specific workflow steps."""
503
-
523
+
504
524
  def __init__(self, workflow_step: str, parameters: dict):
505
525
  super().__init__(f"Custom {workflow_step}", f"Execute {workflow_step}")
506
526
  self.workflow_step = workflow_step
507
527
  self.parameters = parameters
508
-
528
+
509
529
  def execute(self, driver, context):
510
530
  # Implement custom workflow logic
511
531
  return self.perform_workflow_step(driver, context)
@@ -522,12 +542,12 @@ ecommerce_suite = [
522
542
  payment_security_test, # Test payment form security
523
543
  user_data_protection_test, # Test PII protection
524
544
  session_management_test, # Test session security
525
-
545
+
526
546
  # Load testing
527
547
  product_catalog_load_test, # High-traffic product browsing
528
548
  checkout_process_load_test, # Concurrent checkout processes
529
549
  search_functionality_test, # Search under load
530
-
550
+
531
551
  # Workflow testing
532
552
  complete_purchase_journey, # End-to-end purchase flow
533
553
  return_process_journey, # Product return workflow
@@ -600,30 +620,30 @@ def analyze_test_results(orchestration_result):
600
620
  print("="*60)
601
621
  print("COMPREHENSIVE TEST ANALYSIS")
602
622
  print("="*60)
603
-
623
+
604
624
  print(f"Total Executions: {orchestration_result.total_executions}")
605
625
  print(f"Success Rate: {orchestration_result.success_rate:.1f}%")
606
626
  print(f"Average Execution Time: {orchestration_result.average_execution_time:.2f}s")
607
-
627
+
608
628
  # Performance metrics
609
629
  if orchestration_result.metadata.get('performance_stats'):
610
630
  stats = orchestration_result.metadata['performance_stats']
611
631
  print(f"Peak Response Time: {stats.get('peak_response_time', 'N/A')}")
612
632
  print(f"95th Percentile: {stats.get('p95_response_time', 'N/A')}")
613
-
633
+
614
634
  # Geographic distribution (if applicable)
615
635
  if orchestration_result.metadata.get('distribution_stats'):
616
636
  dist = orchestration_result.metadata['distribution_stats']
617
637
  print("Geographic Distribution:")
618
638
  for location, count in dist.get('location_usage', {}).items():
619
639
  print(f" {location}: {count} executions")
620
-
640
+
621
641
  # Error analysis
622
642
  if orchestration_result.errors:
623
643
  print(f"\nErrors Encountered: {len(orchestration_result.errors)}")
624
644
  for i, error in enumerate(orchestration_result.errors[:5], 1):
625
645
  print(f" {i}. {error}")
626
-
646
+
627
647
  print("="*60)
628
648
 
629
649
  # Use with any orchestration result
@@ -724,4 +744,4 @@ This architecture supports testing scenarios from simple security checks to comp
724
744
 
725
745
  ---
726
746
 
727
- **Scythe**: Comprehensive adverse conditions testing for robust, reliable systems.
747
+ **Scythe**: Comprehensive adverse conditions testing for robust, reliable systems.
@@ -0,0 +1 @@
1
+ 0.12.5
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import logging
3
+ import requests
3
4
  from typing import Optional, Dict, Any
4
5
  from selenium.webdriver.remote.webdriver import WebDriver
5
6
  from selenium.webdriver.chrome.options import Options
@@ -8,23 +9,23 @@ from selenium.webdriver.chrome.options import Options
8
9
  class HeaderExtractor:
9
10
  """
10
11
  Utility class for extracting HTTP response headers from WebDriver sessions.
11
-
12
+
12
13
  Specifically designed to capture the X-SCYTHE-TARGET-VERSION header
13
14
  that indicates the version of the web application being tested.
14
15
  """
15
-
16
- SCYTHE_VERSION_HEADER = "X-SCYTHE-TARGET-VERSION"
17
-
16
+
17
+ SCYTHE_VERSION_HEADER = "X-Scythe-Target-Version"
18
+
18
19
  def __init__(self):
19
20
  self.logger = logging.getLogger("HeaderExtractor")
20
-
21
+
21
22
  @staticmethod
22
23
  def enable_logging_for_driver(chrome_options: Options) -> None:
23
24
  """
24
25
  Enable performance logging capabilities for Chrome WebDriver.
25
-
26
+
26
27
  This must be called during WebDriver setup to capture network logs.
27
-
28
+
28
29
  Args:
29
30
  chrome_options: Chrome options object to modify
30
31
  """
@@ -32,15 +33,166 @@ class HeaderExtractor:
32
33
  chrome_options.add_argument("--enable-logging")
33
34
  chrome_options.add_argument("--log-level=0")
34
35
  chrome_options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
35
-
36
+
37
+ def banner_grab(self, url: str, timeout: int = 10, method: str = "HEAD") -> Optional[str]:
38
+ """
39
+ Perform a simple HTTP request to extract the X-SCYTHE-TARGET-VERSION header.
40
+
41
+ This is a more reliable alternative to Selenium's performance logging
42
+ for cases where you just need to grab headers.
43
+
44
+ Args:
45
+ url: URL to make the request to
46
+ timeout: Request timeout in seconds
47
+ method: HTTP method to use ("HEAD" or "GET")
48
+
49
+ Returns:
50
+ Version string if header found, None otherwise
51
+ """
52
+ try:
53
+ self.logger.debug(f"Making {method} request to {url} for header extraction")
54
+
55
+ # Use HEAD by default for efficiency, fallback to GET if needed
56
+ if method.upper() == "HEAD":
57
+ response = requests.head(url, timeout=timeout, allow_redirects=True)
58
+ else:
59
+ response = requests.get(url, timeout=timeout, allow_redirects=True)
60
+
61
+ # Check if request was successful
62
+ response.raise_for_status()
63
+
64
+ # Look for the version header (case-insensitive)
65
+ version = self._find_version_header(dict(response.headers))
66
+ if version:
67
+ self.logger.debug(f"Found target version '{version}' via {method} request to {url}")
68
+ return version
69
+ else:
70
+ self.logger.debug(f"No X-SCYTHE-TARGET-VERSION header found in response from {url}")
71
+ return None
72
+
73
+ except requests.exceptions.RequestException as e:
74
+ self.logger.warning(f"Failed to make {method} request to {url}: {e}")
75
+ return None
76
+ except Exception as e:
77
+ self.logger.warning(f"Unexpected error during banner grab: {e}")
78
+ return None
79
+
80
+ def get_all_headers_via_request(self, url: str, timeout: int = 10, method: str = "HEAD") -> Dict[str, str]:
81
+ """
82
+ Get all headers from a simple HTTP request.
83
+
84
+ Args:
85
+ url: URL to make the request to
86
+ timeout: Request timeout in seconds
87
+ method: HTTP method to use ("HEAD" or "GET")
88
+
89
+ Returns:
90
+ Dictionary of all response headers
91
+ """
92
+ try:
93
+ self.logger.debug(f"Making {method} request to {url} for all headers")
94
+
95
+ if method.upper() == "HEAD":
96
+ response = requests.head(url, timeout=timeout, allow_redirects=True)
97
+ else:
98
+ response = requests.get(url, timeout=timeout, allow_redirects=True)
99
+
100
+ response.raise_for_status()
101
+
102
+ # Convert headers to regular dict with string values
103
+ return {k: str(v) for k, v in response.headers.items()}
104
+
105
+ except requests.exceptions.RequestException as e:
106
+ self.logger.warning(f"Failed to get headers from {url}: {e}")
107
+ return {}
108
+ except Exception as e:
109
+ self.logger.warning(f"Unexpected error getting headers: {e}")
110
+ return {}
111
+
112
+ def debug_headers(self, url: str, timeout: int = 10) -> None:
113
+ """
114
+ Debug method to print all headers received from a URL.
115
+
116
+ This is useful for troubleshooting when headers aren't being detected properly.
117
+ It will show you exactly what headers the server is sending.
118
+
119
+ Args:
120
+ url: URL to make the request to
121
+ timeout: Request timeout in seconds
122
+ """
123
+ print(f"\n{'='*60}")
124
+ print(f"DEBUG: Header dump for {url}")
125
+ print(f"{'='*60}")
126
+
127
+ try:
128
+ # Try HEAD request first
129
+ print("\n--- HEAD Request ---")
130
+ response = requests.head(url, timeout=timeout, allow_redirects=True)
131
+ print(f"Status Code: {response.status_code}")
132
+ print(f"Headers ({len(response.headers)} total):")
133
+
134
+ for name, value in response.headers.items():
135
+ print(f" {name}: {value}")
136
+ if "scythe" in name.lower() or "version" in name.lower():
137
+ print(" *** POTENTIAL VERSION HEADER ***")
138
+
139
+ # Try GET request
140
+ print("\n--- GET Request ---")
141
+ response = requests.get(url, timeout=timeout, allow_redirects=True)
142
+ print(f"Status Code: {response.status_code}")
143
+ print(f"Headers ({len(response.headers)} total):")
144
+
145
+ for name, value in response.headers.items():
146
+ print(f" {name}: {value}")
147
+ if "scythe" in name.lower() or "version" in name.lower():
148
+ print(" *** POTENTIAL VERSION HEADER ***")
149
+
150
+ # Check specifically for the target header
151
+ version = self._find_version_header(dict(response.headers))
152
+ print(f"\nTarget version extraction result: {version}")
153
+
154
+ except Exception as e:
155
+ print(f"ERROR: Failed to debug headers: {e}")
156
+
157
+ print(f"{'='*60}\n")
158
+
159
+ def extract_target_version_hybrid(self, driver: WebDriver, target_url: Optional[str] = None) -> Optional[str]:
160
+ """
161
+ Hybrid approach: Try banner grab first, then fall back to Selenium performance logs.
162
+
163
+ This method attempts to get the version header using a simple HTTP request first,
164
+ which is more reliable than Selenium's performance logging. If that fails or no
165
+ target_url is provided, it falls back to the Selenium-based extraction.
166
+
167
+ Args:
168
+ driver: WebDriver instance
169
+ target_url: URL to check (required for banner grab method)
170
+
171
+ Returns:
172
+ Version string if header found, None otherwise
173
+ """
174
+ # Try banner grab first if we have a target URL
175
+ if target_url:
176
+ self.logger.debug("Attempting banner grab method first")
177
+ version = self.banner_grab(target_url)
178
+ if version:
179
+ self.logger.debug(f"Successfully extracted version '{version}' via banner grab")
180
+ return version
181
+ else:
182
+ self.logger.debug("Banner grab failed, falling back to Selenium performance logs")
183
+
184
+ # Fall back to Selenium performance logs
185
+ self.logger.debug("Using Selenium performance logs method")
186
+ return self.extract_target_version(driver, target_url)
187
+
36
188
  def extract_target_version(self, driver: WebDriver, target_url: Optional[str] = None) -> Optional[str]:
37
189
  """
38
190
  Extract the X-SCYTHE-TARGET-VERSION header from the most recent HTTP response.
39
-
191
+
40
192
  Args:
41
193
  driver: WebDriver instance with performance logging enabled
42
194
  target_url: Optional URL to filter responses for (if None, uses any response)
43
-
195
+
44
196
  Returns:
45
197
  Version string if header found, None otherwise
46
198
  """
@@ -49,77 +201,77 @@ class HeaderExtractor:
49
201
  if not hasattr(driver, 'get_log'):
50
202
  self.logger.warning("WebDriver does not support get_log method")
51
203
  return None
52
-
204
+
53
205
  logs = getattr(driver, 'get_log')('performance')
54
-
206
+
55
207
  # Look for Network.responseReceived events
56
208
  for log_entry in reversed(logs): # Start with most recent
57
209
  try:
58
210
  message = log_entry.get('message', {})
59
211
  if isinstance(message, str):
60
212
  message = json.loads(message)
61
-
213
+
62
214
  method = message.get('message', {}).get('method', '')
63
215
  params = message.get('message', {}).get('params', {})
64
-
216
+
65
217
  if method == 'Network.responseReceived':
66
218
  response = params.get('response', {})
67
219
  headers = response.get('headers', {})
68
220
  response_url = response.get('url', '')
69
-
221
+
70
222
  # Filter by target URL if specified
71
223
  if target_url and target_url not in response_url:
72
224
  continue
73
-
225
+
74
226
  # Look for the version header (case-insensitive)
75
227
  version = self._find_version_header(headers)
76
228
  if version:
77
229
  self.logger.debug(f"Found target version '{version}' in response from {response_url}")
78
230
  return version
79
-
231
+
80
232
  except (json.JSONDecodeError, KeyError, AttributeError) as e:
81
233
  self.logger.debug(f"Error parsing log entry: {e}")
82
234
  continue
83
-
235
+
84
236
  self.logger.debug("No X-SCYTHE-TARGET-VERSION header found in network logs")
85
237
  return None
86
-
238
+
87
239
  except Exception as e:
88
240
  self.logger.warning(f"Failed to extract target version from logs: {e}")
89
241
  return None
90
-
242
+
91
243
  def _find_version_header(self, headers: Dict[str, Any]) -> Optional[str]:
92
244
  """
93
245
  Find the version header in a case-insensitive manner.
94
-
246
+
95
247
  Args:
96
248
  headers: Dictionary of HTTP headers
97
-
249
+
98
250
  Returns:
99
251
  Version string if found, None otherwise
100
252
  """
101
253
  # Check for exact case match first
102
254
  if self.SCYTHE_VERSION_HEADER in headers:
103
255
  return str(headers[self.SCYTHE_VERSION_HEADER]).strip()
104
-
256
+
105
257
  # Check case-insensitive
106
258
  header_lower = self.SCYTHE_VERSION_HEADER.lower()
107
259
  for header_name, header_value in headers.items():
108
260
  if header_name.lower() == header_lower:
109
261
  return str(header_value).strip()
110
-
262
+
111
263
  return None
112
-
264
+
113
265
  def extract_all_headers(self, driver: WebDriver, target_url: Optional[str] = None) -> Dict[str, str]:
114
266
  """
115
267
  Extract all headers from the most recent HTTP response.
116
-
268
+
117
269
  Useful for debugging or when additional headers might be needed.
118
-
270
+
119
271
  Args:
120
272
  driver: WebDriver instance with performance logging enabled
121
273
  target_url: Optional URL to filter responses for
122
-
274
+
123
275
  Returns:
124
276
  Dictionary of headers from the most recent response
125
277
  """
@@ -128,67 +280,67 @@ class HeaderExtractor:
128
280
  if not hasattr(driver, 'get_log'):
129
281
  self.logger.warning("WebDriver does not support get_log method")
130
282
  return {}
131
-
283
+
132
284
  logs = getattr(driver, 'get_log')('performance')
133
-
285
+
134
286
  for log_entry in reversed(logs):
135
287
  try:
136
288
  message = log_entry.get('message', {})
137
289
  if isinstance(message, str):
138
290
  message = json.loads(message)
139
-
291
+
140
292
  method = message.get('message', {}).get('method', '')
141
293
  params = message.get('message', {}).get('params', {})
142
-
294
+
143
295
  if method == 'Network.responseReceived':
144
296
  response = params.get('response', {})
145
297
  headers = response.get('headers', {})
146
298
  response_url = response.get('url', '')
147
-
299
+
148
300
  # Filter by target URL if specified
149
301
  if target_url and target_url not in response_url:
150
302
  continue
151
-
303
+
152
304
  # Convert all header values to strings
153
305
  return {k: str(v) for k, v in headers.items()}
154
-
306
+
155
307
  except (json.JSONDecodeError, KeyError, AttributeError):
156
308
  continue
157
-
309
+
158
310
  return {}
159
-
311
+
160
312
  except Exception as e:
161
313
  self.logger.warning(f"Failed to extract headers from logs: {e}")
162
314
  return {}
163
-
315
+
164
316
  def get_version_summary(self, results: list) -> Dict[str, Any]:
165
317
  """
166
318
  Analyze version information across multiple test results.
167
-
319
+
168
320
  Args:
169
321
  results: List of result dictionaries containing version information
170
-
322
+
171
323
  Returns:
172
324
  Dictionary with version analysis summary
173
325
  """
174
326
  versions = []
175
327
  results_with_version = 0
176
-
328
+
177
329
  for result in results:
178
330
  version = result.get('target_version')
179
331
  if version:
180
332
  versions.append(version)
181
333
  results_with_version += 1
182
-
334
+
183
335
  summary = {
184
336
  'total_results': len(results),
185
337
  'results_with_version': results_with_version,
186
338
  'unique_versions': list(set(versions)) if versions else [],
187
339
  'version_counts': {}
188
340
  }
189
-
341
+
190
342
  # Count occurrences of each version
191
343
  for version in versions:
192
344
  summary['version_counts'][version] = summary['version_counts'].get(version, 0) + 1
193
-
194
- return summary
345
+
346
+ return summary
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scythe-ttp
3
- Version: 0.11.0
3
+ Version: 0.12.4
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
@@ -169,6 +169,26 @@ file_upload_ttp = FileUploadTTP(
169
169
 
170
170
  ### Installation
171
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
+
172
192
  1. Clone the repository:
173
193
  ```bash
174
194
  git clone https://github.com/EpykLab/scythe.git
@@ -458,11 +478,11 @@ from scythe.core.executor import TTPExecutor
458
478
  class MyTTP(TTP):
459
479
  def get_payloads(self):
460
480
  yield "test_payload"
461
-
481
+
462
482
  def execute_step(self, driver, payload):
463
483
  driver.get("http://your-app.com/login")
464
484
  # ... test logic ...
465
-
485
+
466
486
  def verify_result(self, driver):
467
487
  return "welcome" in driver.page_source
468
488
 
@@ -523,7 +543,7 @@ from typing import Generator, Any
523
543
 
524
544
  class CustomBusinessLogicTTP(TTP):
525
545
  """Test specific business logic under adverse conditions."""
526
-
546
+
527
547
  def __init__(self, business_scenarios: list, expected_result: bool = True):
528
548
  super().__init__(
529
549
  name="Business Logic Test",
@@ -531,28 +551,28 @@ class CustomBusinessLogicTTP(TTP):
531
551
  expected_result=expected_result
532
552
  )
533
553
  self.scenarios = business_scenarios
534
-
554
+
535
555
  def get_payloads(self) -> Generator[Any, None, None]:
536
556
  for scenario in self.scenarios:
537
557
  yield scenario
538
-
558
+
539
559
  def execute_step(self, driver, payload):
540
560
  # Implement your specific business logic testing
541
561
  # This could involve API calls, database interactions, etc.
542
562
  pass
543
-
563
+
544
564
  def verify_result(self, driver) -> bool:
545
565
  # Verify the business logic behaved correctly
546
566
  return self.check_business_rules(driver)
547
567
 
548
568
  class CustomWorkflowAction(Action):
549
569
  """Custom action for specific workflow steps."""
550
-
570
+
551
571
  def __init__(self, workflow_step: str, parameters: dict):
552
572
  super().__init__(f"Custom {workflow_step}", f"Execute {workflow_step}")
553
573
  self.workflow_step = workflow_step
554
574
  self.parameters = parameters
555
-
575
+
556
576
  def execute(self, driver, context):
557
577
  # Implement custom workflow logic
558
578
  return self.perform_workflow_step(driver, context)
@@ -569,12 +589,12 @@ ecommerce_suite = [
569
589
  payment_security_test, # Test payment form security
570
590
  user_data_protection_test, # Test PII protection
571
591
  session_management_test, # Test session security
572
-
592
+
573
593
  # Load testing
574
594
  product_catalog_load_test, # High-traffic product browsing
575
595
  checkout_process_load_test, # Concurrent checkout processes
576
596
  search_functionality_test, # Search under load
577
-
597
+
578
598
  # Workflow testing
579
599
  complete_purchase_journey, # End-to-end purchase flow
580
600
  return_process_journey, # Product return workflow
@@ -647,30 +667,30 @@ def analyze_test_results(orchestration_result):
647
667
  print("="*60)
648
668
  print("COMPREHENSIVE TEST ANALYSIS")
649
669
  print("="*60)
650
-
670
+
651
671
  print(f"Total Executions: {orchestration_result.total_executions}")
652
672
  print(f"Success Rate: {orchestration_result.success_rate:.1f}%")
653
673
  print(f"Average Execution Time: {orchestration_result.average_execution_time:.2f}s")
654
-
674
+
655
675
  # Performance metrics
656
676
  if orchestration_result.metadata.get('performance_stats'):
657
677
  stats = orchestration_result.metadata['performance_stats']
658
678
  print(f"Peak Response Time: {stats.get('peak_response_time', 'N/A')}")
659
679
  print(f"95th Percentile: {stats.get('p95_response_time', 'N/A')}")
660
-
680
+
661
681
  # Geographic distribution (if applicable)
662
682
  if orchestration_result.metadata.get('distribution_stats'):
663
683
  dist = orchestration_result.metadata['distribution_stats']
664
684
  print("Geographic Distribution:")
665
685
  for location, count in dist.get('location_usage', {}).items():
666
686
  print(f" {location}: {count} executions")
667
-
687
+
668
688
  # Error analysis
669
689
  if orchestration_result.errors:
670
690
  print(f"\nErrors Encountered: {len(orchestration_result.errors)}")
671
691
  for i, error in enumerate(orchestration_result.errors[:5], 1):
672
692
  print(f" {i}. {error}")
673
-
693
+
674
694
  print("="*60)
675
695
 
676
696
  # Use with any orchestration result
@@ -8,7 +8,7 @@ with open("./requirements.txt", "r", encoding="utf-8") as f:
8
8
 
9
9
  setuptools.setup(
10
10
  name="scythe-ttp",
11
- version="0.11.0",
11
+ version="0.12.4",
12
12
  author="EpykLab",
13
13
  author_email="cyber@epyklab.com",
14
14
  description="An extensible framework for emulating attacker TTPs with Selenium.",
scythe_ttp-0.11.0/VERSION DELETED
@@ -1 +0,0 @@
1
- 0.12.1
File without changes
File without changes
File without changes