pentool 0.1.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 (133) hide show
  1. pentool/__init__.py +4 -0
  2. pentool/__main__.py +16 -0
  3. pentool/api/__init__.py +41 -0
  4. pentool/api/base_api.py +48 -0
  5. pentool/api/comparer_api.py +19 -0
  6. pentool/api/decoder_api.py +24 -0
  7. pentool/api/intruder_api.py +171 -0
  8. pentool/api/proxy_api.py +210 -0
  9. pentool/api/repeater_api.py +57 -0
  10. pentool/api/scanner_api.py +266 -0
  11. pentool/api/sequencer_api.py +17 -0
  12. pentool/api/spider_api.py +106 -0
  13. pentool/api/target_api.py +87 -0
  14. pentool/cli/__init__.py +1 -0
  15. pentool/cli/main.py +98 -0
  16. pentool/cli/project.py +88 -0
  17. pentool/cli/proxy.py +173 -0
  18. pentool/cli/scan.py +115 -0
  19. pentool/core/__init__.py +1 -0
  20. pentool/core/config.py +171 -0
  21. pentool/core/crash_reporter.py +75 -0
  22. pentool/core/database.py +139 -0
  23. pentool/core/event_bus.py +207 -0
  24. pentool/core/events.py +160 -0
  25. pentool/core/features.py +373 -0
  26. pentool/core/license.py +274 -0
  27. pentool/core/logging.py +54 -0
  28. pentool/core/plugin_manager.py +312 -0
  29. pentool/core/project.py +31 -0
  30. pentool/core/storage_interface.py +307 -0
  31. pentool/core/updater.py +96 -0
  32. pentool/core/utils.py +36 -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 +428 -0
  37. pentool/modules/intruder_turbo.py +176 -0
  38. pentool/modules/match_replace.py +142 -0
  39. pentool/modules/proxy.py +797 -0
  40. pentool/modules/repeater.py +195 -0
  41. pentool/modules/sequencer.py +492 -0
  42. pentool/modules/spider.py +679 -0
  43. pentool/modules/target.py +185 -0
  44. pentool/modules/websocket_handler.py +249 -0
  45. pentool/plugins/__init__.py +1 -0
  46. pentool/plugins/builtin/__init__.py +1 -0
  47. pentool/plugins/builtin/payloads_pro.py +381 -0
  48. pentool/plugins/builtin/reports_pro.py +436 -0
  49. pentool/plugins/builtin/scanner_pro.py +291 -0
  50. pentool/plugins/example_plugin.py +39 -0
  51. pentool/services/__init__.py +1 -0
  52. pentool/services/base_service.py +60 -0
  53. pentool/services/intruder_service.py +94 -0
  54. pentool/services/proxy_service.py +178 -0
  55. pentool/services/repeater_service.py +85 -0
  56. pentool/services/scan_service.py +330 -0
  57. pentool/storage/__init__.py +0 -0
  58. pentool/storage/http_storage.py +506 -0
  59. pentool/storage/large_body_handler.py +50 -0
  60. pentool/storage/lru_cache.py +45 -0
  61. pentool/t.py +213 -0
  62. pentool/tui/__init__.py +1 -0
  63. pentool/tui/app.py +1429 -0
  64. pentool/tui/constants.py +79 -0
  65. pentool/tui/dialogs/__init__.py +0 -0
  66. pentool/tui/dialogs/cert_dialog.py +81 -0
  67. pentool/tui/dialogs/file_selector.py +226 -0
  68. pentool/tui/dialogs/load_from_proxy.py +125 -0
  69. pentool/tui/dialogs/match_replace_dialog.py +229 -0
  70. pentool/tui/dialogs/project_dialog.py +82 -0
  71. pentool/tui/dialogs/scope_dialog.py +224 -0
  72. pentool/tui/messages.py +103 -0
  73. pentool/tui/mixins/__init__.py +0 -0
  74. pentool/tui/mixins/app_mixin.py +59 -0
  75. pentool/tui/mixins/dialog_cancel.py +21 -0
  76. pentool/tui/mixins/request_context_menu.py +247 -0
  77. pentool/tui/mixins/tab_rename.py +89 -0
  78. pentool/tui/screens/__init__.py +31 -0
  79. pentool/tui/screens/comparer/__init__.py +3 -0
  80. pentool/tui/screens/comparer/screen.py +229 -0
  81. pentool/tui/screens/dashboard/__init__.py +2 -0
  82. pentool/tui/screens/dashboard/live_dashboard.py +730 -0
  83. pentool/tui/screens/dashboard/screen.py +771 -0
  84. pentool/tui/screens/decoder/__init__.py +3 -0
  85. pentool/tui/screens/decoder/screen.py +306 -0
  86. pentool/tui/screens/extensions/__init__.py +3 -0
  87. pentool/tui/screens/extensions/screen.py +24 -0
  88. pentool/tui/screens/intruder/__init__.py +3 -0
  89. pentool/tui/screens/intruder/screen.py +1356 -0
  90. pentool/tui/screens/proxy/__init__.py +3 -0
  91. pentool/tui/screens/proxy/screen.py +1554 -0
  92. pentool/tui/screens/repeater/__init__.py +3 -0
  93. pentool/tui/screens/repeater/screen.py +621 -0
  94. pentool/tui/screens/scanner/__init__.py +3 -0
  95. pentool/tui/screens/scanner/screen.py +1956 -0
  96. pentool/tui/screens/sequencer/__init__.py +3 -0
  97. pentool/tui/screens/sequencer/screen.py +516 -0
  98. pentool/tui/screens/settings/__init__.py +5 -0
  99. pentool/tui/screens/settings/hotkeys.py +134 -0
  100. pentool/tui/screens/settings/screen.py +540 -0
  101. pentool/tui/screens/spider/__init__.py +3 -0
  102. pentool/tui/screens/spider/screen.py +380 -0
  103. pentool/tui/screens/target/__init__.py +4 -0
  104. pentool/tui/screens/target/screen.py +358 -0
  105. pentool/tui/screens/terminal/__init__.py +5 -0
  106. pentool/tui/screens/terminal/screen.py +179 -0
  107. pentool/tui/widgets/__init__.py +1 -0
  108. pentool/tui/widgets/context_menu.py +148 -0
  109. pentool/tui/widgets/filter_bar.py +204 -0
  110. pentool/tui/widgets/inspector_panel.py +111 -0
  111. pentool/tui/widgets/menu.py +86 -0
  112. pentool/tui/widgets/menu_bar.py +238 -0
  113. pentool/tui/widgets/module_tabs.py +68 -0
  114. pentool/tui/widgets/payload_drop_zone.py +65 -0
  115. pentool/tui/widgets/request_editor.py +495 -0
  116. pentool/tui/widgets/resize_handle.py +116 -0
  117. pentool/tui/widgets/search_bar.py +112 -0
  118. pentool/tui/widgets/statusbar.py +96 -0
  119. pentool/tui/widgets/toolbar_button.py +68 -0
  120. pentool/utils/__init__.py +1 -0
  121. pentool/utils/cert.py +314 -0
  122. pentool/utils/coder.py +170 -0
  123. pentool/utils/copy_as.py +250 -0
  124. pentool/utils/diff.py +82 -0
  125. pentool/utils/http_client.py +145 -0
  126. pentool/utils/parser.py +227 -0
  127. pentool/utils/terminal_check.py +31 -0
  128. pentool-0.1.0.dist-info/METADATA +231 -0
  129. pentool-0.1.0.dist-info/RECORD +133 -0
  130. pentool-0.1.0.dist-info/WHEEL +5 -0
  131. pentool-0.1.0.dist-info/entry_points.txt +2 -0
  132. pentool-0.1.0.dist-info/licenses/LICENSE +34 -0
  133. pentool-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,116 @@
