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
pentool/cli/main.py ADDED
@@ -0,0 +1,100 @@
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
+ """Отправить HTTP-запрос из файла."""
58
+ click.echo(f"Repeater send {request_file} — not implemented yet.")
59
+
60
+
61
+ @cli.group()
62
+ def intruder() -> None:
63
+ """Модуль Intruder: автоматизированные атаки."""
64
+
65
+
66
+ @intruder.command("run")
67
+ @click.option("--request", "request_file", required=True, type=click.Path(exists=True))
68
+ @click.option("--payloads", required=True, type=click.Path(exists=True))
69
+ @click.option("--attack", default="sniper", show_default=True,
70
+ type=click.Choice(["sniper", "battering_ram", "pitchfork", "cluster_bomb"]))
71
+ def intruder_run(request_file: str, payloads: str, attack: str) -> None:
72
+ """Запустить атаку."""
73
+ click.echo(f"Intruder run [{attack}] — not implemented yet.")
74
+
75
+
76
+ from pentool.cli.scan import scan # noqa: E402
77
+
78
+ cli.add_command(scan)
79
+
80
+
81
+ @cli.command("decode")
82
+ @click.argument("operation", type=click.Choice([
83
+ "url_encode", "url_decode",
84
+ "base64_encode", "base64_decode",
85
+ "base64url_encode", "base64url_decode",
86
+ "html_encode", "html_decode",
87
+ "hex_encode", "hex_decode",
88
+ "unicode_escape", "unicode_unescape",
89
+ "md5", "sha1", "sha256",
90
+ ]))
91
+ @click.argument("text")
92
+ def decode_cmd(operation: str, text: str) -> None:
93
+ """Кодировать/декодировать/хэшировать текст."""
94
+ from pentool.utils.coder import apply_operation
95
+ try:
96
+ result = apply_operation(operation, text)
97
+ click.echo(result)
98
+ except ValueError as exc:
99
+ click.echo(f"Error: {exc}", err=True)
100
+ raise SystemExit(1) from exc
pentool/cli/project.py ADDED
@@ -0,0 +1,90 @@
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
+ """Инициализировать новый проект: создать директорию, БД и конфиг."""
32
+ cfg = get_config()
33
+
34
+ if path is None:
35
+ project_dir = Path.home() / ".config" / "pentool" / "projects" / name
36
+ else:
37
+ project_dir = Path(path) / name
38
+
39
+ project_dir.mkdir(parents=True, exist_ok=True)
40
+
41
+ db_path = str(project_dir / "pentool.db")
42
+ log_file = str(project_dir / "pentool.log")
43
+ cert_dir = str(project_dir / "certs")
44
+ plugins_dir = str(project_dir / "plugins")
45
+
46
+ # Обновить и сохранить конфиг проекта
47
+ project_cfg = Config(
48
+ proxy_host=cfg.proxy_host,
49
+ proxy_port=cfg.proxy_port,
50
+ cert_dir=cert_dir,
51
+ db_path=db_path,
52
+ log_file=log_file,
53
+ log_level=cfg.log_level,
54
+ plugins_dir=plugins_dir,
55
+ )
56
+ config_path = project_dir / "config.yaml"
57
+ project_cfg.save(config_path)
58
+
59
+ # Создать таблицы БД
60
+ try:
61
+ asyncio.run(init_db(db_path))
62
+ click.echo(f"Project '{name}' initialized:")
63
+ click.echo(f" Directory : {project_dir}")
64
+ click.echo(f" Database : {db_path}")
65
+ click.echo(f" Config : {config_path}")
66
+ click.echo(f" Log file : {log_file}")
67
+ logger.info("Project '%s' initialized at %s", name, project_dir)
68
+ except Exception as exc:
69
+ click.echo(f"Error initializing database: {exc}", err=True)
70
+ logger.error("Failed to initialize project '%s': %s", name, exc)
71
+ raise SystemExit(1) from exc
72
+
73
+
74
+ @project.command("list")
75
+ def project_list() -> None:
76
+ """Показать список сохранённых проектов."""
77
+ projects_dir = Path.home() / ".config" / "pentool" / "projects"
78
+ if not projects_dir.exists():
79
+ click.echo("No projects found.")
80
+ return
81
+
82
+ entries = [d for d in projects_dir.iterdir() if d.is_dir()]
83
+ if not entries:
84
+ click.echo("No projects found.")
85
+ return
86
+
87
+ click.echo(f"{'Name':<20} {'Path'}")
88
+ click.echo("-" * 60)
89
+ for entry in sorted(entries):
90
+ click.echo(f"{entry.name:<20} {entry}")
pentool/cli/proxy.py ADDED
@@ -0,0 +1,185 @@
1
+ """CLI-команды для управления прокси-сервером."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ import click
12
+
13
+ from pentool.core.config import get_config
14
+ from pentool.core.logging import get_logger, setup_logging
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ @click.group()
20
+ def proxy() -> None:
21
+ """Управление HTTP/HTTPS прокси-сервером."""
22
+
23
+
24
+ @proxy.command("start")
25
+ @click.option("--port", default=8080, show_default=True, help="Порт прокси.")
26
+ @click.option("--host", default="127.0.0.1", show_default=True, help="Адрес для прослушивания.")
27
+ @click.option("--cert-dir", default=None, help="Директория для CA-сертификатов.")
28
+ @click.option("--db", "db_path", default=None, help="Путь к SQLite БД для логирования.")
29
+ @click.option("--intercept", is_flag=True, default=False, help="Включить интерактивный перехват.")
30
+ @click.option("--scope", default="", help="Список хостов через запятую (пусто = все).")
31
+ def proxy_start(
32
+ port: int,
33
+ host: str,
34
+ cert_dir: str | None,
35
+ db_path: str | None,
36
+ intercept: bool,
37
+ scope: str,
38
+ ) -> None:
39
+ """Запустить прокси-сервер в текущем процессе (блокирующий режим).
40
+
41
+ Примеры:
42
+
43
+ pentool proxy start --port 8080
44
+
45
+ pentool proxy start --port 8080 --scope "example.com,api.example.com"
46
+
47
+ pentool proxy start --intercept
48
+ """
49
+ cfg = get_config()
50
+ setup_logging(cfg.log_file, cfg.log_level)
51
+
52
+ _cert_dir = cert_dir or cfg.cert_dir
53
+ _db_path = db_path or cfg.db_path
54
+
55
+ from pentool.api.proxy_api import ProxyAPI
56
+
57
+ api = ProxyAPI()
58
+ server = api.create_proxy(
59
+ host=host,
60
+ port=port,
61
+ cert_dir=_cert_dir,
62
+ db_path=_db_path,
63
+ )
64
+ server.intercept_enabled = intercept
65
+
66
+ if scope:
67
+ server.set_scope([h.strip() for h in scope.split(",") if h.strip()])
68
+
69
+ click.echo(f"Starting proxy on {host}:{port}")
70
+ click.echo(f" CA certs : {_cert_dir}")
71
+ click.echo(f" Database : {_db_path}")
72
+ click.echo(f" Intercept : {'ON' if intercept else 'OFF'}")
73
+ if server.scope:
74
+ click.echo(f" Scope : {', '.join(server.scope)}")
75
+ else:
76
+ click.echo(" Scope : ALL hosts")
77
+ click.echo("Press Ctrl+C to stop.\n")
78
+
79
+ # Инициализировать БД если нужно
80
+ from pentool.core.database import init_db_sync
81
+ try:
82
+ init_db_sync(_db_path)
83
+ except Exception as exc:
84
+ click.echo(f"Warning: could not init database: {exc}", err=True)
85
+
86
+ try:
87
+ asyncio.run(server.serve_forever())
88
+ except KeyboardInterrupt:
89
+ click.echo("\nProxy stopped.")
90
+
91
+
92
+ @proxy.command("status")
93
+ @click.option("--port", default=8080, show_default=True, help="Порт для проверки.")
94
+ def proxy_status(port: int) -> None:
95
+ """Проверить, слушает ли прокси на указанном порту."""
96
+ import socket
97
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
98
+ s.settimeout(1.0)
99
+ result = s.connect_ex(("127.0.0.1", port))
100
+ if result == 0:
101
+ click.echo(f"Proxy is RUNNING on port {port}")
102
+ else:
103
+ click.echo(f"Proxy is NOT running on port {port}")
104
+
105
+
106
+ @proxy.command("history")
107
+ @click.option("--db", "db_path", default=None, help="Путь к SQLite БД.")
108
+ @click.option("--limit", default=20, show_default=True, help="Количество записей.")
109
+ @click.option("--method", default=None, help="Фильтр по методу (GET, POST, ...).")
110
+ @click.option("--host", default=None, help="Фильтр по хосту (подстрока URL).")
111
+ def proxy_history(
112
+ db_path: str | None,
113
+ limit: int,
114
+ method: str | None,
115
+ host: str | None,
116
+ ) -> None:
117
+ """Показать историю перехваченных запросов из БД."""
118
+ cfg = get_config()
119
+ _db_path = db_path or cfg.db_path
120
+
121
+ if not Path(_db_path).exists():
122
+ click.echo(f"Database not found: {_db_path}", err=True)
123
+ raise SystemExit(1)
124
+
125
+ async def _fetch() -> list:
126
+ from pentool.storage.http_storage import HttpStorage
127
+ storage = HttpStorage()
128
+ await storage.init_db(_db_path)
129
+ filters: dict = {}
130
+ if method:
131
+ filters["method"] = [method.upper()]
132
+ if host:
133
+ filters["host"] = host
134
+ return await storage.get_metadata_batch(
135
+ offset=0,
136
+ limit=limit,
137
+ filters=filters if filters else None,
138
+ order_by="id",
139
+ desc=True,
140
+ )
141
+
142
+ rows = asyncio.run(_fetch())
143
+
144
+ if not rows:
145
+ click.echo("No requests found.")
146
+ return
147
+
148
+ click.echo(f"{'ID':<6} {'METHOD':<8} {'STATUS':<8} {'URL'}")
149
+ click.echo("-" * 80)
150
+ for row in rows:
151
+ rid = row["id"]
152
+ meth = row.get("method", "-")
153
+ url = row.get("url", "-")
154
+ status = row.get("status_code")
155
+ status_str = str(status) if status else "-"
156
+ url_short = url[:60] + "..." if len(url) > 60 else url
157
+ click.echo(f"{rid:<6} {meth:<8} {status_str:<8} {url_short}")
158
+
159
+
160
+ @proxy.command("ca-info")
161
+ @click.option("--cert-dir", default=None, help="Директория CA-сертификатов.")
162
+ def proxy_ca_info(cert_dir: str | None) -> None:
163
+ """Показать путь к CA-сертификату и инструкцию по установке."""
164
+ cfg = get_config()
165
+ _cert_dir = cert_dir or cfg.cert_dir
166
+ ca_cert = Path(_cert_dir) / "ca.crt"
167
+
168
+ if ca_cert.exists():
169
+ click.echo(f"CA certificate: {ca_cert}")
170
+ else:
171
+ from pentool.utils.cert import generate_ca_cert
172
+ cert_path, _ = generate_ca_cert(_cert_dir)
173
+ click.echo(f"CA certificate generated: {cert_path}")
174
+
175
+ click.echo("\nTo trust the CA certificate:")
176
+ click.echo("")
177
+ click.echo(" Linux (Ubuntu/Debian):")
178
+ click.echo(f" sudo cp {ca_cert} /usr/local/share/ca-certificates/pentool-ca.crt")
179
+ click.echo(" sudo update-ca-certificates")
180
+ click.echo("")
181
+ click.echo(" macOS:")
182
+ click.echo(f" sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain {ca_cert}")
183
+ click.echo("")
184
+ click.echo(" Firefox / Chrome:")
185
+ click.echo(" Settings → Certificates → Import → select ca.crt → Trust for websites")
pentool/cli/scan.py ADDED
@@ -0,0 +1,118 @@
1
+ """CLI-команды сканирования: pentool scan active / passive / report."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import sys
7
+
8
+ import click
9
+
10
+
11
+ @click.group("scan")
12
+ def scan() -> None:
13
+ """Модуль Scanner: сканирование уязвимостей."""
14
+
15
+
16
+ @scan.command("active")
17
+ @click.option("--url", "urls", required=True, multiple=True, help="Целевой URL (можно несколько).")
18
+ @click.option(
19
+ "--checks", "check_names", default=None,
20
+ help="Проверки через запятую: missing_security_headers,info_leak",
21
+ )
22
+ @click.option("--output", "output", default=None, help="Сохранить findings в файл (json/csv/html).")
23
+ @click.option("--concurrency", default=5, show_default=True, help="Параллельных потоков.")
24
+ @click.option("--delay", default=0.0, show_default=True, help="Задержка между запросами (сек).")
25
+ @click.pass_context
26
+ def scan_active(
27
+ ctx: click.Context,
28
+ urls: tuple[str, ...],
29
+ check_names: str | None,
30
+ output: str | None,
31
+ concurrency: int,
32
+ delay: float,
33
+ ) -> None:
34
+ """Запустить активное сканирование по URL."""
35
+ from pentool.api.scanner_api import ScannerAPI
36
+ from pentool.core.config import get_config
37
+
38
+ cfg = (ctx.obj or {}).get("config") or get_config()
39
+ names = [c.strip() for c in check_names.split(",")] if check_names else None
40
+
41
+ api = ScannerAPI(db_path=cfg.db_path)
42
+
43
+ findings = []
44
+
45
+ def on_finding(f) -> None:
46
+ findings.append(f)
47
+ sev = f.severity.upper()
48
+ click.echo(f" [{sev}] {f.name} — {f.url}")
49
+
50
+ def on_progress(done: int, total: int) -> None:
51
+ click.echo(f"\r Progress: {done}/{total}", nl=False)
52
+
53
+ async def _run() -> None:
54
+ await api.start_active_scan(
55
+ list(urls),
56
+ check_names=names,
57
+ on_finding=on_finding,
58
+ on_progress=on_progress,
59
+ concurrency=concurrency,
60
+ request_delay=delay,
61
+ )
62
+ # Ждём завершения задачи
63
+ if api._active_task:
64
+ await api._active_task
65
+
66
+ click.echo(f"Starting active scan on {len(urls)} target(s)...")
67
+ asyncio.run(_run())
68
+ click.echo(f"\nDone. Found {len(findings)} finding(s).")
69
+
70
+ if output:
71
+ fmt = "html"
72
+ if output.endswith(".json"):
73
+ fmt = "json"
74
+ elif output.endswith(".csv"):
75
+ fmt = "csv"
76
+ asyncio.run(api.generate_report(output, fmt))
77
+ click.echo(f"Report saved: {output}")
78
+
79
+
80
+ @scan.command("passive")
81
+ @click.option("--scope", default=None, help="Фильтр хоста (например *.example.com).")
82
+ @click.pass_context
83
+ def scan_passive(ctx: click.Context, scope: str | None) -> None:
84
+ """Показать информацию о пассивном сканировании."""
85
+ click.echo(
86
+ "Passive scanning runs automatically while the proxy intercepts traffic.\n"
87
+ "Use the TUI (Scanner tab) to enable Passive mode and view findings."
88
+ )
89
+ if scope:
90
+ click.echo(f"Scope filter: {scope}")
91
+
92
+
93
+ @scan.command("report")
94
+ @click.option("--output", required=True, help="Путь к файлу отчёта.")
95
+ @click.option(
96
+ "--format", "fmt", default="html",
97
+ type=click.Choice(["html", "json", "csv"], case_sensitive=False),
98
+ show_default=True,
99
+ help="Формат отчёта.",
100
+ )
101
+ @click.pass_context
102
+ def scan_report(ctx: click.Context, output: str, fmt: str) -> None:
103
+ """Сгенерировать отчёт по найденным уязвимостям."""
104
+ from pentool.api.scanner_api import ScannerAPI
105
+ from pentool.core.config import get_config
106
+
107
+ cfg = (ctx.obj or {}).get("config") or get_config()
108
+ api = ScannerAPI(db_path=cfg.db_path)
109
+
110
+ async def _run() -> None:
111
+ findings = await api.get_findings(limit=10000)
112
+ if not findings:
113
+ click.echo("No findings in database.")
114
+ return
115
+ await api.generate_report(output, fmt)
116
+ click.echo(f"Report saved: {output} ({len(findings)} findings)")
117
+
118
+ asyncio.run(_run())
@@ -0,0 +1 @@
1
+ """Пакет core: конфигурация, логирование, БД."""
pentool/core/config.py ADDED
@@ -0,0 +1,184 @@
1
+ """Управление конфигурацией приложения."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any, Callable
9
+
10
+ import yaml
11
+
12
+
13
+ DEFAULT_CONFIG_DIR = Path.home() / ".config" / "pentool"
14
+ DEFAULT_CONFIG_PATH = DEFAULT_CONFIG_DIR / "config.yaml"
15
+
16
+ # Тип колбэка: вызывается при изменении конфига
17
+ ConfigObserver = Callable[[dict], None]
18
+
19
+
20
+ @dataclass
21
+ class Config:
22
+ """Конфигурация приложения с значениями по умолчанию."""
23
+
24
+ proxy_host: str = "127.0.0.1"
25
+ proxy_port: int = 8080
26
+ cert_dir: str = field(default_factory=lambda: str(DEFAULT_CONFIG_DIR / "certs"))
27
+ db_path: str = field(default_factory=lambda: str(DEFAULT_CONFIG_DIR / "pentool.db"))
28
+ log_file: str = field(default_factory=lambda: str(DEFAULT_CONFIG_DIR / "pentool.log"))
29
+ log_level: str = "INFO"
30
+ plugins_dir: str = field(default_factory=lambda: str(DEFAULT_CONFIG_DIR / "plugins"))
31
+ scope: list[str] = field(default_factory=list)
32
+ intercept_enabled: bool = False
33
+ recent_projects: list[str] = field(default_factory=list)
34
+ auto_save_enabled: bool = False
35
+ auto_save_interval: int = 5 # минуты
36
+ # ── Network / Scanner settings (Блок 4.10) ─────────────────────────────
37
+ default_user_agent: str = (
38
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
39
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
40
+ "Chrome/122.0.0.0 Safari/537.36"
41
+ )
42
+ request_timeout: int = 15 # секунды
43
+ connect_timeout: int = 10 # секунды
44
+ collaborator_url: str = "" # Burp Collaborator / interactsh URL
45
+ max_redirects: int = 10
46
+ verify_ssl: bool = False # проверка SSL-сертификатов в сканере
47
+ # ── Scanner request marking ─────────────────────────────────────────────
48
+ scan_marker_enabled: bool = False
49
+ scan_marker_name: str = "X-Scanner"
50
+ scan_marker_value: str = "pentool/1.0"
51
+
52
+ # Список наблюдателей — не сериализуется (R-16)
53
+ _observers: list[ConfigObserver] = field(default_factory=list, init=False, repr=False, compare=False)
54
+
55
+ def add_observer(self, cb: ConfigObserver) -> None:
56
+ """Подписаться на изменения конфигурации.
57
+
58
+ Args:
59
+ cb: Функция вида cb(changed_fields: dict) → None.
60
+ """
61
+ if cb not in self._observers:
62
+ self._observers.append(cb)
63
+
64
+ def remove_observer(self, cb: ConfigObserver) -> None:
65
+ """Отписаться от изменений конфигурации."""
66
+ try:
67
+ self._observers.remove(cb)
68
+ except ValueError:
69
+ pass
70
+
71
+ def notify_observers(self, changed_fields: dict) -> None:
72
+ """Уведомить всех наблюдателей об изменениях.
73
+
74
+ Args:
75
+ changed_fields: Словарь изменённых полей {name: new_value}.
76
+ """
77
+ for cb in list(self._observers):
78
+ try:
79
+ cb(changed_fields)
80
+ except Exception:
81
+ pass
82
+
83
+ def update(self, **kwargs: Any) -> None:
84
+ """Обновить поля конфига и уведомить наблюдателей.
85
+
86
+ Args:
87
+ **kwargs: Поля для обновления (proxy_host, proxy_port и т.д.).
88
+ """
89
+ changed: dict = {}
90
+ for key, value in kwargs.items():
91
+ if hasattr(self, key) and not key.startswith("_"):
92
+ if getattr(self, key) != value:
93
+ setattr(self, key, value)
94
+ changed[key] = value
95
+ if changed:
96
+ self.notify_observers(changed)
97
+
98
+ def to_dict(self) -> dict[str, Any]:
99
+ """Преобразовать в словарь для сериализации."""
100
+ return {
101
+ "proxy_host": self.proxy_host,
102
+ "proxy_port": self.proxy_port,
103
+ "cert_dir": self.cert_dir,
104
+ "db_path": self.db_path,
105
+ "log_file": self.log_file,
106
+ "log_level": self.log_level,
107
+ "plugins_dir": self.plugins_dir,
108
+ "scope": self.scope,
109
+ "intercept_enabled": self.intercept_enabled,
110
+ "recent_projects": self.recent_projects,
111
+ "auto_save_enabled": self.auto_save_enabled,
112
+ "auto_save_interval": self.auto_save_interval,
113
+ "default_user_agent": self.default_user_agent,
114
+ "request_timeout": self.request_timeout,
115
+ "connect_timeout": self.connect_timeout,
116
+ "collaborator_url": self.collaborator_url,
117
+ "max_redirects": self.max_redirects,
118
+ "verify_ssl": self.verify_ssl,
119
+ "scan_marker_enabled": self.scan_marker_enabled,
120
+ "scan_marker_name": self.scan_marker_name,
121
+ "scan_marker_value": self.scan_marker_value,
122
+ }
123
+
124
+ def add_recent_project(self, path: str) -> None:
125
+ """Добавить путь в список последних проектов (дедупликация, максимум 10)."""
126
+ path = str(path)
127
+ if path in self.recent_projects:
128
+ self.recent_projects.remove(path)
129
+ self.recent_projects.insert(0, path)
130
+ self.recent_projects = self.recent_projects[:10]
131
+ try:
132
+ self.save()
133
+ except Exception:
134
+ pass
135
+
136
+ def save(self, path: str | Path = DEFAULT_CONFIG_PATH) -> None:
137
+ """Сохранить конфигурацию в YAML-файл."""
138
+ path = Path(path)
139
+ path.parent.mkdir(parents=True, exist_ok=True)
140
+ with open(path, "w", encoding="utf-8") as f:
141
+ yaml.dump(self.to_dict(), f, default_flow_style=False, allow_unicode=True)
142
+
143
+ @classmethod
144
+ def load(cls, path: str | Path = DEFAULT_CONFIG_PATH) -> "Config":
145
+ """Загрузить конфигурацию из YAML-файла.
146
+
147
+ Если файл не существует — вернуть конфигурацию по умолчанию.
148
+ """
149
+ path = Path(path)
150
+ if not path.exists():
151
+ return cls()
152
+ with open(path, "r", encoding="utf-8") as f:
153
+ data: dict[str, Any] = yaml.safe_load(f) or {}
154
+ cfg = cls()
155
+ for key, value in data.items():
156
+ if hasattr(cfg, key) and not key.startswith("_"):
157
+ setattr(cfg, key, value)
158
+ # Удалить несуществующие пути из recent_projects при загрузке
159
+ before = len(cfg.recent_projects)
160
+ cfg.recent_projects = [p for p in cfg.recent_projects if os.path.exists(p)]
161
+ if len(cfg.recent_projects) != before:
162
+ try:
163
+ cfg.save()
164
+ except Exception:
165
+ pass
166
+ return cfg
167
+
168
+
169
+ # Глобальный экземпляр конфигурации (инициализируется при запуске)
170
+ _config: Config | None = None
171
+
172
+
173
+ def get_config() -> Config:
174
+ """Получить глобальный экземпляр конфигурации."""
175
+ global _config
176
+ if _config is None:
177
+ _config = Config.load()
178
+ return _config
179
+
180
+
181
+ def set_config(config: Config) -> None:
182
+ """Установить глобальный экземпляр конфигурации."""
183
+ global _config
184
+ _config = config