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/__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()
|