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.
- pentool/__init__.py +4 -0
- pentool/__main__.py +17 -0
- pentool/api/__init__.py +45 -0
- pentool/api/base_api.py +52 -0
- pentool/api/comparer_api.py +23 -0
- pentool/api/decoder_api.py +28 -0
- pentool/api/intruder_api.py +209 -0
- pentool/api/proxy_api.py +319 -0
- pentool/api/repeater_api.py +118 -0
- pentool/api/scanner_api.py +336 -0
- pentool/api/sequencer_api.py +21 -0
- pentool/api/spider_api.py +126 -0
- pentool/api/target_api.py +121 -0
- pentool/cli/__init__.py +1 -0
- pentool/cli/main.py +100 -0
- pentool/cli/project.py +90 -0
- pentool/cli/proxy.py +185 -0
- pentool/cli/scan.py +118 -0
- pentool/core/__init__.py +1 -0
- pentool/core/config.py +184 -0
- pentool/core/crash_reporter.py +75 -0
- pentool/core/database.py +139 -0
- pentool/core/event_bus.py +232 -0
- pentool/core/events.py +164 -0
- pentool/core/features.py +410 -0
- pentool/core/license.py +294 -0
- pentool/core/logging.py +59 -0
- pentool/core/plugin_manager.py +332 -0
- pentool/core/project.py +37 -0
- pentool/core/storage_interface.py +311 -0
- pentool/core/updater.py +96 -0
- pentool/core/utils.py +61 -0
- pentool/modules/__init__.py +1 -0
- pentool/modules/comparer.py +177 -0
- pentool/modules/decoder.py +281 -0
- pentool/modules/intruder.py +432 -0
- pentool/modules/intruder_turbo.py +190 -0
- pentool/modules/match_replace.py +146 -0
- pentool/modules/proxy.py +803 -0
- pentool/modules/repeater.py +237 -0
- pentool/modules/scanner/__init__.py +7 -0
- pentool/modules/scanner/ai_analyzer.py +230 -0
- pentool/modules/scanner/base.py +173 -0
- pentool/modules/scanner/baseline.py +101 -0
- pentool/modules/scanner/checks/__init__.py +47 -0
- pentool/modules/scanner/checks/broken_auth.py +231 -0
- pentool/modules/scanner/checks/cors.py +209 -0
- pentool/modules/scanner/checks/dom_xss.py +114 -0
- pentool/modules/scanner/checks/graphql.py +110 -0
- pentool/modules/scanner/checks/header_injection.py +262 -0
- pentool/modules/scanner/checks/headers.py +75 -0
- pentool/modules/scanner/checks/helpers.py +358 -0
- pentool/modules/scanner/checks/info_leak.py +68 -0
- pentool/modules/scanner/checks/jwt_none.py +238 -0
- pentool/modules/scanner/checks/lfi.py +186 -0
- pentool/modules/scanner/checks/nosql_injection.py +92 -0
- pentool/modules/scanner/checks/oauth.py +123 -0
- pentool/modules/scanner/checks/open_redirect.py +168 -0
- pentool/modules/scanner/checks/path_traversal.py +229 -0
- pentool/modules/scanner/checks/prototype_pollution.py +88 -0
- pentool/modules/scanner/checks/rce.py +338 -0
- pentool/modules/scanner/checks/sensitive_data.py +74 -0
- pentool/modules/scanner/checks/sqli.py +546 -0
- pentool/modules/scanner/checks/ssrf.py +273 -0
- pentool/modules/scanner/checks/ssti.py +353 -0
- pentool/modules/scanner/checks/xss.py +622 -0
- pentool/modules/scanner/checks/xxe.py +240 -0
- pentool/modules/scanner/engine.py +407 -0
- pentool/modules/scanner/fingerprint.py +169 -0
- pentool/modules/scanner/helpers.py +94 -0
- pentool/modules/scanner/mutator.py +378 -0
- pentool/modules/scanner/oob.py +104 -0
- pentool/modules/scanner/passive.py +114 -0
- pentool/modules/scanner/payloads.py +224 -0
- pentool/modules/scanner/report.py +117 -0
- pentool/modules/sequencer.py +500 -0
- pentool/modules/spider.py +702 -0
- pentool/modules/target.py +190 -0
- pentool/modules/websocket_handler.py +259 -0
- pentool/plugins/__init__.py +7 -0
- pentool/plugins/builtin/__init__.py +1 -0
- pentool/plugins/builtin/payloads_pro.py +404 -0
- pentool/plugins/builtin/reports_pro.py +450 -0
- pentool/plugins/builtin/scanner_pro.py +308 -0
- pentool/plugins/example_plugin.py +75 -0
- pentool/project_to_txt.py +67 -0
- pentool/services/__init__.py +1 -0
- pentool/services/base_service.py +64 -0
- pentool/services/intruder_service.py +108 -0
- pentool/services/proxy_service.py +232 -0
- pentool/services/repeater_service.py +111 -0
- pentool/services/scan_service.py +340 -0
- pentool/storage/__init__.py +0 -0
- pentool/storage/http_storage.py +522 -0
- pentool/storage/large_body_handler.py +54 -0
- pentool/storage/lru_cache.py +49 -0
- pentool/t.py +213 -0
- pentool/tui/__init__.py +1 -0
- pentool/tui/app.py +1438 -0
- pentool/tui/constants.py +79 -0
- pentool/tui/dialogs/__init__.py +0 -0
- pentool/tui/dialogs/cert_dialog.py +81 -0
- pentool/tui/dialogs/file_selector.py +227 -0
- pentool/tui/dialogs/load_from_proxy.py +125 -0
- pentool/tui/dialogs/match_replace_dialog.py +229 -0
- pentool/tui/dialogs/project_dialog.py +82 -0
- pentool/tui/dialogs/scope_dialog.py +225 -0
- pentool/tui/messages.py +121 -0
- pentool/tui/mixins/__init__.py +0 -0
- pentool/tui/mixins/app_mixin.py +61 -0
- pentool/tui/mixins/dialog_cancel.py +25 -0
- pentool/tui/mixins/request_context_menu.py +285 -0
- pentool/tui/mixins/tab_rename.py +101 -0
- pentool/tui/screens/__init__.py +31 -0
- pentool/tui/screens/comparer/__init__.py +3 -0
- pentool/tui/screens/comparer/screen.py +233 -0
- pentool/tui/screens/dashboard/__init__.py +2 -0
- pentool/tui/screens/dashboard/live_dashboard.py +741 -0
- pentool/tui/screens/dashboard/screen.py +820 -0
- pentool/tui/screens/decoder/__init__.py +3 -0
- pentool/tui/screens/decoder/screen.py +311 -0
- pentool/tui/screens/extensions/__init__.py +3 -0
- pentool/tui/screens/extensions/screen.py +24 -0
- pentool/tui/screens/intruder/__init__.py +3 -0
- pentool/tui/screens/intruder/screen.py +1362 -0
- pentool/tui/screens/proxy/__init__.py +3 -0
- pentool/tui/screens/proxy/screen.py +1664 -0
- pentool/tui/screens/repeater/__init__.py +3 -0
- pentool/tui/screens/repeater/screen.py +646 -0
- pentool/tui/screens/scanner/__init__.py +3 -0
- pentool/tui/screens/scanner/screen.py +1984 -0
- pentool/tui/screens/sequencer/__init__.py +3 -0
- pentool/tui/screens/sequencer/screen.py +520 -0
- pentool/tui/screens/settings/__init__.py +5 -0
- pentool/tui/screens/settings/hotkeys.py +134 -0
- pentool/tui/screens/settings/screen.py +543 -0
- pentool/tui/screens/spider/__init__.py +3 -0
- pentool/tui/screens/spider/screen.py +385 -0
- pentool/tui/screens/target/__init__.py +4 -0
- pentool/tui/screens/target/screen.py +361 -0
- pentool/tui/screens/terminal/__init__.py +5 -0
- pentool/tui/screens/terminal/screen.py +181 -0
- pentool/tui/widgets/__init__.py +1 -0
- pentool/tui/widgets/context_menu.py +148 -0
- pentool/tui/widgets/filter_bar.py +206 -0
- pentool/tui/widgets/inspector_panel.py +112 -0
- pentool/tui/widgets/menu.py +86 -0
- pentool/tui/widgets/menu_bar.py +238 -0
- pentool/tui/widgets/module_tabs.py +73 -0
- pentool/tui/widgets/payload_drop_zone.py +66 -0
- pentool/tui/widgets/request_editor.py +504 -0
- pentool/tui/widgets/resize_handle.py +116 -0
- pentool/tui/widgets/search_bar.py +113 -0
- pentool/tui/widgets/statusbar.py +99 -0
- pentool/tui/widgets/toolbar_button.py +76 -0
- pentool/utils/__init__.py +1 -0
- pentool/utils/cert.py +361 -0
- pentool/utils/coder.py +170 -0
- pentool/utils/copy_as.py +251 -0
- pentool/utils/diff.py +91 -0
- pentool/utils/http_client.py +175 -0
- pentool/utils/parser.py +227 -0
- pentool/utils/terminal_check.py +32 -0
- pentool-1.0.0.dist-info/METADATA +155 -0
- pentool-1.0.0.dist-info/RECORD +169 -0
- pentool-1.0.0.dist-info/WHEEL +5 -0
- pentool-1.0.0.dist-info/entry_points.txt +2 -0
- pentool-1.0.0.dist-info/licenses/LICENSE +34 -0
- 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
|