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.
Files changed (181) hide show
  1. {pentool-0.2.8 → pentool-0.2.9}/PKG-INFO +22 -2
  2. {pentool-0.2.8 → pentool-0.2.9}/README.md +21 -1
  3. {pentool-0.2.8 → pentool-0.2.9}/pentool/__init__.py +12 -1
  4. {pentool-0.2.8 → pentool-0.2.9}/pentool/api/intruder_api.py +115 -120
  5. pentool-0.2.9/pentool/api/intruder_repository.py +250 -0
  6. {pentool-0.2.8 → pentool-0.2.9}/pentool/api/spider_api.py +70 -1
  7. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/config.py +2 -0
  8. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/database.py +89 -0
  9. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/events.py +36 -1
  10. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/license.py +24 -1
  11. pentool-0.2.9/pentool/core/notification_sound.py +123 -0
  12. pentool-0.2.9/pentool/modules/intruder.py +920 -0
  13. {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/proxy.py +28 -15
  14. {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/spider.py +88 -7
  15. {pentool-0.2.8 → pentool-0.2.9}/pentool/services/proxy_service.py +41 -1
  16. {pentool-0.2.8 → pentool-0.2.9}/pentool/services/repeater_service.py +4 -1
  17. {pentool-0.2.8 → pentool-0.2.9}/pentool/services/scan_service.py +45 -19
  18. pentool-0.2.9/pentool/storage/base_sqlite_storage.py +87 -0
  19. {pentool-0.2.8 → pentool-0.2.9}/pentool/storage/http_storage.py +9 -20
  20. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/app.py +61 -0
  21. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/project_manager.py +10 -2
  22. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/dashboard/screen.py +31 -9
  23. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/intruder/screen.py +779 -105
  24. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/intruder/screen.tcss +19 -1
  25. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/proxy/screen.py +126 -30
  26. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/repeater/screen.py +127 -10
  27. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/repeater/screen.tcss +7 -0
  28. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/settings/screen.py +1 -51
  29. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/settings/screen.tcss +3 -11
  30. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/spider/screen.py +4 -1
  31. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/target/screen.py +2 -1
  32. pentool-0.2.9/pentool/tui/widgets/diff_panel.py +70 -0
  33. pentool-0.2.9/pentool/tui/widgets/diff_panel.tcss +20 -0
  34. pentool-0.2.9/pentool/tui/widgets/notification_center.py +122 -0
  35. pentool-0.2.9/pentool/tui/widgets/notification_center.tcss +54 -0
  36. pentool-0.2.9/pentool/tui/widgets/option_cycler.py +74 -0
  37. pentool-0.2.9/pentool/tui/widgets/option_cycler.tcss +11 -0
  38. pentool-0.2.9/pentool/utils/auth_headers.py +31 -0
  39. pentool-0.2.9/pentool/utils/scope.py +61 -0
  40. {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/PKG-INFO +22 -2
  41. {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/SOURCES.txt +11 -0
  42. {pentool-0.2.8 → pentool-0.2.9}/pyproject.toml +1 -1
  43. pentool-0.2.8/pentool/modules/intruder.py +0 -460
  44. {pentool-0.2.8 → pentool-0.2.9}/LICENSE +0 -0
  45. {pentool-0.2.8 → pentool-0.2.9}/pentool/__main__.py +0 -0
  46. {pentool-0.2.8 → pentool-0.2.9}/pentool/api/__init__.py +0 -0
  47. {pentool-0.2.8 → pentool-0.2.9}/pentool/api/base_api.py +0 -0
  48. {pentool-0.2.8 → pentool-0.2.9}/pentool/api/comparer_api.py +0 -0
  49. {pentool-0.2.8 → pentool-0.2.9}/pentool/api/decoder_api.py +0 -0
  50. {pentool-0.2.8 → pentool-0.2.9}/pentool/api/proxy_api.py +0 -0
  51. {pentool-0.2.8 → pentool-0.2.9}/pentool/api/repeater_api.py +0 -0
  52. {pentool-0.2.8 → pentool-0.2.9}/pentool/api/sequencer_api.py +0 -0
  53. {pentool-0.2.8 → pentool-0.2.9}/pentool/api/target_api.py +0 -0
  54. {pentool-0.2.8 → pentool-0.2.9}/pentool/cli/__init__.py +0 -0
  55. {pentool-0.2.8 → pentool-0.2.9}/pentool/cli/main.py +0 -0
  56. {pentool-0.2.8 → pentool-0.2.9}/pentool/cli/project.py +0 -0
  57. {pentool-0.2.8 → pentool-0.2.9}/pentool/cli/proxy.py +0 -0
  58. {pentool-0.2.8 → pentool-0.2.9}/pentool/cli/scan.py +0 -0
  59. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/__init__.py +0 -0
  60. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/crash_reporter.py +0 -0
  61. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/event_bus.py +0 -0
  62. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/features.py +0 -0
  63. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/logging.py +0 -0
  64. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/plugin_manager.py +0 -0
  65. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/project.py +0 -0
  66. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/storage_interface.py +0 -0
  67. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/updater.py +0 -0
  68. {pentool-0.2.8 → pentool-0.2.9}/pentool/core/utils.py +0 -0
  69. {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/__init__.py +0 -0
  70. {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/comparer.py +0 -0
  71. {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/decoder.py +0 -0
  72. {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/match_replace.py +0 -0
  73. {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/repeater.py +0 -0
  74. {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/sequencer.py +0 -0
  75. {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/target.py +0 -0
  76. {pentool-0.2.8 → pentool-0.2.9}/pentool/modules/websocket_handler.py +0 -0
  77. {pentool-0.2.8 → pentool-0.2.9}/pentool/plugins/__init__.py +0 -0
  78. {pentool-0.2.8 → pentool-0.2.9}/pentool/plugins/builtin/__init__.py +0 -0
  79. {pentool-0.2.8 → pentool-0.2.9}/pentool/plugins/example_plugin.py +0 -0
  80. {pentool-0.2.8 → pentool-0.2.9}/pentool/plugins/example_plugin.tcss +0 -0
  81. {pentool-0.2.8 → pentool-0.2.9}/pentool/services/__init__.py +0 -0
  82. {pentool-0.2.8 → pentool-0.2.9}/pentool/services/base_service.py +0 -0
  83. {pentool-0.2.8 → pentool-0.2.9}/pentool/services/intruder_service.py +0 -0
  84. {pentool-0.2.8 → pentool-0.2.9}/pentool/storage/__init__.py +0 -0
  85. {pentool-0.2.8 → pentool-0.2.9}/pentool/storage/large_body_handler.py +0 -0
  86. {pentool-0.2.8 → pentool-0.2.9}/pentool/storage/lru_cache.py +0 -0
  87. {pentool-0.2.8 → pentool-0.2.9}/pentool/t.py +0 -0
  88. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/__init__.py +0 -0
  89. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/constants.py +0 -0
  90. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/__init__.py +0 -0
  91. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/cert_dialog.py +0 -0
  92. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/cert_dialog.tcss +0 -0
  93. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/comment_dialog.py +0 -0
  94. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/comment_dialog.tcss +0 -0
  95. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/file_selector.py +0 -0
  96. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/file_selector.tcss +0 -0
  97. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/load_from_proxy.py +0 -0
  98. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/load_from_proxy.tcss +0 -0
  99. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/match_replace_dialog.py +0 -0
  100. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/match_replace_dialog.tcss +0 -0
  101. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/project_dialog.py +0 -0
  102. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/project_dialog.tcss +0 -0
  103. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/scope_dialog.py +0 -0
  104. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/dialogs/scope_dialog.tcss +0 -0
  105. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/messages.py +0 -0
  106. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/mixins/__init__.py +0 -0
  107. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/mixins/app_mixin.py +0 -0
  108. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/mixins/dialog_cancel.py +0 -0
  109. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/mixins/request_context_menu.py +0 -0
  110. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/__init__.py +0 -0
  111. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/base.py +0 -0
  112. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/comparer/__init__.py +0 -0
  113. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/comparer/screen.py +0 -0
  114. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/comparer/screen.tcss +0 -0
  115. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/dashboard/__init__.py +0 -0
  116. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/dashboard/live_dashboard.py +0 -0
  117. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/dashboard/screen.tcss +0 -0
  118. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/decoder/__init__.py +0 -0
  119. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/decoder/screen.py +0 -0
  120. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/decoder/screen.tcss +0 -0
  121. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/extensions/__init__.py +0 -0
  122. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/extensions/screen.py +0 -0
  123. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/extensions/screen.tcss +0 -0
  124. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/intruder/__init__.py +0 -0
  125. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/proxy/__init__.py +0 -0
  126. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/proxy/screen.tcss +0 -0
  127. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/repeater/__init__.py +0 -0
  128. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/scanner_unavailable.py +0 -0
  129. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/sequencer/__init__.py +0 -0
  130. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/sequencer/screen.py +0 -0
  131. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/sequencer/screen.tcss +0 -0
  132. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/settings/__init__.py +0 -0
  133. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/settings/hotkeys.py +0 -0
  134. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/settings/hotkeys.tcss +0 -0
  135. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/spider/__init__.py +0 -0
  136. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/spider/screen.tcss +0 -0
  137. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/target/__init__.py +0 -0
  138. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/target/screen.tcss +0 -0
  139. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/terminal/__init__.py +0 -0
  140. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/terminal/screen.py +0 -0
  141. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/screens/terminal/screen.tcss +0 -0
  142. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/__init__.py +0 -0
  143. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/activity_indicator.py +0 -0
  144. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/activity_indicator.tcss +0 -0
  145. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/context_menu.py +0 -0
  146. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/context_menu.tcss +0 -0
  147. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/filter_bar.py +0 -0
  148. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/filter_bar.tcss +0 -0
  149. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/inspector_panel.py +0 -0
  150. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/inspector_panel.tcss +0 -0
  151. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/menu.tcss +0 -0
  152. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/menu_bar.tcss +0 -0
  153. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/module_tabs.py +0 -0
  154. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/module_tabs.tcss +0 -0
  155. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/nice_checkbox.py +0 -0
  156. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/payload_drop_zone.py +0 -0
  157. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/payload_drop_zone.tcss +0 -0
  158. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/request_editor.py +0 -0
  159. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/request_editor.tcss +0 -0
  160. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/resize_handle.py +0 -0
  161. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/resize_handle.tcss +0 -0
  162. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/search_bar.py +0 -0
  163. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/search_bar.tcss +0 -0
  164. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/statusbar.py +0 -0
  165. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/statusbar.tcss +0 -0
  166. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/toolbar_button.py +0 -0
  167. {pentool-0.2.8 → pentool-0.2.9}/pentool/tui/widgets/toolbar_button.tcss +0 -0
  168. {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/__init__.py +0 -0
  169. {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/cert.py +0 -0
  170. {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/coder.py +0 -0
  171. {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/copy_as.py +0 -0
  172. {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/diff.py +0 -0
  173. {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/http_client.py +0 -0
  174. {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/parser.py +0 -0
  175. {pentool-0.2.8 → pentool-0.2.9}/pentool/utils/terminal_check.py +0 -0
  176. {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/dependency_links.txt +0 -0
  177. {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/entry_points.txt +0 -0
  178. {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/requires.txt +0 -0
  179. {pentool-0.2.8 → pentool-0.2.9}/pentool.egg-info/top_level.txt +0 -0
  180. {pentool-0.2.8 → pentool-0.2.9}/setup.cfg +0 -0
  181. {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.8
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
- - 🔑 **PRO license** — get early access and support development → **[@sudores](https://t.me/sudores)**
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
- - 🔑 **PRO license** — get early access and support development → **[@sudores](https://t.me/sudores)**
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
- __version__ = "0.2.8"
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 self._attack:
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
- if not self._db_path:
122
- return
123
- import json
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
- if not self._db_path:
141
- return None
142
- import json
143
-
144
- from pentool.core.database import get_db
145
-
146
- async with get_db(self._db_path) as db:
147
- cursor = await db.execute(
148
- "SELECT template, attack_type, payloads_json FROM intruder_state WHERE tab_name = ?",
149
- (tab_name,),
150
- )
151
- row = await cursor.fetchone()
152
- if not row:
153
- return None
154
- return {
155
- "template": row[0],
156
- "attack_type": row[1],
157
- "payloads": json.loads(row[2]),
158
- }
159
-
160
- async def save_result(self, result: IntruderResult, project_id: int | None = None) -> None:
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
- if not self._db_path:
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
- if not self._db_path:
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