detecti-cli 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- detecti/__init__.py +0 -0
- detecti/cli.py +649 -0
- detecti/config.py +188 -0
- detecti/core/__init__.py +1 -0
- detecti/core/database/__init__.py +5 -0
- detecti/core/database/config_db.py +73 -0
- detecti/core/database/schema.py +136 -0
- detecti/core/database/storage.py +1388 -0
- detecti/core/engine.py +1032 -0
- detecti/core/models.py +278 -0
- detecti/data/config.sqlite +0 -0
- detecti/data/dbs/.gitkeep +2 -0
- detecti/data/dbs/example.com.sqlite +0 -0
- detecti/modules/__init__.py +29 -0
- detecti/modules/base.py +57 -0
- detecti/modules/censys.py +813 -0
- detecti/modules/crtsh.py +98 -0
- detecti/modules/exploitdb.py +138 -0
- detecti/modules/masscan.py +561 -0
- detecti/modules/nuclei.py +449 -0
- detecti/modules/nvd.py +300 -0
- detecti/modules/reverse_whois.py +225 -0
- detecti/modules/shodan.py +412 -0
- detecti/reporters/__init__.py +7 -0
- detecti/reporters/csv_reporter.py +74 -0
- detecti/reporters/html_reporter.py +356 -0
- detecti/reporters/json_reporter.py +26 -0
- detecti/reporters/markdown_reporter.py +203 -0
- detecti/utils/__init__.py +1 -0
- detecti/utils/http.py +294 -0
- detecti/utils/logger.py +378 -0
- detecti/utils/setup.py +453 -0
- detecti/web/__init__.py +6 -0
- detecti/web/api/__init__.py +1 -0
- detecti/web/api/auth.py +109 -0
- detecti/web/api/graph_builder.py +901 -0
- detecti/web/api/routes.py +1602 -0
- detecti/web/process_manager.py +283 -0
- detecti/web/server.py +183 -0
- detecti/web/static/android-chrome-192x192.png +0 -0
- detecti/web/static/android-chrome-512x512.png +0 -0
- detecti/web/static/apple-touch-icon.png +0 -0
- detecti/web/static/css/__init__.py +1 -0
- detecti/web/static/css/dashboard.css +3802 -0
- detecti/web/static/favicon-16x16.png +0 -0
- detecti/web/static/favicon-32x32.png +0 -0
- detecti/web/static/favicon.ico +0 -0
- detecti/web/static/img/DetecTI_Security_Logo.png +0 -0
- detecti/web/static/img/detecti-ico.png +0 -0
- detecti/web/static/index.html +677 -0
- detecti/web/static/js/__init__.py +1 -0
- detecti/web/static/js/api.js +177 -0
- detecti/web/static/js/cytoscape-cose-bilkent.js +458 -0
- detecti/web/static/js/cytoscape-dagre.js +397 -0
- detecti/web/static/js/cytoscape.min.js +31 -0
- detecti/web/static/js/dagre.min.js +3809 -0
- detecti/web/static/js/graph.js +7439 -0
- detecti/web/static/js/lucide.min.js +12 -0
- detecti/web/static/login.html +290 -0
- detecti/web/static/site.webmanifest +1 -0
- detecti_cli-2.0.0.dist-info/METADATA +554 -0
- detecti_cli-2.0.0.dist-info/RECORD +64 -0
- detecti_cli-2.0.0.dist-info/WHEEL +4 -0
- detecti_cli-2.0.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Background process management for DetecTI-CLI web server."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Dict, Optional
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import psutil
|
|
14
|
+
PSUTIL_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
psutil = None
|
|
17
|
+
PSUTIL_AVAILABLE = False
|
|
18
|
+
|
|
19
|
+
from detecti.config import DETECTI_HOME
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WebServerManager:
|
|
23
|
+
"""Manages background web server process lifecycle."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, state_file: Path = DETECTI_HOME / "run" / ".webserver.json"):
|
|
26
|
+
self.state_file = state_file
|
|
27
|
+
self.state_file.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
|
|
29
|
+
def is_running(self) -> bool:
|
|
30
|
+
"""Check if web server is currently running."""
|
|
31
|
+
if not PSUTIL_AVAILABLE:
|
|
32
|
+
# Fallback to basic PID check
|
|
33
|
+
if not self.state_file.exists():
|
|
34
|
+
return False
|
|
35
|
+
try:
|
|
36
|
+
state = self._read_state()
|
|
37
|
+
pid = state.get("pid")
|
|
38
|
+
if not pid:
|
|
39
|
+
return False
|
|
40
|
+
# Basic check if PID exists (Unix/Linux only)
|
|
41
|
+
try:
|
|
42
|
+
os.kill(pid, 0)
|
|
43
|
+
return True
|
|
44
|
+
except (OSError, ProcessLookupError):
|
|
45
|
+
return False
|
|
46
|
+
except (json.JSONDecodeError, ValueError):
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
if not self.state_file.exists():
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
state = self._read_state()
|
|
54
|
+
pid = state.get("pid")
|
|
55
|
+
if not pid:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
# Check if process exists and is running
|
|
59
|
+
return psutil.pid_exists(pid) and psutil.Process(pid).is_running()
|
|
60
|
+
except (json.JSONDecodeError, psutil.NoSuchProcess, psutil.AccessDenied):
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def get_status(self) -> Optional[Dict]:
|
|
64
|
+
"""Get current server status information."""
|
|
65
|
+
if not self.state_file.exists():
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
state = self._read_state()
|
|
70
|
+
pid = state.get("pid")
|
|
71
|
+
|
|
72
|
+
if not pid:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
# Check if process is running
|
|
76
|
+
is_running = False
|
|
77
|
+
if PSUTIL_AVAILABLE:
|
|
78
|
+
try:
|
|
79
|
+
if psutil.pid_exists(pid):
|
|
80
|
+
process = psutil.Process(pid)
|
|
81
|
+
if process.is_running():
|
|
82
|
+
is_running = True
|
|
83
|
+
# Calculate uptime
|
|
84
|
+
start_time = state.get("started_at")
|
|
85
|
+
if start_time:
|
|
86
|
+
from datetime import datetime
|
|
87
|
+
started = datetime.fromisoformat(start_time.replace("Z", "+00:00"))
|
|
88
|
+
uptime = datetime.now().astimezone() - started
|
|
89
|
+
state["uptime_seconds"] = uptime.total_seconds()
|
|
90
|
+
|
|
91
|
+
state["status"] = "RUNNING"
|
|
92
|
+
state["memory_mb"] = process.memory_info().rss / 1024 / 1024
|
|
93
|
+
return state
|
|
94
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
95
|
+
pass
|
|
96
|
+
else:
|
|
97
|
+
# Fallback check
|
|
98
|
+
try:
|
|
99
|
+
os.kill(pid, 0)
|
|
100
|
+
is_running = True
|
|
101
|
+
state["status"] = "RUNNING"
|
|
102
|
+
# Calculate uptime
|
|
103
|
+
start_time = state.get("started_at")
|
|
104
|
+
if start_time:
|
|
105
|
+
from datetime import datetime
|
|
106
|
+
started = datetime.fromisoformat(start_time.replace("Z", "+00:00"))
|
|
107
|
+
uptime = datetime.now().astimezone() - started
|
|
108
|
+
state["uptime_seconds"] = uptime.total_seconds()
|
|
109
|
+
return state
|
|
110
|
+
except (OSError, ProcessLookupError):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
if not is_running:
|
|
114
|
+
# Process not running, clean up state file
|
|
115
|
+
self._cleanup_state()
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
except (json.JSONDecodeError, ValueError):
|
|
119
|
+
self._cleanup_state()
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
def start_server(self, db_path: Optional[str] = None, host: str = "127.0.0.1", port: int = 8000) -> bool:
|
|
125
|
+
"""Start web server in background process."""
|
|
126
|
+
# Check if user wants to override host/port via .webserver.json
|
|
127
|
+
if self.state_file.exists():
|
|
128
|
+
try:
|
|
129
|
+
import json
|
|
130
|
+
state = json.loads(self.state_file.read_text(encoding="utf-8"))
|
|
131
|
+
if not self.is_running():
|
|
132
|
+
if "host" in state and host == "127.0.0.1":
|
|
133
|
+
host = state["host"]
|
|
134
|
+
if "port" in state and port == 8000:
|
|
135
|
+
port = state["port"]
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
if self.is_running():
|
|
140
|
+
return False # Already running
|
|
141
|
+
|
|
142
|
+
resolved_db_path = None
|
|
143
|
+
if db_path:
|
|
144
|
+
# Resolve database path
|
|
145
|
+
if not os.path.isabs(db_path):
|
|
146
|
+
# Check if it's in ./data/dbs/ directory
|
|
147
|
+
data_db_path = DETECTI_HOME / "data" / "dbs" / db_path
|
|
148
|
+
if data_db_path.exists():
|
|
149
|
+
resolved_db_path = str(data_db_path.resolve())
|
|
150
|
+
else:
|
|
151
|
+
# Try with .sqlite extension if not present
|
|
152
|
+
if not db_path.endswith('.sqlite'):
|
|
153
|
+
data_db_path_with_ext = DETECTI_HOME / "data" / "dbs" / f"{db_path}.sqlite"
|
|
154
|
+
if data_db_path_with_ext.exists():
|
|
155
|
+
resolved_db_path = str(data_db_path_with_ext.resolve())
|
|
156
|
+
else:
|
|
157
|
+
# Try removing underscores and using dots (example_com -> example.com)
|
|
158
|
+
normalized_name = db_path.replace('_', '.')
|
|
159
|
+
data_db_normalized = DETECTI_HOME / "data" / "dbs" / f"{normalized_name}.sqlite"
|
|
160
|
+
if data_db_normalized.exists():
|
|
161
|
+
resolved_db_path = str(data_db_normalized.resolve())
|
|
162
|
+
else:
|
|
163
|
+
resolved_db_path = str(Path(db_path).resolve())
|
|
164
|
+
else:
|
|
165
|
+
resolved_db_path = str(Path(db_path).resolve())
|
|
166
|
+
else:
|
|
167
|
+
resolved_db_path = str(Path(db_path).resolve())
|
|
168
|
+
|
|
169
|
+
if not Path(resolved_db_path).exists():
|
|
170
|
+
raise FileNotFoundError(f"Database file not found: {db_path}")
|
|
171
|
+
|
|
172
|
+
# Start server process
|
|
173
|
+
cmd = [
|
|
174
|
+
sys.executable, "-m", "web.server",
|
|
175
|
+
"--host", host,
|
|
176
|
+
"--port", str(port)
|
|
177
|
+
]
|
|
178
|
+
if resolved_db_path:
|
|
179
|
+
cmd.extend(["--db-path", resolved_db_path])
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
# Start detached background process
|
|
183
|
+
process = subprocess.Popen(
|
|
184
|
+
cmd,
|
|
185
|
+
start_new_session=True,
|
|
186
|
+
stdout=subprocess.DEVNULL,
|
|
187
|
+
stderr=subprocess.DEVNULL,
|
|
188
|
+
cwd=str(Path(__file__).resolve().parent.parent)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Give process time to start
|
|
192
|
+
time.sleep(2)
|
|
193
|
+
|
|
194
|
+
# Verify it's still running
|
|
195
|
+
if process.poll() is None:
|
|
196
|
+
# Save state
|
|
197
|
+
from datetime import datetime
|
|
198
|
+
state = {
|
|
199
|
+
"pid": process.pid,
|
|
200
|
+
"port": port,
|
|
201
|
+
"host": host,
|
|
202
|
+
"db_path": resolved_db_path or "No DB pre-loaded (Select from UI)",
|
|
203
|
+
"started_at": datetime.now().isoformat() + "Z",
|
|
204
|
+
"status": "RUNNING"
|
|
205
|
+
}
|
|
206
|
+
self._write_state(state)
|
|
207
|
+
return True
|
|
208
|
+
else:
|
|
209
|
+
return False
|
|
210
|
+
|
|
211
|
+
except Exception:
|
|
212
|
+
return False
|
|
213
|
+
|
|
214
|
+
def stop_server(self) -> bool:
|
|
215
|
+
"""Stop the background web server."""
|
|
216
|
+
if not self.is_running():
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
state = self._read_state()
|
|
221
|
+
pid = state.get("pid")
|
|
222
|
+
|
|
223
|
+
if not pid:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
if PSUTIL_AVAILABLE:
|
|
227
|
+
try:
|
|
228
|
+
if psutil.pid_exists(pid):
|
|
229
|
+
process = psutil.Process(pid)
|
|
230
|
+
|
|
231
|
+
# Send SIGTERM for graceful shutdown
|
|
232
|
+
process.terminate()
|
|
233
|
+
|
|
234
|
+
# Wait up to 10 seconds for graceful shutdown
|
|
235
|
+
try:
|
|
236
|
+
process.wait(timeout=10)
|
|
237
|
+
except psutil.TimeoutExpired:
|
|
238
|
+
# Force kill if still running
|
|
239
|
+
process.kill()
|
|
240
|
+
process.wait(timeout=5)
|
|
241
|
+
|
|
242
|
+
self._cleanup_state()
|
|
243
|
+
return True
|
|
244
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
245
|
+
self._cleanup_state()
|
|
246
|
+
return True
|
|
247
|
+
else:
|
|
248
|
+
# Fallback: send SIGTERM signal
|
|
249
|
+
try:
|
|
250
|
+
os.kill(pid, signal.SIGTERM)
|
|
251
|
+
time.sleep(2) # Give it time to shutdown
|
|
252
|
+
# Check if still running
|
|
253
|
+
try:
|
|
254
|
+
os.kill(pid, 0)
|
|
255
|
+
# Still running, force kill
|
|
256
|
+
os.kill(pid, signal.SIGKILL)
|
|
257
|
+
except (OSError, ProcessLookupError):
|
|
258
|
+
pass # Process already terminated
|
|
259
|
+
|
|
260
|
+
self._cleanup_state()
|
|
261
|
+
return True
|
|
262
|
+
except (OSError, ProcessLookupError):
|
|
263
|
+
self._cleanup_state()
|
|
264
|
+
return True
|
|
265
|
+
|
|
266
|
+
except (json.JSONDecodeError, ValueError):
|
|
267
|
+
self._cleanup_state()
|
|
268
|
+
return True
|
|
269
|
+
|
|
270
|
+
return False
|
|
271
|
+
|
|
272
|
+
def _read_state(self) -> Dict:
|
|
273
|
+
"""Read state from JSON file."""
|
|
274
|
+
return json.loads(self.state_file.read_text())
|
|
275
|
+
|
|
276
|
+
def _write_state(self, state: Dict) -> None:
|
|
277
|
+
"""Write state to JSON file."""
|
|
278
|
+
self.state_file.write_text(json.dumps(state, indent=2))
|
|
279
|
+
|
|
280
|
+
def _cleanup_state(self) -> None:
|
|
281
|
+
"""Remove state file."""
|
|
282
|
+
if self.state_file.exists():
|
|
283
|
+
self.state_file.unlink()
|
detecti/web/server.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""FastAPI web server for DetecTI-CLI EASM dashboard."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict
|
|
7
|
+
|
|
8
|
+
# Add project root to Python path for imports
|
|
9
|
+
project_root = Path(__file__).parent.parent
|
|
10
|
+
sys.path.insert(0, str(project_root))
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import uvicorn
|
|
14
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
15
|
+
from fastapi.staticfiles import StaticFiles
|
|
16
|
+
from fastapi.responses import FileResponse
|
|
17
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
18
|
+
FASTAPI_AVAILABLE = True
|
|
19
|
+
except ImportError:
|
|
20
|
+
FASTAPI_AVAILABLE = False
|
|
21
|
+
# Create dummy classes for type hints
|
|
22
|
+
class FastAPI:
|
|
23
|
+
pass
|
|
24
|
+
class HTTPException(Exception):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
from detecti.core.database.storage import DatabaseManager
|
|
28
|
+
from detecti.web.api.routes import router as api_router
|
|
29
|
+
from detecti.web.api.auth import router as auth_router, get_current_user
|
|
30
|
+
from jose import jwt, JWTError
|
|
31
|
+
from detecti.web.api.auth import SECRET_KEY, ALGORITHM, get_config_db
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def create_app(db_path: str = None) -> FastAPI:
|
|
35
|
+
"""Create FastAPI application with optional database connection."""
|
|
36
|
+
if not FASTAPI_AVAILABLE:
|
|
37
|
+
raise ImportError("FastAPI and uvicorn are required for web server functionality. Install with: pip install fastapi uvicorn")
|
|
38
|
+
|
|
39
|
+
from fastapi import FastAPI
|
|
40
|
+
app = FastAPI(
|
|
41
|
+
title="DetecTI-CLI EASM Dashboard",
|
|
42
|
+
description="Interactive External Attack Surface Management Dashboard",
|
|
43
|
+
version="2.0.0"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Add CORS middleware
|
|
47
|
+
app.add_middleware(
|
|
48
|
+
CORSMiddleware,
|
|
49
|
+
allow_origins=["*"],
|
|
50
|
+
allow_credentials=True,
|
|
51
|
+
allow_methods=["*"],
|
|
52
|
+
allow_headers=["*"],
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Load DETECTI_HOME for global path resolution
|
|
56
|
+
try:
|
|
57
|
+
from config import DETECTI_HOME
|
|
58
|
+
except ImportError:
|
|
59
|
+
DETECTI_HOME = Path.home() / ".detecti"
|
|
60
|
+
|
|
61
|
+
# Check if db_path was provided or auto-discover from data/dbs/
|
|
62
|
+
resolved_db_path = None
|
|
63
|
+
if db_path:
|
|
64
|
+
p = Path(db_path)
|
|
65
|
+
if not p.is_absolute():
|
|
66
|
+
candidate = DETECTI_HOME / "data" / "dbs" / db_path
|
|
67
|
+
if candidate.exists():
|
|
68
|
+
p = candidate
|
|
69
|
+
elif not db_path.endswith(".sqlite"):
|
|
70
|
+
cand_ext = DETECTI_HOME / "data" / "dbs" / f"{db_path}.sqlite"
|
|
71
|
+
if cand_ext.exists():
|
|
72
|
+
p = cand_ext
|
|
73
|
+
if p.exists():
|
|
74
|
+
resolved_db_path = str(p.resolve())
|
|
75
|
+
|
|
76
|
+
if not resolved_db_path:
|
|
77
|
+
# Auto-discover databases in data/dbs/ - Prioritize example.com.sqlite as default if present
|
|
78
|
+
dbs_dir = DETECTI_HOME / "data" / "dbs"
|
|
79
|
+
if dbs_dir.exists():
|
|
80
|
+
example_db = dbs_dir / "example.com.sqlite"
|
|
81
|
+
if example_db.exists():
|
|
82
|
+
resolved_db_path = str(example_db.resolve())
|
|
83
|
+
else:
|
|
84
|
+
existing_dbs = sorted(list(dbs_dir.glob("*.sqlite")), key=lambda x: x.stat().st_mtime, reverse=True)
|
|
85
|
+
if existing_dbs:
|
|
86
|
+
resolved_db_path = str(existing_dbs[0].resolve())
|
|
87
|
+
|
|
88
|
+
# Store database manager in app state
|
|
89
|
+
if resolved_db_path and Path(resolved_db_path).exists():
|
|
90
|
+
app.state.db_manager = DatabaseManager(Path(resolved_db_path))
|
|
91
|
+
app.state.db_path = resolved_db_path
|
|
92
|
+
else:
|
|
93
|
+
app.state.db_manager = None
|
|
94
|
+
app.state.db_path = None
|
|
95
|
+
|
|
96
|
+
# Include API routes
|
|
97
|
+
from fastapi import Depends
|
|
98
|
+
app.include_router(auth_router, prefix="/api/v1/auth")
|
|
99
|
+
app.include_router(api_router, prefix="/api/v1", dependencies=[Depends(get_current_user)])
|
|
100
|
+
|
|
101
|
+
# Serve static files
|
|
102
|
+
static_dir = Path(__file__).parent / "static"
|
|
103
|
+
if static_dir.exists():
|
|
104
|
+
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
|
105
|
+
|
|
106
|
+
@app.get("/")
|
|
107
|
+
async def serve_dashboard(request: Request):
|
|
108
|
+
"""Serve the main dashboard SPA or login page."""
|
|
109
|
+
static_dir = Path(__file__).parent / "static"
|
|
110
|
+
|
|
111
|
+
# Check authentication
|
|
112
|
+
token = request.cookies.get("detecti_token")
|
|
113
|
+
authenticated = False
|
|
114
|
+
if token:
|
|
115
|
+
try:
|
|
116
|
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
117
|
+
username = payload.get("sub")
|
|
118
|
+
if username:
|
|
119
|
+
config_db = get_config_db()
|
|
120
|
+
if config_db.user_exists(username):
|
|
121
|
+
authenticated = True
|
|
122
|
+
except JWTError:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
if not authenticated:
|
|
126
|
+
login_file = static_dir / "login.html"
|
|
127
|
+
if login_file.exists():
|
|
128
|
+
return FileResponse(login_file)
|
|
129
|
+
else:
|
|
130
|
+
return {"message": "Login required but login.html not found"}
|
|
131
|
+
|
|
132
|
+
index_file = static_dir / "index.html"
|
|
133
|
+
if index_file.exists():
|
|
134
|
+
return FileResponse(index_file)
|
|
135
|
+
else:
|
|
136
|
+
return {"message": "DetecTI-CLI EASM Dashboard", "status": "Dashboard files not found"}
|
|
137
|
+
|
|
138
|
+
@app.get("/health")
|
|
139
|
+
async def health_check():
|
|
140
|
+
"""Health check endpoint."""
|
|
141
|
+
return {
|
|
142
|
+
"status": "healthy",
|
|
143
|
+
"database": app.state.db_path,
|
|
144
|
+
"version": "2.0.0"
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return app
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main():
|
|
151
|
+
"""Main entry point for web server."""
|
|
152
|
+
if not FASTAPI_AVAILABLE:
|
|
153
|
+
print("FastAPI and uvicorn are required for web server functionality.", file=sys.stderr)
|
|
154
|
+
print("Install with: pip install fastapi uvicorn", file=sys.stderr)
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
parser = argparse.ArgumentParser(description="DetecTI-CLI Web Server")
|
|
158
|
+
parser.add_argument("--db-path", default=None, help="Path to SQLite database (optional)")
|
|
159
|
+
parser.add_argument("--host", default="0.0.0.0", help="Host to bind to")
|
|
160
|
+
parser.add_argument("--port", type=int, default=8000, help="Port to bind to")
|
|
161
|
+
|
|
162
|
+
args = parser.parse_args()
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
app = create_app(args.db_path)
|
|
166
|
+
|
|
167
|
+
# Run server
|
|
168
|
+
import uvicorn
|
|
169
|
+
uvicorn.run(
|
|
170
|
+
app,
|
|
171
|
+
host=args.host,
|
|
172
|
+
port=args.port,
|
|
173
|
+
log_level="warning", # Reduce log noise
|
|
174
|
+
access_log=False
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
print(f"Failed to start server: {e}", file=sys.stderr)
|
|
179
|
+
sys.exit(1)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
if __name__ == "__main__":
|
|
183
|
+
main()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Static CSS files for DetecTI-CLI dashboard
|