pentool 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pentool/__init__.py +4 -0
- pentool/__main__.py +16 -0
- pentool/api/__init__.py +41 -0
- pentool/api/base_api.py +48 -0
- pentool/api/comparer_api.py +19 -0
- pentool/api/decoder_api.py +24 -0
- pentool/api/intruder_api.py +171 -0
- pentool/api/proxy_api.py +210 -0
- pentool/api/repeater_api.py +57 -0
- pentool/api/scanner_api.py +266 -0
- pentool/api/sequencer_api.py +17 -0
- pentool/api/spider_api.py +106 -0
- pentool/api/target_api.py +87 -0
- pentool/cli/__init__.py +1 -0
- pentool/cli/main.py +98 -0
- pentool/cli/project.py +88 -0
- pentool/cli/proxy.py +173 -0
- pentool/cli/scan.py +115 -0
- pentool/core/__init__.py +1 -0
- pentool/core/config.py +171 -0
- pentool/core/crash_reporter.py +75 -0
- pentool/core/database.py +139 -0
- pentool/core/event_bus.py +207 -0
- pentool/core/events.py +160 -0
- pentool/core/features.py +373 -0
- pentool/core/license.py +274 -0
- pentool/core/logging.py +54 -0
- pentool/core/plugin_manager.py +312 -0
- pentool/core/project.py +31 -0
- pentool/core/storage_interface.py +307 -0
- pentool/core/updater.py +96 -0
- pentool/core/utils.py +36 -0
- pentool/modules/__init__.py +1 -0
- pentool/modules/comparer.py +177 -0
- pentool/modules/decoder.py +281 -0
- pentool/modules/intruder.py +428 -0
- pentool/modules/intruder_turbo.py +176 -0
- pentool/modules/match_replace.py +142 -0
- pentool/modules/proxy.py +797 -0
- pentool/modules/repeater.py +195 -0
- pentool/modules/sequencer.py +492 -0
- pentool/modules/spider.py +679 -0
- pentool/modules/target.py +185 -0
- pentool/modules/websocket_handler.py +249 -0
- pentool/plugins/__init__.py +1 -0
- pentool/plugins/builtin/__init__.py +1 -0
- pentool/plugins/builtin/payloads_pro.py +381 -0
- pentool/plugins/builtin/reports_pro.py +436 -0
- pentool/plugins/builtin/scanner_pro.py +291 -0
- pentool/plugins/example_plugin.py +39 -0
- pentool/services/__init__.py +1 -0
- pentool/services/base_service.py +60 -0
- pentool/services/intruder_service.py +94 -0
- pentool/services/proxy_service.py +178 -0
- pentool/services/repeater_service.py +85 -0
- pentool/services/scan_service.py +330 -0
- pentool/storage/__init__.py +0 -0
- pentool/storage/http_storage.py +506 -0
- pentool/storage/large_body_handler.py +50 -0
- pentool/storage/lru_cache.py +45 -0
- pentool/t.py +213 -0
- pentool/tui/__init__.py +1 -0
- pentool/tui/app.py +1429 -0
- pentool/tui/constants.py +79 -0
- pentool/tui/dialogs/__init__.py +0 -0
- pentool/tui/dialogs/cert_dialog.py +81 -0
- pentool/tui/dialogs/file_selector.py +226 -0
- pentool/tui/dialogs/load_from_proxy.py +125 -0
- pentool/tui/dialogs/match_replace_dialog.py +229 -0
- pentool/tui/dialogs/project_dialog.py +82 -0
- pentool/tui/dialogs/scope_dialog.py +224 -0
- pentool/tui/messages.py +103 -0
- pentool/tui/mixins/__init__.py +0 -0
- pentool/tui/mixins/app_mixin.py +59 -0
- pentool/tui/mixins/dialog_cancel.py +21 -0
- pentool/tui/mixins/request_context_menu.py +247 -0
- pentool/tui/mixins/tab_rename.py +89 -0
- pentool/tui/screens/__init__.py +31 -0
- pentool/tui/screens/comparer/__init__.py +3 -0
- pentool/tui/screens/comparer/screen.py +229 -0
- pentool/tui/screens/dashboard/__init__.py +2 -0
- pentool/tui/screens/dashboard/live_dashboard.py +730 -0
- pentool/tui/screens/dashboard/screen.py +771 -0
- pentool/tui/screens/decoder/__init__.py +3 -0
- pentool/tui/screens/decoder/screen.py +306 -0
- pentool/tui/screens/extensions/__init__.py +3 -0
- pentool/tui/screens/extensions/screen.py +24 -0
- pentool/tui/screens/intruder/__init__.py +3 -0
- pentool/tui/screens/intruder/screen.py +1356 -0
- pentool/tui/screens/proxy/__init__.py +3 -0
- pentool/tui/screens/proxy/screen.py +1554 -0
- pentool/tui/screens/repeater/__init__.py +3 -0
- pentool/tui/screens/repeater/screen.py +621 -0
- pentool/tui/screens/scanner/__init__.py +3 -0
- pentool/tui/screens/scanner/screen.py +1956 -0
- pentool/tui/screens/sequencer/__init__.py +3 -0
- pentool/tui/screens/sequencer/screen.py +516 -0
- pentool/tui/screens/settings/__init__.py +5 -0
- pentool/tui/screens/settings/hotkeys.py +134 -0
- pentool/tui/screens/settings/screen.py +540 -0
- pentool/tui/screens/spider/__init__.py +3 -0
- pentool/tui/screens/spider/screen.py +380 -0
- pentool/tui/screens/target/__init__.py +4 -0
- pentool/tui/screens/target/screen.py +358 -0
- pentool/tui/screens/terminal/__init__.py +5 -0
- pentool/tui/screens/terminal/screen.py +179 -0
- pentool/tui/widgets/__init__.py +1 -0
- pentool/tui/widgets/context_menu.py +148 -0
- pentool/tui/widgets/filter_bar.py +204 -0
- pentool/tui/widgets/inspector_panel.py +111 -0
- pentool/tui/widgets/menu.py +86 -0
- pentool/tui/widgets/menu_bar.py +238 -0
- pentool/tui/widgets/module_tabs.py +68 -0
- pentool/tui/widgets/payload_drop_zone.py +65 -0
- pentool/tui/widgets/request_editor.py +495 -0
- pentool/tui/widgets/resize_handle.py +116 -0
- pentool/tui/widgets/search_bar.py +112 -0
- pentool/tui/widgets/statusbar.py +96 -0
- pentool/tui/widgets/toolbar_button.py +68 -0
- pentool/utils/__init__.py +1 -0
- pentool/utils/cert.py +314 -0
- pentool/utils/coder.py +170 -0
- pentool/utils/copy_as.py +250 -0
- pentool/utils/diff.py +82 -0
- pentool/utils/http_client.py +145 -0
- pentool/utils/parser.py +227 -0
- pentool/utils/terminal_check.py +31 -0
- pentool-0.1.0.dist-info/METADATA +231 -0
- pentool-0.1.0.dist-info/RECORD +133 -0
- pentool-0.1.0.dist-info/WHEEL +5 -0
- pentool-0.1.0.dist-info/entry_points.txt +2 -0
- pentool-0.1.0.dist-info/licenses/LICENSE +34 -0
- pentool-0.1.0.dist-info/top_level.txt +1 -0
pentool/__init__.py
ADDED
pentool/__main__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Точка входа: без аргументов — TUI, с аргументами — CLI."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
if len(sys.argv) > 1:
|
|
8
|
+
from pentool.cli.main import cli
|
|
9
|
+
cli()
|
|
10
|
+
else:
|
|
11
|
+
from pentool.tui.app import PentoolApp
|
|
12
|
+
PentoolApp().run()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
main()
|
pentool/api/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""API-слой pentool — публичный интерфейс между modules/ и TUI/CLI."""
|
|
2
|
+
|
|
3
|
+
from pentool.api.proxy_api import ProxyAPI, InterceptedRequest, MatchReplaceRule
|
|
4
|
+
from pentool.api.repeater_api import RepeaterAPI
|
|
5
|
+
from pentool.api.scanner_api import ScannerAPI
|
|
6
|
+
from pentool.api.intruder_api import IntruderAPI, IntruderConfig, IntruderAttack
|
|
7
|
+
from pentool.api.spider_api import SpiderAPI
|
|
8
|
+
from pentool.api.target_api import TargetAPI
|
|
9
|
+
# Decoder/Comparer/Sequencer — функциональные API без класса-обёртки
|
|
10
|
+
from pentool.api.decoder_api import ( # noqa: F401
|
|
11
|
+
OPERATIONS, OP_LABELS, DecoderChain,
|
|
12
|
+
decode_op, decode_smart, encode_op, run_chain,
|
|
13
|
+
)
|
|
14
|
+
from pentool.api.comparer_api import ( # noqa: F401
|
|
15
|
+
compare, compare_lines, CompareStats, DiffLine, DiffResult,
|
|
16
|
+
)
|
|
17
|
+
from pentool.api.sequencer_api import ( # noqa: F401
|
|
18
|
+
Sequencer, SequencerReport, token_entropy, charset_size,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Proxy
|
|
23
|
+
"ProxyAPI", "InterceptedRequest", "MatchReplaceRule",
|
|
24
|
+
# Repeater
|
|
25
|
+
"RepeaterAPI",
|
|
26
|
+
# Scanner
|
|
27
|
+
"ScannerAPI",
|
|
28
|
+
# Intruder
|
|
29
|
+
"IntruderAPI", "IntruderConfig", "IntruderAttack",
|
|
30
|
+
# Spider
|
|
31
|
+
"SpiderAPI",
|
|
32
|
+
# Target
|
|
33
|
+
"TargetAPI",
|
|
34
|
+
# Decoder
|
|
35
|
+
"OPERATIONS", "OP_LABELS", "DecoderChain",
|
|
36
|
+
"decode_op", "decode_smart", "encode_op", "run_chain",
|
|
37
|
+
# Comparer
|
|
38
|
+
"compare", "compare_lines", "CompareStats", "DiffLine", "DiffResult",
|
|
39
|
+
# Sequencer
|
|
40
|
+
"Sequencer", "SequencerReport", "token_entropy", "charset_size",
|
|
41
|
+
]
|
pentool/api/base_api.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""ExportableAPI — base mixin for API classes supporting project persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ExportableAPI(ABC):
|
|
9
|
+
"""Mixin for API classes that support project data export/import.
|
|
10
|
+
|
|
11
|
+
Subclasses must implement export_project_data and import_project_data.
|
|
12
|
+
These methods are called by core.project.save_project/load_project.
|
|
13
|
+
|
|
14
|
+
Usage::
|
|
15
|
+
|
|
16
|
+
class MyAPI(ExportableAPI):
|
|
17
|
+
def export_project_data(self) -> dict:
|
|
18
|
+
return {"my_data": [...]}
|
|
19
|
+
|
|
20
|
+
def import_project_data(self, data: dict) -> int:
|
|
21
|
+
items = data.get("my_data", [])
|
|
22
|
+
# restore items
|
|
23
|
+
return len(items)
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def export_project_data(self) -> dict:
|
|
28
|
+
"""Export module state to a serializable dict.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
dict: Module-specific data structure (e.g., {"results": [...]}).
|
|
32
|
+
Must be JSON-serializable (no datetime, use .isoformat()).
|
|
33
|
+
"""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def import_project_data(self, data: dict) -> int | tuple[int, str]:
|
|
38
|
+
"""Import module state from a loaded project dict.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
data: The module's block from project.json (e.g., data["intruder"]).
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
int: Number of items loaded successfully.
|
|
45
|
+
OR
|
|
46
|
+
tuple[int, str]: (count, error_message) — empty string if OK.
|
|
47
|
+
"""
|
|
48
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""ComparerAPI — публичный интерфейс Comparer для TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pentool.modules.comparer import ( # noqa: F401
|
|
6
|
+
compare,
|
|
7
|
+
compare_lines,
|
|
8
|
+
CompareStats,
|
|
9
|
+
DiffLine,
|
|
10
|
+
DiffResult,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"compare",
|
|
15
|
+
"compare_lines",
|
|
16
|
+
"CompareStats",
|
|
17
|
+
"DiffLine",
|
|
18
|
+
"DiffResult",
|
|
19
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""DecoderAPI — публичный интерфейс Decoder/Encoder для TUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# Реэкспорт всего публичного из modules/decoder
|
|
6
|
+
from pentool.modules.decoder import ( # noqa: F401
|
|
7
|
+
OPERATIONS,
|
|
8
|
+
OP_LABELS,
|
|
9
|
+
DecoderChain,
|
|
10
|
+
decode_op,
|
|
11
|
+
decode_smart,
|
|
12
|
+
encode_op,
|
|
13
|
+
run_chain,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"OPERATIONS",
|
|
18
|
+
"OP_LABELS",
|
|
19
|
+
"DecoderChain",
|
|
20
|
+
"decode_op",
|
|
21
|
+
"decode_smart",
|
|
22
|
+
"encode_op",
|
|
23
|
+
"run_chain",
|
|
24
|
+
]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""Публичный API Intruder-модуля для TUI и CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from pentool.modules.intruder import (
|
|
8
|
+
AttackType,
|
|
9
|
+
IntruderAttack,
|
|
10
|
+
IntruderConfig,
|
|
11
|
+
IntruderResult,
|
|
12
|
+
count_markers,
|
|
13
|
+
extract_marker_defaults,
|
|
14
|
+
generate_char_payloads,
|
|
15
|
+
generate_numeric_payloads,
|
|
16
|
+
load_payloads_from_file,
|
|
17
|
+
process_payload,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"IntruderAPI",
|
|
22
|
+
"AttackType",
|
|
23
|
+
"IntruderAttack",
|
|
24
|
+
"IntruderConfig",
|
|
25
|
+
"IntruderResult",
|
|
26
|
+
"count_markers",
|
|
27
|
+
"extract_marker_defaults",
|
|
28
|
+
"generate_char_payloads",
|
|
29
|
+
"generate_numeric_payloads",
|
|
30
|
+
"load_payloads_from_file",
|
|
31
|
+
"process_payload",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class IntruderAPI:
|
|
36
|
+
|
|
37
|
+
def __init__(self, db_path: str | None = None) -> None:
|
|
38
|
+
self._db_path = db_path
|
|
39
|
+
self._attack: IntruderAttack | None = None
|
|
40
|
+
|
|
41
|
+
async def start_attack(
|
|
42
|
+
self,
|
|
43
|
+
config: IntruderConfig,
|
|
44
|
+
on_result=None,
|
|
45
|
+
on_progress=None,
|
|
46
|
+
turbo_mode: bool = False,
|
|
47
|
+
) -> str:
|
|
48
|
+
if turbo_mode:
|
|
49
|
+
# Turbo Mode: connection pooling + Keep-Alive
|
|
50
|
+
from pentool.modules.intruder_turbo import TurboIntruderAttack
|
|
51
|
+
self._attack = TurboIntruderAttack(config)
|
|
52
|
+
else:
|
|
53
|
+
# Обычный режим
|
|
54
|
+
self._attack = IntruderAttack(config, db_path=self._db_path)
|
|
55
|
+
|
|
56
|
+
_on_result = on_result if on_result else lambda r: None
|
|
57
|
+
_on_progress = on_progress if on_progress else lambda d, t: None
|
|
58
|
+
|
|
59
|
+
import asyncio
|
|
60
|
+
asyncio.create_task(self._attack.run(_on_result, _on_progress))
|
|
61
|
+
return self._attack.attack_id if hasattr(self._attack, 'attack_id') else "turbo"
|
|
62
|
+
|
|
63
|
+
async def pause(self) -> None:
|
|
64
|
+
if self._attack:
|
|
65
|
+
await self._attack.pause()
|
|
66
|
+
|
|
67
|
+
async def resume(self) -> None:
|
|
68
|
+
"""Возобновить атаку после паузы."""
|
|
69
|
+
if self._attack:
|
|
70
|
+
await self._attack.resume()
|
|
71
|
+
|
|
72
|
+
async def stop(self) -> None:
|
|
73
|
+
if self._attack:
|
|
74
|
+
await self._attack.stop()
|
|
75
|
+
|
|
76
|
+
def get_results(self) -> list[IntruderResult]:
|
|
77
|
+
if self._attack:
|
|
78
|
+
# Turbo mode использует get_results(), обычный - results property
|
|
79
|
+
if hasattr(self._attack, 'get_results'):
|
|
80
|
+
return self._attack.get_results()
|
|
81
|
+
return self._attack.results
|
|
82
|
+
return []
|
|
83
|
+
|
|
84
|
+
def get_progress(self) -> tuple[int, int]:
|
|
85
|
+
if self._attack:
|
|
86
|
+
return self._attack.progress
|
|
87
|
+
return (0, 0)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_running(self) -> bool:
|
|
91
|
+
return bool(self._attack and self._attack.is_running)
|
|
92
|
+
|
|
93
|
+
async def load_payloads(self, path: str) -> list[str]:
|
|
94
|
+
return load_payloads_from_file(path)
|
|
95
|
+
|
|
96
|
+
async def generate_numeric(self, start: int, end: int, step: int = 1) -> list[str]:
|
|
97
|
+
return generate_numeric_payloads(start, end, step)
|
|
98
|
+
|
|
99
|
+
async def generate_chars(
|
|
100
|
+
self, charset: str, min_len: int, max_len: int
|
|
101
|
+
) -> list[str]:
|
|
102
|
+
return generate_char_payloads(charset, min_len, max_len)
|
|
103
|
+
|
|
104
|
+
def export_csv(self, path: str) -> None:
|
|
105
|
+
if self._attack:
|
|
106
|
+
self._attack.export_csv(path)
|
|
107
|
+
|
|
108
|
+
# ── Project persistence ────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
def export_project_data(self) -> dict:
|
|
111
|
+
results = self.get_results()
|
|
112
|
+
return {
|
|
113
|
+
"results": [
|
|
114
|
+
{
|
|
115
|
+
"id": r.id,
|
|
116
|
+
"attack_id": r.attack_id,
|
|
117
|
+
"request_number": r.request_number,
|
|
118
|
+
"payload_values": r.payload_values,
|
|
119
|
+
"request_raw": r.request_raw,
|
|
120
|
+
"response_status": r.response_status,
|
|
121
|
+
"response_length": r.response_length,
|
|
122
|
+
"response_time_ms": r.response_time_ms,
|
|
123
|
+
"error": r.error,
|
|
124
|
+
"timestamp": r.timestamp.isoformat(),
|
|
125
|
+
}
|
|
126
|
+
for r in results
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
def import_project_data(self, data: dict) -> int:
|
|
131
|
+
from datetime import datetime, timezone
|
|
132
|
+
from pentool.modules.intruder import IntruderResult
|
|
133
|
+
|
|
134
|
+
results_data = data.get("results", [])
|
|
135
|
+
# Создаём фиктивный attack для хранения результатов
|
|
136
|
+
# без реального запуска атаки
|
|
137
|
+
if self._attack is None:
|
|
138
|
+
# Ленивая инициализация — просто храним в атрибуте
|
|
139
|
+
self._restored_results: list[IntruderResult] = []
|
|
140
|
+
else:
|
|
141
|
+
self._restored_results = []
|
|
142
|
+
|
|
143
|
+
loaded = 0
|
|
144
|
+
for rd in results_data:
|
|
145
|
+
try:
|
|
146
|
+
ts_raw = rd.get("timestamp", "")
|
|
147
|
+
try:
|
|
148
|
+
ts = datetime.fromisoformat(ts_raw)
|
|
149
|
+
except Exception:
|
|
150
|
+
ts = datetime.now(timezone.utc)
|
|
151
|
+
result = IntruderResult(
|
|
152
|
+
id=rd.get("id", ""),
|
|
153
|
+
attack_id=rd.get("attack_id", ""),
|
|
154
|
+
request_number=rd.get("request_number", 0),
|
|
155
|
+
payload_values=rd.get("payload_values", []),
|
|
156
|
+
request_raw=rd.get("request_raw", ""),
|
|
157
|
+
response_status=rd.get("response_status"),
|
|
158
|
+
response_length=rd.get("response_length"),
|
|
159
|
+
response_time_ms=rd.get("response_time_ms"),
|
|
160
|
+
error=rd.get("error"),
|
|
161
|
+
timestamp=ts,
|
|
162
|
+
)
|
|
163
|
+
if hasattr(self, "_restored_results"):
|
|
164
|
+
self._restored_results.append(result)
|
|
165
|
+
loaded += 1
|
|
166
|
+
except Exception as exc:
|
|
167
|
+
from pentool.core.logging import get_logger
|
|
168
|
+
get_logger(__name__).warning(
|
|
169
|
+
"IntruderAPI.import_project_data: skip result: %s", exc
|
|
170
|
+
)
|
|
171
|
+
return loaded
|
pentool/api/proxy_api.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Публичный API прокси-модуля для TUI и CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Callable
|
|
6
|
+
|
|
7
|
+
from pentool.modules.proxy import InterceptedRequest, MatchReplaceRule, ProxyServer
|
|
8
|
+
from pentool.core.logging import get_logger
|
|
9
|
+
|
|
10
|
+
logger = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
# Реэкспорт типов — TUI импортирует их отсюда, не из modules.proxy
|
|
13
|
+
__all__ = ["ProxyAPI", "InterceptedRequest", "MatchReplaceRule", "ProxyServer"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ProxyAPI:
|
|
17
|
+
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._proxy: ProxyServer | None = None
|
|
20
|
+
|
|
21
|
+
def set_proxy(self, proxy: ProxyServer) -> None:
|
|
22
|
+
logger.debug("ProxyAPI: set_proxy called, proxy=%s", proxy)
|
|
23
|
+
self._proxy = proxy
|
|
24
|
+
|
|
25
|
+
def get_proxy(self) -> ProxyServer | None:
|
|
26
|
+
return self._proxy
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def proxy(self) -> ProxyServer | None:
|
|
30
|
+
"""Легитимный доступ к ProxyServer через API-слой.
|
|
31
|
+
|
|
32
|
+
Используется там где нужен прямой доступ к серверу
|
|
33
|
+
(proxy/screen.py для intercept, repeater/screen.py для scope).
|
|
34
|
+
"""
|
|
35
|
+
return self._proxy
|
|
36
|
+
|
|
37
|
+
def create_proxy(
|
|
38
|
+
self,
|
|
39
|
+
host: str = "127.0.0.1",
|
|
40
|
+
port: int = 8080,
|
|
41
|
+
cert_dir: str = "/tmp/pentool_certs",
|
|
42
|
+
db_path: str | None = None,
|
|
43
|
+
) -> ProxyServer:
|
|
44
|
+
proxy = ProxyServer(host=host, port=port, cert_dir=cert_dir, db_path=db_path)
|
|
45
|
+
self._proxy = proxy
|
|
46
|
+
return proxy
|
|
47
|
+
|
|
48
|
+
def is_running(self) -> bool:
|
|
49
|
+
return bool(self._proxy and self._proxy.is_running)
|
|
50
|
+
|
|
51
|
+
def get_port(self) -> int:
|
|
52
|
+
if self._proxy:
|
|
53
|
+
return self._proxy.port
|
|
54
|
+
return 8080
|
|
55
|
+
|
|
56
|
+
def get_host(self) -> str:
|
|
57
|
+
if self._proxy:
|
|
58
|
+
return self._proxy.host
|
|
59
|
+
return "127.0.0.1"
|
|
60
|
+
|
|
61
|
+
def get_status(self) -> dict:
|
|
62
|
+
if self._proxy is None:
|
|
63
|
+
return {
|
|
64
|
+
"running": False,
|
|
65
|
+
"host": "127.0.0.1",
|
|
66
|
+
"port": 8080,
|
|
67
|
+
"intercept_enabled": False,
|
|
68
|
+
"scope": [],
|
|
69
|
+
"rules_count": 0,
|
|
70
|
+
"requests_count": 0,
|
|
71
|
+
"waiting_count": 0,
|
|
72
|
+
}
|
|
73
|
+
return self._proxy.get_status()
|
|
74
|
+
|
|
75
|
+
def get_requests(
|
|
76
|
+
self,
|
|
77
|
+
limit: int = 100,
|
|
78
|
+
method: str | None = None,
|
|
79
|
+
host: str | None = None,
|
|
80
|
+
) -> list[InterceptedRequest]:
|
|
81
|
+
if self._proxy is None:
|
|
82
|
+
return []
|
|
83
|
+
return self._proxy.get_requests(limit=limit, method=method, host=host)
|
|
84
|
+
|
|
85
|
+
def find_request(self, req_id: str) -> InterceptedRequest | None:
|
|
86
|
+
"""Найти запрос по ID.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
req_id: UUID запроса (полный или частичный).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
InterceptedRequest или None.
|
|
93
|
+
"""
|
|
94
|
+
if self._proxy is None:
|
|
95
|
+
return None
|
|
96
|
+
return self._proxy._find_request(req_id)
|
|
97
|
+
|
|
98
|
+
def clear_requests(self) -> None:
|
|
99
|
+
if self._proxy:
|
|
100
|
+
self._proxy.clear_requests()
|
|
101
|
+
|
|
102
|
+
def forward(self, req_id: str, modified_raw: str | None = None) -> None:
|
|
103
|
+
"""Переслать ожидающий запрос на целевой сервер.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
req_id: ID перехваченного запроса.
|
|
107
|
+
modified_raw: Изменённый сырой HTTP-текст (опционально).
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
None
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
RuntimeError: Если прокси не инициализирован.
|
|
114
|
+
"""
|
|
115
|
+
if self._proxy is None:
|
|
116
|
+
raise RuntimeError("ProxyAPI: прокси не инициализирован")
|
|
117
|
+
logger.debug("ProxyAPI: forward req_id=%s, has_modified=%s", req_id, bool(modified_raw))
|
|
118
|
+
self._proxy.forward(req_id, modified_raw)
|
|
119
|
+
|
|
120
|
+
def drop(self, req_id: str) -> None:
|
|
121
|
+
"""Сбросить ожидающий запрос (вернуть браузеру 502).
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
req_id: ID перехваченного запроса.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
None
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
RuntimeError: Если прокси не инициализирован.
|
|
131
|
+
"""
|
|
132
|
+
if self._proxy is None:
|
|
133
|
+
raise RuntimeError("ProxyAPI: прокси не инициализирован")
|
|
134
|
+
logger.debug("ProxyAPI: drop req_id=%s", req_id)
|
|
135
|
+
self._proxy.drop(req_id)
|
|
136
|
+
|
|
137
|
+
def set_intercept(self, enabled: bool) -> None:
|
|
138
|
+
if self._proxy:
|
|
139
|
+
# ProxyServer.set_intercept() использует call_soon_threadsafe —
|
|
140
|
+
# обязательно для безопасного изменения флага из TUI-треда,
|
|
141
|
+
# пока proxy-loop работает в своём asyncio-треде.
|
|
142
|
+
self._proxy.set_intercept(enabled)
|
|
143
|
+
|
|
144
|
+
def get_intercept(self) -> bool:
|
|
145
|
+
return bool(self._proxy and self._proxy.intercept_enabled)
|
|
146
|
+
|
|
147
|
+
def set_scope(self, hosts: list[str]) -> None:
|
|
148
|
+
if self._proxy:
|
|
149
|
+
self._proxy.set_scope(hosts)
|
|
150
|
+
|
|
151
|
+
def get_scope(self) -> list[str]:
|
|
152
|
+
if self._proxy:
|
|
153
|
+
return list(self._proxy.scope)
|
|
154
|
+
return []
|
|
155
|
+
|
|
156
|
+
def get_match_replace_rules(self) -> list[MatchReplaceRule]:
|
|
157
|
+
if self._proxy:
|
|
158
|
+
return list(self._proxy.match_replace_rules)
|
|
159
|
+
return []
|
|
160
|
+
|
|
161
|
+
def set_match_replace_rules(self, rules: list[MatchReplaceRule]) -> None:
|
|
162
|
+
"""Заменить все правила match/replace.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
rules: Новый список правил MatchReplaceRule.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
None
|
|
169
|
+
"""
|
|
170
|
+
if self._proxy:
|
|
171
|
+
self._proxy.match_replace_rules = rules
|
|
172
|
+
|
|
173
|
+
def export_project_data(self) -> dict:
|
|
174
|
+
if self._proxy is None:
|
|
175
|
+
return {"proxy": {"scope": [], "match_replace": []}, "http_history": []}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
"proxy": {
|
|
179
|
+
"scope": list(self._proxy.scope),
|
|
180
|
+
"match_replace": [r.to_dict() for r in self._proxy.match_replace_rules],
|
|
181
|
+
},
|
|
182
|
+
"http_history": [r.to_dict() for r in self._proxy.get_requests(limit=10000)],
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
def import_project_data(self, data: dict) -> tuple[int, str]:
|
|
186
|
+
if self._proxy is None:
|
|
187
|
+
return 0, "Proxy not initialized"
|
|
188
|
+
|
|
189
|
+
from pentool.utils.parser import ParsedResponse
|
|
190
|
+
from datetime import datetime, timezone
|
|
191
|
+
|
|
192
|
+
# Scope
|
|
193
|
+
self._proxy.scope = data.get("proxy", {}).get("scope", [])
|
|
194
|
+
|
|
195
|
+
# Match/Replace
|
|
196
|
+
mr_data = data.get("proxy", {}).get("match_replace", [])
|
|
197
|
+
self._proxy.match_replace_rules = [MatchReplaceRule.from_dict(r) for r in mr_data]
|
|
198
|
+
|
|
199
|
+
# HTTP History
|
|
200
|
+
self._proxy.requests.clear()
|
|
201
|
+
loaded = 0
|
|
202
|
+
for req_data in data.get("http_history", []):
|
|
203
|
+
try:
|
|
204
|
+
req = InterceptedRequest.from_dict(req_data)
|
|
205
|
+
self._proxy.requests.append(req)
|
|
206
|
+
loaded += 1
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.warning("import_project_data: failed to restore request: %s", e)
|
|
209
|
+
|
|
210
|
+
return loaded, ""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Публичный API repeater-модуля для TUI и CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from pentool.modules.repeater import Repeater, RepeaterEntry
|
|
8
|
+
from pentool.utils.parser import ParsedRequest, ParsedResponse
|
|
9
|
+
|
|
10
|
+
# Реэкспорт типов
|
|
11
|
+
__all__ = ["RepeaterAPI", "RepeaterEntry"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RepeaterAPI:
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
db_path: str,
|
|
19
|
+
project_id: int | None = None,
|
|
20
|
+
timeout: float = 30.0,
|
|
21
|
+
verify_ssl: bool = False,
|
|
22
|
+
) -> None:
|
|
23
|
+
self._repeater = Repeater(
|
|
24
|
+
db_path=db_path,
|
|
25
|
+
project_id=project_id,
|
|
26
|
+
timeout=timeout,
|
|
27
|
+
verify_ssl=verify_ssl,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
async def send(
|
|
31
|
+
self,
|
|
32
|
+
request: ParsedRequest,
|
|
33
|
+
tab_name: str = "Tab",
|
|
34
|
+
save: bool = True,
|
|
35
|
+
) -> ParsedResponse:
|
|
36
|
+
return await self._repeater.send(request, tab_name=tab_name, save=save)
|
|
37
|
+
|
|
38
|
+
async def save_to_history(
|
|
39
|
+
self,
|
|
40
|
+
request: ParsedRequest,
|
|
41
|
+
response: ParsedResponse,
|
|
42
|
+
tab_name: str = "Tab",
|
|
43
|
+
) -> int:
|
|
44
|
+
return await self._repeater.save_to_history(request, response, tab_name)
|
|
45
|
+
|
|
46
|
+
async def get_history(
|
|
47
|
+
self,
|
|
48
|
+
limit: int = 50,
|
|
49
|
+
project_id: int | None = None,
|
|
50
|
+
) -> list[RepeaterEntry]:
|
|
51
|
+
return await self._repeater.get_history(limit=limit, project_id=project_id)
|
|
52
|
+
|
|
53
|
+
async def get_entry(self, entry_id: int) -> RepeaterEntry | None:
|
|
54
|
+
return await self._repeater.get_entry(entry_id)
|
|
55
|
+
|
|
56
|
+
async def delete_entry(self, entry_id: int) -> None:
|
|
57
|
+
return await self._repeater.delete_entry(entry_id)
|