exploitgraph 1.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 (42) hide show
  1. core/__init__.py +0 -0
  2. core/attack_graph.py +83 -0
  3. core/aws_client.py +284 -0
  4. core/config.py +83 -0
  5. core/console.py +469 -0
  6. core/context_engine.py +172 -0
  7. core/correlator.py +476 -0
  8. core/http_client.py +243 -0
  9. core/logger.py +97 -0
  10. core/module_loader.py +69 -0
  11. core/risk_engine.py +47 -0
  12. core/session_manager.py +254 -0
  13. exploitgraph-1.0.0.dist-info/METADATA +429 -0
  14. exploitgraph-1.0.0.dist-info/RECORD +42 -0
  15. exploitgraph-1.0.0.dist-info/WHEEL +5 -0
  16. exploitgraph-1.0.0.dist-info/entry_points.txt +2 -0
  17. exploitgraph-1.0.0.dist-info/licenses/LICENSE +21 -0
  18. exploitgraph-1.0.0.dist-info/top_level.txt +2 -0
  19. modules/__init__.py +0 -0
  20. modules/base.py +82 -0
  21. modules/cloud/__init__.py +0 -0
  22. modules/cloud/aws_credential_validator.py +340 -0
  23. modules/cloud/azure_enum.py +289 -0
  24. modules/cloud/cloudtrail_analyzer.py +494 -0
  25. modules/cloud/gcp_enum.py +272 -0
  26. modules/cloud/iam_enum.py +321 -0
  27. modules/cloud/iam_privilege_escalation.py +515 -0
  28. modules/cloud/metadata_check.py +315 -0
  29. modules/cloud/s3_enum.py +469 -0
  30. modules/discovery/__init__.py +0 -0
  31. modules/discovery/http_enum.py +235 -0
  32. modules/discovery/subdomain_enum.py +260 -0
  33. modules/exploitation/__init__.py +0 -0
  34. modules/exploitation/api_exploit.py +403 -0
  35. modules/exploitation/jwt_attack.py +346 -0
  36. modules/exploitation/ssrf_scanner.py +258 -0
  37. modules/reporting/__init__.py +0 -0
  38. modules/reporting/html_report.py +446 -0
  39. modules/reporting/json_export.py +107 -0
  40. modules/secrets/__init__.py +0 -0
  41. modules/secrets/file_secrets.py +358 -0
  42. modules/secrets/git_secrets.py +267 -0
