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.
- core/__init__.py +0 -0
- core/attack_graph.py +83 -0
- core/aws_client.py +284 -0
- core/config.py +83 -0
- core/console.py +469 -0
- core/context_engine.py +172 -0
- core/correlator.py +476 -0
- core/http_client.py +243 -0
- core/logger.py +97 -0
- core/module_loader.py +69 -0
- core/risk_engine.py +47 -0
- core/session_manager.py +254 -0
- exploitgraph-1.0.0.dist-info/METADATA +429 -0
- exploitgraph-1.0.0.dist-info/RECORD +42 -0
- exploitgraph-1.0.0.dist-info/WHEEL +5 -0
- exploitgraph-1.0.0.dist-info/entry_points.txt +2 -0
- exploitgraph-1.0.0.dist-info/licenses/LICENSE +21 -0
- exploitgraph-1.0.0.dist-info/top_level.txt +2 -0
- modules/__init__.py +0 -0
- modules/base.py +82 -0
- modules/cloud/__init__.py +0 -0
- modules/cloud/aws_credential_validator.py +340 -0
- modules/cloud/azure_enum.py +289 -0
- modules/cloud/cloudtrail_analyzer.py +494 -0
- modules/cloud/gcp_enum.py +272 -0
- modules/cloud/iam_enum.py +321 -0
- modules/cloud/iam_privilege_escalation.py +515 -0
- modules/cloud/metadata_check.py +315 -0
- modules/cloud/s3_enum.py +469 -0
- modules/discovery/__init__.py +0 -0
- modules/discovery/http_enum.py +235 -0
- modules/discovery/subdomain_enum.py +260 -0
- modules/exploitation/__init__.py +0 -0
- modules/exploitation/api_exploit.py +403 -0
- modules/exploitation/jwt_attack.py +346 -0
- modules/exploitation/ssrf_scanner.py +258 -0
- modules/reporting/__init__.py +0 -0
- modules/reporting/html_report.py +446 -0
- modules/reporting/json_export.py +107 -0
- modules/secrets/__init__.py +0 -0
- modules/secrets/file_secrets.py +358 -0
- 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()
|