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.
- {pentool-0.2.0 → pentool-0.2.1}/PKG-INFO +5 -4
- {pentool-0.2.0 → pentool-0.2.1}/README.md +4 -3
- {pentool-0.2.0 → pentool-0.2.1}/pentool/__init__.py +2 -2
- {pentool-0.2.0 → pentool-0.2.1}/pentool/__main__.py +12 -0
- pentool-0.2.1/pentool/core/crash_reporter.py +137 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/services/proxy_service.py +19 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/app.py +68 -3
- pentool-0.2.1/pentool/tui/dialogs/__init__.py +9 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/target/screen.py +18 -7
- pentool-0.2.1/pentool/tui/widgets/activity_indicator.py +143 -0
- pentool-0.2.1/pentool/tui/widgets/activity_indicator.tcss +30 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/module_tabs.py +1 -1
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/statusbar.py +7 -1
- {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/PKG-INFO +5 -4
- {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/SOURCES.txt +2 -0
- {pentool-0.2.0 → pentool-0.2.1}/pyproject.toml +1 -1
- pentool-0.2.0/pentool/core/crash_reporter.py +0 -75
- pentool-0.2.0/pentool/tui/mixins/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/LICENSE +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/api/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/api/base_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/api/comparer_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/api/decoder_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/api/intruder_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/api/proxy_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/api/repeater_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/api/sequencer_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/api/spider_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/api/target_api.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/cli/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/cli/main.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/cli/project.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/cli/proxy.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/cli/scan.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/config.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/database.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/event_bus.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/events.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/features.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/license.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/logging.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/plugin_manager.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/project.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/storage_interface.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/updater.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/core/utils.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/comparer.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/decoder.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/intruder.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/match_replace.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/proxy.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/repeater.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/sequencer.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/spider.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/target.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/modules/websocket_handler.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/plugins/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/plugins/builtin/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/plugins/example_plugin.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/plugins/example_plugin.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/services/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/services/base_service.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/services/intruder_service.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/services/repeater_service.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/services/scan_service.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/storage/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/storage/http_storage.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/storage/large_body_handler.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/storage/lru_cache.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/t.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/constants.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/cert_dialog.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/cert_dialog.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/file_selector.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/file_selector.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/load_from_proxy.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/load_from_proxy.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/match_replace_dialog.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/match_replace_dialog.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/project_dialog.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/project_dialog.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/scope_dialog.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/dialogs/scope_dialog.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/messages.py +0 -0
- {pentool-0.2.0/pentool/tui/dialogs → pentool-0.2.1/pentool/tui/mixins}/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/mixins/app_mixin.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/mixins/dialog_cancel.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/mixins/request_context_menu.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/project_manager.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/base.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/comparer/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/comparer/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/comparer/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/dashboard/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/dashboard/live_dashboard.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/dashboard/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/dashboard/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/decoder/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/decoder/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/decoder/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/extensions/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/extensions/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/extensions/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/intruder/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/intruder/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/intruder/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/proxy/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/proxy/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/proxy/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/repeater/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/repeater/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/repeater/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/scanner_unavailable.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/sequencer/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/sequencer/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/sequencer/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/settings/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/settings/hotkeys.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/settings/hotkeys.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/settings/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/settings/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/spider/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/spider/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/spider/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/target/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/target/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/terminal/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/terminal/screen.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/screens/terminal/screen.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/context_menu.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/context_menu.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/filter_bar.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/filter_bar.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/inspector_panel.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/inspector_panel.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/menu.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/menu_bar.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/module_tabs.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/payload_drop_zone.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/payload_drop_zone.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/request_editor.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/request_editor.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/resize_handle.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/resize_handle.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/search_bar.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/search_bar.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/statusbar.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/toolbar_button.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/tui/widgets/toolbar_button.tcss +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/__init__.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/cert.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/coder.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/copy_as.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/diff.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/http_client.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/parser.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool/utils/terminal_check.py +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/dependency_links.txt +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/entry_points.txt +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/requires.txt +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/pentool.egg-info/top_level.txt +0 -0
- {pentool-0.2.0 → pentool-0.2.1}/setup.cfg +0 -0
- {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.
|
|
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
|
-
|  |  |
|
|
73
|
+
|  |  |
|
|
74
74
|
|
|
75
75
|
| Proxy | Repeater |
|
|
76
76
|
|:-----:|:--------:|
|
|
77
|
-
|  |  |
|
|
77
|
+
|  |  |
|
|
78
78
|
|
|
79
79
|
| Intruder | Settings |
|
|
80
80
|
|:--------:|:--------:|
|
|
81
|
-
|  |  |
|
|
81
|
+
|  |  |
|
|
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
|
-
|  |  |
|
|
27
|
+
|  |  |
|
|
28
28
|
|
|
29
29
|
| Proxy | Repeater |
|
|
30
30
|
|:-----:|:--------:|
|
|
31
|
-
|  |  |
|
|
31
|
+
|  |  |
|
|
32
32
|
|
|
33
33
|
| Intruder | Settings |
|
|
34
34
|
|:--------:|:--------:|
|
|
35
|
-
|  |  |
|
|
35
|
+
|  |  |
|
|
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.
|
|
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
|
-
|
|
272
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|