scythe-ttp 0.11.0__tar.gz → 0.12.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.
Potentially problematic release.
This version of scythe-ttp might be problematic. Click here for more details.
- {scythe_ttp-0.11.0/scythe_ttp.egg-info → scythe_ttp-0.12.1}/PKG-INFO +36 -16
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/README.md +36 -16
- scythe_ttp-0.12.1/VERSION +1 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/core/headers.py +45 -45
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1/scythe_ttp.egg-info}/PKG-INFO +36 -16
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/setup.py +1 -1
- scythe_ttp-0.11.0/VERSION +0 -1
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/LICENSE +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/MANIFEST.in +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/requirements.txt +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/auth/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/auth/base.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/auth/basic.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/auth/bearer.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/behaviors/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/behaviors/base.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/behaviors/default.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/behaviors/human.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/behaviors/machine.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/behaviors/stealth.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/core/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/core/executor.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/core/ttp.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/journeys/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/journeys/actions.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/journeys/base.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/journeys/executor.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/orchestrators/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/orchestrators/base.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/orchestrators/batch.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/orchestrators/distributed.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/orchestrators/scale.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/payloads/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/payloads/generators.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/ttps/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/ttps/web/__init__.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/ttps/web/login_bruteforce.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/ttps/web/sql_injection.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe/ttps/web/uuid_guessing.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe_ttp.egg-info/SOURCES.txt +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe_ttp.egg-info/dependency_links.txt +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe_ttp.egg-info/requires.txt +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/scythe_ttp.egg-info/top_level.txt +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/setup.cfg +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/tests/test_authentication.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/tests/test_behaviors.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/tests/test_expected_results.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/tests/test_feature_completeness.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/tests/test_header_extraction.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/tests/test_journeys.py +0 -0
- {scythe_ttp-0.11.0 → scythe_ttp-0.12.1}/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.1
|
|
4
4
|
Summary: An extensible framework for emulating attacker TTPs with Selenium.
|
|
5
5
|
Home-page: https://github.com/EpykLab/scythe
|
|
6
6
|
Author: EpykLab
|
|
@@ -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.2
|
|
@@ -8,23 +8,23 @@ from selenium.webdriver.chrome.options import Options
|
|
|
8
8
|
class HeaderExtractor:
|
|
9
9
|
"""
|
|
10
10
|
Utility class for extracting HTTP response headers from WebDriver sessions.
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
Specifically designed to capture the X-SCYTHE-TARGET-VERSION header
|
|
13
13
|
that indicates the version of the web application being tested.
|
|
14
14
|
"""
|
|
15
|
-
|
|
16
|
-
SCYTHE_VERSION_HEADER = "X-
|
|
17
|
-
|
|
15
|
+
|
|
16
|
+
SCYTHE_VERSION_HEADER = "X-Scythe-Target-Version"
|
|
17
|
+
|
|
18
18
|
def __init__(self):
|
|
19
19
|
self.logger = logging.getLogger("HeaderExtractor")
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
@staticmethod
|
|
22
22
|
def enable_logging_for_driver(chrome_options: Options) -> None:
|
|
23
23
|
"""
|
|
24
24
|
Enable performance logging capabilities for Chrome WebDriver.
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
This must be called during WebDriver setup to capture network logs.
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
Args:
|
|
29
29
|
chrome_options: Chrome options object to modify
|
|
30
30
|
"""
|
|
@@ -32,15 +32,15 @@ class HeaderExtractor:
|
|
|
32
32
|
chrome_options.add_argument("--enable-logging")
|
|
33
33
|
chrome_options.add_argument("--log-level=0")
|
|
34
34
|
chrome_options.set_capability("goog:loggingPrefs", {"performance": "ALL"})
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
def extract_target_version(self, driver: WebDriver, target_url: Optional[str] = None) -> Optional[str]:
|
|
37
37
|
"""
|
|
38
38
|
Extract the X-SCYTHE-TARGET-VERSION header from the most recent HTTP response.
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
Args:
|
|
41
41
|
driver: WebDriver instance with performance logging enabled
|
|
42
42
|
target_url: Optional URL to filter responses for (if None, uses any response)
|
|
43
|
-
|
|
43
|
+
|
|
44
44
|
Returns:
|
|
45
45
|
Version string if header found, None otherwise
|
|
46
46
|
"""
|
|
@@ -49,77 +49,77 @@ class HeaderExtractor:
|
|
|
49
49
|
if not hasattr(driver, 'get_log'):
|
|
50
50
|
self.logger.warning("WebDriver does not support get_log method")
|
|
51
51
|
return None
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
logs = getattr(driver, 'get_log')('performance')
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
# Look for Network.responseReceived events
|
|
56
56
|
for log_entry in reversed(logs): # Start with most recent
|
|
57
57
|
try:
|
|
58
58
|
message = log_entry.get('message', {})
|
|
59
59
|
if isinstance(message, str):
|
|
60
60
|
message = json.loads(message)
|
|
61
|
-
|
|
61
|
+
|
|
62
62
|
method = message.get('message', {}).get('method', '')
|
|
63
63
|
params = message.get('message', {}).get('params', {})
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
if method == 'Network.responseReceived':
|
|
66
66
|
response = params.get('response', {})
|
|
67
67
|
headers = response.get('headers', {})
|
|
68
68
|
response_url = response.get('url', '')
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
# Filter by target URL if specified
|
|
71
71
|
if target_url and target_url not in response_url:
|
|
72
72
|
continue
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
# Look for the version header (case-insensitive)
|
|
75
75
|
version = self._find_version_header(headers)
|
|
76
76
|
if version:
|
|
77
77
|
self.logger.debug(f"Found target version '{version}' in response from {response_url}")
|
|
78
78
|
return version
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
except (json.JSONDecodeError, KeyError, AttributeError) as e:
|
|
81
81
|
self.logger.debug(f"Error parsing log entry: {e}")
|
|
82
82
|
continue
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
self.logger.debug("No X-SCYTHE-TARGET-VERSION header found in network logs")
|
|
85
85
|
return None
|
|
86
|
-
|
|
86
|
+
|
|
87
87
|
except Exception as e:
|
|
88
88
|
self.logger.warning(f"Failed to extract target version from logs: {e}")
|
|
89
89
|
return None
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
def _find_version_header(self, headers: Dict[str, Any]) -> Optional[str]:
|
|
92
92
|
"""
|
|
93
93
|
Find the version header in a case-insensitive manner.
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
Args:
|
|
96
96
|
headers: Dictionary of HTTP headers
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
Returns:
|
|
99
99
|
Version string if found, None otherwise
|
|
100
100
|
"""
|
|
101
101
|
# Check for exact case match first
|
|
102
102
|
if self.SCYTHE_VERSION_HEADER in headers:
|
|
103
103
|
return str(headers[self.SCYTHE_VERSION_HEADER]).strip()
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
# Check case-insensitive
|
|
106
106
|
header_lower = self.SCYTHE_VERSION_HEADER.lower()
|
|
107
107
|
for header_name, header_value in headers.items():
|
|
108
108
|
if header_name.lower() == header_lower:
|
|
109
109
|
return str(header_value).strip()
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
return None
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
def extract_all_headers(self, driver: WebDriver, target_url: Optional[str] = None) -> Dict[str, str]:
|
|
114
114
|
"""
|
|
115
115
|
Extract all headers from the most recent HTTP response.
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
Useful for debugging or when additional headers might be needed.
|
|
118
|
-
|
|
118
|
+
|
|
119
119
|
Args:
|
|
120
120
|
driver: WebDriver instance with performance logging enabled
|
|
121
121
|
target_url: Optional URL to filter responses for
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
Returns:
|
|
124
124
|
Dictionary of headers from the most recent response
|
|
125
125
|
"""
|
|
@@ -128,67 +128,67 @@ class HeaderExtractor:
|
|
|
128
128
|
if not hasattr(driver, 'get_log'):
|
|
129
129
|
self.logger.warning("WebDriver does not support get_log method")
|
|
130
130
|
return {}
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
logs = getattr(driver, 'get_log')('performance')
|
|
133
|
-
|
|
133
|
+
|
|
134
134
|
for log_entry in reversed(logs):
|
|
135
135
|
try:
|
|
136
136
|
message = log_entry.get('message', {})
|
|
137
137
|
if isinstance(message, str):
|
|
138
138
|
message = json.loads(message)
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
method = message.get('message', {}).get('method', '')
|
|
141
141
|
params = message.get('message', {}).get('params', {})
|
|
142
|
-
|
|
142
|
+
|
|
143
143
|
if method == 'Network.responseReceived':
|
|
144
144
|
response = params.get('response', {})
|
|
145
145
|
headers = response.get('headers', {})
|
|
146
146
|
response_url = response.get('url', '')
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
# Filter by target URL if specified
|
|
149
149
|
if target_url and target_url not in response_url:
|
|
150
150
|
continue
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
# Convert all header values to strings
|
|
153
153
|
return {k: str(v) for k, v in headers.items()}
|
|
154
|
-
|
|
154
|
+
|
|
155
155
|
except (json.JSONDecodeError, KeyError, AttributeError):
|
|
156
156
|
continue
|
|
157
|
-
|
|
157
|
+
|
|
158
158
|
return {}
|
|
159
|
-
|
|
159
|
+
|
|
160
160
|
except Exception as e:
|
|
161
161
|
self.logger.warning(f"Failed to extract headers from logs: {e}")
|
|
162
162
|
return {}
|
|
163
|
-
|
|
163
|
+
|
|
164
164
|
def get_version_summary(self, results: list) -> Dict[str, Any]:
|
|
165
165
|
"""
|
|
166
166
|
Analyze version information across multiple test results.
|
|
167
|
-
|
|
167
|
+
|
|
168
168
|
Args:
|
|
169
169
|
results: List of result dictionaries containing version information
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
Returns:
|
|
172
172
|
Dictionary with version analysis summary
|
|
173
173
|
"""
|
|
174
174
|
versions = []
|
|
175
175
|
results_with_version = 0
|
|
176
|
-
|
|
176
|
+
|
|
177
177
|
for result in results:
|
|
178
178
|
version = result.get('target_version')
|
|
179
179
|
if version:
|
|
180
180
|
versions.append(version)
|
|
181
181
|
results_with_version += 1
|
|
182
|
-
|
|
182
|
+
|
|
183
183
|
summary = {
|
|
184
184
|
'total_results': len(results),
|
|
185
185
|
'results_with_version': results_with_version,
|
|
186
186
|
'unique_versions': list(set(versions)) if versions else [],
|
|
187
187
|
'version_counts': {}
|
|
188
188
|
}
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
# Count occurrences of each version
|
|
191
191
|
for version in versions:
|
|
192
192
|
summary['version_counts'][version] = summary['version_counts'].get(version, 0) + 1
|
|
193
|
-
|
|
194
|
-
return summary
|
|
193
|
+
|
|
194
|
+
return summary
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scythe-ttp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.1
|
|
4
4
|
Summary: An extensible framework for emulating attacker TTPs with Selenium.
|
|
5
5
|
Home-page: https://github.com/EpykLab/scythe
|
|
6
6
|
Author: EpykLab
|
|
@@ -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.1",
|
|
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
|