eidolon-core 3.1.2__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.
eidolon/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ __version__ = "3.1.3"
2
+ CODENAME = "SPECTRAL_CORE"
eidolon/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from eidolon.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
eidolon/cli.py ADDED
@@ -0,0 +1,563 @@
1
+ import os
2
+ import sys
3
+ import cmd
4
+ import copy
5
+ import time
6
+ import shlex
7
+ import traceback
8
+ import shutil
9
+ import glob
10
+
11
+ try:
12
+ import readline
13
+ readline.set_completer_delims(' \t\n')
14
+ readline.parse_and_bind("tab: complete")
15
+ except ImportError:
16
+ pass
17
+
18
+ from pathlib import Path
19
+
20
+ from eidolon.colors import C, rl_prompt_safe, c
21
+ from eidolon.utils import (
22
+ ok, err, info, warn, spectral, hr, print_banner, print_splash, print_table,
23
+ _uptime_str, _fmt_duration, _cvss_severity, _cvss_color, _module_vulns, _module_cvss
24
+ )
25
+ from eidolon.loader import (
26
+ iter_module_files, load_module_from_path, _module_type, _cleanup_legacy_scaffolds,
27
+ MODULE_TYPE_EXPLOIT, MODULE_TYPE_AUXILIARY, MODULE_TYPE_GENERIC
28
+ )
29
+ from eidolon.database import EidolonDB
30
+ from eidolon.repo import RepositoryManager
31
+
32
+ WORKSPACE_DIR = os.path.join(os.path.expanduser("~"), ".eidolon")
33
+ MODULES_DIR = os.path.join(WORKSPACE_DIR, "modules")
34
+ DB_PATH = os.path.join(WORKSPACE_DIR, "eidolon.db")
35
+
36
+ _TYPE_COLOR = {
37
+ MODULE_TYPE_AUXILIARY: "blue",
38
+ MODULE_TYPE_GENERIC: "gray",
39
+ MODULE_TYPE_EXPLOIT: "red",
40
+ }
41
+
42
+ def _type_label(mod) -> str:
43
+ t = _module_type(mod)
44
+ return c(t, _TYPE_COLOR.get(t, "white"))
45
+
46
+ class Eidolon(cmd.Cmd):
47
+ intro = ""
48
+ use_rawinput = True
49
+
50
+ def __init__(self):
51
+ super().__init__()
52
+ os.makedirs(MODULES_DIR, exist_ok=True)
53
+ self.db = EidolonDB(DB_PATH)
54
+ self.repo_mgr = RepositoryManager()
55
+ self.globals = self.db.get_globals()
56
+ self.modules = {}
57
+ self._module_mtimes = {}
58
+ self.current = None
59
+ self.current_name = None
60
+ self.current_options = None
61
+
62
+ self.session_t0 = time.time()
63
+ self.operator_name = "root"
64
+ self.operator_team = "eidolon"
65
+ self._subshell_active = False
66
+ self._subshell_handler = None
67
+ self._subshell_completer = None
68
+ self._subshell_prompt_fn = None
69
+ self._subshell_on_exit = None
70
+ self._subshell_session_id = None
71
+ self._update_prompt()
72
+
73
+ def preloop(self):
74
+ print_splash()
75
+ _cleanup_legacy_scaffolds(MODULES_DIR)
76
+ self.load_modules()
77
+ print()
78
+ spectral(f"welcome back, {C.BOLD}{self.operator_name}{C.RESET}")
79
+ spectral(f"workspace : {C.CYAN}{WORKSPACE_DIR}{C.RESET}")
80
+ spectral(f"modules : {C.GREEN}{len(self.modules)}{C.RESET} loaded")
81
+ spectral(f"type {C.CYAN}help{C.RESET} for commands.\n")
82
+
83
+ def postcmd(self, stop, line):
84
+ self._update_prompt()
85
+ return stop
86
+
87
+ def emptyline(self): return
88
+
89
+ def default(self, line):
90
+ token = line.split()[0] if line.strip() else ""
91
+ err(f"unknown command: {C.BOLD}{token}{C.RESET}")
92
+
93
+ def _update_prompt(self):
94
+ if self._subshell_active and callable(self._subshell_prompt_fn):
95
+ try:
96
+ self.prompt = rl_prompt_safe(self._subshell_prompt_fn())
97
+ return
98
+ except Exception: pass
99
+
100
+ name_part = f"{C.CYAN}{self.operator_name}{C.GRAY}@{C.CYAN}{self.operator_team}{C.RESET}"
101
+ ctx = f"{C.PINK}{self.current_name}{C.RESET}" if self.current_name else f"{C.GRAY}idle{C.RESET}"
102
+ up = _uptime_str(self.session_t0)
103
+
104
+ line1 = f"{C.PURPLE}╭─({name_part}{C.PURPLE})─[{ctx}{C.PURPLE}]─{C.SHADOW}[{up}]{C.PURPLE}"
105
+ line2 = f"{C.PURPLE}╰─{C.PINK}❯{C.RESET} "
106
+ self.prompt = f"\n{rl_prompt_safe(line1)}\n{rl_prompt_safe(line2)}"
107
+
108
+ def load_modules(self):
109
+ self.modules = {}
110
+ self._module_mtimes = {}
111
+ t0 = time.time()
112
+ failed = 0
113
+ seen_names = set()
114
+
115
+ for path, rel in iter_module_files(MODULES_DIR):
116
+ name = rel[:-3]
117
+ if name in seen_names:
118
+ warn(f"duplicate module '{name}' — skipped")
119
+ failed += 1
120
+ continue
121
+ seen_names.add(name)
122
+
123
+ mod, error = load_module_from_path(path, self.globals, self)
124
+ if error:
125
+ failed += 1
126
+ err(f"failed to load '{name}': {error}")
127
+ continue
128
+
129
+ if not hasattr(mod, "run"):
130
+ warn(f"module '{name}' has no run(opts) — skipped")
131
+ failed += 1
132
+ continue
133
+ if not hasattr(mod, "NAME"): mod.NAME = name
134
+
135
+ self.modules[name] = mod
136
+ try: self._module_mtimes[name] = os.path.getmtime(path)
137
+ except OSError: pass
138
+
139
+ if hasattr(mod, "on_load"):
140
+ try: mod.on_load()
141
+ except Exception as e: warn(f"{name}.on_load() failed: {e}")
142
+
143
+ dt = (time.time() - t0) * 1000
144
+ tail = f" · {failed} failed" if failed else ""
145
+ spectral(f"{len(self.modules)} module(s) loaded in {dt:.1f} ms{tail}")
146
+
147
+ def _resolve(self, name):
148
+ name = name.strip()
149
+ if name in self.modules: return name
150
+ matches = [k for k in self.modules if name in k]
151
+ return matches[0] if len(matches) == 1 else None
152
+
153
+ def _auto_session_target(self):
154
+ opts = self.current_options or {}
155
+ for key in ("target", "rhost", "rhosts", "host", "hostname", "url", "uri"):
156
+ if key in opts:
157
+ v = opts[key].get("value")
158
+ if v not in (None, "", False): return str(v)
159
+ for k, v in opts.items():
160
+ val = v.get("value")
161
+ if val not in (None, "", False): return str(val)
162
+ return ""
163
+
164
+ def enter_subshell(self, handler, completer=None, prompt_fn=None, on_exit=None):
165
+ self._subshell_active = True
166
+ self._subshell_handler = handler
167
+ self._subshell_completer = completer
168
+ self._subshell_prompt_fn = prompt_fn
169
+ self._subshell_on_exit = on_exit
170
+
171
+ if (self._subshell_session_id is None and self.current is not None
172
+ and _module_type(self.current) == MODULE_TYPE_EXPLOIT):
173
+ sid = self.db.add_session(
174
+ self.current_name, self._auto_session_target(),
175
+ _module_vulns(self.current), _module_cvss(self.current)
176
+ )
177
+ self._subshell_session_id = sid
178
+ spectral(f"session {C.PINK}#{sid}{C.RESET} opened")
179
+ self._update_prompt()
180
+
181
+ def exit_subshell(self):
182
+ on_exit = self._subshell_on_exit
183
+ if self._subshell_session_id is not None:
184
+ self.db.close_session(self._subshell_session_id)
185
+ self._subshell_session_id = None
186
+ self._subshell_active = False
187
+ if callable(on_exit):
188
+ try: on_exit()
189
+ except Exception as e: warn(f"subshell cleanup: {e}")
190
+ self._update_prompt()
191
+
192
+ def onecmd(self, line):
193
+ if self._subshell_active:
194
+ token = line.strip().split(maxsplit=1)[0] if line.strip() else ""
195
+ if token in ("back", "exit", "quit", "EOF"): return super().onecmd(line)
196
+ if callable(self._subshell_handler):
197
+ try: self._subshell_handler(line)
198
+ except Exception as e: err(f"subshell error: {e}")
199
+ return False
200
+ return super().onecmd(line)
201
+
202
+ # --- REPO COMMANDS ---
203
+ def do_repo(self, arg):
204
+ """repo [list|add <url>|remove <url>|sync|install <name>]"""
205
+ args = arg.strip().split()
206
+ if not args:
207
+ return self.do_repo("list")
208
+
209
+ cmd = args[0].lower()
210
+
211
+ if cmd == "list":
212
+ remotes = self.repo_mgr.remotes
213
+ if not remotes:
214
+ return warn("No remotes configured.")
215
+ rows = [[str(i+1), url] for i, url in enumerate(remotes)]
216
+ print_table(["#", "Remote URL"], rows, title="CONFIGURED REMOTES")
217
+
218
+ elif cmd == "add" and len(args) > 1:
219
+ url = args[1]
220
+ if self.repo_mgr.add_remote(url):
221
+ ok(f"Added remote: {url}")
222
+ else:
223
+ warn("Remote already exists.")
224
+
225
+ elif cmd == "remove" and len(args) > 1:
226
+ url = args[1]
227
+ if self.repo_mgr.remove_remote(url):
228
+ ok(f"Removed remote: {url}")
229
+ else:
230
+ warn("Remote not found.")
231
+
232
+ elif cmd == "sync":
233
+ spectral("Syncing all remotes...")
234
+ modules = self.repo_mgr.get_all_modules(force_refresh=True)
235
+ ok(f"Synced {len(modules)} modules from {len(self.repo_mgr.remotes)} remotes.")
236
+
237
+ elif cmd == "install" and len(args) > 1:
238
+ mod_name = args[1]
239
+ spectral(f"Searching for module '{mod_name}'...")
240
+ modules = self.repo_mgr.get_all_modules()
241
+ if mod_name not in modules:
242
+ return err(f"Module '{mod_name}' not found in any remote.")
243
+
244
+ mod_info = modules[mod_name]
245
+ spectral(f"Installing '{mod_name}' from {mod_info['_repo_url']}...")
246
+ if self.repo_mgr.install_module(mod_info):
247
+ ok(f"Module '{mod_name}' installed successfully.")
248
+ self.load_modules()
249
+ else:
250
+ err("Installation failed.")
251
+ else:
252
+ err("usage: repo [list|add <url>|remove <url>|sync|install <name>]")
253
+
254
+ def do_add_module(self, arg):
255
+ """add_module <path> — Install a new module from a local file"""
256
+ path = arg.strip()
257
+ if not path:
258
+ return err("usage: add_module <path_to_python_file>")
259
+
260
+ if (path.startswith('"') and path.endswith('"')) or (path.startswith("'") and path.endswith("'")):
261
+ path = path[1:-1]
262
+
263
+ src_path = Path(os.path.expanduser(path))
264
+ if not src_path.is_absolute():
265
+ src_path = Path.cwd() / src_path
266
+
267
+ if not src_path.exists() or not src_path.is_file():
268
+ return err(f"file not found: {src_path}")
269
+
270
+ if not str(src_path).endswith(".py"):
271
+ return err("only .py files are allowed.")
272
+
273
+ dest_path = Path(MODULES_DIR) / src_path.name
274
+ try:
275
+ shutil.copy2(src_path, dest_path)
276
+ ok(f"module copied to workspace: {dest_path.name}")
277
+ self.load_modules()
278
+ except Exception as e:
279
+ err(f"failed to copy: {e}")
280
+
281
+ def do_help(self, arg):
282
+ """help [cmd] — show help"""
283
+ arg = arg.strip()
284
+ if arg:
285
+ meth = getattr(self, f"do_{arg}", None)
286
+ if not meth: return err(f"no such command: {arg}")
287
+ print(f"\n {C.PINK}{arg}{C.RESET} — {(meth.__doc__ or '').strip().splitlines()[0]}\n")
288
+ return
289
+
290
+ print(f"\n{C.PURPLE}╭─────────────────────────────────────────╮{C.RESET}")
291
+ print(f"{C.PURPLE}│{C.RESET} {C.PINK}EIDOLON{C.RESET} {C.GRAY}·{C.RESET} {C.CYAN}SPECTRAL CORE{C.RESET} {C.GRAY}·{C.RESET} reference")
292
+ print(f"{C.PURPLE}╰─────────────────────────────────────────╯{C.RESET}")
293
+ print(f" {C.CYAN}help{C.RESET} / {C.CYAN}clear{C.RESET} / {C.CYAN}banner{C.RESET} / {C.CYAN}reload{C.RESET} / {C.CYAN}exit{C.RESET}")
294
+ print(f" {C.CYAN}modules{C.RESET} / {C.CYAN}search{C.RESET} <kw> / {C.CYAN}info{C.RESET} <name>")
295
+ print(f" {C.CYAN}use{C.RESET} <name> / {C.CYAN}back{C.RESET} / {C.CYAN}options{C.RESET} / {C.CYAN}set{C.RESET} <k> <v> / {C.CYAN}run{C.RESET}")
296
+ print(f" {C.CYAN}repo{C.RESET} <list|add|remove|sync|install>")
297
+ print(f" {C.CYAN}sessions{C.RESET} / {C.CYAN}add_module{C.RESET} <path>")
298
+ print(f" {C.CYAN}setg{C.RESET} <k> <v> / {C.CYAN}globals{C.RESET}\n")
299
+
300
+ def do_banner(self, arg): print_banner(animate=True)
301
+ def do_clear(self, arg): os.system("cls" if os.name == "nt" else "clear")
302
+
303
+ def do_reload(self, arg):
304
+ """reload — rescan workspace modules"""
305
+ if self._subshell_active: self.exit_subshell()
306
+ spectral("reloading module arsenal ...")
307
+ self.current = self.current_name = self.current_options = None
308
+ self.load_modules()
309
+ ok("arsenal reloaded")
310
+
311
+ def do_status(self, arg):
312
+ """status — show operator status"""
313
+ sessions = self.db.get_sessions()
314
+ active = sum(1 for s in sessions if s["status"] == "active")
315
+ print(f"\n {C.PURPLE}╭─ OPERATOR STATUS ──────────────────╮{C.RESET}")
316
+ print(f" {C.PURPLE}│{C.RESET} name : {C.BOLD}{self.operator_name}{C.RESET}")
317
+ print(f" {C.PURPLE}│{C.RESET} team : {C.CYAN}{self.operator_team}{C.RESET}")
318
+ print(f" {C.PURPLE}│{C.RESET} uptime : {_uptime_str(self.session_t0)}")
319
+ print(f" {C.PURPLE}│{C.RESET} workspace : {C.GREEN}{WORKSPACE_DIR}{C.RESET}")
320
+ print(f" {C.PURPLE}│{C.RESET} sessions : {C.GREEN}{active}{C.RESET} active")
321
+ print(f" {C.PURPLE}╰────────────────────────────────────╯\n")
322
+
323
+ def do_whoami(self, arg): self.do_status("")
324
+
325
+ def do_operator(self, arg):
326
+ parts = arg.strip().split(maxsplit=1)
327
+ if not parts: return print(f"operator: {self.operator_name} @ {self.operator_team}")
328
+ field, value = parts[0].lower(), (parts[1].strip() if len(parts) > 1 else "")
329
+ if field == "name" and value: self.operator_name = value; ok("name updated")
330
+ elif field == "team" and value: self.operator_team = value; ok("team updated")
331
+ else: err("usage: operator name|team <val>")
332
+
333
+ def do_exit(self, arg):
334
+ """exit / quit — leave tool"""
335
+ if self._subshell_active:
336
+ self.exit_subshell(); ok("returned to Eidolon"); return False
337
+ print(f"\n {C.PURPLE}╰─{C.RESET} {C.CYAN}eidolon out.{C.RESET} {C.GRAY}uptime: {_uptime_str(self.session_t0)}{C.RESET}\n")
338
+ return True
339
+
340
+ do_quit = do_exit
341
+ def do_EOF(self, arg): print(); return self.do_exit(arg)
342
+
343
+ def do_modules(self, arg):
344
+ if not self.modules: return warn("no modules loaded.")
345
+ rows = []
346
+ for i, (name, m) in enumerate(sorted(self.modules.items()), 1):
347
+ rows.append([f"{C.PURPLE}{i:02d}{C.RESET}", f"{C.CYAN}{name}{C.RESET}", _type_label(m), getattr(m, "CATEGORY", "-"), str(getattr(m, "DESCRIPTION", "-"))[:36]])
348
+ print_table(["#", "Arsenal", "Type", "Category", "Description"], rows, title=f"EIDOLON ARSENAL ({len(self.modules)})")
349
+ do_list = do_ls = do_arsenal = do_modules
350
+
351
+ def do_search(self, arg):
352
+ kw = arg.strip().lower()
353
+ if not kw: return err("usage: search <keyword>")
354
+ hits = [name for name, m in self.modules.items() if kw in (name + str(getattr(m, "DESCRIPTION", ""))).lower()]
355
+ if not hits: return warn("no matches.")
356
+ for h in sorted(hits): print(f" {C.PINK}▸{C.RESET} {h}")
357
+
358
+ def do_info(self, arg):
359
+ name = self._resolve(arg)
360
+ if not name: return err(f"not found: '{arg.strip()}'")
361
+ m = self.modules[name]
362
+ print(f"\n {C.PURPLE}╭─ MODULE INTEL ─────────────────────╮{C.RESET}")
363
+ print(f" {C.PURPLE}│{C.RESET} name: {c(name, C.WHITE)}")
364
+ print(f" {C.PURPLE}│{C.RESET} type: {_type_label(m)} | cat: {getattr(m,'CATEGORY','-')}")
365
+ print(f" {C.PURPLE}│{C.RESET} desc: {getattr(m,'DESCRIPTION','-')}")
366
+ print(f" {C.PURPLE}│{C.RESET} file: {getattr(m,'__eidolon_path__','?')}")
367
+ print(f" {C.PURPLE}╰────────────────────────────────────╯")
368
+ opts = getattr(m, "OPTIONS", {})
369
+ if opts:
370
+ rows = [[k, str(v.get("value", "")), "yes" if v.get("required") else "no", v.get("desc", "")] for k, v in opts.items()]
371
+ print_table(["Name", "Default", "Req", "Desc"], rows, title="OPTIONS")
372
+ print()
373
+
374
+ def do_use(self, arg):
375
+ name = self._resolve(arg)
376
+ if not name: return err(f"module '{arg.strip()}' not found.")
377
+ mod = self.modules[name]
378
+ self.current = mod
379
+ self.current_name = name
380
+ self.current_options = copy.deepcopy(getattr(mod, "OPTIONS", {}))
381
+ ok(f"selected: {c(name, C.PINK)}")
382
+ self.do_options("")
383
+
384
+ def do_back(self, arg):
385
+ if self._subshell_active: self.exit_subshell()
386
+ if not self.current: return warn("no module selected.")
387
+ spectral(f"left {self.current_name}")
388
+ self.current = self.current_name = self.current_options = None
389
+ do_vanish = do_back
390
+
391
+ def do_options(self, arg):
392
+ if not self.current: return err("no module selected.")
393
+ opts = self.current_options or {}
394
+ if not opts: return info("no options.")
395
+ rows = [[k, str(v.get("value", "")), "yes" if v.get("required") else "no", v.get("desc", "")] for k, v in opts.items()]
396
+ print_table(["Name", "Value", "Req", "Desc"], rows, title=f"OPTIONS · {self.current_name}")
397
+
398
+ def do_show(self, arg):
399
+ a = arg.strip().lower()
400
+ if a in ("options", "opt", "o", ""): self.do_options("")
401
+ elif a in ("modules", "m"): self.do_modules("")
402
+ else: err("usage: show [options|modules]")
403
+
404
+ @staticmethod
405
+ def _cast(value, reference):
406
+ s = value.strip()
407
+ if isinstance(reference, bool): return s.lower() in ("1", "true", "yes", "on", "y")
408
+ if isinstance(reference, int) and not isinstance(reference, bool):
409
+ try: return int(s)
410
+ except ValueError: return s
411
+ if isinstance(reference, float):
412
+ try: return float(s)
413
+ except ValueError: return s
414
+ return s
415
+
416
+ def do_set(self, arg):
417
+ if not self.current: return err("no module selected.")
418
+ try: parts = shlex.split(arg)
419
+ except ValueError as e: return err(f"parse error: {e}")
420
+ if len(parts) < 2: return err("usage: set <name> <value>")
421
+ key, val = parts[0], " ".join(parts[1:])
422
+ if key not in self.current_options: return err(f"unknown option: {key}")
423
+ old = self.current_options[key].get("value")
424
+ self.current_options[key]["value"] = self._cast(val, old)
425
+ ok(f"{C.CYAN}{key}{C.RESET} → {self.current_options[key]['value']!r}")
426
+
427
+ def do_unset(self, arg):
428
+ key = arg.strip()
429
+ if not self.current or key not in (self.current_options or {}): return err(f"unknown: {key}")
430
+ default = getattr(self.current, "OPTIONS", {}).get(key, {}).get("value", "")
431
+ self.current_options[key]["value"] = copy.deepcopy(default)
432
+ spectral(f"{key} reset to default")
433
+
434
+ def do_run(self, arg):
435
+ if not self.current: return err("no module selected.")
436
+ missing = [k for k, v in (self.current_options or {}).items() if v.get("required") and v.get("value") in (None, "", [], {})]
437
+ if missing: return err(f"missing required: {', '.join(missing)}")
438
+
439
+ values = {k: v.get("value") for k, v in self.current_options.items()}
440
+ hr(C.PURPLE)
441
+ print(f" {C.PINK}◈{C.RESET} {C.BOLD}EXECUTING OPERATION{C.RESET}")
442
+ print(f" {C.SHADOW}├─{C.RESET} module : {C.PINK}{self.current_name}{C.RESET}")
443
+ hr(C.PURPLE)
444
+
445
+ try: self.current.run(values)
446
+ except KeyboardInterrupt: warn("interrupted.")
447
+ except Exception as e:
448
+ err(f"module error: {e}")
449
+ if os.environ.get("EIDOLON_DEBUG"): traceback.print_exc()
450
+
451
+ if not self._subshell_active:
452
+ hr(C.PURPLE); ok("operation complete")
453
+
454
+ do_exploit = do_run
455
+ do_exec = do_run
456
+ do_haunt = do_run
457
+
458
+ def do_sessions(self, arg):
459
+ arg = arg.strip()
460
+ if arg:
461
+ try: sid = int(arg.lstrip("#"))
462
+ except ValueError: return err(f"invalid id: {arg!r}")
463
+ s = self.db.get_session(sid)
464
+ if not s: return err(f"session #{sid} not found")
465
+ print(f"\n {C.PURPLE}╭─ SESSION #{sid} ─────────────────────╮{C.RESET}")
466
+ print(f" {C.PURPLE}│{C.RESET} module : {s['module']}")
467
+ print(f" {C.PURPLE}│{C.RESET} target : {s['target']}")
468
+ print(f" {C.PURPLE}│{C.RESET} status : {s['status']}")
469
+ print(f" {C.PURPLE}╰──────────────────────────────────────╯\n")
470
+ return
471
+
472
+ sessions = self.db.get_sessions()
473
+ if not sessions: return info("no sessions recorded.")
474
+ rows = []
475
+ for s in sessions:
476
+ status = f"{C.GREEN}● active{C.RESET}" if s["status"] == "active" else f"{C.GRAY}○ closed{C.RESET}"
477
+ end = s.get("closed_at") or time.time()
478
+ dur = _fmt_duration(end - s.get("started_at", end))
479
+ rows.append([f"{C.PINK}#{s['id']}{C.RESET}", s["module"], s.get("target","-"), status, dur])
480
+ print_table(["ID", "Module", "Target", "Status", "Duration"], rows, title="SESSIONS")
481
+
482
+ def do_setg(self, arg):
483
+ try: parts = shlex.split(arg)
484
+ except ValueError as e: return err(f"parse error: {e}")
485
+ if len(parts) < 2: return err("usage: setg <name> <value>")
486
+ key, val = parts[0], " ".join(parts[1:])
487
+ self.globals[key] = val
488
+ self.db.set_global(key, val)
489
+ ok(f"global {C.CYAN}{key}{C.RESET} → {val!r} (saved to DB)")
490
+
491
+ def do_globals(self, arg):
492
+ if not self.globals: return info("no globals set.")
493
+ rows = [[k, v] for k, v in self.globals.items()]
494
+ print_table(["Name", "Value"], rows, title="GLOBAL VARIABLES")
495
+
496
+ # --- TAB COMPLETION ---
497
+
498
+ def complete_add_module(self, text, line, begidx, endidx):
499
+ clean_text = text.strip("'\"")
500
+ expanded_text = os.path.expanduser(clean_text)
501
+ matches = glob.glob(expanded_text + '*')
502
+
503
+ results = []
504
+ for match in matches:
505
+ is_dir = os.path.isdir(match)
506
+ if is_dir or match.endswith('.py'):
507
+ display_match = match
508
+ if clean_text.startswith('~'):
509
+ home = os.path.expanduser('~')
510
+ if display_match.startswith(home):
511
+ display_match = '~' + display_match[len(home):]
512
+ if is_dir and not display_match.endswith('/'):
513
+ display_match += '/'
514
+ if text.startswith("'") and not display_match.startswith("'"):
515
+ display_match = "'" + display_match + "'"
516
+ elif text.startswith('"') and not display_match.startswith('"'):
517
+ display_match = '"' + display_match + '"'
518
+ results.append(display_match)
519
+ return results
520
+
521
+ def complete_repo(self, text, line, begidx, endidx):
522
+ parts = line.split()
523
+ if line.endswith(' '):
524
+ parts.append('')
525
+
526
+ if len(parts) == 2:
527
+ # "repo " -> parts=['repo', '']
528
+ cmds = ["list", "add", "remove", "sync", "install"]
529
+ return [c for c in cmds if c.startswith(text)]
530
+ elif len(parts) == 3 and parts[1] == 'install':
531
+ # "repo install " -> parts=['repo', 'install', '']
532
+ try:
533
+ modules = self.repo_mgr.get_all_modules()
534
+ return [m for m in modules if m.startswith(text)]
535
+ except Exception:
536
+ return []
537
+ return []
538
+
539
+ def complete_use(self, text, *_): return [m for m in self.modules if m.startswith(text)]
540
+ def complete_info(self, text, *_): return self.complete_use(text, *_)
541
+ def complete_set(self, text, *_): return [k for k in (self.current_options or {}) if k.startswith(text)]
542
+ def complete_unset(self, text, *_): return self.complete_set(text, *_)
543
+ def complete_operator(self, text, *_): return [x for x in ["name", "team"] if x.startswith(text)]
544
+ def complete_help(self, text, *_):
545
+ return [x for x in ["help","banner","clear","reload","exit","modules","search","info","use","back","options","set","unset","run","sessions","add_module","setg","globals","status","whoami","operator", "repo"] if x.startswith(text)]
546
+
547
+ def completedefault(self, text, line, begidx, endidx):
548
+ if self._subshell_active and callable(self._subshell_completer):
549
+ try: return self._subshell_completer(text, line, begidx, endidx)
550
+ except Exception: return []
551
+ return []
552
+
553
+ def main():
554
+ if "--no-banner" not in sys.argv: print_banner(animate=True)
555
+ else: print(f" {C.PINK}Eidolon{C.RESET} v3.1.2\n")
556
+
557
+ shell = Eidolon()
558
+ try: shell.cmdloop()
559
+ except KeyboardInterrupt: print(); shell.do_exit("")
560
+ except EOFError: shell.do_exit("")
561
+
562
+ if __name__ == "__main__":
563
+ main()
eidolon/colors.py ADDED
@@ -0,0 +1,39 @@
1
+ import re
2
+
3
+ _ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]")
4
+
5
+ def rl_prompt_safe(s: str) -> str:
6
+ return _ANSI_RE.sub(lambda m: f"\001{m.group(0)}\002", s)
7
+
8
+ class C:
9
+ RESET = "\033[0m"
10
+ BOLD = "\033[1m"
11
+ DIM = "\033[2m"
12
+
13
+ RED = "\033[38;2;255;60;100m"
14
+ CRIMSON = "\033[38;2;200;30;60m"
15
+ GREEN = "\033[38;2;0;255;140m"
16
+ LIME = "\033[38;2;140;255;80m"
17
+ YELLOW = "\033[38;2;255;210;60m"
18
+ GOLD = "\033[38;2;220;170;30m"
19
+ CYAN = "\033[38;2;0;240;255m"
20
+ ELECTRIC = "\033[38;2;100;200;255m"
21
+ BLUE = "\033[38;2;80;130;255m"
22
+ PURPLE = "\033[38;2;170;90;255m"
23
+ VIOLET = "\033[38;2;130;60;220m"
24
+ PINK = "\033[38;2;255;90;200m"
25
+ MAGENTA = "\033[38;2;230;60;180m"
26
+ GRAY = "\033[38;2;120;130;150m"
27
+ SILVER = "\033[38;2;180;190;210m"
28
+ WHITE = "\033[38;2;240;245;250m"
29
+ SPECTRAL = "\033[38;2;200;210;240m"
30
+ SHADOW = "\033[38;2;60;65;90m"
31
+
32
+ _COLOR_MAP = {
33
+ "red": C.RED, "crimson": C.CRIMSON, "yellow": C.YELLOW, "lime": C.LIME,
34
+ "gray": C.GRAY, "cyan": C.CYAN, "purple": C.PURPLE, "pink": C.PINK,
35
+ "green": C.GREEN, "blue": C.BLUE,
36
+ }
37
+
38
+ def c(text, color_name):
39
+ return f"{_COLOR_MAP.get(color_name, C.WHITE)}{text}{C.RESET}"
eidolon/database.py ADDED
@@ -0,0 +1,69 @@
1
+ import sqlite3
2
+ import os
3
+ import json
4
+ import time
5
+
6
+ class EidolonDB:
7
+ def __init__(self, db_path):
8
+ self.db_path = db_path
9
+ self.conn = sqlite3.connect(self.db_path, check_same_thread=False)
10
+ self.conn.row_factory = sqlite3.Row
11
+ self._init_db()
12
+
13
+ def _init_db(self):
14
+ with self.conn:
15
+ self.conn.execute("""
16
+ CREATE TABLE IF NOT EXISTS globals (
17
+ key TEXT PRIMARY KEY,
18
+ value TEXT
19
+ )
20
+ """)
21
+ self.conn.execute("""
22
+ CREATE TABLE IF NOT EXISTS sessions (
23
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
24
+ module TEXT,
25
+ target TEXT,
26
+ vulns TEXT,
27
+ cvss REAL,
28
+ started_at REAL,
29
+ closed_at REAL,
30
+ status TEXT
31
+ )
32
+ """)
33
+
34
+ def get_globals(self):
35
+ cursor = self.conn.execute("SELECT key, value FROM globals")
36
+ res = {}
37
+ for row in cursor:
38
+ try: res[row['key']] = json.loads(row['value'])
39
+ except: res[row['key']] = row['value']
40
+ return res
41
+
42
+ def set_global(self, key, value):
43
+ val_str = json.dumps(value)
44
+ with self.conn:
45
+ self.conn.execute("INSERT OR REPLACE INTO globals (key, value) VALUES (?, ?)", (key, val_str))
46
+
47
+ def add_session(self, module, target, vulns, cvss):
48
+ with self.conn:
49
+ cur = self.conn.execute(
50
+ "INSERT INTO sessions (module, target, vulns, cvss, started_at, status) VALUES (?, ?, ?, ?, ?, ?)",
51
+ (module, target, json.dumps(vulns), cvss, time.time(), "active")
52
+ )
53
+ return cur.lastrowid
54
+
55
+ def close_session(self, session_id):
56
+ with self.conn:
57
+ self.conn.execute(
58
+ "UPDATE sessions SET closed_at = ?, status = ? WHERE id = ?",
59
+ (time.time(), "closed", session_id)
60
+ )
61
+
62
+ def get_sessions(self):
63
+ cursor = self.conn.execute("SELECT * FROM sessions")
64
+ return [dict(row) for row in cursor]
65
+
66
+ def get_session(self, session_id):
67
+ cursor = self.conn.execute("SELECT * FROM sessions WHERE id = ?", (session_id,))
68
+ row = cursor.fetchone()
69
+ return dict(row) if row else None