pentool 0.2.0__tar.gz → 0.2.1__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.1}/PKG-INFO +5 -4
  2. {pentool-0.2.0 → pentool-0.2.1}/README.md +4 -3
  3. {pentool-0.2.0 → pentool-0.2.1}/pentool/__init__.py +2 -2
  4. {pentool-0.2.0 → pentool-0.2.1}/pentool/__main__.py +12 -0
  5. pentool-0.2.1/pentool/core/crash_reporter.py +137 -0
  6. {pentool-0.2.0 → pentool-0.2.1}/pentool/services/proxy_service.py +19 -0
  7. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/app.py +68 -3
  8. pentool-0.2.1/pentool/tui/dialogs/__init__.py +9 -0
  9. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/target/screen.py +18 -7
  10. pentool-0.2.1/pentool/tui/widgets/activity_indicator.py +143 -0
  11. pentool-0.2.1/pentool/tui/widgets/activity_indicator.tcss +30 -0
  12. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/module_tabs.py +1 -1
  13. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/statusbar.py +7 -1
  14. {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/PKG-INFO +5 -4
  15. {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/SOURCES.txt +2 -0
  16. {pentool-0.2.0 → pentool-0.2.1}/pyproject.toml +1 -1
  17. pentool-0.2.0/pentool/core/crash_reporter.py +0 -75
  18. pentool-0.2.0/pentool/tui/mixins/__init__.py +0 -0
  19. {pentool-0.2.0 → pentool-0.2.1}/LICENSE +0 -0
  20. {pentool-0.2.0 → pentool-0.2.1}/pentool/api/__init__.py +0 -0
  21. {pentool-0.2.0 → pentool-0.2.1}/pentool/api/base_api.py +0 -0
  22. {pentool-0.2.0 → pentool-0.2.1}/pentool/api/comparer_api.py +0 -0
  23. {pentool-0.2.0 → pentool-0.2.1}/pentool/api/decoder_api.py +0 -0
  24. {pentool-0.2.0 → pentool-0.2.1}/pentool/api/intruder_api.py +0 -0
  25. {pentool-0.2.0 → pentool-0.2.1}/pentool/api/proxy_api.py +0 -0
  26. {pentool-0.2.0 → pentool-0.2.1}/pentool/api/repeater_api.py +0 -0
  27. {pentool-0.2.0 → pentool-0.2.1}/pentool/api/sequencer_api.py +0 -0
  28. {pentool-0.2.0 → pentool-0.2.1}/pentool/api/spider_api.py +0 -0
  29. {pentool-0.2.0 → pentool-0.2.1}/pentool/api/target_api.py +0 -0
  30. {pentool-0.2.0 → pentool-0.2.1}/pentool/cli/__init__.py +0 -0
  31. {pentool-0.2.0 → pentool-0.2.1}/pentool/cli/main.py +0 -0
  32. {pentool-0.2.0 → pentool-0.2.1}/pentool/cli/project.py +0 -0
  33. {pentool-0.2.0 → pentool-0.2.1}/pentool/cli/proxy.py +0 -0
  34. {pentool-0.2.0 → pentool-0.2.1}/pentool/cli/scan.py +0 -0
  35. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/__init__.py +0 -0
  36. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/config.py +0 -0
  37. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/database.py +0 -0
  38. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/event_bus.py +0 -0
  39. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/events.py +0 -0
  40. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/features.py +0 -0
  41. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/license.py +0 -0
  42. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/logging.py +0 -0
  43. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/plugin_manager.py +0 -0
  44. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/project.py +0 -0
  45. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/storage_interface.py +0 -0
  46. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/updater.py +0 -0
  47. {pentool-0.2.0 → pentool-0.2.1}/pentool/core/utils.py +0 -0
  48. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/__init__.py +0 -0
  49. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/comparer.py +0 -0
  50. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/decoder.py +0 -0
  51. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/intruder.py +0 -0
  52. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/match_replace.py +0 -0
  53. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/proxy.py +0 -0
  54. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/repeater.py +0 -0
  55. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/sequencer.py +0 -0
  56. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/spider.py +0 -0
  57. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/target.py +0 -0
  58. {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/websocket_handler.py +0 -0
  59. {pentool-0.2.0 → pentool-0.2.1}/pentool/plugins/__init__.py +0 -0
  60. {pentool-0.2.0 → pentool-0.2.1}/pentool/plugins/builtin/__init__.py +0 -0
  61. {pentool-0.2.0 → pentool-0.2.1}/pentool/plugins/example_plugin.py +0 -0
  62. {pentool-0.2.0 → pentool-0.2.1}/pentool/plugins/example_plugin.tcss +0 -0
  63. {pentool-0.2.0 → pentool-0.2.1}/pentool/services/__init__.py +0 -0
  64. {pentool-0.2.0 → pentool-0.2.1}/pentool/services/base_service.py +0 -0
  65. {pentool-0.2.0 → pentool-0.2.1}/pentool/services/intruder_service.py +0 -0
  66. {pentool-0.2.0 → pentool-0.2.1}/pentool/services/repeater_service.py +0 -0
  67. {pentool-0.2.0 → pentool-0.2.1}/pentool/services/scan_service.py +0 -0
  68. {pentool-0.2.0 → pentool-0.2.1}/pentool/storage/__init__.py +0 -0
  69. {pentool-0.2.0 → pentool-0.2.1}/pentool/storage/http_storage.py +0 -0
  70. {pentool-0.2.0 → pentool-0.2.1}/pentool/storage/large_body_handler.py +0 -0
  71. {pentool-0.2.0 → pentool-0.2.1}/pentool/storage/lru_cache.py +0 -0
  72. {pentool-0.2.0 → pentool-0.2.1}/pentool/t.py +0 -0
  73. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/__init__.py +0 -0
  74. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/constants.py +0 -0
  75. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/cert_dialog.py +0 -0
  76. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/cert_dialog.tcss +0 -0
  77. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/file_selector.py +0 -0
  78. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/file_selector.tcss +0 -0
  79. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/load_from_proxy.py +0 -0
  80. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/load_from_proxy.tcss +0 -0
  81. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/match_replace_dialog.py +0 -0
  82. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/match_replace_dialog.tcss +0 -0
  83. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/project_dialog.py +0 -0
  84. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/project_dialog.tcss +0 -0
  85. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/scope_dialog.py +0 -0
  86. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/scope_dialog.tcss +0 -0
  87. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/messages.py +0 -0
  88. {pentool-0.2.0/pentool/tui/dialogs → pentool-0.2.1/pentool/tui/mixins}/__init__.py +0 -0
  89. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/mixins/app_mixin.py +0 -0
  90. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/mixins/dialog_cancel.py +0 -0
  91. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/mixins/request_context_menu.py +0 -0
  92. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/project_manager.py +0 -0
  93. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/__init__.py +0 -0
  94. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/base.py +0 -0
  95. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/comparer/__init__.py +0 -0
  96. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/comparer/screen.py +0 -0
  97. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/comparer/screen.tcss +0 -0
  98. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/dashboard/__init__.py +0 -0
  99. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/dashboard/live_dashboard.py +0 -0
  100. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/dashboard/screen.py +0 -0
  101. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/dashboard/screen.tcss +0 -0
  102. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/decoder/__init__.py +0 -0
  103. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/decoder/screen.py +0 -0
  104. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/decoder/screen.tcss +0 -0
  105. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/extensions/__init__.py +0 -0
  106. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/extensions/screen.py +0 -0
  107. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/extensions/screen.tcss +0 -0
  108. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/intruder/__init__.py +0 -0
  109. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/intruder/screen.py +0 -0
  110. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/intruder/screen.tcss +0 -0
  111. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/proxy/__init__.py +0 -0
  112. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/proxy/screen.py +0 -0
  113. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/proxy/screen.tcss +0 -0
  114. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/repeater/__init__.py +0 -0
  115. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/repeater/screen.py +0 -0
  116. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/repeater/screen.tcss +0 -0
  117. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/scanner_unavailable.py +0 -0
  118. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/sequencer/__init__.py +0 -0
  119. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/sequencer/screen.py +0 -0
  120. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/sequencer/screen.tcss +0 -0
  121. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/settings/__init__.py +0 -0
  122. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/settings/hotkeys.py +0 -0
  123. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/settings/hotkeys.tcss +0 -0
  124. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/settings/screen.py +0 -0
  125. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/settings/screen.tcss +0 -0
  126. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/spider/__init__.py +0 -0
  127. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/spider/screen.py +0 -0
  128. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/spider/screen.tcss +0 -0
  129. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/target/__init__.py +0 -0
  130. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/target/screen.tcss +0 -0
  131. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/terminal/__init__.py +0 -0
  132. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/terminal/screen.py +0 -0
  133. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/terminal/screen.tcss +0 -0
  134. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/__init__.py +0 -0
  135. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/context_menu.py +0 -0
  136. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/context_menu.tcss +0 -0
  137. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/filter_bar.py +0 -0
  138. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/filter_bar.tcss +0 -0
  139. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/inspector_panel.py +0 -0
  140. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/inspector_panel.tcss +0 -0
  141. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/menu.tcss +0 -0
  142. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/menu_bar.tcss +0 -0
  143. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/module_tabs.tcss +0 -0
  144. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/payload_drop_zone.py +0 -0
  145. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/payload_drop_zone.tcss +0 -0
  146. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/request_editor.py +0 -0
  147. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/request_editor.tcss +0 -0
  148. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/resize_handle.py +0 -0
  149. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/resize_handle.tcss +0 -0
  150. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/search_bar.py +0 -0
  151. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/search_bar.tcss +0 -0
  152. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/statusbar.tcss +0 -0
  153. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/toolbar_button.py +0 -0
  154. {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/toolbar_button.tcss +0 -0
  155. {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/__init__.py +0 -0
  156. {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/cert.py +0 -0
  157. {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/coder.py +0 -0
  158. {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/copy_as.py +0 -0
  159. {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/diff.py +0 -0
  160. {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/http_client.py +0 -0
  161. {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/parser.py +0 -0
  162. {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/terminal_check.py +0 -0
  163. {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/dependency_links.txt +0 -0
  164. {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/entry_points.txt +0 -0
  165. {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/requires.txt +0 -0
  166. {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/top_level.txt +0 -0
  167. {pentool-0.2.0 → pentool-0.2.1}/setup.cfg +0 -0
  168. {pentool-0.2.0 → pentool-0.2.1}/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.1
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.1"
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()
@@ -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
@@ -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
@@ -649,6 +685,24 @@ class PentoolApp(App):
649
685
  except Exception:
650
686
  pass
651
687
 
688
+ def _on_storage_error(self, message: str) -> None:
689
+ """ProxyService.init_storage()/switch_db() failed to open the DB.
690
+
691
+ Most common cause: the project's .db file is locked by another
692
+ process (e.g. a second Pentool instance already has it open) or the
693
+ path is unwritable. Previously this was logged only — requests
694
+ silently stopped being saved to history with no visible indication
695
+ why. init_storage()/switch_db() both run on the app's own event
696
+ loop (awaited directly, never from a worker thread), so a plain
697
+ notify() here is safe without call_from_thread.
698
+ """
699
+ self.notify(
700
+ f"{message}\n\nRequests will not be saved until this is fixed — "
701
+ f"check that no other Pentool instance has this project open.",
702
+ severity="error",
703
+ timeout=8,
704
+ )
705
+
652
706
  def show_context_menu(
653
707
  self,
654
708
  items: list[tuple[str, str]],
@@ -769,11 +823,22 @@ class PentoolApp(App):
769
823
  await self._proxy._server.serve_forever()
770
824
  except Exception as exc:
771
825
  logger.error("Proxy error: %s", exc)
826
+ # Surface this to the user — previously only logged, so a
827
+ # "port already in use" / "another process holds this file"
828
+ # failure looked like the proxy silently did nothing when the
829
+ # toolbar button was pressed, with no clue why.
830
+ msg = str(exc) or type(exc).__name__
831
+ self.call_from_thread(
832
+ self.notify,
833
+ f"Proxy failed to start: {msg}",
834
+ severity="error",
835
+ timeout=6,
836
+ )
772
837
  finally:
773
838
  self.call_from_thread(self._update_status)
774
839
  self.call_from_thread(self._update_proxy_screen_labels)
775
840
  self.call_from_thread(self._update_dashboard_proxy_status, False)
776
- self.call_from_thread(self.flash, "○ Proxy остановлен", "warning")
841
+ self.call_from_thread(self.flash, "○ Proxy stopped", "warning")
777
842
 
778
843
  def _stop_proxy(self) -> None:
779
844
  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)
@@ -0,0 +1,143 @@
1
+ """ActivityIndicator — small emoji-based "what's running right now" strip.
2
+
3
+ Sits in StatusBar, to the right of the existing proxy/project/time fields.
4
+ Each tracked background process gets one emoji glyph that is bright (its
5
+ normal color) while active and dim/grey while idle — Proxy 🌐, Spider 🕷,
6
+ Scanner 🔍, Intruder 💥. Hovering shows a tooltip listing exactly which
7
+ processes are currently running (or "All idle").
8
+
9
+ Deliberately implemented as polling rather than another EventBus
10
+ subscription: every module already tracks its own running/scanning state
11
+ in a plain attribute (ProxyServer.is_running, SpiderScreen._running,
12
+ ScannerScreen._tabs[*].scanning, IntruderScreen._running) — wiring up
13
+ start/stop events for four more places would touch a lot of module code
14
+ for a purely cosmetic feature. Reading those attributes off a 1s timer
15
+ (the same cadence StatusBar already uses for the clock) is simpler and
16
+ cannot get out of sync with reality the way a missed event could.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ from pathlib import Path
23
+ from typing import Callable
24
+
25
+ from textual.app import ComposeResult
26
+ from textual.widget import Widget
27
+ from textual.widgets import Static
28
+
29
+ _CSS = (Path(__file__).parent / "activity_indicator.tcss").read_text(encoding="utf-8")
30
+
31
+
32
+ @dataclass
33
+ class _Tracked:
34
+ key: str
35
+ emoji: str
36
+ label: str
37
+ # () -> bool | None — None means "could not determine" (module screen
38
+ # not mounted / PRO scanner unavailable / etc.), treated as idle.
39
+ is_active: Callable[[], bool | None]
40
+
41
+
42
+ class ActivityIndicator(Widget):
43
+ """Emoji strip showing which background processes are currently active."""
44
+
45
+ DEFAULT_CSS = _CSS
46
+
47
+ def __init__(self, app_ref, **kwargs) -> None:
48
+ """
49
+ Args:
50
+ app_ref: the PentoolApp instance (passed explicitly rather than
51
+ relying on self.app, which is only available once mounted —
52
+ the checker callables below are built at construction time).
53
+ """
54
+ super().__init__(**kwargs)
55
+ self._checkers: list[_Tracked] = self._build_checkers(app_ref)
56
+
57
+ # ── checker construction ────────────────────────────────────────────────
58
+
59
+ def _build_checkers(self, app_ref) -> list[_Tracked]:
60
+ from pentool.tui.constants import (
61
+ SCREEN_INTRUDER,
62
+ SCREEN_PROXY,
63
+ SCREEN_SCANNER,
64
+ SCREEN_SPIDER,
65
+ )
66
+
67
+ def _proxy_active() -> bool | None:
68
+ try:
69
+ proxy = getattr(app_ref, "_proxy", None)
70
+ if proxy is None:
71
+ return False
72
+ return bool(proxy.is_running)
73
+ except Exception:
74
+ return None
75
+
76
+ def _spider_active() -> bool | None:
77
+ try:
78
+ from pentool.tui.screens.spider.screen import SpiderScreen
79
+ screen = app_ref.query_one(SCREEN_SPIDER, SpiderScreen)
80
+ return bool(getattr(screen, "_running", False))
81
+ except Exception:
82
+ return None
83
+
84
+ def _scanner_active() -> bool | None:
85
+ try:
86
+ from pentool.tui.screens import SCANNER_SCREEN_AVAILABLE
87
+ if not SCANNER_SCREEN_AVAILABLE:
88
+ return False
89
+ from pentool.tui.screens.scanner.screen import ScannerScreen
90
+ screen = app_ref.query_one(SCREEN_SCANNER, ScannerScreen)
91
+ tabs = getattr(screen, "_tabs", None)
92
+ if not tabs:
93
+ return False
94
+ return any(getattr(t, "scanning", False) for t in tabs)
95
+ except Exception:
96
+ return None
97
+
98
+ def _intruder_active() -> bool | None:
99
+ try:
100
+ from pentool.tui.screens.intruder.screen import IntruderScreen
101
+ screen = app_ref.query_one(SCREEN_INTRUDER, IntruderScreen)
102
+ return bool(getattr(screen, "_running", False))
103
+ except Exception:
104
+ return None
105
+
106
+ return [
107
+ _Tracked("proxy", "🌐", "Proxy", _proxy_active),
108
+ _Tracked("spider", "🕷", "Spider", _spider_active),
109
+ _Tracked("scanner", "🔍", "Scanner", _scanner_active),
110
+ _Tracked("intruder", "💥", "Intruder", _intruder_active),
111
+ ]
112
+
113
+ # ── compose / refresh ────────────────────────────────────────────────────
114
+
115
+ def compose(self) -> ComposeResult:
116
+ for t in self._checkers:
117
+ yield Static(t.emoji, id=f"activity-{t.key}", classes="activity-glyph idle")
118
+
119
+ def on_mount(self) -> None:
120
+ self._refresh()
121
+ self.set_interval(1.0, self._refresh)
122
+
123
+ def _refresh(self) -> None:
124
+ active_labels: list[str] = []
125
+ for t in self._checkers:
126
+ try:
127
+ active = bool(t.is_active())
128
+ except Exception:
129
+ active = False
130
+ try:
131
+ widget = self.query_one(f"#activity-{t.key}", Static)
132
+ except Exception:
133
+ continue
134
+ if active:
135
+ widget.remove_class("idle")
136
+ widget.add_class("active")
137
+ active_labels.append(t.label)
138
+ else:
139
+ widget.remove_class("active")
140
+ widget.add_class("idle")
141
+
142
+ tooltip = ", ".join(active_labels) if active_labels else "All idle"
143
+ self.tooltip = tooltip
@@ -0,0 +1,30 @@
1
+ ActivityIndicator {
2
+ /* Match StatusBar's own background ($primary-darken-3) instead of
3
+ $panel — this sits inside StatusBar, not inside the module tabs
4
+ row above it, so it must blend with its own row, not the one
5
+ above. Kept as a distinct block only via the margin gaps on each
6
+ side, not a contrasting fill. */
7
+ width: auto;
8
+ height: 1;
9
+ layout: horizontal;
10
+ padding: 0 1;
11
+ margin: 0 1;
12
+ background: $primary-darken-3;
13
+ }
14
+ ActivityIndicator .activity-glyph {
15
+ width: 3;
16
+ height: 1;
17
+ text-align: center;
18
+ content-align: center middle;
19
+ }
20
+ /* Bright/saturated while the process is running */
21
+ ActivityIndicator .activity-glyph.active {
22
+ color: $success;
23
+ text-style: bold;
24
+ }
25
+ /* Dim/greyed-out while idle — same glyph, just muted */
26
+ ActivityIndicator .activity-glyph.idle {
27
+ color: $text-muted;
28
+ text-style: dim;
29
+ opacity: 60%;
30
+ }
@@ -94,7 +94,7 @@ class ModuleTabs(Widget):
94
94
  tab = tabs.query_one("#tab-scanner", Tab)
95
95
  if locked:
96
96
  tab.add_class("tab-locked")
97
- tab.tooltip = "Scanner — платная функция. Активируйте лицензию: pentool license trial"
97
+ tab.tooltip = "Scanner is a PRO feature. Activate a license: pentool license trial"
98
98
  else:
99
99
  tab.remove_class("tab-locked")
100
100
  tab.tooltip = None