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
pentool/core/features.py
ADDED
|
@@ -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
|
+
}
|
pentool/core/license.py
ADDED
|
@@ -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
|