pentool 1.0.0__py3-none-any.whl

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 (169) hide show
  1. pentool/__init__.py +4 -0
  2. pentool/__main__.py +17 -0
  3. pentool/api/__init__.py +45 -0
  4. pentool/api/base_api.py +52 -0
  5. pentool/api/comparer_api.py +23 -0
  6. pentool/api/decoder_api.py +28 -0
  7. pentool/api/intruder_api.py +209 -0
  8. pentool/api/proxy_api.py +319 -0
  9. pentool/api/repeater_api.py +118 -0
  10. pentool/api/scanner_api.py +336 -0
  11. pentool/api/sequencer_api.py +21 -0
  12. pentool/api/spider_api.py +126 -0
  13. pentool/api/target_api.py +121 -0
  14. pentool/cli/__init__.py +1 -0
  15. pentool/cli/main.py +100 -0
  16. pentool/cli/project.py +90 -0
  17. pentool/cli/proxy.py +185 -0
  18. pentool/cli/scan.py +118 -0
  19. pentool/core/__init__.py +1 -0
  20. pentool/core/config.py +184 -0
  21. pentool/core/crash_reporter.py +75 -0
  22. pentool/core/database.py +139 -0
  23. pentool/core/event_bus.py +232 -0
  24. pentool/core/events.py +164 -0
  25. pentool/core/features.py +410 -0
  26. pentool/core/license.py +294 -0
  27. pentool/core/logging.py +59 -0
  28. pentool/core/plugin_manager.py +332 -0
  29. pentool/core/project.py +37 -0
  30. pentool/core/storage_interface.py +311 -0
  31. pentool/core/updater.py +96 -0
  32. pentool/core/utils.py +61 -0
  33. pentool/modules/__init__.py +1 -0
  34. pentool/modules/comparer.py +177 -0
  35. pentool/modules/decoder.py +281 -0
  36. pentool/modules/intruder.py +432 -0
  37. pentool/modules/intruder_turbo.py +190 -0
  38. pentool/modules/match_replace.py +146 -0
  39. pentool/modules/proxy.py +803 -0
  40. pentool/modules/repeater.py +237 -0
  41. pentool/modules/scanner/__init__.py +7 -0
  42. pentool/modules/scanner/ai_analyzer.py +230 -0
  43. pentool/modules/scanner/base.py +173 -0
  44. pentool/modules/scanner/baseline.py +101 -0
  45. pentool/modules/scanner/checks/__init__.py +47 -0
  46. pentool/modules/scanner/checks/broken_auth.py +231 -0
  47. pentool/modules/scanner/checks/cors.py +209 -0
  48. pentool/modules/scanner/checks/dom_xss.py +114 -0
  49. pentool/modules/scanner/checks/graphql.py +110 -0
  50. pentool/modules/scanner/checks/header_injection.py +262 -0
  51. pentool/modules/scanner/checks/headers.py +75 -0
  52. pentool/modules/scanner/checks/helpers.py +358 -0
  53. pentool/modules/scanner/checks/info_leak.py +68 -0
  54. pentool/modules/scanner/checks/jwt_none.py +238 -0
  55. pentool/modules/scanner/checks/lfi.py +186 -0
  56. pentool/modules/scanner/checks/nosql_injection.py +92 -0
  57. pentool/modules/scanner/checks/oauth.py +123 -0
  58. pentool/modules/scanner/checks/open_redirect.py +168 -0
  59. pentool/modules/scanner/checks/path_traversal.py +229 -0
  60. pentool/modules/scanner/checks/prototype_pollution.py +88 -0
  61. pentool/modules/scanner/checks/rce.py +338 -0
  62. pentool/modules/scanner/checks/sensitive_data.py +74 -0
  63. pentool/modules/scanner/checks/sqli.py +546 -0
  64. pentool/modules/scanner/checks/ssrf.py +273 -0
  65. pentool/modules/scanner/checks/ssti.py +353 -0
  66. pentool/modules/scanner/checks/xss.py +622 -0
  67. pentool/modules/scanner/checks/xxe.py +240 -0
  68. pentool/modules/scanner/engine.py +407 -0
  69. pentool/modules/scanner/fingerprint.py +169 -0
  70. pentool/modules/scanner/helpers.py +94 -0
  71. pentool/modules/scanner/mutator.py +378 -0
  72. pentool/modules/scanner/oob.py +104 -0
  73. pentool/modules/scanner/passive.py +114 -0
  74. pentool/modules/scanner/payloads.py +224 -0
  75. pentool/modules/scanner/report.py +117 -0
  76. pentool/modules/sequencer.py +500 -0
  77. pentool/modules/spider.py +702 -0
  78. pentool/modules/target.py +190 -0
  79. pentool/modules/websocket_handler.py +259 -0
  80. pentool/plugins/__init__.py +7 -0
  81. pentool/plugins/builtin/__init__.py +1 -0
  82. pentool/plugins/builtin/payloads_pro.py +404 -0
  83. pentool/plugins/builtin/reports_pro.py +450 -0
  84. pentool/plugins/builtin/scanner_pro.py +308 -0
  85. pentool/plugins/example_plugin.py +75 -0
  86. pentool/project_to_txt.py +67 -0
  87. pentool/services/__init__.py +1 -0
  88. pentool/services/base_service.py +64 -0
  89. pentool/services/intruder_service.py +108 -0
  90. pentool/services/proxy_service.py +232 -0
  91. pentool/services/repeater_service.py +111 -0
  92. pentool/services/scan_service.py +340 -0
  93. pentool/storage/__init__.py +0 -0
  94. pentool/storage/http_storage.py +522 -0
  95. pentool/storage/large_body_handler.py +54 -0
  96. pentool/storage/lru_cache.py +49 -0
  97. pentool/t.py +213 -0
  98. pentool/tui/__init__.py +1 -0
  99. pentool/tui/app.py +1438 -0
  100. pentool/tui/constants.py +79 -0
  101. pentool/tui/dialogs/__init__.py +0 -0
  102. pentool/tui/dialogs/cert_dialog.py +81 -0
  103. pentool/tui/dialogs/file_selector.py +227 -0
  104. pentool/tui/dialogs/load_from_proxy.py +125 -0
  105. pentool/tui/dialogs/match_replace_dialog.py +229 -0
  106. pentool/tui/dialogs/project_dialog.py +82 -0
  107. pentool/tui/dialogs/scope_dialog.py +225 -0
  108. pentool/tui/messages.py +121 -0
  109. pentool/tui/mixins/__init__.py +0 -0
  110. pentool/tui/mixins/app_mixin.py +61 -0
  111. pentool/tui/mixins/dialog_cancel.py +25 -0
  112. pentool/tui/mixins/request_context_menu.py +285 -0
  113. pentool/tui/mixins/tab_rename.py +101 -0
  114. pentool/tui/screens/__init__.py +31 -0
  115. pentool/tui/screens/comparer/__init__.py +3 -0
  116. pentool/tui/screens/comparer/screen.py +233 -0
  117. pentool/tui/screens/dashboard/__init__.py +2 -0
  118. pentool/tui/screens/dashboard/live_dashboard.py +741 -0
  119. pentool/tui/screens/dashboard/screen.py +820 -0
  120. pentool/tui/screens/decoder/__init__.py +3 -0
  121. pentool/tui/screens/decoder/screen.py +311 -0
  122. pentool/tui/screens/extensions/__init__.py +3 -0
  123. pentool/tui/screens/extensions/screen.py +24 -0
  124. pentool/tui/screens/intruder/__init__.py +3 -0
  125. pentool/tui/screens/intruder/screen.py +1362 -0
  126. pentool/tui/screens/proxy/__init__.py +3 -0
  127. pentool/tui/screens/proxy/screen.py +1664 -0
  128. pentool/tui/screens/repeater/__init__.py +3 -0
  129. pentool/tui/screens/repeater/screen.py +646 -0
  130. pentool/tui/screens/scanner/__init__.py +3 -0
  131. pentool/tui/screens/scanner/screen.py +1984 -0
  132. pentool/tui/screens/sequencer/__init__.py +3 -0
  133. pentool/tui/screens/sequencer/screen.py +520 -0
  134. pentool/tui/screens/settings/__init__.py +5 -0
  135. pentool/tui/screens/settings/hotkeys.py +134 -0
  136. pentool/tui/screens/settings/screen.py +543 -0
  137. pentool/tui/screens/spider/__init__.py +3 -0
  138. pentool/tui/screens/spider/screen.py +385 -0
  139. pentool/tui/screens/target/__init__.py +4 -0
  140. pentool/tui/screens/target/screen.py +361 -0
  141. pentool/tui/screens/terminal/__init__.py +5 -0
  142. pentool/tui/screens/terminal/screen.py +181 -0
  143. pentool/tui/widgets/__init__.py +1 -0
  144. pentool/tui/widgets/context_menu.py +148 -0
  145. pentool/tui/widgets/filter_bar.py +206 -0
  146. pentool/tui/widgets/inspector_panel.py +112 -0
  147. pentool/tui/widgets/menu.py +86 -0
  148. pentool/tui/widgets/menu_bar.py +238 -0
  149. pentool/tui/widgets/module_tabs.py +73 -0
  150. pentool/tui/widgets/payload_drop_zone.py +66 -0
  151. pentool/tui/widgets/request_editor.py +504 -0
  152. pentool/tui/widgets/resize_handle.py +116 -0
  153. pentool/tui/widgets/search_bar.py +113 -0
  154. pentool/tui/widgets/statusbar.py +99 -0
  155. pentool/tui/widgets/toolbar_button.py +76 -0
  156. pentool/utils/__init__.py +1 -0
  157. pentool/utils/cert.py +361 -0
  158. pentool/utils/coder.py +170 -0
  159. pentool/utils/copy_as.py +251 -0
  160. pentool/utils/diff.py +91 -0
  161. pentool/utils/http_client.py +175 -0
  162. pentool/utils/parser.py +227 -0
  163. pentool/utils/terminal_check.py +32 -0
  164. pentool-1.0.0.dist-info/METADATA +155 -0
  165. pentool-1.0.0.dist-info/RECORD +169 -0
  166. pentool-1.0.0.dist-info/WHEEL +5 -0
  167. pentool-1.0.0.dist-info/entry_points.txt +2 -0
  168. pentool-1.0.0.dist-info/licenses/LICENSE +34 -0
  169. pentool-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,238 @@
