scythe-ttp 0.12.4__py3-none-any.whl → 0.14.0__py3-none-any.whl

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.

@@ -3,6 +3,7 @@ import logging
3
3
  from selenium import webdriver
4
4
  from selenium.webdriver.chrome.options import Options
5
5
  from typing import Optional, Dict, Any, List
6
+ import requests
6
7
  from ..behaviors.base import Behavior
7
8
  from .base import Journey
8
9
  from ..core.headers import HeaderExtractor
@@ -24,6 +25,10 @@ class JourneyExecutor:
24
25
 
25
26
  Similar to TTPExecutor but designed for complex multi-step scenarios
26
27
  involving journeys composed of steps and actions.
28
+
29
+ Supports two interaction modes:
30
+ - UI: browser-driven via Selenium (default, backward-compatible)
31
+ - API: REST-driven via requests without starting a browser
27
32
  """
28
33
 
29
34
  def __init__(self,
@@ -31,7 +36,8 @@ class JourneyExecutor:
31
36
  target_url: str,
32
37
  headless: bool = True,
33
38
  behavior: Optional[Behavior] = None,
34
- driver_options: Optional[Dict[str, Any]] = None):
39
+ driver_options: Optional[Dict[str, Any]] = None,
40
+ mode: str = "UI"):
35
41
  """
36
42
  Initialize the Journey executor.
37
43
 
@@ -45,6 +51,7 @@ class JourneyExecutor:
45
51
  self.journey = journey
46
52
  self.target_url = target_url
47
53
  self.behavior = behavior
54
+ self.mode = (mode or "UI").upper()
48
55
  self.logger = logging.getLogger(f"Journey.{self.journey.name}")
49
56
 
50
57
  # Setup Chrome options
@@ -108,29 +115,62 @@ class JourneyExecutor:
108
115
  self.logger.info(f"Using behavior: {self.behavior.name}")
109
116
  self.logger.info(f"Behavior description: {self.behavior.description}")
110
117
 
111
- self._setup_driver()
112
-
113
118
  try:
114
- # Pre-execution behavior setup
115
- if self.behavior and self.driver:
116
- self.behavior.pre_execution(self.driver, self.target_url)
117
-
118
- # Execute the journey
119
- if self.driver:
120
- self.execution_results = self.journey.execute(self.driver, self.target_url)
119
+ if self.mode == 'API':
120
+ # API mode: no WebDriver, prepare requests session and context
121
+ session = requests.Session()
122
+ auth_headers = {}
123
+ auth_cookies = {}
124
+ if getattr(self.journey, 'authentication', None):
125
+ # Try to obtain headers/cookies directly (no browser flow)
126
+ try:
127
+ auth_headers = self.journey.authentication.get_auth_headers() or {}
128
+ except Exception as e:
129
+ self.logger.warning(f"Failed to get auth headers from authentication: {e}")
130
+ try:
131
+ if hasattr(self.journey.authentication, 'get_auth_cookies'):
132
+ auth_cookies = self.journey.authentication.get_auth_cookies() or {}
133
+ except Exception as e:
134
+ self.logger.warning(f"Failed to get auth cookies from authentication: {e}")
135
+ if auth_headers:
136
+ session.headers.update(auth_headers)
137
+ if auth_cookies:
138
+ for ck, cv in auth_cookies.items():
139
+ try:
140
+ session.cookies.set(ck, cv)
141
+ except Exception:
142
+ pass
143
+
144
+ # Seed journey context for API actions
145
+ self.journey.set_context('mode', 'API')
146
+ self.journey.set_context('requests_session', session)
147
+ self.journey.set_context('auth_headers', auth_headers)
148
+ self.journey.set_context('auth_cookies', auth_cookies)
149
+
150
+ # Execute a journey with a None driver (API actions ignore a driver)
151
+ self.execution_results = self.journey.execute(None, self.target_url)
121
152
  else:
