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/__init__.py ADDED
File without changes
core/attack_graph.py ADDED
@@ -0,0 +1,83 @@
1
+ """ExploitGraph - Attack path graph engine (networkx)."""
2
+ from __future__ import annotations
3
+ import json
4
+ from typing import TYPE_CHECKING
5
+ import networkx as nx
6
+ if TYPE_CHECKING:
7
+ from core.session_manager import Session
8
+
9
+ SEVERITY_COLOR = {"CRITICAL":"#dc2626","HIGH":"#ea580c","MEDIUM":"#d97706",
10
+ "LOW":"#16a34a","INFO":"#2563eb"}
11
+ MITRE_REF = {
12
+ "T1595":"Active Scanning", "T1595.003":"Wordlist Scanning",
13
+ "T1580":"Cloud Infrastructure Discovery", "T1530":"Data from Cloud Storage",
14
+ "T1552.001":"Credentials in Files", "T1552.004":"Private Keys",
15
+ "T1552.005":"Cloud Instance Metadata API", "T1078":"Valid Accounts",
16
+ "T1078.004":"Valid Accounts: Cloud Accounts", "T1548":"Abuse Elevation Control",
17
+ "T1550.001":"Application Access Token", "T1069.003":"Cloud Groups",
18
+ "T1110.001":"Password Guessing",
19
+ }
20
+
21
+
22
+ class AttackGraph:
23
+ def __init__(self): self.G: nx.DiGraph = nx.DiGraph()
24
+
25
+ def build(self, session: "Session"):
26
+ self.G.clear()
27
+ for n in session.graph_nodes:
28
+ self.G.add_node(n["node_id"], label=n.get("label",""),
29
+ type=n.get("node_type","asset"),
30
+ severity=n.get("severity","INFO"),
31
+ details=n.get("details",""),
32
+ color=SEVERITY_COLOR.get(n.get("severity","INFO"),"#6b7280"))
33
+ for e in session.graph_edges:
34
+ if e["source"] in self.G and e["target"] in self.G:
35
+ self.G.add_edge(e["source"], e["target"],
36
+ label=e.get("label",""), technique=e.get("technique",""))
37
+
38
+ def find_paths(self, src="attacker", tgt="compromise") -> list[list[str]]:
39
+ try:
40
+ nodes = list(self.G.nodes())
41
+ src = next((n for n in nodes if src in n), src)
42
+ tgt = next((n for n in nodes if tgt in n.lower()), tgt)
43
+ return list(nx.all_simple_paths(self.G, src, tgt))
44
+ except Exception:
45
+ return []
46
+
47
+ def stats(self) -> dict:
48
+ techs = set()
49
+ for _,_,d in self.G.edges(data=True):
50
+ if d.get("technique"): techs.add(d["technique"])
51
+ critical = [n for n,d in self.G.nodes(data=True) if d.get("severity") in ("CRITICAL","HIGH")]
52
+ return dict(nodes=self.G.number_of_nodes(), edges=self.G.number_of_edges(),
53
+ critical_nodes=len(critical), mitre_techniques=sorted(techs),
54
+ is_connected=nx.is_weakly_connected(self.G) if self.G.nodes() else False)
55
+
56
+ def to_json(self) -> dict:
57
+ nodes = [dict(id=nid, label=d.get("label",nid), type=d.get("type","asset"),
58
+ severity=d.get("severity","INFO"), details=d.get("details",""),
59
+ color=d.get("color","#6b7280"))
60
+ for nid,d in self.G.nodes(data=True)]
61
+ edges = [dict(source=s, target=t, label=d.get("label",""), technique=d.get("technique",""))
62
+ for s,t,d in self.G.edges(data=True)]
63
+ return dict(nodes=nodes, edges=edges, stats=self.stats(), attack_paths=self.find_paths())
64
+
65
+ def print_ascii(self) -> str:
66
+ paths = self.find_paths()
67
+ if not paths: return "No complete attack path found."
68
+ path = max(paths, key=len)
69
+ lines = []
70
+ colors = {"CRITICAL":"\033[91m","HIGH":"\033[93m","MEDIUM":"\033[96m","INFO":"\033[97m"}
71
+ for i, nid in enumerate(path):
72
+ d = self.G.nodes.get(nid, {})
73
+ label = d.get("label", nid).split("\n")[0]
74
+ sev = d.get("severity","INFO")
75
+ if i > 0:
76
+ ed = self.G.edges.get((path[i-1], nid), {})
77
+ tech = ed.get("technique","")
78
+ lines.append(f" ↓ {tech}")
79
+ lines.append(f"{colors.get(sev,'')} [{sev}] {label}\033[0m")
80
+ return "\n".join(lines)
81
+
82
+
83
+ attack_graph = AttackGraph()
core/aws_client.py ADDED
@@ -0,0 +1,284 @@
1
+ """
2
+ ExploitGraph - AWS Client Factory
3
+ Production-grade boto3 session management with:
4
+ - Automatic retry with exponential backoff
5
+ - Region fallback (us-east-1 → us-west-2 → eu-west-1)
6
+ - Credential injection from session secrets
7
+ - Standardised response parsing
8
+ - GuardDuty detection-awareness tagging
9
+
10
+ All modules call get_client() — no per-module boto3 setup needed.
11
+ """
12
+ from __future__ import annotations
13
+ import time
14
+ import functools
15
+ from typing import TYPE_CHECKING, Any, Optional
16
+
17
+ if TYPE_CHECKING:
18
+ from core.session_manager import Session
19
+
20
+ try:
21
+ import boto3
22
+ import botocore.config
23
+ import botocore.exceptions
24
+ HAS_BOTO3 = True
25
+ except ImportError:
26
+ HAS_BOTO3 = False
27
+
28
+ DEFAULT_REGION = "us-east-1"
29
+ FALLBACK_REGIONS = ["us-east-1", "us-west-2", "eu-west-1", "ap-southeast-1"]
30
+
31
+ # Retry config: 3 attempts, exponential backoff
32
+ _RETRY_CONFIG = None
33
+ if HAS_BOTO3:
34
+ _RETRY_CONFIG = botocore.config.Config(
35
+ retries={"max_attempts": 3, "mode": "adaptive"},
36
+ connect_timeout=10,
37
+ read_timeout=20,
38
+ )
39
+
40
+ # GuardDuty-awareness: these API calls are known to trigger alerts
41
+ GUARDDUTY_NOISY_APIS = {
42
+ "GetCallerIdentity": "HIGH", # Recon — always logged
43
+ "GetAccountAuthorizationDetails": "CRITICAL", # Full IAM dump — very noisy
44
+ "ListUsers": "MEDIUM",
45
+ "ListRoles": "MEDIUM",
46
+ "CreateAccessKey": "HIGH", # Backdoor credential
47
+ "DeleteTrail": "CRITICAL", # Covering tracks
48
+ "StopLogging": "CRITICAL",
49
+ "PutBucketLogging": "HIGH",
50
+ "DescribeInstances": "LOW", # Normal ops but logged
51
+ }
52
+
53
+ GUARDDUTY_STEALTHY_APIS = {
54
+ "ListBuckets": "no-sign-request avoids auth logs",
55
+ "GetObject": "anonymous S3 access not in CloudTrail",
56
+ "GetBucketAcl": "no-sign-request avoids auth logs",
57
+ }
58
+
59
+
60
+ def get_client(service: str,
61
+ session: "Optional[Session]" = None,
62
+ region: str = DEFAULT_REGION,
63
+ profile: str = "",
64
+ access_key: str = "",
65
+ secret_key: str = "",
66
+ session_token: str = "") -> Any:
67
+ """
68
+ Return a boto3 client. Credential resolution order:
69
+ 1. Explicit access_key/secret_key args
70
+ 2. Session secrets (injected by file_secrets / cloudtrail_analyzer)
71
+ 3. AWS CLI profile
72
+ 4. Default boto3 chain (env vars → ~/.aws → IMDS)
73
+
74
+ Includes automatic retry + exponential backoff.
75
+ Returns None if boto3 unavailable or all credential sources fail.
76
+ """
77
+ if not HAS_BOTO3:
78
+ return None
79
+
80
+ if session and not access_key:
81
+ access_key, secret_key, session_token = _creds_from_session(session)
82
+
83
+ return _build_client(service, region, profile,
84
+ access_key, secret_key, session_token)
85
+
86
+
87
+ def _build_client(service: str, region: str, profile: str,
88
+ access_key: str, secret_key: str,
89
+ session_token: str) -> Any:
90
+ """Build boto3 client with retry config and region fallback."""
91
+ regions_to_try = [region] if region else FALLBACK_REGIONS
92
+
93
+ for reg in regions_to_try:
94
+ try:
95
+ if access_key and secret_key:
96
+ client = boto3.client(
97
+ service,
98
+ region_name=reg,
99
+ aws_access_key_id=access_key,
100
+ aws_secret_access_key=secret_key,
101
+ aws_session_token=session_token or None,
102
+ config=_RETRY_CONFIG,
103
+ )
104
+ elif profile:
105
+ boto_session = boto3.Session(profile_name=profile, region_name=reg)
106
+ client = boto_session.client(service, config=_RETRY_CONFIG)
107
+ else:
108
+ client = boto3.client(service, region_name=reg, config=_RETRY_CONFIG)
109
+ return client
110
+ except Exception:
111
+ continue
112
+
113
+ return None
114
+
115
+
116
+ def get_session(session: "Optional[Session]" = None,
117
+ region: str = DEFAULT_REGION,
118
+ profile: str = "") -> Any:
119
+ """Return a boto3 Session for multi-service use."""
120
+ if not HAS_BOTO3:
121
+ return None
122
+
123
+ access_key, secret_key, session_token = "", "", ""
124
+ if session:
125
+ access_key, secret_key, session_token = _creds_from_session(session)
126
+
127
+ try:
128
+ if access_key and secret_key:
129
+ return boto3.Session(
130
+ region_name=region,
131
+ aws_access_key_id=access_key,
132
+ aws_secret_access_key=secret_key,
133
+ aws_session_token=session_token or None,
134
+ )
135
+ elif profile:
136
+ return boto3.Session(profile_name=profile, region_name=region)
137
+ else:
138
+ return boto3.Session(region_name=region)
139
+ except Exception:
140
+ return None
141
+
142
+
143
+ def safe_call(client: Any, method: str, **kwargs) -> tuple[bool, Any, str]:
144
+ """
145
+ Safely call a boto3 client method with retry + error handling.
146
+
147
+ Returns: (success, response_or_None, error_message)
148
+
149
+ Usage:
150
+ ok, resp, err = safe_call(iam, 'list_users', MaxItems=5)
151
+ if ok:
152
+ users = resp['Users']
153
+ """
154
+ if client is None:
155
+ return False, None, "boto3 client is None"
156
+
157
+ max_attempts = 3
158
+ for attempt in range(max_attempts):
159
+ try:
160
+ fn = getattr(client, method)
161
+ response = fn(**kwargs)
162
+ return True, response, ""
163
+ except botocore.exceptions.ClientError as e:
164
+ code = e.response["Error"]["Code"]
165
+ msg = e.response["Error"]["Message"]
166
+ # Don't retry auth errors
167
+ if code in ("InvalidClientTokenId", "AuthFailure",
168
+ "UnauthorizedAccess", "AccessDenied",
169
+ "ExpiredTokenException"):
170
+ return False, None, f"{code}: {msg}"
171
+ # Retry throttle errors
172
+ if code in ("Throttling", "RequestLimitExceeded",
173
+ "TooManyRequestsException"):
174
+ if attempt < max_attempts - 1:
175
+ time.sleep(2 ** attempt)
176
+ continue
177
+ return False, None, f"{code}: {msg}"
178
+ except Exception as e:
179
+ if attempt < max_attempts - 1:
180
+ time.sleep(1)
181
+ continue
182
+ return False, None, str(e)
183
+
184
+ return False, None, "Max retry attempts exceeded"
185
+
186
+
187
+ def verify_credentials(access_key: str, secret_key: str,
188
+ session_token: str = "",
189
+ region: str = DEFAULT_REGION) -> dict:
190
+ """
191
+ Verify AWS credentials via STS:GetCallerIdentity.
192
+ Returns structured result — never raises.
193
+
194
+ NOTE: GetCallerIdentity IS logged in CloudTrail and flagged by GuardDuty.
195
+ Severity: HIGH (known recon indicator).
196
+ """
197
+ if not HAS_BOTO3:
198
+ return {"valid": False, "error": "boto3 not installed",
199
+ "guardduty_risk": "N/A"}
200
+
201
+ result = {
202
+ "valid": False,
203
+ "arn": "",
204
+ "account": "",
205
+ "user_id": "",
206
+ "username": "",
207
+ "error": "",
208
+ "guardduty_risk": GUARDDUTY_NOISY_APIS.get("GetCallerIdentity", "HIGH"),
209
+ "guardduty_note": "GetCallerIdentity is always logged and a known recon indicator",
210
+ }
211
+
212
+ client = _build_client("sts", region, "", access_key, secret_key, session_token)
213
+ if not client:
214
+ result["error"] = "Could not create STS client"
215
+ return result
216
+
217
+ ok, resp, err = safe_call(client, "get_caller_identity")
218
+ if ok:
219
+ result["valid"] = True
220
+ result["arn"] = resp.get("Arn", "")
221
+ result["account"] = resp.get("Account", "")
222
+ result["user_id"] = resp.get("UserId", "")
223
+ # Extract username from ARN
224
+ arn = result["arn"]
225
+ if "/" in arn:
226
+ result["username"] = arn.split("/")[-1]
227
+ else:
228
+ result["error"] = err
229
+
230
+ return result
231
+
232
+
233
+ def detect_region(access_key: str, secret_key: str,
234
+ session_token: str = "") -> str:
235
+ """
236
+ Detect the primary region for these credentials.
237
+ Falls back to us-east-1 if detection fails.
238
+ """
239
+ if not HAS_BOTO3:
240
+ return DEFAULT_REGION
241
+
242
+ for region in FALLBACK_REGIONS:
243
+ client = _build_client("sts", region, "", access_key, secret_key, session_token)
244
+ ok, resp, _ = safe_call(client, "get_caller_identity")
245
+ if ok:
246
+ return region
247
+
248
+ return DEFAULT_REGION
249
+
250
+
251
+ def _creds_from_session(session: "Session") -> tuple[str, str, str]:
252
+ """Extract best available AWS credentials from session secrets."""
253
+ access_key = secret_key = session_token = ""
254
+ for s in session.secrets:
255
+ t = s.get("secret_type", "")
256
+ v = s.get("value", "")
257
+ if t == "AWS_ACCESS_KEY" and not access_key:
258
+ access_key = v
259
+ elif t == "AWS_SECRET_KEY" and not secret_key:
260
+ secret_key = v
261
+ elif t == "AWS_SESSION_TOKEN" and not session_token:
262
+ session_token = v
263
+ return access_key, secret_key, session_token
264
+
265
+
266
+ def guardduty_risk(api_name: str) -> tuple[str, str]:
267
+ """
268
+ Return (risk_level, explanation) for a given API call.
269
+ Used by modules to annotate findings with detection awareness.
270
+ """
271
+ if api_name in GUARDDUTY_NOISY_APIS:
272
+ return GUARDDUTY_NOISY_APIS[api_name], "GuardDuty: known high-signal event"
273
+ if api_name in GUARDDUTY_STEALTHY_APIS:
274
+ return "LOW", GUARDDUTY_STEALTHY_APIS[api_name]
275
+ return "LOW", "Not a known GuardDuty trigger"
276
+
277
+
278
+ def is_available() -> bool:
279
+ return HAS_BOTO3
280
+
281
+
282
+ def has_credentials(session: "Session") -> bool:
283
+ ak, sk, _ = _creds_from_session(session)
284
+ return bool(ak and sk)
core/config.py ADDED
@@ -0,0 +1,83 @@
1
+ """ExploitGraph - YAML configuration loader."""
2
+ from __future__ import annotations
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ BASE_DIR = Path(__file__).parent.parent
8
+ CONFIG_FILE = BASE_DIR / "config.yaml"
9
+
10
+ DEFAULTS = {
11
+ "framework": {"version":"1.0.0","max_threads":10,"timeout":8,"retry":2,
12
+ "user_agent":"ExploitGraph/1.0 (Security Research)",
13
+ "output_dir":"reports","log_level":"INFO","verify_ssl":False},
14
+ "wordlists": {"http_paths":"data/wordlists/common_paths.txt",
15
+ "s3_buckets":"data/wordlists/s3_buckets.txt",
16
+ "backup_files":"data/wordlists/backup_files.txt",
17
+ "subdomains":"data/wordlists/subdomains.txt"},
18
+ "aws": {"profile":None,"region":"us-east-1","enabled":False},
19
+ "reporting": {"formats":["html","json"],"cvss_minimum":0.0,"open_browser":False},
20
+ }
21
+
22
+
23
+ class Config:
24
+ def __init__(self):
25
+ import copy; self._data = copy.deepcopy(DEFAULTS)
26
+ self._load()
27
+
28
+ def _load(self):
29
+ try:
30
+ import yaml
31
+ if CONFIG_FILE.exists():
32
+ with open(CONFIG_FILE) as f:
33
+ self._merge(self._data, yaml.safe_load(f) or {})
34
+ except ImportError:
35
+ pass
36
+ for k, v in os.environ.items():
37
+ if k.startswith("EG_"):
38
+ parts = k[3:].lower().split("_", 1)
39
+ if len(parts) == 2 and parts[0] in self._data:
40
+ if isinstance(self._data[parts[0]], dict):
41
+ self._data[parts[0]][parts[1]] = v
42
+
43
+ def _merge(self, base, override):
44
+ for k, v in override.items():
45
+ if k in base and isinstance(base[k], dict) and isinstance(v, dict):
46
+ self._merge(base[k], v)
47
+ else:
48
+ base[k] = v
49
+
50
+ def __getitem__(self, k): return self._data[k]
51
+
52
+ def get(self, dotpath: str, default: Any = None) -> Any:
53
+ node = self._data
54
+ for p in dotpath.split("."):
55
+ if not isinstance(node, dict) or p not in node: return default
56
+ node = node[p]
57
+ return node
58
+
59
+ def set(self, dotpath: str, value: Any):
60
+ parts = dotpath.split(".")
61
+ node = self._data
62
+ for p in parts[:-1]: node = node.setdefault(p, {})
63
+ node[parts[-1]] = value
64
+
65
+ def wordlist_path(self, name: str) -> Path:
66
+ rel = self.get(f"wordlists.{name}", "")
67
+ return BASE_DIR / rel if rel else BASE_DIR / "data" / "wordlists" / f"{name}.txt"
68
+
69
+ @property
70
+ def timeout(self) -> int: return int(self.get("framework.timeout", 8))
71
+ @property
72
+ def max_threads(self) -> int: return int(self.get("framework.max_threads", 10))
73
+ @property
74
+ def user_agent(self) -> str: return self.get("framework.user_agent","ExploitGraph/1.0")
75
+ @property
76
+ def verify_ssl(self) -> bool: return bool(self.get("framework.verify_ssl", False))
77
+ @property
78
+ def aws_enabled(self) -> bool:
79
+ try: import boto3; return bool(self.get("aws.enabled", False))
80
+ except ImportError: return False
81
+
82
+
83
+ cfg = Config()