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.
Files changed (64) hide show
  1. detecti/__init__.py +0 -0
  2. detecti/cli.py +649 -0
  3. detecti/config.py +188 -0
  4. detecti/core/__init__.py +1 -0
  5. detecti/core/database/__init__.py +5 -0
  6. detecti/core/database/config_db.py +73 -0
  7. detecti/core/database/schema.py +136 -0
  8. detecti/core/database/storage.py +1388 -0
  9. detecti/core/engine.py +1032 -0
  10. detecti/core/models.py +278 -0
  11. detecti/data/config.sqlite +0 -0
  12. detecti/data/dbs/.gitkeep +2 -0
  13. detecti/data/dbs/example.com.sqlite +0 -0
  14. detecti/modules/__init__.py +29 -0
  15. detecti/modules/base.py +57 -0
  16. detecti/modules/censys.py +813 -0
  17. detecti/modules/crtsh.py +98 -0
  18. detecti/modules/exploitdb.py +138 -0
  19. detecti/modules/masscan.py +561 -0
  20. detecti/modules/nuclei.py +449 -0
  21. detecti/modules/nvd.py +300 -0
  22. detecti/modules/reverse_whois.py +225 -0
  23. detecti/modules/shodan.py +412 -0
  24. detecti/reporters/__init__.py +7 -0
  25. detecti/reporters/csv_reporter.py +74 -0
  26. detecti/reporters/html_reporter.py +356 -0
  27. detecti/reporters/json_reporter.py +26 -0
  28. detecti/reporters/markdown_reporter.py +203 -0
  29. detecti/utils/__init__.py +1 -0
  30. detecti/utils/http.py +294 -0
  31. detecti/utils/logger.py +378 -0
  32. detecti/utils/setup.py +453 -0
  33. detecti/web/__init__.py +6 -0
  34. detecti/web/api/__init__.py +1 -0
  35. detecti/web/api/auth.py +109 -0
  36. detecti/web/api/graph_builder.py +901 -0
  37. detecti/web/api/routes.py +1602 -0
  38. detecti/web/process_manager.py +283 -0
  39. detecti/web/server.py +183 -0
  40. detecti/web/static/android-chrome-192x192.png +0 -0
  41. detecti/web/static/android-chrome-512x512.png +0 -0
  42. detecti/web/static/apple-touch-icon.png +0 -0
  43. detecti/web/static/css/__init__.py +1 -0
  44. detecti/web/static/css/dashboard.css +3802 -0
  45. detecti/web/static/favicon-16x16.png +0 -0
  46. detecti/web/static/favicon-32x32.png +0 -0
  47. detecti/web/static/favicon.ico +0 -0
  48. detecti/web/static/img/DetecTI_Security_Logo.png +0 -0
  49. detecti/web/static/img/detecti-ico.png +0 -0
  50. detecti/web/static/index.html +677 -0
  51. detecti/web/static/js/__init__.py +1 -0
  52. detecti/web/static/js/api.js +177 -0
  53. detecti/web/static/js/cytoscape-cose-bilkent.js +458 -0
  54. detecti/web/static/js/cytoscape-dagre.js +397 -0
  55. detecti/web/static/js/cytoscape.min.js +31 -0
  56. detecti/web/static/js/dagre.min.js +3809 -0
  57. detecti/web/static/js/graph.js +7439 -0
  58. detecti/web/static/js/lucide.min.js +12 -0
  59. detecti/web/static/login.html +290 -0
  60. detecti/web/static/site.webmanifest +1 -0
  61. detecti_cli-2.0.0.dist-info/METADATA +554 -0
  62. detecti_cli-2.0.0.dist-info/RECORD +64 -0
  63. detecti_cli-2.0.0.dist-info/WHEEL +4 -0
  64. detecti_cli-2.0.0.dist-info/entry_points.txt +3 -0
