scythe-ttp 0.12.4__tar.gz → 0.14.0__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.12.4 → scythe_ttp-0.14.0}/PKG-INFO +84 -16
- scythe_ttp-0.12.4/scythe_ttp.egg-info/PKG-INFO → scythe_ttp-0.14.0/README.md +76 -47
- scythe_ttp-0.14.0/VERSION +1 -0
- scythe_ttp-0.14.0/pyproject.toml +56 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/requirements.txt +6 -2
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/auth/__init__.py +3 -1
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/auth/base.py +9 -0
- scythe_ttp-0.14.0/scythe/auth/cookie_jwt.py +172 -0
- scythe_ttp-0.14.0/scythe/cli/__init__.py +3 -0
- scythe_ttp-0.14.0/scythe/cli/main.py +601 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/core/headers.py +69 -9
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/journeys/__init__.py +2 -1
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/journeys/actions.py +235 -1
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/journeys/base.py +161 -12
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/journeys/executor.py +102 -22
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/ttps/web/uuid_guessing.py +3 -2
- scythe_ttp-0.12.4/README.md → scythe_ttp-0.14.0/scythe_ttp.egg-info/PKG-INFO +115 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe_ttp.egg-info/SOURCES.txt +8 -1
- scythe_ttp-0.14.0/scythe_ttp.egg-info/entry_points.txt +2 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe_ttp.egg-info/requires.txt +5 -2
- scythe_ttp-0.14.0/scythe_ttp.egg-info/top_level.txt +4 -0
- scythe_ttp-0.14.0/tests/test_api_models.py +126 -0
- scythe_ttp-0.14.0/tests/test_cli.py +152 -0
- scythe_ttp-0.14.0/tests/test_cookie_jwt_auth.py +201 -0
- scythe_ttp-0.12.4/VERSION +0 -1
- scythe_ttp-0.12.4/scythe_ttp.egg-info/top_level.txt +0 -1
- scythe_ttp-0.12.4/setup.py +0 -43
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/LICENSE +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/MANIFEST.in +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/__init__.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/auth/basic.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/auth/bearer.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/__init__.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/base.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/default.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/human.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/machine.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/stealth.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/core/__init__.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/core/executor.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/core/ttp.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/orchestrators/__init__.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/orchestrators/base.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/orchestrators/batch.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/orchestrators/distributed.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/orchestrators/scale.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/payloads/__init__.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/payloads/generators.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/ttps/__init__.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/ttps/web/__init__.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/ttps/web/login_bruteforce.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/ttps/web/sql_injection.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe_ttp.egg-info/dependency_links.txt +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/setup.cfg +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_authentication.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_behaviors.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_expected_results.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_feature_completeness.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_header_extraction.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_journeys.py +0 -0
- {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_orchestrators.py +0 -0
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scythe-ttp
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: An extensible framework for emulating attacker TTPs with Selenium.
|
|
5
|
-
|
|
6
|
-
Author: EpykLab
|
|
7
|
-
Author-email: cyber@epyklab.com
|
|
5
|
+
Author-email: EpykLab <cyber@epyklab.com>
|
|
8
6
|
Classifier: Programming Language :: Python :: 3
|
|
9
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
8
|
Classifier: Operating System :: OS Independent
|
|
@@ -13,37 +11,31 @@ Classifier: Intended Audience :: Developers
|
|
|
13
11
|
Classifier: Intended Audience :: Information Technology
|
|
14
12
|
Classifier: Topic :: Security
|
|
15
13
|
Classifier: Framework :: Pytest
|
|
16
|
-
Requires-Python:
|
|
14
|
+
Requires-Python: <=3.13,>=3.8
|
|
17
15
|
Description-Content-Type: text/markdown
|
|
18
16
|
License-File: LICENSE
|
|
17
|
+
Requires-Dist: PySocks==1.7.1
|
|
19
18
|
Requires-Dist: attrs==25.3.0
|
|
20
19
|
Requires-Dist: certifi==2025.6.15
|
|
21
20
|
Requires-Dist: charset-normalizer==3.4.2
|
|
22
21
|
Requires-Dist: h11==0.16.0
|
|
23
22
|
Requires-Dist: idna==3.10
|
|
24
23
|
Requires-Dist: outcome==1.3.0.post0
|
|
25
|
-
Requires-Dist:
|
|
24
|
+
Requires-Dist: pydantic-core==2.18.2
|
|
25
|
+
Requires-Dist: pydantic==2.7.1
|
|
26
26
|
Requires-Dist: requests==2.32.4
|
|
27
27
|
Requires-Dist: selenium==4.34.0
|
|
28
28
|
Requires-Dist: setuptools==80.9.0
|
|
29
29
|
Requires-Dist: sniffio==1.3.1
|
|
30
30
|
Requires-Dist: sortedcontainers==2.4.0
|
|
31
|
-
Requires-Dist: trio==0.30.0
|
|
32
31
|
Requires-Dist: trio-websocket==0.12.2
|
|
32
|
+
Requires-Dist: trio==0.30.0
|
|
33
33
|
Requires-Dist: typing_extensions==4.14.0
|
|
34
34
|
Requires-Dist: urllib3==2.4.0
|
|
35
35
|
Requires-Dist: websocket-client==1.8.0
|
|
36
36
|
Requires-Dist: wsproto==1.2.0
|
|
37
|
-
|
|
38
|
-
Dynamic: author-email
|
|
39
|
-
Dynamic: classifier
|
|
40
|
-
Dynamic: description
|
|
41
|
-
Dynamic: description-content-type
|
|
42
|
-
Dynamic: home-page
|
|
37
|
+
Requires-Dist: typer
|
|
43
38
|
Dynamic: license-file
|
|
44
|
-
Dynamic: requires-dist
|
|
45
|
-
Dynamic: requires-python
|
|
46
|
-
Dynamic: summary
|
|
47
39
|
|
|
48
40
|
<h1 align="center">Scythe</h1>
|
|
49
41
|
|
|
@@ -792,3 +784,79 @@ This architecture supports testing scenarios from simple security checks to comp
|
|
|
792
784
|
---
|
|
793
785
|
|
|
794
786
|
**Scythe**: Comprehensive adverse conditions testing for robust, reliable systems.
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
## Scythe CLI (embedded)
|
|
791
|
+
|
|
792
|
+
Scythe now ships with a lightweight CLI that helps you bootstrap and manage your local Scythe testing workspace. After installing the package (pipx recommended), a `scythe` command is available.
|
|
793
|
+
|
|
794
|
+
Note: The CLI is implemented with Typer, so `scythe --help` and per-command help (e.g., `scythe run --help`) are available. Command names and options remain the same as before.
|
|
795
|
+
|
|
796
|
+
- Install with pipx:
|
|
797
|
+
- pipx install scythe-ttp
|
|
798
|
+
- Or install locally in editable mode for development:
|
|
799
|
+
- pip install -e .
|
|
800
|
+
|
|
801
|
+
### Commands
|
|
802
|
+
|
|
803
|
+
- scythe init [--path PATH]
|
|
804
|
+
- Initializes a Scythe project at PATH (default: current directory).
|
|
805
|
+
- Creates:
|
|
806
|
+
- ./.scythe/scythe.db (SQLite DB with tests and runs tables)
|
|
807
|
+
- ./.scythe/scythe_tests/ (where your test scripts live)
|
|
808
|
+
|
|
809
|
+
- scythe new <name>
|
|
810
|
+
- Creates a new test template at ./.scythe/scythe_tests/<name>.py and registers it in the DB (tests table).
|
|
811
|
+
|
|
812
|
+
- scythe run <name or name.py>
|
|
813
|
+
- Runs the specified test from ./.scythe/scythe_tests and records the run into the DB (runs table). Exit code reflects success (0) or failure (non-zero).
|
|
814
|
+
|
|
815
|
+
- scythe db dump
|
|
816
|
+
- Prints a JSON dump of the tests and runs tables from ./.scythe/scythe.db.
|
|
817
|
+
|
|
818
|
+
- scythe db sync-compat <name>
|
|
819
|
+
- Reads COMPATIBLE_VERSIONS from ./.scythe/scythe_tests/<name>.py (if present) and updates the `tests.compatible_versions` field in the DB. If the variable is missing, the DB entry is set to empty and the command exits successfully.
|
|
820
|
+
|
|
821
|
+
### Test template
|
|
822
|
+
|
|
823
|
+
Created tests use a minimal template so you can start quickly:
|
|
824
|
+
|
|
825
|
+
```python
|
|
826
|
+
#!/usr/bin/env python3
|
|
827
|
+
|
|
828
|
+
# scythe test initial template
|
|
829
|
+
|
|
830
|
+
import argparse
|
|
831
|
+
import os
|
|
832
|
+
import sys
|
|
833
|
+
import time
|
|
834
|
+
from typing import List, Tuple
|
|
835
|
+
|
|
836
|
+
# Scythe framework imports
|
|
837
|
+
from scythe.core.executor import TTPExecutor
|
|
838
|
+
from scythe.behaviors import HumanBehavior
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def scythe_test_definition(args):
|
|
842
|
+
# TODO: implement your test using Scythe primitives.
|
|
843
|
+
return True
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
def main():
|
|
847
|
+
parser = argparse.ArgumentParser(description="Scythe test script")
|
|
848
|
+
parser.add_argument('--url', help='Target URL (overridden by localhost unless FORCE_USE_CLI_URL=1)')
|
|
849
|
+
args = parser.parse_args()
|
|
850
|
+
|
|
851
|
+
ok = scythe_test_definition(args)
|
|
852
|
+
sys.exit(0 if ok else 1)
|
|
853
|
+
|
|
854
|
+
|
|
855
|
+
if __name__ == "__main__":
|
|
856
|
+
main()
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
Notes:
|
|
860
|
+
- The CLI looks for tests in ./.scythe/scythe_tests.
|
|
861
|
+
- Each `run` creates a record in the `runs` table with datetime, name_of_test, x_scythe_target_version (best-effort parsed from output), result, raw_output.
|
|
862
|
+
- Each `new` creates a record in the `tests` table with name, path, created_date, compatible_versions.
|
|
@@ -1,50 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: scythe-ttp
|
|
3
|
-
Version: 0.12.4
|
|
4
|
-
Summary: An extensible framework for emulating attacker TTPs with Selenium.
|
|
5
|
-
Home-page: https://github.com/EpykLab/scythe
|
|
6
|
-
Author: EpykLab
|
|
7
|
-
Author-email: cyber@epyklab.com
|
|
8
|
-
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
-
Classifier: Operating System :: OS Independent
|
|
11
|
-
Classifier: Development Status :: 3 - Alpha
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Intended Audience :: Information Technology
|
|
14
|
-
Classifier: Topic :: Security
|
|
15
|
-
Classifier: Framework :: Pytest
|
|
16
|
-
Requires-Python: >=3.8
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
License-File: LICENSE
|
|
19
|
-
Requires-Dist: attrs==25.3.0
|
|
20
|
-
Requires-Dist: certifi==2025.6.15
|
|
21
|
-
Requires-Dist: charset-normalizer==3.4.2
|
|
22
|
-
Requires-Dist: h11==0.16.0
|
|
23
|
-
Requires-Dist: idna==3.10
|
|
24
|
-
Requires-Dist: outcome==1.3.0.post0
|
|
25
|
-
Requires-Dist: PySocks==1.7.1
|
|
26
|
-
Requires-Dist: requests==2.32.4
|
|
27
|
-
Requires-Dist: selenium==4.34.0
|
|
28
|
-
Requires-Dist: setuptools==80.9.0
|
|
29
|
-
Requires-Dist: sniffio==1.3.1
|
|
30
|
-
Requires-Dist: sortedcontainers==2.4.0
|
|
31
|
-
Requires-Dist: trio==0.30.0
|
|
32
|
-
Requires-Dist: trio-websocket==0.12.2
|
|
33
|
-
Requires-Dist: typing_extensions==4.14.0
|
|
34
|
-
Requires-Dist: urllib3==2.4.0
|
|
35
|
-
Requires-Dist: websocket-client==1.8.0
|
|
36
|
-
Requires-Dist: wsproto==1.2.0
|
|
37
|
-
Dynamic: author
|
|
38
|
-
Dynamic: author-email
|
|
39
|
-
Dynamic: classifier
|
|
40
|
-
Dynamic: description
|
|
41
|
-
Dynamic: description-content-type
|
|
42
|
-
Dynamic: home-page
|
|
43
|
-
Dynamic: license-file
|
|
44
|
-
Dynamic: requires-dist
|
|
45
|
-
Dynamic: requires-python
|
|
46
|
-
Dynamic: summary
|
|
47
|
-
|
|
48
1
|
<h1 align="center">Scythe</h1>
|
|
49
2
|
|
|
50
3
|
<h2 align="center">
|
|
@@ -792,3 +745,79 @@ This architecture supports testing scenarios from simple security checks to comp
|
|
|
792
745
|
---
|
|
793
746
|
|
|
794
747
|
**Scythe**: Comprehensive adverse conditions testing for robust, reliable systems.
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
## Scythe CLI (embedded)
|
|
752
|
+
|
|
753
|
+
Scythe now ships with a lightweight CLI that helps you bootstrap and manage your local Scythe testing workspace. After installing the package (pipx recommended), a `scythe` command is available.
|
|
754
|
+
|
|
755
|
+
Note: The CLI is implemented with Typer, so `scythe --help` and per-command help (e.g., `scythe run --help`) are available. Command names and options remain the same as before.
|
|
756
|
+
|
|
757
|
+
- Install with pipx:
|
|
758
|
+
- pipx install scythe-ttp
|
|
759
|
+
- Or install locally in editable mode for development:
|
|
760
|
+
- pip install -e .
|
|
761
|
+
|
|
762
|
+
### Commands
|
|
763
|
+
|
|
764
|
+
- scythe init [--path PATH]
|
|
765
|
+
- Initializes a Scythe project at PATH (default: current directory).
|
|
766
|
+
- Creates:
|
|
767
|
+
- ./.scythe/scythe.db (SQLite DB with tests and runs tables)
|
|
768
|
+
- ./.scythe/scythe_tests/ (where your test scripts live)
|
|
769
|
+
|
|
770
|
+
- scythe new <name>
|
|
771
|
+
- Creates a new test template at ./.scythe/scythe_tests/<name>.py and registers it in the DB (tests table).
|
|
772
|
+
|
|
773
|
+
- scythe run <name or name.py>
|
|
774
|
+
- Runs the specified test from ./.scythe/scythe_tests and records the run into the DB (runs table). Exit code reflects success (0) or failure (non-zero).
|
|
775
|
+
|
|
776
|
+
- scythe db dump
|
|
777
|
+
- Prints a JSON dump of the tests and runs tables from ./.scythe/scythe.db.
|
|
778
|
+
|
|
779
|
+
- scythe db sync-compat <name>
|
|
780
|
+
- Reads COMPATIBLE_VERSIONS from ./.scythe/scythe_tests/<name>.py (if present) and updates the `tests.compatible_versions` field in the DB. If the variable is missing, the DB entry is set to empty and the command exits successfully.
|
|
781
|
+
|
|
782
|
+
### Test template
|
|
783
|
+
|
|
784
|
+
Created tests use a minimal template so you can start quickly:
|
|
785
|
+
|
|
786
|
+
```python
|
|
787
|
+
#!/usr/bin/env python3
|
|
788
|
+
|
|
789
|
+
# scythe test initial template
|
|
790
|
+
|
|
791
|
+
import argparse
|
|
792
|
+
import os
|
|
793
|
+
import sys
|
|
794
|
+
import time
|
|
795
|
+
from typing import List, Tuple
|
|
796
|
+
|
|
797
|
+
# Scythe framework imports
|
|
798
|
+
from scythe.core.executor import TTPExecutor
|
|
799
|
+
from scythe.behaviors import HumanBehavior
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def scythe_test_definition(args):
|
|
803
|
+
# TODO: implement your test using Scythe primitives.
|
|
804
|
+
return True
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def main():
|
|
808
|
+
parser = argparse.ArgumentParser(description="Scythe test script")
|
|
809
|
+
parser.add_argument('--url', help='Target URL (overridden by localhost unless FORCE_USE_CLI_URL=1)')
|
|
810
|
+
args = parser.parse_args()
|
|
811
|
+
|
|
812
|
+
ok = scythe_test_definition(args)
|
|
813
|
+
sys.exit(0 if ok else 1)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
if __name__ == "__main__":
|
|
817
|
+
main()
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
Notes:
|
|
821
|
+
- The CLI looks for tests in ./.scythe/scythe_tests.
|
|
822
|
+
- Each `run` creates a record in the `runs` table with datetime, name_of_test, x_scythe_target_version (best-effort parsed from output), result, raw_output.
|
|
823
|
+
- Each `new` creates a record in the `tests` table with name, path, created_date, compatible_versions.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.15.0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scythe-ttp"
|
|
7
|
+
description = "An extensible framework for emulating attacker TTPs with Selenium."
|
|
8
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
9
|
+
authors = [{name = "EpykLab", email = "cyber@epyklab.com"}]
|
|
10
|
+
requires-python = ">=3.8,<=3.13"
|
|
11
|
+
version = "0.14.0"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: Information Technology",
|
|
19
|
+
"Topic :: Security",
|
|
20
|
+
"Framework :: Pytest",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
dependencies = [
|
|
24
|
+
"PySocks==1.7.1",
|
|
25
|
+
"attrs==25.3.0",
|
|
26
|
+
"certifi==2025.6.15",
|
|
27
|
+
"charset-normalizer==3.4.2",
|
|
28
|
+
"h11==0.16.0",
|
|
29
|
+
"idna==3.10",
|
|
30
|
+
"outcome==1.3.0.post0",
|
|
31
|
+
"pydantic-core==2.18.2",
|
|
32
|
+
"pydantic==2.7.1",
|
|
33
|
+
"requests==2.32.4",
|
|
34
|
+
"selenium==4.34.0",
|
|
35
|
+
"setuptools==80.9.0",
|
|
36
|
+
"sniffio==1.3.1",
|
|
37
|
+
"sortedcontainers==2.4.0",
|
|
38
|
+
"trio-websocket==0.12.2",
|
|
39
|
+
"trio==0.30.0",
|
|
40
|
+
"typing_extensions==4.14.0",
|
|
41
|
+
"urllib3==2.4.0",
|
|
42
|
+
"websocket-client==1.8.0",
|
|
43
|
+
"wsproto==1.2.0",
|
|
44
|
+
"typer"
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.scripts]
|
|
48
|
+
scythe = "scythe.cli.main:main"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools]
|
|
51
|
+
# Use find_packages and exclude tests/examples from the distribution
|
|
52
|
+
packages = {find = {exclude = ["tests*", "examples*"]}}
|
|
53
|
+
|
|
54
|
+
[tool.setuptools.dynamic]
|
|
55
|
+
version = {file = "VERSION"}
|
|
56
|
+
dependencies = {file = "requirements.txt"}
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
+
PySocks==1.7.1
|
|
1
2
|
attrs==25.3.0
|
|
2
3
|
certifi==2025.6.15
|
|
3
4
|
charset-normalizer==3.4.2
|
|
4
5
|
h11==0.16.0
|
|
5
6
|
idna==3.10
|
|
6
7
|
outcome==1.3.0.post0
|
|
7
|
-
|
|
8
|
+
pydantic-core==2.18.2
|
|
9
|
+
pydantic==2.7.1
|
|
8
10
|
requests==2.32.4
|
|
9
11
|
selenium==4.34.0
|
|
10
12
|
setuptools==80.9.0
|
|
11
13
|
sniffio==1.3.1
|
|
12
14
|
sortedcontainers==2.4.0
|
|
13
|
-
trio==0.30.0
|
|
14
15
|
trio-websocket==0.12.2
|
|
16
|
+
trio==0.30.0
|
|
15
17
|
typing_extensions==4.14.0
|
|
16
18
|
urllib3==2.4.0
|
|
17
19
|
websocket-client==1.8.0
|
|
18
20
|
wsproto==1.2.0
|
|
21
|
+
# CLI framework
|
|
22
|
+
typer==0.12.5
|
|
@@ -8,9 +8,11 @@ authenticate before executing their main functionality.
|
|
|
8
8
|
from .base import Authentication
|
|
9
9
|
from .bearer import BearerTokenAuth
|
|
10
10
|
from .basic import BasicAuth
|
|
11
|
+
from .cookie_jwt import CookieJWTAuth
|
|
11
12
|
|
|
12
13
|
__all__ = [
|
|
13
14
|
'Authentication',
|
|
14
15
|
'BearerTokenAuth',
|
|
15
|
-
'BasicAuth'
|
|
16
|
+
'BasicAuth',
|
|
17
|
+
'CookieJWTAuth',
|
|
16
18
|
]
|
|
@@ -80,6 +80,15 @@ class Authentication(ABC):
|
|
|
80
80
|
"""
|
|
81
81
|
return {}
|
|
82
82
|
|
|
83
|
+
def get_auth_cookies(self) -> Dict[str, str]:
|
|
84
|
+
"""
|
|
85
|
+
Get authentication cookies that should be set for API requests.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Dictionary mapping cookie name to cookie value.
|
|
89
|
+
"""
|
|
90
|
+
return {}
|
|
91
|
+
|
|
83
92
|
def store_auth_data(self, key: str, value: Any) -> None:
|
|
84
93
|
"""
|
|
85
94
|
Store authentication-related data.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from typing import Dict, Optional, Any
|
|
5
|
+
from urllib.parse import urlparse
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import requests # type: ignore
|
|
9
|
+
except Exception: # pragma: no cover - tests may run without requests installed
|
|
10
|
+
requests = None # type: ignore
|
|
11
|
+
from selenium.webdriver.remote.webdriver import WebDriver
|
|
12
|
+
|
|
13
|
+
from .base import Authentication, AuthenticationError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _extract_by_dot_path(data: Any, path: str) -> Optional[Any]:
|
|
17
|
+
"""
|
|
18
|
+
Extract a value from a nested dict/list structure using a simple dot path.
|
|
19
|
+
Supports numeric indices for lists, e.g., "data.items.0.token".
|
|
20
|
+
"""
|
|
21
|
+
if not path:
|
|
22
|
+
return None
|
|
23
|
+
parts = path.split(".")
|
|
24
|
+
current: Any = data
|
|
25
|
+
for part in parts:
|
|
26
|
+
if isinstance(current, dict):
|
|
27
|
+
if part in current:
|
|
28
|
+
current = current[part]
|
|
29
|
+
else:
|
|
30
|
+
return None
|
|
31
|
+
elif isinstance(current, list):
|
|
32
|
+
try:
|
|
33
|
+
idx = int(part)
|
|
34
|
+
except ValueError:
|
|
35
|
+
return None
|
|
36
|
+
if idx < 0 or idx >= len(current):
|
|
37
|
+
return None
|
|
38
|
+
current = current[idx]
|
|
39
|
+
else:
|
|
40
|
+
return None
|
|
41
|
+
return current
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CookieJWTAuth(Authentication):
|
|
45
|
+
"""
|
|
46
|
+
Hybrid authentication where a JWT is acquired from an API login response and
|
|
47
|
+
then used as a cookie for subsequent requests. Useful when the target server
|
|
48
|
+
expects a cookie (e.g., "stellarbridge") instead of Authorization headers.
|
|
49
|
+
|
|
50
|
+
Behavior:
|
|
51
|
+
- In API mode: JourneyExecutor will call get_auth_cookies(); this class will
|
|
52
|
+
perform a POST to login_url (if token not cached), parse JSON, extract the
|
|
53
|
+
token via jwt_json_path, and return {cookie_name: token}.
|
|
54
|
+
- In UI mode: authenticate() will ensure the browser has the cookie set for
|
|
55
|
+
the target domain.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self,
|
|
59
|
+
login_url: str,
|
|
60
|
+
username: Optional[str] = None,
|
|
61
|
+
password: Optional[str] = None,
|
|
62
|
+
username_field: str = "email",
|
|
63
|
+
password_field: str = "password",
|
|
64
|
+
extra_fields: Optional[Dict[str, Any]] = None,
|
|
65
|
+
jwt_json_path: str = "token",
|
|
66
|
+
cookie_name: str = "stellarbridge",
|
|
67
|
+
session: Optional[requests.Session] = None,
|
|
68
|
+
description: str = "Authenticate via API and set JWT cookie"):
|
|
69
|
+
super().__init__(
|
|
70
|
+
name="Cookie JWT Authentication",
|
|
71
|
+
description=description
|
|
72
|
+
)
|
|
73
|
+
self.login_url = login_url
|
|
74
|
+
self.username = username
|
|
75
|
+
self.password = password
|
|
76
|
+
self.username_field = username_field
|
|
77
|
+
self.password_field = password_field
|
|
78
|
+
self.extra_fields = extra_fields or {}
|
|
79
|
+
self.jwt_json_path = jwt_json_path
|
|
80
|
+
self.cookie_name = cookie_name
|
|
81
|
+
# Avoid importing requests in test environments; allow injected session
|
|
82
|
+
self._session = session or (requests.Session() if requests is not None else None)
|
|
83
|
+
self.token: Optional[str] = None
|
|
84
|
+
|
|
85
|
+
def _login_and_get_token(self) -> str:
|
|
86
|
+
payload: Dict[str, Any] = dict(self.extra_fields)
|
|
87
|
+
if self.username is not None:
|
|
88
|
+
payload[self.username_field] = self.username
|
|
89
|
+
if self.password is not None:
|
|
90
|
+
payload[self.password_field] = self.password
|
|
91
|
+
try:
|
|
92
|
+
resp = self._session.post(self.login_url, json=payload, timeout=15)
|
|
93
|
+
# try json; raise on non-2xx to surface errors
|
|
94
|
+
resp.raise_for_status()
|
|
95
|
+
data = resp.json()
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise AuthenticationError(f"Login request failed: {e}", self.name)
|
|
98
|
+
token = _extract_by_dot_path(data, self.jwt_json_path)
|
|
99
|
+
if not token or not isinstance(token, str):
|
|
100
|
+
raise AuthenticationError(
|
|
101
|
+
f"JWT not found at path '{self.jwt_json_path}' in login response",
|
|
102
|
+
self.name,
|
|
103
|
+
)
|
|
104
|
+
self.token = token
|
|
105
|
+
self.store_auth_data('jwt', token)
|
|
106
|
+
self.store_auth_data('login_time', time.time())
|
|
107
|
+
return token
|
|
108
|
+
|
|
109
|
+
def get_auth_cookies(self) -> Dict[str, str]:
|
|
110
|
+
"""
|
|
111
|
+
Return cookie mapping for API mode. Will perform login if token absent.
|
|
112
|
+
"""
|
|
113
|
+
if not self.token:
|
|
114
|
+
self._login_and_get_token()
|
|
115
|
+
if not self.token:
|
|
116
|
+
return {}
|
|
117
|
+
return {self.cookie_name: self.token}
|
|
118
|
+
|
|
119
|
+
def get_auth_headers(self) -> Dict[str, str]:
|
|
120
|
+
"""
|
|
121
|
+
For this hybrid approach, we typically do not use auth headers.
|
|
122
|
+
"""
|
|
123
|
+
return {}
|
|
124
|
+
|
|
125
|
+
def authenticate(self, driver: WebDriver, target_url: str) -> bool:
|
|
126
|
+
"""
|
|
127
|
+
UI path: ensure the cookie exists on the browser for the target domain.
|
|
128
|
+
Will perform the API login if token not yet acquired.
|
|
129
|
+
"""
|
|
130
|
+
try:
|
|
131
|
+
if not self.token:
|
|
132
|
+
self._login_and_get_token()
|
|
133
|
+
if not self.token:
|
|
134
|
+
return False
|
|
135
|
+
# Navigate to the target domain base so cookie domain matches
|
|
136
|
+
parsed = urlparse(target_url)
|
|
137
|
+
base = f"{parsed.scheme}://{parsed.netloc}"
|
|
138
|
+
try:
|
|
139
|
+
driver.get(base)
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
cookie_dict = {
|
|
143
|
+
'name': self.cookie_name,
|
|
144
|
+
'value': self.token,
|
|
145
|
+
'path': '/',
|
|
146
|
+
}
|
|
147
|
+
# If domain available, set explicitly to be safe
|
|
148
|
+
if parsed.netloc:
|
|
149
|
+
cookie_dict['domain'] = parsed.hostname or parsed.netloc
|
|
150
|
+
driver.add_cookie(cookie_dict)
|
|
151
|
+
self.authenticated = True
|
|
152
|
+
return True
|
|
153
|
+
except Exception as e:
|
|
154
|
+
raise AuthenticationError(f"Cookie auth failed: {e}", self.name)
|
|
155
|
+
|
|
156
|
+
def is_authenticated(self, driver: WebDriver) -> bool:
|
|
157
|
+
return self.authenticated and self.token is not None
|
|
158
|
+
|
|
159
|
+
def logout(self, driver: WebDriver) -> bool:
|
|
160
|
+
try:
|
|
161
|
+
self.token = None
|
|
162
|
+
self.authenticated = False
|
|
163
|
+
self.clear_auth_data()
|
|
164
|
+
# Best-effort cookie removal
|
|
165
|
+
try:
|
|
166
|
+
driver.delete_cookie(self.cookie_name)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
super().logout(driver)
|
|
170
|
+
return True
|
|
171
|
+
except Exception:
|
|
172
|
+
return False
|