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/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())
|
pentool/core/__init__.py
ADDED
|
@@ -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
|