pentool 0.2.4__tar.gz → 0.2.5__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.
Files changed (172) hide show
  1. {pentool-0.2.4 → pentool-0.2.5}/PKG-INFO +10 -10
  2. {pentool-0.2.4 → pentool-0.2.5}/README.md +5 -5
  3. {pentool-0.2.4 → pentool-0.2.5}/pentool/__init__.py +29 -2
  4. pentool-0.2.5/pentool/__main__.py +95 -0
  5. {pentool-0.2.4 → pentool-0.2.5}/pentool/cli/main.py +42 -3
  6. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/license.py +167 -28
  7. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/plugin_manager.py +34 -1
  8. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/app.py +39 -5
  9. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/intruder/screen.py +24 -13
  10. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/spider/screen.py +27 -5
  11. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/target/screen.py +30 -15
  12. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/terminal/screen.py +10 -5
  13. pentool-0.2.5/pentool/tui/widgets/activity_indicator.py +215 -0
  14. pentool-0.2.5/pentool/tui/widgets/activity_indicator.tcss +63 -0
  15. {pentool-0.2.4 → pentool-0.2.5}/pentool.egg-info/PKG-INFO +10 -10
  16. {pentool-0.2.4 → pentool-0.2.5}/pyproject.toml +5 -5
  17. pentool-0.2.4/pentool/__main__.py +0 -39
  18. pentool-0.2.4/pentool/tui/widgets/activity_indicator.py +0 -143
  19. pentool-0.2.4/pentool/tui/widgets/activity_indicator.tcss +0 -30
  20. {pentool-0.2.4 → pentool-0.2.5}/LICENSE +0 -0
  21. {pentool-0.2.4 → pentool-0.2.5}/pentool/api/__init__.py +0 -0
  22. {pentool-0.2.4 → pentool-0.2.5}/pentool/api/base_api.py +0 -0
  23. {pentool-0.2.4 → pentool-0.2.5}/pentool/api/comparer_api.py +0 -0
  24. {pentool-0.2.4 → pentool-0.2.5}/pentool/api/decoder_api.py +0 -0
  25. {pentool-0.2.4 → pentool-0.2.5}/pentool/api/intruder_api.py +0 -0
  26. {pentool-0.2.4 → pentool-0.2.5}/pentool/api/proxy_api.py +0 -0
  27. {pentool-0.2.4 → pentool-0.2.5}/pentool/api/repeater_api.py +0 -0
  28. {pentool-0.2.4 → pentool-0.2.5}/pentool/api/sequencer_api.py +0 -0
  29. {pentool-0.2.4 → pentool-0.2.5}/pentool/api/spider_api.py +0 -0
  30. {pentool-0.2.4 → pentool-0.2.5}/pentool/api/target_api.py +0 -0
  31. {pentool-0.2.4 → pentool-0.2.5}/pentool/cli/__init__.py +0 -0
  32. {pentool-0.2.4 → pentool-0.2.5}/pentool/cli/project.py +0 -0
  33. {pentool-0.2.4 → pentool-0.2.5}/pentool/cli/proxy.py +0 -0
  34. {pentool-0.2.4 → pentool-0.2.5}/pentool/cli/scan.py +0 -0
  35. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/__init__.py +0 -0
  36. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/config.py +0 -0
  37. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/crash_reporter.py +0 -0
  38. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/database.py +0 -0
  39. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/event_bus.py +0 -0
  40. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/events.py +0 -0
  41. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/features.py +0 -0
  42. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/logging.py +0 -0
  43. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/project.py +0 -0
  44. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/storage_interface.py +0 -0
  45. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/updater.py +0 -0
  46. {pentool-0.2.4 → pentool-0.2.5}/pentool/core/utils.py +0 -0
  47. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/__init__.py +0 -0
  48. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/comparer.py +0 -0
  49. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/decoder.py +0 -0
  50. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/intruder.py +0 -0
  51. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/match_replace.py +0 -0
  52. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/proxy.py +0 -0
  53. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/repeater.py +0 -0
  54. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/sequencer.py +0 -0
  55. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/spider.py +0 -0
  56. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/target.py +0 -0
  57. {pentool-0.2.4 → pentool-0.2.5}/pentool/modules/websocket_handler.py +0 -0
  58. {pentool-0.2.4 → pentool-0.2.5}/pentool/plugins/__init__.py +0 -0
  59. {pentool-0.2.4 → pentool-0.2.5}/pentool/plugins/builtin/__init__.py +0 -0
  60. {pentool-0.2.4 → pentool-0.2.5}/pentool/plugins/example_plugin.py +0 -0
  61. {pentool-0.2.4 → pentool-0.2.5}/pentool/plugins/example_plugin.tcss +0 -0
  62. {pentool-0.2.4 → pentool-0.2.5}/pentool/services/__init__.py +0 -0
  63. {pentool-0.2.4 → pentool-0.2.5}/pentool/services/base_service.py +0 -0
  64. {pentool-0.2.4 → pentool-0.2.5}/pentool/services/intruder_service.py +0 -0
  65. {pentool-0.2.4 → pentool-0.2.5}/pentool/services/proxy_service.py +0 -0
  66. {pentool-0.2.4 → pentool-0.2.5}/pentool/services/repeater_service.py +0 -0
  67. {pentool-0.2.4 → pentool-0.2.5}/pentool/services/scan_service.py +0 -0
  68. {pentool-0.2.4 → pentool-0.2.5}/pentool/storage/__init__.py +0 -0
  69. {pentool-0.2.4 → pentool-0.2.5}/pentool/storage/http_storage.py +0 -0
  70. {pentool-0.2.4 → pentool-0.2.5}/pentool/storage/large_body_handler.py +0 -0
  71. {pentool-0.2.4 → pentool-0.2.5}/pentool/storage/lru_cache.py +0 -0
  72. {pentool-0.2.4 → pentool-0.2.5}/pentool/t.py +0 -0
  73. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/__init__.py +0 -0
  74. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/constants.py +0 -0
  75. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/__init__.py +0 -0
  76. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/cert_dialog.py +0 -0
  77. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/cert_dialog.tcss +0 -0
  78. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/comment_dialog.py +0 -0
  79. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/comment_dialog.tcss +0 -0
  80. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/file_selector.py +0 -0
  81. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/file_selector.tcss +0 -0
  82. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/load_from_proxy.py +0 -0
  83. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/load_from_proxy.tcss +0 -0
  84. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/match_replace_dialog.py +0 -0
  85. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/match_replace_dialog.tcss +0 -0
  86. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/project_dialog.py +0 -0
  87. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/project_dialog.tcss +0 -0
  88. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/scope_dialog.py +0 -0
  89. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/dialogs/scope_dialog.tcss +0 -0
  90. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/messages.py +0 -0
  91. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/mixins/__init__.py +0 -0
  92. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/mixins/app_mixin.py +0 -0
  93. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/mixins/dialog_cancel.py +0 -0
  94. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/mixins/request_context_menu.py +0 -0
  95. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/project_manager.py +0 -0
  96. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/__init__.py +0 -0
  97. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/base.py +0 -0
  98. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/comparer/__init__.py +0 -0
  99. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/comparer/screen.py +0 -0
  100. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/comparer/screen.tcss +0 -0
  101. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/dashboard/__init__.py +0 -0
  102. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/dashboard/live_dashboard.py +0 -0
  103. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/dashboard/screen.py +0 -0
  104. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/dashboard/screen.tcss +0 -0
  105. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/decoder/__init__.py +0 -0
  106. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/decoder/screen.py +0 -0
  107. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/decoder/screen.tcss +0 -0
  108. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/extensions/__init__.py +0 -0
  109. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/extensions/screen.py +0 -0
  110. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/extensions/screen.tcss +0 -0
  111. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/intruder/__init__.py +0 -0
  112. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/intruder/screen.tcss +0 -0
  113. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/proxy/__init__.py +0 -0
  114. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/proxy/screen.py +0 -0
  115. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/proxy/screen.tcss +0 -0
  116. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/repeater/__init__.py +0 -0
  117. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/repeater/screen.py +0 -0
  118. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/repeater/screen.tcss +0 -0
  119. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/scanner_unavailable.py +0 -0
  120. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/sequencer/__init__.py +0 -0
  121. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/sequencer/screen.py +0 -0
  122. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/sequencer/screen.tcss +0 -0
  123. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/settings/__init__.py +0 -0
  124. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/settings/hotkeys.py +0 -0
  125. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/settings/hotkeys.tcss +0 -0
  126. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/settings/screen.py +0 -0
  127. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/settings/screen.tcss +0 -0
  128. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/spider/__init__.py +0 -0
  129. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/spider/screen.tcss +0 -0
  130. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/target/__init__.py +0 -0
  131. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/target/screen.tcss +0 -0
  132. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/terminal/__init__.py +0 -0
  133. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/screens/terminal/screen.tcss +0 -0
  134. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/__init__.py +0 -0
  135. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/context_menu.py +0 -0
  136. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/context_menu.tcss +0 -0
  137. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/filter_bar.py +0 -0
  138. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/filter_bar.tcss +0 -0
  139. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/inspector_panel.py +0 -0
  140. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/inspector_panel.tcss +0 -0
  141. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/menu.tcss +0 -0
  142. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/menu_bar.tcss +0 -0
  143. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/module_tabs.py +0 -0
  144. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/module_tabs.tcss +0 -0
  145. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/nice_checkbox.py +0 -0
  146. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/payload_drop_zone.py +0 -0
  147. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/payload_drop_zone.tcss +0 -0
  148. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/request_editor.py +0 -0
  149. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/request_editor.tcss +0 -0
  150. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/resize_handle.py +0 -0
  151. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/resize_handle.tcss +0 -0
  152. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/search_bar.py +0 -0
  153. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/search_bar.tcss +0 -0
  154. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/statusbar.py +0 -0
  155. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/statusbar.tcss +0 -0
  156. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/toolbar_button.py +0 -0
  157. {pentool-0.2.4 → pentool-0.2.5}/pentool/tui/widgets/toolbar_button.tcss +0 -0
  158. {pentool-0.2.4 → pentool-0.2.5}/pentool/utils/__init__.py +0 -0
  159. {pentool-0.2.4 → pentool-0.2.5}/pentool/utils/cert.py +0 -0
  160. {pentool-0.2.4 → pentool-0.2.5}/pentool/utils/coder.py +0 -0
  161. {pentool-0.2.4 → pentool-0.2.5}/pentool/utils/copy_as.py +0 -0
  162. {pentool-0.2.4 → pentool-0.2.5}/pentool/utils/diff.py +0 -0
  163. {pentool-0.2.4 → pentool-0.2.5}/pentool/utils/http_client.py +0 -0
  164. {pentool-0.2.4 → pentool-0.2.5}/pentool/utils/parser.py +0 -0
  165. {pentool-0.2.4 → pentool-0.2.5}/pentool/utils/terminal_check.py +0 -0
  166. {pentool-0.2.4 → pentool-0.2.5}/pentool.egg-info/SOURCES.txt +0 -0
  167. {pentool-0.2.4 → pentool-0.2.5}/pentool.egg-info/dependency_links.txt +0 -0
  168. {pentool-0.2.4 → pentool-0.2.5}/pentool.egg-info/entry_points.txt +0 -0
  169. {pentool-0.2.4 → pentool-0.2.5}/pentool.egg-info/requires.txt +0 -0
  170. {pentool-0.2.4 → pentool-0.2.5}/pentool.egg-info/top_level.txt +0 -0
  171. {pentool-0.2.4 → pentool-0.2.5}/setup.cfg +0 -0
  172. {pentool-0.2.4 → pentool-0.2.5}/tests/test_user_workflow.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pentool
