-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstart.py
More file actions
298 lines (239 loc) · 9.46 KB
/
start.py
File metadata and controls
298 lines (239 loc) · 9.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
#!/usr/bin/env python3
"""Portable launcher for the ReconScript web interface."""
from __future__ import annotations
import atexit
import os
import shutil
import subprocess
import sys
import threading
import time
import venv
import webbrowser
from pathlib import Path
from typing import List
try: # pragma: no cover - optional dependency fallback
from dotenv import load_dotenv
except (
ModuleNotFoundError
): # pragma: no cover - minimal shim when python-dotenv missing
def load_dotenv() -> None: # type: ignore[return-value]
"""Fallback no-op when python-dotenv is unavailable."""
return None
from install_dependencies import create_console, install_dependencies
load_dotenv()
ROOT = Path(__file__).resolve().parent
DEFAULT_PORT = int(os.environ.get("DEFAULT_PORT", "5000"))
class _StreamMultiplexer:
"""Mirror writes to stdout/stderr into the on-disk log."""
def __init__(self, *streams):
self._streams = streams
def write(self, data: str) -> None:
for stream in self._streams:
stream.write(data)
stream.flush()
def flush(self) -> None: # pragma: no cover - compatibility shim
for stream in self._streams:
stream.flush()
console = create_console()
def render_banner(version: str, environment: str) -> None:
lines = [
f"ReconScript v{version}",
f"Mode: {environment.capitalize()}",
'"Automated Reconnaissance"',
]
banner_lines = ["┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓"]
banner_lines.extend(f"┃ {text:<26} ┃" for text in lines)
banner_lines.append("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛")
banner = "\n".join(banner_lines)
try: # pragma: no cover - enhanced output when Rich is present
from rich.panel import Panel
console.print(
Panel(banner, expand=False, border_style="bright_cyan", style="bold cyan")
)
except Exception:
console.print(banner)
def in_virtualenv() -> bool:
return sys.prefix != getattr(sys, "base_prefix", sys.prefix) or bool(
os.environ.get("VIRTUAL_ENV")
)
def in_docker() -> bool:
if Path("/.dockerenv").exists():
return True
if os.environ.get("RUNNING_IN_DOCKER") == "1":
return True
if Path("/var/run/docker.sock").exists():
return True
try:
cgroup = Path("/proc/self/cgroup")
if cgroup.exists() and "docker" in cgroup.read_text():
return True
except OSError:
pass
return False
def in_wsl() -> bool:
return bool(os.environ.get("WSL_INTEROP") or os.environ.get("WSL_DISTRO_NAME"))
def describe_environment() -> str:
if in_docker():
return "docker"
if in_wsl():
return "wsl"
return "local"
def venv_python_path(venv_dir: Path) -> Path:
if os.name == "nt":
return venv_dir / "Scripts" / "python.exe"
return venv_dir / "bin" / "python"
def ensure_virtualenv() -> Path:
venv_dir = ROOT / ".venv"
python_path = venv_python_path(venv_dir)
if python_path.exists():
return python_path
console.print("Creating isolated Python environment in .venv …")
builder = venv.EnvBuilder(with_pip=True)
builder.create(venv_dir)
return python_path
def _wait_and_open_browser(url: str, health_url: str, environment: str) -> None:
def _open() -> None:
console.print(
"Waiting for ReconScript to pass health check before opening browser…"
)
import requests # Lazily import so dependency installation can complete first
deadline = time.time() + 60
while time.time() < deadline:
try:
response = requests.get(health_url, timeout=2)
if response.ok:
if environment == "docker":
console.print(
"ReconScript UI is ready inside Docker — open your browser at "
+ url
)
return
console.print(
"ReconScript UI is ready — launching default browser."
)
try:
if environment == "wsl" and shutil.which("wslview"):
subprocess.Popen(
["wslview", url]
) # noqa: S603,S607 - best effort helper
else:
webbrowser.open_new(url)
except Exception:
console.print(
"Unable to open browser automatically. Please navigate to "
+ url
)
return
except requests.RequestException:
pass
time.sleep(1)
console.print(
"[yellow]Timed out waiting for health check. Please open the UI manually at "
f"{url}[/yellow]"
)
thread = threading.Thread(target=_open, daemon=True)
thread.start()
def _run_app(host: str, port: int) -> None:
from reconscript.ui import create_app
app = create_app()
try:
app.run(host=host, port=port, threaded=True)
except OSError as exc:
console.print(f"[red]Failed to start the web server: {exc}[/red]")
sys.exit(1)
def main() -> None:
global console # must be declared before first use
os.chdir(ROOT)
configured_results = Path(os.environ.get("RESULTS_DIR", "results")).expanduser()
if not configured_results.is_absolute():
configured_results = (ROOT / configured_results).resolve()
log_root = configured_results
log_root.mkdir(parents=True, exist_ok=True)
log_path = log_root / "latest.log"
log_handle = log_path.open("w", encoding="utf-8")
atexit.register(log_handle.close)
sys.stdout = _StreamMultiplexer(sys.__stdout__, log_handle)
sys.stderr = _StreamMultiplexer(sys.__stderr__, log_handle)
console = create_console()
version_warning: str | None = None
if sys.version_info < (3, 9) or sys.version_info > (3, 13):
version_warning = (
"[yellow]Warning: ReconScript targets Python versions 3.9 through 3.13. You are running"
f" Python {'.'.join(map(str, sys.version_info[:3]))}. Proceeding anyway…[/yellow]"
)
environment = describe_environment()
venv_active = in_virtualenv()
post_launch_messages: List[str] = []
if environment != "docker" and not venv_active:
python_path = ensure_virtualenv()
if (
Path(sys.executable).resolve() != python_path.resolve()
and os.environ.get("RECONSCRIPT_BOOTSTRAPPED") != "1"
):
console.print("Switching to the project virtual environment …")
env = os.environ.copy()
env["RECONSCRIPT_BOOTSTRAPPED"] = "1"
command = [str(python_path), str(ROOT / "start.py"), *sys.argv[1:]]
raise SystemExit(subprocess.call(command, env=env))
post_launch_messages.append(
f"Using virtual environment at {python_path.parent}"
)
elif environment == "docker":
post_launch_messages.append(
"Running inside a Docker container — using system interpreter."
)
else:
post_launch_messages.append(
"Virtual environment detected — continuing with current interpreter."
)
python_executable = Path(sys.executable)
try:
install_dependencies(python_executable, console=console)
except RuntimeError as exc:
console.print(f"[red]{exc}[/red]")
sys.exit(1)
# ✅ Reinitialize Rich console after dependencies are ensured
console = create_console()
post_launch_messages.append(f"Session log: {log_path.resolve()}")
try:
from reconscript import __version__ as package_version
except ModuleNotFoundError:
package_version = "unknown"
render_banner(package_version, environment)
if version_warning:
console.print(version_warning)
for message in post_launch_messages:
console.print(message)
try:
from reconscript.report import ensure_results_dir
except ModuleNotFoundError as exc:
console.print(f"[red]Unable to import ReconScript package: {exc}[/red]")
sys.exit(1)
try:
results_dir = ensure_results_dir()
except PermissionError as exc:
console.print(
"[red]Cannot create the results/ directory. Please adjust permissions or run as administrator.[/red]"
)
console.print(f"System error: {exc}")
sys.exit(1)
console.print(f"Results will be stored in {results_dir.resolve()}")
if environment == "wsl":
console.print("Detected Windows Subsystem for Linux environment.")
public_url = f"http://127.0.0.1:{DEFAULT_PORT}"
health_url = f"http://127.0.0.1:{DEFAULT_PORT}/health"
if environment == "docker":
console.print(
"Browser auto-launch disabled inside Docker. Access the UI at " + public_url
)
else:
_wait_and_open_browser(public_url, health_url, environment)
host = "0.0.0.0" if environment in {"docker", "wsl"} else "127.0.0.1"
console.print(f"Starting ReconScript web UI on {host}:{DEFAULT_PORT} …")
try:
_run_app(host, DEFAULT_PORT)
except KeyboardInterrupt:
console.print("Exiting. Goodbye!")
if __name__ == "__main__":
main()