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/http_client.py ADDED
@@ -0,0 +1,243 @@
1
+ """
2
+ ExploitGraph - Centralised HTTP Client
3
+ All modules use this instead of calling requests.get() directly.
4
+
5
+ Provides:
6
+ - Automatic retry with exponential backoff (3 attempts)
7
+ - Consistent User-Agent header
8
+ - SSL verification control from config
9
+ - Connection pool reuse (session-based)
10
+ - Structured response object with helper methods
11
+ - Per-request timeout enforcement
12
+ - Silent-failure protection: never raises, always returns a Response
13
+ """
14
+ from __future__ import annotations
15
+ import time
16
+ from typing import Any, Optional
17
+
18
+ import requests
19
+ import urllib3
20
+
21
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
22
+
23
+ DEFAULT_TIMEOUT = 8
24
+ DEFAULT_UA = "ExploitGraph/1.2 (Security Research; +https://github.com/prajwal-infosec/ExploitGraph)"
25
+ MAX_RETRIES = 3
26
+ RETRY_BACKOFF = [0, 1, 2] # seconds before each attempt
27
+ RETRY_ON_STATUS = {429, 500, 502, 503, 504}
28
+ NO_RETRY_ON_STATUS = {400, 401, 403, 404, 405}
29
+
30
+
31
+ class EGResponse:
32
+ """
33
+ Wrapper around requests.Response that never raises and provides
34
+ helper methods for the most common access patterns in modules.
35
+ """
36
+ def __init__(self, resp: Optional[requests.Response], error: str = ""):
37
+ self._resp = resp
38
+ self._error = error
39
+
40
+ # ── Core properties ────────────────────────────────────────────────────
41
+
42
+ @property
43
+ def ok(self) -> bool:
44
+ return self._resp is not None and self._resp.status_code < 400
45
+
46
+ @property
47
+ def status_code(self) -> int:
48
+ return self._resp.status_code if self._resp is not None else 0
49
+
50
+ @property
51
+ def text(self) -> str:
52
+ if self._resp is None:
53
+ return ""
54
+ try:
55
+ return self._resp.text
56
+ except Exception:
57
+ return ""
58
+
59
+ @property
60
+ def content(self) -> bytes:
61
+ if self._resp is None:
62
+ return b""
63
+ return self._resp.content or b""
64
+
65
+ @property
66
+ def error(self) -> str:
67
+ return self._error
68
+
69
+ @property
70
+ def headers(self) -> dict:
71
+ if self._resp is None:
72
+ return {}
73
+ return dict(self._resp.headers)
74
+
75
+ @property
76
+ def url(self) -> str:
77
+ if self._resp is None:
78
+ return ""
79
+ return self._resp.url or ""
80
+
81
+ # ── Helper methods ─────────────────────────────────────────────────────
82
+
83
+ def json(self, default: Any = None) -> Any:
84
+ """Parse JSON body. Returns `default` on failure — never raises."""
85
+ if self._resp is None:
86
+ return default
87
+ try:
88
+ return self._resp.json()
89
+ except Exception:
90
+ return default
91
+
92
+ def contains(self, *strings: str) -> bool:
93
+ """True if all strings are in the response text."""
94
+ body = self.text
95
+ return all(s in body for s in strings)
96
+
97
+ def header(self, name: str, default: str = "") -> str:
98
+ return self.headers.get(name, self.headers.get(name.lower(), default))
99
+
100
+ def __bool__(self) -> bool:
101
+ return self.ok
102
+
103
+ def __repr__(self) -> str:
104
+ if self._resp is None:
105
+ return f"<EGResponse ERROR: {self._error}>"
106
+ return f"<EGResponse {self.status_code} {self.url[:60]}>"
107
+
108
+
109
+ class HTTPClient:
110
+ """
111
+ Thread-safe HTTP client using a shared requests.Session.
112
+ All ExploitGraph modules get one instance via the module-level `http` singleton.
113
+ """
114
+
115
+ def __init__(self, timeout: int = DEFAULT_TIMEOUT,
116
+ verify_ssl: bool = False,
117
+ user_agent: str = DEFAULT_UA):
118
+ self.timeout = timeout
119
+ self.verify_ssl = verify_ssl
120
+ self._session = self._make_session(user_agent)
121
+
122
+ def _make_session(self, user_agent: str) -> requests.Session:
123
+ s = requests.Session()
124
+ s.headers.update({
125
+ "User-Agent": user_agent,
126
+ "Accept": "application/json, text/html, */*",
127
+ })
128
+ # Connection pool: reuse up to 20 connections
129
+ adapter = requests.adapters.HTTPAdapter(
130
+ pool_connections=20,
131
+ pool_maxsize=50,
132
+ max_retries=0, # We handle retries ourselves
133
+ )
134
+ s.mount("http://", adapter)
135
+ s.mount("https://", adapter)
136
+ return s
137
+
138
+ def get(self, url: str, *,
139
+ headers: dict = None,
140
+ params: dict = None,
141
+ timeout: int = None,
142
+ allow_redirects: bool = True,
143
+ retries: int = MAX_RETRIES) -> EGResponse:
144
+ return self._request("GET", url, headers=headers, params=params,
145
+ timeout=timeout, allow_redirects=allow_redirects,
146
+ retries=retries)
147
+
148
+ def post(self, url: str, *,
149
+ json: Any = None,
150
+ data: Any = None,
151
+ headers: dict = None,
152
+ timeout: int = None,
153
+ retries: int = 1) -> EGResponse: # POST: fewer retries by default
154
+ return self._request("POST", url, json=json, data=data,
155
+ headers=headers, timeout=timeout, retries=retries)
156
+
157
+ def _request(self, method: str, url: str, *,
158
+ headers: dict = None,
159
+ params: dict = None,
160
+ json: Any = None,
161
+ data: Any = None,
162
+ timeout: int = None,
163
+ allow_redirects: bool = True,
164
+ retries: int = MAX_RETRIES) -> EGResponse:
165
+ """
166
+ Execute HTTP request with retry + exponential backoff.
167
+ Never raises — always returns EGResponse.
168
+ """
169
+ effective_timeout = timeout or self.timeout
170
+ kwargs = {
171
+ "headers": headers or {},
172
+ "params": params,
173
+ "timeout": effective_timeout,
174
+ "verify": self.verify_ssl,
175
+ "allow_redirects": allow_redirects,
176
+ }
177
+ if json is not None:
178
+ kwargs["json"] = json
179
+ if data is not None:
180
+ kwargs["data"] = data
181
+
182
+ last_error = ""
183
+ for attempt in range(retries):
184
+ if attempt > 0:
185
+ delay = RETRY_BACKOFF[min(attempt, len(RETRY_BACKOFF) - 1)]
186
+ if delay:
187
+ time.sleep(delay)
188
+
189
+ try:
190
+ resp = self._session.request(method, url, **kwargs)
191
+
192
+ # Don't retry on definitive failure codes
193
+ if resp.status_code in NO_RETRY_ON_STATUS:
194
+ return EGResponse(resp)
195
+
196
+ # Retry on transient failures
197
+ if resp.status_code in RETRY_ON_STATUS and attempt < retries - 1:
198
+ last_error = f"HTTP {resp.status_code}"
199
+ continue
200
+
201
+ return EGResponse(resp)
202
+
203
+ except requests.exceptions.Timeout:
204
+ last_error = f"Timeout after {effective_timeout}s"
205
+ except requests.exceptions.ConnectionError as e:
206
+ last_error = f"Connection error: {_short_err(e)}"
207
+ except requests.exceptions.TooManyRedirects:
208
+ last_error = "Too many redirects"
209
+ break
210
+ except Exception as e:
211
+ last_error = f"Unexpected error: {_short_err(e)}"
212
+ break # Don't retry unknown errors
213
+
214
+ return EGResponse(None, last_error)
215
+
216
+ def update_config(self, timeout: int = None, verify_ssl: bool = None):
217
+ if timeout is not None:
218
+ self.timeout = timeout
219
+ if verify_ssl is not None:
220
+ self.verify_ssl = verify_ssl
221
+
222
+
223
+ def _short_err(e: Exception) -> str:
224
+ s = str(e)
225
+ return s[:120] + "..." if len(s) > 120 else s
226
+
227
+
228
+ # ── Global singleton ───────────────────────────────────────────────────────────
229
+ # All modules import this and call http.get(...) / http.post(...)
230
+ http = HTTPClient()
231
+
232
+
233
+ def configure(timeout: int = None, verify_ssl: bool = None, user_agent: str = None):
234
+ """Called at startup from config.py to apply framework settings."""
235
+ global http
236
+ if user_agent:
237
+ http = HTTPClient(
238
+ timeout = timeout or http.timeout,
239
+ verify_ssl = verify_ssl if verify_ssl is not None else http.verify_ssl,
240
+ user_agent = user_agent,
241
+ )
242
+ else:
243
+ http.update_config(timeout=timeout, verify_ssl=verify_ssl)
core/logger.py ADDED
@@ -0,0 +1,97 @@
1
+ """ExploitGraph - Structured Logger (colored console + rotating file)."""
2
+ from __future__ import annotations
3
+ import logging
4
+ from pathlib import Path
5
+ from colorama import Fore, Style, init
6
+
7
+ init(autoreset=True)
8
+ LOG_DIR = Path(__file__).parent.parent / "logs"
9
+ LOG_DIR.mkdir(exist_ok=True)
10
+
11
+
12
+ class ExploitGraphLogger:
13
+ def __init__(self):
14
+ self._quiet = False
15
+ self._flog = None
16
+ self._setup("startup")
17
+
18
+ def _setup(self, sid: str):
19
+ self._flog = logging.getLogger(f"eg_{sid}")
20
+ self._flog.setLevel(logging.DEBUG)
21
+ if not self._flog.handlers:
22
+ fh = logging.FileHandler(LOG_DIR / f"exploitgraph_{sid}.log", encoding="utf-8")
23
+ fh.setFormatter(logging.Formatter("%(asctime)s | %(levelname)s | %(message)s",
24
+ datefmt="%Y-%m-%d %H:%M:%S"))
25
+ self._flog.addHandler(fh)
26
+
27
+ def new_session(self, sid: str): self._setup(sid)
28
+ def set_quiet(self, q: bool): self._quiet = q
29
+
30
+ def _p(self, prefix, msg, color=""):
31
+ c = color or Fore.WHITE
32
+ if not self._quiet:
33
+ print(f"{c}{prefix}{Style.RESET_ALL} {msg}")
34
+ if self._flog:
35
+ self._flog.info(f"{prefix} {msg}")
36
+
37
+ def info(self, m): self._p("[*]", m, Fore.CYAN)
38
+ def success(self, m): self._p("[+]", m, Fore.GREEN)
39
+ def warning(self, m): self._p("[!]", m, Fore.YELLOW)
40
+ def error(self, m): self._p("[-]", m, Fore.RED)
41
+ def result(self, m): self._p("[>]", m, Fore.MAGENTA)
42
+ def step(self, m): self._p(" →", m, Fore.BLUE)
43
+ def newline(self): print()
44
+
45
+ def critical(self, m):
46
+ print(f"{Fore.WHITE}[{Fore.RED}CRITICAL{Fore.WHITE}]{Style.RESET_ALL} {Fore.RED}{m}{Style.RESET_ALL}")
47
+ if self._flog: self._flog.critical(m)
48
+
49
+ def found(self, m):
50
+ self._p("[FOUND]", m, Fore.GREEN)
51
+
52
+ def secret(self, label: str, value: str):
53
+ print(f" {Fore.RED}►{Style.RESET_ALL} {Fore.YELLOW}{label:<26}{Style.RESET_ALL}{Fore.WHITE}{value}{Style.RESET_ALL}")
54
+ if self._flog: self._flog.info(f"SECRET | {label}: {value}")
55
+
56
+ def kv(self, key: str, val: str, w: int = 28):
57
+ print(f" {Fore.CYAN}{key:<{w}}{Style.RESET_ALL}{val}")
58
+
59
+ def banner(self, title: str, width: int = 62):
60
+ bar = "═" * width
61
+ print(f"\n{Fore.RED}╔{bar}╗")
62
+ print(f"║{Fore.WHITE} {title.center(width-2)}{Fore.RED} ║")
63
+ print(f"╚{bar}╝{Style.RESET_ALL}")
64
+
65
+ def section(self, title: str):
66
+ print(f"\n{Fore.CYAN}{'─'*60}\n{Fore.WHITE}{title.upper()}{Style.RESET_ALL}\n{Fore.CYAN}{'─'*60}{Style.RESET_ALL}")
67
+
68
+ def phase(self, n: int, total: int, name: str):
69
+ print(f"\n{Fore.CYAN}{'─'*60}")
70
+ print(f"{Fore.CYAN}[{n}/{total}]{Fore.WHITE} {name.upper()}{Style.RESET_ALL}")
71
+ print(f"{Fore.CYAN}{'─'*60}{Style.RESET_ALL}")
72
+
73
+ def severity_badge(self, s: str) -> str:
74
+ m = {"CRITICAL": Fore.RED, "HIGH": Fore.YELLOW,
75
+ "MEDIUM": Fore.CYAN, "LOW": Fore.GREEN, "INFO": Fore.WHITE}
76
+ c = m.get(s.upper(), Fore.WHITE)
77
+ return f"{c}[{s}]{Style.RESET_ALL}"
78
+
79
+ def table(self, headers, rows, title=""):
80
+ try:
81
+ from tabulate import tabulate
82
+ if title: print(f"\n{Fore.YELLOW}{title}{Style.RESET_ALL}")
83
+ print(tabulate(rows, headers=headers, tablefmt="rounded_outline") if rows
84
+ else f" {Fore.CYAN}(no data){Style.RESET_ALL}")
85
+ except ImportError:
86
+ if title: print(f"\n{title}")
87
+ for r in rows: print(" " + " | ".join(str(c) for c in r))
88
+
89
+ # Module-facing aliases
90
+ def module_info(self, m): self.info(m)
91
+ def module_success(self, m): self.success(m)
92
+ def module_warn(self, m): self.warning(m)
93
+ def module_error(self, m): self.error(m)
94
+ def module_critical(self, m): self.critical(m)
95
+
96
+
97
+ log = ExploitGraphLogger()
core/module_loader.py ADDED
@@ -0,0 +1,69 @@
1
+ """ExploitGraph - Dynamic module discovery and loading."""
2
+ from __future__ import annotations
3
+ import sys, importlib.util
4
+ from pathlib import Path
5
+ from typing import Optional
6
+ from modules.base import BaseModule
7
+
8
+ MODULE_ROOT = Path(__file__).parent.parent / "modules"
9
+ CATEGORY_ORDER = ["discovery","cloud","secrets","exploitation","reporting","misc","custom"]
10
+
11
+
12
+ class ModuleLoader:
13
+ def __init__(self):
14
+ self._registry: dict[str, type[BaseModule]] = {}
15
+ self._errors: list[str] = []
16
+
17
+ def discover(self) -> int:
18
+ self._registry.clear(); self._errors.clear()
19
+ for cat_dir in sorted(MODULE_ROOT.iterdir()):
20
+ if not cat_dir.is_dir() or cat_dir.name.startswith("_"): continue
21
+ for py in sorted(cat_dir.glob("*.py")):
22
+ if not py.name.startswith("_"):
23
+ self._try_load(py, cat_dir.name)
24
+ return len(self._registry)
25
+
26
+ def _try_load(self, path: Path, category: str):
27
+ mod_name = f"_eg_{category}_{path.stem}"
28
+ try:
29
+ spec = importlib.util.spec_from_file_location(mod_name, path)
30
+ mod = importlib.util.module_from_spec(spec)
31
+ sys.modules[mod_name] = mod
32
+ spec.loader.exec_module(mod)
33
+ for attr in dir(mod):
34
+ obj = getattr(mod, attr)
35
+ if (isinstance(obj, type) and issubclass(obj, BaseModule)
36
+ and obj is not BaseModule and obj.NAME != "unnamed_module"):
37
+ self._registry[f"{category}/{obj.NAME}"] = obj
38
+ break
39
+ except Exception as e:
40
+ self._errors.append(f"{path.name}: {e}")
41
+
42
+ def get(self, path: str) -> Optional[type[BaseModule]]:
43
+ if path in self._registry: return self._registry[path]
44
+ matches = [k for k in self._registry if k.split("/")[-1] == path]
45
+ if len(matches) == 1: return self._registry[matches[0]]
46
+ if len(matches) > 1: raise ValueError(f"Ambiguous: {path} → {matches}")
47
+ return None
48
+
49
+ def instantiate(self, path: str) -> Optional[BaseModule]:
50
+ cls = self.get(path)
51
+ return cls() if cls else None
52
+
53
+ def all_modules(self) -> dict[str, list[dict]]:
54
+ grouped = {c: [] for c in CATEGORY_ORDER}
55
+ for key, cls in self._registry.items():
56
+ cat = key.split("/")[0]
57
+ grouped.setdefault(cat, []).append({
58
+ "path": key, "name": cls.NAME, "description": cls.DESCRIPTION,
59
+ "severity": cls.SEVERITY, "author": cls.AUTHOR,
60
+ "version": cls.VERSION, "mitre": ", ".join(cls.MITRE),
61
+ })
62
+ return {k: v for k, v in grouped.items() if v}
63
+
64
+ def list_names(self) -> list[str]: return sorted(self._registry.keys())
65
+ def count(self) -> int: return len(self._registry)
66
+ def load_errors(self) -> list[str]: return self._errors
67
+
68
+
69
+ loader = ModuleLoader()
core/risk_engine.py ADDED
@@ -0,0 +1,47 @@
1
+ """ExploitGraph - CVSS-style risk scoring engine."""
2
+ from __future__ import annotations
3
+ from typing import TYPE_CHECKING
4
+ if TYPE_CHECKING:
5
+ from core.session_manager import Session
6
+
7
+ SEVERITY_BASE = {"CRITICAL":9.0,"HIGH":7.0,"MEDIUM":5.0,"LOW":3.0,"INFO":1.0}
8
+ CLASSIFICATION = [(9.0,"CRITICAL"),(7.0,"HIGH"),(4.0,"MEDIUM"),(1.0,"LOW"),(0.0,"INFO")]
9
+
10
+
11
+ class RiskEngine:
12
+ def score_finding(self, f: dict) -> tuple[float, str]:
13
+ if f.get("cvss_score", 0) > 0:
14
+ raw = float(f["cvss_score"])
15
+ else:
16
+ raw = SEVERITY_BASE.get(f.get("severity","INFO"), 1.0)
17
+ score = round(min(raw, 10.0), 1)
18
+ return score, self._classify(score)
19
+
20
+ def session_score(self, session: "Session") -> tuple[float, str]:
21
+ if not session.findings: return 0.0, "INFO"
22
+ scores = sorted([self.score_finding(f)[0] for f in session.findings], reverse=True)
23
+ if len(scores) == 1:
24
+ agg = scores[0]
25
+ else:
26
+ chain_bonus = min(len(session.graph_edges) * 0.2, 1.5)
27
+ agg = min((scores[0]*0.5 + sum(scores[1:])/len(scores[1:])*0.5) + chain_bonus, 10.0)
28
+ score = round(agg, 1)
29
+ return score, self._classify(score)
30
+
31
+ def score_breakdown(self, session: "Session") -> dict:
32
+ score, label = self.session_score(session)
33
+ sev = {k: 0 for k in ("CRITICAL","HIGH","MEDIUM","LOW","INFO")}
34
+ for f in session.findings:
35
+ s = f.get("severity","INFO")
36
+ sev[s] = sev.get(s, 0) + 1
37
+ return dict(session_score=score, classification=label, severity_counts=sev,
38
+ total_findings=len(session.findings), secrets_found=len(session.secrets),
39
+ graph_nodes=len(session.graph_nodes), graph_edges=len(session.graph_edges))
40
+
41
+ def _classify(self, score: float) -> str:
42
+ for threshold, label in CLASSIFICATION:
43
+ if score >= threshold: return label
44
+ return "INFO"
45
+
46
+
47
+ risk_engine = RiskEngine()