3
- Version: 0.2.4
3
+ Version: 0.2.5
4
4
  Summary: Modern Web Pentesting TUI - Professional security testing tool
5
5
  Author-email: Pentool Team <dev@pentool.pro>
6
6
  License: AGPL-3.0
7
- Project-URL: Homepage, https://github.com/docxqwerty/pentool
8
- Project-URL: Repository, https://github.com/docxqwerty/pentool
9
- Project-URL: Issues, https://github.com/docxqwerty/pentool/issues
10
- Project-URL: Changelog, https://github.com/docxqwerty/pentool/blob/main/CHANGELOG.md
7
+ Project-URL: Homepage, https://github.com/DrXOps/pentool
8
+ Project-URL: Repository, https://github.com/DrXOps/pentool
9
+ Project-URL: Issues, https://github.com/DrXOps/pentool/issues
10
+ Project-URL: Changelog, https://github.com/DrXOps/pentool/blob/main/CHANGELOG.md
11
11
  Keywords: pentesting,security,burp,proxy,scanner,intruder
12
12
  Classifier: Development Status :: 3 - Alpha
13
13
  Classifier: Intended Audience :: Developers
@@ -50,7 +50,7 @@ Dynamic: license-file
50
50
 
51
51
  [![PyPI version](https://img.shields.io/pypi/v/pentool)](https://pypi.org/project/pentool/)
52
52
  [![Python versions](https://img.shields.io/pypi/pyversions/pentool)](https://pypi.org/project/pentool/)
53
- [![CI](https://github.com/docxqwerty/pentool/actions/workflows/tests.yml/badge.svg)](https://github.com/docxqwerty/pentool/actions)
53
+ [![CI](https://github.com/DrXOps/pentool/actions/workflows/tests.yml/badge.svg)](https://github.com/DrXOps/pentool/actions)
54
54
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
55
55
  [![Downloads](https://img.shields.io/pypi/dm/pentool)](https://pypi.org/project/pentool/)
56
56
 
@@ -70,15 +70,15 @@ Fast, transparent, and built for real-world testing.
70
70
 
71
71
  | Dashboard | Scanner |
72
72
  |:---------:|:-------:|
73
- | ![Dashboard](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/dashboard.png) | ![Scanner](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/scaner.png) |
73
+ | ![Dashboard](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/dashboard.png) | ![Scanner](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/scaner.png) |
74
74
 
75
75
  | Proxy | Repeater |
76
76
  |:-----:|:--------:|
77
- | ![Proxy](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/proxy.png) | ![Repeater](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/repeater.png) |
77
+ | ![Proxy](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/proxy.png) | ![Repeater](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/repeater.png) |
78
78
 
79
79
  | Intruder | Settings |
80
80
  |:--------:|:--------:|
81
- | ![Intruder](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/intruder.png) | ![Settings](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/settings.png) |
81
+ | ![Intruder](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/intruder.png) | ![Settings](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/settings.png) |
82
82
 
83
83
  ---
84
84
 
@@ -185,7 +185,7 @@ No minimum follower count. We value quality over reach.
185
185
  Pentool is built and maintained by a solo developer in spare time.
186
186
  If it saves you hours on a pentest — consider giving back. Every contribution directly funds new features, fixes, and faster releases.
187
187
 
188
- - ⭐ **[Star on GitHub](https://github.com/docxqwerty/pentool)** — free, takes 2 seconds, helps visibility enormously
188
+ - ⭐ **[Star on GitHub](https://github.com/DrXOps/pentool)** — free, takes 2 seconds, helps visibility enormously
189
189
  - 🔑 **PRO license** — get early access and support development → **[@sudores](https://t.me/sudores)**
190
190
  - 💬 **Share** — tell a colleague, post a review, or mention Pentool in your writeups
191
191
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![PyPI version](https://img.shields.io/pypi/v/pentool)](https://pypi.org/project/pentool/)
6
6
  [![Python versions](https://img.shields.io/pypi/pyversions/pentool)](https://pypi.org/project/pentool/)
7
- [![CI](https://github.com/docxqwerty/pentool/actions/workflows/tests.yml/badge.svg)](https://github.com/docxqwerty/pentool/actions)
7
+ [![CI](https://github.com/DrXOps/pentool/actions/workflows/tests.yml/badge.svg)](https://github.com/DrXOps/pentool/actions)
8
8
  [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
9
9
  [![Downloads](https://img.shields.io/pypi/dm/pentool)](https://pypi.org/project/pentool/)
10
10
 
@@ -24,15 +24,15 @@ Fast, transparent, and built for real-world testing.
24
24
 
25
25
  | Dashboard | Scanner |
26
26
  |:---------:|:-------:|
27
- | ![Dashboard](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/dashboard.png) | ![Scanner](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/scaner.png) |
27
+ | ![Dashboard](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/dashboard.png) | ![Scanner](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/scaner.png) |
28
28
 
29
29
  | Proxy | Repeater |
30
30
  |:-----:|:--------:|
31
- | ![Proxy](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/proxy.png) | ![Repeater](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/repeater.png) |
31
+ | ![Proxy](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/proxy.png) | ![Repeater](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/repeater.png) |
32
32
 
33
33
  | Intruder | Settings |
34
34
  |:--------:|:--------:|
35
- | ![Intruder](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/intruder.png) | ![Settings](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/settings.png) |
35
+ | ![Intruder](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/intruder.png) | ![Settings](https://raw.githubusercontent.com/DrXOps/pentool/main/screens/settings.png) |
36
36
 
37
37
  ---
38
38
 
@@ -139,7 +139,7 @@ No minimum follower count. We value quality over reach.
139
139
  Pentool is built and maintained by a solo developer in spare time.
140
140
  If it saves you hours on a pentest — consider giving back. Every contribution directly funds new features, fixes, and faster releases.
141
141
 
142
- - ⭐ **[Star on GitHub](https://github.com/docxqwerty/pentool)** — free, takes 2 seconds, helps visibility enormously
142
+ - ⭐ **[Star on GitHub](https://github.com/DrXOps/pentool)** — free, takes 2 seconds, helps visibility enormously
143
143
  - 🔑 **PRO license** — get early access and support development → **[@sudores](https://t.me/sudores)**
144
144
  - 💬 **Share** — tell a colleague, post a review, or mention Pentool in your writeups
145
145
 
@@ -1,6 +1,6 @@
1
1
  """Pentool — professional web security testing toolkit with Textual TUI."""
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.2.5"
4
4
  __author__ = "pentool"
5
5
 
6
6
 
@@ -26,6 +26,22 @@ def _bootstrap_pro() -> None:
26
26
  every obfuscated PRO module fails to import with
27
27
  `ModuleNotFoundError: No module named 'codeenigma_runtime'` even though
28
28
  pentool/ itself resolves fine via __path__ extension above.
29
+
30
+ The installed (~/.pentool/pro/) PRO package is skipped entirely — not
31
+ even added to __path__ — if it was built for a different FREE version
32
+ than this one (core.license.is_pro_package_compatible()). This is the
33
+ scenario where a FREE upgrade (`pip install --upgrade pentool` /
34
+ `pentool update`) succeeded but the PRO package's own re-sync
35
+ afterwards failed or never ran (e.g. offline, or an unrelated version
36
+ check like the GitHub release lookup errored out first and stopped the
37
+ whole `pentool update` command before it got to the PRO sync step).
38
+ Loading a mismatched PRO build there risks a hard-to-diagnose crash: it
39
+ ships a compiled Cython extension, and an ABI/version mismatch can
40
+ segfault the process instead of raising a catchable Python exception.
41
+ The dev `pro/` submodule checkout is never version-mismatched (same
42
+ source tree as the running FREE code) and is always loaded normally.
43
+ A warning is printed to stderr so the user isn't left with unexplained
44
+ missing PRO features and no context.
29
45
  """
30
46
  import sys
31
47
  from pathlib import Path
@@ -33,9 +49,10 @@ def _bootstrap_pro() -> None:
33
49
  _pkg_dir = Path(__file__).resolve().parent # .../pentool/pentool/
34
50
  _repo_root = _pkg_dir.parent # .../pentool/
35
51
 
52
+ _installed_pro_pkg = Path.home() / ".pentool" / "pro" / "pentool"
36
53
  candidates: list[Path] = [
37
54
  _repo_root / "pro" / "pentool", # dev submodule
38
- Path.home() / ".pentool" / "pro" / "pentool", # installed PRO package
55
+ _installed_pro_pkg, # installed PRO package
39
56
  ]
40
57
 
41
58
  import pentool as _self
@@ -44,6 +61,16 @@ def _bootstrap_pro() -> None:
44
61
  if not _pro_pkg.exists():
45
62
  continue
46
63
 
64
+ if _pro_pkg == _installed_pro_pkg:
65
+ try:
66
+ from pentool.core.license import is_pro_package_compatible
67
+ _compatible, _warning = is_pro_package_compatible()
68
+ except Exception:
69
+ _compatible, _warning = True, "" # never block startup over this check itself
70
+ if not _compatible:
71
+ print(f"[pentool] {_warning}", file=sys.stderr)
72
+ continue
73
+
47
74
  # 1. Extend top-level pentool.__path__ so that sub-packages
48
75
  # resolved via pkgutil.extend_path below will find pro/pentool/XXX.
49
76
  if str(_pro_pkg) not in _self.__path__:
@@ -0,0 +1,95 @@
1
+ """Entry point: no arguments — TUI, with arguments — CLI."""
2
+
3
+ import sys
4
+ import threading
5
+
6
+ # Escape hatch for local development: let the TUI start even when the
7
+ # installed PRO package is stale/version-mismatched (see
8
+ # pentool.core.license.is_pro_package_compatible). Intentionally verbose and
9
+ # scary-looking rather than a short flag like "-f" — this bypasses a check
10
+ # that exists specifically because loading a mismatched PRO build can
11
+ # segfault the process, so an accidental/muscle-memory use should be hard.
12
+ # NOT meant for end users — only for developers iterating on FREE-only code
13
+ # who don't want to rebuild/reactivate PRO for every unrelated test run.
14
+ #
15
+ # Note this does NOT force-load the mismatched PRO package itself — that
16
+ # refusal (pentool.__init__._bootstrap_pro) runs unconditionally at import
17
+ # time, before this flag is even parsed, and always prints its own warning
18
+ # when it skips a mismatched PRO install. This flag only bypasses the
19
+ # separate hard stop below that would otherwise refuse to start the TUI at
20
+ # all in that situation — with it, the TUI still starts, just with PRO
21
+ # features unavailable (same as if no PRO package were installed).
22
+ _UNSAFE_SKIP_PRO_CHECK_FLAG = "--unsafe-skip-pro-compat-check"
23
+
24
+
25
+ def main() -> None:
26
+ unsafe_skip_pro_check = _UNSAFE_SKIP_PRO_CHECK_FLAG in sys.argv
27
+ if unsafe_skip_pro_check:
28
+ sys.argv.remove(_UNSAFE_SKIP_PRO_CHECK_FLAG)
29
+
30
+ if len(sys.argv) > 1:
31
+ from pentool.cli.main import cli
32
+ cli()
33
+ else:
34
+ # Refuse to start the TUI at all if a PRO package is installed but
35
+ # was built for a different (or unknown) FREE version — e.g. FREE
36
+ # was upgraded via `pip install --upgrade pentool` / `pentool
37
+ # update`, but the PRO package's own re-sync afterwards failed or
38
+ # never ran (offline, or an unrelated check — like the GitHub
39
+ # release lookup used for the FREE version check — errored out
40
+ # first and stopped `pentool update` before it reached the PRO
41
+ # sync step). The PRO package bundles a compiled Cython extension;
42
+ # importing a mismatched build can segfault the process with no
43
+ # log output at all, which is exactly the silent, unexplained
44
+ # crash this check exists to prevent. Bail out here, before any
45
+ # Textual app or PRO import is attempted, with a clear message and
46
+ # a fix instead of a mystery crash.
47
+ try:
48
+ from pentool.core.license import is_pro_package_compatible
49
+ compatible, warning = is_pro_package_compatible()
50
+ except Exception:
51
+ compatible, warning = True, "" # never block startup over this check itself
52
+
53
+ if not compatible:
54
+ if unsafe_skip_pro_check:
55
+ print(
56
+ "[pentool] UNSAFE: --unsafe-skip-pro-compat-check set — "
57
+ "starting anyway despite the PRO version mismatch above. "
58
+ "PRO features stay disabled (as already reported), but "
59
+ "be aware this bypass is unpredictable if anything in "
60
+ "this session still ends up touching the mismatched "
61
+ "PRO package. Do not use this outside local development.",
62
+ file=sys.stderr,
63
+ )
64
+ else:
65
+ print(f"[pentool] {warning}", file=sys.stderr)
66
+ raise SystemExit(1)
67
+
68
+ # Anonymous install-counter ping (fire-and-forget, one increment per
69
+ # machine — dedup happens server-side). Runs in a background thread —
70
+ # send_first_run_ping() opens a network connection with an 8s
71
+ # timeout, which must never delay the TUI actually starting when
72
+ # the network is slow/unreachable.
73
+ try:
74
+ from pentool.core.crash_reporter import send_first_run_ping
75
+ threading.Thread(target=send_first_run_ping, daemon=True).start()
76
+ except Exception:
77
+ pass
78
+
79
+ try:
80
+ from pentool.tui.app import PentoolApp
81
+ PentoolApp().run()
82
+ except (KeyboardInterrupt, SystemExit):
83
+ raise
84
+ except Exception as exc:
85
+ # Send anonymous crash report (if not disabled in settings)
86
+ try:
87
+ from pentool.core.crash_reporter import send_crash
88
+ send_crash(exc)
89
+ except Exception:
90
+ pass
91
+ raise
92
+
93
+
94
+ if __name__ == "__main__":
95
+ main()
@@ -110,16 +110,30 @@ def update_cmd(check_only: bool) -> None:
110
110
 
111
111
  if info.error:
112
112
  click.echo(f"Could not check for updates: {info.error}", err=True)
113
+ # The FREE-version check above failing (network down, GitHub API
114
+ # rate-limited/404, ...) is unrelated to the PRO package, but it
115
+ # used to make this command bail out here BEFORE ever reaching
116
+ # _sync_pro_package_after_upgrade() below — silently leaving an
117
+ # already-installed PRO package on a stale build with no warning.
118
+ # Since PRO ships a compiled Cython extension, running a stale
119
+ # build against a since-upgraded FREE version can segfault the
120
+ # process with no log output — exactly the "just crashed, no idea
121
+ # why" report this is meant to prevent. So: still check/warn about
122
+ # PRO staleness before exiting, even though we can't do the FREE
123
+ # upgrade itself right now.
124
+ _warn_if_pro_incompatible()
113
125
  raise SystemExit(1)
114
126
 
115
127
  if not info.has_update:
116
128
  click.echo(f"Already up to date (version {info.latest_version}).")
129
+ _warn_if_pro_incompatible()
117
130
  return
118
131
 
119
132
  click.echo(f"New version available: {info.latest_version}")
120
133
  click.echo(f"Release notes: {info.url}")
121
134
 
122
135
  if check_only:
136
+ _warn_if_pro_incompatible()
123
137
  return
124
138
 
125
139
  if click.confirm(f"Install {info.latest_version} now?", default=True):
@@ -134,6 +148,25 @@ def update_cmd(check_only: bool) -> None:
134
148
  err=True,
135
149
  )
136
150
  raise SystemExit(1)
151
+ else:
152
+ _warn_if_pro_incompatible()
153
+
154
+
155
+ def _warn_if_pro_incompatible() -> None:
156
+ """Print a warning to the terminal if the installed PRO package (if any)
157
+ was built for a different FREE version than the one currently running.
158
+
159
+ Called from every exit path of `pentool update` that does NOT go through
160
+ _sync_pro_package_after_upgrade() below — so a user is never left with a
161
+ stale/incompatible PRO package and zero indication of it, regardless of
162
+ which branch (FREE check failed, already up to date, --check, declined
163
+ to install) the command took.
164
+ """
165
+ from pentool.core.license import is_pro_package_compatible
166
+
167
+ compatible, warning = is_pro_package_compatible()
168
+ if not compatible:
169
+ click.echo(f"Warning: {warning}", err=True)
137
170
 
138
171
 
139
172
  def _sync_pro_package_after_upgrade() -> None:
@@ -144,7 +177,10 @@ def _sync_pro_package_after_upgrade() -> None:
144
177
  (re)downloaded by check_and_update_pro_package(), which normally runs in
145
178
  the background on TUI startup. Doing it here too means a CLI-driven
146
179
  upgrade doesn't leave a stale PRO build around until the next TUI launch.
147
- Best-effort — never fails the overall upgrade if this part doesn't work.
180
+ Best-effort for the FREE upgrade's sake — a failure here never raises
181
+ SystemExit — but always surfaces a warning to the terminal if the PRO
182
+ package ends up (or remains) incompatible with the just-upgraded FREE
183
+ version, since that combination risks a crash on next launch.
148
184
  """
149
185
  import asyncio
150
186
 
@@ -155,11 +191,14 @@ def _sync_pro_package_after_upgrade() -> None:
155
191
 
156
192
  try:
157
193
  click.echo("Checking for a newer PRO build...")
158
- updated = asyncio.run(check_and_update_pro_package())
159
- if updated:
194
+ result = asyncio.run(check_and_update_pro_package())
195
+ if result.updated:
160
196
  click.echo("PRO package updated to the latest build.")
197
+ elif result.warning:
198
+ click.echo(f"Warning: {result.warning}", err=True)
161
199
  except Exception as exc:
162
200
  click.echo(f"Could not check for a PRO update: {exc}", err=True)
201
+ _warn_if_pro_incompatible()
163
202
 
164
203
 
165
204
  @cli.group()
@@ -503,13 +503,131 @@ def _current_platform() -> str:
503
503
  return "linux"
504
504
 
505
505
 
506
- # File recording which PRO build is currently unpacked into PRO_PACKAGE_DIR —
507
- # lets check_and_update_pro_package() tell "same build as last time" apart
508
- # from "a new build was published" without re-downloading the archive to
509
- # compare it. See _fetch_pro_build_id() / /api/pro/version on the Worker.
506
+ # File recording which PRO build is currently unpacked into PRO_PACKAGE_DIR,
507
+ # together with the FREE (pip) pentool version it was downloaded for — lets
508
+ # check_and_update_pro_package() tell "same build as last time" apart from "a
509
+ # new build was published" without re-downloading the archive to compare it,
510
+ # and lets is_pro_package_compatible() detect a FREE/PRO version mismatch.
511
+ # See _fetch_pro_build_id() / /api/pro/version on the Worker.
512
+ _PRO_META_FILE = PRO_PACKAGE_DIR / ".build_meta.json"
513
+
514
+ # Legacy pre-2026-08 marker: plain text build_id, no version recorded. Still
515
+ # read (as "unknown version" -> treated as incompatible/stale, see
516
+ # is_pro_package_compatible()) so an old install isn't mistaken for corrupt.
510
517
  _PRO_BUILD_MARKER = PRO_PACKAGE_DIR / ".build_id"
511
518
 
512
519
 
520
+ @dataclass
521
+ class ProSyncResult:
522
+ """Outcome of a check_and_update_pro_package() call.
523
+
524
+ `warning` is meant to be shown to the user verbatim (TUI notify / CLI
525
+ stderr) whenever the locally installed PRO package is stale or was built
526
+ for a different FREE version than the one currently running — silently
527
+ running on a mismatched PRO package is what risks a hard crash (it ships
528
+ a compiled Cython extension; an ABI/version mismatch there can segfault
529
+ instead of raising a catchable Python exception).
530
+ """
531
+ updated: bool = False
532
+ warning: str = ""
533
+
534
+
535
+ def _read_pro_meta() -> dict:
536
+ """Read the PRO package's build metadata (build_id + the FREE pentool
537
+ version it was downloaded for). Falls back to the legacy plain-text
538
+ .build_id file with an empty free_version if the JSON meta file isn't
539
+ there yet — is_pro_package_compatible() then correctly treats that as
540
+ "unknown version, don't trust it" rather than crashing on missing data.
541
+ """
542
+ try:
543
+ if _PRO_META_FILE.exists():
544
+ data = json.loads(_PRO_META_FILE.read_text(encoding="utf-8"))
545
+ if isinstance(data, dict):
546
+ return data
547
+ except Exception:
548
+ pass
549
+ try:
550
+ if _PRO_BUILD_MARKER.exists():
551
+ return {"build_id": _PRO_BUILD_MARKER.read_text(encoding="utf-8").strip(), "free_version": ""}
552
+ except Exception:
553
+ pass
554
+ return {}
555
+
556
+
557
+ def _write_pro_meta(build_id: str) -> None:
558
+ """Record which build_id is now unpacked, and which FREE version it was
559
+ fetched for — called right after a successful extract in
560
+ download_pro_package()."""
561
+ try:
562
+ from pentool import __version__ as free_version
563
+ except Exception:
564
+ free_version = ""
565
+ try:
566
+ _PRO_META_FILE.write_text(
567
+ json.dumps({"build_id": build_id, "free_version": free_version}),
568
+ encoding="utf-8",
569
+ )
570
+ _PRO_BUILD_MARKER.unlink(missing_ok=True) # migrate off the legacy marker
571
+ except Exception:
572
+ pass
573
+
574
+
575
+ def is_pro_package_compatible() -> tuple[bool, str]:
576
+ """Check whether the PRO package in PRO_PACKAGE_DIR matches the FREE
577
+ (pip) version of pentool currently running.
578
+
579
+ This is the guard against a stale/incompatible PRO package: the FREE
580
+ package updates via `pip install --upgrade pentool`, but the PRO package
581
+ (a separately downloaded, platform-specific compiled Cython extension)
582
+ only updates when check_and_update_pro_package() successfully reaches
583
+ the server — e.g. `pentool update`'s own version-check step failing
584
+ (network down, GitHub 404, ...) does NOT stop the FREE upgrade, but DOES
585
+ leave PRO on its old build. Loading that mismatched PRO build can
586
+ segfault the whole process instead of raising a catchable exception,
587
+ so every call site that would otherwise import from the PRO package
588
+ (plugin_manager, __init__._bootstrap_pro) checks this FIRST and refuses
589
+ to load it rather than relying on try/except around the import.
590
+
591
+ Returns:
592
+ (True, "") — nothing installed, or the installed PRO package matches
593
+ the running FREE version.
594
+ (False, message) — a PRO package is installed but was built for a
595
+ different (or unknown) FREE version. `message` is a
596
+ user-facing explanation of what to do.
597
+ """
598
+ if not PRO_PACKAGE_DIR.exists():
599
+ return True, ""
600
+
601
+ try:
602
+ from pentool import __version__ as current_version
603
+ except Exception:
604
+ current_version = ""
605
+
606
+ meta = _read_pro_meta()
607
+ pro_free_version = meta.get("free_version", "")
608
+
609
+ if not pro_free_version:
610
+ return False, (
611
+ "A PRO package is installed, but its build metadata doesn't record "
612
+ "which Pentool version it was built for (a previous PRO update "
613
+ "likely didn't finish). PRO features are disabled to avoid a "
614
+ "crash from a version mismatch — run "
615
+ "'pentool license activate <key>' to re-download the PRO package."
616
+ )
617
+
618
+ if pro_free_version != current_version:
619
+ return False, (
620
+ f"Version mismatch: Pentool {current_version} is installed, but "
621
+ f"the PRO package was built for version {pro_free_version}. "
622
+ f"Running it as-is could crash the app. PRO features are "
623
+ f"disabled until this is resynced — run "
624
+ f"'pentool license activate <key>' or wait for the background "
625
+ f"PRO update to complete."
626
+ )
627
+
628
+ return True, ""
629
+
630
+
513
631
  async def download_pro_package(key: str, machine_id: str) -> bool:
514
632
  """Download, verify, and install the obfuscated PRO package.
515
633
 
@@ -562,13 +680,15 @@ async def download_pro_package(key: str, machine_id: str) -> bool:
562
680
  finally:
563
681
  tmp_archive.unlink(missing_ok=True)
564
682
 
565
- # Record which build this is, so a later check_and_update_pro_package()
566
- # can tell "nothing changed" from "a new build was published" without
567
- # re-downloading the archive just to compare it.
683
+ # Record which build this is (and which FREE version it was fetched
684
+ # for — see is_pro_package_compatible()), so a later
685
+ # check_and_update_pro_package() can tell "nothing changed" from "a
686
+ # new build was published" without re-downloading the archive just
687
+ # to compare it.
568
688
  try:
569
689
  build_id = await _fetch_pro_build_id(plat)
570
690
  if build_id:
571
- _PRO_BUILD_MARKER.write_text(build_id, encoding="utf-8")
691
+ _write_pro_meta(build_id)
572
692
  except Exception:
573
693
  pass # non-fatal — worst case, next check just re-downloads once more
574
694
 
@@ -604,8 +724,9 @@ async def _fetch_pro_build_id(plat: str) -> str | None:
604
724
  return None
605
725
 
606
726
 
607
- async def check_and_update_pro_package() -> bool:
608
- """Re-download the PRO package if a newer build has been published.
727
+ async def check_and_update_pro_package() -> "ProSyncResult":
728
+ """Re-download the PRO package if a newer build has been published, or if
729
+ the currently-installed one no longer matches the running FREE version.
609
730
 
610
731
  Called on TUI startup (background worker, same pattern as
611
732
  _check_for_updates for the pip package) and after `pentool update` — this
@@ -614,33 +735,51 @@ async def check_and_update_pro_package() -> bool:
614
735
  ~/.pentool/pro/ forever, because download_pro_package() otherwise only
615
736
  ever runs once, at initial activation/trial-start.
616
737
 
617
- Silently does nothing (returns False) if:
618
- - there's no active PRO license (nothing to update)
619
- - the PRO package was never downloaded in the first place (that's
620
- activate_license's job, not this one)
621
- - the version check or download fails for any reason
622
-
623
- Returns True if a new build was found and successfully installed.
738
+ Crucially, this is also the ONLY place that re-syncs PRO after a FREE
739
+ upgrade (`pip install --upgrade pentool` / `pentool update`) — if this
740
+ step can't reach the license server (network down, or the *unrelated*
741
+ FREE-side GitHub version check already failed and the caller never even
742
+ got here), the PRO package silently stays on its old build while FREE
743
+ moves on. Since the PRO package ships a compiled Cython extension, that
744
+ mismatch can segfault the process instead of raising a Python exception.
745
+ So on every "couldn't sync" path below, this now also checks
746
+ is_pro_package_compatible() and returns a non-empty `warning` if the
747
+ installed PRO build is stale/unknown — callers (TUI, CLI) must surface
748
+ that warning to the user rather than silently doing nothing.
749
+
750
+ Returns a ProSyncResult:
751
+ - updated=True — a new build was found and successfully installed.
752
+ - updated=False, warning="" — nothing to do (no PRO license, PRO
753
+ never installed, or already on the latest compatible build).
754
+ - updated=False, warning="..." — could not verify/refresh the PRO
755
+ package AND the installed one is stale or version-mismatched;
756
+ `warning` is a user-facing message to display.
624
757
  """
625
758
  info = get_license()
626
759
  if not info.valid or not info.features:
627
- return False
760
+ return ProSyncResult(updated=False, warning="")
628
761
  if not PRO_PACKAGE_DIR.exists():
629
- return False # never activated on this machine — nothing to refresh
762
+ return ProSyncResult(updated=False, warning="") # never activated — nothing to refresh
630
763
 
631
764
  plat = _current_platform()
632
765
  remote_build_id = await _fetch_pro_build_id(plat)
633
766
  if not remote_build_id:
634
- return False # couldn't reach the server — don't touch anything
767
+ # Couldn't reach the server to check — don't touch the package, but
768
+ # DO tell the user if what's on disk is already known-stale/mismatched.
769
+ _compatible, warning = is_pro_package_compatible()
770
+ return ProSyncResult(updated=False, warning=warning)
635
771
 
636
- local_build_id = None
637
- try:
638
- if _PRO_BUILD_MARKER.exists():
639
- local_build_id = _PRO_BUILD_MARKER.read_text(encoding="utf-8").strip()
640
- except Exception:
641
- pass
772
+ meta = _read_pro_meta()
773
+ local_build_id = meta.get("build_id", "")
642
774
 
643
775
  if local_build_id == remote_build_id:
644
- return False # already on the latest build
776
+ _compatible, warning = is_pro_package_compatible()
777
+ return ProSyncResult(updated=False, warning=warning)
778
+
779
+ updated = await download_pro_package(info.license_key, info.machine_id)
780
+ if updated:
781
+ return ProSyncResult(updated=True, warning="")
645
782
 
646
- return await download_pro_package(info.license_key, info.machine_id)
783
+ # Download of the newer build failed — warn if what's left on disk is stale.
784
+ _compatible, warning = is_pro_package_compatible()
785
+ return ProSyncResult(updated=False, warning=warning)
@@ -161,6 +161,12 @@ class PluginManager:
161
161
  self._passive_checks: list[type[BaseCheck]] = []
162
162
  self._loaded: list[str] = []
163
163
  self._meta: list[PluginMeta] = []
164
+ # Set by load_user_plugins() when a PRO package is present on disk
165
+ # but was built for a different (or unknown) FREE version — see
166
+ # pentool.core.license.is_pro_package_compatible(). Empty string
167
+ # means no incompatibility was detected. Callers (TUI startup, CLI)
168
+ # should surface this to the user rather than silently ignoring it.
169
+ self.pro_incompatible_warning: str = ""
164
170
 
165
171
  # ── Loading ───────────────────────────────────────────────────────────────
166
172
 
@@ -197,7 +203,21 @@ class PluginManager:
197
203
  # Obfuscated PRO package downloaded via 'pentool license trial'/'activate'
198
204
  # (see pentool.core.license.download_pro_package). Its plugins directory
199
205
  # mirrors the builtin layout: pro/pentool/plugins/builtin/*.py
200
- from pentool.core.license import PRO_PACKAGE_DIR
206
+ #
207
+ # Refuse to import it if it was built for a different FREE version
208
+ # than the one currently running (e.g. FREE was upgraded via
209
+ # `pip install --upgrade pentool` but the PRO re-sync afterwards
210
+ # failed/never ran) — the PRO package bundles a compiled Cython
211
+ # extension, and importing a mismatched build there can segfault the
212
+ # whole process instead of raising a catchable Python exception. See
213
+ # pentool.core.license.is_pro_package_compatible().
214
+ from pentool.core.license import PRO_PACKAGE_DIR, is_pro_package_compatible
215
+ compatible, warning = is_pro_package_compatible()
216
+ if not compatible:
217
+ logger.warning("PRO package skipped — %s", warning)
218
+ self.pro_incompatible_warning = warning
219
+ return
220
+
201
221
  pro_plugins_dir = PRO_PACKAGE_DIR / "pentool" / "plugins" / "builtin"
202
222
  if pro_plugins_dir.exists():
203
223
  self.load_plugins([str(pro_plugins_dir)], warn_untrusted=False)
@@ -360,11 +380,24 @@ def load_pro_module(module_stem: str):
360
380
  Raises FileNotFoundError if the module isn't found in any known PRO
361
381
  location (dev submodule or installed package) — callers should show
362
382
  this to the user instead of silently failing.
383
+
384
+ Raises RuntimeError if the installed PRO package (PRO_PACKAGE_DIR) was
385
+ built for a different FREE version than the one currently running — see
386
+ pentool.core.license.is_pro_package_compatible(). Refusing to import it
387
+ is deliberate: it's a compiled Cython extension, and importing a
388
+ mismatched build can segfault the process instead of raising a
389
+ catchable Python exception. Does NOT apply to the dev `pro/` submodule
390
+ checkout, which is always the same source tree as the running FREE code.
363
391
  """
364
392
  module_name = f"_pentool_plugin_{module_stem}"
365
393
  if module_name in sys.modules:
366
394
  return sys.modules[module_name]
367
395
 
396
+ from pentool.core.license import PRO_PACKAGE_DIR, is_pro_package_compatible
397
+ compatible, warning = is_pro_package_compatible()
398
+ if not compatible:
399
+ raise RuntimeError(warning)
400
+
368
401
  for plugin_dir in _candidate_pro_builtin_dirs():
369
402
  candidate = plugin_dir / f"{module_stem}.py"
370
403
  if not candidate.exists():