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,410 @@
1
+ """Feature flags и лимиты для лицензионной модели Pentool.
2
+
3
+ Определяет доступность функций и лимиты для каждого плана (Free/Lite/Medium/Full).
4
+ """
5
+
6
+ from enum import Enum
7
+ from dataclasses import dataclass
8
+
9
+
10
+ class FeatureStatus(str, Enum):
11
+ """Статус фичи."""
12
+ STABLE = "stable" # Стабильная, готова к production
13
+ BETA = "beta" # Стабильная, но в beta-тестировании
14
+ ALPHA = "alpha" # Экспериментальная, может быть нестабильной
15
+
16
+
17
+ @dataclass
18
+ class Feature:
19
+ """Описание одной фичи."""
20
+ name: str
21
+ description: str
22
+ status: FeatureStatus
23
+ required_plan: str # "free", "lite", "medium", "full"
24
+
25
+
26
+ # ═══════════════════════════════════════════════════════════════════════
27
+ # FEATURE CONSTANTS — строковые идентификаторы для require_feature
28
+ # ═══════════════════════════════════════════════════════════════════════
29
+
30
+ # Scanner PRO checks
31
+ FEATURE_SCANNER_ADVANCED_WAF = "scanner_advanced_waf"
32
+ FEATURE_SCANNER_OOB = "scanner_oob"
33
+ FEATURE_SCANNER_SSRF_PORTSCAN = "scanner_ssrf_portscan"
34
+ FEATURE_SCANNER_SQLI_UNION = "scanner_sqli_union"
35
+ FEATURE_SCANNER_SQLI_OOB = "scanner_sqli_oob"
36
+ FEATURE_SCANNER_LFI_LOG_POISON = "scanner_lfi_log_poison"
37
+ FEATURE_SCANNER_XSS_STORED = "scanner_xss_stored"
38
+ FEATURE_SCANNER_DOM_XSS_ACTIVE = "scanner_dom_xss_active"
39
+ FEATURE_SCANNER_DESER = "scanner_deser"
40
+ FEATURE_SCANNER_SMUGGLING = "scanner_smuggling"
41
+ FEATURE_SCANNER_FILE_UPLOAD = "scanner_file_upload"
42
+ FEATURE_SCANNER_IDOR = "scanner_idor"
43
+ FEATURE_SCANNER_PROMPT_INJECT = "scanner_prompt_inject"
44
+ FEATURE_SCANNER_RCE_OOB = "scanner_rce_oob"
45
+
46
+ # AI features
47
+ FEATURE_AI_ANALYSIS = "ai_analysis"
48
+ FEATURE_AI_STRATEGY = "ai_strategy"
49
+
50
+ # Reports
51
+ FEATURE_REPORTS_PRO = "pro_reports"
52
+
53
+
54
+ # ═══════════════════════════════════════════════════════════════════════
55
+ # FEATURES — доступность фич по планам
56
+ # ═══════════════════════════════════════════════════════════════════════
57
+
58
+ FEATURES = {
59
+ # ────────────────────────────────────────────────────────────────────
60
+ # FREE PLAN — базовые инструменты
61
+ # ────────────────────────────────────────────────────────────────────
62
+ "proxy": Feature(
63
+ "proxy",
64
+ "HTTP/HTTPS Proxy с перехватом",
65
+ FeatureStatus.STABLE,
66
+ "free"
67
+ ),
68
+ "repeater": Feature(
69
+ "repeater",
70
+ "Repeater — повтор и модификация запросов",
71
+ FeatureStatus.STABLE,
72
+ "free"
73
+ ),
74
+ "intruder_basic": Feature(
75
+ "intruder_basic",
76
+ "Intruder (Sniper, Battering Ram)",
77
+ FeatureStatus.BETA,
78
+ "free"
79
+ ),
80
+ "decoder": Feature(
81
+ "decoder",
82
+ "Decoder — кодирование/декодирование данных",
83
+ FeatureStatus.STABLE,
84
+ "free"
85
+ ),
86
+ "comparer": Feature(
87
+ "comparer",
88
+ "Comparer — сравнение запросов/ответов",
89
+ FeatureStatus.STABLE,
90
+ "free"
91
+ ),
92
+ "httpql": Feature(
93
+ "httpql",
94
+ "HTTPQL — фильтрация истории запросов",
95
+ FeatureStatus.BETA,
96
+ "free"
97
+ ),
98
+
99
+ # ────────────────────────────────────────────────────────────────────
100
+ # LITE PLAN — расширенные возможности
101
+ # ────────────────────────────────────────────────────────────────────
102
+ "scanner_extended": Feature(
103
+ "scanner_extended",
104
+ "Scanner — расширенные проверки безопасности",
105
+ FeatureStatus.ALPHA,
106
+ "lite"
107
+ ),
108
+ "intruder_all_types": Feature(
109
+ "intruder_all_types",
110
+ "Intruder (Pitchfork, Cluster Bomb)",
111
+ FeatureStatus.ALPHA,
112
+ "lite"
113
+ ),
114
+ "spider": Feature(
115
+ "spider",
116
+ "Spider — автоматическое сканирование сайта",
117
+ FeatureStatus.BETA,
118
+ "lite"
119
+ ),
120
+ "match_replace": Feature(
121
+ "match_replace",
122
+ "Match & Replace — авто-модификация запросов",
123
+ FeatureStatus.BETA,
124
+ "lite"
125
+ ),
126
+ "target_scope": Feature(
127
+ "target_scope",
128
+ "Target Scope — управление областью тестирования",
129
+ FeatureStatus.STABLE,
130
+ "lite"
131
+ ),
132
+
133
+ # ────────────────────────────────────────────────────────────────────
134
+ # MEDIUM PLAN — профессиональные инструменты
135
+ # ────────────────────────────────────────────────────────────────────
136
+ "oob_detection": Feature(
137
+ "oob_detection",
138
+ "Out-of-Band детекция уязвимостей",
139
+ FeatureStatus.ALPHA,
140
+ "medium"
141
+ ),
142
+ "websocket_intercept": Feature(
143
+ "websocket_intercept",
144
+ "WebSocket перехват и модификация",
145
+ FeatureStatus.ALPHA,
146
+ "medium"
147
+ ),
148
+ "plugins": Feature(
149
+ "plugins",
150
+ "Система плагинов",
151
+ FeatureStatus.ALPHA,
152
+ "medium"
153
+ ),
154
+ "sequencer": Feature(
155
+ "sequencer",
156
+ "Sequencer — анализ случайности токенов",
157
+ FeatureStatus.BETA,
158
+ "medium"
159
+ ),
160
+ "passive_scanner": Feature(
161
+ "passive_scanner",
162
+ "Passive Scanner — фоновый анализ трафика",
163
+ FeatureStatus.BETA,
164
+ "medium"
165
+ ),
166
+ "turbo_mode": Feature(
167
+ "turbo_mode",
168
+ "Turbo Mode Intruder (10x ускорение)",
169
+ FeatureStatus.BETA,
170
+ "medium"
171
+ ),
172
+
173
+ # ────────────────────────────────────────────────────────────────────
174
+ # FULL PLAN — enterprise возможности
175
+ # ────────────────────────────────────────────────────────────────────
176
+ "ai_analysis": Feature(
177
+ "ai_analysis",
178
+ "AI-анализ уязвимостей (GPT-4)",
179
+ FeatureStatus.ALPHA,
180
+ "full"
181
+ ),
182
+ "pro_reports": Feature(
183
+ "pro_reports",
184
+ "PRO отчёты (HTML/PDF/JSON)",
185
+ FeatureStatus.ALPHA,
186
+ "full"
187
+ ),
188
+ "collaboration": Feature(
189
+ "collaboration",
190
+ "Совместная работа над проектами",
191
+ FeatureStatus.ALPHA,
192
+ "full"
193
+ ),
194
+ "api_access": Feature(
195
+ "api_access",
196
+ "REST API для автоматизации",
197
+ FeatureStatus.BETA,
198
+ "full"
199
+ ),
200
+ "custom_scanner_checks": Feature(
201
+ "custom_scanner_checks",
202
+ "Кастомные проверки Scanner",
203
+ FeatureStatus.ALPHA,
204
+ "full"
205
+ ),
206
+ }
207
+
208
+
209
+ # ═══════════════════════════════════════════════════════════════════════
210
+ # LIMITS — лимиты по планам
211
+ # ═══════════════════════════════════════════════════════════════════════
212
+
213
+ LIMITS = {
214
+ # История запросов
215
+ "history_max_entries": {
216
+ "free": 500,
217
+ "lite": 5000,
218
+ "medium": 50000,
219
+ "full": -1, # unlimited
220
+ },
221
+
222
+ # Intruder
223
+ "intruder_max_threads": {
224
+ "free": 10,
225
+ "lite": 20,
226
+ "medium": 50,
227
+ "full": 100,
228
+ },
229
+ "intruder_max_requests": {
230
+ "free": 1000,
231
+ "lite": 10000,
232
+ "medium": 100000,
233
+ "full": -1, # unlimited
234
+ },
235
+
236
+ # Scanner
237
+ "scanner_max_depth": {
238
+ "free": 2,
239
+ "lite": 5,
240
+ "medium": 10,
241
+ "full": -1, # unlimited
242
+ },
243
+ "scanner_max_pages": {
244
+ "free": 20,
245
+ "lite": 100,
246
+ "medium": 1000,
247
+ "full": -1, # unlimited
248
+ },
249
+ "scanner_threads": {
250
+ "free": 5,
251
+ "lite": 10,
252
+ "medium": 20,
253
+ "full": 50,
254
+ },
255
+
256
+ # Spider
257
+ "spider_max_depth": {
258
+ "free": 2,
259
+ "lite": 5,
260
+ "medium": 10,
261
+ "full": -1, # unlimited
262
+ },
263
+ "spider_max_pages": {
264
+ "free": 50,
265
+ "lite": 200,
266
+ "medium": 1000,
267
+ "full": -1, # unlimited
268
+ },
269
+
270
+ # Проекты
271
+ "projects_max": {
272
+ "free": 5,
273
+ "lite": 20,
274
+ "medium": 100,
275
+ "full": -1, # unlimited
276
+ },
277
+
278
+ # Export
279
+ "export_formats": {
280
+ "free": ["txt"],
281
+ "lite": ["txt", "csv", "json"],
282
+ "medium": ["txt", "csv", "json", "html"],
283
+ "full": ["txt", "csv", "json", "html", "pdf"],
284
+ },
285
+ }
286
+
287
+
288
+ # ═══════════════════════════════════════════════════════════════════════
289
+ # PLAN NAMES — человекочитаемые названия планов
290
+ # ═══════════════════════════════════════════════════════════════════════
291
+
292
+ PLAN_NAMES = {
293
+ "free": "Free",
294
+ "lite": "Lite",
295
+ "medium": "Medium",
296
+ "full": "Full (PRO)",
297
+ }
298
+
299
+
300
+ PLAN_DESCRIPTIONS = {
301
+ "free": "Базовые инструменты для тестирования",
302
+ "lite": "Расширенные возможности для профессионалов",
303
+ "medium": "Продвинутые инструменты + плагины",
304
+ "full": "Enterprise функции + AI + API",
305
+ }
306
+
307
+
308
+ # ═══════════════════════════════════════════════════════════════════════
309
+ # HELPER FUNCTIONS
310
+ # ═══════════════════════════════════════════════════════════════════════
311
+
312
+ def has_feature(feature_name: str, plan: str) -> bool:
313
+ """Проверить, доступна ли фича для указанного плана.
314
+
315
+ Args:
316
+ feature_name: Название фичи (ключ из FEATURES)
317
+ plan: Название плана ("free", "lite", "medium", "full")
318
+
319
+ Returns:
320
+ True если фича доступна для этого плана
321
+ """
322
+ feature = FEATURES.get(feature_name)
323
+ if not feature:
324
+ return False
325
+
326
+ # Порядок планов от младшего к старшему
327
+ plan_order = ["free", "lite", "medium", "full"]
328
+
329
+ # Если текущий план >= требуемого плана, фича доступна
330
+ try:
331
+ current_level = plan_order.index(plan.lower())
332
+ required_level = plan_order.index(feature.required_plan)
333
+ return current_level >= required_level
334
+ except (ValueError, AttributeError):
335
+ return False
336
+
337
+
338
+ def get_limit(limit_name: str, plan: str, default: int = 0) -> int | list:
339
+ """Получить лимит для указанного плана.
340
+
341
+ Args:
342
+ limit_name: Название лимита (ключ из LIMITS)
343
+ plan: Название плана
344
+ default: Значение по умолчанию если лимит не найден
345
+
346
+ Returns:
347
+ Значение лимита (int или list для export_formats)
348
+ """
349
+ limits = LIMITS.get(limit_name, {})
350
+ return limits.get(plan.lower(), default)
351
+
352
+
353
+ def get_feature_status(feature_name: str) -> str:
354
+ """Получить статус фичи (stable/beta/alpha).
355
+
356
+ Args:
357
+ feature_name: Название фичи
358
+
359
+ Returns:
360
+ Статус фичи в виде строки
361
+ """
362
+ feature = FEATURES.get(feature_name)
363
+ return feature.status.value if feature else "unknown"
364
+
365
+
366
+ def get_features_for_plan(plan: str) -> list[Feature]:
367
+ """Получить список всех фич доступных для плана.
368
+
369
+ Args:
370
+ plan: Название плана
371
+
372
+ Returns:
373
+ Список Feature объектов
374
+ """
375
+ return [
376
+ feature
377
+ for feature in FEATURES.values()
378
+ if has_feature(feature.name, plan)
379
+ ]
380
+
381
+
382
+ def get_plan_info(plan: str) -> dict:
383
+ """Получить полную информацию о плане.
384
+
385
+ Args:
386
+ plan: Название плана
387
+
388
+ Returns:
389
+ Словарь с информацией о плане
390
+ """
391
+ features = get_features_for_plan(plan)
392
+
393
+ return {
394
+ "plan": plan,
395
+ "name": PLAN_NAMES.get(plan, plan.capitalize()),
396
+ "description": PLAN_DESCRIPTIONS.get(plan, ""),
397
+ "features_count": len(features),
398
+ "features": [
399
+ {
400
+ "name": f.name,
401
+ "description": f.description,
402
+ "status": f.status.value,
403
+ }
404
+ for f in features
405
+ ],
406
+ "limits": {
407
+ limit_name: get_limit(limit_name, plan)
408
+ for limit_name in LIMITS.keys()
409
+ },
410
+ }
@@ -0,0 +1,294 @@
1
+ """Система лицензирования Pentool.
2
+
3
+ - FREE: встроенные функции (Proxy, Repeater, Intruder, Decoder, Target)
4
+ - PRO: дополнительные плагины (PRO Scanner, PRO Reports, PRO Payloads)
5
+ - Лицензия проверяется онлайн при активации, кэшируется локально
6
+ - Grace period: 7 дней оффлайн без перепроверки
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import hashlib
12
+ import json
13
+ import platform
14
+ import time
15
+ import uuid
16
+ from dataclasses import dataclass, field
17
+
18
+ from pathlib import Path
19
+
20
+
21
+ _LICENSE_FILE = Path.home() / ".pentool" / "license.dat"
22
+ _GRACE_PERIOD_DAYS = 7
23
+
24
+
25
+ @dataclass
26
+ class LicenseInfo:
27
+ """Информация о текущей лицензии."""
28
+
29
+ valid: bool = False
30
+ plan: str = "free" # "free" | "pro" | "enterprise"
31
+ features: list[str] = field(default_factory=list)
32
+ expires: str | None = None # ISO date или None (permanent)
33
+ machine_id: str = ""
34
+ license_key: str = ""
35
+ last_check: float | None = None # timestamp последней онлайн-проверки
36
+ error: str = "" # сообщение об ошибке если not valid
37
+
38
+ def has_feature(self, feature: str) -> bool:
39
+ """Проверить наличие конкретной функции."""
40
+ return self.valid and feature in self.features
41
+
42
+ def is_pro(self) -> bool:
43
+ return self.valid and self.plan in ("pro", "enterprise")
44
+
45
+ @property
46
+ def status_text(self) -> str:
47
+ if not self.valid:
48
+ return "FREE"
49
+ return self.plan.upper()
50
+
51
+ @property
52
+ def expires_text(self) -> str:
53
+ if not self.expires:
54
+ return "Lifetime"
55
+ return self.expires
56
+
57
+
58
+ def get_machine_id() -> str:
59
+ """Вернуть уникальный ID машины (sha256 hostname + MAC)."""
60
+ try:
61
+ import socket
62
+ hostname = socket.gethostname()
63
+ # Получить MAC первого интерфейса через uuid
64
+ mac = hex(uuid.getnode())[2:]
65
+ raw = f"{hostname}:{mac}"
66
+ return hashlib.sha256(raw.encode()).hexdigest()[:32]
67
+ except Exception:
68
+ return hashlib.sha256(platform.node().encode()).hexdigest()[:32]
69
+
70
+
71
+ def _load_cached() -> dict | None:
72
+ """Загрузить кэшированные данные лицензии из файла."""
73
+ try:
74
+ if _LICENSE_FILE.exists():
75
+ data = json.loads(_LICENSE_FILE.read_text(encoding="utf-8"))
76
+ return data
77
+ except Exception:
78
+ pass
79
+ return None
80
+
81
+
82
+ def _save_cached(data: dict) -> None:
83
+ """Сохранить данные лицензии в файл."""
84
+ try:
85
+ _LICENSE_FILE.parent.mkdir(parents=True, exist_ok=True)
86
+ _LICENSE_FILE.write_text(
87
+ json.dumps(data, indent=2, ensure_ascii=False),
88
+ encoding="utf-8",
89
+ )
90
+ except Exception:
91
+ pass
92
+
93
+
94
+ def get_license() -> LicenseInfo:
95
+ """Получить текущую информацию о лицензии.
96
+
97
+ 1. Читает кэш из ~/.pentool/license.dat
98
+ 2. Если кэш валиден и не истёк grace period — возвращает его
99
+ 3. Иначе — возвращает FREE
100
+
101
+ Для реальной проверки — использовать activate_license().
102
+ """
103
+ cached = _load_cached()
104
+ if cached is None:
105
+ return LicenseInfo(valid=False, plan="free", machine_id=get_machine_id())
106
+
107
+ last_check = cached.get("last_check", 0)
108
+ grace_seconds = _GRACE_PERIOD_DAYS * 24 * 3600
109
+ if (time.time() - last_check) > grace_seconds:
110
+ # Grace period истёк — деактивируем
111
+ return LicenseInfo(
112
+ valid=False,
113
+ plan="free",
114
+ machine_id=get_machine_id(),
115
+ license_key=cached.get("license_key", ""),
116
+ error=f"Grace period expired. Please reconnect to validate license.",
117
+ )
118
+
119
+ return LicenseInfo(
120
+ valid=cached.get("valid", False),
121
+ plan=cached.get("plan", "free"),
122
+ features=cached.get("features", []),
123
+ expires=cached.get("expires"),
124
+ machine_id=cached.get("machine_id", get_machine_id()),
125
+ license_key=cached.get("license_key", ""),
126
+ last_check=last_check,
127
+ )
128
+
129
+
130
+ # Session-level license cache (avoids repeated file reads per check)
131
+ _session_license: "LicenseInfo | None" = None
132
+
133
+
134
+ def get_session_license() -> "LicenseInfo":
135
+ """Return cached session license (refreshed once per process)."""
136
+ global _session_license
137
+ if _session_license is None:
138
+ _session_license = get_license()
139
+ return _session_license
140
+
141
+
142
+ def invalidate_session_license() -> None:
143
+ """Force re-read on next get_session_license() call."""
144
+ global _session_license
145
+ _session_license = None
146
+
147
+
148
+ class FeatureNotAvailable(Exception):
149
+ """Feature not available in the current plan."""
150
+ def __init__(self, feature: str, plan_required: str = "pro"):
151
+ self.feature = feature
152
+ self.plan_required = plan_required
153
+ super().__init__(
154
+ f"Feature '{feature}' requires {plan_required.upper()} plan. "
155
+ f"Upgrade at https://pentool.dev/upgrade"
156
+ )
157
+
158
+
159
+ def require_feature(feature: str, plan_required: str = "pro"):
160
+ """Decorator: raise FeatureNotAvailable if feature is not licensed."""
161
+ def decorator(func):
162
+ import functools
163
+ import asyncio as _asyncio
164
+
165
+ @functools.wraps(func)
166
+ async def async_wrapper(*args, **kwargs):
167
+ if not get_session_license().has_feature(feature):
168
+ raise FeatureNotAvailable(feature, plan_required)
169
+ return await func(*args, **kwargs)
170
+
171
+ @functools.wraps(func)
172
+ def sync_wrapper(*args, **kwargs):
173
+ if not get_session_license().has_feature(feature):
174
+ raise FeatureNotAvailable(feature, plan_required)
175
+ return func(*args, **kwargs)
176
+
177
+ if _asyncio.iscoroutinefunction(func):
178
+ return async_wrapper
179
+ return sync_wrapper
180
+ return decorator
181
+
182
+
183
+ async def activate_license(key: str) -> LicenseInfo:
184
+ """Активировать лицензию онлайн (license.pentool.dev/api/validate).
185
+
186
+ Returns:
187
+ LicenseInfo с результатом активации.
188
+ """
189
+ key = key.strip().upper()
190
+ machine_id = get_machine_id()
191
+
192
+ if not key:
193
+ return LicenseInfo(
194
+ valid=False, plan="free",
195
+ machine_id=machine_id,
196
+ error="License key is empty",
197
+ )
198
+
199
+ # Попытка реальной онлайн-проверки
200
+ try:
201
+ import aiohttp
202
+ async with aiohttp.ClientSession(
203
+ timeout=aiohttp.ClientTimeout(total=10)
204
+ ) as session:
205
+ async with session.post(
206
+ "https://license.pentool.dev/api/validate",
207
+ json={"key": key, "machine_id": machine_id},
208
+ ssl=False,
209
+ ) as resp:
210
+ if resp.status == 200:
211
+ data = await resp.json()
212
+ info = LicenseInfo(
213
+ valid=data.get("valid", False),
214
+ plan=data.get("plan", "free"),
215
+ features=data.get("features", []),
216
+ expires=data.get("expires"),
217
+ machine_id=machine_id,
218
+ license_key=key,
219
+ last_check=time.time(),
220
+ )
221
+ if info.valid:
222
+ _save_cached({
223
+ "valid": True,
224
+ "plan": info.plan,
225
+ "features": info.features,
226
+ "expires": info.expires,
227
+ "machine_id": machine_id,
228
+ "license_key": key,
229
+ "last_check": time.time(),
230
+ })
231
+ return info
232
+ except Exception:
233
+ pass # Сервер недоступен — fallback к локальной проверке
234
+
235
+ # Оффлайн-fallback: проверяем формат ключа
236
+ # Формат: XXXX-XXXX-XXXX-XXXX (16 hex символов через дефисы)
237
+ import re
238
+ if re.match(r'^[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$', key):
239
+ if key.startswith("DEMO"):
240
+ info = LicenseInfo(
241
+ valid=True,
242
+ plan="pro",
243
+ features=["scanner_pro", "reports_pro", "payloads_pro"],
244
+ expires=None,
245
+ machine_id=machine_id,
246
+ license_key=key,
247
+ last_check=time.time(),
248
+ )
249
+ _save_cached({
250
+ "valid": True,
251
+ "plan": "pro",
252
+ "features": ["scanner_pro", "reports_pro", "payloads_pro"],
253
+ "expires": None,
254
+ "machine_id": machine_id,
255
+ "license_key": key,
256
+ "last_check": time.time(),
257
+ })
258
+ return info
259
+
260
+ return LicenseInfo(
261
+ valid=False,
262
+ plan="free",
263
+ machine_id=machine_id,
264
+ license_key=key,
265
+ error="License server unavailable. Key format invalid or not activated.",
266
+ )
267
+
268
+
269
+ def deactivate_license() -> None:
270
+ """Деактивировать лицензию (удалить кэш)."""
271
+ try:
272
+ if _LICENSE_FILE.exists():
273
+ _LICENSE_FILE.unlink()
274
+ except Exception:
275
+ pass
276
+
277
+
278
+ # Глобальный кэш лицензии для текущей сессии
279
+ _session_license: LicenseInfo | None = None
280
+
281
+
282
+ def get_session_license() -> LicenseInfo:
283
+ """Получить лицензию текущей сессии (с кэшированием в памяти)."""
284
+ global _session_license
285
+ if _session_license is None:
286
+ _session_license = get_license()
287
+ return _session_license
288
+
289
+
290
+ def refresh_session_license(info: LicenseInfo | None = None) -> LicenseInfo:
291
+ """Обновить кэш лицензии сессии."""
292
+ global _session_license
293
+ _session_license = info or get_license()
294
+ return _session_license