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.
- {scythe_ttp-0.11.0/scythe_ttp.egg-info → scythe_ttp-0.12.4}/PKG-INFO +36 -16
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/README.md +36 -16
- scythe_ttp-0.12.4/VERSION +1 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/core/headers.py +197 -45
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4/scythe_ttp.egg-info}/PKG-INFO +36 -16
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/setup.py +1 -1
- scythe_ttp-0.11.0/VERSION +0 -1
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/LICENSE +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/MANIFEST.in +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/requirements.txt +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/auth/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/auth/base.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/auth/basic.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/auth/bearer.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/base.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/default.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/human.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/machine.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/behaviors/stealth.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/core/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/core/executor.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/core/ttp.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/journeys/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/journeys/actions.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/journeys/base.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/journeys/executor.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/orchestrators/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/orchestrators/base.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/orchestrators/batch.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/orchestrators/distributed.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/orchestrators/scale.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/payloads/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/payloads/generators.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/ttps/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/ttps/web/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/ttps/web/login_bruteforce.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/ttps/web/sql_injection.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe/ttps/web/uuid_guessing.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe_ttp.egg-info/SOURCES.txt +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe_ttp.egg-info/dependency_links.txt +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe_ttp.egg-info/requires.txt +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/scythe_ttp.egg-info/top_level.txt +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/setup.cfg +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_authentication.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_behaviors.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_expected_results.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_feature_completeness.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_header_extraction.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.4}/tests/test_journeys.py +0 -0
- {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.
|
|
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-
|
|
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.
|
|
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
|
+
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|