detecti/__init__.py ADDED
File without changes
detecti/cli.py ADDED
@@ -0,0 +1,649 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ # Ensure project root is in sys.path when running cli.py directly
9
+ _proj_root = str(Path(__file__).resolve().parent)
10
+ if _proj_root not in sys.path:
11
+ sys.path.insert(0, _proj_root)
12
+
13
+ from typing import List, Optional
14
+ import click
15
+ import typer
16
+ from rich.panel import Panel
17
+ from rich.progress import Progress, SpinnerColumn, TextColumn
18
+
19
+ # Click 8.3+ compatibility patch for Typer help formatting
20
+ _orig_make_metavar = click.Option.make_metavar
21
+
22
+
23
+ def _compat_make_metavar(self, ctx=None):
24
+ if ctx is None:
25
+ try:
26
+ return _orig_make_metavar(self, None)
27
+ except TypeError:
28
+ return self.name.upper() if self.name else "TEXT"
29
+ return _orig_make_metavar(self, ctx)
30
+
31
+
32
+ click.Option.make_metavar = _compat_make_metavar
33
+
34
+ try:
35
+ from __init__ import __version__
36
+ except (ImportError, ModuleNotFoundError):
37
+ __version__ = "2.0.0"
38
+
39
+ from detecti.config import settings, DETECTI_HOME
40
+ from detecti.core.engine import ThreatTrackEngine, DetectIEngine
41
+ from detecti.modules.exploitdb import ExploitDBModule
42
+ from detecti.reporters.html_reporter import HTMLReporter
43
+ from detecti.reporters.json_reporter import JSONReporter
44
+ from detecti.reporters.markdown_reporter import MarkdownReporter
45
+ from detecti.reporters.csv_reporter import CSVReporter
46
+ from detecti.utils.logger import (
47
+ console,
48
+ get_real_ip,
49
+ print_banner,
50
+ print_error,
51
+ print_info,
52
+ print_section_header,
53
+ print_success,
54
+ print_warning,
55
+ render_executive_summary,
56
+ render_scan_output,
57
+ render_summary_panel,
58
+ )
59
+
60
+ # Print banner on --help as well
61
+ _orig_format_help = click.Command.format_help
62
+
63
+
64
+ def _banner_format_help(self, ctx, formatter):
65
+ print_banner()
66
+ return _orig_format_help(self, ctx, formatter)
67
+
68
+
69
+ click.Command.format_help = _banner_format_help
70
+
71
+ # Hardcode cli_name for global wrapper execution
72
+ cli_name = "detecti-cli"
73
+
74
+ app = typer.Typer(
75
+ name=cli_name,
76
+ help="DetecTI-CLI: External Attack Surface Mapping & Threat Intelligence Engine",
77
+ add_completion=False,
78
+ no_args_is_help=True,
79
+ rich_markup_mode=None,
80
+ )
81
+
82
+ # Create hound subcommand group (Interactive EASM Attack Surface Graph Dashboard)
83
+ hound_app = typer.Typer(
84
+ name="hound",
85
+ help="DetecTIHound - Interactive EASM Attack Surface Graph Dashboard management",
86
+ add_completion=False,
87
+ )
88
+ app.add_typer(hound_app, name="hound")
89
+
90
+
91
+ def target_to_db_name(target: str) -> str:
92
+ """Convert target string to a clean SQLite database filename (e.g. example.com.sqlite)."""
93
+ t = target.strip()
94
+ # If target is a file path, use its stem
95
+ if Path(t).is_file() and not t.startswith("http"):
96
+ base = Path(t).stem or "file_target"
97
+ return f"{base}.sqlite"
98
+
99
+ try:
100
+ from core.engine import ThreatTrackEngine
101
+ engine = ThreatTrackEngine()
102
+ meta = engine.parse_target_metadata(t)
103
+ clean_target = meta.get("clean_target") or t
104
+ except Exception:
105
+ clean_target = t
106
+
107
+ # Replace invalid/unfriendly path characters while preserving dots, hyphens and underscores
108
+ clean = []
109
+ for c in clean_target:
110
+ if c.isalnum() or c in (".", "-", "_"):
111
+ clean.append(c)
112
+ else:
113
+ clean.append("_")
114
+ cleaned_name = "".join(clean).strip("._-")
115
+ if not cleaned_name:
116
+ cleaned_name = "scan_target"
117
+
118
+ if not cleaned_name.endswith(".sqlite"):
119
+ cleaned_name = f"{cleaned_name}.sqlite"
120
+ return cleaned_name
121
+
122
+
123
+ @app.command(name="scan")
124
+ def scan_command(
125
+ target: Optional[str] = typer.Option(
126
+ None,
127
+ "-t",
128
+ "--target",
129
+ help="Target IP, CIDR, domain, email, CVE, search query, or targets file (e.g., targets.txt)",
130
+ ),
131
+ output_format: str = typer.Option(
132
+ "table",
133
+ "-o",
134
+ "--format",
135
+ help="Output report format: table, json, markdown, html, csv, all",
136
+ ),
137
+ output_file: Optional[Path] = typer.Option(
138
+ None,
139
+ "-f",
140
+ "--output-file",
141
+ help="Custom file path to export the report (e.g., report.json, report.md, report.html, or report.csv)",
142
+ ),
143
+ output_dir: Optional[Path] = typer.Option(
144
+ None,
145
+ "-d",
146
+ "--output-dir",
147
+ help="Directory to save generated JSON/Markdown/HTML reports",
148
+ ),
149
+ cvss_filter: Optional[str] = typer.Option(
150
+ None,
151
+ "--cvss",
152
+ help="Filter vulnerabilities by CVSS severity: critical, high, medium, low",
153
+ ),
154
+ create_db: Optional[str] = typer.Option(
155
+ None,
156
+ "--create-db",
157
+ help="Custom name for SQLite database in ./data/dbs/ (optional, defaults to target root)",
158
+ ),
159
+ ) -> None:
160
+ """Execute complete attack surface mapping and threat intelligence analysis.
161
+
162
+ Examples:
163
+ {cli_name} scan -t example.com
164
+ {cli_name} scan -t example.com --create-db custom_name
165
+ {cli_name} scan -t 192.168.1.0/24
166
+ {cli_name} scan -t CVE-2021-44228
167
+ """.format(cli_name=cli_name)
168
+
169
+ # Check if target is provided
170
+ if not target:
171
+ print_banner()
172
+ print_error("Target is required. Use -t/--target to specify a target.")
173
+ print_info("Examples:")
174
+ print_info(f" {cli_name} scan -t example.com")
175
+ print_info(f" {cli_name} scan -t targets.txt")
176
+ print_info(f" {cli_name} scan -t 192.168.1.0/24")
177
+ print_info(f" {cli_name} scan -t CVE-2021-44228")
178
+ print_info(f"Use '{cli_name} scan --help' for more options.")
179
+ raise typer.Exit(1)
180
+
181
+ # Pre-validate target before starting scan progress
182
+ try:
183
+ temp_engine = ThreatTrackEngine()
184
+ meta = temp_engine.parse_target_metadata(target)
185
+ except (FileNotFoundError, ValueError) as exc:
186
+ print_banner()
187
+ print_error(str(exc))
188
+ print_info("Target must be a valid IP, CIDR, Domain, URL, CVE, existing File, or Shodan Query filter (e.g., org:'Target', port:443).")
189
+ raise typer.Exit(1)
190
+
191
+ print_banner()
192
+
193
+ print_section_header("Scan Configuration")
194
+ console.print(f" [cyan]Target:[/cyan] [bold white]{target}[/bold white]")
195
+ if create_db:
196
+ console.print(f" [cyan]Custom DB:[/cyan] [bold white]{create_db}[/bold white]")
197
+ if cvss_filter:
198
+ console.print(f" [cyan]CVSS Filter:[/cyan] [bold yellow]{cvss_filter.upper()}[/bold yellow]")
199
+
200
+ # Shift-Left: Initialize Database before scan begins to capture live logs
201
+ is_cve = target.strip().upper().startswith("CVE-")
202
+ db_manager = None
203
+ final_db_name = None
204
+ if not is_cve:
205
+ try:
206
+ from core.database.storage import DatabaseManager
207
+ if create_db:
208
+ db_name = create_db if create_db.endswith('.sqlite') else f"{create_db}.sqlite"
209
+ else:
210
+ db_name = target_to_db_name(target)
211
+
212
+ dbs_dir = DETECTI_HOME / "data" / "dbs"
213
+ dbs_dir.mkdir(parents=True, exist_ok=True)
214
+ final_db_path = dbs_dir / db_name
215
+ final_db_name = db_name
216
+
217
+ db_manager = DatabaseManager(final_db_path)
218
+ except Exception as e:
219
+ print_warning(f"Could not initialize database early: {e}")
220
+
221
+ # Run async engine
222
+ with Progress(
223
+ SpinnerColumn(spinner_name="dots"),
224
+ TextColumn("[progress.description]{task.description}"),
225
+ console=console,
226
+ transient=True,
227
+ ) as progress:
228
+ task_id = progress.add_task("[bold cyan]Initializing DetecTI-CLI Intelligence Engine...", total=None)
229
+
230
+ def progress_cb(module_name: str, message: str) -> None:
231
+ progress.update(task_id, description=f"[bold cyan][{module_name}][/bold cyan] {message}")
232
+
233
+ engine = ThreatTrackEngine(progress_callback=progress_cb, db_manager=db_manager)
234
+ result = asyncio.run(
235
+ engine.scan(
236
+ target=target,
237
+ enabled_modules=["all"],
238
+ cvss_filter=cvss_filter,
239
+ )
240
+ )
241
+
242
+ # Check if scan yielded any actionable findings / assets
243
+ has_results = (
244
+ (result.summary and (result.summary.total_hosts_count > 0 or result.summary.total_findings > 0))
245
+ or len(result.hosts) > 0
246
+ or len(result.findings) > 0
247
+ )
248
+
249
+ # Automatic SQLite database storage for non-CVE targets with valid findings
250
+ if not is_cve and has_results and db_manager:
251
+ try:
252
+ progress.update(task_id, description=f"[bold cyan]Storing results in SQLite database ({final_db_name})...")
253
+ db_manager.store_scan_result(result)
254
+ print_success(f"Scan results stored in database: [bold underline]{db_manager.db_path.resolve() if hasattr(db_manager.db_path, 'resolve') else db_manager.db_path}[/bold underline]")
255
+ except Exception as e:
256
+ print_error(f"Failed to store results in database: {e}")
257
+
258
+ # Automatically launch DetecTIHound WebGUI (only if not already running)
259
+ try:
260
+ from web.process_manager import WebServerManager
261
+
262
+ ws_manager = WebServerManager()
263
+ host = "0.0.0.0"
264
+ port = 8000
265
+ real_ip = get_real_ip()
266
+
267
+ if ws_manager.is_running():
268
+ status = ws_manager.get_status() or {}
269
+ srv_port = status.get("port", port)
270
+ print_info(f"🌐 DetecTIHound WebGUI is already active (PID: {status.get('pid', 'N/A')}).")
271
+ console.print(f" 👉 [bold cyan]Local URL:[/bold cyan] [bold underline cyan]http://localhost:{srv_port}[/bold underline cyan] (Select [bold cyan]{final_db_name or db_name}[/bold cyan] in database dropdown)")
272
+ console.print(f" 👉 [bold cyan]Network URL:[/bold cyan] [bold underline cyan]http://{real_ip}:{srv_port}[/bold underline cyan]")
273
+ else:
274
+ started = ws_manager.start_server(final_db_name or db_name, host, port)
275
+ if started:
276
+ print_success(f"🚀 DetecTIHound WebGUI started automatically with database: [bold cyan]{final_db_name or db_name}[/bold cyan]")
277
+ console.print(f" 👉 [bold cyan]Local URL:[/bold cyan] [bold underline cyan]http://localhost:{port}[/bold underline cyan]")
278
+ console.print(f" 👉 [bold cyan]Network URL:[/bold cyan] [bold underline cyan]http://{real_ip}:{port}[/bold underline cyan]")
279
+ else:
280
+ print_info(f"Open DetecTIHound: [bold cyan]{cli_name} hound start --db {final_db_name or db_name}[/bold cyan]")
281
+ except Exception as e:
282
+ print_warning(f"Could not automatically launch DetecTIHound WebGUI: {e}")
283
+ elif not is_cve and not has_results:
284
+ print_warning(f"No intelligence assets or findings discovered for target '{target}'. SQLite database was not created.")
285
+
286
+ # 1. Executive Terminal Output
287
+ if output_format.lower() in ("table", "all") or not output_file:
288
+ render_executive_summary(result)
289
+
290
+ # 2. File Export Handling
291
+ safe_target = "".join(c if c.isalnum() else "_" for c in target)[:40]
292
+ timestamp = result.started_at.strftime("%Y%m%d_%H%M%S")
293
+
294
+ save_dir = output_dir or Path.cwd()
295
+ save_dir.mkdir(parents=True, exist_ok=True)
296
+
297
+ # Determine what to export (supports explicit -o or inferred from -f extension)
298
+ fmt = output_format.lower()
299
+ export_json = fmt in ("json", "all") or (output_file and output_file.suffix == ".json")
300
+ export_md = fmt in ("markdown", "md", "all") or (output_file and output_file.suffix in (".md", ".markdown"))
301
+ export_html = fmt in ("html", "all") or (output_file and output_file.suffix in (".html", ".htm"))
302
+ export_csv = fmt in ("csv", "all") or (output_file and output_file.suffix == ".csv")
303
+
304
+ # JSON Export
305
+ if export_json:
306
+ json_path = (
307
+ output_file
308
+ if output_file and output_file.suffix == ".json"
309
+ else save_dir / f"detecti_{safe_target}_{timestamp}.json"
310
+ )
311
+ JSONReporter.save(result, json_path)
312
+ print_success(f"JSON report saved to: [bold underline]{json_path.resolve()}[/bold underline]")
313
+
314
+ # CSV Export
315
+ if export_csv:
316
+ csv_path = (
317
+ output_file
318
+ if output_file and output_file.suffix == ".csv"
319
+ else save_dir / f"detecti_{safe_target}_{timestamp}.csv"
320
+ )
321
+ CSVReporter.save(result, csv_path)
322
+ print_success(f"CSV report saved to: [bold underline]{csv_path.resolve()}[/bold underline]")
323
+
324
+ # Markdown Export
325
+ if export_md:
326
+ md_path = (
327
+ output_file
328
+ if output_file and output_file.suffix in (".md", ".markdown")
329
+ else save_dir / f"detecti_{safe_target}_{timestamp}.md"
330
+ )
331
+ MarkdownReporter.save(result, md_path)
332
+ print_success(f"Markdown executive report saved to: [bold underline]{md_path.resolve()}[/bold underline]")
333
+
334
+ # HTML Export
335
+ if export_html:
336
+ html_path = (
337
+ output_file
338
+ if output_file and output_file.suffix in (".html", ".htm")
339
+ else save_dir / f"detecti_{safe_target}_{timestamp}.html"
340
+ )
341
+ HTMLReporter.save(result, html_path)
342
+ print_success(f"HTML executive report saved to: [bold underline]{html_path.resolve()}[/bold underline]")
343
+
344
+
345
+ @app.command(name="update-xdb")
346
+ def update_xdb_command() -> None:
347
+ """Update the local ExploitDB / searchsploit vulnerability mapping database."""
348
+ print_banner()
349
+ print_info("Refreshing ExploitDB database...")
350
+ try:
351
+ ExploitDBModule.update_database()
352
+ print_success("ExploitDB database successfully updated!")
353
+ except Exception as exc:
354
+ print_error(f"Error updating ExploitDB: {exc}")
355
+
356
+
357
+ @app.command(name="config-check")
358
+ def config_check_command(
359
+ setup: bool = typer.Option(
360
+ False,
361
+ "--setup",
362
+ "--install",
363
+ "--fix",
364
+ help="Automatically configure prerequisites, install missing dependencies, set capabilities, and update databases",
365
+ ),
366
+ ) -> None:
367
+ """Check prerequisites, API keys, environment health, or run automated setup."""
368
+ print_banner()
369
+
370
+ from utils.setup import SetupManager
371
+ setup_mgr = SetupManager(console=console)
372
+
373
+ if setup:
374
+ setup_mgr.run_automated_setup()
375
+
376
+ # 1. System & Environment Health Diagnostics
377
+ print_section_header("System & Environment Diagnostics")
378
+ checks = setup_mgr.check_all()
379
+ setup_mgr.render_diagnostics_table(checks)
380
+
381
+ # 2. Live API Verification
382
+ print_section_header("API Credentials & Live Endpoint Verification")
383
+ engine = ThreatTrackEngine()
384
+ api_statuses = asyncio.run(engine.verify_environment_apis())
385
+
386
+ for mod_key, info in api_statuses.items():
387
+ name = info.get("name", mod_key.title())
388
+ status = info.get("status", "Unknown")
389
+ if info.get("valid"):
390
+ status_styled = f"[bold green]{status}[/bold green]"
391
+ elif info.get("configured"):
392
+ status_styled = f"[bold red]{status}[/bold red]"
393
+ else:
394
+ status_styled = f"[dim]{status}[/dim]"
395
+ console.print(f" • [cyan]{name}:[/cyan] {status_styled}")
396
+
397
+ console.print(f" • [cyan]HTTP Concurrency Limit:[/cyan] {settings.http_concurrency_limit}")
398
+ console.print(f" • [cyan]HTTP Timeout:[/cyan] {settings.http_timeout}s")
399
+ console.print(f" • [cyan]Shodan Rate Limit Delay:[/cyan] {getattr(settings, 'shodan_delay', 1.05)}s")
400
+
401
+ needs_setup = not all(c.get("ok", False) for k, c in checks.items() if k != "nuclei")
402
+ if needs_setup and not setup:
403
+ console.print(
404
+ "\n[bold yellow]💡 Tip:[/bold yellow] Run [bold cyan]./detecti-cli setup[/bold cyan] or [bold cyan]./detecti-cli config-check --setup[/bold cyan] to automatically configure missing prerequisites.\n"
405
+ )
406
+
407
+
408
+
409
+ print_section_header("API Credentials Status")
410
+ engine = ThreatTrackEngine()
411
+ api_statuses = asyncio.run(engine.verify_environment_apis())
412
+ for mod_key, info in api_statuses.items():
413
+ name = info.get("name", mod_key.title())
414
+ status = info.get("status", "Unknown")
415
+ status_styled = (
416
+ f"[bold green]{status}[/bold green]"
417
+ if info.get("valid")
418
+ else (f"[bold red]{status}[/bold red]" if info.get("configured") else f"[dim]{status}[/dim]")
419
+ )
420
+ console.print(f" • [cyan]{name}:[/cyan] {status_styled}")
421
+
422
+
423
+ @hound_app.command("start")
424
+ def start_server(
425
+ db: Optional[str] = typer.Option(
426
+ None,
427
+ "--db",
428
+ "-d",
429
+ help="Target SQLite database file inside ./data/dbs/ or full path (optional, can be selected via UI)",
430
+ ),
431
+ port: int = typer.Option(
432
+ 8000,
433
+ "--port",
434
+ "-p",
435
+ help="Port for the HTTP server",
436
+ ),
437
+ host: str = typer.Option(
438
+ "0.0.0.0",
439
+ "--host",
440
+ "-h",
441
+ help="Host binding address",
442
+ ),
443
+ ) -> None:
444
+ """Start the non-blocking EASM graph webserver in the background."""
445
+ print_banner()
446
+
447
+ try:
448
+ import fastapi
449
+ import uvicorn
450
+ import psutil
451
+ except ImportError as e:
452
+ print_error("Web server dependencies not installed. Run: pip install fastapi uvicorn psutil")
453
+ return
454
+
455
+ try:
456
+ from web.process_manager import WebServerManager
457
+
458
+ manager = WebServerManager()
459
+
460
+ # Check if server is already running
461
+ if manager.is_running():
462
+ status = manager.get_status()
463
+ if status:
464
+ print_warning(f"Web server is already running on {status['host']}:{status['port']}")
465
+ print_info(f"Database: {status['db_path']}")
466
+ print_info(f"PID: {status['pid']}")
467
+ return
468
+
469
+ print_info(f"Starting DetecTI-CLI web server on {host}:{port}...")
470
+ if db:
471
+ print_info(f"Database: {db}")
472
+ else:
473
+ print_info("Database: [italic cyan]Dynamic (selectable via Web UI)[/italic cyan]")
474
+
475
+ # Start the server
476
+ success = manager.start_server(db, host, port)
477
+
478
+ if success:
479
+ real_ip = get_real_ip()
480
+ print_success(f"✅ DetecTIHound web server started successfully!")
481
+ console.print(f" 👉 [bold cyan]Local Access:[/bold cyan] [bold underline cyan]http://localhost:{port}[/bold underline cyan]")
482
+ console.print(f" 👉 [bold cyan]Network Access:[/bold cyan] [bold underline cyan]http://{real_ip}:{port}[/bold underline cyan]")
483
+ if db:
484
+ print_info(f"📊 Initial Database: {db}")
485
+ else:
486
+ print_info(f"📊 Database: Dynamic selector active in Web UI")
487
+ print_info(f"🔧 Use '{cli_name} hound status' to check server status")
488
+ print_info(f"🛑 Use '{cli_name} hound stop' to stop the server")
489
+ else:
490
+ print_error("❌ Failed to start web server")
491
+ print_info("Check that the port is available and dependencies are installed")
492
+
493
+ except FileNotFoundError as e:
494
+ print_error(f"Database file not found: {e}")
495
+ except Exception as e:
496
+ print_error(f"Failed to start server: {e}")
497
+
498
+
499
+ @hound_app.command("status")
500
+ def server_status() -> None:
501
+ """Check the status of the background webserver."""
502
+ print_banner()
503
+
504
+ try:
505
+ import psutil
506
+ except ImportError:
507
+ print_error("Web server dependencies not installed. Run: pip install fastapi uvicorn psutil")
508
+ return
509
+
510
+ try:
511
+ from web.process_manager import WebServerManager
512
+ from rich.table import Table
513
+
514
+ manager = WebServerManager()
515
+ status = manager.get_status()
516
+
517
+ if status:
518
+ print_section_header("Web Server Status")
519
+
520
+ # Create status table
521
+ table = Table(show_header=True, header_style="bold cyan")
522
+ table.add_column("Property", style="bold white")
523
+ table.add_column("Value", style="green")
524
+
525
+ real_ip = get_real_ip()
526
+ table.add_row("Status", "🟢 RUNNING")
527
+ table.add_row("PID", str(status['pid']))
528
+ table.add_row("Local URL", f"http://localhost:{status['port']}")
529
+ table.add_row("Network URL", f"http://{real_ip}:{status['port']}")
530
+ table.add_row("Database", status['db_path'])
531
+ table.add_row("Started At", status.get('started_at', 'Unknown'))
532
+
533
+ if 'uptime_seconds' in status:
534
+ uptime = int(status['uptime_seconds'])
535
+ hours, remainder = divmod(uptime, 3600)
536
+ minutes, seconds = divmod(remainder, 60)
537
+ uptime_str = f"{hours:02d}:{minutes:02d}:{seconds:02d}"
538
+ table.add_row("Uptime", uptime_str)
539
+
540
+ if 'memory_mb' in status:
541
+ table.add_row("Memory Usage", f"{status['memory_mb']:.1f} MB")
542
+
543
+ console.print(table)
544
+ print_success(f"🌐 Access Dashboard: [bold underline cyan]http://localhost:{status['port']}[/bold underline cyan] | [bold underline cyan]http://{real_ip}:{status['port']}[/bold underline cyan]")
545
+ else:
546
+ print_warning("🔴 Web server is not running")
547
+ print_info(f"Use '{cli_name} hound start' to start the server")
548
+
549
+ except Exception as e:
550
+ print_error(f"Failed to check server status: {e}")
551
+
552
+
553
+ @hound_app.command("stop")
554
+ def stop_server() -> None:
555
+ """Stop the background webserver gracefully."""
556
+ print_banner()
557
+
558
+ try:
559
+ import psutil
560
+ except ImportError:
561
+ print_error("Web server dependencies not installed. Run: pip install psutil")
562
+ return
563
+
564
+ try:
565
+ from web.process_manager import WebServerManager
566
+
567
+ manager = WebServerManager()
568
+
569
+ if not manager.is_running():
570
+ print_warning("🔴 Web server is not running")
571
+ return
572
+
573
+ print_info("🛑 Stopping web server...")
574
+
575
+ if manager.stop_server():
576
+ print_success("✅ Web server stopped successfully")
577
+ else:
578
+ print_error("❌ Failed to stop web server")
579
+
580
+ except Exception as e:
581
+ print_error(f"Failed to stop server: {e}")
582
+
583
+
584
+ @hound_app.command("list-dbs")
585
+ def list_databases() -> None:
586
+ """List all available EASM target SQLite databases in ./data/dbs/."""
587
+ print_banner()
588
+
589
+ data_dir = DETECTI_HOME / "data" / "dbs"
590
+ if not data_dir.exists():
591
+ print_warning("No databases directory found. Run a scan with --persist to create databases.")
592
+ return
593
+
594
+ db_files = list(data_dir.glob("*.sqlite"))
595
+ if not db_files:
596
+ print_warning("No SQLite databases found in ./data/dbs/")
597
+ return
598
+
599
+ print_section_header(f"Available EASM Databases ({len(db_files)} found)")
600
+
601
+ from rich.table import Table
602
+ table = Table(show_header=True, header_style="bold cyan")
603
+ table.add_column("Database File", style="bold white")
604
+ table.add_column("Target", style="cyan")
605
+ table.add_column("Size", style="dim")
606
+ table.add_column("Modified", style="dim")
607
+
608
+ for db_file in sorted(db_files):
609
+ size_mb = db_file.stat().st_size / (1024 * 1024)
610
+ modified = db_file.stat().st_mtime
611
+ from datetime import datetime
612
+ mod_time = datetime.fromtimestamp(modified).strftime("%Y-%m-%d %H:%M")
613
+
614
+ # Try to get target from database
615
+ target = "Unknown"
616
+ try:
617
+ from core.database.storage import DatabaseManager
618
+ db_manager = DatabaseManager(db_file)
619
+ stats = db_manager.get_summary_stats()
620
+ if 'target' in stats:
621
+ target = stats['target']
622
+ except Exception:
623
+ pass
624
+
625
+ table.add_row(
626
+ db_file.name,
627
+ target,
628
+ f"{size_mb:.2f} MB",
629
+ mod_time
630
+ )
631
+
632
+ console.print(table)
633
+ print_info(f"Use '{cli_name} hound start' to start the web dashboard (select database in UI)")
634
+
635
+
636
+ @app.command(name="version")
637
+ def version_command() -> None:
638
+ """Show DetecTI-CLI version and maintainer information."""
639
+ console.print(f"[bold cyan]DetecTI-CLI[/bold cyan] version [bold white]{__version__}[/bold white] - Cyber Lead Intelligence Engine")
640
+ console.print("[dim]Developed by Lucas S. (Ls4ss) - https://lucassouza.io[/dim]")
641
+
642
+
643
+ def main() -> None:
644
+ """Main CLI entry point."""
645
+ app(prog_name="detecti-cli")
646
+
647
+
648
+ if __name__ == "__main__":
649
+ main()