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
detecti/utils/setup.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""Automated Environment Setup, Prerequisite Diagnostics & Auto-Configuration for DetecTI-CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
|
|
15
|
+
from detecti.config import DETECTI_HOME
|
|
16
|
+
|
|
17
|
+
class SetupManager:
|
|
18
|
+
"""Manages prerequisite diagnostics and automated system setup for DetecTI-CLI."""
|
|
19
|
+
|
|
20
|
+
REQUIRED_PYTHON_MODULES = [
|
|
21
|
+
("httpx", "httpx"),
|
|
22
|
+
("typer", "typer"),
|
|
23
|
+
("rich", "rich"),
|
|
24
|
+
("fastapi", "fastapi"),
|
|
25
|
+
("uvicorn", "uvicorn"),
|
|
26
|
+
("pydantic", "pydantic"),
|
|
27
|
+
("pydantic_settings", "pydantic-settings"),
|
|
28
|
+
("psutil", "psutil"),
|
|
29
|
+
("tldextract", "tldextract"),
|
|
30
|
+
("cve_searchsploit", "cve-searchsploit"),
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
def __init__(self, console: Optional[Console] = None) -> None:
|
|
34
|
+
self.console = console or Console()
|
|
35
|
+
self.root_dir = Path(__file__).resolve().parent.parent
|
|
36
|
+
|
|
37
|
+
def check_all(self) -> Dict[str, Any]:
|
|
38
|
+
"""Run all diagnostic checks across system, environment, binaries, and local databases."""
|
|
39
|
+
return {
|
|
40
|
+
"python_version": self.check_python_version(),
|
|
41
|
+
"python_modules": self.check_python_modules(),
|
|
42
|
+
"directories": self.check_directories(),
|
|
43
|
+
"env_file": self.check_env_file(),
|
|
44
|
+
"masscan": self.check_masscan(),
|
|
45
|
+
"nuclei": self.check_nuclei(),
|
|
46
|
+
"exploitdb": self.check_exploitdb(),
|
|
47
|
+
"demo_db": self.check_demo_database(),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
def check_python_version(self) -> Dict[str, Any]:
|
|
51
|
+
"""Check Python interpreter version (requires 3.11+)."""
|
|
52
|
+
ver = sys.version_info
|
|
53
|
+
is_ok = ver >= (3, 11)
|
|
54
|
+
ver_str = f"{ver.major}.{ver.minor}.{ver.micro}"
|
|
55
|
+
return {
|
|
56
|
+
"name": "Python Runtime",
|
|
57
|
+
"status": f"Python {ver_str}",
|
|
58
|
+
"ok": is_ok,
|
|
59
|
+
"required": ">= 3.11",
|
|
60
|
+
"message": "Python 3.11 or newer is required." if not is_ok else "Compatible version detected.",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
def check_python_modules(self) -> Dict[str, Any]:
|
|
64
|
+
"""Check availability of core Python packages."""
|
|
65
|
+
missing = []
|
|
66
|
+
installed = []
|
|
67
|
+
for mod_name, pkg_name in self.REQUIRED_PYTHON_MODULES:
|
|
68
|
+
try:
|
|
69
|
+
__import__(mod_name)
|
|
70
|
+
installed.append(pkg_name)
|
|
71
|
+
except ImportError:
|
|
72
|
+
missing.append(pkg_name)
|
|
73
|
+
|
|
74
|
+
is_ok = len(missing) == 0
|
|
75
|
+
return {
|
|
76
|
+
"name": "Python Dependencies",
|
|
77
|
+
"status": f"{len(installed)}/{len(self.REQUIRED_PYTHON_MODULES)} Installed",
|
|
78
|
+
"ok": is_ok,
|
|
79
|
+
"missing": missing,
|
|
80
|
+
"message": f"Missing packages: {', '.join(missing)}" if missing else "All core packages are installed.",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def check_directories(self) -> Dict[str, Any]:
|
|
84
|
+
"""Ensure required operational directories exist."""
|
|
85
|
+
dirs = [
|
|
86
|
+
DETECTI_HOME / "data" / "dbs",
|
|
87
|
+
Path.cwd() / "reports",
|
|
88
|
+
]
|
|
89
|
+
missing = [d for d in dirs if not d.exists()]
|
|
90
|
+
return {
|
|
91
|
+
"name": "Project Directories",
|
|
92
|
+
"status": "Ready" if not missing else f"Missing {len(missing)} folders",
|
|
93
|
+
"ok": len(missing) == 0,
|
|
94
|
+
"missing": [str(d) for d in missing],
|
|
95
|
+
"message": "Operational directories (./data/dbs, ./reports) are configured." if not missing else f"Folders need creation: {', '.join([str(d) for d in missing])}",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
def check_env_file(self) -> Dict[str, Any]:
|
|
99
|
+
"""Check if .env configuration file exists."""
|
|
100
|
+
env_path = DETECTI_HOME / ".env"
|
|
101
|
+
exists = env_path.is_file()
|
|
102
|
+
return {
|
|
103
|
+
"name": "Environment Configuration (.env)",
|
|
104
|
+
"status": "Configured (.env found)" if exists else "Missing (.env not found)",
|
|
105
|
+
"ok": exists,
|
|
106
|
+
"message": "Configured and active." if exists else ".env file not found (can be created from .env.example).",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
def check_masscan(self) -> Dict[str, Any]:
|
|
110
|
+
"""Check Masscan binary installation and Linux raw socket capabilities."""
|
|
111
|
+
masscan_path = shutil.which("masscan")
|
|
112
|
+
if not masscan_path:
|
|
113
|
+
return {
|
|
114
|
+
"name": "Masscan Active Port Scanner",
|
|
115
|
+
"status": "Not Installed",
|
|
116
|
+
"ok": False,
|
|
117
|
+
"path": None,
|
|
118
|
+
"has_caps": False,
|
|
119
|
+
"message": "Binary 'masscan' not found in PATH. Required for active port scanning in WebGUI.",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Check raw socket capabilities or root execution
|
|
123
|
+
has_caps = False
|
|
124
|
+
is_root = hasattr(os, "geteuid") and os.geteuid() == 0
|
|
125
|
+
if is_root:
|
|
126
|
+
has_caps = True
|
|
127
|
+
else:
|
|
128
|
+
getcap_bin = shutil.which("getcap")
|
|
129
|
+
if getcap_bin:
|
|
130
|
+
try:
|
|
131
|
+
res = subprocess.run([getcap_bin, masscan_path], capture_output=True, text=True, check=False)
|
|
132
|
+
if "cap_net_raw" in res.stdout:
|
|
133
|
+
has_caps = True
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
status_text = "Ready (Non-root caps active)" if has_caps else "Installed (Needs setcap capabilities)"
|
|
138
|
+
if is_root:
|
|
139
|
+
status_text = "Ready (Running as root)"
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"name": "Masscan Active Port Scanner",
|
|
143
|
+
"status": status_text,
|
|
144
|
+
"ok": masscan_path is not None,
|
|
145
|
+
"path": masscan_path,
|
|
146
|
+
"has_caps": has_caps,
|
|
147
|
+
"message": "Masscan is fully configured and ready for non-root execution." if has_caps else f"Masscan found at {masscan_path}, but raw socket capabilities should be set: sudo setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip {masscan_path}",
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def check_nuclei(self) -> Dict[str, Any]:
|
|
151
|
+
"""Check Nuclei active vulnerability scanner binary and templates."""
|
|
152
|
+
nuclei_path = shutil.which("nuclei")
|
|
153
|
+
if not nuclei_path:
|
|
154
|
+
return {
|
|
155
|
+
"name": "Nuclei Vulnerability Scanner",
|
|
156
|
+
"status": "Optional / Not Installed",
|
|
157
|
+
"ok": True, # Nuclei is optional but recommended
|
|
158
|
+
"installed": False,
|
|
159
|
+
"path": None,
|
|
160
|
+
"message": "Nuclei binary not found in PATH. Install to enable active vulnerability verification.",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# Check version
|
|
164
|
+
version = "Unknown"
|
|
165
|
+
try:
|
|
166
|
+
res = subprocess.run([nuclei_path, "-version"], capture_output=True, text=True, check=False, timeout=3)
|
|
167
|
+
for line in (res.stdout + res.stderr).splitlines():
|
|
168
|
+
if "nuclei" in line.lower() or "version" in line.lower():
|
|
169
|
+
version = line.strip()
|
|
170
|
+
break
|
|
171
|
+
except Exception:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"name": "Nuclei Vulnerability Scanner",
|
|
176
|
+
"status": f"Ready ({version})",
|
|
177
|
+
"ok": True,
|
|
178
|
+
"installed": True,
|
|
179
|
+
"path": nuclei_path,
|
|
180
|
+
"message": f"Nuclei is available at {nuclei_path}.",
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
def check_exploitdb(self) -> Dict[str, Any]:
|
|
184
|
+
"""Check local ExploitDB searchsploit mapping database."""
|
|
185
|
+
try:
|
|
186
|
+
import cve_searchsploit as cs
|
|
187
|
+
mapping_path = Path(cs.pdir) / "exploitdb_mapping.json"
|
|
188
|
+
exists = mapping_path.is_file() and mapping_path.stat().st_size > 1024
|
|
189
|
+
return {
|
|
190
|
+
"name": "ExploitDB SearchSploit Cache",
|
|
191
|
+
"status": "Ready (Populated)" if exists else "Needs Update",
|
|
192
|
+
"ok": exists,
|
|
193
|
+
"path": str(mapping_path) if exists else None,
|
|
194
|
+
"message": "Local ExploitDB mapping cache is populated." if exists else "Run update-xdb to download local exploit mapping.",
|
|
195
|
+
}
|
|
196
|
+
except Exception as exc:
|
|
197
|
+
return {
|
|
198
|
+
"name": "ExploitDB SearchSploit Cache",
|
|
199
|
+
"status": "Error",
|
|
200
|
+
"ok": False,
|
|
201
|
+
"message": f"Could not inspect ExploitDB mapping: {exc}",
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
def check_demo_database(self) -> Dict[str, Any]:
|
|
205
|
+
"""Check default demo graph database."""
|
|
206
|
+
db_path = DETECTI_HOME / "data" / "dbs" / "example.com.sqlite"
|
|
207
|
+
exists = db_path.is_file()
|
|
208
|
+
return {
|
|
209
|
+
"name": "Default Demo Graph Dataset",
|
|
210
|
+
"status": "Ready (example.com.sqlite)" if exists else "Missing",
|
|
211
|
+
"ok": exists,
|
|
212
|
+
"message": "Pre-packaged demo dataset available for instant DetecTIHound visualization." if exists else "Demo dataset missing.",
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
def render_diagnostics_table(self, checks: Dict[str, Any]) -> None:
|
|
216
|
+
"""Render a formatted, high-contrast Rich diagnostics table."""
|
|
217
|
+
table = Table(title="DetecTI-CLI System & Environment Diagnostics", show_header=True, header_style="bold cyan")
|
|
218
|
+
table.add_column("Component / Subsystem", style="bold white", min_width=28)
|
|
219
|
+
table.add_column("Status", style="bold", min_width=24)
|
|
220
|
+
table.add_column("Diagnostic Details", style="dim")
|
|
221
|
+
|
|
222
|
+
for key, info in checks.items():
|
|
223
|
+
name = info.get("name", key)
|
|
224
|
+
status = info.get("status", "Unknown")
|
|
225
|
+
is_ok = info.get("ok", False)
|
|
226
|
+
msg = info.get("message", "")
|
|
227
|
+
|
|
228
|
+
if is_ok and "Needs" not in status:
|
|
229
|
+
status_styled = f"[bold green]✔ {status}[/bold green]"
|
|
230
|
+
elif "Optional" in status:
|
|
231
|
+
status_styled = f"[yellow]⚠ {status}[/yellow]"
|
|
232
|
+
elif not is_ok:
|
|
233
|
+
status_styled = f"[bold red]✘ {status}[/bold red]"
|
|
234
|
+
else:
|
|
235
|
+
status_styled = f"[yellow]⚠ {status}[/yellow]"
|
|
236
|
+
|
|
237
|
+
table.add_row(name, status_styled, msg)
|
|
238
|
+
|
|
239
|
+
self.console.print(table)
|
|
240
|
+
|
|
241
|
+
def run_automated_setup(self) -> bool:
|
|
242
|
+
"""Run automated setup: creates directories, .env file, configures capabilities, and updates databases."""
|
|
243
|
+
self.console.print("\n[bold cyan]🚀 Starting DetecTI-CLI Automated Environment Setup...[/bold cyan]\n")
|
|
244
|
+
|
|
245
|
+
all_success = True
|
|
246
|
+
|
|
247
|
+
# Step 0: Dashboard Admin Password Setup
|
|
248
|
+
self.console.print("🔐 [bold white]Step 0/8: Configuring DetecTIHound Dashboard Admin...[/bold white]")
|
|
249
|
+
try:
|
|
250
|
+
import getpass
|
|
251
|
+
import sys
|
|
252
|
+
def update_env_jwt(password: str):
|
|
253
|
+
import hashlib
|
|
254
|
+
import re
|
|
255
|
+
jwt_secret = hashlib.sha256(password.encode('utf-8')).hexdigest()
|
|
256
|
+
env_path = DETECTI_HOME / '.env'
|
|
257
|
+
if env_path.exists():
|
|
258
|
+
with open(env_path, 'r') as f:
|
|
259
|
+
env_content = f.read()
|
|
260
|
+
if 'JWT_SECRET_KEY=' in env_content:
|
|
261
|
+
env_content = re.sub(r'JWT_SECRET_KEY=.*', f'JWT_SECRET_KEY={jwt_secret}', env_content)
|
|
262
|
+
else:
|
|
263
|
+
if env_content and not env_content.endswith('\n'):
|
|
264
|
+
env_content += '\n'
|
|
265
|
+
env_content += f'JWT_SECRET_KEY={jwt_secret}\n'
|
|
266
|
+
with open(env_path, 'w') as f:
|
|
267
|
+
f.write(env_content)
|
|
268
|
+
else:
|
|
269
|
+
with open(env_path, 'w') as f:
|
|
270
|
+
f.write(f'JWT_SECRET_KEY={jwt_secret}\n')
|
|
271
|
+
import os
|
|
272
|
+
sys.path.insert(0, str(self.root_dir))
|
|
273
|
+
from core.database.config_db import ConfigDBManager, get_password_hash
|
|
274
|
+
|
|
275
|
+
db_dir = DETECTI_HOME / "data" / "dbs"
|
|
276
|
+
db_dir.mkdir(parents=True, exist_ok=True)
|
|
277
|
+
config_db = ConfigDBManager(DETECTI_HOME / "data" / "config.sqlite")
|
|
278
|
+
|
|
279
|
+
if config_db.user_exists("admin"):
|
|
280
|
+
change = self.console.input(" [yellow]Admin user already exists. Do you want to change the password? (y/N): [/yellow]").strip().lower()
|
|
281
|
+
if change == 'y':
|
|
282
|
+
while True:
|
|
283
|
+
pwd1 = getpass.getpass(" Enter new password for 'admin': ")
|
|
284
|
+
pwd2 = getpass.getpass(" Confirm new password: ")
|
|
285
|
+
if pwd1 == pwd2 and len(pwd1) >= 4:
|
|
286
|
+
config_db.update_user_password("admin", get_password_hash(pwd1))
|
|
287
|
+
update_env_jwt(pwd1)
|
|
288
|
+
self.console.print(" [green]✔ Admin password updated successfully.[/green]")
|
|
289
|
+
break
|
|
290
|
+
elif len(pwd1) < 4:
|
|
291
|
+
self.console.print(" [red]Password must be at least 4 characters.[/red]")
|
|
292
|
+
else:
|
|
293
|
+
self.console.print(" [red]Passwords do not match. Try again.[/red]")
|
|
294
|
+
else:
|
|
295
|
+
self.console.print(" [green]✔ Admin configuration skipped.[/green]")
|
|
296
|
+
else:
|
|
297
|
+
self.console.print(" [cyan]Creating default 'admin' user for the web dashboard.[/cyan]")
|
|
298
|
+
while True:
|
|
299
|
+
pwd1 = getpass.getpass(" Enter password for 'admin': ")
|
|
300
|
+
pwd2 = getpass.getpass(" Confirm password: ")
|
|
301
|
+
if pwd1 == pwd2 and len(pwd1) >= 4:
|
|
302
|
+
config_db.create_user("admin", get_password_hash(pwd1))
|
|
303
|
+
update_env_jwt(pwd1)
|
|
304
|
+
self.console.print(" [green]✔ Admin user created successfully.[/green]")
|
|
305
|
+
break
|
|
306
|
+
elif len(pwd1) < 4:
|
|
307
|
+
self.console.print(" [red]Password must be at least 4 characters.[/red]")
|
|
308
|
+
else:
|
|
309
|
+
self.console.print(" [red]Passwords do not match. Try again.[/red]")
|
|
310
|
+
except Exception as e:
|
|
311
|
+
self.console.print(f" [red]⚠ Failed to configure admin: {e}[/red]")
|
|
312
|
+
import traceback
|
|
313
|
+
traceback.print_exc()
|
|
314
|
+
|
|
315
|
+
# Step 1: Create Directories & Copy Demo DB
|
|
316
|
+
self.console.print("📁 [bold white]Step 1/8: Initializing project directories...[/bold white]")
|
|
317
|
+
for d in [DETECTI_HOME / "data" / "dbs", Path.cwd() / "reports"]:
|
|
318
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
319
|
+
|
|
320
|
+
demo_db_src = self.root_dir / "data" / "dbs" / "example.com.sqlite"
|
|
321
|
+
demo_db_dst = DETECTI_HOME / "data" / "dbs" / "example.com.sqlite"
|
|
322
|
+
if demo_db_src.exists() and not demo_db_dst.exists():
|
|
323
|
+
shutil.copy2(demo_db_src, demo_db_dst)
|
|
324
|
+
self.console.print(" [green]✔ Demo database (example.com.sqlite) initialized.[/green]")
|
|
325
|
+
|
|
326
|
+
self.console.print(" [green]✔ Operational directories verified (data/dbs, reports).[/green]")
|
|
327
|
+
|
|
328
|
+
# Step 2: Configure .env
|
|
329
|
+
self.console.print("\n⚙️ [bold white]Step 2/8: Checking environment configuration (.env)...[/bold white]")
|
|
330
|
+
env_file = DETECTI_HOME / ".env"
|
|
331
|
+
env_example = self.root_dir / ".env.example"
|
|
332
|
+
if not env_file.exists() and env_example.exists():
|
|
333
|
+
shutil.copy(env_example, env_file)
|
|
334
|
+
self.console.print(f" [green]✔ Created .env file at {env_file} from template.[/green]")
|
|
335
|
+
elif env_file.exists():
|
|
336
|
+
self.console.print(f" [green]✔ Existing .env file detected at {env_file} and preserved.[/green]")
|
|
337
|
+
else:
|
|
338
|
+
self.console.print(" [yellow]⚠ No .env template found. Skipped.[/yellow]")
|
|
339
|
+
|
|
340
|
+
# Step 3: Python dependencies check / install
|
|
341
|
+
self.console.print("\n🐍 [bold white]Step 3/8: Verifying Python dependencies...[/bold white]")
|
|
342
|
+
req_file = self.root_dir / "requirements.txt"
|
|
343
|
+
dep_check = self.check_python_modules()
|
|
344
|
+
if not dep_check["ok"] and req_file.exists():
|
|
345
|
+
self.console.print(f" [yellow]Installing missing dependencies: {', '.join(dep_check['missing'])}...[/yellow]")
|
|
346
|
+
try:
|
|
347
|
+
subprocess.run([sys.executable, "-m", "pip", "install", "-r", str(req_file)], check=True)
|
|
348
|
+
self.console.print(" [green]✔ Python dependencies installed successfully.[/green]")
|
|
349
|
+
except Exception as exc:
|
|
350
|
+
self.console.print(f" [red]✘ Failed to install Python dependencies: {exc}[/red]")
|
|
351
|
+
all_success = False
|
|
352
|
+
else:
|
|
353
|
+
self.console.print(" [green]✔ All Python core dependencies are satisfied.[/green]")
|
|
354
|
+
|
|
355
|
+
# Step 4: Masscan capabilities configuration
|
|
356
|
+
self.console.print("\n⚡ [bold white]Step 4/8: Configuring Masscan network capabilities...[/bold white]")
|
|
357
|
+
masscan_bin = shutil.which("masscan")
|
|
358
|
+
if masscan_bin:
|
|
359
|
+
is_root = hasattr(os, "geteuid") and os.geteuid() == 0
|
|
360
|
+
if is_root:
|
|
361
|
+
self.console.print(" [green]✔ Running as root: raw packet sockets are natively authorized.[/green]")
|
|
362
|
+
else:
|
|
363
|
+
setcap_bin = shutil.which("setcap")
|
|
364
|
+
if setcap_bin:
|
|
365
|
+
try:
|
|
366
|
+
cmd = ["sudo", setcap_bin, "cap_net_raw,cap_net_admin,cap_net_bind_service+eip", masscan_bin]
|
|
367
|
+
self.console.print(f" [cyan]Applying Linux capabilities via setcap...[/cyan]")
|
|
368
|
+
res = subprocess.run(cmd, check=False)
|
|
369
|
+
if res.returncode == 0:
|
|
370
|
+
self.console.print(" [green]✔ Granted non-root raw socket capabilities to masscan.[/green]")
|
|
371
|
+
else:
|
|
372
|
+
self.console.print(f" [yellow]⚠ Could not apply setcap automatically. Run manually if needed:[/yellow]\n sudo setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip {masscan_bin}")
|
|
373
|
+
except Exception as exc:
|
|
374
|
+
self.console.print(f" [yellow]⚠ Note: Run manually if non-root WebGUI scanning is needed:\n sudo setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip {masscan_bin}[/yellow]")
|
|
375
|
+
else:
|
|
376
|
+
self.console.print(f" [yellow]⚠ 'setcap' binary not found. Masscan may require root permissions to scan.[/yellow]")
|
|
377
|
+
else:
|
|
378
|
+
self.console.print(" [yellow]⚠ Masscan is not installed on this system.[/yellow]")
|
|
379
|
+
self.console.print(" [dim]Install on Linux with: sudo apt install -y masscan (or pacman/dnf)[/dim]")
|
|
380
|
+
|
|
381
|
+
# Step 5: ExploitDB Cache Update
|
|
382
|
+
self.console.print("\n💣 [bold white]Step 5/8: Initializing ExploitDB vulnerability mapping...[/bold white]")
|
|
383
|
+
try:
|
|
384
|
+
from modules.exploitdb import ExploitDBModule
|
|
385
|
+
ExploitDBModule.update_database()
|
|
386
|
+
self.console.print(" [green]✔ ExploitDB mapping database initialized & updated.[/green]")
|
|
387
|
+
except Exception as exc:
|
|
388
|
+
self.console.print(f" [yellow]⚠ ExploitDB update notice: {exc}[/yellow]")
|
|
389
|
+
|
|
390
|
+
# Step 6: Nuclei Templates Check
|
|
391
|
+
self.console.print("\n🛡️ [bold white]Step 6/8: Checking Nuclei vulnerability engine...[/bold white]")
|
|
392
|
+
nuclei_bin = shutil.which("nuclei")
|
|
393
|
+
if nuclei_bin:
|
|
394
|
+
try:
|
|
395
|
+
self.console.print(" [cyan]Updating Nuclei community vulnerability templates...[/cyan]")
|
|
396
|
+
subprocess.run([nuclei_bin, "-update-templates", "-silent"], check=False, timeout=15)
|
|
397
|
+
self.console.print(" [green]✔ Nuclei templates checked & updated.[/green]")
|
|
398
|
+
except Exception:
|
|
399
|
+
self.console.print(" [green]✔ Nuclei engine is active.[/green]")
|
|
400
|
+
else:
|
|
401
|
+
self.console.print(" [dim]Nuclei is optional and not currently installed.[/dim]")
|
|
402
|
+
|
|
403
|
+
# Step 7: Configure Global Executable & Install Source
|
|
404
|
+
self.console.print("\n🌍 [bold white]Step 7/8: Installing Source & Configuring Global Executable...[/bold white]")
|
|
405
|
+
try:
|
|
406
|
+
is_root = hasattr(os, "geteuid") and os.geteuid() == 0
|
|
407
|
+
if is_root:
|
|
408
|
+
bin_dir = Path("/usr/local/bin")
|
|
409
|
+
install_dir = Path("/opt/detecti-cli")
|
|
410
|
+
else:
|
|
411
|
+
bin_dir = Path.home() / ".local" / "bin"
|
|
412
|
+
install_dir = DETECTI_HOME / "app"
|
|
413
|
+
|
|
414
|
+
if self.root_dir.resolve() == install_dir.resolve():
|
|
415
|
+
self.console.print(f" [cyan]Running from installed location ({install_dir}). Skipping source copy.[/cyan]")
|
|
416
|
+
else:
|
|
417
|
+
self.console.print(f" [cyan]Copying source code to {install_dir}...[/cyan]")
|
|
418
|
+
|
|
419
|
+
# Remove old install dir if exists to ensure clean install
|
|
420
|
+
if install_dir.exists():
|
|
421
|
+
shutil.rmtree(install_dir)
|
|
422
|
+
|
|
423
|
+
# Copy source code, ignoring unnecessary/heavy folders
|
|
424
|
+
shutil.copytree(
|
|
425
|
+
self.root_dir,
|
|
426
|
+
install_dir,
|
|
427
|
+
ignore=shutil.ignore_patterns('.git', '__pycache__', 'node_modules', 'reports', '.pytest_cache')
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
431
|
+
wrapper_path = bin_dir / "detecti-cli"
|
|
432
|
+
|
|
433
|
+
# The actual python script is now in the install_dir
|
|
434
|
+
target_script = install_dir / "cli.py"
|
|
435
|
+
|
|
436
|
+
wrapper_content = f"""#!/usr/bin/env bash
|
|
437
|
+
# DetecTI-CLI Global Wrapper
|
|
438
|
+
python3 "{target_script}" "$@"
|
|
439
|
+
"""
|
|
440
|
+
wrapper_path.write_text(wrapper_content)
|
|
441
|
+
# Make it executable (chmod +x)
|
|
442
|
+
wrapper_path.chmod(wrapper_path.stat().st_mode | 0o111)
|
|
443
|
+
|
|
444
|
+
self.console.print(f" [green]✔ Source installed securely in {install_dir}[/green]")
|
|
445
|
+
self.console.print(f" [green]✔ Global executable created at {wrapper_path}[/green]")
|
|
446
|
+
if not is_root:
|
|
447
|
+
self.console.print(f" [cyan]ℹ Make sure {bin_dir} is in your PATH.[/cyan]")
|
|
448
|
+
except Exception as exc:
|
|
449
|
+
self.console.print(f" [red]✘ Failed to install source or create global executable: {exc}[/red]")
|
|
450
|
+
all_success = False
|
|
451
|
+
|
|
452
|
+
self.console.print("\n[bold green]✅ DetecTI-CLI setup routine completed![/bold green]\n")
|
|
453
|
+
return all_success
|
detecti/web/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API routes for DetecTI-CLI web dashboard."""
|
detecti/web/api/auth.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Depends, HTTPException, status, Response, Request
|
|
6
|
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
from jose import JWTError, jwt
|
|
9
|
+
|
|
10
|
+
from detecti.core.database.config_db import ConfigDBManager, verify_password
|
|
11
|
+
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
load_dotenv()
|
|
14
|
+
|
|
15
|
+
# Secret key for JWT
|
|
16
|
+
SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
|
|
17
|
+
if not SECRET_KEY:
|
|
18
|
+
# Emite um aviso ou falha se não houver chave (força rodar o setup)
|
|
19
|
+
raise ValueError("JWT_SECRET_KEY is missing from environment/ .env file. Please run the setup command.")
|
|
20
|
+
ALGORITHM = "HS256"
|
|
21
|
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 30 minutos
|
|
22
|
+
|
|
23
|
+
router = APIRouter(tags=["Auth"])
|
|
24
|
+
|
|
25
|
+
class Token(BaseModel):
|
|
26
|
+
access_token: str
|
|
27
|
+
token_type: str
|
|
28
|
+
|
|
29
|
+
def get_config_db():
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
project_root = Path(__file__).parent.parent.parent
|
|
32
|
+
db_path = project_root / "data" / "config.sqlite"
|
|
33
|
+
return ConfigDBManager(db_path)
|
|
34
|
+
|
|
35
|
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
|
36
|
+
to_encode = data.copy()
|
|
37
|
+
if expires_delta:
|
|
38
|
+
expire = datetime.utcnow() + expires_delta
|
|
39
|
+
else:
|
|
40
|
+
expire = datetime.utcnow() + timedelta(minutes=15)
|
|
41
|
+
to_encode.update({"exp": expire})
|
|
42
|
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
|
43
|
+
return encoded_jwt
|
|
44
|
+
|
|
45
|
+
# Use a custom dependency instead of OAuth2PasswordBearer directly
|
|
46
|
+
# to check the cookie if the header is not present
|
|
47
|
+
def get_current_user(request: Request, config_db: ConfigDBManager = Depends(get_config_db)):
|
|
48
|
+
token = request.cookies.get("detecti_token")
|
|
49
|
+
if not token:
|
|
50
|
+
# fallback to bearer
|
|
51
|
+
auth = request.headers.get("Authorization")
|
|
52
|
+
if auth and auth.startswith("Bearer "):
|
|
53
|
+
token = auth.split(" ")[1]
|
|
54
|
+
|
|
55
|
+
credentials_exception = HTTPException(
|
|
56
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
57
|
+
detail="Could not validate credentials",
|
|
58
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
59
|
+
)
|
|
60
|
+
if not token:
|
|
61
|
+
raise credentials_exception
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
65
|
+
username: str = payload.get("sub")
|
|
66
|
+
if username is None:
|
|
67
|
+
raise credentials_exception
|
|
68
|
+
except JWTError:
|
|
69
|
+
raise credentials_exception
|
|
70
|
+
|
|
71
|
+
user = config_db.get_user(username)
|
|
72
|
+
if user is None:
|
|
73
|
+
raise credentials_exception
|
|
74
|
+
return user
|
|
75
|
+
|
|
76
|
+
@router.post("/login", response_model=Token)
|
|
77
|
+
async def login_for_access_token(
|
|
78
|
+
response: Response,
|
|
79
|
+
form_data: OAuth2PasswordRequestForm = Depends(),
|
|
80
|
+
config_db: ConfigDBManager = Depends(get_config_db)
|
|
81
|
+
):
|
|
82
|
+
user = config_db.get_user(form_data.username)
|
|
83
|
+
if not user or not verify_password(form_data.password, user["password_hash"]):
|
|
84
|
+
raise HTTPException(
|
|
85
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
86
|
+
detail="Incorrect username or password",
|
|
87
|
+
headers={"WWW-Authenticate": "Bearer"},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
|
91
|
+
access_token = create_access_token(
|
|
92
|
+
data={"sub": user["username"]}, expires_delta=access_token_expires
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
response.set_cookie(
|
|
96
|
+
key="detecti_token",
|
|
97
|
+
value=access_token,
|
|
98
|
+
httponly=True,
|
|
99
|
+
max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
|
100
|
+
expires=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
|
101
|
+
samesite="lax"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return {"access_token": access_token, "token_type": "bearer"}
|
|
105
|
+
|
|
106
|
+
@router.post("/logout")
|
|
107
|
+
async def logout(response: Response):
|
|
108
|
+
response.delete_cookie("detecti_token")
|
|
109
|
+
return {"message": "Successfully logged out"}
|