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 +2 -0
- eidolon/__main__.py +4 -0
- eidolon/cli.py +563 -0
- eidolon/colors.py +39 -0
- eidolon/database.py +69 -0
- eidolon/loader.py +63 -0
- eidolon/repo.py +136 -0
- eidolon/utils.py +115 -0
- eidolon_core-3.1.2.dist-info/METADATA +23 -0
- eidolon_core-3.1.2.dist-info/RECORD +14 -0
- eidolon_core-3.1.2.dist-info/WHEEL +5 -0
- eidolon_core-3.1.2.dist-info/entry_points.txt +2 -0
- eidolon_core-3.1.2.dist-info/licenses/LICENSE +373 -0
- eidolon_core-3.1.2.dist-info/top_level.txt +1 -0
eidolon/__init__.py
ADDED
eidolon/__main__.py
ADDED
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
|