122
- raise RuntimeError("WebDriver not initialized")
123
-
124
- # Apply behavior timing between steps if configured
125
- if self.behavior:
126
- # Let behavior influence the journey execution
127
- self._apply_behavior_to_journey()
128
-
129
- # Post-execution behavior cleanup
130
- if self.behavior and self.driver:
131
- # Convert journey results to format expected by behavior
132
- behavior_results = self._convert_results_for_behavior()
133
- self.behavior.post_execution(self.driver, behavior_results)
153
+ # UI mode (default)
154
+ self._setup_driver()
155
+
156
+ # Pre-execution behavior setup
157
+ if self.behavior and self.driver:
158
+ self.behavior.pre_execution(self.driver, self.target_url)
159
+
160
+ # Execute the journey
161
+ if self.driver:
162
+ self.execution_results = self.journey.execute(self.driver, self.target_url)
163
+ else:
164
+ raise RuntimeError("WebDriver not initialized")
165
+
166
+ # Apply behavior timing between steps if configured
167
+ if self.behavior:
168
+ self._apply_behavior_to_journey()
169
+
170
+ # Post-execution behavior cleanup
171
+ if self.behavior and self.driver:
172
+ behavior_results = self._convert_results_for_behavior()
173
+ self.behavior.post_execution(self.driver, behavior_results)
134
174
 
135
175
  except KeyboardInterrupt:
136
176
  self.logger.info("Journey interrupted by user.")
@@ -143,6 +183,7 @@ class JourneyExecutor:
143
183
  self.execution_results = self._create_error_results(str(e))
144
184
 
145
185
  finally:
186
+ # Cleanup and print summary (driver quit only if initialized)
146
187
  self._cleanup()
147
188
 
148
189
  return self.execution_results
@@ -311,6 +352,45 @@ class JourneyExecutor:
311
352
 
312
353
  self.logger.info(f" {status} Step {i}: {step_name} - {result_text} ({expected_text}){version_info}")
313
354
  self.logger.info(f" Actions: {len([a for a in actions if a.get('actual', False)])}/{len(actions)} succeeded")
355
+ # Print diagnostic details only for unexpected outcomes
356
+ for a in actions:
357
+ actual = a.get('actual', False)
358
+ expected = a.get('expected', True)
359
+ if actual != expected:
360
+ prefix = "✗ Action failed" if expected else "✗ Action unexpectedly succeeded"
361
+ self.logger.error(f" {prefix}: {a.get('action_name')}")
362
+ ad = a.get('details', {}) or {}
363
+ method = ad.get('request_method')
364
+ url = ad.get('url')
365
+ status_code = ad.get('status_code')
366
+ dur = ad.get('duration_ms')
367
+ parts = []
368
+ if method:
369
+ parts.append(f"method={method}")
370
+ if url:
371
+ parts.append(f"url={url}")
372
+ if status_code is not None:
373
+ parts.append(f"status={status_code}")
374
+ if dur is not None:
375
+ parts.append(f"duration_ms={dur}")
376
+ if parts:
377
+ self.logger.error(" Details: " + ", ".join(parts))
378
+ if ad.get('request_headers'):
379
+ self.logger.error(f" Request headers: {ad.get('request_headers')}")
380
+ if ad.get('request_params'):
381
+ self.logger.error(f" Request params: {ad.get('request_params')}")
382
+ if ad.get('request_json') is not None:
383
+ self.logger.error(f" Request JSON: {ad.get('request_json')}")
384
+ if ad.get('request_data') is not None:
385
+ self.logger.error(f" Request data: {ad.get('request_data')}")
386
+ if ad.get('response_headers'):
387
+ self.logger.error(f" Response headers: {ad.get('response_headers')}")
388
+ if 'response_json' in ad:
389
+ self.logger.error(f" Response JSON: {ad.get('response_json')}")
390
+ elif 'response_text' in ad:
391
+ self.logger.error(f" Response text: {ad.get('response_text')}")
392
+ if ad.get('error'):
393
+ self.logger.error(f" Error: {ad.get('error')}")
314
394
 
315
395
  # Version summary
316
396
  target_versions = self.execution_results.get('target_versions', [])
@@ -5,6 +5,7 @@ from ...payloads.generators import PayloadGenerator
5
5
  import requests
6
6
  from uuid import UUID
