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