core/console.py ADDED
@@ -0,0 +1,469 @@
1
+ """ExploitGraph - msfconsole-style interactive shell with tab completion."""
2
+ from __future__ import annotations
3
+ import os, sys, cmd, readline
4
+ from colorama import Fore, Style, init
5
+ init(autoreset=True)
6
+
7
+ from core.logger import log
8
+ from core.module_loader import loader
9
+ from core.session_manager import session_manager, Session
10
+ from core.risk_engine import risk_engine
11
+ from core.attack_graph import attack_graph
12
+
13
+
14
+ def print_banner():
15
+ print(Fore.RED + r"""
16
+ ███████╗██╗ ██╗██████╗ ██╗ ██████╗ ██╗████████╗ ██████╗ ██████╗ █████╗ ██████╗ ██╗ ██╗
17
+ ██╔════╝╚██╗██╔╝██╔══██╗██║ ██╔═══██╗██║╚══██╔══╝██╔════╝ ██╔══██╗██╔══██╗██╔══██╗██║ ██║
18
+ █████╗ ╚███╔╝ ██████╔╝██║ ██║ ██║██║ ██║ ██║ ███╗██████╔╝███████║██████╔╝███████║
19
+ ██╔══╝ ██╔██╗ ██╔═══╝ ██║ ██║ ██║██║ ██║ ██║ ██║██╔══██╗██╔══██║██╔═══╝ ██╔══██║
20
+ ███████╗██╔╝ ██╗██║ ███████╗╚██████╔╝██║ ██║ ╚██████╔╝██║ ██║██║ ██║██║ ██║ ██║
21
+ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝
22
+ """ + Style.RESET_ALL)
23
+ print(f" {Fore.WHITE}ExploitGraph {Fore.YELLOW}v1.0.0{Style.RESET_ALL}")
24
+ print(f" {Fore.CYAN}Automated Attack Path Discovery & Exploitation Framework{Style.RESET_ALL}")
25
+ print(f" {Fore.RED}Misconfig → Exposure → Secrets → API Abuse → Compromise{Style.RESET_ALL}")
26
+ print(f" {Fore.WHITE}github.com/prajwal-infosec/ExploitGraph | Authorized use only{Style.RESET_ALL}\n")
27
+
28
+
29
+ class _Completer:
30
+ CMDS = ["help","use","set","unset","options","run","back","exit","quit",
31
+ "show","info","sessions","workspace","search","export","clear"]
32
+
33
+ def __init__(self, console):
34
+ self.console = console
35
+
36
+ def complete(self, text, state):
37
+ try:
38
+ buf = readline.get_line_buffer().lstrip()
39
+ parts = buf.split()
40
+ first = parts[0] if parts else ""
41
+
42
+ if not parts or (len(parts)==1 and not buf.endswith(" ")):
43
+ matches = [c for c in self.CMDS if c.startswith(text)]
44
+ elif first == "use":
45
+ prefix = parts[1] if len(parts)>1 else ""
46
+ matches = [m for m in loader.list_names() if m.startswith(prefix if buf.endswith(" ") else text)]
47
+ elif first == "show":
48
+ subs = ["modules","options","findings","secrets","sessions","attack-path","exploits","summary"]
49
+ matches = [s for s in subs if s.startswith(text)]
50
+ elif first == "sessions":
51
+ matches = [s for s in ["-i","-k","-l"] if s.startswith(text)]
52
+ elif first == "workspace":
53
+ matches = [s for s in ["list","new","switch","delete"] if s.startswith(text)]
54
+ elif first == "export":
55
+ matches = [s for s in ["html","json","all"] if s.startswith(text)]
56
+ elif first == "set":
57
+ m = self.console._active_module
58
+ opts = list(m.OPTIONS.keys()) if m else ["TARGET","MODE"]
59
+ matches = [o for o in opts if o.startswith(text.upper())]
60
+ else:
61
+ matches = []
62
+
63
+ return matches[state] if state < len(matches) else None
64
+ except Exception:
65
+ return None
66
+
67
+
68
+ class ExploitGraphConsole(cmd.Cmd):
69
+ intro = ""
70
+ prompt = f"{Fore.RED}exploitgraph{Fore.WHITE}>{Style.RESET_ALL} "
71
+
72
+ def __init__(self):
73
+ super().__init__()
74
+ self._active_module = None
75
+ self._active_module_path = ""
76
+ self._mode = "offensive"
77
+
78
+ n = loader.discover()
79
+ for err in loader.load_errors():
80
+ log.warning(f"Module load error: {err}")
81
+
82
+ _c = _Completer(self)
83
+ readline.set_completer(_c.complete)
84
+ readline.parse_and_bind("tab: complete")
85
+ readline.set_completer_delims(" \t")
86
+
87
+ if not session_manager.active:
88
+ session_manager.new("http://127.0.0.1:5000", "default")
89
+
90
+ log.info(f"Loaded {n} modules | Session: {session_manager.active.session_id}")
91
+ log.info("Type 'help' for commands. Type 'run auto' to start full attack chain.\n")
92
+
93
+ @property
94
+ def session(self) -> Session:
95
+ return session_manager.active
96
+
97
+ def _update_prompt(self):
98
+ if self._active_module_path:
99
+ self.prompt = (f"{Fore.RED}exploitgraph{Fore.WHITE}"
100
+ f"({Fore.YELLOW}{self._active_module_path}{Fore.WHITE})"
101
+ f">{Style.RESET_ALL} ")
102
+ else:
103
+ self.prompt = f"{Fore.RED}exploitgraph{Fore.WHITE}>{Style.RESET_ALL} "
104
+
105
+ # ── Commands ──────────────────────────────────────────────────────────────
106
+
107
+ def do_use(self, args):
108
+ """Select a module. use discovery/http_enum"""
109
+ path = args.strip()
110
+ if not path: log.error("Usage: use <module_path>"); return
111
+ cls = loader.get(path)
112
+ if not cls: log.error(f"Not found: {path} (try: show modules)"); return
113
+ self._active_module = cls()
114
+ self._active_module_path = path
115
+ if "TARGET" in self._active_module.OPTIONS:
116
+ self._active_module.set_option("TARGET", self.session.target)
117
+ if "MODE" in self._active_module.OPTIONS:
118
+ self._active_module.set_option("MODE", self._mode)
119
+ self._update_prompt()
120
+ log.success(f"Using: {path}")
121
+ log.info(self._active_module.DESCRIPTION)
122
+ if self._active_module.MITRE:
123
+ log.info(f"MITRE: {', '.join(self._active_module.MITRE)}")
124
+ log.info("Type 'options' to configure. Type 'run' to execute.")
125
+
126
+ def do_set(self, args):
127
+ """Set option or session variable. set TARGET http://x.com"""
128
+ parts = args.strip().split(None, 1)
129
+ if len(parts) < 2: log.error("Usage: set OPTION value"); return
130
+ key, val = parts[0].upper(), parts[1]
131
+ if key == "TARGET":
132
+ self.session.target = val
133
+ log.success(f"TARGET => {val}")
134
+ if self._active_module and "TARGET" in self._active_module.OPTIONS:
135
+ self._active_module.set_option("TARGET", val)
136
+ return
137
+ if key == "MODE":
138
+ if val.lower() not in ("offensive","defensive"):
139
+ log.error("MODE must be offensive or defensive"); return
140
+ self._mode = val.lower()
141
+ log.success(f"MODE => {val.lower()}")
142
+ if self._active_module and "MODE" in self._active_module.OPTIONS:
143
+ self._active_module.set_option("MODE", val)
144
+ return
145
+ if not self._active_module: log.error("No module selected. use <module>"); return
146
+ ok, msg = self._active_module.set_option(key, val)
147
+ (log.success if ok else log.error)(msg)
148
+
149
+ def do_unset(self, args):
150
+ """Reset an option to its default. unset OPTION"""
151
+ key = args.strip().upper()
152
+ if self._active_module and key in self._active_module.OPTIONS:
153
+ self._active_module.set_option(key, self._active_module.OPTIONS[key].get("default",""))
154
+ log.success(f"{key} reset to default")
155
+ else:
156
+ log.error(f"No option: {key}")
157
+
158
+ def do_options(self, args):
159
+ """Show current module options."""
160
+ if not self._active_module:
161
+ log.kv("TARGET", self.session.target)
162
+ log.kv("MODE", self._mode); return
163
+ rows = self._active_module.show_options()
164
+ log.table(["Option","Value","Required","Description"], rows,
165
+ title=f"Options: {self._active_module_path}")
166
+
167
+ def do_run(self, args):
168
+ """Execute module or full chain. run | run auto"""
169
+ if args.strip().lower() == "auto":
170
+ self._run_auto_chain(); return
171
+ if not self._active_module:
172
+ log.error("No module selected. use <module> OR run auto"); return
173
+ ok, err = self._active_module.validate(self.session)
174
+ if not ok: log.error(f"Validation: {err}"); return
175
+ log.info(f"Running: {self._active_module_path}")
176
+ try:
177
+ result = self._active_module.run(self.session)
178
+ if result.success:
179
+ log.success("Module completed")
180
+ for k,v in result.data.items():
181
+ if not isinstance(v,(list,dict)): log.result(f"{k}: {v}")
182
+ else:
183
+ log.error(f"Module failed: {result.error}")
184
+ except Exception as e:
185
+ log.error(f"Module error: {e}"); import traceback; traceback.print_exc()
186
+
187
+ def _run_auto_chain(self):
188
+ """Run the full attack chain — static discovery first, then context-driven."""
189
+ log.banner("AUTO ATTACK CHAIN")
190
+ log.info(f"Target: {self.session.target}")
191
+ log.info(f"Mode: {self._mode}")
192
+ log.info(f"Session: {self.session.session_id}\n")
193
+
194
+ from core.context_engine import context_engine
195
+
196
+ # Phase A: Static discovery chain (always runs)
197
+ static_chain = [
198
+ "discovery/http_enum",
199
+ "cloud/s3_enum",
200
+ "secrets/git_secrets",
201
+ ]
202
+
203
+ total_modules = len(static_chain)
204
+ for i, path in enumerate(static_chain, 1):
205
+ self._run_module_in_chain(path, i, total_modules)
206
+
207
+ # Phase B: Context-driven dynamic chain
208
+ log.newline()
209
+ log.info("Building dynamic chain based on discoveries...")
210
+ dynamic_chain = context_engine.build_dynamic_chain(self.session)
211
+ log.info(f"Context engine selected {len(dynamic_chain)} additional modules:")
212
+ for m in dynamic_chain:
213
+ log.step(m)
214
+ log.newline()
215
+
216
+ for i, path in enumerate(dynamic_chain, len(static_chain) + 1):
217
+ self._run_module_in_chain(path, i, len(static_chain) + len(dynamic_chain))
218
+
219
+ # Final summary
220
+ print()
221
+ log.banner("ATTACK CHAIN COMPLETE")
222
+ s = self.session.summary()
223
+ score, label = risk_engine.session_score(self.session)
224
+ log.kv("Session ID", s["session_id"])
225
+ log.kv("Target", s["target"])
226
+ log.kv("Risk Score", f"{score}/10 [{label}]")
227
+ log.kv("Endpoints", str(s["endpoints"]))
228
+ log.kv("Exposed Files", str(s["exposed_files"]))
229
+ log.kv("Secrets Found", str(s["secrets"]))
230
+ log.kv("Findings", str(s["findings"]))
231
+ log.kv("CRITICAL", str(s["severity"]["CRITICAL"]))
232
+ log.kv("HIGH", str(s["severity"]["HIGH"]))
233
+ print()
234
+ log.info("View report: export html")
235
+
236
+ def _run_module_in_chain(self, path: str, step: int, total: int):
237
+ """Execute a single module within the auto chain."""
238
+ cls = loader.get(path)
239
+ if not cls:
240
+ log.warning(f"Skipping (not found): {path}")
241
+ return
242
+ log.phase(step, total, path)
243
+ mod = cls()
244
+ for opt, key in [("TARGET", self.session.target), ("MODE", self._mode)]:
245
+ if opt in mod.OPTIONS:
246
+ mod.set_option(opt, key)
247
+ ok, err = mod.validate(self.session)
248
+ if not ok:
249
+ log.error(f"Skip {path}: {err}")
250
+ return
251
+ try:
252
+ result = mod.run(self.session)
253
+ if result.success:
254
+ for k, v in result.data.items():
255
+ if not isinstance(v, (list, dict)):
256
+ log.result(f"{k}: {v}")
257
+ except Exception as e:
258
+ log.error(f"{path} error: {e}")
259
+
260
+ def do_show(self, args):
261
+ """show modules | findings | secrets | sessions | attack-path | exploits | summary | options"""
262
+ arg = args.strip().lower()
263
+ if arg in ("","help"):
264
+ for cmd, desc in [("show modules","List all modules"),("show options","Current options"),
265
+ ("show findings","Security findings"),("show secrets","Extracted credentials"),
266
+ ("show sessions","All sessions"),("show attack-path","ASCII kill chain"),
267
+ ("show exploits","Exploitation results"),("show summary","Stats + risk score")]:
268
+ log.kv(f" {cmd}", desc)
269
+ return
270
+
271
+ if arg == "modules":
272
+ for cat, mods in loader.all_modules().items():
273
+ if mods:
274
+ print(f"\n {Fore.YELLOW}{cat.upper()}{Style.RESET_ALL}")
275
+ print(f" {'─'*58}")
276
+ for m in mods:
277
+ sc = {"CRITICAL":Fore.RED,"HIGH":Fore.YELLOW,"MEDIUM":Fore.CYAN}.get(m["severity"],Fore.WHITE)
278
+ print(f" {Fore.CYAN}{m['path']:<38}{Style.RESET_ALL}"
279
+ f"{sc}{m['severity']:<10}{Style.RESET_ALL}{m['description'][:40]}")
280
+ print(f"\n Total: {loader.count()} modules | Tab-complete: use <Tab>")
281
+
282
+ elif arg == "options": self.do_options("")
283
+
284
+ elif arg == "findings":
285
+ if not self.session.findings: log.warning("No findings yet."); return
286
+ log.section(f"Findings ({len(self.session.findings)})")
287
+ for i,f in enumerate(self.session.findings,1):
288
+ print(f" {log.severity_badge(f.get('severity','INFO'))} {i:>3}. {f['title']}")
289
+ if f.get("cvss_score"): log.step(f"CVSS: {f['cvss_score']} | {f.get('mitre_technique','')}")
290
+
291
+ elif arg == "secrets":
292
+ if not self.session.secrets: log.warning("No secrets yet."); return
293
+ log.section(f"Secrets ({len(self.session.secrets)})")
294
+ for s in self.session.secrets:
295
+ log.secret(s["secret_type"], s["value"][:60])
296
+ log.step(f"Source: {s['source']}")
297
+
298
+ elif arg == "sessions": self.do_sessions("")
299
+
300
+ elif arg == "attack-path":
301
+ if not self.session.graph_nodes: log.warning("No graph yet. Run modules first."); return
302
+ attack_graph.build(self.session)
303
+ print(); print(attack_graph.print_ascii())
304
+ stats = attack_graph.stats()
305
+ print()
306
+ log.info(f"Nodes: {stats['nodes']} | Edges: {stats['edges']} | MITRE: {', '.join(stats['mitre_techniques'])}")
307
+
308
+ elif arg == "exploits":
309
+ if not self.session.exploit_results: log.warning("No exploits yet."); return
310
+ log.section(f"Exploit Results ({len(self.session.exploit_results)})")
311
+ for r in self.session.exploit_results:
312
+ st = f"{Fore.GREEN}[OK]{Style.RESET_ALL}" if r["success"] else f"{Fore.RED}[FAIL]{Style.RESET_ALL}"
313
+ print(f" {st} {r['action']}")
314
+ log.step(r["detail"])
315
+
316
+ elif arg == "summary":
317
+ s = self.session.summary()
318
+ score, label = risk_engine.session_score(self.session)
319
+ log.banner("SESSION SUMMARY")
320
+ for k,v in s.items():
321
+ if isinstance(v,dict):
322
+ for k2,v2 in v.items(): log.kv(f" {k2}", str(v2))
323
+ else: log.kv(k, str(v))
324
+ log.kv("Risk Score", f"{score}/10 [{label}]")
325
+ else:
326
+ log.error(f"Unknown: show {arg}")
327
+
328
+ def do_sessions(self, args):
329
+ """sessions | sessions -i ID | sessions -k ID"""
330
+ arg = args.strip()
331
+ rows = session_manager.list_all()
332
+ if not arg or arg == "-l":
333
+ if not rows: log.warning("No sessions."); return
334
+ active_id = self.session.session_id if self.session else ""
335
+ for r in rows:
336
+ m = f"{Fore.GREEN}*{Style.RESET_ALL}" if r["session_id"]==active_id else " "
337
+ print(f" {m} {Fore.CYAN}{r['session_id']}{Style.RESET_ALL} | "
338
+ f"{r['name']:<15} | {r['target']:<35} | risk={r['risk_score']:.1f} | {r['status']}")
339
+ return
340
+ parts = arg.split()
341
+ if len(parts) >= 2:
342
+ flag, sid = parts[0], parts[1]
343
+ if flag == "-i":
344
+ s = session_manager.switch(sid)
345
+ log.success(f"Switched to: {sid}") if s else log.error(f"Not found: {sid}")
346
+ self._update_prompt()
347
+ elif flag == "-k":
348
+ log.success(f"Killed: {sid}") if session_manager.kill(sid) else log.error(f"Not found: {sid}")
349
+
350
+ def do_workspace(self, args):
351
+ """workspace list | workspace new <name> <url> | workspace switch <id>"""
352
+ parts = args.strip().split(None, 2)
353
+ if not parts: log.error("Usage: workspace [list|new|switch]"); return
354
+ sub = parts[0].lower()
355
+ if sub == "list": self.do_sessions("")
356
+ elif sub == "new":
357
+ if len(parts) < 3: log.error("Usage: workspace new <name> <url>"); return
358
+ t = parts[2] if parts[2].startswith(("http://","https://")) else "http://"+parts[2]
359
+ s = session_manager.new(t, parts[1], self._mode)
360
+ log.success(f"Workspace '{parts[1]}' created → {t} [{s.session_id}]")
361
+ self._update_prompt()
362
+ elif sub == "switch":
363
+ if len(parts) < 2: log.error("Usage: workspace switch <id>"); return
364
+ self.do_sessions(f"-i {parts[1]}")
365
+ else: log.error(f"Unknown: workspace {sub}")
366
+
367
+ def do_export(self, args):
368
+ """Export report. export html | export json | export all"""
369
+ fmt = args.strip().lower() or "all"
370
+ fmts = ["html","json"] if fmt=="all" else [fmt]
371
+ for f in fmts:
372
+ mod = loader.instantiate(f"html_report" if f=="html" else "json_export")
373
+ if mod: mod.run(self.session)
374
+ else: log.error(f"Reporting module not found: {f}")
375
+
376
+ def do_info(self, args):
377
+ """Module details. info discovery/http_enum"""
378
+ path = args.strip() or self._active_module_path
379
+ if not path: log.error("Usage: info <module>"); return
380
+ cls = loader.get(path)
381
+ if not cls: log.error(f"Not found: {path}"); return
382
+ log.banner(f"MODULE: {path.upper()}")
383
+ for k,v in [("Name",cls.NAME),("Description",cls.DESCRIPTION),("Author",cls.AUTHOR),
384
+ ("Version",cls.VERSION),("Category",cls.CATEGORY),("Severity",cls.SEVERITY),
385
+ ("MITRE",", ".join(cls.MITRE) or "N/A"),("AWS Parallel",cls.AWS_PARALLEL or "N/A")]:
386
+ log.kv(k, v)
387
+ if cls.OPTIONS:
388
+ log.table(["Option","Default","Required","Description"], cls().show_options())
389
+
390
+ def do_search(self, args):
391
+ """Search modules by keyword. search jwt | search s3 | search cloud"""
392
+ kw = args.strip().lower()
393
+ if not kw: log.error("Usage: search <keyword>"); return
394
+ results = [m for mods in loader.all_modules().values() for m in mods
395
+ if kw in m["name"] or kw in m["description"].lower()
396
+ or kw in m.get("mitre","").lower()]
397
+ if not results: log.warning(f"No results for: {kw}"); return
398
+ print(f"\n {Fore.YELLOW}Results for '{kw}':{Style.RESET_ALL}")
399
+ for m in results:
400
+ print(f" {Fore.CYAN}{m['path']:<38}{Style.RESET_ALL}{m['description'][:48]}")
401
+
402
+ def do_back(self, args):
403
+ """Deselect current module."""
404
+ self._active_module = None; self._active_module_path = ""
405
+ self._update_prompt(); log.info("Returned to main console.")
406
+
407
+ def do_clear(self, args):
408
+ """Clear screen."""
409
+ os.system("clear"); print_banner()
410
+
411
+ def do_exit(self, args):
412
+ """Exit ExploitGraph."""
413
+ log.info("Exiting. Run 'export html' first if you haven't saved your report.")
414
+ if self.session: self.session.close()
415
+ return True
416
+
417
+ def do_quit(self, args): return self.do_exit(args)
418
+ def do_EOF(self, args): print(); return self.do_exit(args)
419
+ def emptyline(self): pass
420
+ def default(self, line):
421
+ log.error(f"Unknown: {line.split()[0] if line.split() else line} (type 'help')")
422
+
423
+ def do_help(self, args):
424
+ """Show all commands."""
425
+ if args: super().do_help(args); return
426
+ log.banner("EXPLOITGRAPH COMMANDS")
427
+ sections = [
428
+ ("SESSION", [
429
+ (" workspace new <n> <url>", "Create named workspace"),
430
+ (" set TARGET <url>", "Set scan target"),
431
+ (" set MODE offensive|defensive", "Set scan mode"),
432
+ ]),
433
+ ("ATTACK", [
434
+ (" run auto", "Full automatic attack chain"),
435
+ (" use <module>", "Select a module (Tab to complete)"),
436
+ (" run", "Execute selected module"),
437
+ (" options", "Show module options"),
438
+ (" set OPTION value", "Configure option"),
439
+ (" back", "Deselect module"),
440
+ ]),
441
+ ("INFORMATION", [
442
+ (" show modules", "All modules by category"),
443
+ (" show findings", "Security findings"),
444
+ (" show secrets", "Extracted credentials"),
445
+ (" show attack-path", "ASCII kill chain"),
446
+ (" show summary", "Risk score + stats"),
447
+ (" info <module>", "Module details"),
448
+ (" search <keyword>", "Search modules"),
449
+ ]),
450
+ ("SESSIONS", [
451
+ (" sessions", "List all sessions"),
452
+ (" sessions -i <id>", "Switch to session"),
453
+ (" sessions -k <id>", "Kill session"),
454
+ ]),
455
+ ("OUTPUT", [
456
+ (" export html", "HTML report (D3.js graph)"),
457
+ (" export json", "JSON for SIEM/ticketing"),
458
+ (" export all", "All formats"),
459
+ ]),
460
+ ("MISC", [
461
+ (" clear", "Clear screen"),
462
+ (" exit / quit", "Exit framework"),
463
+ ]),
464
+ ]
465
+ for section, cmds in sections:
466
+ print(f"\n {Fore.YELLOW}{section}{Style.RESET_ALL}")
467
+ for name, desc in cmds: log.kv(name, desc)
468
+ print()
469
+ log.info("Quick start: workspace new test http://target.com → run auto")
core/context_engine.py ADDED
@@ -0,0 +1,172 @@
1
+ """
2
+ ExploitGraph - Context Engine
3
+ Analyses what modules have found and decides dynamically which modules
4
+ to run next. Replaces the static 6-step chain with intelligent sequencing.
5
+
6
+ Logic:
7
+ Discovery → Cloud Enum → [if .gz found] CloudTrail Analysis
8
+ → [if secrets found] Credential Validation
9
+ → [if valid creds] IAM Enum + Priv-Esc
10
+ → Reporting
11
+ """
12
+ from __future__ import annotations
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from core.session_manager import Session
17
+
18
+
19
+ class ContextEngine:
20
+ """
21
+ Examines session state after each module and returns the next
22
+ module path(s) to execute.
23
+ """
24
+
25
+ # Static discovery chain — always runs in this order
26
+ DISCOVERY_CHAIN = [
27
+ "discovery/http_enum",
28
+ "cloud/s3_enum",
29
+ "secrets/git_secrets",
30
+ ]
31
+
32
+ # Always runs at the end
33
+ REPORTING_CHAIN = [
34
+ "reporting/html_report",
35
+ "reporting/json_export",
36
+ ]
37
+
38
+ def build_dynamic_chain(self, session: "Session") -> list[str]:
39
+ """
40
+ Build the full module execution chain based on what was discovered.
41
+ Call this AFTER the discovery chain has run.
42
+ """
43
+ chain = []
44
+
45
+ # ── Conditional: CloudTrail logs found ─────────────────────────────
46
+ if self._has_cloudtrail_logs(session):
47
+ chain.append("cloud/cloudtrail_analyzer")
48
+
49
+ # ── Conditional: Subdomains not yet checked ────────────────────────
50
+ if not self._already_ran("subdomain_enum", session):
51
+ chain.append("discovery/subdomain_enum")
52
+
53
+ # ── Conditional: Azure/GCP indicators in responses ─────────────────
54
+ body = self._all_response_text(session)
55
+ if "blob.core.windows.net" in body or "azurewebsites" in body:
56
+ chain.append("cloud/azure_enum")
57
+ if "storage.googleapis.com" in body or "appspot.com" in body:
58
+ chain.append("cloud/gcp_enum")
59
+
60
+ # ── Conditional: IMDS / metadata hints ─────────────────────────────
61
+ if "169.254.169.254" in body or "metadata" in body.lower():
62
+ chain.append("cloud/metadata_check")
63
+
64
+ # ── Always: scan for secrets in all discovered content ──────────────
65
+ chain.append("secrets/file_secrets")
66
+
67
+ # ── Conditional: AWS credentials found → validate them ──────────────
68
+ if self._has_aws_credentials(session):
69
+ chain.append("cloud/aws_credential_validator")
70
+ chain.append("cloud/iam_enum")
71
+ chain.append("cloud/iam_privilege_escalation")
72
+
73
+ # ── Conditional: Login/auth endpoints found → try exploitation ──────
74
+ if self._has_auth_endpoints(session):
75
+ chain.append("exploitation/api_exploit")
76
+ chain.append("exploitation/jwt_attack")
77
+
78
+ # ── Conditional: SSRF-prone parameters found ────────────────────────
79
+ if self._has_ssrf_params(session):
80
+ chain.append("exploitation/ssrf_scanner")
81
+
82
+ # ── Always: reporting at the end ────────────────────────────────────
83
+ chain.extend(self.REPORTING_CHAIN)
84
+
85
+ # Deduplicate while preserving order
86
+ seen = set()
87
+ return [x for x in chain if not (x in seen or seen.add(x))]
88
+
89
+ def next_modules(self, completed: str, session: "Session") -> list[str]:
90
+ """
91
+ After a specific module completes, return immediate follow-up modules.
92
+ Used for real-time chaining during execution.
93
+ """
94
+ follow_ups = []
95
+
96
+ if completed == "cloud/s3_enum":
97
+ if self._has_cloudtrail_logs(session):
98
+ follow_ups.append("cloud/cloudtrail_analyzer")
99
+ if session.exposed_files:
100
+ follow_ups.append("secrets/file_secrets")
101
+
102
+ elif completed in ("secrets/file_secrets", "cloud/cloudtrail_analyzer"):
103
+ if self._has_aws_credentials(session):
104
+ follow_ups.append("cloud/aws_credential_validator")
105
+
106
+ elif completed == "cloud/aws_credential_validator":
107
+ if self._has_valid_credentials(session):
108
+ follow_ups.extend(["cloud/iam_enum", "cloud/iam_privilege_escalation"])
109
+
110
+ return follow_ups
111
+
112
+ # ── Detection helpers ──────────────────────────────────────────────────
113
+
114
+ def _has_cloudtrail_logs(self, session: "Session") -> bool:
115
+ """Check if any downloaded files are CloudTrail .json.gz logs."""
116
+ for f in session.exposed_files:
117
+ path = f.get("path", "") or f.get("url", "")
118
+ if "CloudTrail" in path or (
119
+ path.endswith(".json.gz") and "AWSLogs" in path
120
+ ):
121
+ return True
122
+ return False
123
+
124
+ def _has_aws_credentials(self, session: "Session") -> bool:
125
+ """Check if any AWS access key + secret pair exists in secrets."""
126
+ types = {s["secret_type"] for s in session.secrets}
127
+ return "AWS_ACCESS_KEY" in types
128
+
129
+ def _has_valid_credentials(self, session: "Session") -> bool:
130
+ """Check if credential validator confirmed valid credentials."""
131
+ for r in session.exploit_results:
132
+ if "credential_validator" in r.get("module", "") and r.get("success"):
133
+ return True
134
+ return False
135
+
136
+ def _has_auth_endpoints(self, session: "Session") -> bool:
137
+ """Check if login/auth endpoints were discovered."""
138
+ for ep in session.endpoints:
139
+ p = ep.get("path", "").lower()
140
+ if any(kw in p for kw in ["login", "auth", "token", "signin"]):
141
+ return True
142
+ return False
143
+
144
+ def _has_ssrf_params(self, session: "Session") -> bool:
145
+ """Check if any discovered endpoints have SSRF-prone parameters."""
146
+ for ep in session.endpoints:
147
+ url = ep.get("url", "")
148
+ if any(p in url for p in ["url=", "redirect=", "proxy=", "fetch=", "dest="]):
149
+ return True
150
+ return False
151
+
152
+ def _already_ran(self, module_name: str, session: "Session") -> bool:
153
+ """Check if a module already produced results in this session."""
154
+ for r in session.exploit_results:
155
+ if module_name in r.get("module", ""):
156
+ return True
157
+ return False
158
+
159
+ def _all_response_text(self, session: "Session") -> str:
160
+ """Concatenate all downloaded file content for pattern matching."""
161
+ parts = []
162
+ for f in session.exposed_files:
163
+ content = f.get("content", "") or ""
164
+ if isinstance(content, str):
165
+ parts.append(content[:500])
166
+ for ep in session.endpoints:
167
+ parts.append(ep.get("url", ""))
168
+ return " ".join(parts)
169
+
170
+
171
+ # Global singleton
172
+ context_engine = ContextEngine()