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
@@ -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
@@ -0,0 +1 @@
1
+ # Static CSS files for DetecTI-CLI dashboard