pentool 0.2.0__tar.gz → 0.2.2__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 (168) hide show
  1. {pentool-0.2.0 → pentool-0.2.2}/PKG-INFO +5 -4
  2. {pentool-0.2.0 → pentool-0.2.2}/README.md +4 -3
  3. {pentool-0.2.0 → pentool-0.2.2}/pentool/__init__.py +2 -2
  4. {pentool-0.2.0 → pentool-0.2.2}/pentool/__main__.py +12 -0
  5. {pentool-0.2.0 → pentool-0.2.2}/pentool/cli/main.py +27 -0
  6. pentool-0.2.2/pentool/core/crash_reporter.py +137 -0
  7. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/license.py +86 -0
  8. {pentool-0.2.0 → pentool-0.2.2}/pentool/services/proxy_service.py +19 -0
  9. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/app.py +98 -3
  10. pentool-0.2.2/pentool/tui/dialogs/__init__.py +9 -0
  11. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/target/screen.py +18 -7
  12. pentool-0.2.2/pentool/tui/widgets/activity_indicator.py +143 -0
  13. pentool-0.2.2/pentool/tui/widgets/activity_indicator.tcss +30 -0
  14. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/module_tabs.py +1 -1
  15. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/statusbar.py +7 -1
  16. {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/PKG-INFO +5 -4
  17. {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/SOURCES.txt +2 -0
  18. {pentool-0.2.0 → pentool-0.2.2}/pyproject.toml +1 -1
  19. pentool-0.2.0/pentool/core/crash_reporter.py +0 -75
  20. pentool-0.2.0/pentool/tui/mixins/__init__.py +0 -0
  21. {pentool-0.2.0 → pentool-0.2.2}/LICENSE +0 -0
  22. {pentool-0.2.0 → pentool-0.2.2}/pentool/api/__init__.py +0 -0
  23. {pentool-0.2.0 → pentool-0.2.2}/pentool/api/base_api.py +0 -0
  24. {pentool-0.2.0 → pentool-0.2.2}/pentool/api/comparer_api.py +0 -0
  25. {pentool-0.2.0 → pentool-0.2.2}/pentool/api/decoder_api.py +0 -0
  26. {pentool-0.2.0 → pentool-0.2.2}/pentool/api/intruder_api.py +0 -0
  27. {pentool-0.2.0 → pentool-0.2.2}/pentool/api/proxy_api.py +0 -0
  28. {pentool-0.2.0 → pentool-0.2.2}/pentool/api/repeater_api.py +0 -0
  29. {pentool-0.2.0 → pentool-0.2.2}/pentool/api/sequencer_api.py +0 -0
  30. {pentool-0.2.0 → pentool-0.2.2}/pentool/api/spider_api.py +0 -0
  31. {pentool-0.2.0 → pentool-0.2.2}/pentool/api/target_api.py +0 -0
  32. {pentool-0.2.0 → pentool-0.2.2}/pentool/cli/__init__.py +0 -0
  33. {pentool-0.2.0 → pentool-0.2.2}/pentool/cli/project.py +0 -0
  34. {pentool-0.2.0 → pentool-0.2.2}/pentool/cli/proxy.py +0 -0
  35. {pentool-0.2.0 → pentool-0.2.2}/pentool/cli/scan.py +0 -0
  36. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/__init__.py +0 -0
  37. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/config.py +0 -0
  38. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/database.py +0 -0
  39. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/event_bus.py +0 -0
  40. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/events.py +0 -0
  41. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/features.py +0 -0
  42. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/logging.py +0 -0
  43. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/plugin_manager.py +0 -0
  44. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/project.py +0 -0
  45. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/storage_interface.py +0 -0
  46. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/updater.py +0 -0
  47. {pentool-0.2.0 → pentool-0.2.2}/pentool/core/utils.py +0 -0
  48. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/__init__.py +0 -0
  49. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/comparer.py +0 -0
  50. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/decoder.py +0 -0
  51. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/intruder.py +0 -0
  52. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/match_replace.py +0 -0
  53. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/proxy.py +0 -0
  54. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/repeater.py +0 -0
  55. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/sequencer.py +0 -0
  56. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/spider.py +0 -0
  57. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/target.py +0 -0
  58. {pentool-0.2.0 → pentool-0.2.2}/pentool/modules/websocket_handler.py +0 -0
  59. {pentool-0.2.0 → pentool-0.2.2}/pentool/plugins/__init__.py +0 -0
  60. {pentool-0.2.0 → pentool-0.2.2}/pentool/plugins/builtin/__init__.py +0 -0
  61. {pentool-0.2.0 → pentool-0.2.2}/pentool/plugins/example_plugin.py +0 -0
  62. {pentool-0.2.0 → pentool-0.2.2}/pentool/plugins/example_plugin.tcss +0 -0
  63. {pentool-0.2.0 → pentool-0.2.2}/pentool/services/__init__.py +0 -0
  64. {pentool-0.2.0 → pentool-0.2.2}/pentool/services/base_service.py +0 -0
  65. {pentool-0.2.0 → pentool-0.2.2}/pentool/services/intruder_service.py +0 -0
  66. {pentool-0.2.0 → pentool-0.2.2}/pentool/services/repeater_service.py +0 -0
  67. {pentool-0.2.0 → pentool-0.2.2}/pentool/services/scan_service.py +0 -0
  68. {pentool-0.2.0 → pentool-0.2.2}/pentool/storage/__init__.py +0 -0
  69. {pentool-0.2.0 → pentool-0.2.2}/pentool/storage/http_storage.py +0 -0
  70. {pentool-0.2.0 → pentool-0.2.2}/pentool/storage/large_body_handler.py +0 -0
  71. {pentool-0.2.0 → pentool-0.2.2}/pentool/storage/lru_cache.py +0 -0
  72. {pentool-0.2.0 → pentool-0.2.2}/pentool/t.py +0 -0
  73. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/__init__.py +0 -0
  74. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/constants.py +0 -0
  75. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/cert_dialog.py +0 -0
  76. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/cert_dialog.tcss +0 -0
  77. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/file_selector.py +0 -0
  78. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/file_selector.tcss +0 -0
  79. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/load_from_proxy.py +0 -0
  80. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/load_from_proxy.tcss +0 -0
  81. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/match_replace_dialog.py +0 -0
  82. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/match_replace_dialog.tcss +0 -0
  83. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/project_dialog.py +0 -0
  84. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/project_dialog.tcss +0 -0
  85. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/scope_dialog.py +0 -0
  86. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/dialogs/scope_dialog.tcss +0 -0
  87. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/messages.py +0 -0
  88. {pentool-0.2.0/pentool/tui/dialogs → pentool-0.2.2/pentool/tui/mixins}/__init__.py +0 -0
  89. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/mixins/app_mixin.py +0 -0
  90. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/mixins/dialog_cancel.py +0 -0
  91. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/mixins/request_context_menu.py +0 -0
  92. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/project_manager.py +0 -0
  93. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/__init__.py +0 -0
  94. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/base.py +0 -0
  95. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/comparer/__init__.py +0 -0
  96. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/comparer/screen.py +0 -0
  97. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/comparer/screen.tcss +0 -0
  98. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/dashboard/__init__.py +0 -0
  99. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/dashboard/live_dashboard.py +0 -0
  100. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/dashboard/screen.py +0 -0
  101. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/dashboard/screen.tcss +0 -0
  102. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/decoder/__init__.py +0 -0
  103. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/decoder/screen.py +0 -0
  104. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/decoder/screen.tcss +0 -0
  105. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/extensions/__init__.py +0 -0
  106. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/extensions/screen.py +0 -0
  107. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/extensions/screen.tcss +0 -0
  108. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/intruder/__init__.py +0 -0
  109. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/intruder/screen.py +0 -0
  110. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/intruder/screen.tcss +0 -0
  111. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/proxy/__init__.py +0 -0
  112. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/proxy/screen.py +0 -0
  113. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/proxy/screen.tcss +0 -0
  114. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/repeater/__init__.py +0 -0
  115. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/repeater/screen.py +0 -0
  116. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/repeater/screen.tcss +0 -0
  117. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/scanner_unavailable.py +0 -0
  118. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/sequencer/__init__.py +0 -0
  119. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/sequencer/screen.py +0 -0
  120. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/sequencer/screen.tcss +0 -0
  121. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/settings/__init__.py +0 -0
  122. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/settings/hotkeys.py +0 -0
  123. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/settings/hotkeys.tcss +0 -0
  124. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/settings/screen.py +0 -0
  125. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/settings/screen.tcss +0 -0
  126. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/spider/__init__.py +0 -0
  127. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/spider/screen.py +0 -0
  128. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/spider/screen.tcss +0 -0
  129. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/target/__init__.py +0 -0
  130. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/target/screen.tcss +0 -0
  131. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/terminal/__init__.py +0 -0
  132. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/terminal/screen.py +0 -0
  133. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/screens/terminal/screen.tcss +0 -0
  134. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/__init__.py +0 -0
  135. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/context_menu.py +0 -0
  136. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/context_menu.tcss +0 -0
  137. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/filter_bar.py +0 -0
  138. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/filter_bar.tcss +0 -0
  139. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/inspector_panel.py +0 -0
  140. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/inspector_panel.tcss +0 -0
  141. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/menu.tcss +0 -0
  142. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/menu_bar.tcss +0 -0
  143. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/module_tabs.tcss +0 -0
  144. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/payload_drop_zone.py +0 -0
  145. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/payload_drop_zone.tcss +0 -0
  146. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/request_editor.py +0 -0
  147. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/request_editor.tcss +0 -0
  148. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/resize_handle.py +0 -0
  149. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/resize_handle.tcss +0 -0
  150. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/search_bar.py +0 -0
  151. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/search_bar.tcss +0 -0
  152. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/statusbar.tcss +0 -0
  153. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/toolbar_button.py +0 -0
  154. {pentool-0.2.0 → pentool-0.2.2}/pentool/tui/widgets/toolbar_button.tcss +0 -0
  155. {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/__init__.py +0 -0
  156. {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/cert.py +0 -0
  157. {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/coder.py +0 -0
  158. {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/copy_as.py +0 -0
  159. {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/diff.py +0 -0
  160. {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/http_client.py +0 -0
  161. {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/parser.py +0 -0
  162. {pentool-0.2.0 → pentool-0.2.2}/pentool/utils/terminal_check.py +0 -0
  163. {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/dependency_links.txt +0 -0
  164. {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/entry_points.txt +0 -0
  165. {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/requires.txt +0 -0
  166. {pentool-0.2.0 → pentool-0.2.2}/pentool.egg-info/top_level.txt +0 -0
  167. {pentool-0.2.0 → pentool-0.2.2}/setup.cfg +0 -0
  168. {pentool-0.2.0 → pentool-0.2.2}/tests/test_user_workflow.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pentool
3
- Version: 0.2.0
3
+ Version: 0.2.2
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
@@ -70,15 +70,15 @@ Fast, transparent, and built for real-world testing.
70
70
 
71
71
  | Dashboard | Scanner |
72
72
  |:---------:|:-------:|
73
- | ![Dashboard](screens/dashboard.png) | ![Scanner](screens/scaner.png) |
73
+ | ![Dashboard](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/dashboard.png) | ![Scanner](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/scaner.png) |
74
74
 
75
75
  | Proxy | Repeater |
76
76
  |:-----:|:--------:|
77
- | ![Proxy](screens/proxy.png) | ![Repeater](screens/repeater.png) |
77
+ | ![Proxy](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/proxy.png) | ![Repeater](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/repeater.png) |
78
78
 
79
79
  | Intruder | Settings |
80
80
  |:--------:|:--------:|
81
- | ![Intruder](screens/intruder.png) | ![Settings](screens/settings.png) |
81
+ | ![Intruder](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/intruder.png) | ![Settings](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/settings.png) |
82
82
 
83
83
  ---
84
84
 
@@ -213,6 +213,7 @@ PRO extensions are available under a commercial license.
213
213
  ## 📬 Contact
214
214
 
215
215
  - **Website:** [pentool.pro](https://pentool.pro)
216
+ - **Telegram channel:** [t.me/pentool_pro](https://t.me/pentool_pro)
216
217
  - **Telegram:** [@sudores](https://t.me/sudores)
217
218
  - **Email:** support@pentool.pro
218
219
  - **Author:** Anatoly Kashtanov (DoctorX)
@@ -24,15 +24,15 @@ Fast, transparent, and built for real-world testing.
24
24
 
25
25
  | Dashboard | Scanner |
26
26
  |:---------:|:-------:|
27
- | ![Dashboard](screens/dashboard.png) | ![Scanner](screens/scaner.png) |
27
+ | ![Dashboard](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/dashboard.png) | ![Scanner](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/scaner.png) |
28
28
 
29
29
  | Proxy | Repeater |
30
30
  |:-----:|:--------:|
31
- | ![Proxy](screens/proxy.png) | ![Repeater](screens/repeater.png) |
31
+ | ![Proxy](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/proxy.png) | ![Repeater](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/repeater.png) |
32
32
 
33
33
  | Intruder | Settings |
34
34
  |:--------:|:--------:|
35
- | ![Intruder](screens/intruder.png) | ![Settings](screens/settings.png) |
35
+ | ![Intruder](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/intruder.png) | ![Settings](https://raw.githubusercontent.com/docxqwerty/pentool/main/screens/settings.png) |
36
36
 
37
37
  ---
38
38
 
@@ -167,6 +167,7 @@ PRO extensions are available under a commercial license.
167
167
  ## 📬 Contact
168
168
 
169
169
  - **Website:** [pentool.pro](https://pentool.pro)
170
+ - **Telegram channel:** [t.me/pentool_pro](https://t.me/pentool_pro)
170
171
  - **Telegram:** [@sudores](https://t.me/sudores)
171
172
  - **Email:** support@pentool.pro
172
173
  - **Author:** Anatoly Kashtanov (DoctorX)
@@ -1,6 +1,6 @@
1
1
  """Pentool — professional web security testing toolkit with Textual TUI."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.2"
4
4
  __author__ = "pentool"
5
5
 
6
6
 
@@ -34,8 +34,8 @@ def _bootstrap_pro() -> None:
34
34
  _repo_root = _pkg_dir.parent # .../pentool/
35
35
 
36
36
  candidates: list[Path] = [
37
- Path.home() / ".pentool" / "pro" / "pentool", # installed PRO package
38
37
  _repo_root / "pro" / "pentool", # dev submodule
38
+ Path.home() / ".pentool" / "pro" / "pentool", # installed PRO package
39
39
  ]
40
40
 
41
41
  import pentool as _self
@@ -1,6 +1,7 @@
1
1
  """Entry point: no arguments — TUI, with arguments — CLI."""
2
2
 
3
3
  import sys
4
+ import threading
4
5
 
5
6
 
6
7
  def main() -> None:
@@ -8,6 +9,17 @@ def main() -> None:
8
9
  from pentool.cli.main import cli
9
10
  cli()
10
11
  else:
12
+ # Anonymous install-counter ping (fire-and-forget, one increment per
13
+ # machine — dedup happens server-side). Runs in a background thread —
14
+ # send_first_run_ping() opens a network connection with an 8s
15
+ # timeout, which must never delay the TUI actually starting when
16
+ # the network is slow/unreachable.
17
+ try:
18
+ from pentool.core.crash_reporter import send_first_run_ping
19
+ threading.Thread(target=send_first_run_ping, daemon=True).start()
20
+ except Exception:
21
+ pass
22
+
11
23
  try:
12
24
  from pentool.tui.app import PentoolApp
13
25
  PentoolApp().run()
@@ -126,6 +126,7 @@ def update_cmd(check_only: bool) -> None:
126
126
  click.echo("Upgrading via pip...")
127
127
  if do_pip_upgrade():
128
128
  click.echo("Upgrade successful. Restart Pentool to use the new version.")
129
+ _sync_pro_package_after_upgrade()
129
130
  else:
130
131
  click.echo(
131
132
  "pip upgrade failed. You can update manually:\n"
@@ -135,6 +136,32 @@ def update_cmd(check_only: bool) -> None:
135
136
  raise SystemExit(1)
136
137
 
137
138
 
139
+ def _sync_pro_package_after_upgrade() -> None:
140
+ """Re-download the PRO package too, if one is active on this machine.
141
+
142
+ `pip install --upgrade pentool` only touches the FREE package — the PRO
143
+ package lives separately in ~/.pentool/pro/ and is otherwise only ever
144
+ (re)downloaded by check_and_update_pro_package(), which normally runs in
145
+ the background on TUI startup. Doing it here too means a CLI-driven
146
+ 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.
148
+ """
149
+ import asyncio
150
+
151
+ from pentool.core.license import PRO_PACKAGE_DIR, check_and_update_pro_package
152
+
153
+ if not PRO_PACKAGE_DIR.exists():
154
+ return # PRO was never activated on this machine — nothing to sync
155
+
156
+ try:
157
+ click.echo("Checking for a newer PRO build...")
158
+ updated = asyncio.run(check_and_update_pro_package())
159
+ if updated:
160
+ click.echo("PRO package updated to the latest build.")
161
+ except Exception as exc:
162
+ click.echo(f"Could not check for a PRO update: {exc}", err=True)
163
+
164
+
138
165
  @cli.group()
139
166
  def license() -> None:
140
167
  """License management: trial, activation, status."""
@@ -0,0 +1,137 @@
1
+ """Crash reporter — collects and sends anonymous crash reports.
2
+
3
+ Also sends a one-shot "first run" ping so the admin panel can show an
4
+ install counter — see send_first_run_ping(). Both go to the same
5
+ pentool-license Cloudflare Worker that already handles license
6
+ validation (pentool.core.license._LICENSE_API_BASE) — /api/crash and
7
+ /api/telemetry/first_run are public, unauthenticated ingestion
8
+ endpoints there, mirrored in pentool-backend's admin.html dashboard.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import asyncio
14
+ import os
15
+ import platform
16
+ import sys
17
+ import traceback
18
+ from typing import Any
19
+
20
+ # Same Worker as pentool.core.license — kept as a separate literal here
21
+ # (rather than importing _LICENSE_API_BASE) so crash reporting has no
22
+ # import-time dependency on the licensing module.
23
+ _API_BASE = "https://pentool-license.akashtanov2020.workers.dev"
24
+
25
+
26
+ def _get_metadata() -> dict[str, Any]:
27
+ """Collect anonymous metadata for crash report."""
28
+ try:
29
+ from pentool import __version__ as version
30
+ except Exception:
31
+ version = "unknown"
32
+ return {
33
+ "version": version,
34
+ "python": sys.version.split()[0],
35
+ "os": platform.system(),
36
+ "os_version": platform.release(),
37
+ "arch": platform.machine(),
38
+ }
39
+
40
+
41
+ def _anonymize(text: str) -> str:
42
+ """Remove paths and potential tokens from traceback text."""
43
+ import re
44
+ # Remove absolute paths — keep only filename
45
+ text = re.sub(r' File "([^"]+)"', lambda m: f' File "{os.path.basename(m.group(1))}"', text)
46
+ # Remove anything that looks like a token/key (long hex strings)
47
+ text = re.sub(r'\b[A-Fa-f0-9]{32,}\b', '<TOKEN>', text)
48
+ return text
49
+
50
+
51
+ async def send_crash_async(exc: BaseException, endpoint: str = f"{_API_BASE}/api/crash") -> None:
52
+ """Send crash report asynchronously. Silently ignores any errors."""
53
+ try:
54
+ import aiohttp # type: ignore[import]
55
+ except ImportError:
56
+ return # aiohttp not available — skip silently
57
+
58
+ try:
59
+ tb_lines = traceback.format_exception(type(exc), exc, exc.__traceback__)
60
+ tb_text = _anonymize("".join(tb_lines))
61
+ payload = {
62
+ "traceback": tb_text,
63
+ "exception": type(exc).__name__,
64
+ **_get_metadata(),
65
+ }
66
+ timeout = aiohttp.ClientTimeout(total=8)
67
+ async with aiohttp.ClientSession(timeout=timeout) as session:
68
+ await session.post(endpoint, json=payload, ssl=False)
69
+ except Exception:
70
+ pass # never propagate errors from crash reporter
71
+
72
+
73
+ def send_crash(exc: BaseException) -> None:
74
+ """Fire-and-forget crash report from sync context."""
75
+ try:
76
+ from pentool.core.config import get_config
77
+ cfg = get_config()
78
+ if not getattr(cfg, "send_crash_reports", True):
79
+ return
80
+ except Exception:
81
+ pass
82
+
83
+ try:
84
+ loop = asyncio.new_event_loop()
85
+ loop.run_until_complete(send_crash_async(exc))
86
+ loop.close()
87
+ except Exception:
88
+ pass
89
+
90
+
91
+ async def send_first_run_ping_async(endpoint: str = f"{_API_BASE}/api/telemetry/first_run") -> None:
92
+ """Ping the install counter once per machine. Silently ignores errors.
93
+
94
+ Dedup happens server-side (keyed by machine_id) — safe to call on every
95
+ startup; only the very first call from a given machine increments the
96
+ counter, so no local "have I already sent this" state is needed here.
97
+ """
98
+ try:
99
+ import aiohttp # type: ignore[import]
100
+ except ImportError:
101
+ return
102
+
103
+ try:
104
+ from pentool.core.license import get_machine_id
105
+ machine_id = get_machine_id()
106
+ except Exception:
107
+ return
108
+
109
+ try:
110
+ timeout = aiohttp.ClientTimeout(total=8)
111
+ async with aiohttp.ClientSession(timeout=timeout) as session:
112
+ await session.post(endpoint, json={"machine_id": machine_id}, ssl=False)
113
+ except Exception:
114
+ pass
115
+
116
+
117
+ def send_first_run_ping() -> None:
118
+ """Fire-and-forget install-counter ping from sync context.
119
+
120
+ Respects the same send_crash_reports opt-out as send_crash() — both are
121
+ "anonymous telemetry to Anthropic-free, self-hosted infra" in spirit,
122
+ so one setting covers both rather than adding a second toggle.
123
+ """
124
+ try:
125
+ from pentool.core.config import get_config
126
+ cfg = get_config()
127
+ if not getattr(cfg, "send_crash_reports", True):
128
+ return
129
+ except Exception:
130
+ pass
131
+
132
+ try:
133
+ loop = asyncio.new_event_loop()
134
+ loop.run_until_complete(send_first_run_ping_async())
135
+ loop.close()
136
+ except Exception:
137
+ pass
@@ -503,6 +503,13 @@ 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.
510
+ _PRO_BUILD_MARKER = PRO_PACKAGE_DIR / ".build_id"
511
+
512
+
506
513
  async def download_pro_package(key: str, machine_id: str) -> bool:
507
514
  """Download, verify, and install the obfuscated PRO package.
508
515
 
@@ -555,6 +562,85 @@ async def download_pro_package(key: str, machine_id: str) -> bool:
555
562
  finally:
556
563
  tmp_archive.unlink(missing_ok=True)
557
564
 
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.
568
+ try:
569
+ build_id = await _fetch_pro_build_id(plat)
570
+ if build_id:
571
+ _PRO_BUILD_MARKER.write_text(build_id, encoding="utf-8")
572
+ except Exception:
573
+ pass # non-fatal — worst case, next check just re-downloads once more
574
+
558
575
  return True
559
576
  except Exception:
560
577
  return False
578
+
579
+
580
+ async def _fetch_pro_build_id(plat: str) -> str | None:
581
+ """Query /api/pro/version for the current PRO release's build_id.
582
+
583
+ No license key needed — this only identifies which build is published,
584
+ not whether the caller is entitled to it (that's still enforced by
585
+ /api/download re-validating the key server-side). Returns None on any
586
+ failure (network, missing asset, etc.) — callers should treat that as
587
+ "couldn't check, don't act on it" rather than "no update available".
588
+ """
589
+ try:
590
+ import aiohttp
591
+ async with aiohttp.ClientSession(
592
+ timeout=aiohttp.ClientTimeout(total=8)
593
+ ) as session:
594
+ async with session.get(
595
+ f"{_LICENSE_API_BASE}/api/pro/version",
596
+ params={"platform": plat},
597
+ ssl=False,
598
+ ) as resp:
599
+ if resp.status != 200:
600
+ return None
601
+ data = await resp.json()
602
+ return data.get("build_id")
603
+ except Exception:
604
+ return None
605
+
606
+
607
+ async def check_and_update_pro_package() -> bool:
608
+ """Re-download the PRO package if a newer build has been published.
609
+
610
+ Called on TUI startup (background worker, same pattern as
611
+ _check_for_updates for the pip package) and after `pentool update` — this
612
+ is what closes the gap where pentool-pro ships a fix but a machine that
613
+ already activated its license keeps running the stale code from
614
+ ~/.pentool/pro/ forever, because download_pro_package() otherwise only
615
+ ever runs once, at initial activation/trial-start.
616
+
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.
624
+ """
625
+ info = get_license()
626
+ if not info.valid or not info.features:
627
+ return False
628
+ if not PRO_PACKAGE_DIR.exists():
629
+ return False # never activated on this machine — nothing to refresh
630
+
631
+ plat = _current_platform()
632
+ remote_build_id = await _fetch_pro_build_id(plat)
633
+ if not remote_build_id:
634
+ return False # couldn't reach the server — don't touch anything
635
+
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
642
+
643
+ if local_build_id == remote_build_id:
644
+ return False # already on the latest build
645
+
646
+ return await download_pro_package(info.license_key, info.machine_id)
@@ -23,6 +23,7 @@ class ProxyService(BaseService):
23
23
  event_bus: EventBus | None = None,
24
24
  tui_loop: asyncio.AbstractEventLoop | None = None,
25
25
  on_log: Callable[[str], None] | None = None,
26
+ on_storage_error: Callable[[str], None] | None = None,
26
27
  ) -> None:
27
28
  super().__init__(event_bus=event_bus, tui_loop=tui_loop, on_log=on_log)
28
29
  self._proxy_api = proxy_api
@@ -31,6 +32,20 @@ class ProxyService(BaseService):
31
32
  self._storage_ready = False
32
33
  self._pre_storage_queue: list[InterceptedRequest] = []
33
34
  self._pre_storage_queue_max = 2000
35
+ # Optional callback(message: str) — fired when the DB file can't be
36
+ # opened (e.g. "database is locked" because another Pentool
37
+ # instance/process already has it open, or the path is unwritable).
38
+ # Without this, such failures were logged only — requests silently
39
+ # stopped being saved with zero indication in the UI of why.
40
+ self._on_storage_error = on_storage_error
41
+
42
+ def _report_storage_error(self, context: str, exc: Exception) -> None:
43
+ msg = str(exc) or type(exc).__name__
44
+ if self._on_storage_error:
45
+ try:
46
+ self._on_storage_error(f"{context}: {msg}")
47
+ except Exception:
48
+ pass
34
49
 
35
50
  async def init_storage(self) -> None:
36
51
  try:
@@ -49,6 +64,7 @@ class ProxyService(BaseService):
49
64
  self._pre_storage_queue.clear()
50
65
  except Exception as exc:
51
66
  logger.error("ProxyService: storage init failed: %s", exc)
67
+ self._report_storage_error("Failed to open project database", exc)
52
68
 
53
69
  async def store_request(self, req: InterceptedRequest) -> int | None:
54
70
  if not self._storage_ready:
@@ -193,6 +209,9 @@ class ProxyService(BaseService):
193
209
  except Exception as exc:
194
210
  logger.error("ProxyService: switch_db failed: %s", exc)
195
211
  self._storage_ready = False
212
+ self._report_storage_error(
213
+ f"Failed to open project database ({new_db_path})", exc
214
+ )
196
215
 
197
216
  async def clear_history(self) -> None:
198
217
  if not self._storage_ready:
@@ -10,6 +10,7 @@ import threading
10
10
  from textual import on
11
11
  from textual.app import App, ComposeResult
12
12
  from textual.binding import Binding
13
+ from textual.containers import Vertical
13
14
  from textual.widgets import Checkbox, ContentSwitcher, Footer
14
15
 
15
16
  # Nicer checkbox glyph applied project-wide — Checkbox is a subclass of
@@ -135,6 +136,31 @@ class PentoolApp(App):
135
136
  dock: bottom;
136
137
  }
137
138
 
139
+ /* Wrapper around Footer + StatusBar — see compose()'s comment for why
140
+ this exists: two independently-docked bottom widgets overlap rather
141
+ than stack, so both are laid out normally inside one docked container
142
+ instead. Height 2 = Footer's 1 + StatusBar's 1.
143
+
144
+ Both Footer (in its own DEFAULT_CSS) and StatusBar (in statusbar.tcss)
145
+ hard-code `dock: bottom` themselves — just nesting them inside this
146
+ wrapper is not enough, they'd still each dock to the bottom of the
147
+ wrapper and overlap each other exactly as before. Both docks must be
148
+ explicitly cancelled here so they lay out normally (stacked, one
149
+ above the other) inside #bottom-dock instead. */
150
+ #bottom-dock {
151
+ dock: bottom;
152
+ height: 2;
153
+ layout: vertical;
154
+ }
155
+ #bottom-dock Footer {
156
+ dock: none;
157
+ height: 1;
158
+ }
159
+ #bottom-dock StatusBar {
160
+ dock: none;
161
+ height: 1;
162
+ }
163
+
138
164
  /* Global checkbox style — compact, height 1, no border.
139
165
  Glyph itself ("[ ]" / "[✓]") is set once in Checkbox.BUTTON_LEFT/
140
166
  INNER/RIGHT above; this just controls color/spacing/hover so every
@@ -268,8 +294,17 @@ class PentoolApp(App):
268
294
  yield ExtensionsScreen(id="screen-extensions")
269
295
  yield TerminalScreen(id="screen-terminal")
270
296
  yield SettingsScreen(id="screen-settings")
271
- yield StatusBar(id="statusbar")
272
- yield Footer()
297
+ # Footer and StatusBar both used to `dock: bottom` independently —
298
+ # in Textual, multiple independently-docked widgets at the same
299
+ # edge don't stack, they all pin to the same row and overlap each
300
+ # other (whichever was composed last wins the pixels). That's why
301
+ # StatusBar (and, inside it, the new ActivityIndicator) never
302
+ # rendered — Footer, composed after it, was drawn on top and
303
+ # covered it completely. Wrapping both in one docked container
304
+ # lets them lay out normally (one above the other) inside it.
305
+ with Vertical(id="bottom-dock"):
306
+ yield Footer()
307
+ yield StatusBar(id="statusbar")
273
308
 
274
309
  async def on_mount(self) -> None:
275
310
  setup_logging(self._cfg.log_file, self._cfg.log_level)
@@ -298,6 +333,7 @@ class PentoolApp(App):
298
333
  proxy_api=self._proxy_api,
299
334
  db_path=self._cfg.db_path,
300
335
  event_bus=get_event_bus(),
336
+ on_storage_error=self._on_storage_error,
301
337
  )
302
338
  try:
303
339
  from pentool.tui.screens.proxy.screen import ProxyScreen
@@ -372,6 +408,12 @@ class PentoolApp(App):
372
408
  if getattr(self._cfg, "check_updates", True):
373
409
  self.run_worker(self._check_for_updates(), exclusive=False, thread=False)
374
410
 
411
+ # Check for a newer PRO build in background (if a PRO license is
412
+ # active) — closes the gap where pentool-pro ships a fix but an
413
+ # already-activated machine keeps running the stale code forever,
414
+ # since the PRO package otherwise only downloads once, at activation.
415
+ self.run_worker(self._check_for_pro_update(), exclusive=False, thread=False)
416
+
375
417
  async def _auto_open_last_project(self) -> None:
376
418
  """Open the last project from recent_projects at startup."""
377
419
  await asyncio.sleep(0.2) # give Textual time to render the UI
@@ -403,6 +445,30 @@ class PentoolApp(App):
403
445
  except Exception as exc:
404
446
  logger.debug("APP: update check failed: %s", exc)
405
447
 
448
+ async def _check_for_pro_update(self) -> None:
449
+ """Re-download the PRO package in the background if a newer build
450
+ has been published, then notify so the user knows to restart.
451
+
452
+ No-op (silently) if there's no active PRO license or the package was
453
+ never downloaded in the first place — see
454
+ pentool.core.license.check_and_update_pro_package for the actual
455
+ build_id comparison logic.
456
+ """
457
+ import asyncio as _asyncio
458
+ await _asyncio.sleep(4.0) # after the pip-update check, UI settled
459
+ try:
460
+ from pentool.core.license import check_and_update_pro_package
461
+ updated = await check_and_update_pro_package()
462
+ if updated:
463
+ self.notify(
464
+ "PRO package updated to the latest build — restart Pentool to use it.",
465
+ severity="information",
466
+ timeout=10,
467
+ )
468
+ logger.info("APP: PRO package updated to a newer build")
469
+ except Exception as exc:
470
+ logger.debug("APP: PRO update check failed: %s", exc)
471
+
406
472
  def _setup_auto_save(self) -> None:
407
473
  """Configure / restart the auto-save timer from the current config.
408
474
 
@@ -649,6 +715,24 @@ class PentoolApp(App):
649
715
  except Exception:
650
716
  pass
651
717
 
718
+ def _on_storage_error(self, message: str) -> None:
719
+ """ProxyService.init_storage()/switch_db() failed to open the DB.
720
+
721
+ Most common cause: the project's .db file is locked by another
722
+ process (e.g. a second Pentool instance already has it open) or the
723
+ path is unwritable. Previously this was logged only — requests
724
+ silently stopped being saved to history with no visible indication
725
+ why. init_storage()/switch_db() both run on the app's own event
726
+ loop (awaited directly, never from a worker thread), so a plain
727
+ notify() here is safe without call_from_thread.
728
+ """
729
+ self.notify(
730
+ f"{message}\n\nRequests will not be saved until this is fixed — "
731
+ f"check that no other Pentool instance has this project open.",
732
+ severity="error",
733
+ timeout=8,
734
+ )
735
+
652
736
  def show_context_menu(
653
737
  self,
654
738
  items: list[tuple[str, str]],
@@ -769,11 +853,22 @@ class PentoolApp(App):
769
853
  await self._proxy._server.serve_forever()
770
854
  except Exception as exc:
771
855
  logger.error("Proxy error: %s", exc)
856
+ # Surface this to the user — previously only logged, so a
857
+ # "port already in use" / "another process holds this file"
858
+ # failure looked like the proxy silently did nothing when the
859
+ # toolbar button was pressed, with no clue why.
860
+ msg = str(exc) or type(exc).__name__
861
+ self.call_from_thread(
862
+ self.notify,
863
+ f"Proxy failed to start: {msg}",
864
+ severity="error",
865
+ timeout=6,
866
+ )
772
867
  finally:
773
868
  self.call_from_thread(self._update_status)
774
869
  self.call_from_thread(self._update_proxy_screen_labels)
775
870
  self.call_from_thread(self._update_dashboard_proxy_status, False)
776
- self.call_from_thread(self.flash, "○ Proxy остановлен", "warning")
871
+ self.call_from_thread(self.flash, "○ Proxy stopped", "warning")
777
872
 
778
873
  def _stop_proxy(self) -> None:
779
874
  logger.info("APP: _stop_proxy called")
@@ -0,0 +1,9 @@
1
+ """tui.dialogs package: modal dialogs shared across screens.
2
+
3
+ extend_path lets the PRO package (pro/pentool/tui/dialogs/, e.g.
4
+ checks_dialog.py) merge into this namespace the same way
5
+ pentool.tui/pentool.tui.screens already do — see pentool/tui/__init__.py
6
+ and pentool/tui/screens/__init__.py for the same pattern.
7
+ """
8
+ import pkgutil
9
+ __path__ = pkgutil.extend_path(__path__, __name__)
@@ -50,8 +50,6 @@ class TargetScreen(Widget):
50
50
  tooltip="Crawl the selected host in the tree with the Spider"
51
51
  )
52
52
  yield Static(" │ ", classes="toolbar-sep")
53
- yield ToolbarButton("↺ Reload from DB", "btn-refresh")
54
- yield Static(" │ ", classes="toolbar-sep")
55
53
  yield ToolbarButton("🗑 Clear", "btn-clear")
56
54
  yield Static(" │ ", classes="toolbar-sep")
57
55
  yield ToolbarButton("📄 Export JSON", "btn-export")
@@ -64,7 +62,7 @@ class TargetScreen(Widget):
64
62
  yield RichLog(id="detail-log", markup=True, highlight=False)
65
63
 
66
64
  yield Static(
67
- "Refresh: Reload site map │ Scope: Add/Remove Scope │ M: Context menu",
65
+ "Scope: Add/Remove Scope │ M: Context menu",
68
66
  id="status-bar",
69
67
  )
70
68
 
@@ -79,6 +77,17 @@ class TargetScreen(Widget):
79
77
  return self._target_api
80
78
 
81
79
  def _load_sitemap(self) -> None:
80
+ # Reset selection/details BEFORE the async reload — otherwise a stale
81
+ # node from the previous project stays shown in the Details panel
82
+ # until the user clicks something in the new tree (looks like an
83
+ # artifact "leaking" between projects).
84
+ self._selected_host = None
85
+ self._selected_node_data = None
86
+ try:
87
+ self.query_one("#detail-log", RichLog).clear()
88
+ except Exception:
89
+ pass
90
+
82
91
  # Save current scope hosts before reload
83
92
  scope_hosts: set[str] = set()
84
93
  try:
@@ -243,10 +252,6 @@ class TargetScreen(Widget):
243
252
  def on_btn_crawl_host(self, _: ToolbarButton.Pressed) -> None:
244
253
  self.action_crawl_selected_host()
245
254
 
246
- @on(ToolbarButton.Pressed, "#btn-refresh")
247
- def on_btn_refresh(self, _: ToolbarButton.Pressed) -> None:
248
- self._load_sitemap()
249
-
250
255
  @on(ToolbarButton.Pressed, "#btn-clear")
251
256
  def on_btn_clear(self, _: ToolbarButton.Pressed) -> None:
252
257
  self.action_clear()
@@ -314,6 +319,12 @@ class TargetScreen(Widget):
314
319
  api = self._get_api()
315
320
  api.clear()
316
321
  self._build_tree({})
322
+ self._selected_host = None
323
+ self._selected_node_data = None
324
+ try:
325
+ self.query_one("#detail-log", RichLog).clear()
326
+ except Exception:
327
+ pass
317
328
  self.app.notify("Site map cleared", severity="information")
318
329
  except Exception as exc:
319
330
  logger.warning("_clear_worker: %s", exc)