pentool 0.2.8__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.8 → pentool-0.2.9}/PKG-INFO +22 -2
- {pentool-0.2.8 → pentool-0.2.9}/README.md +21 -1
- {pentool-0.2.8 → pentool-0.2.9}/pentool/__init__.py +12 -1
- {pentool-0.2.8 → 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.8 → pentool-0.2.9}/pentool/api/spider_api.py +70 -1
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/config.py +2 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/database.py +89 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/events.py +36 -1
- {pentool-0.2.8 → 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.8 → pentool-0.2.9}/pentool/modules/proxy.py +28 -15
- {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/spider.py +88 -7
- {pentool-0.2.8 → pentool-0.2.9}/pentool/services/proxy_service.py +41 -1
- {pentool-0.2.8 → pentool-0.2.9}/pentool/services/repeater_service.py +4 -1
- {pentool-0.2.8 → 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.8 → pentool-0.2.9}/pentool/storage/http_storage.py +9 -20
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/app.py +61 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/project_manager.py +10 -2
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/dashboard/screen.py +31 -9
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/intruder/screen.py +779 -105
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/intruder/screen.tcss +19 -1
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/proxy/screen.py +126 -30
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/repeater/screen.py +127 -10
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/repeater/screen.tcss +7 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/settings/screen.py +1 -51
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/settings/screen.tcss +3 -11
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/spider/screen.py +4 -1
- {pentool-0.2.8 → 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.8 → pentool-0.2.9}/pentool.egg-info/PKG-INFO +22 -2
- {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/SOURCES.txt +11 -0
- {pentool-0.2.8 → pentool-0.2.9}/pyproject.toml +1 -1
- pentool-0.2.8/pentool/modules/intruder.py +0 -460
- {pentool-0.2.8 → pentool-0.2.9}/LICENSE +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/__main__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/api/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/api/base_api.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/api/comparer_api.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/api/decoder_api.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/api/proxy_api.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/api/repeater_api.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/api/sequencer_api.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/api/target_api.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/cli/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/cli/main.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/cli/project.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/cli/proxy.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/cli/scan.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/crash_reporter.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/event_bus.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/features.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/logging.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/plugin_manager.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/project.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/storage_interface.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/updater.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/core/utils.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/comparer.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/decoder.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/match_replace.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/repeater.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/sequencer.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/target.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/websocket_handler.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/plugins/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/plugins/builtin/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/plugins/example_plugin.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/plugins/example_plugin.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/services/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/services/base_service.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/services/intruder_service.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/storage/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/storage/large_body_handler.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/storage/lru_cache.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/t.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/constants.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/cert_dialog.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/cert_dialog.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/comment_dialog.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/comment_dialog.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/file_selector.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/file_selector.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/load_from_proxy.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/load_from_proxy.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/match_replace_dialog.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/match_replace_dialog.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/project_dialog.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/project_dialog.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/scope_dialog.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/scope_dialog.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/messages.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/mixins/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/mixins/app_mixin.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/mixins/dialog_cancel.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/mixins/request_context_menu.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/base.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/comparer/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/comparer/screen.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/comparer/screen.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/dashboard/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/dashboard/live_dashboard.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/dashboard/screen.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/decoder/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/decoder/screen.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/decoder/screen.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/extensions/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/extensions/screen.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/extensions/screen.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/intruder/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/proxy/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/proxy/screen.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/repeater/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/scanner_unavailable.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/sequencer/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/sequencer/screen.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/sequencer/screen.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/settings/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/settings/hotkeys.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/settings/hotkeys.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/spider/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/spider/screen.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/target/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/target/screen.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/terminal/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/terminal/screen.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/terminal/screen.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/activity_indicator.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/activity_indicator.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/context_menu.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/context_menu.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/filter_bar.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/filter_bar.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/inspector_panel.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/inspector_panel.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/menu.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/menu_bar.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/module_tabs.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/module_tabs.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/nice_checkbox.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/payload_drop_zone.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/payload_drop_zone.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/request_editor.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/request_editor.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/resize_handle.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/resize_handle.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/search_bar.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/search_bar.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/statusbar.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/statusbar.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/toolbar_button.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/toolbar_button.tcss +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/__init__.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/cert.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/coder.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/copy_as.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/diff.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/http_client.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/parser.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/terminal_check.py +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/dependency_links.txt +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/entry_points.txt +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/requires.txt +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/top_level.txt +0 -0
- {pentool-0.2.8 → pentool-0.2.9}/setup.cfg +0 -0
- {pentool-0.2.8 → 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
|
|
|
@@ -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
|
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""IntruderRepository — SQL for Intruder tab state and attack result persistence.
|
|
2
|
+
|
|
3
|
+
Extracted from `IntruderAPI` (see
|
|
4
|
+
MYPLANS/ARCHITECTURE_REFACTOR_PLAN_2026-08-09.md, section 2.6). Pure
|
|
5
|
+
data-access: no attack orchestration knowledge, only the `intruder_state`/
|
|
6
|
+
`intruder_results` table CRUD that used to live directly on `IntruderAPI`.
|
|
7
|
+
Behavior (upsert-by-delete-then-insert for state, column selection/ordering
|
|
8
|
+
for results) is unchanged from the original
|
|
9
|
+
`IntruderAPI.save_state`/`load_state`/`save_result`/`get_results_from_db`.
|
|
10
|
+
|
|
11
|
+
Mirrors the pattern already used for Scanner
|
|
12
|
+
(`pentool.api.scanner_tab_repository.ScannerTabRepository`, pro/) — same
|
|
13
|
+
extraction, applied to the one remaining API class that still wrote raw
|
|
14
|
+
SQL directly (`ProxyAPI`/`RepeaterAPI` already delegate all SQL to
|
|
15
|
+
`HttpStorage`/`Repeater` in modules/).
|
|
16
|
+
|
|
17
|
+
Intruder became multi-tab (see IntruderScreen._TabState) — `tab_uid` is the
|
|
18
|
+
new stable per-tab identity (mirrors Scanner's tab_uid), persisted/restored
|
|
19
|
+
independently of the cosmetic, user-renameable `tab_name`. All `tab_uid`
|
|
20
|
+
parameters below default to "" and fall back to the original tab_name-keyed
|
|
21
|
+
behavior when omitted, so existing single-tab callers/tests are unaffected.
|
|
22
|
+
|
|
23
|
+
Connection lifecycle: inherits `BaseSqliteStorage` (see
|
|
24
|
+
pentool/storage/base_sqlite_storage.py) instead of opening/closing a fresh
|
|
25
|
+
`aiosqlite` connection via `core.database.get_db()` on every call. That old
|
|
26
|
+
per-call open/close pattern was the direct cause of a real crash — a fast
|
|
27
|
+
Intruder attack calls `save_result()` per HTTP response (thousands/sec),
|
|
28
|
+
each one opening+closing its own connection to the same file HttpStorage
|
|
29
|
+
already holds open. Every public method here now calls
|
|
30
|
+
`await self.ensure_open()` first, which connects once (lazily, on first
|
|
31
|
+
use) and reuses that connection for the object's lifetime — the "safe
|
|
32
|
+
no-op when db_path is falsy" contract (see TestNoDbPath in
|
|
33
|
+
tests/unit/api/test_intruder_repository.py) is preserved because
|
|
34
|
+
`ensure_open()` returns False when `self._db_path` is empty.
|
|
35
|
+
"""
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import json
|
|
39
|
+
from datetime import datetime, timezone
|
|
40
|
+
|
|
41
|
+
from pentool.modules.intruder import IntruderResult
|
|
42
|
+
from pentool.storage.base_sqlite_storage import BaseSqliteStorage
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IntruderRepository(BaseSqliteStorage):
|
|
46
|
+
"""CRUD for `intruder_state` and `intruder_results`, scoped to one project DB."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, db_path: str | None = None) -> None:
|
|
49
|
+
super().__init__(db_path=db_path)
|
|
50
|
+
|
|
51
|
+
async def save_state(
|
|
52
|
+
self,
|
|
53
|
+
tab_name: str,
|
|
54
|
+
template: str,
|
|
55
|
+
attack_type: str,
|
|
56
|
+
payloads: list[list[str]],
|
|
57
|
+
tab_uid: str = "",
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Save Intruder tab state (template, attack type, payloads) to DB.
|
|
60
|
+
|
|
61
|
+
When `tab_uid` is given, upserts by that stable identity (matches
|
|
62
|
+
ScannerTabRepository.save_tab's rationale — tab_name alone can't
|
|
63
|
+
distinguish two same-named tabs across restarts). When omitted,
|
|
64
|
+
falls back to the original delete-by-tab_name-then-insert behavior
|
|
65
|
+
for backward compatibility with single-tab callers.
|
|
66
|
+
"""
|
|
67
|
+
if not await self.ensure_open():
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
db = self._db
|
|
71
|
+
if tab_uid:
|
|
72
|
+
cursor = await db.execute(
|
|
73
|
+
"SELECT id FROM intruder_state WHERE tab_uid = ?", (tab_uid,)
|
|
74
|
+
)
|
|
75
|
+
row = await cursor.fetchone()
|
|
76
|
+
if row:
|
|
77
|
+
await db.execute(
|
|
78
|
+
"""UPDATE intruder_state SET tab_name = ?, template = ?,
|
|
79
|
+
attack_type = ?, payloads_json = ?, updated_at = datetime('now')
|
|
80
|
+
WHERE tab_uid = ?""",
|
|
81
|
+
(tab_name, template, attack_type, json.dumps(payloads), tab_uid),
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
await db.execute(
|
|
85
|
+
"""INSERT INTO intruder_state
|
|
86
|
+
(tab_uid, tab_name, template, attack_type, payloads_json, updated_at)
|
|
87
|
+
VALUES (?, ?, ?, ?, ?, datetime('now'))""",
|
|
88
|
+
(tab_uid, tab_name, template, attack_type, json.dumps(payloads)),
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
# Legacy path — delete-then-insert by tab_name. Restricted to
|
|
92
|
+
# rows with no tab_uid so it never clobbers a multi-tab
|
|
93
|
+
# caller's uid-keyed row that happens to share the same name.
|
|
94
|
+
await db.execute(
|
|
95
|
+
"DELETE FROM intruder_state WHERE tab_name = ? AND (tab_uid IS NULL OR tab_uid = '')",
|
|
96
|
+
(tab_name,),
|
|
97
|
+
)
|
|
98
|
+
await db.execute(
|
|
99
|
+
"""INSERT INTO intruder_state (tab_name, template, attack_type, payloads_json, updated_at)
|
|
100
|
+
VALUES (?, ?, ?, ?, datetime('now'))""",
|
|
101
|
+
(tab_name, template, attack_type, json.dumps(payloads)),
|
|
102
|
+
)
|
|
103
|
+
await db.commit()
|
|
104
|
+
|
|
105
|
+
async def load_state(self, tab_name: str, tab_uid: str = "") -> dict | None:
|
|
106
|
+
"""Load Intruder tab state from DB — by tab_uid if given, else tab_name."""
|
|
107
|
+
if not await self.ensure_open():
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
db = self._db
|
|
111
|
+
if tab_uid:
|
|
112
|
+
cursor = await db.execute(
|
|
113
|
+
"SELECT template, attack_type, payloads_json FROM intruder_state WHERE tab_uid = ?",
|
|
114
|
+
(tab_uid,),
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
cursor = await db.execute(
|
|
118
|
+
"SELECT template, attack_type, payloads_json FROM intruder_state WHERE tab_name = ?",
|
|
119
|
+
(tab_name,),
|
|
120
|
+
)
|
|
121
|
+
row = await cursor.fetchone()
|
|
122
|
+
if not row:
|
|
123
|
+
return None
|
|
124
|
+
return {
|
|
125
|
+
"template": row[0],
|
|
126
|
+
"attack_type": row[1],
|
|
127
|
+
"payloads": json.loads(row[2]),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async def get_tabs(self) -> list[dict]:
|
|
131
|
+
"""List all Intruder tabs that have saved state (tab_uid set), most
|
|
132
|
+
recently updated first — used to restore tabs on project load."""
|
|
133
|
+
if not await self.ensure_open():
|
|
134
|
+
return []
|
|
135
|
+
|
|
136
|
+
cursor = await self._db.execute(
|
|
137
|
+
"SELECT tab_uid, tab_name FROM intruder_state "
|
|
138
|
+
"WHERE tab_uid IS NOT NULL AND tab_uid != '' "
|
|
139
|
+
"ORDER BY updated_at DESC"
|
|
140
|
+
)
|
|
141
|
+
rows = await cursor.fetchall()
|
|
142
|
+
return [{"tab_uid": row[0], "tab_name": row[1]} for row in rows]
|
|
143
|
+
|
|
144
|
+
async def delete_tab(self, tab_uid: str) -> None:
|
|
145
|
+
"""Delete a tab's saved state and results by its stable tab_uid."""
|
|
146
|
+
if not tab_uid or not await self.ensure_open():
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
db = self._db
|
|
150
|
+
await db.execute("DELETE FROM intruder_state WHERE tab_uid = ?", (tab_uid,))
|
|
151
|
+
await db.execute("DELETE FROM intruder_results WHERE tab_uid = ?", (tab_uid,))
|
|
152
|
+
await db.commit()
|
|
153
|
+
|
|
154
|
+
async def save_result(
|
|
155
|
+
self,
|
|
156
|
+
result: IntruderResult,
|
|
157
|
+
project_id: int | None = None,
|
|
158
|
+
tab_uid: str = "",
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Save a single intruder result to DB."""
|
|
161
|
+
if not await self.ensure_open():
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
await self._db.execute(
|
|
165
|
+
"""INSERT INTO intruder_results
|
|
166
|
+
(project_id, attack_id, request_number, payload_values, request_raw,
|
|
167
|
+
response_raw, response_status, response_length, response_time_ms, error, timestamp, tab_uid)
|
|
168
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
169
|
+
(
|
|
170
|
+
project_id,
|
|
171
|
+
result.attack_id,
|
|
172
|
+
result.request_number,
|
|
173
|
+
json.dumps(result.payload_values),
|
|
174
|
+
result.request_raw,
|
|
175
|
+
result.response_raw,
|
|
176
|
+
result.response_status,
|
|
177
|
+
result.response_length,
|
|
178
|
+
result.response_time_ms,
|
|
179
|
+
result.error,
|
|
180
|
+
result.timestamp.isoformat(),
|
|
181
|
+
tab_uid,
|
|
182
|
+
),
|
|
183
|
+
)
|
|
184
|
+
await self._db.commit()
|
|
185
|
+
|
|
186
|
+
async def get_results(
|
|
187
|
+
self,
|
|
188
|
+
attack_id: str | None = None,
|
|
189
|
+
limit: int = 1000,
|
|
190
|
+
tab_uid: str = "",
|
|
191
|
+
) -> list[IntruderResult]:
|
|
192
|
+
"""Load intruder results from DB.
|
|
193
|
+
|
|
194
|
+
Filters by attack_id if given, else by tab_uid if given (all results
|
|
195
|
+
across every attack ever run in that tab), else returns the most
|
|
196
|
+
recent `limit` results overall (legacy single-tab behavior).
|
|
197
|
+
"""
|
|
198
|
+
if not await self.ensure_open():
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
db = self._db
|
|
202
|
+
if attack_id:
|
|
203
|
+
cursor = await db.execute(
|
|
204
|
+
"""SELECT id, attack_id, request_number, payload_values, request_raw,
|
|
205
|
+
response_raw, response_status, response_length, response_time_ms, error, timestamp
|
|
206
|
+
FROM intruder_results WHERE attack_id = ?
|
|
207
|
+
ORDER BY request_number LIMIT ?""",
|
|
208
|
+
(attack_id, limit),
|
|
209
|
+
)
|
|
210
|
+
elif tab_uid:
|
|
211
|
+
cursor = await db.execute(
|
|
212
|
+
"""SELECT id, attack_id, request_number, payload_values, request_raw,
|
|
213
|
+
response_raw, response_status, response_length, response_time_ms, error, timestamp
|
|
214
|
+
FROM intruder_results WHERE tab_uid = ?
|
|
215
|
+
ORDER BY timestamp DESC LIMIT ?""",
|
|
216
|
+
(tab_uid, limit),
|
|
217
|
+
)
|
|
218
|
+
else:
|
|
219
|
+
cursor = await db.execute(
|
|
220
|
+
"""SELECT id, attack_id, request_number, payload_values, request_raw,
|
|
221
|
+
response_raw, response_status, response_length, response_time_ms, error, timestamp
|
|
222
|
+
FROM intruder_results
|
|
223
|
+
ORDER BY timestamp DESC LIMIT ?""",
|
|
224
|
+
(limit,),
|
|
225
|
+
)
|
|
226
|
+
rows = await cursor.fetchall()
|
|
227
|
+
results = []
|
|
228
|
+
for row in rows:
|
|
229
|
+
try:
|
|
230
|
+
ts = datetime.fromisoformat(row[10])
|
|
231
|
+
if ts.tzinfo is None:
|
|
232
|
+
ts = ts.replace(tzinfo=timezone.utc)
|
|
233
|
+
except Exception:
|
|
234
|
+
ts = datetime.now(timezone.utc)
|
|
235
|
+
results.append(
|
|
236
|
+
IntruderResult(
|
|
237
|
+
id=str(row[0]),
|
|
238
|
+
attack_id=row[1],
|
|
239
|
+
request_number=row[2],
|
|
240
|
+
payload_values=json.loads(row[3]) if row[3] else [],
|
|
241
|
+
request_raw=row[4],
|
|
242
|
+
response_raw=row[5],
|
|
243
|
+
response_status=row[6],
|
|
244
|
+
response_length=row[7],
|
|
245
|
+
response_time_ms=row[8],
|
|
246
|
+
error=row[9],
|
|
247
|
+
timestamp=ts,
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
return results
|