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,59 @@
1
+ """Настройка системы логирования."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import sys
7
+ from pathlib import Path
8
+
9
+
10
+ def setup_logging(log_file: str, level: str = "INFO") -> logging.Logger:
11
+ """Настроить логирование: запись в файл (DEBUG) и в консоль (указанный уровень).
12
+
13
+ Args:
14
+ log_file: Путь к лог-файлу.
15
+ level: Уровень логирования для консоли (DEBUG/INFO/WARNING/ERROR).
16
+
17
+ Returns:
18
+ Настроенный корневой логгер.
19
+ """
20
+ log_path = Path(log_file)
21
+ log_path.parent.mkdir(parents=True, exist_ok=True)
22
+
23
+ numeric_level = getattr(logging, level.upper(), logging.INFO)
24
+
25
+ logger = logging.getLogger("pentool")
26
+ logger.setLevel(logging.DEBUG)
27
+
28
+ # Очистить существующие хендлеры, чтобы избежать дублирования
29
+ logger.handlers.clear()
30
+
31
+ # FileHandler — пишет всё (DEBUG и выше), дописывает в лог (не очищает)
32
+ fmt = logging.Formatter(
33
+ "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
34
+ datefmt="%Y-%m-%d %H:%M:%S",
35
+ )
36
+ file_handler = logging.FileHandler(log_file, encoding="utf-8", mode="a")
37
+ file_handler.setLevel(logging.DEBUG)
38
+ file_handler.setFormatter(fmt)
39
+
40
+ # StreamHandler — пишет в stderr на указанном уровне
41
+ console_handler = logging.StreamHandler(sys.stderr)
42
+ console_handler.setLevel(numeric_level)
43
+ console_handler.setFormatter(
44
+ logging.Formatter("[%(levelname)s] %(message)s")
45
+ )
46
+
47
+ logger.addHandler(file_handler)
48
+ logger.addHandler(console_handler)
49
+
50
+ return logger
51
+
52
+
53
+ def get_logger(name: str = "pentool") -> logging.Logger:
54
+ """Получить логгер по имени.
55
+
56
+ Args:
57
+ name: Имя логгера (по умолчанию корневой логгер pentool).
58
+ """
59
+ return logging.getLogger(name)
@@ -0,0 +1,332 @@
1
+ """Система плагинов Pentool: базовые классы и менеджер загрузки."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import logging
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ if TYPE_CHECKING:
13
+ import click
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Текущая версия Plugin API. Плагины с api_version > CURRENT несовместимы.
18
+ CURRENT_API_VERSION = 1
19
+
20
+ # Директория пользовательских плагинов (PRO-плагины устанавливаются сюда)
21
+ USER_PLUGINS_DIR = Path.home() / ".pentool" / "plugins"
22
+
23
+
24
+ class BasePlugin:
25
+ """Базовый класс для всех плагинов Pentool.
26
+
27
+ Каждый плагин должен объявить атрибуты класса:
28
+ name — уникальный ID (snake_case)
29
+ version — строка версии ("1.0")
30
+ author — имя автора
31
+ description — краткое описание
32
+ api_version — версия Plugin API (сейчас 1)
33
+ required_feature — строка feature из LicenseInfo.features, либо ""
34
+ (пустая строка = бесплатный плагин, не требует PRO)
35
+ """
36
+
37
+ name: str = ""
38
+ version: str = "0.1"
39
+ author: str = ""
40
+ description: str = ""
41
+ api_version: int = CURRENT_API_VERSION
42
+ required_feature: str = "" # "" = free; "scanner_pro" / "reports_pro" / ...
43
+
44
+
45
+ class BaseCheck:
46
+ """Базовый класс для отдельной проверки (активной или пассивной).
47
+
48
+ Используется в modules/scanner/ и в плагинах-сканерах.
49
+ """
50
+
51
+ name: str = ""
52
+ description: str = ""
53
+ severity: str = "info" # critical | high | medium | low | info
54
+ passive: bool = False # True → запускается на каждый прокси-запрос
55
+
56
+ async def scan(self, target: Any, http_client: Any, **kwargs) -> list:
57
+ """Запустить проверку. Возвращает список Finding."""
58
+ return []
59
+
60
+
61
+ class BaseScanner(BasePlugin):
62
+ """Базовый класс для плагина-сканера.
63
+
64
+ Плагин-сканер объединяет несколько BaseCheck под одним именем.
65
+ Пример: XSSScanner регистрирует reflected_xss, dom_xss и т.д.
66
+ """
67
+
68
+ checks: list[type[BaseCheck]] = []
69
+
70
+ async def scan(self, target: Any, http_client: Any, **kwargs) -> list:
71
+ """Запустить все checks и вернуть объединённый список Finding."""
72
+ results = []
73
+ for check_cls in self.checks:
74
+ check = check_cls()
75
+ try:
76
+ findings = await check.scan(target, http_client, **kwargs)
77
+ results.extend(findings)
78
+ except Exception as exc:
79
+ logger.warning("Check %s failed: %s", check_cls.name, exc)
80
+ return results
81
+
82
+
83
+ @dataclass
84
+ class PluginScreen:
85
+ """Экран, зарегистрированный плагином."""
86
+ name: str
87
+ widget_class: type
88
+ hotkey: str | None = None
89
+ plugin_name: str = ""
90
+
91
+
92
+ @dataclass
93
+ class PluginCommand:
94
+ """CLI-команда, зарегистрированная плагином."""
95
+ group_name: str
96
+ command: Any # click.Command
97
+ plugin_name: str = ""
98
+
99
+
100
+ class PluginHook:
101
+ """Объект, передаваемый в register(hook) каждого плагина.
102
+
103
+ Через него плагин объявляет свои вклады в TUI, CLI и сканер.
104
+ """
105
+
106
+ def __init__(self, plugin_name: str) -> None:
107
+ self._plugin_name = plugin_name
108
+ self._screens: list[PluginScreen] = []
109
+ self._commands: list[PluginCommand] = []
110
+ self._scanners: list[type[BaseScanner]] = []
111
+ self._passive_checks: list[type[BaseCheck]] = []
112
+
113
+ def register_screen(
114
+ self,
115
+ name: str,
116
+ widget_class: type,
117
+ hotkey: str | None = None,
118
+ ) -> None:
119
+ """Добавить экран в боковое меню TUI.
120
+
121
+ Args:
122
+ name: Название в меню.
123
+ widget_class: Класс Widget (не Screen!).
124
+ hotkey: Опциональная горячая клавиша, например "S+0".
125
+ """
126
+ self._screens.append(
127
+ PluginScreen(name=name, widget_class=widget_class,
128
+ hotkey=hotkey, plugin_name=self._plugin_name)
129
+ )
130
+ logger.debug("Plugin '%s': registered screen '%s'", self._plugin_name, name)
131
+
132
+ def register_cli_command(self, group_name: str, command: Any) -> None:
133
+ """Добавить click-команду в CLI-группу.
134
+
135
+ Args:
136
+ group_name: Имя группы ("proxy", "scan", ...).
137
+ command: Объект @click.command().
138
+ """
139
+ self._commands.append(
140
+ PluginCommand(group_name=group_name, command=command,
141
+ plugin_name=self._plugin_name)
142
+ )
143
+ logger.debug("Plugin '%s': registered CLI command '%s' → group '%s'",
144
+ self._plugin_name, command.name, group_name)
145
+
146
+ def register_scanner(self, scanner_class: type[BaseScanner]) -> None:
147
+ """Зарегистрировать активный сканер."""
148
+ self._scanners.append(scanner_class)
149
+ logger.debug("Plugin '%s': registered scanner '%s'",
150
+ self._plugin_name, scanner_class.name)
151
+
152
+ def register_passive_check(self, check_class: type[BaseCheck]) -> None:
153
+ """Зарегистрировать пассивную проверку (на каждый прокси-запрос)."""
154
+ self._passive_checks.append(check_class)
155
+ logger.debug("Plugin '%s': registered passive check '%s'",
156
+ self._plugin_name, check_class.name)
157
+
158
+
159
+ @dataclass
160
+ class PluginMeta:
161
+ """Метаданные о загруженном плагине."""
162
+ name: str
163
+ path: str
164
+ version: str = "?"
165
+ author: str = ""
166
+ description: str = ""
167
+ required_feature: str = "" # "" = free
168
+ loaded: bool = True # False = заблокирован лицензией
169
+
170
+
171
+ class PluginManager:
172
+ """Загружает и хранит все зарегистрированные плагины."""
173
+
174
+ def __init__(self) -> None:
175
+ self._screens: list[PluginScreen] = []
176
+ self._commands: list[PluginCommand] = []
177
+ self._scanners: list[type[BaseScanner]] = []
178
+ self._passive_checks: list[type[BaseCheck]] = []
179
+ self._loaded: list[str] = []
180
+ self._meta: list[PluginMeta] = []
181
+
182
+ # ── Загрузка ──────────────────────────────────────────────────────────────
183
+
184
+ def load_plugins(self, dirs: list[str], *, warn_untrusted: bool = True) -> None:
185
+ """Сканирует директории и загружает плагины.
186
+
187
+ Плагины с required_feature проверяются через get_session_license().
188
+ Если лицензия не покрывает feature — плагин пропускается с логом WARNING.
189
+
190
+ Args:
191
+ dirs: Список путей к директориям с плагинами.
192
+ warn_untrusted: Логировать предупреждение для нестандартных путей.
193
+ """
194
+ builtin_dir = str(
195
+ Path(__file__).parent.parent / "plugins" / "builtin"
196
+ )
197
+ for dir_path in dirs:
198
+ is_builtin = Path(dir_path).resolve() == Path(builtin_dir).resolve()
199
+ for py_file in sorted(Path(dir_path).glob("*.py")):
200
+ if py_file.name.startswith("_"):
201
+ continue
202
+ if warn_untrusted and not is_builtin:
203
+ logger.warning(
204
+ "Loading plugin from untrusted source: %s", py_file
205
+ )
206
+ self._load_file(py_file)
207
+
208
+ def load_user_plugins(self) -> None:
209
+ """Загрузить плагины из ~/.pentool/plugins/ (PRO-плагины пользователя)."""
210
+ if USER_PLUGINS_DIR.exists():
211
+ self.load_plugins([str(USER_PLUGINS_DIR)], warn_untrusted=False)
212
+ else:
213
+ logger.debug("User plugins dir not found: %s", USER_PLUGINS_DIR)
214
+
215
+ def _load_file(self, path: Path) -> None:
216
+ module_name = f"_pentool_plugin_{path.stem}"
217
+ try:
218
+ spec = importlib.util.spec_from_file_location(module_name, path)
219
+ if spec is None or spec.loader is None:
220
+ logger.warning("Cannot load plugin: %s", path)
221
+ return
222
+ module = importlib.util.module_from_spec(spec)
223
+ sys.modules[module_name] = module
224
+ spec.loader.exec_module(module) # type: ignore[union-attr]
225
+
226
+ if not hasattr(module, "register"):
227
+ logger.warning("Plugin has no register(): %s", path)
228
+ return
229
+
230
+ # Определяем имя и метаданные плагина
231
+ plugin_name = path.stem
232
+ plugin_cls = self._find_plugin_class(module)
233
+ version = getattr(plugin_cls, "version", "?") if plugin_cls else "?"
234
+ author = getattr(plugin_cls, "author", "") if plugin_cls else ""
235
+ description = getattr(plugin_cls, "description", "") if plugin_cls else ""
236
+ required_feature = getattr(plugin_cls, "required_feature", "") if plugin_cls else ""
237
+
238
+ # Проверка совместимости api_version
239
+ if plugin_cls is not None:
240
+ api_ver = getattr(plugin_cls, "api_version", 1)
241
+ if api_ver > CURRENT_API_VERSION:
242
+ logger.warning(
243
+ "Plugin '%s' requires api_version=%d, current=%d — skipped",
244
+ plugin_name, api_ver, CURRENT_API_VERSION,
245
+ )
246
+ self._meta.append(PluginMeta(
247
+ name=plugin_name, path=str(path),
248
+ version=version, author=author,
249
+ description=f"Requires Plugin API v{api_ver}",
250
+ required_feature=required_feature, loaded=False,
251
+ ))
252
+ return
253
+
254
+ # Проверка лицензии для PRO-плагинов
255
+ if required_feature:
256
+ if not self._check_license_feature(required_feature):
257
+ logger.warning(
258
+ "Plugin '%s' requires license feature '%s' — skipped (license not active)",
259
+ plugin_name, required_feature,
260
+ )
261
+ self._meta.append(PluginMeta(
262
+ name=plugin_name, path=str(path),
263
+ version=version, author=author,
264
+ description=description,
265
+ required_feature=required_feature, loaded=False,
266
+ ))
267
+ return
268
+
269
+ hook = PluginHook(plugin_name)
270
+ module.register(hook)
271
+
272
+ self._screens.extend(hook._screens)
273
+ self._commands.extend(hook._commands)
274
+ self._scanners.extend(hook._scanners)
275
+ self._passive_checks.extend(hook._passive_checks)
276
+ self._loaded.append(str(path))
277
+ self._meta.append(PluginMeta(
278
+ name=plugin_name, path=str(path),
279
+ version=version, author=author, description=description,
280
+ required_feature=required_feature, loaded=True,
281
+ ))
282
+ logger.info("Loaded plugin: %s", path.name)
283
+
284
+ except Exception as exc:
285
+ logger.error("Failed to load plugin %s: %s", path, exc)
286
+
287
+ @staticmethod
288
+ def _find_plugin_class(module: Any) -> type | None:
289
+ """Найти первый подкласс BasePlugin в модуле."""
290
+ for cls_name in dir(module):
291
+ cls = getattr(module, cls_name)
292
+ if (isinstance(cls, type)
293
+ and issubclass(cls, BasePlugin)
294
+ and cls is not BasePlugin
295
+ and cls is not BaseScanner):
296
+ return cls
297
+ return None
298
+
299
+ @staticmethod
300
+ def _check_license_feature(feature: str) -> bool:
301
+ """Проверить что текущая лицензия покрывает указанную feature."""
302
+ try:
303
+ from pentool.core.license import get_session_license
304
+ info = get_session_license()
305
+ return info.has_feature(feature)
306
+ except Exception:
307
+ return False
308
+
309
+ # ── Геттеры ───────────────────────────────────────────────────────────────
310
+
311
+ def get_screens(self) -> list[PluginScreen]:
312
+ return list(self._screens)
313
+
314
+ def get_commands(self) -> list[PluginCommand]:
315
+ return list(self._commands)
316
+
317
+ def get_scanners(self) -> list[type[BaseScanner]]:
318
+ return list(self._scanners)
319
+
320
+ def get_passive_checks(self) -> list[type[BaseCheck]]:
321
+ return list(self._passive_checks)
322
+
323
+ def loaded_plugins(self) -> list[str]:
324
+ return list(self._loaded)
325
+
326
+ def get_meta(self) -> list[PluginMeta]:
327
+ """Список метаданных всех плагинов (загруженных и заблокированных)."""
328
+ return list(self._meta)
329
+
330
+ def is_feature_available(self, feature: str) -> bool:
331
+ """Проверить доступность PRO-фичи через лицензию (удобный хелпер)."""
332
+ return self._check_license_feature(feature)
@@ -0,0 +1,37 @@
1
+ """Сохранение/загрузка проекта в JSON."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+
11
+ def save_project(path: str | Path, project_data: dict[str, Any]) -> None:
12
+ """Сохранить проект в JSON."""
13
+ path = Path(path)
14
+ path.parent.mkdir(parents=True, exist_ok=True)
15
+ data = {
16
+ "saved_at": datetime.now(timezone.utc).isoformat(),
17
+ **project_data,
18
+ }
19
+ with open(path, "w", encoding="utf-8") as f:
20
+ json.dump(data, f, ensure_ascii=False, indent=2)
21
+
22
+
23
+ def load_project(path: str | Path) -> tuple[dict[str, Any], str]:
24
+ """Загрузить проект из JSON.
25
+
26
+ Returns:
27
+ (data, error_message) — пустой dict при ошибке, пустая строка если OK.
28
+ """
29
+ path = Path(path)
30
+ if not path.exists():
31
+ return {}, f"File not found: {path}"
32
+ try:
33
+ with open(path, "r", encoding="utf-8") as f:
34
+ data = json.load(f)
35
+ except Exception as e:
36
+ return {}, f"JSON parse error: {e}"
37
+ return data, ""