7
7
 
8
+
8
9
  class GuessUUIDInURL(TTP):
9
10
  def __init__(self,
10
11
  target_url: str,
@@ -18,10 +19,10 @@ class GuessUUIDInURL(TTP):
18
19
  description="simulate bruteforcing UUID's in the URL path",
19
20
  expected_result=expected_result,
20
21
  authentication=authentication)
21
-
22
+
22
23
  self.target_url = target_url
23
24
  self.uri_root_path = uri_root_path
24
- self.payload_generator = payload_generator
25
+ self.payload_generator = payload_generator
25
26
 
26
27
  def get_payloads(self):
27
28
  yield from self.payload_generator()
@@ -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,22 +1,25 @@
1
1
  scythe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- scythe/auth/__init__.py,sha256=OoAzMnvkTHYG3RRptt_Z79Dsi0sz6lhR6M1W9bF950k,360
3
- scythe/auth/base.py,sha256=qNQgue4Jeu4U5r6RKYVTYi4BktlIp0IvdKbVyB-vVUE,3552
2
+ scythe/auth/__init__.py,sha256=InEANqWEIAULFyzH9IyxWDPs_gJd3m_JYmzoaBk_37M,420
3
+ scythe/auth/base.py,sha256=DllKaPGj0MRyRh4PQgQ2TUFgeAXjgXOT2h6zUz2ZAag,3807
4
4
  scythe/auth/basic.py,sha256=H4IG9-Y7wFe7ZQCNHmmqhre-Pp9CnBxlT23h2uvOPWo,14354
5
5
  scythe/auth/bearer.py,sha256=ngOL-sS6FcfB8XAKMR-CZbpqyySu2MaKxUl10SyBmmI,12687
6
+ scythe/auth/cookie_jwt.py,sha256=z5Q-c594_m-dmh2Rv_4Xfeu0fXSQdlZ12Q-emtyj63g,6337
6
7
  scythe/behaviors/__init__.py,sha256=w-WRBGRgna5a1N8oHP2aXSQnkQUHyOXiujpwEVf_ZyM,291
7
8
  scythe/behaviors/base.py,sha256=INvIYKVIWzEi5w_4njOwKZ3X9IvySvqiMJnYX7_2Lns,3955
8
9
  scythe/behaviors/default.py,sha256=MDx4N-KwC23pPLGu1-ZIkGiTRNUG3Lxjbvo7SJ3UwMc,2117
9
10
  scythe/behaviors/human.py,sha256=1PqYvE7cnxlj-KDmDIr3uzfWHvDAbbxQxJ0V0iTl9yo,10291
10
11
  scythe/behaviors/machine.py,sha256=NDMUq3mDhpCvujzAFxhn2eSVq78-J-LSBhIcvHkzKXo,4624
11
12
  scythe/behaviors/stealth.py,sha256=xv7MrPQgRCdCUJyYTcXV2aasWZoAw8rAQWg-AuQVb7U,15278
13
+ scythe/cli/__init__.py,sha256=9EVxmFiWsAoqWJ6br1bc3BxlA71JyOQP28fUHhX2k7E,43
14
+ scythe/cli/main.py,sha256=ZqkTn2DKro68ODd3jr35QORgglo2RqA9Bw7GZSsY1Y0,21328
12
15
  scythe/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
16
  scythe/core/executor.py,sha256=x1w2nByVu2G70sh7t0kOh6urlrTm_r_pbk0S7v1Ov28,9736
14
- scythe/core/headers.py,sha256=lHlxAwV4xOoR9A0rHDM5k48HmLp1y03uOkXXq68vcAU,13867
17
+ scythe/core/headers.py,sha256=AokCQ3F5QGUcfoK7iO57hA1HHL4IznZeWV464_MqYcE,16670
15
18
  scythe/core/ttp.py,sha256=Xw9GgptYsjZ-pMLdyPv64bhiwGKobrXHdF32pjIY7OU,3102
