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
@@ -0,0 +1,42 @@
1
+ core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ core/attack_graph.py,sha256=Osej6YKoPJ6q5wrqcW0hQLgUsn95XV04HeqItOdzqIk,3856
3
+ core/aws_client.py,sha256=D70-URLTRQbUfufCaKLeY-So-QPwuFSUJtMF_COuaEI,9796
4
+ core/config.py,sha256=FgkykICftoB1slI3mzAls0kj1VOPYhtl2lqcExGUPLU,3086
5
+ core/console.py,sha256=MKeGRWPwNvQqM1uwZcGmIK3hxxBX6_W4BeNRR73NpnQ,23051
6
+ core/context_engine.py,sha256=6wVx-TZhAoY83xG8tIul_mKzmrK_tdmxRHzIieN2DK4,7226
7
+ core/correlator.py,sha256=R63X_eR08UMuyR0IOjam5YKxZ-7GhurS20ibSAlfm3o,22238
8
+ core/http_client.py,sha256=LY8W_To2vamHheCvpb9f1_iCJP8TDuScmbuPjSLQ_D8,8595
9
+ core/logger.py,sha256=iazXJqI-OVLCQtIovjzD-b30cjzyRQWWsVhiZN9ReOo,3893
10
+ core/module_loader.py,sha256=2MVVXcZUFdeM4PmY82FP7Pn22fMVxRjXdwRpELmfPkw,2886
11
+ core/risk_engine.py,sha256=2bz6sPjKVZjNQdRqrYzaxXy8FUuK-kEe4B4NVnTx2Dw,1968
12
+ core/session_manager.py,sha256=FdHzVADV_jWIEzYJDqMOV8CLBcg0hArYunnFaBQ58rQ,10688
13
+ exploitgraph-1.0.0.dist-info/licenses/LICENSE,sha256=Nt_gc-40jBtmfDF4rIXJXB_mQ_ApZdn49uTSXrhv5w0,1070
14
+ modules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ modules/base.py,sha256=oiopwu3TOxR_iYKFx1vso0krmWnv_Cu06FNlUKiQAvg,3283
16
+ modules/cloud/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ modules/cloud/aws_credential_validator.py,sha256=6JAyDr4AaIdVBfCocb6SJMOlzz3dZ_Hyii5Nbi91riw,15443
18
+ modules/cloud/azure_enum.py,sha256=oqdjWM19gsGce1CmzvnLdggKcvSMhCw6r5_XJ_O7tZU,13277
19
+ modules/cloud/cloudtrail_analyzer.py,sha256=B6QGJqexJhJjCATgCiDoRlrkv8FwuKafeWNIeylbjpM,22866
20
+ modules/cloud/gcp_enum.py,sha256=5HC2Ln0Bqsj32gIbcEmQ1nGn4-eyOAvuz-TTh3i03bY,11758
21
+ modules/cloud/iam_enum.py,sha256=dStnYqqmKY6uWSPrgflqqlbWq4_Dr2dameLxPtYJ838,15591
22
+ modules/cloud/iam_privilege_escalation.py,sha256=Kfqz-sHUaom_uV29cVAjQ40VZWw267oK-zRBxlFhieU,21593
23
+ modules/cloud/metadata_check.py,sha256=fXG0UKSVD5P1EUqVa2Y79XhvMkmVJMNd_zIjUkeoXqA,14583
24
+ modules/cloud/s3_enum.py,sha256=YQdCkyjXWS31EgRWccfUZswktbuC2MDu5gZqZAalYW4,22203
25
+ modules/discovery/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ modules/discovery/http_enum.py,sha256=Vup-c49TOamzgHHbm6prlnfxb22yZ_fz7YXXg3qTTeo,10866
27
+ modules/discovery/subdomain_enum.py,sha256=WTfQn3rQWMutIkFbDVLgUFjrDKmhOmx8YEeC8vRzGmQ,10548
28
+ modules/exploitation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ modules/exploitation/api_exploit.py,sha256=ldfvTS66oFUcTcM1SotWTEhmQnuLizjPW6S6msJ7x8w,19526
30
+ modules/exploitation/jwt_attack.py,sha256=qXMTqnGTBXCGKvpCceFaFskOQgUqmFQChpCIH8w5_R8,15591
31
+ modules/exploitation/ssrf_scanner.py,sha256=D8RtgYEftvCJUXiCk8unyPEyXC_9VtNAZUxD4zvGDbg,11402
32
+ modules/reporting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ modules/reporting/html_report.py,sha256=kgHxR_VXJni9c4NrVSpFrV0-VvQr782lVP1yeLqLNAY,24944
34
+ modules/reporting/json_export.py,sha256=KHn5kguSGYRZw213PX1CBsk-ylqzixSpm-Eh0YwEd1s,4078
35
+ modules/secrets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
+ modules/secrets/file_secrets.py,sha256=ic0NhwLbCvB1ARQMT8cyYUNmjrIWVYE0Picv-XPpR-g,16820
37
+ modules/secrets/git_secrets.py,sha256=HcNqg7YXSqf-XkYiVOoH_dxYeO9VyYHe-Ylz3ehgfC0,11418
38
+ exploitgraph-1.0.0.dist-info/METADATA,sha256=k8sSj2WrymQ-FxvnFUAHxjzkxj_D5vuM44N5LPM766o,15193
39
+ exploitgraph-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
40
+ exploitgraph-1.0.0.dist-info/entry_points.txt,sha256=dG0vV2jtfX9ZXJ7upLKyG6pgbPJN1Ox2Rit9PO0BjjU,51
41
+ exploitgraph-1.0.0.dist-info/top_level.txt,sha256=dXGiKnRy6F5ai_9PsEJ3Wl2UDtLkQJC3z8Q1MZJvAtw,13
42
+ exploitgraph-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ exploitgraph = exploitgraph:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Prajwal Pawar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ core
2
+ modules
modules/__init__.py ADDED
File without changes
modules/base.py ADDED
@@ -0,0 +1,82 @@
1
+ """ExploitGraph - BaseModule: the contract every module must fulfill."""
2
+ from __future__ import annotations
3
+ import abc, time, datetime
4
+ from typing import TYPE_CHECKING, Any
5
+ if TYPE_CHECKING:
6
+ from core.session_manager import Session
7
+
8
+
9
+ class ModuleResult:
10
+ def __init__(self, success: bool, data: dict = None, error: str = ""):
11
+ self.success = success
12
+ self.data = data or {}
13
+ self.error = error
14
+ self.timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
15
+ def __bool__(self): return self.success
16
+
17
+
18
+ class BaseModule(abc.ABC):
19
+ """
20
+ Abstract base class for all ExploitGraph modules.
21
+ Modules communicate ONLY via the shared Session object.
22
+ Modules must NEVER import or call other modules directly.
23
+ """
24
+ NAME: str = "unnamed_module"
25
+ DESCRIPTION: str = "No description"
26
+ AUTHOR: str = "ExploitGraph Community"
27
+ VERSION: str = "1.0.0"
28
+ CATEGORY: str = "misc"
29
+ SEVERITY: str = "INFO"
30
+ MITRE: list[str] = []
31
+ AWS_PARALLEL: str = ""
32
+ OPTIONS: dict = {}
33
+
34
+ def __init__(self):
35
+ self._options: dict[str, Any] = {k: v["default"] for k,v in self.OPTIONS.items()}
36
+ self._start: float = 0.0
37
+ self._elapsed: float = 0.0
38
+
39
+ @abc.abstractmethod
40
+ def run(self, session: "Session") -> ModuleResult: ...
41
+
42
+ def validate(self, session: "Session") -> tuple[bool, str]:
43
+ for name, meta in self.OPTIONS.items():
44
+ if meta.get("required") and not self._options.get(name):
45
+ return False, f"Required option '{name}' not set. set {name} <value>"
46
+ return True, ""
47
+
48
+ def report(self, session: "Session") -> dict:
49
+ return {"module": self.NAME, "category": self.CATEGORY,
50
+ "severity": self.SEVERITY, "elapsed": self._elapsed}
51
+
52
+ def set_option(self, key: str, value: Any) -> tuple[bool, str]:
53
+ if key.upper() not in self.OPTIONS:
54
+ return False, f"Unknown option: {key}. Valid: {list(self.OPTIONS.keys())}"
55
+ self._options[key.upper()] = value
56
+ return True, f"{key.upper()} => {value}"
57
+
58
+ def get_option(self, key: str, default: Any = None) -> Any:
59
+ return self._options.get(key.upper(), default)
60
+
61
+ def show_options(self) -> list[tuple]:
62
+ return [(name, str(self._options.get(name, meta.get("default",""))),
63
+ "yes" if meta.get("required") else "no", meta.get("description",""))
64
+ for name, meta in self.OPTIONS.items()]
65
+
66
+ def _timer_start(self): self._start = time.monotonic()
67
+ def _timer_stop(self) -> float:
68
+ self._elapsed = round(time.monotonic() - self._start, 2)
69
+ return self._elapsed
70
+
71
+ @staticmethod
72
+ def info(m): from core.logger import log; log.module_info(m)
73
+ @staticmethod
74
+ def ok(m): from core.logger import log; log.module_success(m)
75
+ @staticmethod
76
+ def warn(m): from core.logger import log; log.module_warn(m)
77
+ @staticmethod
78
+ def err(m): from core.logger import log; log.module_error(m)
79
+ @staticmethod
80
+ def critical(m): from core.logger import log; log.module_critical(m)
81
+
82
+ def __repr__(self): return f"<Module {self.CATEGORY}/{self.NAME} v{self.VERSION}>"
File without changes
@@ -0,0 +1,340 @@
1
+ """
2
+ ExploitGraph Module: AWS Credential Validator
3
+ Category: cloud
4
+
5
+ Validates AWS credentials discovered by file_secrets or cloudtrail_analyzer.
6
+ Uses read-only STS + IAM calls to:
7
+ 1. Confirm the key is still active (GetCallerIdentity)
8
+ 2. Enumerate accessible services and permissions
9
+ 3. Detect privilege level (admin vs limited)
10
+ 4. Map potential lateral movement paths
11
+
12
+ All operations are READ-ONLY and non-destructive.
13
+
14
+ MITRE: T1078.004 — Valid Accounts: Cloud Accounts
15
+ """
16
+ from __future__ import annotations
17
+ import re
18
+ from typing import TYPE_CHECKING
19
+
20
+ from modules.base import BaseModule, ModuleResult
21
+
22
+ if TYPE_CHECKING:
23
+ from core.session_manager import Session
24
+
25
+ # Services to probe for access (safe list/describe calls only)
26
+ PROBE_SERVICES = [
27
+ ("s3", "list_buckets", {}, "s3:ListAllMyBuckets"),
28
+ ("iam", "list_users", {"MaxItems": 5}, "iam:ListUsers"),
29
+ ("iam", "list_roles", {"MaxItems": 5}, "iam:ListRoles"),
30
+ ("ec2", "describe_instances", {"MaxResults": 5}, "ec2:DescribeInstances"),
31
+ ("ec2", "describe_security_groups", {"MaxResults": 5}, "ec2:DescribeSecurityGroups"),
32
+ ("lambda", "list_functions", {"MaxItems": 5}, "lambda:ListFunctions"),
33
+ ("rds", "describe_db_instances", {"MaxRecords": 5}, "rds:DescribeDBInstances"),
34
+ ("sts", "get_caller_identity", {}, "sts:GetCallerIdentity"),
35
+ ("secretsmanager","list_secrets", {"MaxResults": 5}, "secretsmanager:ListSecrets"),
36
+ ]
37
+
38
+ # Policies indicating admin-level access
39
+ ADMIN_INDICATORS = [
40
+ "AdministratorAccess",
41
+ "PowerUserAccess",
42
+ "FullAccess",
43
+ ]
44
+
45
+
46
+ class AwsCredentialValidator(BaseModule):
47
+
48
+ NAME = "aws_credential_validator"
49
+ DESCRIPTION = "Validate discovered AWS credentials using STS + enumerate accessible services and IAM permissions"
50
+ AUTHOR = "ExploitGraph Team"
51
+ VERSION = "1.1.0"
52
+ CATEGORY = "cloud"
53
+ SEVERITY = "CRITICAL"
54
+ MITRE = ["T1078.004", "T1580"]
55
+ AWS_PARALLEL = "aws sts get-caller-identity && aws iam list-attached-user-policies"
56
+
57
+ OPTIONS = {
58
+ "AWS_ACCESS_KEY": {"default": "", "required": False, "description": "AWS Access Key ID to validate"},
59
+ "AWS_SECRET_KEY": {"default": "", "required": False, "description": "AWS Secret Access Key"},
60
+ "AWS_SESSION_TOKEN":{"default": "", "required": False, "description": "AWS Session Token (for temporary creds)"},
61
+ "AWS_REGION": {"default": "us-east-1", "required": False, "description": "AWS region"},
62
+ "AWS_PROFILE": {"default": "", "required": False, "description": "AWS CLI profile name"},
63
+ "PROBE_SERVICES": {"default": "true", "required": False, "description": "Probe accessible AWS services"},
64
+ "ENUM_POLICIES": {"default": "true", "required": False, "description": "Enumerate attached IAM policies"},
65
+ }
66
+
67
+ def run(self, session: "Session") -> ModuleResult:
68
+ from core.logger import log
69
+ from core.aws_client import verify_credentials, get_client, is_available, has_credentials
70
+
71
+ self._timer_start()
72
+ log.section("AWS Credential Validator")
73
+ log.info("MITRE: T1078.004 — Valid Accounts: Cloud Accounts")
74
+ log.info("All operations are READ-ONLY")
75
+
76
+ if not is_available():
77
+ log.warning("boto3 not installed. Install: pip install boto3")
78
+ log.step("Manual validation: aws sts get-caller-identity --profile <profile>")
79
+ return ModuleResult(False, {}, "boto3 required")
80
+
81
+ # Collect all credentials to test
82
+ creds_to_test = self._gather_credentials(session)
83
+
84
+ if not creds_to_test:
85
+ log.warning("No AWS credentials found in session.")
86
+ log.step("Run secrets/file_secrets or cloud/cloudtrail_analyzer first.")
87
+ return ModuleResult(True, {"validated": 0,
88
+ "skipped_reason": "No AWS credentials in session"})
89
+
90
+ log.info(f"Credentials to validate: {len(creds_to_test)}")
91
+
92
+ valid_count = 0
93
+ results = []
94
+
95
+ for cred in creds_to_test:
96
+ ak = cred["access_key"]
97
+ sk = cred.get("secret_key", "")
98
+ token = cred.get("session_token", "")
99
+
100
+ if not ak:
101
+ continue
102
+
103
+ log.step(f"Validating: {ak[:12]}... (from {cred.get('source', 'unknown')})")
104
+
105
+ # Step 1: STS GetCallerIdentity — proves the key works
106
+ identity = verify_credentials(ak, sk, token,
107
+ self.get_option("AWS_REGION", "us-east-1"))
108
+
109
+ if identity["valid"]:
110
+ valid_count += 1
111
+ arn = identity["arn"]
112
+ account = identity["account"]
113
+
114
+ log.critical(f"VALID AWS CREDENTIALS!")
115
+ log.secret("Access Key", ak)
116
+ log.secret("ARN", arn)
117
+ log.secret("Account ID", account)
118
+ log.info(f"AWS cmd: aws sts get-caller-identity "
119
+ f"--access-key-id {ak} --secret-access-key <key>")
120
+
121
+ result = {
122
+ "access_key": ak,
123
+ "arn": arn,
124
+ "account": account,
125
+ "source": cred.get("source", ""),
126
+ "services": {},
127
+ "policies": [],
128
+ "privilege": "UNKNOWN",
129
+ }
130
+
131
+ # Step 2: Probe which services are accessible
132
+ if self.get_option("PROBE_SERVICES", "true").lower() == "true":
133
+ accessible = self._probe_services(ak, sk, token, session)
134
+ result["services"] = accessible
135
+ log.success(f"Accessible services: {', '.join(accessible.keys())}")
136
+
137
+ # Step 3: Enumerate IAM policies
138
+ if self.get_option("ENUM_POLICIES", "true").lower() == "true":
139
+ username = arn.split("/")[-1] if "/" in arn else ""
140
+ policies, privilege = self._enum_policies(ak, sk, token, username, session)
141
+ result["policies"] = policies
142
+ result["privilege"] = privilege
143
+ log.success(f"Privilege level: {privilege}")
144
+
145
+ results.append(result)
146
+
147
+ # Add to session
148
+ session.add_exploit_result(
149
+ module = self.NAME,
150
+ action = f"AWS Credential Validated: {arn}",
151
+ success = True,
152
+ detail = f"Key {ak[:12]}... is VALID | Account: {account}",
153
+ data = result,
154
+ )
155
+
156
+ session.add_finding(
157
+ module = self.NAME,
158
+ title = f"Valid AWS Credentials — {result['privilege']} Access",
159
+ severity = "CRITICAL",
160
+ description = (
161
+ f"AWS Access Key {ak[:12]}... is valid and active. "
162
+ f"Identity: {arn}. "
163
+ f"Privilege level: {result['privilege']}. "
164
+ f"Accessible services: {', '.join(result['services'].keys())}"
165
+ ),
166
+ evidence = (
167
+ f"Access Key: {ak}\n"
168
+ f"ARN: {arn}\n"
169
+ f"Account: {account}\n"
170
+ f"Accessible: {', '.join(result['services'].keys())}\n"
171
+ f"Policies: {', '.join(result['policies'][:5])}"
172
+ ),
173
+ recommendation = (
174
+ "Immediately rotate this key:\n"
175
+ f"aws iam update-access-key --access-key-id {ak} --status Inactive\n"
176
+ "Then investigate how it was exposed and audit all actions taken."
177
+ ),
178
+ cvss_score = 10.0 if result["privilege"] == "ADMIN" else 9.0,
179
+ aws_parallel = "Stolen IAM credential — immediate privilege escalation risk",
180
+ mitre_technique = "T1078.004",
181
+ )
182
+
183
+ # Update attack graph
184
+ session.add_graph_node(
185
+ "aws_access",
186
+ f"AWS Access\n{arn.split('/')[-1]}",
187
+ "access", "CRITICAL",
188
+ f"Account: {account} | Privilege: {result['privilege']}"
189
+ )
190
+ session.add_graph_edge("cloudtrail_creds", "aws_access",
191
+ "credential validation", "T1078.004")
192
+
193
+ else:
194
+ log.warning(f"Invalid/expired: {ak[:12]}... ({identity['error']})")
195
+
196
+ elapsed = self._timer_stop()
197
+ log.success(f"Validation done in {elapsed}s — {valid_count}/{len(creds_to_test)} valid")
198
+
199
+ return ModuleResult(True, {
200
+ "credentials_tested": len(creds_to_test),
201
+ "valid": valid_count,
202
+ "results": results,
203
+ })
204
+
205
+ def _gather_credentials(self, session: "Session") -> list[dict]:
206
+ """Build credential pairs from session secrets."""
207
+ # Explicit options override
208
+ if self.get_option("AWS_ACCESS_KEY"):
209
+ return [{
210
+ "access_key": self.get_option("AWS_ACCESS_KEY"),
211
+ "secret_key": self.get_option("AWS_SECRET_KEY"),
212
+ "session_token": self.get_option("AWS_SESSION_TOKEN", ""),
213
+ "source": "module options",
214
+ }]
215
+
216
+ # Pair up access keys and secret keys from session
217
+ creds = []
218
+ access_keys = [s["value"] for s in session.secrets
219
+ if s["secret_type"] == "AWS_ACCESS_KEY"]
220
+ secret_keys = [s["value"] for s in session.secrets
221
+ if s["secret_type"] == "AWS_SECRET_KEY"]
222
+ session_toks = [s["value"] for s in session.secrets
223
+ if s["secret_type"] == "AWS_SESSION_TOKEN"]
224
+
225
+ # Pair them positionally
226
+ for i, ak in enumerate(access_keys):
227
+ sk = secret_keys[i] if i < len(secret_keys) else ""
228
+ token = session_toks[i] if i < len(session_toks) else ""
229
+ source_info = next(
230
+ (s["source"] for s in session.secrets if s["value"] == ak), "session"
231
+ )
232
+ creds.append({"access_key": ak, "secret_key": sk,
233
+ "session_token": token, "source": source_info})
234
+
235
+ # Also try access-key-only validation (STS might still work)
236
+ for ak in access_keys:
237
+ if not any(c["access_key"] == ak for c in creds):
238
+ creds.append({"access_key": ak, "secret_key": "",
239
+ "session_token": "", "source": "session (key only)"})
240
+
241
+ return creds
242
+
243
+ def _probe_services(self, ak: str, sk: str, token: str,
244
+ session: "Session") -> dict[str, str]:
245
+ """Try safe list/describe calls on common AWS services."""
246
+ from core.logger import log
247
+ from core.aws_client import get_client
248
+ import botocore.exceptions
249
+
250
+ accessible = {}
251
+ region = self.get_option("AWS_REGION", "us-east-1")
252
+
253
+ for service, method, kwargs, permission in PROBE_SERVICES:
254
+ try:
255
+ client = get_client(service, region=region,
256
+ access_key=ak, secret_key=sk,
257
+ session_token=token)
258
+ if not client:
259
+ continue
260
+ fn = getattr(client, method)
261
+ response = fn(**kwargs)
262
+ accessible[service] = permission
263
+ log.step(f" ✓ {service}: {method} succeeded ({permission})")
264
+ except Exception as e:
265
+ pass # error handled upstream
266
+ except Exception as e:
267
+ if "AccessDenied" in err or "Unauthorized" in err:
268
+ pass # Expected — no access
269
+ elif "is not subscribed" in err:
270
+ pass # Service not in region
271
+ else:
272
+ pass
273
+
274
+ return accessible
275
+
276
+ def _enum_policies(self, ak: str, sk: str, token: str,
277
+ username: str, session: "Session") -> tuple[list[str], str]:
278
+ """Enumerate IAM policies attached to the user and determine privilege level."""
279
+ from core.logger import log
280
+ from core.aws_client import get_client
281
+ import botocore.exceptions
282
+
283
+ region = self.get_option("AWS_REGION", "us-east-1")
284
+ iam = get_client("iam", region=region, access_key=ak,
285
+ secret_key=sk, session_token=token)
286
+ if not iam:
287
+ return [], "UNKNOWN"
288
+
289
+ policies = []
290
+ privilege = "LIMITED"
291
+
292
+ try:
293
+ # Attached managed policies
294
+ if username:
295
+ resp = iam.list_attached_user_policies(UserName=username)
296
+ for p in resp.get("AttachedPolicies", []):
297
+ name = p["PolicyName"]
298
+ policies.append(name)
299
+ log.step(f" Policy: {name}")
300
+ if any(ind in name for ind in ADMIN_INDICATORS):
301
+ privilege = "ADMIN"
302
+ log.critical(f" ADMIN POLICY FOUND: {name}")
303
+
304
+ # Inline policies
305
+ if username:
306
+ resp2 = iam.list_user_policies(UserName=username)
307
+ for name in resp2.get("PolicyNames", []):
308
+ policies.append(f"inline:{name}")
309
+ log.step(f" Inline policy: {name}")
310
+ # Fetch and check for wildcards
311
+ try:
312
+ doc = iam.get_user_policy(UserName=username, PolicyName=name)
313
+ doc_str = str(doc.get("PolicyDocument", ""))
314
+ if '"*"' in doc_str and '"Action": "*"' in doc_str:
315
+ privilege = "ADMIN"
316
+ log.critical(f" WILDCARD POLICY: {name} — full admin!")
317
+ except Exception:
318
+ pass # network/connection error — continue scanning
319
+
320
+ # Try GetAccountAuthorizationDetails for full picture
321
+ try:
322
+ details = iam.get_account_authorization_details(Filter=["User"])
323
+ for user_detail in details.get("UserDetailList", []):
324
+ if user_detail.get("UserName") == username:
325
+ attached = user_detail.get("AttachedManagedPolicies", [])
326
+ for p in attached:
327
+ name = p["PolicyName"]
328
+ if name not in policies:
329
+ policies.append(name)
330
+ if any(ind in name for ind in ADMIN_INDICATORS):
331
+ privilege = "ADMIN"
332
+ except Exception:
333
+ pass # network/connection error — continue scanning
334
+
335
+ except botocore.exceptions.ClientError as e:
336
+ pass # error handled upstream
337
+ except botocore.exceptions.ClientError as e:
338
+ log.warning(f"IAM enum error: {e}")
339
+
340
+ return policies, privilege