1
+ """ResizeHandle — перетаскиваемый разделитель между двумя панелями."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.events import MouseDown, MouseMove, MouseUp
6
+ from textual.widget import Widget
7
+ from pathlib import Path
8
+
9
+ _CSS = (Path(__file__).parent / "resize_handle.tcss").read_text(encoding="utf-8")
10
+
11
+
12
+ class ResizeHandle(Widget):
13
+ """Перетаскиваемый разделитель между двумя виджетами.
14
+
15
+ Механика (без пустот, без отставания):
16
+ - mouse_down: фиксируем screen_x/y, размер левой панели и суммарный
17
+ размер пары (left.size + right.size). Total кешируется и не меняется
18
+ во время drag — иначе 1fr плавает.
19
+ - mouse_move: new_left = start_left + (screen_x - start_x).
20
+ new_right = total - new_left. Обе панели в абсолютных единицах → нет пустот.
21
+ - mouse_up: release_mouse.
22
+ """
23
+
24
+ DEFAULT_CSS = _CSS
25
+
26
+ def __init__(
27
+ self,
28
+ left_id: str,
29
+ right_id: str,
30
+ vertical: bool = False,
31
+ min_left: int = 8,
32
+ min_right: int = 8,
33
+ **kwargs,
34
+ ) -> None:
35
+ super().__init__(**kwargs)
36
+ self._left_id = left_id
37
+ self._right_id = right_id
38
+ self._vertical = vertical
39
+ self._min_left = min_left
40
+ self._min_right = min_right
41
+ self._dragging = False
42
+ self._drag_start_x: int = 0
43
+ self._drag_start_y: int = 0
44
+ self._start_size: int = 0
45
+ self._pair_total_cached: int = 0
46
+
47
+ self.tooltip = "Drag to resize"
48
+ if vertical:
49
+ self.add_class("-vertical")
50
+
51
+ def render(self) -> str:
52
+ """Рендерим символ-разделитель вместо текста по умолчанию."""
53
+ if self._vertical:
54
+ return "─" * (self.size.width or 1)
55
+ return "│"
56
+
57
+ def on_mouse_down(self, event: MouseDown) -> None:
58
+ if event.button != 1:
59
+ return
60
+ try:
61
+ left = self.app.query_one(f"#{self._left_id}")
62
+ right = self.app.query_one(f"#{self._right_id}")
63
+ except Exception:
64
+ return
65
+
66
+ self._dragging = True
67
+ self._drag_start_x = event.screen_x
68
+ self._drag_start_y = event.screen_y
69
+
70
+ if self._vertical:
71
+ self._start_size = left.size.height
72
+ self._pair_total_cached = left.size.height + right.size.height
73
+ else:
74
+ self._start_size = left.size.width
75
+ self._pair_total_cached = left.size.width + right.size.width
76
+
77
+ self.add_class("-dragging")
78
+ self.capture_mouse()
79
+ event.stop()
80
+
81
+ def on_mouse_move(self, event: MouseMove) -> None:
82
+ if not self._dragging:
83
+ return
84
+ try:
85
+ left = self.app.query_one(f"#{self._left_id}")
86
+ right = self.app.query_one(f"#{self._right_id}")
87
+ except Exception:
88
+ return
89
+
90
+ delta = event.screen_y - self._drag_start_y if self._vertical else event.screen_x - self._drag_start_x
91
+ total = self._pair_total_cached
92
+
93
+ new_left = self._start_size + delta
94
+ new_left = max(self._min_left, min(new_left, total - self._min_right))
95
+ new_right = total - new_left
96
+
97
+ if self._vertical:
98
+ left.styles.height = new_left
99
+ right.styles.height = new_right
100
+ else:
101
+ left.styles.width = new_left
102
+ right.styles.width = new_right
103
+
104
+ # Принудительный немедленный перерасчёт layout
105
+ if self.parent is not None:
106
+ self.parent.refresh(layout=True)
107
+
108
+ event.stop()
109
+
110
+ def on_mouse_up(self, event: MouseUp) -> None:
111
+ if not self._dragging:
112
+ return
113
+ self._dragging = False
114
+ self.remove_class("-dragging")
115
+ self.release_mouse()
116
+ event.stop()
@@ -0,0 +1,112 @@
1
+ """SearchBar — полоса поиска, появляющаяся по Ctrl+F."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.message import Message
7
+ from textual.widget import Widget
8
+ from textual.widgets import Button, Input, Label, Static
9
+ from pathlib import Path
10
+
11
+ _CSS = (Path(__file__).parent / "search_bar.tcss").read_text(encoding="utf-8")
12
+
13
+
14
+ class SearchBar(Widget):
15
+ """Полоса поиска, появляющаяся по Ctrl+F поверх TextArea/RichLog.
16
+
17
+ Использование:
18
+ yield SearchBar(id="search-bar")
19
+
20
+ Открыть:
21
+ self.query_one(SearchBar).show()
22
+
23
+ Закрыть:
24
+ self.query_one(SearchBar).hide()
25
+ """
26
+
27
+ DEFAULT_CSS = _CSS
28
+
29
+ class Search(Message):
30
+ """Пользователь ввёл поисковый запрос."""
31
+ def __init__(self, query: str, regex: bool = False, direction: int = 1) -> None:
32
+ super().__init__()
33
+ self.query = query
34
+ self.regex = regex
35
+ self.direction = direction # 1 = вперёд, -1 = назад
36
+
37
+ class Closed(Message):
38
+ """Поисковая строка закрыта."""
39
+
40
+ def __init__(self, **kwargs) -> None:
41
+ super().__init__(**kwargs)
42
+ self._regex_enabled = False
43
+
44
+ def compose(self) -> ComposeResult:
45
+ yield Label("Find:")
46
+ yield Input(placeholder="search...", id="search-input", compact=True)
47
+ yield Button("◀", id="btn-prev", variant="default")
48
+ yield Button("▶", id="btn-next", variant="default")
49
+ yield Static("Regex", id="search-regex-toggle")
50
+ yield Static("", id="search-count")
51
+
52
+ def show(self) -> None:
53
+ self.display = True
54
+ self.call_after_refresh(self._focus_input)
55
+
56
+ def hide(self) -> None:
57
+ self.display = False
58
+ self.post_message(self.Closed())
59
+
60
+ def _focus_input(self) -> None:
61
+ try:
62
+ self.query_one("#search-input", Input).focus()
63
+ except Exception:
64
+ pass
65
+
66
+ def _fire_search(self, direction: int = 1) -> None:
67
+ try:
68
+ query = self.query_one("#search-input", Input).value
69
+ except Exception:
70
+ return
71
+ if query:
72
+ self.post_message(self.Search(query, self._regex_enabled, direction))
73
+
74
+ def on_input_submitted(self, event: Input.Submitted) -> None:
75
+ if event.input.id == "search-input":
76
+ self._fire_search(1)
77
+
78
+ def on_button_pressed(self, event: Button.Pressed) -> None:
79
+ if event.button.id == "btn-next":
80
+ self._fire_search(1)
81
+ elif event.button.id == "btn-prev":
82
+ self._fire_search(-1)
83
+
84
+ def on_static_click(self, event) -> None:
85
+ try:
86
+ widget = event.widget
87
+ if getattr(widget, "id", None) == "search-regex-toggle":
88
+ self._regex_enabled = not self._regex_enabled
89
+ if self._regex_enabled:
90
+ widget.add_class("-active")
91
+ else:
92
+ widget.remove_class("-active")
93
+ except Exception:
94
+ pass
95
+
96
+ def on_key(self, event) -> None:
97
+ if event.key == "escape":
98
+ self.hide()
99
+ event.prevent_default()
100
+ elif event.key == "enter":
101
+ self._fire_search(1)
102
+ event.prevent_default()
103
+
104
+ def set_count(self, current: int, total: int) -> None:
105
+ try:
106
+ count_label = self.query_one("#search-count", Static)
107
+ if total == 0:
108
+ count_label.update("No matches")
109
+ else:
110
+ count_label.update(f"{current}/{total}")
111
+ except Exception:
112
+ pass
@@ -0,0 +1,96 @@
1
+ """Статусная строка: прокси, проект, время."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+
7
+ from textual.app import ComposeResult
8
+ from textual.reactive import reactive
9
+ from textual.widget import Widget
10
+ from textual.widgets import Static
11
+ from pathlib import Path
12
+
13
+ _CSS = (Path(__file__).parent / "statusbar.tcss").read_text(encoding="utf-8")
14
+
15
+
16
+ class StatusBar(Widget):
17
+ """Нижняя строка с состоянием прокси, именем проекта и временем."""
18
+
19
+ DEFAULT_CSS = _CSS
20
+
21
+ proxy_running: reactive[bool] = reactive(False)
22
+ proxy_port: reactive[int] = reactive(8080)
23
+ project_name: reactive[str] = reactive("no project")
24
+ project_path: reactive[str] = reactive("")
25
+ project_saved: reactive[bool] = reactive(True)
26
+ current_time: reactive[str] = reactive("")
27
+
28
+ def compose(self) -> ComposeResult:
29
+ yield Static("", id="proxy-status", classes="status-proxy stopped")
30
+ yield Static("", id="project-name", classes="status-project")
31
+ yield Static("", id="saved-status", classes="status-saved")
32
+ yield Static("", id="current-time", classes="status-time")
33
+
34
+ def on_mount(self) -> None:
35
+ self._update_proxy()
36
+ self._update_project()
37
+ self._update_saved()
38
+ self._update_time()
39
+ self.set_interval(1.0, self._update_time)
40
+
41
+ def _update_proxy(self) -> None:
42
+ widget = self.query_one("#proxy-status", Static)
43
+ if self.proxy_running:
44
+ widget.update(f"[green]● Proxy :{self.proxy_port}[/green]")
45
+ widget.remove_class("stopped")
46
+ widget.add_class("running")
47
+ else:
48
+ widget.update("[red]○ Proxy stopped[/red]")
49
+ widget.remove_class("running")
50
+ widget.add_class("stopped")
51
+
52
+ def _update_project(self) -> None:
53
+ name = self.project_name
54
+ path = self.project_path
55
+ if name == "no project":
56
+ markup = "[dim]no project[/dim]"
57
+ elif path:
58
+ markup = f"[dim]project:[/dim] [bold cyan]{name}[/bold cyan] [dim]{path}[/dim]"
59
+ else:
60
+ markup = f"[dim]project:[/dim] [bold cyan]{name}[/bold cyan]"
61
+ self.query_one("#project-name", Static).update(markup)
62
+
63
+ def _update_saved(self) -> None:
64
+ widget = self.query_one("#saved-status", Static)
65
+ if self.project_name == "no project":
66
+ widget.update("")
67
+ elif self.project_saved:
68
+ widget.update("[dim green]● Saved[/dim green]")
69
+ else:
70
+ widget.update("[yellow]● Unsaved[/yellow]")
71
+
72
+ def _update_time(self) -> None:
73
+ self.query_one("#current-time", Static).update(
74
+ datetime.now().strftime("%H:%M:%S")
75
+ )
76
+
77
+ def set_proxy_status(self, running: bool, port: int = 8080) -> None:
78
+ self.proxy_running = running
79
+ self.proxy_port = port
80
+ self._update_proxy()
81
+
82
+ def set_project_name(self, name: str) -> None:
83
+ self.project_name = name
84
+ self._update_project()
85
+ self._update_saved()
86
+
87
+ def set_project(self, name: str, path: str, saved: bool = True) -> None:
88
+ self.project_name = name
89
+ self.project_path = path
90
+ self.project_saved = saved
91
+ self._update_project()
92
+ self._update_saved()
93
+
94
+ def set_saved(self, saved: bool) -> None:
95
+ self.project_saved = saved
96
+ self._update_saved()
@@ -0,0 +1,68 @@
1
+ """ToolbarButton — единая плоская кнопка для тулбаров всех экранов."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.message import Message
6
+ from textual.widgets import Static
7
+ from pathlib import Path
8
+
9
+ _CSS = (Path(__file__).parent / "toolbar_button.tcss").read_text(encoding="utf-8")
10
+
11
+
12
+ class ToolbarButton(Static):
13
+ """Плоская кнопка для тулбара — без рамки, высота 1.
14
+
15
+ CSS-классы:
16
+ .active — зелёный цвет (включено / активно)
17
+ .inactive — красный цвет (выключено)
18
+ .disabled — серый цвет, клик игнорируется
19
+ .warn — оранжевый цвет (предупреждение)
20
+ .sending — жёлтый цвет (ожидание ответа)
21
+ """
22
+
23
+ DEFAULT_CSS = _CSS
24
+
25
+ class Pressed(Message):
26
+ """Сообщение о нажатии кнопки."""
27
+
28
+ ALLOW_SELECTOR_MATCH = True
29
+
30
+ def __init__(self, button: "ToolbarButton") -> None:
31
+ super().__init__()
32
+ self.button = button
33
+
34
+ @property
35
+ def control(self) -> "ToolbarButton":
36
+ """Позволяет @on(ToolbarButton.Pressed, "#btn-id") CSS-селектор."""
37
+ return self.button
38
+
39
+ def __init__(self, label: str, btn_id: str, classes: str = "") -> None:
40
+ super().__init__(label, id=btn_id, classes=classes)
41
+ self._label = label
42
+ self._disabled = "disabled" in classes.split()
43
+
44
+ @property
45
+ def label(self) -> str:
46
+ return self._label
47
+
48
+ @label.setter
49
+ def label(self, value: str) -> None:
50
+ self._label = value
51
+ self.update(value)
52
+
53
+ @property
54
+ def disabled(self) -> bool:
55
+ return self._disabled
56
+
57
+ @disabled.setter
58
+ def disabled(self, value: bool) -> None:
59
+ self._disabled = value
60
+ if value:
61
+ self.add_class("disabled")
62
+ else:
63
+ self.remove_class("disabled")
64
+
65
+ def on_click(self) -> None:
66
+ """Постит Pressed только если кнопка не disabled."""
67
+ if not self._disabled:
68
+ self.post_message(self.Pressed(self))
@@ -0,0 +1 @@
1
+ """Пакет utils: вспомогательные функции."""