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/correlator.py ADDED
@@ -0,0 +1,476 @@
1
+ """
2
+ ExploitGraph - Data Correlation Engine
3
+ Links findings across modules into a coherent attack narrative.
4
+
5
+ Instead of treating findings independently, this engine:
6
+ - Connects S3 exposure → CloudTrail logs → credentials → IAM identity
7
+ - Builds a structured attack timeline
8
+ - Generates the "attack story" for the final report
9
+ - Prioritizes attack paths by impact
10
+ - Maps credential usage across modules
11
+
12
+ This is what transforms raw findings into intelligence.
13
+ """
14
+ from __future__ import annotations
15
+ from typing import TYPE_CHECKING, Optional
16
+ from dataclasses import dataclass, field
17
+
18
+ if TYPE_CHECKING:
19
+ from core.session_manager import Session
20
+
21
+
22
+ @dataclass
23
+ class AttackStep:
24
+ """A single step in the correlated attack chain."""
25
+ step: int
26
+ title: str
27
+ description: str
28
+ module: str
29
+ severity: str
30
+ evidence: list[str] = field(default_factory=list)
31
+ leads_to: list[str] = field(default_factory=list)
32
+ mitre: str = ""
33
+ aws_parallel:str = ""
34
+ guardduty: bool = False # Would this trigger GuardDuty?
35
+
36
+
37
+ @dataclass
38
+ class CredentialNode:
39
+ """A discovered credential with all its associated context."""
40
+ key_id: str
41
+ secret_key: str = ""
42
+ session_token:str = ""
43
+ source: str = "" # Where it came from (S3, CloudTrail, .env, etc.)
44
+ iam_identity: str = "" # ARN once validated
45
+ account_id: str = ""
46
+ username: str = ""
47
+ privilege: str = "unknown" # admin | user | readonly | unknown
48
+ valid: Optional[bool] = None
49
+ permissions: list[str] = field(default_factory=list)
50
+ services: list[str] = field(default_factory=list)
51
+
52
+
53
+ class CorrelationEngine:
54
+ """
55
+ Correlates findings across all modules into a structured attack narrative.
56
+ Call correlate(session) after all modules have run to build the full picture.
57
+ """
58
+
59
+ def correlate(self, session: "Session") -> dict:
60
+ """
61
+ Main entry point. Returns a structured correlation report.
62
+ """
63
+ narrative = self._build_narrative(session)
64
+ credentials = self._extract_credential_nodes(session)
65
+ timeline = self._build_timeline(session)
66
+ attack_path = self._build_attack_path(session, narrative)
67
+ impact = self._assess_impact(session, credentials)
68
+
69
+ return {
70
+ "narrative": narrative,
71
+ "credentials": [self._cred_to_dict(c) for c in credentials],
72
+ "timeline": timeline,
73
+ "attack_path": attack_path,
74
+ "impact": impact,
75
+ "summary": self._executive_summary(session, credentials, impact),
76
+ }
77
+
78
+ # ── Narrative Building ─────────────────────────────────────────────────────
79
+
80
+ def _build_narrative(self, session: "Session") -> list[dict]:
81
+ """
82
+ Build ordered attack steps from session findings.
83
+ Each step connects to the next — this IS the attack story.
84
+ """
85
+ steps = []
86
+ step_num = 0
87
+
88
+ def add_step(title, desc, module, severity, evidence=None,
89
+ leads_to=None, mitre="", aws="", guardduty=False):
90
+ nonlocal step_num
91
+ step_num += 1
92
+ steps.append({
93
+ "step": step_num,
94
+ "title": title,
95
+ "description": desc,
96
+ "module": module,
97
+ "severity": severity,
98
+ "evidence": evidence or [],
99
+ "leads_to": leads_to or [],
100
+ "mitre": mitre,
101
+ "aws_parallel":aws,
102
+ "guardduty_risk": guardduty,
103
+ })
104
+
105
+ # Step 1: Initial reconnaissance
106
+ endpoints = session.endpoints
107
+ if endpoints:
108
+ interesting = [e for e in endpoints if e.get("interesting")]
109
+ add_step(
110
+ title = "Target Reconnaissance",
111
+ desc = f"HTTP enumeration discovered {len(endpoints)} endpoints on the target. "
112
+ f"Server fingerprinting revealed cloud infrastructure.",
113
+ module = "discovery/http_enum",
114
+ severity = "INFO",
115
+ evidence = [e["url"] for e in endpoints[:5]],
116
+ leads_to = ["Cloud Storage Exposure"],
117
+ mitre = "T1595.003",
118
+ aws = "Equivalent to nmap/curl against EC2 or CloudFront",
119
+ guardduty = False,
120
+ )
121
+
122
+ # Step 2: Cloud storage exposure
123
+ buckets = [f for f in session.exposed_files if
124
+ f.get("source") in ("s3_enum",) or
125
+ "AWSLogs" in f.get("path", "") or
126
+ f.get("path", "").endswith(".gz")]
127
+ if buckets or any("s3_enum" in f.get("source","") for f in session.exposed_files):
128
+ all_files = session.exposed_files
129
+ ct_files = [f for f in all_files if "CloudTrail" in f.get("path","") or
130
+ f.get("path","").endswith(".json.gz")]
131
+ add_step(
132
+ title = "Cloud Storage Exposure",
133
+ desc = f"Public S3 bucket discovered containing {len(all_files)} accessible files. "
134
+ f"{len(ct_files)} CloudTrail audit log files found — "
135
+ "these logs record every AWS API call made in the account.",
136
+ module = "cloud/s3_enum",
137
+ severity = "CRITICAL",
138
+ evidence = [f.get("url", f.get("path", "")) for f in all_files[:5]],
139
+ leads_to = ["CloudTrail Log Analysis"] if ct_files else ["Secret Extraction"],
140
+ mitre = "T1530",
141
+ aws = "aws s3 ls s3://bucket --no-sign-request",
142
+ guardduty = True, # Anonymous S3 access triggers GuardDuty
143
+ )
144
+
145
+ # Step 3: CloudTrail analysis
146
+ ct_findings = [f for f in session.findings if "CloudTrail" in f.get("title","")]
147
+ if ct_findings:
148
+ creds_in_ct = [s for s in session.secrets
149
+ if "CloudTrail" in s.get("source","") or
150
+ "cloudtrail" in s.get("source","").lower()]
151
+ add_step(
152
+ title = "CloudTrail Log Analysis",
153
+ desc = f"AWS CloudTrail audit logs parsed from downloaded .json.gz files. "
154
+ f"Log analysis revealed {len(creds_in_ct)} AWS access key(s) and "
155
+ f"detailed API call history including IAM usernames and source IPs.",
156
+ module = "cloud/cloudtrail_analyzer",
157
+ severity = "CRITICAL",
158
+ evidence = [f["title"] for f in ct_findings[:3]],
159
+ leads_to = ["Credential Validation"] if creds_in_ct else [],
160
+ mitre = "T1530,T1552.005",
161
+ aws = "CloudTrail logs reveal complete AWS API history",
162
+ guardduty = False, # Reading already-public logs
163
+ )
164
+
165
+ # Step 4: Secret extraction
166
+ secrets_by_type: dict[str, int] = {}
167
+ for s in session.secrets:
168
+ t = s.get("secret_type","")
169
+ secrets_by_type[t] = secrets_by_type.get(t, 0) + 1
170
+
171
+ if session.secrets:
172
+ add_step(
173
+ title = "Credential Extraction",
174
+ desc = f"Secret scanning of {len(session.exposed_files)} exposed files "
175
+ f"yielded {len(session.secrets)} credentials across "
176
+ f"{len(secrets_by_type)} types: {', '.join(secrets_by_type.keys())}.",
177
+ module = "secrets/file_secrets",
178
+ severity = "CRITICAL",
179
+ evidence = [f"{s['secret_type']}: {s['value'][:20]}..." for s in session.secrets[:4]],
180
+ leads_to = ["AWS Credential Validation"] if any(
181
+ s["secret_type"] in ("AWS_ACCESS_KEY","AWS_SECRET_KEY")
182
+ for s in session.secrets
183
+ ) else [],
184
+ mitre = "T1552.001",
185
+ aws = "Secrets exposed in S3 instead of AWS Secrets Manager",
186
+ guardduty = False,
187
+ )
188
+
189
+ # Step 5: Credential validation
190
+ validated = [r for r in session.exploit_results
191
+ if "credential_validator" in r.get("module","") and r.get("success")]
192
+ if validated:
193
+ cred_data = validated[0].get("data", {})
194
+ arn = cred_data.get("arn", "unknown")
195
+ add_step(
196
+ title = f"AWS Credential Validated: {arn.split('/')[-1] if arn != 'unknown' else 'IAM User'}",
197
+ desc = f"Discovered AWS credentials confirmed valid via STS GetCallerIdentity. "
198
+ f"Identity: {arn}. "
199
+ f"This grants real AWS API access equivalent to that IAM identity.",
200
+ module = "cloud/aws_credential_validator",
201
+ severity = "CRITICAL",
202
+ evidence = [f"ARN: {arn}", f"Account: {cred_data.get('account','')}"],
203
+ leads_to = ["IAM Permission Enumeration", "Privilege Escalation"],
204
+ mitre = "T1078.004",
205
+ aws = "aws sts get-caller-identity (stolen credentials)",
206
+ guardduty = True, # GetCallerIdentity from unknown IP triggers GuardDuty
207
+ )
208
+
209
+ # Step 6: IAM enumeration
210
+ iam_findings = [f for f in session.findings
211
+ if any(kw in f.get("module","")
212
+ for kw in ("iam_enum","iam_privilege"))]
213
+ if iam_findings:
214
+ privesc = [f for f in iam_findings if "escal" in f.get("title","").lower() or
215
+ f.get("severity") == "CRITICAL"]
216
+ add_step(
217
+ title = "IAM Permission Enumeration & Privilege Escalation",
218
+ desc = f"IAM enumeration with stolen credentials revealed {len(iam_findings)} "
219
+ f"security issues. {len(privesc)} privilege escalation path(s) identified.",
220
+ module = "cloud/iam_privilege_escalation",
221
+ severity = "CRITICAL" if privesc else "HIGH",
222
+ evidence = [f["title"] for f in iam_findings[:4]],
223
+ leads_to = ["Full Account Compromise"] if privesc else [],
224
+ mitre = "T1078.004,T1548",
225
+ aws = "aws iam list-attached-user-policies (unauthorized)",
226
+ guardduty = True, # IAM enumeration is very noisy
227
+ )
228
+
229
+ # Step 7: Full compromise
230
+ critical_findings = [f for f in session.findings if f.get("severity") == "CRITICAL"]
231
+ if len(critical_findings) >= 3 and session.secrets:
232
+ add_step(
233
+ title = "Full AWS Account Compromise",
234
+ desc = "Complete attack chain demonstrated: misconfigured S3 bucket exposed "
235
+ "CloudTrail logs → IAM credentials extracted → credentials validated → "
236
+ "IAM permissions enumerated. Account is fully compromised.",
237
+ module = "framework",
238
+ severity = "CRITICAL",
239
+ evidence = [f"Total CRITICAL findings: {len(critical_findings)}",
240
+ f"Credentials extracted: {len(session.secrets)}",
241
+ f"Validated credentials: {len(validated)}"],
242
+ leads_to = [],
243
+ mitre = "T1078.004,T1530,T1552",
244
+ aws = "Complete kill chain — Capital One breach pattern",
245
+ guardduty = True,
246
+ )
247
+
248
+ return steps
249
+
250
+ # ── Credential Nodes ───────────────────────────────────────────────────────
251
+
252
+ def _extract_credential_nodes(self, session: "Session") -> list[CredentialNode]:
253
+ """Build rich credential objects by correlating secrets with validation results."""
254
+ nodes = []
255
+ # Pair access keys with secret keys
256
+ access_keys = [s for s in session.secrets if s["secret_type"] == "AWS_ACCESS_KEY"]
257
+ secret_keys = [s for s in session.secrets if s["secret_type"] == "AWS_SECRET_KEY"]
258
+
259
+ for ak in access_keys:
260
+ node = CredentialNode(
261
+ key_id = ak["value"],
262
+ source = ak.get("source", ""),
263
+ )
264
+ # Find matching secret key
265
+ for sk in secret_keys:
266
+ node.secret_key = sk["value"]
267
+ break
268
+
269
+ # Enrich from validation results
270
+ for result in session.exploit_results:
271
+ if "credential_validator" in result.get("module", "") and result.get("success"):
272
+ data = result.get("data", {})
273
+ node.iam_identity = data.get("arn", "")
274
+ node.account_id = data.get("account", "")
275
+ node.username = node.iam_identity.split("/")[-1] if node.iam_identity else ""
276
+ node.privilege = data.get("privilege", "unknown")
277
+ node.valid = True
278
+ node.services = data.get("accessible_services", [])
279
+
280
+ # Check CloudTrail findings for this key
281
+ for f in session.findings:
282
+ if ak["value"] in f.get("evidence", "") and "CloudTrail" in f.get("title", ""):
283
+ if not node.iam_identity:
284
+ # Try to extract from evidence
285
+ import re
286
+ arn_match = re.search(r'arn:aws:\S+', f.get("evidence", ""))
287
+ if arn_match:
288
+ node.iam_identity = arn_match.group()
289
+
290
+ nodes.append(node)
291
+
292
+ return nodes
293
+
294
+ # ── Timeline ───────────────────────────────────────────────────────────────
295
+
296
+ def _build_timeline(self, session: "Session") -> list[dict]:
297
+ """Build chronological timeline of findings and events."""
298
+ events = []
299
+ for f in session.findings:
300
+ events.append({
301
+ "time": f.get("created_at", "")[:19].replace("T", " "),
302
+ "type": "finding",
303
+ "severity": f.get("severity", "INFO"),
304
+ "title": f.get("title", ""),
305
+ "module": f.get("module", ""),
306
+ })
307
+ for s in session.secrets:
308
+ events.append({
309
+ "time": s.get("created_at", "")[:19].replace("T", " "),
310
+ "type": "secret",
311
+ "severity": s.get("severity", "HIGH"),
312
+ "title": f"Secret found: {s['secret_type']}",
313
+ "module": "secrets",
314
+ })
315
+ # Sort by time
316
+ events.sort(key=lambda x: x.get("time", ""))
317
+ return events
318
+
319
+ # ── Attack Path ────────────────────────────────────────────────────────────
320
+
321
+ def _build_attack_path(self, session: "Session", narrative: list[dict]) -> dict:
322
+ """Build the primary attack path — the highest-impact chain."""
323
+ if not narrative:
324
+ return {}
325
+
326
+ steps = [n["title"] for n in narrative]
327
+ total_severity = sum(
328
+ {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1, "INFO": 0}.get(
329
+ n.get("severity", "INFO"), 0
330
+ )
331
+ for n in narrative
332
+ )
333
+ guardduty_steps = [n["title"] for n in narrative if n.get("guardduty_risk")]
334
+
335
+ return {
336
+ "steps": steps,
337
+ "length": len(steps),
338
+ "total_impact": total_severity,
339
+ "guardduty_risks": guardduty_steps,
340
+ "is_complete": len(steps) >= 4,
341
+ "chain_summary": " → ".join(steps),
342
+ }
343
+
344
+ # ── Impact Assessment ──────────────────────────────────────────────────────
345
+
346
+ def _assess_impact(self, session: "Session",
347
+ credentials: list[CredentialNode]) -> dict:
348
+ """Assess the real-world impact of what was found."""
349
+ has_valid_creds = any(c.valid for c in credentials)
350
+ admin_creds = any(c.privilege == "admin" for c in credentials)
351
+
352
+ impact_level = "LOW"
353
+ impact_desc = "Limited exposure detected."
354
+
355
+ if admin_creds:
356
+ impact_level = "CRITICAL"
357
+ impact_desc = (
358
+ "Administrative AWS credentials are compromised. "
359
+ "Attacker has full control of the AWS account including "
360
+ "all services, data, and the ability to create backdoor accounts."
361
+ )
362
+ elif has_valid_creds:
363
+ impact_level = "HIGH"
364
+ impact_desc = (
365
+ "Valid AWS credentials are compromised. "
366
+ "Attacker can access all services permitted to this IAM identity. "
367
+ "Lateral movement to other services is possible."
368
+ )
369
+ elif session.secrets:
370
+ impact_level = "HIGH"
371
+ impact_desc = (
372
+ f"{len(session.secrets)} credentials were extracted from exposed files. "
373
+ "Immediate rotation required."
374
+ )
375
+ elif session.exposed_files:
376
+ impact_level = "MEDIUM"
377
+ impact_desc = (
378
+ f"{len(session.exposed_files)} sensitive files are publicly accessible. "
379
+ "Data exposure and credential leakage risk."
380
+ )
381
+
382
+ return {
383
+ "level": impact_level,
384
+ "description": impact_desc,
385
+ "valid_credentials":has_valid_creds,
386
+ "admin_access": admin_creds,
387
+ "data_exposed": len(session.exposed_files),
388
+ "secrets_found": len(session.secrets),
389
+ "lateral_movement": has_valid_creds,
390
+ "immediate_actions": self._remediation_priority(session, credentials),
391
+ }
392
+
393
+ def _remediation_priority(self, session: "Session",
394
+ credentials: list[CredentialNode]) -> list[str]:
395
+ """Return prioritized list of immediate actions."""
396
+ actions = []
397
+ if any(c.valid for c in credentials):
398
+ for c in credentials:
399
+ if c.valid and c.key_id:
400
+ actions.append(
401
+ f"IMMEDIATE: Deactivate IAM key {c.key_id[:12]}... "
402
+ f"(aws iam update-access-key --access-key-id {c.key_id} --status Inactive)"
403
+ )
404
+ if session.exposed_files:
405
+ bucket_sources = {f.get("source","") for f in session.exposed_files}
406
+ if "s3_enum" in bucket_sources:
407
+ actions.append(
408
+ "IMMEDIATE: Enable S3 Block Public Access on all buckets "
409
+ "(aws s3api put-public-access-block --bucket BUCKET ...)"
410
+ )
411
+ if session.secrets:
412
+ actions.append("HIGH: Rotate all extracted credentials — assume they are compromised")
413
+ actions.append("HIGH: Enable CloudTrail in all regions if not already active")
414
+ actions.append("MEDIUM: Enable GuardDuty for ongoing threat detection")
415
+ actions.append("MEDIUM: Review IAM policies — apply least-privilege principle")
416
+ return actions
417
+
418
+ # ── Executive Summary ──────────────────────────────────────────────────────
419
+
420
+ def _executive_summary(self, session: "Session",
421
+ credentials: list[CredentialNode],
422
+ impact: dict) -> str:
423
+ """One-paragraph executive summary for the report header."""
424
+ target = session.target
425
+ n_findings = len(session.findings)
426
+ n_secrets = len(session.secrets)
427
+ n_files = len(session.exposed_files)
428
+ impact_lvl = impact.get("level", "UNKNOWN")
429
+
430
+ parts = [
431
+ f"ExploitGraph security assessment of {target} identified "
432
+ f"{n_findings} findings with overall impact level {impact_lvl}."
433
+ ]
434
+
435
+ if n_files:
436
+ parts.append(
437
+ f"{n_files} sensitive files were accessible from publicly exposed "
438
+ f"cloud storage without authentication."
439
+ )
440
+ if n_secrets:
441
+ parts.append(
442
+ f"Secret scanning extracted {n_secrets} credentials from these files, "
443
+ f"including AWS access keys and API tokens."
444
+ )
445
+ if credentials:
446
+ valid = [c for c in credentials if c.valid]
447
+ if valid:
448
+ arns = [c.iam_identity for c in valid if c.iam_identity]
449
+ parts.append(
450
+ f"{'Credential validation confirmed' if valid else 'Extracted credentials'} "
451
+ f"{'active access as: ' + ', '.join(arns[:2]) if arns else 'valid AWS access'}. "
452
+ "Immediate credential rotation is required."
453
+ )
454
+
455
+ parts.append(impact.get("description", ""))
456
+ return " ".join(parts)
457
+
458
+ # ── Helpers ────────────────────────────────────────────────────────────────
459
+
460
+ @staticmethod
461
+ def _cred_to_dict(c: CredentialNode) -> dict:
462
+ return {
463
+ "key_id": c.key_id,
464
+ "secret_key": c.secret_key[:8] + "..." if c.secret_key else "",
465
+ "source": c.source,
466
+ "iam_identity": c.iam_identity,
467
+ "account_id": c.account_id,
468
+ "username": c.username,
469
+ "privilege": c.privilege,
470
+ "valid": c.valid,
471
+ "services": c.services,
472
+ }
473
+
474
+
475
+ # Global singleton
476
+ correlator = CorrelationEngine()