pentool 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. pentool/__init__.py +4 -0
  2. pentool/__main__.py +16 -0
  3. pentool/api/__init__.py +41 -0
  4. pentool/api/base_api.py +48 -0
  5. pentool/api/comparer_api.py +19 -0
  6. pentool/api/decoder_api.py +24 -0
  7. pentool/api/intruder_api.py +171 -0
  8. pentool/api/proxy_api.py +210 -0
  9. pentool/api/repeater_api.py +57 -0
  10. pentool/api/scanner_api.py +266 -0
  11. pentool/api/sequencer_api.py +17 -0
  12. pentool/api/spider_api.py +106 -0
  13. pentool/api/target_api.py +87 -0
  14. pentool/cli/__init__.py +1 -0
  15. pentool/cli/main.py +98 -0
  16. pentool/cli/project.py +88 -0
  17. pentool/cli/proxy.py +173 -0
  18. pentool/cli/scan.py +115 -0
  19. pentool/core/__init__.py +1 -0
  20. pentool/core/config.py +171 -0
  21. pentool/core/crash_reporter.py +75 -0
  22. pentool/core/database.py +139 -0
  23. pentool/core/event_bus.py +207 -0
  24. pentool/core/events.py +160 -0
  25. pentool/core/features.py +373 -0
  26. pentool/core/license.py +274 -0
  27. pentool/core/logging.py +54 -0
  28. pentool/core/plugin_manager.py +312 -0
  29. pentool/core/project.py +31 -0
  30. pentool/core/storage_interface.py +307 -0
  31. pentool/core/updater.py +96 -0
  32. pentool/core/utils.py +36 -0
  33. pentool/modules/__init__.py +1 -0
  34. pentool/modules/comparer.py +177 -0
  35. pentool/modules/decoder.py +281 -0
  36. pentool/modules/intruder.py +428 -0
  37. pentool/modules/intruder_turbo.py +176 -0
  38. pentool/modules/match_replace.py +142 -0
  39. pentool/modules/proxy.py +797 -0
  40. pentool/modules/repeater.py +195 -0
  41. pentool/modules/sequencer.py +492 -0
  42. pentool/modules/spider.py +679 -0
  43. pentool/modules/target.py +185 -0
  44. pentool/modules/websocket_handler.py +249 -0
  45. pentool/plugins/__init__.py +1 -0
  46. pentool/plugins/builtin/__init__.py +1 -0
  47. pentool/plugins/builtin/payloads_pro.py +381 -0
  48. pentool/plugins/builtin/reports_pro.py +436 -0
  49. pentool/plugins/builtin/scanner_pro.py +291 -0
  50. pentool/plugins/example_plugin.py +39 -0
  51. pentool/services/__init__.py +1 -0
  52. pentool/services/base_service.py +60 -0
  53. pentool/services/intruder_service.py +94 -0
  54. pentool/services/proxy_service.py +178 -0
  55. pentool/services/repeater_service.py +85 -0
  56. pentool/services/scan_service.py +330 -0
  57. pentool/storage/__init__.py +0 -0
  58. pentool/storage/http_storage.py +506 -0
  59. pentool/storage/large_body_handler.py +50 -0
  60. pentool/storage/lru_cache.py +45 -0
  61. pentool/t.py +213 -0
  62. pentool/tui/__init__.py +1 -0
  63. pentool/tui/app.py +1429 -0
  64. pentool/tui/constants.py +79 -0
  65. pentool/tui/dialogs/__init__.py +0 -0
  66. pentool/tui/dialogs/cert_dialog.py +81 -0
  67. pentool/tui/dialogs/file_selector.py +226 -0
  68. pentool/tui/dialogs/load_from_proxy.py +125 -0
  69. pentool/tui/dialogs/match_replace_dialog.py +229 -0
  70. pentool/tui/dialogs/project_dialog.py +82 -0
  71. pentool/tui/dialogs/scope_dialog.py +224 -0
  72. pentool/tui/messages.py +103 -0
  73. pentool/tui/mixins/__init__.py +0 -0
  74. pentool/tui/mixins/app_mixin.py +59 -0
  75. pentool/tui/mixins/dialog_cancel.py +21 -0
  76. pentool/tui/mixins/request_context_menu.py +247 -0
  77. pentool/tui/mixins/tab_rename.py +89 -0
  78. pentool/tui/screens/__init__.py +31 -0
  79. pentool/tui/screens/comparer/__init__.py +3 -0
  80. pentool/tui/screens/comparer/screen.py +229 -0
  81. pentool/tui/screens/dashboard/__init__.py +2 -0
  82. pentool/tui/screens/dashboard/live_dashboard.py +730 -0
  83. pentool/tui/screens/dashboard/screen.py +771 -0
  84. pentool/tui/screens/decoder/__init__.py +3 -0
  85. pentool/tui/screens/decoder/screen.py +306 -0
  86. pentool/tui/screens/extensions/__init__.py +3 -0
  87. pentool/tui/screens/extensions/screen.py +24 -0
  88. pentool/tui/screens/intruder/__init__.py +3 -0
  89. pentool/tui/screens/intruder/screen.py +1356 -0
  90. pentool/tui/screens/proxy/__init__.py +3 -0
  91. pentool/tui/screens/proxy/screen.py +1554 -0
  92. pentool/tui/screens/repeater/__init__.py +3 -0
  93. pentool/tui/screens/repeater/screen.py +621 -0
  94. pentool/tui/screens/scanner/__init__.py +3 -0
  95. pentool/tui/screens/scanner/screen.py +1956 -0
  96. pentool/tui/screens/sequencer/__init__.py +3 -0
  97. pentool/tui/screens/sequencer/screen.py +516 -0
  98. pentool/tui/screens/settings/__init__.py +5 -0
  99. pentool/tui/screens/settings/hotkeys.py +134 -0
  100. pentool/tui/screens/settings/screen.py +540 -0
  101. pentool/tui/screens/spider/__init__.py +3 -0
  102. pentool/tui/screens/spider/screen.py +380 -0
  103. pentool/tui/screens/target/__init__.py +4 -0
  104. pentool/tui/screens/target/screen.py +358 -0
  105. pentool/tui/screens/terminal/__init__.py +5 -0
  106. pentool/tui/screens/terminal/screen.py +179 -0
  107. pentool/tui/widgets/__init__.py +1 -0
  108. pentool/tui/widgets/context_menu.py +148 -0
  109. pentool/tui/widgets/filter_bar.py +204 -0
  110. pentool/tui/widgets/inspector_panel.py +111 -0
  111. pentool/tui/widgets/menu.py +86 -0
  112. pentool/tui/widgets/menu_bar.py +238 -0
  113. pentool/tui/widgets/module_tabs.py +68 -0
  114. pentool/tui/widgets/payload_drop_zone.py +65 -0
  115. pentool/tui/widgets/request_editor.py +495 -0
  116. pentool/tui/widgets/resize_handle.py +116 -0
  117. pentool/tui/widgets/search_bar.py +112 -0
  118. pentool/tui/widgets/statusbar.py +96 -0
  119. pentool/tui/widgets/toolbar_button.py +68 -0
  120. pentool/utils/__init__.py +1 -0
  121. pentool/utils/cert.py +314 -0
  122. pentool/utils/coder.py +170 -0
  123. pentool/utils/copy_as.py +250 -0
  124. pentool/utils/diff.py +82 -0
  125. pentool/utils/http_client.py +145 -0
  126. pentool/utils/parser.py +227 -0
  127. pentool/utils/terminal_check.py +31 -0
  128. pentool-0.1.0.dist-info/METADATA +231 -0
  129. pentool-0.1.0.dist-info/RECORD +133 -0
  130. pentool-0.1.0.dist-info/WHEEL +5 -0
  131. pentool-0.1.0.dist-info/entry_points.txt +2 -0
  132. pentool-0.1.0.dist-info/licenses/LICENSE +34 -0
  133. pentool-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,266 @@