1
+ """MenuBar — горизонтальная полоса с каскадными выпадающими меню."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+
7
+ from textual import on
8
+ from textual.app import ComposeResult
9
+ from textual.message import Message
10
+ from textual.widget import Widget
11
+ from textual.widgets import Static
12
+ from pathlib import Path
13
+
14
+ _CSS = (Path(__file__).parent / "menu_bar.tcss").read_text(encoding="utf-8")
15
+
16
+
17
+ class MenuItem(Static):
18
+ """Пункт выпадающего меню."""
19
+
20
+ DEFAULT_CSS = _CSS
21
+
22
+ class Selected(Message):
23
+ """Пользователь выбрал пункт меню."""
24
+ def __init__(self, action: str) -> None:
25
+ super().__init__()
26
+ self.action = action
27
+
28
+ def __init__(
29
+ self,
30
+ label: str,
31
+ action: str = "",
32
+ shortcut: str = "",
33
+ disabled: bool = False,
34
+ **kwargs,
35
+ ) -> None:
36
+ self._action = action
37
+ self._shortcut = shortcut
38
+ self._is_separator = label == "---"
39
+ self._is_disabled = disabled or self._is_separator
40
+
41
+ if self._is_separator:
42
+ display_text = "─" * 20
43
+ elif shortcut:
44
+ display_text = f"{label:<22}{shortcut}"
45
+ else:
46
+ display_text = label
47
+
48
+ super().__init__(display_text, **kwargs)
49
+
50
+ if self._is_separator:
51
+ self.add_class("-separator")
52
+ if self._is_disabled:
53
+ self.add_class("-disabled")
54
+
55
+ def on_click(self) -> None:
56
+ if self._is_disabled or not self._action:
57
+ return
58
+ self.post_message(self.Selected(self._action))
59
+
60
+
61
+ class MenuDropdown(Widget):
62
+ """Выпадающий список пунктов меню."""
63
+
64
+ DEFAULT_CSS = _CSS
65
+
66
+ def __init__(self, items: list[tuple], **kwargs) -> None:
67
+ super().__init__(**kwargs)
68
+ self._items = items
69
+
70
+ def compose(self) -> ComposeResult:
71
+ for item in self._items:
72
+ if item[0] == "---":
73
+ yield MenuItem("---")
74
+ elif len(item) == 2:
75
+ yield MenuItem(item[0], action=item[1])
76
+ elif len(item) == 3:
77
+ yield MenuItem(item[0], action=item[1], shortcut=item[2])
78
+
79
+ def show(self, x: int, y: int) -> None:
80
+ self.styles.offset = (x, y)
81
+ self.display = True
82
+
83
+ def hide(self) -> None:
84
+ self.display = False
85
+
86
+ @on(MenuItem.Selected)
87
+ def on_menu_item_selected(self, event: MenuItem.Selected) -> None:
88
+ event.stop()
89
+ self.hide()
90
+ # Закрыть остальные дропдауны через MenuBar
91
+ try:
92
+ self.app.query_one("#menu-bar", MenuBar)._close_all()
93
+ except Exception:
94
+ pass
95
+ action = event.action
96
+ if action.startswith("app.action_"):
97
+ # run_action принимает имя без префикса "action_"
98
+ action_name = action[len("app.action_"):]
99
+ self.app.run_worker(self.app.run_action(action_name), exclusive=False)
100
+
101
+
102
+ class MenuHeader(Static):
103
+ """Заголовок одного меню (кликабельный)."""
104
+
105
+ DEFAULT_CSS = _CSS
106
+
107
+ class Activated(Message):
108
+ """MenuHeader был нажат."""
109
+ def __init__(self, header: MenuHeader) -> None:
110
+ super().__init__()
111
+ self.header = header
112
+
113
+ def __init__(self, label: str, menu_id: str, **kwargs) -> None:
114
+ super().__init__(label, **kwargs)
115
+ self._menu_id = menu_id
116
+
117
+ @property
118
+ def menu_id(self) -> str:
119
+ return self._menu_id
120
+
121
+ def on_click(self) -> None:
122
+ self.post_message(self.Activated(self))
123
+
124
+
125
+ class MenuBar(Static):
126
+ """Горизонтальная полоса с выпадающими меню — аналог десктопных приложений."""
127
+
128
+ DEFAULT_CSS = _CSS
129
+
130
+ # Структура: [(label, menu_id, [(item_label, action, shortcut?), ...])]
131
+ MENUS: list[tuple[str, str, list]] = [
132
+ ("PenTool", "burp", [
133
+ ("About PenTool", "app.action_about"),
134
+ ("Check for updates...", "app.action_check_updates"),
135
+ ]),
136
+ ("Project", "project", [
137
+ ("New Project", "app.action_new_project", "Ctrl+N"),
138
+ ("Open...", "app.action_open_project", "Ctrl+O"),
139
+ ("Save", "app.action_save_project", "Ctrl+S"),
140
+ ("Recent", "app.action_recent_projects"),
141
+ ("---", ""),
142
+ ("Exit", "app.action_quit", "Ctrl+Q"),
143
+ ]),
144
+ ("Intruder", "intruder", [
145
+ ("New Attack", "app.action_intruder_new"),
146
+ ("Start Attack", "app.action_intruder_start"),
147
+ ("Stop Attack", "app.action_intruder_stop"),
148
+ ]),
149
+ ("Repeater", "repeater", [
150
+ ("Send to Repeater", "app.action_send_to_repeater"),
151
+ ("New Tab", "app.action_repeater_new_tab"),
152
+ ]),
153
+ ("View", "view", [
154
+ ("Filter settings", "app.action_filter_settings"),
155
+ ("Toggle Dark/Light theme","app.action_toggle_theme"),
156
+ ("Word wrap", "app.action_toggle_word_wrap"),
157
+ ]),
158
+ ("Help", "help", [
159
+ ("Documentation", "app.action_documentation"),
160
+ ("Keyboard shortcuts", "app.action_keyboard_shortcuts"),
161
+ ("---", ""),
162
+ ("CA Certificate", "app.action_open_ca_cert"),
163
+ ("---", ""),
164
+ ("About", "app.action_about"),
165
+ ]),
166
+ ]
167
+
168
+ def __init__(self, **kwargs) -> None:
169
+ super().__init__(**kwargs)
170
+ self._active_menu: str | None = None
171
+ self._dropdowns: dict[str, MenuDropdown] = {}
172
+
173
+ def compose(self) -> ComposeResult:
174
+ for label, menu_id, items in self.MENUS:
175
+ yield MenuHeader(label, menu_id, id=f"menu-header-{menu_id}")
176
+
177
+ # Выпадающие списки монтируются вне MenuBar (через app.mount)
178
+ # чтобы они поверх всего контента
179
+
180
+ def on_mount(self) -> None:
181
+ # Создаём дропдауны и монтируем в app
182
+ for label, menu_id, items in self.MENUS:
183
+ dropdown = MenuDropdown(items, id=f"menu-dropdown-{menu_id}")
184
+ self._dropdowns[menu_id] = dropdown
185
+ self.app.mount(dropdown)
186
+
187
+ def on_unmount(self) -> None:
188
+ for dropdown in self._dropdowns.values():
189
+ try:
190
+ dropdown.remove()
191
+ except Exception:
192
+ pass
193
+
194
+ @on(MenuHeader.Activated)
195
+ def on_menu_header_activated(self, event: MenuHeader.Activated) -> None:
196
+ menu_id = event.header.menu_id
197
+ if self._active_menu == menu_id:
198
+ self._close_all()
199
+ return
200
+
201
+ self._close_all()
202
+ self._open_menu(menu_id, event.header)
203
+
204
+ def _open_menu(self, menu_id: str, header: MenuHeader) -> None:
205
+ dropdown = self._dropdowns.get(menu_id)
206
+ if dropdown is None:
207
+ return
208
+
209
+ # Вычисляем позицию под заголовком
210
+ try:
211
+ region = header.content_region
212
+ x = region.x
213
+ y = region.y + 1
214
+ except Exception:
215
+ x, y = 0, 1
216
+
217
+ dropdown.show(x, y)
218
+ header.add_class("-active")
219
+ self._active_menu = menu_id
220
+
221
+ def _close_all(self) -> None:
222
+ for dropdown in self._dropdowns.values():
223
+ dropdown.hide()
224
+ for label, menu_id, _ in self.MENUS:
225
+ try:
226
+ header = self.query_one(f"#menu-header-{menu_id}", MenuHeader)
227
+ header.remove_class("-active")
228
+ except Exception:
229
+ pass
230
+ self._active_menu = None
231
+
232
+ def on_key(self, event) -> None:
233
+ if event.key == "escape":
234
+ self._close_all()
235
+
236
+ def on_click(self, event) -> None:
237
+ # Клик вне MenuBar — закрыть (обрабатывается через app)
238
+ pass
@@ -0,0 +1,73 @@
1
+ """Горизонтальная панель вкладок для навигации по модулям (Burp-стиль)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.reactive import reactive
7
+ from textual.widget import Widget
8
+ from textual.widgets import Tab, Tabs
9
+
10
+ from pentool.tui.widgets.menu import BASIC_MODULES, MODULES, ModuleSelected
11
+ from pathlib import Path
12
+
13
+ _CSS = (Path(__file__).parent / "module_tabs.tcss").read_text(encoding="utf-8")
14
+
15
+
16
+ class ModuleTabs(Widget):
17
+ """Горизонтальные вкладки навигации по модулям.
18
+
19
+ Постит тот же ModuleSelected message что и SideMenu —
20
+ app.on_module_selected не требует изменений.
21
+ """
22
+
23
+ DEFAULT_CSS = _CSS
24
+
25
+ active_module: reactive[str] = reactive("proxy")
26
+
27
+ def compose(self) -> ComposeResult:
28
+ tabs = [Tab(label, id=f"tab-{mod_id}") for mod_id, label, _ in MODULES]
29
+ yield Tabs(*tabs, id="module-tabs-inner")
30
+
31
+ def on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:
32
+ if event.tab is None:
33
+ return
34
+ tab_id = event.tab.id or ""
35
+ if tab_id.startswith("tab-"):
36
+ module_id = tab_id[4:]
37
+ self.active_module = module_id
38
+ self.post_message(ModuleSelected(module_id))
39
+
40
+ def select_module(self, module_id: str) -> None:
41
+ """Программно переключить вкладку (без постинга события).
42
+
43
+ Args:
44
+ module_id: ID модуля из MODULES.
45
+ """
46
+ self.active_module = module_id
47
+ try:
48
+ tabs = self.query_one("#module-tabs-inner", Tabs)
49
+ tabs.active = f"tab-{module_id}"
50
+ except Exception:
51
+ pass
52
+
53
+ def set_mode(self, mode: str) -> None:
54
+ """Переключить режим: 'basic' показывает только базовые вкладки.
55
+
56
+ Args:
57
+ mode: 'basic' | 'advanced'
58
+ """
59
+ try:
60
+ tabs = self.query_one("#module-tabs-inner", Tabs)
61
+ for mod_id, _, _ in MODULES:
62
+ tab_id = f"tab-{mod_id}"
63
+ try:
64
+ tab = tabs.query_one(f"#{tab_id}", Tab)
65
+ if mode == "basic" and mod_id not in BASIC_MODULES:
66
+ tab.styles.display = "none"
67
+ else:
68
+ tab.styles.display = "block"
69
+ except Exception:
70
+ pass
71
+ tabs.refresh()
72
+ except Exception:
73
+ pass
@@ -0,0 +1,66 @@
1
+ """PayloadDropZone — зона для перетаскивания файлов с payload'ами."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.message import Message
6
+ from textual.widget import Widget
7
+ from pathlib import Path
8
+
9
+ _CSS = (Path(__file__).parent / "payload_drop_zone.tcss").read_text(encoding="utf-8")
10
+
11
+
12
+ class PayloadDropZone(Widget):
13
+ """Визуальная зона drag & drop для payload-файлов.
14
+
15
+ При отсутствии нативного DragDrop (textual-filedrop) — показывает
16
+ подсказку. Интегрируется с кнопкой "Load from file".
17
+
18
+ Сообщения:
19
+ PayloadDropZone.PayloadsLoaded — payload'ы загружены из файла.
20
+ """
21
+
22
+ DEFAULT_CSS = _CSS
23
+
24
+ class PayloadsLoaded(Message):
25
+ """Payload'ы успешно загружены из файла."""
26
+ def __init__(self, payloads: list[str], source_path: str = "") -> None:
27
+ super().__init__()
28
+ self.payloads = payloads
29
+ self.source_path = source_path
30
+
31
+ def __init__(self, **kwargs) -> None:
32
+ super().__init__(**kwargs)
33
+ self.update_text()
34
+
35
+ def update_text(self, count: int = 0) -> None:
36
+ pass # count аргумент сохранён для обратной совместимости API
37
+
38
+ def render(self) -> str:
39
+ return " Drop .txt / .yaml payload file here\n (or use 'Load from file' button)"
40
+
41
+ def load_from_path(self, path: str) -> None:
42
+ """Загрузить payload'ы из файла и отправить сообщение."""
43
+ from pentool.api.intruder_api import load_payloads_from_file
44
+ payloads = load_payloads_from_file(path)
45
+ if payloads:
46
+ self.post_message(self.PayloadsLoaded(payloads, source_path=path))
47
+
48
+ def on_click(self) -> None:
49
+ """Клик по зоне — открыть FileSelectorDialog как альтернативу DragDrop."""
50
+ from pentool.tui.dialogs.file_selector import FileSelectorDialog, FileSelectorMode
51
+
52
+ def _on_selected(path: str | None) -> None:
53
+ if path:
54
+ self.load_from_path(path)
55
+
56
+ try:
57
+ self.app.push_screen(
58
+ FileSelectorDialog(
59
+ mode=FileSelectorMode.OPEN,
60
+ filter_ext=["*.txt", "*.yaml", "*.yml"],
61
+ title="Select payload file",
62
+ ),
63
+ _on_selected,
64
+ )
65
+ except Exception:
66
+ pass