pentool 0.2.7__tar.gz → 0.2.9__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.7 → pentool-0.2.9}/PKG-INFO +22 -2
- {pentool-0.2.7 → pentool-0.2.9}/README.md +21 -1
- {pentool-0.2.7 → pentool-0.2.9}/pentool/__init__.py +12 -1
- {pentool-0.2.7 → pentool-0.2.9}/pentool/__main__.py +31 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/api/intruder_api.py +115 -120
- pentool-0.2.9/pentool/api/intruder_repository.py +250 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/api/spider_api.py +70 -1
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/config.py +2 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/database.py +89 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/events.py +36 -1
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/license.py +24 -1
- pentool-0.2.9/pentool/core/notification_sound.py +123 -0
- pentool-0.2.9/pentool/modules/intruder.py +920 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/modules/proxy.py +28 -15
- {pentool-0.2.7 → pentool-0.2.9}/pentool/modules/spider.py +88 -7
- {pentool-0.2.7 → pentool-0.2.9}/pentool/services/proxy_service.py +41 -1
- {pentool-0.2.7 → pentool-0.2.9}/pentool/services/repeater_service.py +4 -1
- {pentool-0.2.7 → pentool-0.2.9}/pentool/services/scan_service.py +45 -19
- pentool-0.2.9/pentool/storage/base_sqlite_storage.py +87 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/storage/http_storage.py +9 -20
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/app.py +61 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/project_manager.py +10 -2
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/dashboard/screen.py +31 -9
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/intruder/screen.py +779 -105
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/intruder/screen.tcss +19 -1
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/proxy/screen.py +126 -30
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/repeater/screen.py +127 -10
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/repeater/screen.tcss +7 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/settings/screen.py +1 -51
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/settings/screen.tcss +3 -11
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/spider/screen.py +4 -1
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/target/screen.py +2 -1
- pentool-0.2.9/pentool/tui/widgets/diff_panel.py +70 -0
- pentool-0.2.9/pentool/tui/widgets/diff_panel.tcss +20 -0
- pentool-0.2.9/pentool/tui/widgets/notification_center.py +122 -0
- pentool-0.2.9/pentool/tui/widgets/notification_center.tcss +54 -0
- pentool-0.2.9/pentool/tui/widgets/option_cycler.py +74 -0
- pentool-0.2.9/pentool/tui/widgets/option_cycler.tcss +11 -0
- pentool-0.2.9/pentool/utils/auth_headers.py +31 -0
- pentool-0.2.9/pentool/utils/scope.py +61 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool.egg-info/PKG-INFO +22 -2
- {pentool-0.2.7 → pentool-0.2.9}/pentool.egg-info/SOURCES.txt +11 -0
- {pentool-0.2.7 → pentool-0.2.9}/pyproject.toml +1 -1
- pentool-0.2.7/pentool/modules/intruder.py +0 -460
- {pentool-0.2.7 → pentool-0.2.9}/LICENSE +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/api/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/api/base_api.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/api/comparer_api.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/api/decoder_api.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/api/proxy_api.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/api/repeater_api.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/api/sequencer_api.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/api/target_api.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/cli/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/cli/main.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/cli/project.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/cli/proxy.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/cli/scan.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/crash_reporter.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/event_bus.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/features.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/logging.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/plugin_manager.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/project.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/storage_interface.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/updater.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/core/utils.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/modules/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/modules/comparer.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/modules/decoder.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/modules/match_replace.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/modules/repeater.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/modules/sequencer.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/modules/target.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/modules/websocket_handler.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/plugins/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/plugins/builtin/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/plugins/example_plugin.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/plugins/example_plugin.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/services/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/services/base_service.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/services/intruder_service.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/storage/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/storage/large_body_handler.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/storage/lru_cache.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/t.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/constants.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/cert_dialog.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/cert_dialog.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/comment_dialog.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/comment_dialog.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/file_selector.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/file_selector.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/load_from_proxy.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/load_from_proxy.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/match_replace_dialog.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/match_replace_dialog.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/project_dialog.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/project_dialog.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/scope_dialog.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/dialogs/scope_dialog.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/messages.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/mixins/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/mixins/app_mixin.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/mixins/dialog_cancel.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/mixins/request_context_menu.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/base.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/comparer/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/comparer/screen.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/comparer/screen.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/dashboard/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/dashboard/live_dashboard.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/dashboard/screen.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/decoder/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/decoder/screen.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/decoder/screen.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/extensions/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/extensions/screen.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/extensions/screen.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/intruder/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/proxy/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/proxy/screen.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/repeater/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/scanner_unavailable.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/sequencer/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/sequencer/screen.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/sequencer/screen.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/settings/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/settings/hotkeys.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/settings/hotkeys.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/spider/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/spider/screen.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/target/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/target/screen.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/terminal/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/terminal/screen.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/screens/terminal/screen.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/activity_indicator.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/activity_indicator.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/context_menu.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/context_menu.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/filter_bar.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/filter_bar.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/inspector_panel.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/inspector_panel.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/menu.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/menu_bar.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/module_tabs.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/module_tabs.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/nice_checkbox.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/payload_drop_zone.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/payload_drop_zone.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/request_editor.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/request_editor.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/resize_handle.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/resize_handle.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/search_bar.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/search_bar.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/statusbar.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/statusbar.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/toolbar_button.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/tui/widgets/toolbar_button.tcss +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/utils/__init__.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/utils/cert.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/utils/coder.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/utils/copy_as.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/utils/diff.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/utils/http_client.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/utils/parser.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool/utils/terminal_check.py +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool.egg-info/dependency_links.txt +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool.egg-info/entry_points.txt +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool.egg-info/requires.txt +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/pentool.egg-info/top_level.txt +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/setup.cfg +0 -0
- {pentool-0.2.7 → pentool-0.2.9}/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.9
|
|
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
|
|
@@ -58,6 +58,25 @@ Dynamic: license-file
|
|
|
58
58
|
|
|
59
59
|
---
|
|
60
60
|
|
|
61
|
+
## 💸 Support & Pricing
|
|
62
|
+
|
|
63
|
+
> Pentool is open-source and free to use. If it saves you time on a pentest — consider supporting development.
|
|
64
|
+
> Full pricing details and sponsorship options: **[pentool.pro](https://pentool.pro)**
|
|
65
|
+
|
|
66
|
+
| | | |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| ⭐ **Sponsor (individual)** | **$5** one-time or /month | [Sponsor on GitHub](https://github.com/sponsors/DrXOps) |
|
|
69
|
+
| 🏢 **Sponsor (company)** | **$50** one-time or /month | Logo in README + website · [pentool.pro/sponsor](https://pentool.pro/sponsor) |
|
|
70
|
+
| 🔑 **PRO License — Early Access** | **$29** *(beta price, until Dec 31 2026)* | After release: $99/year |
|
|
71
|
+
|
|
72
|
+
> **🎁 Beta loyalty discount:** Everyone who buys PRO for $29 gets a **lifetime 50% discount on annual renewal** (i.e. $49.50/year instead of $99).
|
|
73
|
+
> This discount applies **only to first buyers during the beta — until December 31, 2026**.
|
|
74
|
+
|
|
75
|
+
**PRO license is issued manually during beta.**
|
|
76
|
+
To purchase, contact: **[@sudores](https://t.me/sudores)** (Telegram) · **dev@pentool.pro**
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
61
80
|
**Pentool** is a terminal-based (TUI) security toolkit for penetration testers and security researchers.
|
|
62
81
|
It combines HTTP interception, vulnerability scanning, automated attacks, and data analysis — all inside your terminal.
|
|
63
82
|
Fast, transparent, and built for real-world testing.
|
|
@@ -186,7 +205,8 @@ Pentool is built and maintained by a solo developer in spare time.
|
|
|
186
205
|
If it saves you hours on a pentest — consider giving back. Every contribution directly funds new features, fixes, and faster releases.
|
|
187
206
|
|
|
188
207
|
- ⭐ **[Star on GitHub](https://github.com/DrXOps/pentool)** — free, takes 2 seconds, helps visibility enormously
|
|
189
|
-
-
|
|
208
|
+
- 💸 **[Sponsor the project](https://pentool.pro/sponsor)** — $5 (individual) or $50 (company, includes logo placement)
|
|
209
|
+
- 🔑 **[PRO license — $29 beta price](https://pentool.pro)** — early access + lifetime loyalty discount
|
|
190
210
|
- 💬 **Share** — tell a colleague, post a review, or mention Pentool in your writeups
|
|
191
211
|
|
|
192
212
|
> Building tools is lonely work. A star or a kind word genuinely matters. Thank you. 🙏
|
|
@@ -12,6 +12,25 @@
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## 💸 Support & Pricing
|
|
16
|
+
|
|
17
|
+
> Pentool is open-source and free to use. If it saves you time on a pentest — consider supporting development.
|
|
18
|
+
> Full pricing details and sponsorship options: **[pentool.pro](https://pentool.pro)**
|
|
19
|
+
|
|
20
|
+
| | | |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| ⭐ **Sponsor (individual)** | **$5** one-time or /month | [Sponsor on GitHub](https://github.com/sponsors/DrXOps) |
|
|
23
|
+
| 🏢 **Sponsor (company)** | **$50** one-time or /month | Logo in README + website · [pentool.pro/sponsor](https://pentool.pro/sponsor) |
|
|
24
|
+
| 🔑 **PRO License — Early Access** | **$29** *(beta price, until Dec 31 2026)* | After release: $99/year |
|
|
25
|
+
|
|
26
|
+
> **🎁 Beta loyalty discount:** Everyone who buys PRO for $29 gets a **lifetime 50% discount on annual renewal** (i.e. $49.50/year instead of $99).
|
|
27
|
+
> This discount applies **only to first buyers during the beta — until December 31, 2026**.
|
|
28
|
+
|
|
29
|
+
**PRO license is issued manually during beta.**
|
|
30
|
+
To purchase, contact: **[@sudores](https://t.me/sudores)** (Telegram) · **dev@pentool.pro**
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
15
34
|
**Pentool** is a terminal-based (TUI) security toolkit for penetration testers and security researchers.
|
|
16
35
|
It combines HTTP interception, vulnerability scanning, automated attacks, and data analysis — all inside your terminal.
|
|
17
36
|
Fast, transparent, and built for real-world testing.
|
|
@@ -140,7 +159,8 @@ Pentool is built and maintained by a solo developer in spare time.
|
|
|
140
159
|
If it saves you hours on a pentest — consider giving back. Every contribution directly funds new features, fixes, and faster releases.
|
|
141
160
|
|
|
142
161
|
- ⭐ **[Star on GitHub](https://github.com/DrXOps/pentool)** — free, takes 2 seconds, helps visibility enormously
|
|
143
|
-
-
|
|
162
|
+
- 💸 **[Sponsor the project](https://pentool.pro/sponsor)** — $5 (individual) or $50 (company, includes logo placement)
|
|
163
|
+
- 🔑 **[PRO license — $29 beta price](https://pentool.pro)** — early access + lifetime loyalty discount
|
|
144
164
|
- 💬 **Share** — tell a colleague, post a review, or mention Pentool in your writeups
|
|
145
165
|
|
|
146
166
|
> Building tools is lonely work. A star or a kind word genuinely matters. Thank you. 🙏
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
"""Pentool — professional web security testing toolkit with Textual TUI."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
# Single source of truth: the version pip actually installed (matches
|
|
7
|
+
# `pentool --version` / `pip show pentool`), including dev-build suffixes
|
|
8
|
+
# like "0.2.8.dev5" that CI stamps only into the package metadata, not
|
|
9
|
+
# into this file. Falls back to the literal below for editable/dev
|
|
10
|
+
# checkouts where the package isn't registered in the environment.
|
|
11
|
+
__version__ = _pkg_version("pentool")
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
__version__ = "0.2.9"
|
|
14
|
+
|
|
4
15
|
__author__ = "pentool"
|
|
5
16
|
|
|
6
17
|
|
|
@@ -50,6 +50,37 @@ def main() -> None:
|
|
|
50
50
|
except Exception:
|
|
51
51
|
compatible, warning = True, "" # never block startup over this check itself
|
|
52
52
|
|
|
53
|
+
if not compatible:
|
|
54
|
+
# Try to self-heal BEFORE deciding to exit. check_and_update_pro_
|
|
55
|
+
# package() is normally only reached from inside a running TUI
|
|
56
|
+
# session (on_mount's background worker) or from `pentool
|
|
57
|
+
# update`/`--upgrade` — but a bare `pentool` launch with an
|
|
58
|
+
# incompatible package used to bail out via SystemExit below
|
|
59
|
+
# before ever getting a chance to call it, so a broken install
|
|
60
|
+
# could never repair itself from a plain `pentool` invocation:
|
|
61
|
+
# the user was stuck needing `pentool license activate <key>`
|
|
62
|
+
# even though the exact same license key/machine_id could have
|
|
63
|
+
# fixed it automatically. Attempt one repair pass here first.
|
|
64
|
+
try:
|
|
65
|
+
import asyncio
|
|
66
|
+
|
|
67
|
+
from pentool.core.license import check_and_update_pro_package
|
|
68
|
+
result = asyncio.run(check_and_update_pro_package())
|
|
69
|
+
if result.updated:
|
|
70
|
+
print(
|
|
71
|
+
"[pentool] PRO package was out of sync — "
|
|
72
|
+
"re-downloaded successfully.",
|
|
73
|
+
file=sys.stderr,
|
|
74
|
+
)
|
|
75
|
+
compatible, warning = True, ""
|
|
76
|
+
elif not result.warning:
|
|
77
|
+
# No warning back means check_and_update_pro_package()
|
|
78
|
+
# itself now considers things fine (e.g. it just healed
|
|
79
|
+
# a previously-broken package with a matching build_id).
|
|
80
|
+
compatible, warning = is_pro_package_compatible()
|
|
81
|
+
except Exception:
|
|
82
|
+
pass # fall through to the original warning/exit below
|
|
83
|
+
|
|
53
84
|
if not compatible:
|
|
54
85
|
if unsafe_skip_pro_check:
|
|
55
86
|
print(
|
|
@@ -5,11 +5,18 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
|
|
7
7
|
from pentool.api.base_api import ExportableAPI
|
|
8
|
+
from pentool.api.intruder_repository import IntruderRepository
|
|
8
9
|
from pentool.modules.intruder import (
|
|
9
10
|
AttackType,
|
|
11
|
+
ChainedPayloadSource,
|
|
12
|
+
CharPayloadSource,
|
|
13
|
+
FilePayloadSource,
|
|
10
14
|
IntruderAttack,
|
|
11
15
|
IntruderConfig,
|
|
12
16
|
IntruderResult,
|
|
17
|
+
NumericPayloadSource,
|
|
18
|
+
ProcessedPayloads,
|
|
19
|
+
count_lines_with_progress,
|
|
13
20
|
count_markers,
|
|
14
21
|
extract_marker_defaults,
|
|
15
22
|
generate_char_payloads,
|
|
@@ -24,6 +31,12 @@ __all__ = [
|
|
|
24
31
|
"IntruderAttack",
|
|
25
32
|
"IntruderConfig",
|
|
26
33
|
"IntruderResult",
|
|
34
|
+
"ChainedPayloadSource",
|
|
35
|
+
"CharPayloadSource",
|
|
36
|
+
"FilePayloadSource",
|
|
37
|
+
"NumericPayloadSource",
|
|
38
|
+
"ProcessedPayloads",
|
|
39
|
+
"count_lines_with_progress",
|
|
27
40
|
"count_markers",
|
|
28
41
|
"extract_marker_defaults",
|
|
29
42
|
"generate_char_payloads",
|
|
@@ -33,13 +46,33 @@ __all__ = [
|
|
|
33
46
|
]
|
|
34
47
|
|
|
35
48
|
|
|
49
|
+
|
|
36
50
|
class IntruderAPI(ExportableAPI):
|
|
37
51
|
|
|
38
|
-
def __init__(self, db_path: str | None = None) -> None:
|
|
52
|
+
def __init__(self, db_path: str | None = None, http_client=None) -> None:
|
|
39
53
|
self._db_path = db_path
|
|
54
|
+
# Optional injected HTTPClient (DIP — see
|
|
55
|
+
# MYPLANS/ARCHITECTURE_REFACTOR_PLAN_2026-08-09.md section 2.2).
|
|
56
|
+
# IntruderAttack already accepted this as an optional constructor
|
|
57
|
+
# param (reusing one HTTPClient/connection pool across the whole
|
|
58
|
+
# attack — see the БАГ-D fix in modules/intruder.py) but IntruderAPI
|
|
59
|
+
# had no way to pass one in — it always let IntruderAttack create
|
|
60
|
+
# its own from scratch. Threading it through here just extends the
|
|
61
|
+
# existing `http_client=None` pattern one layer up, matching how
|
|
62
|
+
# ScannerAPI(db_path, http_client) already works. Does not apply to
|
|
63
|
+
# Turbo mode — TurboIntruderAttack manages its own aiohttp session
|
|
64
|
+
# pool internally by design (Keep-Alive tuning specific to Turbo),
|
|
65
|
+
# unrelated to this DI change.
|
|
66
|
+
self._http_client = http_client
|
|
40
67
|
self._attack: IntruderAttack | None = None
|
|
41
68
|
self._task: asyncio.Task | None = None
|
|
42
69
|
self._restored_results: list = []
|
|
70
|
+
# SQL for tab state / attack results lives in IntruderRepository
|
|
71
|
+
# (see MYPLANS/ARCHITECTURE_REFACTOR_PLAN_2026-08-09.md section 2.6)
|
|
72
|
+
# — mirrors ScannerTabRepository, the same extraction already done
|
|
73
|
+
# for Scanner. IntruderAPI keeps its existing public method names
|
|
74
|
+
# as a thin facade so no caller needs to change.
|
|
75
|
+
self._repo = IntruderRepository(db_path=db_path)
|
|
43
76
|
|
|
44
77
|
async def start_attack(
|
|
45
78
|
self,
|
|
@@ -48,27 +81,56 @@ class IntruderAPI(ExportableAPI):
|
|
|
48
81
|
on_progress=None,
|
|
49
82
|
turbo_mode: bool = False,
|
|
50
83
|
) -> str:
|
|
84
|
+
"""Run the attack to completion and return its attack_id.
|
|
85
|
+
|
|
86
|
+
NOTE: this method awaits the whole attack — it does NOT return as
|
|
87
|
+
soon as the attack starts. Before this fix it fired the attack via
|
|
88
|
+
`asyncio.create_task(...)` and returned immediately without
|
|
89
|
+
awaiting it, so a caller doing `await api.start_attack(...)` got
|
|
90
|
+
control back right after the attack merely started, not when it
|
|
91
|
+
finished — `get_results()` called right after would see a near-
|
|
92
|
+
empty/partial result set (see IntruderService.start_attack, which
|
|
93
|
+
relies on start_attack() having completed the attack before it
|
|
94
|
+
reads get_results() and emits IntruderFinished). The task handle is
|
|
95
|
+
still kept on self._task so stop()/pause()/resume() and callers
|
|
96
|
+
that want to cancel mid-attack (via self._task) keep working the
|
|
97
|
+
same way as before.
|
|
98
|
+
"""
|
|
51
99
|
if turbo_mode:
|
|
52
100
|
# Turbo Mode: connection pooling + Keep-Alive
|
|
53
101
|
from pentool.modules.intruder_turbo import TurboIntruderAttack
|
|
54
102
|
self._attack = TurboIntruderAttack(config)
|
|
55
103
|
else:
|
|
56
104
|
# Standard mode
|
|
57
|
-
self._attack = IntruderAttack(config, db_path=self._db_path)
|
|
105
|
+
self._attack = IntruderAttack(config, db_path=self._db_path, http_client=self._http_client)
|
|
58
106
|
|
|
59
107
|
_on_result = on_result if on_result else lambda r: None
|
|
60
108
|
_on_progress = on_progress if on_progress else lambda d, t: None
|
|
61
109
|
|
|
62
110
|
self._task = asyncio.create_task(self._attack.run(_on_result, _on_progress))
|
|
111
|
+
try:
|
|
112
|
+
await self._task
|
|
113
|
+
except asyncio.CancelledError:
|
|
114
|
+
pass
|
|
63
115
|
return self._attack.attack_id if hasattr(self._attack, 'attack_id') else "turbo"
|
|
64
116
|
|
|
65
117
|
async def pause(self) -> None:
|
|
66
|
-
if
|
|
118
|
+
"""Pause the running attack, if supported.
|
|
119
|
+
|
|
120
|
+
TurboIntruderAttack has no pause/resume — Turbo mode intentionally
|
|
121
|
+
runs to completion or stop() only (see modules/intruder_turbo.py).
|
|
122
|
+
Before this API method was actually reachable from IntruderScreen,
|
|
123
|
+
turbo_mode was silently never honored there (a pre-existing bug —
|
|
124
|
+
see MYPLANS/ARCHITECTURE_REFACTOR_PLAN_2026-08-09.md section 2.7),
|
|
125
|
+
so Pause/Resume during a real Turbo run was never exercised. Guard
|
|
126
|
+
with hasattr so enabling real Turbo mode doesn't crash Pause.
|
|
127
|
+
"""
|
|
128
|
+
if self._attack and hasattr(self._attack, "pause"):
|
|
67
129
|
await self._attack.pause()
|
|
68
130
|
|
|
69
131
|
async def resume(self) -> None:
|
|
70
|
-
"""Resume the attack after a pause."""
|
|
71
|
-
if self._attack:
|
|
132
|
+
"""Resume the attack after a pause, if supported (see pause() note)."""
|
|
133
|
+
if self._attack and hasattr(self._attack, "resume"):
|
|
72
134
|
await self._attack.resume()
|
|
73
135
|
|
|
74
136
|
async def stop(self) -> None:
|
|
@@ -97,11 +159,24 @@ class IntruderAPI(ExportableAPI):
|
|
|
97
159
|
return load_payloads_from_file(path)
|
|
98
160
|
|
|
99
161
|
async def generate_numeric(self, start: int, end: int, step: int = 1) -> list[str]:
|
|
162
|
+
"""Eager numeric generation — kept for backward compatibility.
|
|
163
|
+
|
|
164
|
+
Prefer constructing `NumericPayloadSource(start, end, step)` directly
|
|
165
|
+
for anything the UI will hold onto (see IntruderScreen._GenerateDialog),
|
|
166
|
+
since this materializes the whole range into a list immediately.
|
|
167
|
+
"""
|
|
100
168
|
return generate_numeric_payloads(start, end, step)
|
|
101
169
|
|
|
102
170
|
async def generate_chars(
|
|
103
171
|
self, charset: str, min_len: int, max_len: int
|
|
104
172
|
) -> list[str]:
|
|
173
|
+
"""Eager charset brute-force generation — kept for backward
|
|
174
|
+
compatibility. Prefer constructing
|
|
175
|
+
`CharPayloadSource(charset, min_len, max_len)` directly — this
|
|
176
|
+
materializes every combination into a list immediately, which for
|
|
177
|
+
even a modest charset/length is a combinatorial explosion (see
|
|
178
|
+
CharPayloadSource's docstring).
|
|
179
|
+
"""
|
|
105
180
|
return generate_char_payloads(charset, min_len, max_len)
|
|
106
181
|
|
|
107
182
|
def export_csv(self, path: str) -> None:
|
|
@@ -116,132 +191,52 @@ class IntruderAPI(ExportableAPI):
|
|
|
116
191
|
template: str,
|
|
117
192
|
attack_type: str,
|
|
118
193
|
payloads: list[list[str]],
|
|
194
|
+
tab_uid: str = "",
|
|
119
195
|
) -> None:
|
|
120
196
|
"""Save Intruder tab state (template, attack type, payloads) to DB."""
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
from pentool.core.database import get_db
|
|
126
|
-
|
|
127
|
-
async with get_db(self._db_path) as db:
|
|
128
|
-
# Delete old state for this tab
|
|
129
|
-
await db.execute("DELETE FROM intruder_state WHERE tab_name = ?", (tab_name,))
|
|
130
|
-
# Insert new state
|
|
131
|
-
await db.execute(
|
|
132
|
-
"""INSERT INTO intruder_state (tab_name, template, attack_type, payloads_json, updated_at)
|
|
133
|
-
VALUES (?, ?, ?, ?, datetime('now'))""",
|
|
134
|
-
(tab_name, template, attack_type, json.dumps(payloads)),
|
|
135
|
-
)
|
|
136
|
-
await db.commit()
|
|
137
|
-
|
|
138
|
-
async def load_state(self, tab_name: str) -> dict | None:
|
|
197
|
+
await self._repo.save_state(tab_name, template, attack_type, payloads, tab_uid=tab_uid)
|
|
198
|
+
|
|
199
|
+
async def load_state(self, tab_name: str, tab_uid: str = "") -> dict | None:
|
|
139
200
|
"""Load Intruder tab state from DB."""
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
201
|
+
return await self._repo.load_state(tab_name, tab_uid=tab_uid)
|
|
202
|
+
|
|
203
|
+
async def get_tabs(self) -> list[dict]:
|
|
204
|
+
"""List all Intruder tabs with saved state, most recently updated first."""
|
|
205
|
+
return await self._repo.get_tabs()
|
|
206
|
+
|
|
207
|
+
async def delete_tab(self, tab_uid: str) -> None:
|
|
208
|
+
"""Delete a tab's saved state and results by its stable tab_uid."""
|
|
209
|
+
await self._repo.delete_tab(tab_uid)
|
|
210
|
+
|
|
211
|
+
async def switch_db(self, db_path: str) -> None:
|
|
212
|
+
"""Point this API's repository at a different project DB file.
|
|
213
|
+
|
|
214
|
+
Lets IntruderScreen keep ONE persistent IntruderAPI/IntruderRepository
|
|
215
|
+
instance for the app's lifetime (mirrors ProxyService/HttpStorage)
|
|
216
|
+
instead of constructing a new one — and therefore a new SQLite
|
|
217
|
+
connection — on every single call. See BaseSqliteStorage.switch_db.
|
|
218
|
+
"""
|
|
219
|
+
self._db_path = db_path
|
|
220
|
+
await self._repo.switch_db(db_path)
|
|
221
|
+
|
|
222
|
+
async def close(self) -> None:
|
|
223
|
+
"""Close the underlying persistent SQLite connection, if open."""
|
|
224
|
+
await self._repo.close()
|
|
225
|
+
|
|
226
|
+
async def save_result(
|
|
227
|
+
self, result: IntruderResult, project_id: int | None = None, tab_uid: str = ""
|
|
228
|
+
) -> None:
|
|
161
229
|
"""Save a single intruder result to DB."""
|
|
162
|
-
|
|
163
|
-
return
|
|
164
|
-
import json
|
|
165
|
-
|
|
166
|
-
from pentool.core.database import get_db
|
|
167
|
-
|
|
168
|
-
async with get_db(self._db_path) as db:
|
|
169
|
-
await db.execute(
|
|
170
|
-
"""INSERT INTO intruder_results
|
|
171
|
-
(project_id, attack_id, request_number, payload_values, request_raw,
|
|
172
|
-
response_raw, response_status, response_length, response_time_ms, error, timestamp)
|
|
173
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
174
|
-
(
|
|
175
|
-
project_id,
|
|
176
|
-
result.attack_id,
|
|
177
|
-
result.request_number,
|
|
178
|
-
json.dumps(result.payload_values),
|
|
179
|
-
result.request_raw,
|
|
180
|
-
result.response_raw,
|
|
181
|
-
result.response_status,
|
|
182
|
-
result.response_length,
|
|
183
|
-
result.response_time_ms,
|
|
184
|
-
result.error,
|
|
185
|
-
result.timestamp.isoformat(),
|
|
186
|
-
),
|
|
187
|
-
)
|
|
188
|
-
await db.commit()
|
|
230
|
+
await self._repo.save_result(result, project_id, tab_uid=tab_uid)
|
|
189
231
|
|
|
190
232
|
async def get_results_from_db(
|
|
191
233
|
self,
|
|
192
234
|
attack_id: str | None = None,
|
|
193
235
|
limit: int = 1000,
|
|
236
|
+
tab_uid: str = "",
|
|
194
237
|
) -> list[IntruderResult]:
|
|
195
238
|
"""Load intruder results from DB."""
|
|
196
|
-
|
|
197
|
-
return []
|
|
198
|
-
import json
|
|
199
|
-
from datetime import datetime, timezone
|
|
200
|
-
|
|
201
|
-
from pentool.core.database import get_db
|
|
202
|
-
|
|
203
|
-
async with get_db(self._db_path) as db:
|
|
204
|
-
if attack_id:
|
|
205
|
-
cursor = await db.execute(
|
|
206
|
-
"""SELECT id, attack_id, request_number, payload_values, request_raw,
|
|
207
|
-
response_raw, response_status, response_length, response_time_ms, error, timestamp
|
|
208
|
-
FROM intruder_results WHERE attack_id = ?
|
|
209
|
-
ORDER BY request_number LIMIT ?""",
|
|
210
|
-
(attack_id, limit),
|
|
211
|
-
)
|
|
212
|
-
else:
|
|
213
|
-
cursor = await db.execute(
|
|
214
|
-
"""SELECT id, attack_id, request_number, payload_values, request_raw,
|
|
215
|
-
response_raw, response_status, response_length, response_time_ms, error, timestamp
|
|
216
|
-
FROM intruder_results
|
|
217
|
-
ORDER BY timestamp DESC LIMIT ?""",
|
|
218
|
-
(limit,),
|
|
219
|
-
)
|
|
220
|
-
rows = await cursor.fetchall()
|
|
221
|
-
results = []
|
|
222
|
-
for row in rows:
|
|
223
|
-
try:
|
|
224
|
-
ts = datetime.fromisoformat(row[10])
|
|
225
|
-
if ts.tzinfo is None:
|
|
226
|
-
ts = ts.replace(tzinfo=timezone.utc)
|
|
227
|
-
except Exception:
|
|
228
|
-
ts = datetime.now(timezone.utc)
|
|
229
|
-
results.append(
|
|
230
|
-
IntruderResult(
|
|
231
|
-
id=str(row[0]),
|
|
232
|
-
attack_id=row[1],
|
|
233
|
-
request_number=row[2],
|
|
234
|
-
payload_values=json.loads(row[3]) if row[3] else [],
|
|
235
|
-
request_raw=row[4],
|
|
236
|
-
response_raw=row[5],
|
|
237
|
-
response_status=row[6],
|
|
238
|
-
response_length=row[7],
|
|
239
|
-
response_time_ms=row[8],
|
|
240
|
-
error=row[9],
|
|
241
|
-
timestamp=ts,
|
|
242
|
-
)
|
|
243
|
-
)
|
|
244
|
-
return results
|
|
239
|
+
return await self._repo.get_results(attack_id, limit, tab_uid=tab_uid)
|
|
245
240
|
|
|
246
241
|
# ── Project persistence ────────────────────────────────────────────────────
|
|
247
242
|
|