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,207 @@
1
+ """Event Bus — внутренняя шина событий приложения."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import threading
7
+ from collections import defaultdict, deque
8
+ from typing import Callable, TypeVar
9
+
10
+ from pentool.core.events import AppEvent
11
+ from pentool.core.logging import get_logger
12
+
13
+ logger = get_logger(__name__)
14
+
15
+ T = TypeVar("T", bound=AppEvent)
16
+
17
+
18
+ class EventBus:
19
+ """Thread-safe шина событий с историей.
20
+
21
+ Attributes:
22
+ _subscribers: Словарь type → список обработчиков.
23
+ _history: Кольцевой буфер всех эмиченных событий.
24
+ _lock: RLock для thread-safe операций.
25
+ """
26
+
27
+ def __init__(self, max_history: int = 10_000) -> None:
28
+ self._subscribers: dict[type, list[Callable]] = defaultdict(list)
29
+ self._history: deque[AppEvent] = deque(maxlen=max_history)
30
+ self._lock = threading.RLock()
31
+
32
+ # ── subscribe / unsubscribe ────────────────────────────────────────────────
33
+
34
+ def subscribe(self, event_type: type[T], handler: Callable[[T], None]) -> None:
35
+ """Подписаться на события заданного типа.
36
+
37
+ Args:
38
+ event_type: Класс события (подкласс AppEvent).
39
+ handler: Функция-обработчик, принимающая экземпляр события.
40
+ """
41
+ with self._lock:
42
+ if handler not in self._subscribers[event_type]:
43
+ self._subscribers[event_type].append(handler)
44
+
45
+ def unsubscribe(self, event_type: type, handler: Callable) -> None:
46
+ """Отписаться от событий.
47
+
48
+ Безопасен при повторном вызове — не бросает исключение если
49
+ обработчик не был подписан.
50
+ """
51
+ with self._lock:
52
+ try:
53
+ self._subscribers[event_type].remove(handler)
54
+ except ValueError:
55
+ pass
56
+
57
+ def unsubscribe_all(self, handler: Callable) -> None:
58
+ """Отписать обработчик от всех типов событий сразу."""
59
+ with self._lock:
60
+ for handlers in self._subscribers.values():
61
+ try:
62
+ handlers.remove(handler)
63
+ except ValueError:
64
+ pass
65
+
66
+ # ── emit ──────────────────────────────────────────────────────────────────
67
+
68
+ def emit(self, event: AppEvent) -> None:
69
+ """Синхронный emit из основного потока.
70
+
71
+ Вызывает всех подписчиков немедленно (в том же потоке).
72
+ Сохраняет событие в историю.
73
+
74
+ Args:
75
+ event: Экземпляр события (подкласс AppEvent).
76
+ """
77
+ with self._lock:
78
+ self._history.append(event)
79
+ handlers = list(self._subscribers.get(type(event), []))
80
+
81
+ for handler in handlers:
82
+ try:
83
+ handler(event)
84
+ except Exception as exc:
85
+ logger.warning(
86
+ "EventBus: handler %s raised %s for event %s",
87
+ handler, exc, type(event).__name__,
88
+ )
89
+
90
+ def emit_threadsafe(
91
+ self,
92
+ event: AppEvent,
93
+ loop: asyncio.AbstractEventLoop,
94
+ ) -> None:
95
+ """Thread-safe emit из worker-потока.
96
+
97
+ Сохраняет событие в историю немедленно (под локом),
98
+ затем планирует вызов обработчиков в event loop через
99
+ call_soon_threadsafe.
100
+
101
+ Args:
102
+ event: Экземпляр события.
103
+ loop: Event loop основного потока (app._loop).
104
+ """
105
+ with self._lock:
106
+ self._history.append(event)
107
+ handlers = list(self._subscribers.get(type(event), []))
108
+
109
+ if not handlers:
110
+ return
111
+
112
+ def _dispatch() -> None:
113
+ for handler in handlers:
114
+ try:
115
+ handler(event)
116
+ except Exception as exc:
117
+ logger.warning(
118
+ "EventBus: threadsafe handler %s raised %s for %s",
119
+ handler, exc, type(event).__name__,
120
+ )
121
+
122
+ try:
123
+ loop.call_soon_threadsafe(_dispatch)
124
+ except RuntimeError:
125
+ # loop закрыт — приложение завершается
126
+ pass
127
+
128
+ # ── history / replay ──────────────────────────────────────────────────────
129
+
130
+ def get_history(
131
+ self,
132
+ event_type: type | None = None,
133
+ limit: int = 0,
134
+ ) -> list[AppEvent]:
135
+ with self._lock:
136
+ events = list(self._history)
137
+
138
+ if event_type is not None:
139
+ events = [e for e in events if isinstance(e, event_type)]
140
+
141
+ if limit > 0:
142
+ events = events[-limit:]
143
+
144
+ return events
145
+
146
+ def replay(
147
+ self,
148
+ handler: Callable,
149
+ event_type: type,
150
+ limit: int = 0,
151
+ ) -> None:
152
+ """Воспроизвести историю событий для нового подписчика.
153
+
154
+ Вызывает handler для каждого сохранённого события заданного типа.
155
+ Полезно при инициализации экрана, который подключился позже
156
+ (например, Dashboard после запуска сканирования).
157
+
158
+ Args:
159
+ handler: Функция-обработчик.
160
+ event_type: Тип событий для replay.
161
+ limit: Максимум событий (0 = все).
162
+ """
163
+ events = self.get_history(event_type=event_type, limit=limit)
164
+ for event in events:
165
+ try:
166
+ handler(event)
167
+ except Exception as exc:
168
+ logger.warning(
169
+ "EventBus.replay: handler %s raised %s", handler, exc
170
+ )
171
+
172
+ def clear_history(self) -> None:
173
+ with self._lock:
174
+ self._history.clear()
175
+
176
+ # ── stats ─────────────────────────────────────────────────────────────────
177
+
178
+ def stats(self) -> dict[str, int]:
179
+ """Статистика для отладки."""
180
+ with self._lock:
181
+ return {
182
+ "history_size": len(self._history),
183
+ "subscriber_types": len(self._subscribers),
184
+ "total_handlers": sum(len(v) for v in self._subscribers.values()),
185
+ }
186
+
187
+
188
+ # ── Глобальный синглтон ────────────────────────────────────────────────────────
189
+
190
+ _bus: EventBus | None = None
191
+ _bus_lock = threading.Lock()
192
+
193
+
194
+ def get_event_bus() -> EventBus:
195
+ global _bus
196
+ if _bus is None:
197
+ with _bus_lock:
198
+ if _bus is None:
199
+ _bus = EventBus()
200
+ return _bus
201
+
202
+
203
+ def reset_event_bus() -> None:
204
+ """Сбросить глобальный синглтон (только для тестов)."""
205
+ global _bus
206
+ with _bus_lock:
207
+ _bus = None
pentool/core/events.py ADDED
@@ -0,0 +1,160 @@
1
+ """Типизированные события приложения для Event Bus."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+
10
+ @dataclass
11
+ class AppEvent:
12
+ """Базовый класс для всех событий."""
13
+ timestamp: float = field(default_factory=time.time)
14
+ source: str = "" # имя модуля-источника, для отладки
15
+
16
+
17
+ # ── Scanner events ─────────────────────────────────────────────────────────────
18
+
19
+ @dataclass
20
+ class ScanStarted(AppEvent):
21
+ """Сканирование запущено."""
22
+ targets: list[str] = field(default_factory=list)
23
+ checks: list[str] = field(default_factory=list)
24
+
25
+
26
+ @dataclass
27
+ class ScanFinished(AppEvent):
28
+ """Сканирование завершено (нормально или по стопу)."""
29
+ total_findings: int = 0
30
+ stopped_early: bool = False
31
+
32
+
33
+ @dataclass
34
+ class ScanProgressEvent(AppEvent):
35
+ """Прогресс сканирования."""
36
+ done: int = 0
37
+ total: int = 0
38
+ scanning: bool = True
39
+
40
+
41
+ @dataclass
42
+ class FindingDiscovered(AppEvent):
43
+ """Обнаружена уязвимость (активный или пассивный скан)."""
44
+ finding: Any = None
45
+ scan_source: str = "active" # "active" | "passive"
46
+
47
+
48
+ # ── Spider events ──────────────────────────────────────────────────────────────
49
+
50
+ @dataclass
51
+ class UrlCrawled(AppEvent):
52
+ """Spider нашёл новый URL."""
53
+ url: str = ""
54
+ base_target: str = ""
55
+
56
+
57
+ @dataclass
58
+ class SpiderFinished(AppEvent):
59
+ """Краулинг завершён."""
60
+ base_url: str = ""
61
+ pages_count: int = 0
62
+ forms_count: int = 0
63
+ endpoints_count: int = 0
64
+
65
+
66
+ # ── Intruder events ────────────────────────────────────────────────────────────
67
+
68
+ @dataclass
69
+ class IntruderResultAdded(AppEvent):
70
+ """Получен результат одного запроса атаки."""
71
+ result: Any = None # IntruderResult
72
+
73
+
74
+ @dataclass
75
+ class IntruderFinished(AppEvent):
76
+ """Атака завершена."""
77
+ total_results: int = 0
78
+ stopped_early: bool = False
79
+
80
+
81
+ # ── Proxy events ───────────────────────────────────────────────────────────────
82
+
83
+ @dataclass
84
+ class ProxyRequestCaptured(AppEvent):
85
+ """Прокси перехватил новый запрос.
86
+
87
+ request: полный объект InterceptedRequest (Any чтобы не создавать
88
+ циклических импортов modules → core).
89
+ """
90
+ request_id: str = ""
91
+ method: str = ""
92
+ url: str = ""
93
+ host: str = ""
94
+ request: Any = None # InterceptedRequest
95
+
96
+
97
+ @dataclass
98
+ class ProxyRequestCompleted(AppEvent):
99
+ """Запрос через прокси завершён (получен ответ).
100
+
101
+ request: полный объект InterceptedRequest (Any чтобы не создавать
102
+ циклических импортов modules → core).
103
+ """
104
+ request_id: str = ""
105
+ status_code: int = 0
106
+ request: Any = None # InterceptedRequest
107
+
108
+
109
+ # Алиас для обратной совместимости: Sequencer подписывается на это событие
110
+ ProxyRequestDoneEvent = ProxyRequestCompleted
111
+
112
+
113
+ # ── Target / SiteMap events ────────────────────────────────────────────────────
114
+
115
+ @dataclass
116
+ class TargetUrlAdded(AppEvent):
117
+ """URL добавлен в SiteMap/Target."""
118
+ url: str = ""
119
+ host: str = ""
120
+
121
+
122
+ # ── Project events ─────────────────────────────────────────────────────────────
123
+
124
+ @dataclass
125
+ class ProjectSaved(AppEvent):
126
+ """Проект сохранён."""
127
+ path: str = ""
128
+
129
+
130
+ @dataclass
131
+ class ProjectLoaded(AppEvent):
132
+ """Проект загружен."""
133
+ path: str = ""
134
+ findings_count: int = 0
135
+ history_count: int = 0
136
+
137
+
138
+ # ── Scanner passive events ──────────────────���───────────────────────────────────
139
+
140
+ @dataclass
141
+ class PassiveScanToggled(AppEvent):
142
+ """Пассивный скан включён/выключен."""
143
+ enabled: bool = False
144
+
145
+
146
+ # ── WebSocket events ───────────────────────────────────────────────────────────
147
+
148
+ @dataclass
149
+ class WebSocketFrameEvent(AppEvent):
150
+ """Перехвачен WebSocket-фрейм (отдельное сообщение).
151
+
152
+ direction: "client→server" или "server→client"
153
+ opcode: 0x1=text, 0x2=binary, 0x8=close, 0x9=ping, 0xA=pong
154
+ payload: тело фрейма (уже без маски)
155
+ """
156
+ request_id: str = "" # ID родительского WS-соединения (upgrade-запрос)
157
+ direction: str = "" # "client→server" | "server→client"
158
+ opcode: int = 0x1 # 1=text, 2=binary, 8=close, 9=ping, 10=pong
159
+ payload: bytes = field(default_factory=bytes)
160
+ payload_text: str = "" # UTF-8 декодированный payload (для text-фреймов)