1
+ """ScannerAPI — публичный интерфейс Scanner для TUI и CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Callable
7
+
8
+ from pentool.core.utils import run_async_sync
9
+
10
+ from pentool.core.logging import get_logger
11
+ from pentool.modules.scanner.base import BaseCheck, Finding
12
+ from pentool.modules.scanner.checks import (
13
+ InfoLeakCheck,
14
+ LFICheck,
15
+ MissingSecurityHeadersCheck,
16
+ OpenRedirectCheck,
17
+ PathTraversalCheck,
18
+ HeaderInjectionCheck,
19
+ RCECheck,
20
+ SQLiCheck,
21
+ SSTICheck,
22
+ SSRFCheck,
23
+ XSSCheck,
24
+ XXECheck,
25
+ CORSCheck,
26
+ BrokenAuthCheck,
27
+ JWTNoneCheck,
28
+ NoSQLInjectionCheck,
29
+ GraphQLCheck,
30
+ PrototypePollutionCheck,
31
+ DOMXSSCheck,
32
+ OAuthCheck,
33
+ SensitiveDataCheck,
34
+ )
35
+ from pentool.modules.scanner.checks.header_injection import HostHeaderInjectionCheck
36
+ from pentool.modules.scanner.checks.sqli import SQLiUnionCheck
37
+ from pentool.modules.scanner.engine import ScanEngine
38
+ from pentool.modules.scanner.passive import PassiveScanner
39
+ from pentool.modules.scanner.report import (
40
+ generate_csv_report,
41
+ generate_html_report,
42
+ generate_json_report,
43
+ )
44
+
45
+ logger = get_logger(__name__)
46
+
47
+ __all__ = ["ScannerAPI", "Finding", "BaseCheck"]
48
+
49
+
50
+ class ScannerAPI:
51
+
52
+ def __init__(self, db_path: str = "", http_client=None) -> None:
53
+ self._db_path = db_path
54
+ self._http_client = http_client
55
+ self._engine: ScanEngine | None = None
56
+ self._passive: PassiveScanner | None = None
57
+ self._active_task: asyncio.Task | None = None
58
+ self._scan_id: str | None = None
59
+
60
+ def _get_engine(self) -> ScanEngine:
61
+ if self._engine is None:
62
+ self._engine = ScanEngine(
63
+ db_path=self._db_path,
64
+ http_client=self._http_client,
65
+ )
66
+ self._engine.register_checks([
67
+ MissingSecurityHeadersCheck(),
68
+ InfoLeakCheck(),
69
+ SQLiCheck(),
70
+ XSSCheck(),
71
+ SSTICheck(),
72
+ LFICheck(),
73
+ PathTraversalCheck(),
74
+ HeaderInjectionCheck(),
75
+ HostHeaderInjectionCheck(),
76
+ RCECheck(),
77
+ OpenRedirectCheck(),
78
+ SSRFCheck(),
79
+ XXECheck(),
80
+ CORSCheck(),
81
+ BrokenAuthCheck(),
82
+ JWTNoneCheck(),
83
+ NoSQLInjectionCheck(),
84
+ GraphQLCheck(),
85
+ PrototypePollutionCheck(),
86
+ DOMXSSCheck(),
87
+ OAuthCheck(),
88
+ SensitiveDataCheck(),
89
+ SQLiUnionCheck(),
90
+ ])
91
+ return self._engine
92
+
93
+ async def start_active_scan(
94
+ self,
95
+ targets: list[str],
96
+ check_names: list[str] | None = None,
97
+ on_finding: Callable[[Finding | None, None]] = None,
98
+ on_progress: Callable[[int, int | None, None]] = None,
99
+ on_request: Callable[[str, str | None, None]] = None,
100
+ concurrency: int = 5,
101
+ request_delay: float = 0.0,
102
+ ) -> str:
103
+ import uuid
104
+ self._scan_id = str(uuid.uuid4())
105
+ engine = self._get_engine()
106
+ engine._concurrency = concurrency
107
+ engine._request_delay = request_delay
108
+
109
+ async def _run() -> None:
110
+ findings = await engine.run_active(
111
+ targets,
112
+ check_names=check_names,
113
+ on_finding=on_finding,
114
+ on_progress=on_progress,
115
+ on_request=on_request,
116
+ )
117
+ await engine.save_findings(findings)
118
+
119
+ self._active_task = asyncio.create_task(_run())
120
+ return self._scan_id
121
+
122
+ async def stop_scan(self) -> None:
123
+ if self._active_task and not self._active_task.done():
124
+ self._active_task.cancel()
125
+ try:
126
+ await self._active_task
127
+ except asyncio.CancelledError:
128
+ pass
129
+ self._active_task = None
130
+
131
+ def is_scanning(self) -> bool:
132
+ return bool(self._active_task and not self._active_task.done())
133
+
134
+ async def get_findings(self, limit: int = 200) -> list[Finding]:
135
+ return await self._get_engine().get_findings(limit)
136
+
137
+ async def mark_false_positive(self, finding_id: str) -> None:
138
+ await self._get_engine().mark_false_positive(finding_id)
139
+
140
+ async def attach_passive(self, proxy_api=None) -> None:
141
+ engine = self._get_engine()
142
+ self._passive = PassiveScanner(engine)
143
+ self._passive.attach_bus()
144
+ logger.info("Passive scanner attached via EventBus")
145
+
146
+ async def detach_passive(self) -> None:
147
+ if self._passive:
148
+ self._passive.detach_bus()
149
+ self._passive = None
150
+
151
+ def set_passive_callback(self, callback: Callable[[Finding], None]) -> None:
152
+ if self._passive:
153
+ self._passive.on_finding = callback
154
+
155
+ async def generate_report(self, path: str, fmt: str = "html") -> None:
156
+ findings = await self.get_findings(limit=10000)
157
+ fmt = fmt.lower()
158
+ if fmt == "html":
159
+ generate_html_report(findings, path)
160
+ elif fmt == "json":
161
+ generate_json_report(findings, path)
162
+ elif fmt == "csv":
163
+ generate_csv_report(findings, path)
164
+ else:
165
+ raise ValueError(f"Unknown report format: {fmt}")
166
+ logger.info("Report generated: %s (%s)", path, fmt)
167
+
168
+ def register_check(self, check: BaseCheck) -> None:
169
+ self._get_engine().register_check(check)
170
+
171
+ def get_registered_checks(self) -> list[BaseCheck]:
172
+ return self._get_engine().get_registered_checks()
173
+
174
+ # ── Project persistence ────────────────────────────────────────────────────
175
+
176
+ def export_project_data(self) -> dict:
177
+ try:
178
+ findings = run_async_sync(self.get_findings(limit=10_000), timeout=10)
179
+ except Exception as exc:
180
+ logger.warning("export_project_data: %s", exc)
181
+ findings = []
182
+ return {"findings": [f.to_dict() for f in (findings or [])]}
183
+
184
+ def import_project_data(self, data: dict) -> int:
185
+ findings_data = data.get("findings", [])
186
+ if not findings_data:
187
+ return 0
188
+
189
+ findings = []
190
+ for fd in findings_data:
191
+ try:
192
+ findings.append(Finding.from_dict(fd))
193
+ except Exception as exc:
194
+ logger.warning("ScannerAPI.import_project_data: skip finding: %s", exc)
195
+
196
+ if not findings:
197
+ return 0
198
+
199
+ # Пишем в SQLite через save_findings (нужен async контекст)
200
+ engine = self._get_engine()
201
+ try:
202
+ run_async_sync(engine.save_findings(findings), timeout=15)
203
+ return len(findings)
204
+ except Exception as exc:
205
+ logger.warning("import_project_data save_findings: %s", exc)
206
+ return 0
207
+
208
+ async def get_stats(self) -> dict:
209
+ return await self._get_engine().get_stats()
210
+
211
+ async def get_host_count(self) -> int:
212
+ try:
213
+ from pentool.storage.http_storage import HttpStorage
214
+ storage = HttpStorage()
215
+ await storage.init_db(self._db_path)
216
+ try:
217
+ return await storage.count_distinct_hosts()
218
+ finally:
219
+ await storage.close()
220
+ except Exception as exc:
221
+ logger.debug("ScannerAPI.get_host_count: %s", exc)
222
+ return 0
223
+
224
+ async def get_history_requests(
225
+ self,
226
+ scope_host: str = "",
227
+ limit: int = 500,
228
+ ) -> list:
229
+ from pentool.storage.http_storage import HttpStorage
230
+ from pentool.utils.parser import ParsedRequest
231
+ import re as _re
232
+
233
+ storage = HttpStorage()
234
+ try:
235
+ await storage.init_db(self._db_path)
236
+ entries = await storage.export_all_requests(limit=limit)
237
+ finally:
238
+ await storage.close()
239
+
240
+ seen: set[str] = set()
241
+ result: list[ParsedRequest] = []
242
+
243
+ for entry in entries:
244
+ url: str = entry.get("url", "") or ""
245
+ method: str = (entry.get("method", "GET") or "GET").upper()
246
+ if not url:
247
+ continue
248
+ if scope_host and scope_host not in url:
249
+ continue
250
+ # Дедупликация по (method, path без значений параметров)
251
+ path_tmpl = _re.sub(r"=[^&]*", "=", url.split("?")[0])
252
+ dedup_key = f"{method}:{path_tmpl}"
253
+ if dedup_key in seen:
254
+ continue
255
+ seen.add(dedup_key)
256
+
257
+ headers: dict = entry.get("request_headers", {}) or {}
258
+ body: str = entry.get("request_body", "") or ""
259
+ result.append(ParsedRequest(
260
+ method=method,
261
+ url=url,
262
+ headers=headers,
263
+ body=body,
264
+ ))
265
+
266
+ return result
@@ -0,0 +1,17 @@
1
+ """SequencerAPI — публичный интерфейс Sequencer для TUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pentool.modules.sequencer import ( # noqa: F401
6
+ Sequencer,
7
+ SequencerReport,
8
+ token_entropy,
9
+ charset_size,
10
+ )
11
+
12
+ __all__ = [
13
+ "Sequencer",
14
+ "SequencerReport",
15
+ "token_entropy",
16
+ "charset_size",
17
+ ]
@@ -0,0 +1,106 @@
1
+ """SpiderAPI — публичный интерфейс Spider-модуля для TUI и CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Callable
7
+
8
+ from pentool.core.logging import get_logger
9
+ from pentool.modules.spider import (
10
+ AsyncSpider,
11
+ SpiderEndpoint,
12
+ SpiderForm,
13
+ SpiderResult,
14
+ is_playwright_available,
15
+ )
16
+
17
+ logger = get_logger(__name__)
18
+
19
+ # Реэкспорт типов — TUI использует их отсюда
20
+ __all__ = [
21
+ "SpiderAPI", "SpiderResult", "SpiderForm", "SpiderEndpoint", "SpiderConfig",
22
+ "is_playwright_available",
23
+ ]
24
+
25
+
26
+ @dataclass
27
+ class SpiderConfig:
28
+ """Конфигурация краулера."""
29
+ max_depth: int = 3
30
+ max_pages: int = 100
31
+ concurrency: int = 5
32
+ timeout: float = 10.0
33
+ user_agent: str = "pentool/1.0"
34
+ respect_scope: bool = False
35
+ js_render: bool = False # Playwright JS-рендеринг (если установлен)
36
+
37
+
38
+ class SpiderAPI:
39
+
40
+ def __init__(self, config: SpiderConfig | None = None) -> None:
41
+ self._config = config or SpiderConfig()
42
+ self._spider: AsyncSpider | None = None
43
+ self._stop_requested = False
44
+
45
+ @classmethod
46
+ def from_params(
47
+ cls,
48
+ max_depth: int = 3,
49
+ max_pages: int = 100,
50
+ concurrency: int = 5,
51
+ timeout: float = 10.0,
52
+ ) -> "SpiderAPI":
53
+ """Удобный фабричный метод."""
54
+ return cls(SpiderConfig(
55
+ max_depth=max_depth,
56
+ max_pages=max_pages,
57
+ concurrency=concurrency,
58
+ timeout=timeout,
59
+ ))
60
+
61
+ async def crawl(
62
+ self,
63
+ url: str,
64
+ on_page: Callable[[str], None] | None = None,
65
+ on_progress: Callable[[int, int], None] | None = None,
66
+ extra_headers: dict | None = None,
67
+ ) -> SpiderResult:
68
+ self._stop_requested = False
69
+ cfg = self._config
70
+ self._spider = AsyncSpider(
71
+ max_depth=cfg.max_depth,
72
+ max_pages=cfg.max_pages,
73
+ concurrency=cfg.concurrency,
74
+ respect_scope=cfg.respect_scope,
75
+ js_render=cfg.js_render,
76
+ extra_headers=extra_headers or {},
77
+ )
78
+ if on_page:
79
+ self._spider.on_page = on_page
80
+ if on_progress:
81
+ self._spider.on_progress = on_progress
82
+
83
+ try:
84
+ result = await self._spider.crawl(url)
85
+ logger.info(
86
+ "SpiderAPI.crawl: %s → %d pages, %d forms, %d endpoints",
87
+ url, len(result.pages), len(result.forms), len(result.endpoints),
88
+ )
89
+ return result
90
+ except Exception as exc:
91
+ logger.warning("SpiderAPI.crawl error for %s: %s", url, exc)
92
+ return SpiderResult(pages=[], forms=[], endpoints=[], js_files=[], errors=[str(exc)])
93
+
94
+ def stop(self) -> None:
95
+ """Запросить остановку краулинга."""
96
+ self._stop_requested = True
97
+ if self._spider is not None:
98
+ try:
99
+ self._spider._stop_requested = True
100
+ except Exception:
101
+ pass
102
+
103
+ @property
104
+ def config(self) -> SpiderConfig:
105
+ """Текущая конфигурация краулера (max_depth, max_pages, concurrency)."""
106
+ return self._config
@@ -0,0 +1,87 @@
1
+ """TargetAPI — публичный интерфейс Target/SiteMap для TUI и CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from pentool.core.logging import get_logger
10
+ from pentool.modules.target import SiteMap, SiteNode
11
+
12
+ if TYPE_CHECKING:
13
+ from pentool.utils.parser import ParsedRequest
14
+
15
+ logger = get_logger(__name__)
16
+
17
+ __all__ = ["TargetAPI", "SiteNode", "SiteMap"]
18
+
19
+
20
+ class TargetAPI:
21
+
22
+ def __init__(self, db_path: str = "") -> None:
23
+ self._db_path = db_path
24
+ self._sitemap = SiteMap(db_path=db_path)
25
+
26
+ @property
27
+ def sitemap(self) -> SiteMap:
28
+ """Прямой доступ к объекту SiteMap."""
29
+ return self._sitemap
30
+
31
+ async def load(self) -> None:
32
+ await self._sitemap.load()
33
+
34
+ async def save(self) -> None:
35
+ await self._sitemap.save()
36
+
37
+ def add_request(self, req: "ParsedRequest") -> None:
38
+ self._sitemap.add_request(req)
39
+
40
+ async def get_tree(self) -> dict[str, list[SiteNode]]:
41
+ return self._sitemap.get_tree()
42
+
43
+ def get_hosts(self) -> list[str]:
44
+ return self._sitemap.get_hosts()
45
+
46
+ def get_paths(self, host: str) -> list[SiteNode]:
47
+ return self._sitemap.get_paths(host)
48
+
49
+ async def set_in_scope(self, host: str, in_scope: bool) -> None:
50
+ """Задать scope-флаг для хоста.
51
+
52
+ Args:
53
+ host: Имя хоста.
54
+ in_scope: True — хост в scope, False — вне scope.
55
+ """
56
+ self._sitemap.set_in_scope(host, in_scope)
57
+
58
+ async def get_scope(self) -> list[str]:
59
+ return self._sitemap.get_scope()
60
+
61
+ async def clear(self) -> None:
62
+ self._sitemap.clear()
63
+
64
+ async def export_json(self, path: str) -> None:
65
+ data = self._sitemap.export_json()
66
+ Path(path).write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
67
+ logger.info("SiteMap exported to %s", path)
68
+
69
+ # ── Project persistence ────────────────────────────────────────────────────
70
+
71
+ def export_project_data(self) -> dict:
72
+ return {"sitemap": self._sitemap.export_json()}
73
+
74
+ def import_project_data(self, data: dict) -> int:
75
+ from pentool.modules.target import SiteNode
76
+ sitemap_data = data.get("sitemap", {})
77
+ self._sitemap.clear()
78
+ loaded = 0
79
+ for host, nodes in sitemap_data.items():
80
+ for node_dict in nodes:
81
+ try:
82
+ node = SiteNode.from_dict(node_dict)
83
+ self._sitemap._nodes.setdefault(host, {})[node.path] = node
84
+ loaded += 1
85
+ except Exception as exc:
86
+ logger.warning("TargetAPI.import_project_data: skip node: %s", exc)
87
+ return loaded
@@ -0,0 +1 @@
1
+ """Пакет cli: команды Click."""
pentool/cli/main.py ADDED
@@ -0,0 +1,98 @@
1
+ """Главная группа CLI-команд."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+
7
+ from pentool.core.config import get_config
8
+ from pentool.core.logging import setup_logging
9
+
10
+
11
+ @click.group()
12
+ @click.version_option(package_name="pentool")
13
+ @click.option("--config", "config_path", default=None, help="Путь к файлу конфигурации.")
14
+ @click.option("--verbose", "-v", is_flag=True, default=False, help="Подробный вывод (DEBUG).")
15
+ @click.pass_context
16
+ def cli(ctx: click.Context, config_path: str | None, verbose: bool) -> None:
17
+ """Pentool — консольный инструмент для пентестинга веб-приложений.
18
+
19
+ Запустите без аргументов для открытия TUI:
20
+
21
+ pentool
22
+
23
+ Или используйте команды ниже для работы из командной строки.
24
+ """
25
+ ctx.ensure_object(dict)
26
+
27
+ cfg = get_config()
28
+ if config_path:
29
+ from pentool.core.config import Config
30
+ cfg = Config.load(config_path)
31
+
32
+ log_level = "DEBUG" if verbose else cfg.log_level
33
+ setup_logging(cfg.log_file, log_level)
34
+
35
+ ctx.obj["config"] = cfg
36
+
37
+
38
+ # Импорт и регистрация групп команд
39
+ from pentool.cli.project import project # noqa: E402
40
+
41
+ cli.add_command(project)
42
+
43
+
44
+ from pentool.cli.proxy import proxy # noqa: E402
45
+
46
+ cli.add_command(proxy)
47
+
48
+
49
+ @cli.group()
50
+ def repeater() -> None:
51
+ """Модуль Repeater: ручная отправка запросов."""
52
+
53
+
54
+ @repeater.command("send")
55
+ @click.option("--request-file", required=True, type=click.Path(exists=True), help="Файл с HTTP-запросом.")
56
+ def repeater_send(request_file: str) -> None:
57
+ click.echo(f"Repeater send {request_file} — not implemented yet.")
58
+
59
+
60
+ @cli.group()
61
+ def intruder() -> None:
62
+ """Модуль Intruder: автоматизированные атаки."""
63
+
64
+
65
+ @intruder.command("run")
66
+ @click.option("--request", "request_file", required=True, type=click.Path(exists=True))
67
+ @click.option("--payloads", required=True, type=click.Path(exists=True))
68
+ @click.option("--attack", default="sniper", show_default=True,
69
+ type=click.Choice(["sniper", "battering_ram", "pitchfork", "cluster_bomb"]))
70
+ def intruder_run(request_file: str, payloads: str, attack: str) -> None:
71
+ click.echo(f"Intruder run [{attack}] — not implemented yet.")
72
+
73
+
74
+ from pentool.cli.scan import scan # noqa: E402
75
+
76
+ cli.add_command(scan)
77
+
78
+
79
+ @cli.command("decode")
80
+ @click.argument("operation", type=click.Choice([
81
+ "url_encode", "url_decode",
82
+ "base64_encode", "base64_decode",
83
+ "base64url_encode", "base64url_decode",
84
+ "html_encode", "html_decode",
85
+ "hex_encode", "hex_decode",
86
+ "unicode_escape", "unicode_unescape",
87
+ "md5", "sha1", "sha256",
88
+ ]))
89
+ @click.argument("text")
90
+ def decode_cmd(operation: str, text: str) -> None:
91
+ """Кодировать/декодировать/хэшировать текст."""
92
+ from pentool.utils.coder import apply_operation
93
+ try:
94
+ result = apply_operation(operation, text)
95
+ click.echo(result)
96
+ except ValueError as exc:
97
+ click.echo(f"Error: {exc}", err=True)
98
+ raise SystemExit(1) from exc
pentool/cli/project.py ADDED
@@ -0,0 +1,88 @@
1
+ """CLI-команды для управления проектами."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import os
7
+ from pathlib import Path
8
+
9
+ import click
10
+
11
+ from pentool.core.config import Config, get_config
12
+ from pentool.core.database import init_db
13
+ from pentool.core.logging import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ @click.group()
19
+ def project() -> None:
20
+ """Управление проектами: инициализация, список, открытие."""
21
+
22
+
23
+ @project.command("init")
24
+ @click.option("--name", default="default", show_default=True, help="Имя проекта.")
25
+ @click.option(
26
+ "--path",
27
+ default=None,
28
+ help="Директория проекта (по умолчанию: ~/.config/pentool/projects/<name>).",
29
+ )
30
+ def project_init(name: str, path: str | None) -> None:
31
+ cfg = get_config()
32
+
33
+ if path is None:
34
+ project_dir = Path.home() / ".config" / "pentool" / "projects" / name
35
+ else:
36
+ project_dir = Path(path) / name
37
+
38
+ project_dir.mkdir(parents=True, exist_ok=True)
39
+
40
+ db_path = str(project_dir / "pentool.db")
41
+ log_file = str(project_dir / "pentool.log")
42
+ cert_dir = str(project_dir / "certs")
43
+ plugins_dir = str(project_dir / "plugins")
44
+
45
+ # Обновить и сохранить конфиг проекта
46
+ project_cfg = Config(
47
+ proxy_host=cfg.proxy_host,
48
+ proxy_port=cfg.proxy_port,
49
+ cert_dir=cert_dir,
50
+ db_path=db_path,
51
+ log_file=log_file,
52
+ log_level=cfg.log_level,
53
+ plugins_dir=plugins_dir,
54
+ )
55
+ config_path = project_dir / "config.yaml"
56
+ project_cfg.save(config_path)
57
+
58
+ # Создать таблицы БД
59
+ try:
60
+ asyncio.run(init_db(db_path))
61
+ click.echo(f"Project '{name}' initialized:")
62
+ click.echo(f" Directory : {project_dir}")
63
+ click.echo(f" Database : {db_path}")
64
+ click.echo(f" Config : {config_path}")
65
+ click.echo(f" Log file : {log_file}")
66
+ logger.info("Project '%s' initialized at %s", name, project_dir)
67
+ except Exception as exc:
68
+ click.echo(f"Error initializing database: {exc}", err=True)
69
+ logger.error("Failed to initialize project '%s': %s", name, exc)
70
+ raise SystemExit(1) from exc
71
+
72
+
73
+ @project.command("list")
74
+ def project_list() -> None:
75
+ projects_dir = Path.home() / ".config" / "pentool" / "projects"
76
+ if not projects_dir.exists():
77
+ click.echo("No projects found.")
78
+ return
79
+
80
+ entries = [d for d in projects_dir.iterdir() if d.is_dir()]
81
+ if not entries:
82
+ click.echo("No projects found.")
83
+ return
84
+
85
+ click.echo(f"{'Name':<20} {'Path'}")
86
+ click.echo("-" * 60)
87
+ for entry in sorted(entries):
88
+ click.echo(f"{entry.name:<20} {entry}")