16
- scythe/journeys/__init__.py,sha256=-8AIpCmkeWtQ656yU3omj_guMG4v4i1koIpD6NZhUGM,612
17
- scythe/journeys/actions.py,sha256=Ez6Bpzs2VHzXMl6GtPve85XxzQV09rDscmDuzSs3VBE,25229
18
- scythe/journeys/base.py,sha256=BWf35Ee3N9qy76Awh-r04-waUTDfLyxssvDmwYToXgY,15461
19
- scythe/journeys/executor.py,sha256=1D_HUzvi4Z7a5uE7QbIDWH7HTGz5DoxcQffr-05bi_0,19978
19
+ scythe/journeys/__init__.py,sha256=Odi8NhRg7Hefmo1EJj1guakrCSPhsuus4i-_62uUUjs,654
20
+ scythe/journeys/actions.py,sha256=cDBYdhY5pCXKG-57-op8gH8z9u3_wbIOhwqSZ2Z_jDs,36432
21
+ scythe/journeys/base.py,sha256=vXIgEnSW__iYTriBbuMG4l_XCM96xojJH_fyFScKoBY,24969
22
+ scythe/journeys/executor.py,sha256=_q2hzl4G9iv07I6NVMtNaK3O8QGLDwLNMiaxIle-nsY,24654
20
23
  scythe/orchestrators/__init__.py,sha256=_vemcXjKbB1jI0F2dPA0F1zNsyUekjcXImLDUDhWDN0,560
21
24
  scythe/orchestrators/base.py,sha256=YOZV0ewlzJ49H08P_LKnimutUms8NnDrQprFpSKhOeM,13595
22
25
  scythe/orchestrators/batch.py,sha256=FpK501kk-earJzz6v7dcuw2y708rTvt_IMH_5qjKdrc,26635
@@ -28,9 +31,10 @@ scythe/ttps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
31
  scythe/ttps/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
32
  scythe/ttps/web/login_bruteforce.py,sha256=D4G8zB_nU9LD5w3Vv2ABTuOl4XTeg2BgZwYMObt4JJw,2488
30
33
  scythe/ttps/web/sql_injection.py,sha256=aWk4DFePbtFDsieOOj03Ux-5OiykyOs2_d_3SvWMOVE,2910
31
- scythe/ttps/web/uuid_guessing.py,sha256=WwCIQPLIixd5U2EY4bhnj7YP2AQDaPfQy7Yhj84UHy8,1245
32
- scythe_ttp-0.12.4.dist-info/licenses/LICENSE,sha256=B7iB4Fv6zDQolC7IgqNF8F4GEp_DLe2jrPPuR_MYMOM,1064
33
- scythe_ttp-0.12.4.dist-info/METADATA,sha256=KeT_tzQrCnJZQtfOFpaMKJCRqdSKbOSZ6xouzqQa3fw,27741
34
- scythe_ttp-0.12.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
- scythe_ttp-0.12.4.dist-info/top_level.txt,sha256=BCKTrPuVvmLyhOR07C1ggOh6sU7g2LoVvwDMn46O55Y,7
36
- scythe_ttp-0.12.4.dist-info/RECORD,,
34
+ scythe/ttps/web/uuid_guessing.py,sha256=JwNt_9HVynMWFPPU6UGJFcpxvMVDsvc_wAnJVtcYbps,1235
35
+ scythe_ttp-0.14.0.dist-info/licenses/LICENSE,sha256=B7iB4Fv6zDQolC7IgqNF8F4GEp_DLe2jrPPuR_MYMOM,1064
36
+ scythe_ttp-0.14.0.dist-info/METADATA,sha256=mfwhEqF4PYFk7TYCvqHwvqBV90vb9rItOoFa5qLTobU,30161
37
+ scythe_ttp-0.14.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ scythe_ttp-0.14.0.dist-info/entry_points.txt,sha256=rAAsFBcCm0OX3I4uRyclfx4YJGoTuumZKY43HN7R5Ro,48
39
+ scythe_ttp-0.14.0.dist-info/top_level.txt,sha256=BCKTrPuVvmLyhOR07C1ggOh6sU7g2LoVvwDMn46O55Y,7
40
+ scythe_ttp-0.14.0.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scythe = scythe.cli.main:main