pentool 0.2.1__tar.gz → 0.2.3__tar.gz
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-0.2.1 → pentool-0.2.3}/PKG-INFO +1 -1
- {pentool-0.2.1 → pentool-0.2.3}/pentool/__init__.py +1 -1
- {pentool-0.2.1 → pentool-0.2.3}/pentool/cli/main.py +27 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/database.py +36 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/license.py +86 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/proxy.py +48 -3
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/app.py +76 -4
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/match_replace_dialog.py +1 -1
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/project_manager.py +121 -64
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/intruder/screen.py +1 -1
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/proxy/screen.py +184 -18
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/proxy/screen.tcss +0 -8
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/repeater/screen.py +2 -21
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/settings/screen.py +7 -4
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/spider/screen.py +1 -1
- pentool-0.2.3/pentool/tui/widgets/nice_checkbox.py +35 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/request_editor.py +44 -15
- {pentool-0.2.1 → pentool-0.2.3}/pentool.egg-info/PKG-INFO +1 -1
- {pentool-0.2.1 → pentool-0.2.3}/pentool.egg-info/SOURCES.txt +1 -0
- {pentool-0.2.1 → pentool-0.2.3}/pyproject.toml +1 -1
- {pentool-0.2.1 → pentool-0.2.3}/LICENSE +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/README.md +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/__main__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/api/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/api/base_api.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/api/comparer_api.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/api/decoder_api.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/api/intruder_api.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/api/proxy_api.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/api/repeater_api.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/api/sequencer_api.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/api/spider_api.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/api/target_api.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/cli/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/cli/project.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/cli/proxy.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/cli/scan.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/config.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/crash_reporter.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/event_bus.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/events.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/features.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/logging.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/plugin_manager.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/project.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/storage_interface.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/updater.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/core/utils.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/comparer.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/decoder.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/intruder.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/match_replace.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/repeater.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/sequencer.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/spider.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/target.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/modules/websocket_handler.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/plugins/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/plugins/builtin/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/plugins/example_plugin.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/plugins/example_plugin.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/services/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/services/base_service.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/services/intruder_service.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/services/proxy_service.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/services/repeater_service.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/services/scan_service.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/storage/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/storage/http_storage.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/storage/large_body_handler.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/storage/lru_cache.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/t.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/constants.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/cert_dialog.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/cert_dialog.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/file_selector.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/file_selector.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/load_from_proxy.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/load_from_proxy.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/match_replace_dialog.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/project_dialog.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/project_dialog.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/scope_dialog.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/dialogs/scope_dialog.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/messages.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/mixins/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/mixins/app_mixin.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/mixins/dialog_cancel.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/mixins/request_context_menu.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/base.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/comparer/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/comparer/screen.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/comparer/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/dashboard/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/dashboard/live_dashboard.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/dashboard/screen.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/dashboard/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/decoder/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/decoder/screen.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/decoder/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/extensions/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/extensions/screen.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/extensions/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/intruder/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/intruder/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/proxy/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/repeater/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/repeater/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/scanner_unavailable.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/sequencer/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/sequencer/screen.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/sequencer/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/settings/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/settings/hotkeys.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/settings/hotkeys.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/settings/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/spider/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/spider/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/target/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/target/screen.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/target/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/terminal/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/terminal/screen.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/screens/terminal/screen.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/activity_indicator.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/activity_indicator.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/context_menu.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/context_menu.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/filter_bar.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/filter_bar.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/inspector_panel.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/inspector_panel.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/menu.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/menu_bar.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/module_tabs.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/module_tabs.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/payload_drop_zone.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/payload_drop_zone.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/request_editor.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/resize_handle.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/resize_handle.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/search_bar.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/search_bar.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/statusbar.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/statusbar.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/toolbar_button.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/tui/widgets/toolbar_button.tcss +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/utils/__init__.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/utils/cert.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/utils/coder.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/utils/copy_as.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/utils/diff.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/utils/http_client.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/utils/parser.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool/utils/terminal_check.py +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool.egg-info/dependency_links.txt +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool.egg-info/entry_points.txt +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool.egg-info/requires.txt +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/pentool.egg-info/top_level.txt +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/setup.cfg +0 -0
- {pentool-0.2.1 → pentool-0.2.3}/tests/test_user_workflow.py +0 -0
|
@@ -126,6 +126,7 @@ def update_cmd(check_only: bool) -> None:
|
|
|
126
126
|
click.echo("Upgrading via pip...")
|
|
127
127
|
if do_pip_upgrade():
|
|
128
128
|
click.echo("Upgrade successful. Restart Pentool to use the new version.")
|
|
129
|
+
_sync_pro_package_after_upgrade()
|
|
129
130
|
else:
|
|
130
131
|
click.echo(
|
|
131
132
|
"pip upgrade failed. You can update manually:\n"
|
|
@@ -135,6 +136,32 @@ def update_cmd(check_only: bool) -> None:
|
|
|
135
136
|
raise SystemExit(1)
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
def _sync_pro_package_after_upgrade() -> None:
|
|
140
|
+
"""Re-download the PRO package too, if one is active on this machine.
|
|
141
|
+
|
|
142
|
+
`pip install --upgrade pentool` only touches the FREE package — the PRO
|
|
143
|
+
package lives separately in ~/.pentool/pro/ and is otherwise only ever
|
|
144
|
+
(re)downloaded by check_and_update_pro_package(), which normally runs in
|
|
145
|
+
the background on TUI startup. Doing it here too means a CLI-driven
|
|
146
|
+
upgrade doesn't leave a stale PRO build around until the next TUI launch.
|
|
147
|
+
Best-effort — never fails the overall upgrade if this part doesn't work.
|
|
148
|
+
"""
|
|
149
|
+
import asyncio
|
|
150
|
+
|
|
151
|
+
from pentool.core.license import PRO_PACKAGE_DIR, check_and_update_pro_package
|
|
152
|
+
|
|
153
|
+
if not PRO_PACKAGE_DIR.exists():
|
|
154
|
+
return # PRO was never activated on this machine — nothing to sync
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
click.echo("Checking for a newer PRO build...")
|
|
158
|
+
updated = asyncio.run(check_and_update_pro_package())
|
|
159
|
+
if updated:
|
|
160
|
+
click.echo("PRO package updated to the latest build.")
|
|
161
|
+
except Exception as exc:
|
|
162
|
+
click.echo(f"Could not check for a PRO update: {exc}", err=True)
|
|
163
|
+
|
|
164
|
+
|
|
138
165
|
@cli.group()
|
|
139
166
|
def license() -> None:
|
|
140
167
|
"""License management: trial, activation, status."""
|
|
@@ -95,6 +95,14 @@ CREATE TABLE IF NOT EXISTS site_map (
|
|
|
95
95
|
);
|
|
96
96
|
|
|
97
97
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_site_map_host_path ON site_map (host, path);
|
|
98
|
+
|
|
99
|
+
-- Generic per-project key/value settings (e.g. "proxy.enforce_scope").
|
|
100
|
+
-- Lives in the project .db file itself so it travels with the project,
|
|
101
|
+
-- unlike pentool's global ~/.config/pentool/config.yaml.
|
|
102
|
+
CREATE TABLE IF NOT EXISTS project_settings (
|
|
103
|
+
key TEXT PRIMARY KEY,
|
|
104
|
+
value TEXT NOT NULL DEFAULT ''
|
|
105
|
+
);
|
|
98
106
|
"""
|
|
99
107
|
|
|
100
108
|
|
|
@@ -170,3 +178,31 @@ def init_db_sync(db_path: str) -> None:
|
|
|
170
178
|
db_path: Path to the SQLite file.
|
|
171
179
|
"""
|
|
172
180
|
asyncio.run(init_db(db_path))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
async def get_project_setting(db_path: str, key: str, default: str | None = None) -> str | None:
|
|
184
|
+
"""Read a single per-project key/value setting from `project_settings`.
|
|
185
|
+
|
|
186
|
+
Returns `default` if the key is missing or the table/column doesn't
|
|
187
|
+
exist yet (e.g. a brand-new .db before init_db() has run).
|
|
188
|
+
"""
|
|
189
|
+
try:
|
|
190
|
+
async with aiosqlite.connect(db_path) as db:
|
|
191
|
+
async with db.execute(
|
|
192
|
+
"SELECT value FROM project_settings WHERE key = ?", (key,)
|
|
193
|
+
) as cur:
|
|
194
|
+
row = await cur.fetchone()
|
|
195
|
+
return row[0] if row else default
|
|
196
|
+
except Exception:
|
|
197
|
+
return default
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
async def set_project_setting(db_path: str, key: str, value: str) -> None:
|
|
201
|
+
"""Upsert a single per-project key/value setting into `project_settings`."""
|
|
202
|
+
async with aiosqlite.connect(db_path) as db:
|
|
203
|
+
await db.execute(
|
|
204
|
+
"INSERT INTO project_settings (key, value) VALUES (?, ?) "
|
|
205
|
+
"ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
|
206
|
+
(key, value),
|
|
207
|
+
)
|
|
208
|
+
await db.commit()
|
|
@@ -503,6 +503,13 @@ def _current_platform() -> str:
|
|
|
503
503
|
return "linux"
|
|
504
504
|
|
|
505
505
|
|
|
506
|
+
# File recording which PRO build is currently unpacked into PRO_PACKAGE_DIR —
|
|
507
|
+
# lets check_and_update_pro_package() tell "same build as last time" apart
|
|
508
|
+
# from "a new build was published" without re-downloading the archive to
|
|
509
|
+
# compare it. See _fetch_pro_build_id() / /api/pro/version on the Worker.
|
|
510
|
+
_PRO_BUILD_MARKER = PRO_PACKAGE_DIR / ".build_id"
|
|
511
|
+
|
|
512
|
+
|
|
506
513
|
async def download_pro_package(key: str, machine_id: str) -> bool:
|
|
507
514
|
"""Download, verify, and install the obfuscated PRO package.
|
|
508
515
|
|
|
@@ -555,6 +562,85 @@ async def download_pro_package(key: str, machine_id: str) -> bool:
|
|
|
555
562
|
finally:
|
|
556
563
|
tmp_archive.unlink(missing_ok=True)
|
|
557
564
|
|
|
565
|
+
# Record which build this is, so a later check_and_update_pro_package()
|
|
566
|
+
# can tell "nothing changed" from "a new build was published" without
|
|
567
|
+
# re-downloading the archive just to compare it.
|
|
568
|
+
try:
|
|
569
|
+
build_id = await _fetch_pro_build_id(plat)
|
|
570
|
+
if build_id:
|
|
571
|
+
_PRO_BUILD_MARKER.write_text(build_id, encoding="utf-8")
|
|
572
|
+
except Exception:
|
|
573
|
+
pass # non-fatal — worst case, next check just re-downloads once more
|
|
574
|
+
|
|
558
575
|
return True
|
|
559
576
|
except Exception:
|
|
560
577
|
return False
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
async def _fetch_pro_build_id(plat: str) -> str | None:
|
|
581
|
+
"""Query /api/pro/version for the current PRO release's build_id.
|
|
582
|
+
|
|
583
|
+
No license key needed — this only identifies which build is published,
|
|
584
|
+
not whether the caller is entitled to it (that's still enforced by
|
|
585
|
+
/api/download re-validating the key server-side). Returns None on any
|
|
586
|
+
failure (network, missing asset, etc.) — callers should treat that as
|
|
587
|
+
"couldn't check, don't act on it" rather than "no update available".
|
|
588
|
+
"""
|
|
589
|
+
try:
|
|
590
|
+
import aiohttp
|
|
591
|
+
async with aiohttp.ClientSession(
|
|
592
|
+
timeout=aiohttp.ClientTimeout(total=8)
|
|
593
|
+
) as session:
|
|
594
|
+
async with session.get(
|
|
595
|
+
f"{_LICENSE_API_BASE}/api/pro/version",
|
|
596
|
+
params={"platform": plat},
|
|
597
|
+
ssl=False,
|
|
598
|
+
) as resp:
|
|
599
|
+
if resp.status != 200:
|
|
600
|
+
return None
|
|
601
|
+
data = await resp.json()
|
|
602
|
+
return data.get("build_id")
|
|
603
|
+
except Exception:
|
|
604
|
+
return None
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
async def check_and_update_pro_package() -> bool:
|
|
608
|
+
"""Re-download the PRO package if a newer build has been published.
|
|
609
|
+
|
|
610
|
+
Called on TUI startup (background worker, same pattern as
|
|
611
|
+
_check_for_updates for the pip package) and after `pentool update` — this
|
|
612
|
+
is what closes the gap where pentool-pro ships a fix but a machine that
|
|
613
|
+
already activated its license keeps running the stale code from
|
|
614
|
+
~/.pentool/pro/ forever, because download_pro_package() otherwise only
|
|
615
|
+
ever runs once, at initial activation/trial-start.
|
|
616
|
+
|
|
617
|
+
Silently does nothing (returns False) if:
|
|
618
|
+
- there's no active PRO license (nothing to update)
|
|
619
|
+
- the PRO package was never downloaded in the first place (that's
|
|
620
|
+
activate_license's job, not this one)
|
|
621
|
+
- the version check or download fails for any reason
|
|
622
|
+
|
|
623
|
+
Returns True if a new build was found and successfully installed.
|
|
624
|
+
"""
|
|
625
|
+
info = get_license()
|
|
626
|
+
if not info.valid or not info.features:
|
|
627
|
+
return False
|
|
628
|
+
if not PRO_PACKAGE_DIR.exists():
|
|
629
|
+
return False # never activated on this machine — nothing to refresh
|
|
630
|
+
|
|
631
|
+
plat = _current_platform()
|
|
632
|
+
remote_build_id = await _fetch_pro_build_id(plat)
|
|
633
|
+
if not remote_build_id:
|
|
634
|
+
return False # couldn't reach the server — don't touch anything
|
|
635
|
+
|
|
636
|
+
local_build_id = None
|
|
637
|
+
try:
|
|
638
|
+
if _PRO_BUILD_MARKER.exists():
|
|
639
|
+
local_build_id = _PRO_BUILD_MARKER.read_text(encoding="utf-8").strip()
|
|
640
|
+
except Exception:
|
|
641
|
+
pass
|
|
642
|
+
|
|
643
|
+
if local_build_id == remote_build_id:
|
|
644
|
+
return False # already on the latest build
|
|
645
|
+
|
|
646
|
+
return await download_pro_package(info.license_key, info.machine_id)
|
|
@@ -150,6 +150,14 @@ class ProxyServer:
|
|
|
150
150
|
|
|
151
151
|
self.intercept_enabled: bool = False
|
|
152
152
|
self.scope: list[str] = [] # Empty = intercept everything
|
|
153
|
+
# When False (default), scope is informational only — ALL traffic is
|
|
154
|
+
# captured/shown regardless of self.scope. Scope filtering (dropping
|
|
155
|
+
# out-of-scope requests from capture) only takes effect when this is
|
|
156
|
+
# explicitly turned on via the "Skip out-of-scope" toggle button.
|
|
157
|
+
# Without this flag, a leftover/stale `scope` list (e.g. saved from a
|
|
158
|
+
# previous project) would silently blackhole all non-matching traffic
|
|
159
|
+
# the moment the proxy starts, with no visible indication why.
|
|
160
|
+
self.enforce_scope: bool = False
|
|
153
161
|
|
|
154
162
|
# Match/Replace via dedicated engine
|
|
155
163
|
self._match_replace_engine = MatchReplaceEngine()
|
|
@@ -205,7 +213,10 @@ class ProxyServer:
|
|
|
205
213
|
self._running = False
|
|
206
214
|
if self._server:
|
|
207
215
|
self._server.close()
|
|
208
|
-
|
|
216
|
+
try:
|
|
217
|
+
await asyncio.wait_for(self._server.wait_closed(), timeout=2.0)
|
|
218
|
+
except asyncio.TimeoutError:
|
|
219
|
+
logger.debug("ProxyServer.stop: wait_closed timeout, continuing")
|
|
209
220
|
self._server = None
|
|
210
221
|
# Close singleton HTTP client
|
|
211
222
|
if self._http_client:
|
|
@@ -214,6 +225,25 @@ class ProxyServer:
|
|
|
214
225
|
except Exception as e:
|
|
215
226
|
logger.warning("ProxyServer.stop: http_client.close() error: %s", e)
|
|
216
227
|
self._http_client = None
|
|
228
|
+
# Cancel all remaining active tasks (open TLS tunnels, keep-alive
|
|
229
|
+
# connections, intercept waits) so the event loop can exit promptly.
|
|
230
|
+
# Without this, _handle_connect loops on _READ_TIMEOUT=30s and
|
|
231
|
+
# _INTERCEPT_TIMEOUT=300s — the proxy thread would not exit for up to
|
|
232
|
+
# 5 minutes after stop(), blocking project switches and restarts.
|
|
233
|
+
loop = asyncio.get_running_loop()
|
|
234
|
+
current = asyncio.current_task()
|
|
235
|
+
pending = [t for t in asyncio.all_tasks(loop) if t is not current and not t.done()]
|
|
236
|
+
if pending:
|
|
237
|
+
logger.debug("ProxyServer.stop: cancelling %d active task(s)", len(pending))
|
|
238
|
+
for task in pending:
|
|
239
|
+
task.cancel()
|
|
240
|
+
try:
|
|
241
|
+
await asyncio.wait_for(
|
|
242
|
+
asyncio.gather(*pending, return_exceptions=True),
|
|
243
|
+
timeout=2.0,
|
|
244
|
+
)
|
|
245
|
+
except asyncio.TimeoutError:
|
|
246
|
+
logger.debug("ProxyServer.stop: task cancellation timed out")
|
|
217
247
|
logger.info("Proxy stopped")
|
|
218
248
|
|
|
219
249
|
async def serve_forever(self) -> None:
|
|
@@ -296,6 +326,19 @@ class ProxyServer:
|
|
|
296
326
|
else:
|
|
297
327
|
self.intercept_enabled = enabled
|
|
298
328
|
|
|
329
|
+
def set_enforce_scope(self, enabled: bool) -> None:
|
|
330
|
+
"""Thread-safe setting of enforce_scope flag from any thread.
|
|
331
|
+
|
|
332
|
+
Same rationale as set_intercept — the proxy loop reads this flag
|
|
333
|
+
in _handle_http/_handle_connect, so writes from the TUI thread must
|
|
334
|
+
go through call_soon_threadsafe to avoid a torn read.
|
|
335
|
+
"""
|
|
336
|
+
loop = self._loop
|
|
337
|
+
if loop is not None and loop.is_running():
|
|
338
|
+
loop.call_soon_threadsafe(setattr, self, "enforce_scope", enabled)
|
|
339
|
+
else:
|
|
340
|
+
self.enforce_scope = enabled
|
|
341
|
+
|
|
299
342
|
def _find_request(self, req_id: str) -> InterceptedRequest | None:
|
|
300
343
|
with self._requests_lock:
|
|
301
344
|
for r in reversed(self.requests):
|
|
@@ -367,7 +410,8 @@ class ProxyServer:
|
|
|
367
410
|
await writer.drain()
|
|
368
411
|
|
|
369
412
|
# If host is out of scope — just tunnel without interception
|
|
370
|
-
|
|
413
|
+
# (only enforced when the user turned on "Skip out-of-scope")
|
|
414
|
+
if self.enforce_scope and not self.is_in_scope(domain):
|
|
371
415
|
await self._tunnel_raw(domain, host_port, reader, writer)
|
|
372
416
|
return
|
|
373
417
|
|
|
@@ -491,7 +535,8 @@ class ProxyServer:
|
|
|
491
535
|
req.method, req.url, is_https, self.intercept_enabled, self.is_in_scope(host))
|
|
492
536
|
|
|
493
537
|
# Check scope (for HTTP; HTTPS already checked in _handle_connect)
|
|
494
|
-
|
|
538
|
+
# (only enforced when the user turned on "Skip out-of-scope")
|
|
539
|
+
if self.enforce_scope and not is_https and not self.is_in_scope(host):
|
|
495
540
|
logger.debug("PROXY: _handle_http: host %s out of scope, forwarding direct", host)
|
|
496
541
|
return await self._forward_direct(req, writer)
|
|
497
542
|
|
|
@@ -408,6 +408,12 @@ class PentoolApp(App):
|
|
|
408
408
|
if getattr(self._cfg, "check_updates", True):
|
|
409
409
|
self.run_worker(self._check_for_updates(), exclusive=False, thread=False)
|
|
410
410
|
|
|
411
|
+
# Check for a newer PRO build in background (if a PRO license is
|
|
412
|
+
# active) — closes the gap where pentool-pro ships a fix but an
|
|
413
|
+
# already-activated machine keeps running the stale code forever,
|
|
414
|
+
# since the PRO package otherwise only downloads once, at activation.
|
|
415
|
+
self.run_worker(self._check_for_pro_update(), exclusive=False, thread=False)
|
|
416
|
+
|
|
411
417
|
async def _auto_open_last_project(self) -> None:
|
|
412
418
|
"""Open the last project from recent_projects at startup."""
|
|
413
419
|
await asyncio.sleep(0.2) # give Textual time to render the UI
|
|
@@ -439,6 +445,30 @@ class PentoolApp(App):
|
|
|
439
445
|
except Exception as exc:
|
|
440
446
|
logger.debug("APP: update check failed: %s", exc)
|
|
441
447
|
|
|
448
|
+
async def _check_for_pro_update(self) -> None:
|
|
449
|
+
"""Re-download the PRO package in the background if a newer build
|
|
450
|
+
has been published, then notify so the user knows to restart.
|
|
451
|
+
|
|
452
|
+
No-op (silently) if there's no active PRO license or the package was
|
|
453
|
+
never downloaded in the first place — see
|
|
454
|
+
pentool.core.license.check_and_update_pro_package for the actual
|
|
455
|
+
build_id comparison logic.
|
|
456
|
+
"""
|
|
457
|
+
import asyncio as _asyncio
|
|
458
|
+
await _asyncio.sleep(4.0) # after the pip-update check, UI settled
|
|
459
|
+
try:
|
|
460
|
+
from pentool.core.license import check_and_update_pro_package
|
|
461
|
+
updated = await check_and_update_pro_package()
|
|
462
|
+
if updated:
|
|
463
|
+
self.notify(
|
|
464
|
+
"PRO package updated to the latest build — restart Pentool to use it.",
|
|
465
|
+
severity="information",
|
|
466
|
+
timeout=10,
|
|
467
|
+
)
|
|
468
|
+
logger.info("APP: PRO package updated to a newer build")
|
|
469
|
+
except Exception as exc:
|
|
470
|
+
logger.debug("APP: PRO update check failed: %s", exc)
|
|
471
|
+
|
|
442
472
|
def _setup_auto_save(self) -> None:
|
|
443
473
|
"""Configure / restart the auto-save timer from the current config.
|
|
444
474
|
|
|
@@ -671,6 +701,14 @@ class PentoolApp(App):
|
|
|
671
701
|
pass
|
|
672
702
|
self.query_one(ContentSwitcher).current = f"screen-{module_id}"
|
|
673
703
|
self._active_module = module_id
|
|
704
|
+
# _update_proxy_screen_labels() only refreshes the ProxyScreen label
|
|
705
|
+
# when it IS the active module — so if the proxy was stopped/started
|
|
706
|
+
# while the user was on a different tab (e.g. right after creating a
|
|
707
|
+
# new project, which force-stops the proxy while Dashboard is shown),
|
|
708
|
+
# the label update was skipped and stayed stale until the next actual
|
|
709
|
+
# start/stop toggle. Refresh it explicitly on every switch into Proxy.
|
|
710
|
+
if module_id == "proxy":
|
|
711
|
+
self._update_proxy_screen_labels()
|
|
674
712
|
|
|
675
713
|
def get_proxy(self) -> ProxyServer | None:
|
|
676
714
|
return self._proxy
|
|
@@ -728,6 +766,21 @@ class PentoolApp(App):
|
|
|
728
766
|
if self._proxy.is_running:
|
|
729
767
|
self._stop_proxy()
|
|
730
768
|
else:
|
|
769
|
+
if not self._project_loaded:
|
|
770
|
+
# Project DB switch/open (auto-open at startup, New/Open
|
|
771
|
+
# Project) is still finishing in the background — starting
|
|
772
|
+
# the proxy now would race HttpStorage.switch_db() (its
|
|
773
|
+
# connection may be mid-close/reopen) and silently lose or
|
|
774
|
+
# fail to persist the first captured requests. _project_loaded
|
|
775
|
+
# is set as soon as the DB switch itself completes (see
|
|
776
|
+
# ProjectManager._do_switch) — the other screens may still be
|
|
777
|
+
# reloading, but that's independent of Proxy.
|
|
778
|
+
self.notify(
|
|
779
|
+
"Проект ещё открывается — подождите пару секунд перед запуском Proxy",
|
|
780
|
+
severity="warning",
|
|
781
|
+
timeout=4,
|
|
782
|
+
)
|
|
783
|
+
return
|
|
731
784
|
self._start_proxy()
|
|
732
785
|
|
|
733
786
|
def action_toggle_intercept(self) -> None:
|
|
@@ -847,13 +900,25 @@ class PentoolApp(App):
|
|
|
847
900
|
self._proxy.stop(), self._proxy_loop
|
|
848
901
|
)
|
|
849
902
|
try:
|
|
850
|
-
|
|
903
|
+
# stop() now cancels all active tasks and waits up to ~4s
|
|
904
|
+
# internally, so 6s here is generous headroom
|
|
905
|
+
future.result(timeout=6)
|
|
851
906
|
except Exception as e:
|
|
852
907
|
logger.warning("APP: proxy.stop() error or timeout: %s", e)
|
|
908
|
+
# Force-cancel anything still running in the proxy loop so
|
|
909
|
+
# the thread can exit even if stop() itself timed out
|
|
910
|
+
if self._proxy_loop and not self._proxy_loop.is_closed():
|
|
911
|
+
try:
|
|
912
|
+
def _cancel_all():
|
|
913
|
+
for t in asyncio.all_tasks(self._proxy_loop):
|
|
914
|
+
t.cancel()
|
|
915
|
+
self._proxy_loop.call_soon_threadsafe(_cancel_all)
|
|
916
|
+
except Exception:
|
|
917
|
+
pass
|
|
853
918
|
if self._proxy_thread and self._proxy_thread.is_alive():
|
|
854
|
-
self._proxy_thread.join(timeout=
|
|
919
|
+
self._proxy_thread.join(timeout=5)
|
|
855
920
|
if self._proxy_thread.is_alive():
|
|
856
|
-
logger.warning("APP: proxy thread did not stop in
|
|
921
|
+
logger.warning("APP: proxy thread did not stop in 5s — port 8080 may still be in use")
|
|
857
922
|
self.call_after_refresh(self._update_status)
|
|
858
923
|
self.call_after_refresh(self._update_proxy_screen_labels)
|
|
859
924
|
|
|
@@ -944,7 +1009,14 @@ class PentoolApp(App):
|
|
|
944
1009
|
target = self.query_one(SCREEN_TARGET, TargetScreen)
|
|
945
1010
|
api = target._get_api()
|
|
946
1011
|
api.sitemap.set_in_scope(msg.host, msg.in_scope)
|
|
947
|
-
|
|
1012
|
+
# Rebuild the tree from the in-memory sitemap only — NOT
|
|
1013
|
+
# target._load_sitemap(), which does a full api.load() from
|
|
1014
|
+
# the DB. The sitemap is persisted in batches of 20 rows
|
|
1015
|
+
# (see target/screen.py _do_save_sitemap), so any host/path
|
|
1016
|
+
# added in-memory since the last batch save would be wiped
|
|
1017
|
+
# out by that reload, making a simple "Add to Scope" click
|
|
1018
|
+
# from Proxy appear to clear the whole Target tree.
|
|
1019
|
+
target._refresh_tree()
|
|
948
1020
|
except Exception as e:
|
|
949
1021
|
logger.debug("on_sync_scope_to_target: %s", e)
|
|
950
1022
|
|
|
@@ -13,7 +13,6 @@ from textual.screen import ModalScreen
|
|
|
13
13
|
_CSS = (Path(__file__).parent / "match_replace_dialog.tcss").read_text(encoding="utf-8")
|
|
14
14
|
from textual.widgets import (
|
|
15
15
|
Button,
|
|
16
|
-
Checkbox,
|
|
17
16
|
DataTable,
|
|
18
17
|
Input,
|
|
19
18
|
Label,
|
|
@@ -21,6 +20,7 @@ from textual.widgets import (
|
|
|
21
20
|
)
|
|
22
21
|
|
|
23
22
|
from pentool.api.proxy_api import MatchReplaceRule
|
|
23
|
+
from pentool.tui.widgets.nice_checkbox import NiceCheckbox as Checkbox
|
|
24
24
|
from pentool.tui.widgets.toolbar_button import ToolbarButton
|
|
25
25
|
|
|
26
26
|
_TARGET_OPTIONS = [("request", "Request"), ("response", "Response"), ("both", "Both")]
|
|
@@ -325,9 +325,14 @@ class ProjectManager:
|
|
|
325
325
|
)
|
|
326
326
|
self._app._stop_proxy()
|
|
327
327
|
|
|
328
|
+
# Gate Start Proxy (action_toggle_proxy) until _do_switch flips this
|
|
329
|
+
# back to True once HttpStorage.switch_db() has actually completed —
|
|
330
|
+
# otherwise a Start Proxy click during the switch could race the
|
|
331
|
+
# storage connection being closed/reopened underneath it.
|
|
332
|
+
self._app._project_loaded = False
|
|
333
|
+
|
|
328
334
|
self._cfg.db_path = path
|
|
329
335
|
self._app._project_path = path
|
|
330
|
-
# Note: _project_loaded will be set AFTER _reload_project_screens in _do_switch
|
|
331
336
|
|
|
332
337
|
if self._proxy:
|
|
333
338
|
self._proxy.db_path = path
|
|
@@ -337,6 +342,10 @@ class ProjectManager:
|
|
|
337
342
|
self._proxy.clear_requests()
|
|
338
343
|
self._proxy.scope = []
|
|
339
344
|
self._proxy.match_replace_rules = []
|
|
345
|
+
# New project's DB has no project_settings row yet — reset to
|
|
346
|
+
# the documented default (OFF) rather than carrying over
|
|
347
|
+
# whatever the previous project had in memory.
|
|
348
|
+
self._proxy.set_enforce_scope(False)
|
|
340
349
|
except Exception:
|
|
341
350
|
pass
|
|
342
351
|
|
|
@@ -371,12 +380,44 @@ class ProjectManager:
|
|
|
371
380
|
"""Single entry point for all project switches.
|
|
372
381
|
|
|
373
382
|
Runs in an exclusive worker — no two switches can overlap.
|
|
374
|
-
Order
|
|
383
|
+
Order:
|
|
384
|
+
0. Wait for the proxy thread to fully exit (if _stop_proxy was
|
|
385
|
+
called but the thread didn't die within join(timeout) — the
|
|
386
|
+
old loop may still hold the SQLite WAL lock or the TCP port).
|
|
375
387
|
1. init_db (CREATE TABLE IF NOT EXISTS, migrations)
|
|
376
388
|
2. switch_db on ProxyService (close old connection, open new one)
|
|
377
389
|
3. clear in-memory proxy state if new project
|
|
378
|
-
4.
|
|
390
|
+
4. set _project_loaded — DB is ready, Start Proxy is now safe,
|
|
391
|
+
even though the other screens (Repeater/Scanner/Target/Dashboard)
|
|
392
|
+
may still be reloading in the background (step 5). Proxy only
|
|
393
|
+
depends on the HttpStorage connection from step 2, not on any
|
|
394
|
+
of those screens, so there is no reason to make the user wait
|
|
395
|
+
for all of them before allowing Start Proxy.
|
|
396
|
+
5. reload the remaining screens — run concurrently (they read
|
|
397
|
+
independent tables/state), not sequentially.
|
|
379
398
|
"""
|
|
399
|
+
# 0. Wait for the proxy thread to die before touching the DB.
|
|
400
|
+
# _stop_proxy() already called join(timeout=5), but if the thread
|
|
401
|
+
# is still alive (e.g. a 30-second _READ_TIMEOUT blocked task didn't
|
|
402
|
+
# get cancelled in time) we must not open a new SQLite connection
|
|
403
|
+
# while the old proxy event loop might still be writing to it.
|
|
404
|
+
proxy_thread = self._app._proxy_thread
|
|
405
|
+
if proxy_thread is not None and proxy_thread.is_alive():
|
|
406
|
+
logger.info("_do_switch: waiting for proxy thread to exit before DB switch…")
|
|
407
|
+
loop = asyncio.get_running_loop()
|
|
408
|
+
# Poll in the async event loop so we don't block the TUI thread
|
|
409
|
+
for _ in range(50): # up to 5 seconds in 100ms steps
|
|
410
|
+
alive = await loop.run_in_executor(
|
|
411
|
+
None, lambda: proxy_thread.is_alive()
|
|
412
|
+
)
|
|
413
|
+
if not alive:
|
|
414
|
+
break
|
|
415
|
+
await asyncio.sleep(0.1)
|
|
416
|
+
if proxy_thread.is_alive():
|
|
417
|
+
logger.warning("_do_switch: proxy thread still alive after extra 5s — proceeding anyway")
|
|
418
|
+
else:
|
|
419
|
+
logger.info("_do_switch: proxy thread exited, proceeding with DB switch")
|
|
420
|
+
|
|
380
421
|
# 1. Ensure schema exists (safe for both new and existing DBs)
|
|
381
422
|
await self._init_new_db(path)
|
|
382
423
|
|
|
@@ -405,75 +446,91 @@ class ProjectManager:
|
|
|
405
446
|
except Exception as exc:
|
|
406
447
|
logger.debug("_do_switch: clear for new project: %s", exc)
|
|
407
448
|
|
|
408
|
-
# 4.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
# 5. Set flag AFTER all screens are reloaded (fixes race condition)
|
|
449
|
+
# 4. Set flag as soon as the DB itself is ready — Start Proxy only
|
|
450
|
+
# needs HttpStorage (step 2) done, not the other screens below.
|
|
412
451
|
self._app._project_loaded = True
|
|
413
|
-
logger.info("_do_switch: project loaded flag set")
|
|
452
|
+
logger.info("_do_switch: project loaded flag set (DB ready)")
|
|
453
|
+
|
|
454
|
+
# 5. Reload the remaining screens concurrently — independent of
|
|
455
|
+
# each other and of the flag above.
|
|
456
|
+
await self._reload_project_screens(path, is_new=is_new)
|
|
457
|
+
logger.info("_do_switch: all screens reloaded")
|
|
414
458
|
|
|
415
459
|
async def _reload_project_screens(self, path: str, is_new: bool = False) -> None:
|
|
416
|
-
"""Reload data from DB into all screens. Called from _do_switch only.
|
|
460
|
+
"""Reload data from DB into all screens. Called from _do_switch only.
|
|
417
461
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
repeater = self._app.query_one(SCREEN_REPEATER, RepeaterScreen)
|
|
423
|
-
if is_new:
|
|
424
|
-
await repeater.reset_for_new_project()
|
|
425
|
-
else:
|
|
426
|
-
await repeater.reload_from_project(path)
|
|
427
|
-
logger.info("_reload_project_screens: repeater %s",
|
|
428
|
-
"reset" if is_new else "reloaded")
|
|
429
|
-
except Exception as exc:
|
|
430
|
-
logger.debug("_reload_project_screens repeater: %s", exc)
|
|
462
|
+
Each screen's reload is independent (different tables/state), so they
|
|
463
|
+
run concurrently via asyncio.gather instead of one after another —
|
|
464
|
+
total time is bounded by the slowest single reload, not the sum.
|
|
465
|
+
"""
|
|
431
466
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
467
|
+
async def _reload_repeater() -> None:
|
|
468
|
+
try:
|
|
469
|
+
from pentool.tui.constants import SCREEN_REPEATER
|
|
470
|
+
from pentool.tui.screens.repeater.screen import RepeaterScreen
|
|
471
|
+
repeater = self._app.query_one(SCREEN_REPEATER, RepeaterScreen)
|
|
472
|
+
if is_new:
|
|
473
|
+
await repeater.reset_for_new_project()
|
|
474
|
+
else:
|
|
475
|
+
await repeater.reload_from_project(path)
|
|
476
|
+
logger.info("_reload_project_screens: repeater %s",
|
|
477
|
+
"reset" if is_new else "reloaded")
|
|
478
|
+
except Exception as exc:
|
|
479
|
+
logger.debug("_reload_project_screens repeater: %s", exc)
|
|
439
480
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
scanner_screen._scanner_api = scanner_screen._get_or_create_api(path)
|
|
448
|
-
scanner_screen._populate_from_db([])
|
|
449
|
-
scanner_screen._load_findings_worker()
|
|
450
|
-
logger.info("_reload_project_screens: scanner reloaded")
|
|
451
|
-
except Exception as exc:
|
|
452
|
-
logger.debug("_reload_project_screens scanner: %s", exc)
|
|
481
|
+
async def _reload_proxy() -> None:
|
|
482
|
+
try:
|
|
483
|
+
screen = self._app.query_one(SCREEN_PROXY, ProxyScreen)
|
|
484
|
+
await screen._reload_from_storage()
|
|
485
|
+
logger.info("_reload_project_screens: proxy reloaded from %s", path)
|
|
486
|
+
except Exception as exc:
|
|
487
|
+
logger.debug("_reload_project_screens proxy: %s", exc)
|
|
453
488
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
except Exception as exc:
|
|
468
|
-
logger.debug("_reload_project_screens target: %s", exc)
|
|
489
|
+
async def _reload_scanner() -> None:
|
|
490
|
+
try:
|
|
491
|
+
from pentool.tui.screens.scanner.screen import ScannerScreen
|
|
492
|
+
scanner_screen = self._app.query_one(SCREEN_SCANNER, ScannerScreen)
|
|
493
|
+
if scanner_screen._scanner_api is not None:
|
|
494
|
+
scanner_screen._scanner_api._db_path = path
|
|
495
|
+
scanner_screen._scanner_api = None
|
|
496
|
+
scanner_screen._scanner_api = scanner_screen._get_or_create_api(path)
|
|
497
|
+
scanner_screen._populate_from_db([])
|
|
498
|
+
scanner_screen._load_findings_worker()
|
|
499
|
+
logger.info("_reload_project_screens: scanner reloaded")
|
|
500
|
+
except Exception as exc:
|
|
501
|
+
logger.debug("_reload_project_screens scanner: %s", exc)
|
|
469
502
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
503
|
+
async def _reload_target() -> None:
|
|
504
|
+
try:
|
|
505
|
+
from pentool.tui.screens.target.screen import TargetScreen
|
|
506
|
+
target_screen = self._app.query_one(SCREEN_TARGET, TargetScreen)
|
|
507
|
+
if target_screen._target_api is not None:
|
|
508
|
+
try:
|
|
509
|
+
await target_screen._target_api.save()
|
|
510
|
+
except Exception:
|
|
511
|
+
pass
|
|
512
|
+
target_screen._target_api = None
|
|
513
|
+
target_screen._get_api()
|
|
514
|
+
target_screen._load_sitemap()
|
|
515
|
+
logger.info("_reload_project_screens: target reloaded from %s", path)
|
|
516
|
+
except Exception as exc:
|
|
517
|
+
logger.debug("_reload_project_screens target: %s", exc)
|
|
518
|
+
|
|
519
|
+
async def _reload_dashboard() -> None:
|
|
520
|
+
try:
|
|
521
|
+
from pentool.tui.screens.dashboard.screen import DashboardScreen
|
|
522
|
+
dash = self._app.query_one(SCREEN_DASHBOARD, DashboardScreen)
|
|
523
|
+
dash.refresh_stats()
|
|
524
|
+
except Exception as exc:
|
|
525
|
+
logger.debug("_reload_project_screens dashboard: %s", exc)
|
|
526
|
+
|
|
527
|
+
await asyncio.gather(
|
|
528
|
+
_reload_repeater(),
|
|
529
|
+
_reload_proxy(),
|
|
530
|
+
_reload_scanner(),
|
|
531
|
+
_reload_target(),
|
|
532
|
+
_reload_dashboard(),
|
|
533
|
+
)
|
|
477
534
|
|
|
478
535
|
async def _init_new_db(self, path: str) -> None:
|
|
479
536
|
try:
|