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.

Files changed (61) hide show
  1. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/PKG-INFO +84 -16
  2. scythe_ttp-0.12.4/scythe_ttp.egg-info/PKG-INFO → scythe_ttp-0.14.0/README.md +76 -47
  3. scythe_ttp-0.14.0/VERSION +1 -0
  4. scythe_ttp-0.14.0/pyproject.toml +56 -0
  5. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/requirements.txt +6 -2
  6. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/auth/__init__.py +3 -1
  7. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/auth/base.py +9 -0
  8. scythe_ttp-0.14.0/scythe/auth/cookie_jwt.py +172 -0
  9. scythe_ttp-0.14.0/scythe/cli/__init__.py +3 -0
  10. scythe_ttp-0.14.0/scythe/cli/main.py +601 -0
  11. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/core/headers.py +69 -9
  12. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/journeys/__init__.py +2 -1
  13. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/journeys/actions.py +235 -1
  14. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/journeys/base.py +161 -12
  15. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/journeys/executor.py +102 -22
  16. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/ttps/web/uuid_guessing.py +3 -2
  17. scythe_ttp-0.12.4/README.md → scythe_ttp-0.14.0/scythe_ttp.egg-info/PKG-INFO +115 -0
  18. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe_ttp.egg-info/SOURCES.txt +8 -1
  19. scythe_ttp-0.14.0/scythe_ttp.egg-info/entry_points.txt +2 -0
  20. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe_ttp.egg-info/requires.txt +5 -2
  21. scythe_ttp-0.14.0/scythe_ttp.egg-info/top_level.txt +4 -0
  22. scythe_ttp-0.14.0/tests/test_api_models.py +126 -0
  23. scythe_ttp-0.14.0/tests/test_cli.py +152 -0
  24. scythe_ttp-0.14.0/tests/test_cookie_jwt_auth.py +201 -0
  25. scythe_ttp-0.12.4/VERSION +0 -1
  26. scythe_ttp-0.12.4/scythe_ttp.egg-info/top_level.txt +0 -1
  27. scythe_ttp-0.12.4/setup.py +0 -43
  28. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/LICENSE +0 -0
  29. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/MANIFEST.in +0 -0
  30. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/__init__.py +0 -0
  31. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/auth/basic.py +0 -0
  32. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/auth/bearer.py +0 -0
  33. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/__init__.py +0 -0
  34. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/base.py +0 -0
  35. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/default.py +0 -0
  36. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/human.py +0 -0
  37. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/machine.py +0 -0
  38. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/behaviors/stealth.py +0 -0
  39. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/core/__init__.py +0 -0
  40. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/core/executor.py +0 -0
  41. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/core/ttp.py +0 -0
  42. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/orchestrators/__init__.py +0 -0
  43. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/orchestrators/base.py +0 -0
  44. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/orchestrators/batch.py +0 -0
  45. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/orchestrators/distributed.py +0 -0
  46. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/orchestrators/scale.py +0 -0
  47. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/payloads/__init__.py +0 -0
  48. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/payloads/generators.py +0 -0
  49. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/ttps/__init__.py +0 -0
  50. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/ttps/web/__init__.py +0 -0
  51. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/ttps/web/login_bruteforce.py +0 -0
  52. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe/ttps/web/sql_injection.py +0 -0
  53. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/scythe_ttp.egg-info/dependency_links.txt +0 -0
  54. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/setup.cfg +0 -0
  55. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_authentication.py +0 -0
  56. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_behaviors.py +0 -0
  57. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_expected_results.py +0 -0
  58. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_feature_completeness.py +0 -0
  59. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_header_extraction.py +0 -0
  60. {scythe_ttp-0.12.4 → scythe_ttp-0.14.0}/tests/test_journeys.py +0 -0
  61. {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.12.4
3
+ Version: 0.14.0
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
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: >=3.8
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: PySocks==1.7.1
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
- Dynamic: author
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
- PySocks==1.7.1
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
@@ -0,0 +1,3 @@
1
+ from .main import main
2
+
3
+ __all__ = ["main"]