pentool 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. pentool/__init__.py +4 -0
  2. pentool/__main__.py +16 -0
  3. pentool/api/__init__.py +41 -0
  4. pentool/api/base_api.py +48 -0
  5. pentool/api/comparer_api.py +19 -0
  6. pentool/api/decoder_api.py +24 -0
  7. pentool/api/intruder_api.py +171 -0
  8. pentool/api/proxy_api.py +210 -0
  9. pentool/api/repeater_api.py +57 -0
  10. pentool/api/scanner_api.py +266 -0
  11. pentool/api/sequencer_api.py +17 -0
  12. pentool/api/spider_api.py +106 -0
  13. pentool/api/target_api.py +87 -0
  14. pentool/cli/__init__.py +1 -0
  15. pentool/cli/main.py +98 -0
  16. pentool/cli/project.py +88 -0
  17. pentool/cli/proxy.py +173 -0
  18. pentool/cli/scan.py +115 -0
  19. pentool/core/__init__.py +1 -0
  20. pentool/core/config.py +171 -0
  21. pentool/core/crash_reporter.py +75 -0
  22. pentool/core/database.py +139 -0
  23. pentool/core/event_bus.py +207 -0
  24. pentool/core/events.py +160 -0
  25. pentool/core/features.py +373 -0
  26. pentool/core/license.py +274 -0
  27. pentool/core/logging.py +54 -0
  28. pentool/core/plugin_manager.py +312 -0
  29. pentool/core/project.py +31 -0
  30. pentool/core/storage_interface.py +307 -0
  31. pentool/core/updater.py +96 -0
  32. pentool/core/utils.py +36 -0
  33. pentool/modules/__init__.py +1 -0
  34. pentool/modules/comparer.py +177 -0
  35. pentool/modules/decoder.py +281 -0
  36. pentool/modules/intruder.py +428 -0
  37. pentool/modules/intruder_turbo.py +176 -0
  38. pentool/modules/match_replace.py +142 -0
  39. pentool/modules/proxy.py +797 -0
  40. pentool/modules/repeater.py +195 -0
  41. pentool/modules/sequencer.py +492 -0
  42. pentool/modules/spider.py +679 -0
  43. pentool/modules/target.py +185 -0
  44. pentool/modules/websocket_handler.py +249 -0
  45. pentool/plugins/__init__.py +1 -0
  46. pentool/plugins/builtin/__init__.py +1 -0
  47. pentool/plugins/builtin/payloads_pro.py +381 -0
  48. pentool/plugins/builtin/reports_pro.py +436 -0
  49. pentool/plugins/builtin/scanner_pro.py +291 -0
  50. pentool/plugins/example_plugin.py +39 -0
  51. pentool/services/__init__.py +1 -0
  52. pentool/services/base_service.py +60 -0
  53. pentool/services/intruder_service.py +94 -0
  54. pentool/services/proxy_service.py +178 -0
  55. pentool/services/repeater_service.py +85 -0
  56. pentool/services/scan_service.py +330 -0
  57. pentool/storage/__init__.py +0 -0
  58. pentool/storage/http_storage.py +506 -0
  59. pentool/storage/large_body_handler.py +50 -0
  60. pentool/storage/lru_cache.py +45 -0
  61. pentool/t.py +213 -0
  62. pentool/tui/__init__.py +1 -0
  63. pentool/tui/app.py +1429 -0
  64. pentool/tui/constants.py +79 -0
  65. pentool/tui/dialogs/__init__.py +0 -0
  66. pentool/tui/dialogs/cert_dialog.py +81 -0
  67. pentool/tui/dialogs/file_selector.py +226 -0
  68. pentool/tui/dialogs/load_from_proxy.py +125 -0
  69. pentool/tui/dialogs/match_replace_dialog.py +229 -0
  70. pentool/tui/dialogs/project_dialog.py +82 -0
  71. pentool/tui/dialogs/scope_dialog.py +224 -0
  72. pentool/tui/messages.py +103 -0
  73. pentool/tui/mixins/__init__.py +0 -0
  74. pentool/tui/mixins/app_mixin.py +59 -0
  75. pentool/tui/mixins/dialog_cancel.py +21 -0
  76. pentool/tui/mixins/request_context_menu.py +247 -0
  77. pentool/tui/mixins/tab_rename.py +89 -0
  78. pentool/tui/screens/__init__.py +31 -0
  79. pentool/tui/screens/comparer/__init__.py +3 -0
  80. pentool/tui/screens/comparer/screen.py +229 -0
  81. pentool/tui/screens/dashboard/__init__.py +2 -0
  82. pentool/tui/screens/dashboard/live_dashboard.py +730 -0
  83. pentool/tui/screens/dashboard/screen.py +771 -0
  84. pentool/tui/screens/decoder/__init__.py +3 -0
  85. pentool/tui/screens/decoder/screen.py +306 -0
  86. pentool/tui/screens/extensions/__init__.py +3 -0
  87. pentool/tui/screens/extensions/screen.py +24 -0
  88. pentool/tui/screens/intruder/__init__.py +3 -0
  89. pentool/tui/screens/intruder/screen.py +1356 -0
  90. pentool/tui/screens/proxy/__init__.py +3 -0
  91. pentool/tui/screens/proxy/screen.py +1554 -0
  92. pentool/tui/screens/repeater/__init__.py +3 -0
  93. pentool/tui/screens/repeater/screen.py +621 -0
  94. pentool/tui/screens/scanner/__init__.py +3 -0
  95. pentool/tui/screens/scanner/screen.py +1956 -0
  96. pentool/tui/screens/sequencer/__init__.py +3 -0
  97. pentool/tui/screens/sequencer/screen.py +516 -0
  98. pentool/tui/screens/settings/__init__.py +5 -0
  99. pentool/tui/screens/settings/hotkeys.py +134 -0
  100. pentool/tui/screens/settings/screen.py +540 -0
  101. pentool/tui/screens/spider/__init__.py +3 -0
  102. pentool/tui/screens/spider/screen.py +380 -0
  103. pentool/tui/screens/target/__init__.py +4 -0
  104. pentool/tui/screens/target/screen.py +358 -0
  105. pentool/tui/screens/terminal/__init__.py +5 -0
  106. pentool/tui/screens/terminal/screen.py +179 -0
  107. pentool/tui/widgets/__init__.py +1 -0
  108. pentool/tui/widgets/context_menu.py +148 -0
  109. pentool/tui/widgets/filter_bar.py +204 -0
  110. pentool/tui/widgets/inspector_panel.py +111 -0
  111. pentool/tui/widgets/menu.py +86 -0
  112. pentool/tui/widgets/menu_bar.py +238 -0
  113. pentool/tui/widgets/module_tabs.py +68 -0
  114. pentool/tui/widgets/payload_drop_zone.py +65 -0
  115. pentool/tui/widgets/request_editor.py +495 -0
  116. pentool/tui/widgets/resize_handle.py +116 -0
  117. pentool/tui/widgets/search_bar.py +112 -0
  118. pentool/tui/widgets/statusbar.py +96 -0
  119. pentool/tui/widgets/toolbar_button.py +68 -0
  120. pentool/utils/__init__.py +1 -0
  121. pentool/utils/cert.py +314 -0
  122. pentool/utils/coder.py +170 -0
  123. pentool/utils/copy_as.py +250 -0
  124. pentool/utils/diff.py +82 -0
  125. pentool/utils/http_client.py +145 -0
  126. pentool/utils/parser.py +227 -0
  127. pentool/utils/terminal_check.py +31 -0
  128. pentool-0.1.0.dist-info/METADATA +231 -0
  129. pentool-0.1.0.dist-info/RECORD +133 -0
  130. pentool-0.1.0.dist-info/WHEEL +5 -0
  131. pentool-0.1.0.dist-info/entry_points.txt +2 -0
  132. pentool-0.1.0.dist-info/licenses/LICENSE +34 -0
  133. pentool-0.1.0.dist-info/top_level.txt +1 -0
pentool/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Pentool — консольный аналог Burp Suite с TUI на Textual."""
2
+
3
+ __version__ = "0.1.0"
4
+ __author__ = "pentool"
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()
@@ -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
+ ]
@@ -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
@@ -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)