cloudg 0.3.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.
- cloudg/__init__.py +21 -0
- cloudg/api.py +613 -0
- cloudg/cli.py +934 -0
- cloudg/collectors/__init__.py +5 -0
- cloudg/collectors/aws.py +830 -0
- cloudg/collectors/azure.py +362 -0
- cloudg/collectors/base.py +44 -0
- cloudg/collectors/gcp.py +170 -0
- cloudg/collectors/multi.py +329 -0
- cloudg/config.py +261 -0
- cloudg/coverage.py +93 -0
- cloudg/credentials.py +451 -0
- cloudg/graph/__init__.py +6 -0
- cloudg/graph/builder.py +336 -0
- cloudg/graph/ontology.py +914 -0
- cloudg/graph/rag_export.py +533 -0
- cloudg/graph/reachability.py +237 -0
- cloudg/normaliser.py +390 -0
- cloudg/policies/custodian-aws-security.yml +139 -0
- cloudg/policies/custodian-azure.yml +95 -0
- cloudg/policies/custodian-gcp.yml +87 -0
- cloudg/policies/custodian.yml +131 -0
- cloudg/region_discovery.py +330 -0
- cloudg/registry.py +142 -0
- cloudg/renderers/__init__.py +1 -0
- cloudg/renderers/html_report.py +430 -0
- cloudg/renderers/json_export.py +65 -0
- cloudg/renderers/svg.py +689 -0
- cloudg/renderers/terraform_export.py +666 -0
- cloudg/retry.py +103 -0
- cloudg/rules/cis_aws_v3.yaml +148 -0
- cloudg/rules/cis_azure_v2.yaml +142 -0
- cloudg/rules/cis_gcp_v3.yaml +133 -0
- cloudg/rules/frameworks/aws_foundational_security_best_practices_aws.yaml +1286 -0
- cloudg/rules/frameworks/cis_5.0_aws.yaml +280 -0
- cloudg/rules/frameworks/cis_5.0_azure.yaml +475 -0
- cloudg/rules/frameworks/cis_5.0_gcp.yaml +327 -0
- cloudg/rules/frameworks/gdpr_aws.yaml +95 -0
- cloudg/rules/frameworks/hipaa_aws.yaml +461 -0
- cloudg/rules/frameworks/hipaa_azure.yaml +504 -0
- cloudg/rules/frameworks/hipaa_gcp.yaml +211 -0
- cloudg/rules/frameworks/iso27001_2022_aws.yaml +789 -0
- cloudg/rules/frameworks/iso27001_2022_azure.yaml +477 -0
- cloudg/rules/frameworks/iso27001_2022_gcp.yaml +240 -0
- cloudg/rules/frameworks/mitre_attack_aws.yaml +596 -0
- cloudg/rules/frameworks/mitre_attack_azure.yaml +418 -0
- cloudg/rules/frameworks/mitre_attack_gcp.yaml +300 -0
- cloudg/rules/frameworks/nist_800_53_revision_5_aws.yaml +3234 -0
- cloudg/rules/frameworks/nist_csf_2.0_aws.yaml +828 -0
- cloudg/rules/frameworks/pci_4.0_aws.yaml +7108 -0
- cloudg/rules/frameworks/pci_4.0_azure.yaml +4696 -0
- cloudg/rules/frameworks/pci_4.0_gcp.yaml +5405 -0
- cloudg/rules/frameworks/soc2_aws.yaml +421 -0
- cloudg/rules/frameworks/soc2_azure.yaml +477 -0
- cloudg/rules/frameworks/soc2_gcp.yaml +342 -0
- cloudg/rules/gdpr.yaml +56 -0
- cloudg/rules/hipaa_security_rule.yaml +83 -0
- cloudg/rules/iso_27001_2022.yaml +88 -0
- cloudg/rules/nist_800_53.yaml +134 -0
- cloudg/rules/pci_dss_v4.yaml +108 -0
- cloudg/rules/soc2_tsc.yaml +93 -0
- cloudg/scanners/__init__.py +1 -0
- cloudg/scanners/checkov.py +174 -0
- cloudg/scanners/iam_linter.py +200 -0
- cloudg/scanners/prowler.py +237 -0
- cloudg/scanners/scoutsuite.py +163 -0
- cloudg/scanners/trivy.py +339 -0
- cloudg/schema/__init__.py +25 -0
- cloudg/schema/models.py +260 -0
- cloudg/templates/report.html.j2 +828 -0
- cloudg-0.3.0.dist-info/METADATA +330 -0
- cloudg-0.3.0.dist-info/RECORD +75 -0
- cloudg-0.3.0.dist-info/WHEEL +4 -0
- cloudg-0.3.0.dist-info/entry_points.txt +14 -0
- cloudg-0.3.0.dist-info/licenses/LICENSE +21 -0
cloudg/credentials.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""Credential resolution for AWS, Azure, and GCP cloud providers.
|
|
2
|
+
|
|
3
|
+
Every provider supports several auth methods, resolved in a fixed priority
|
|
4
|
+
order so the same config works on a laptop, in CI, and on cloud compute.
|
|
5
|
+
|
|
6
|
+
AWS (build_aws_session):
|
|
7
|
+
1. Direct access keys (access_key_id + secret_access_key [+ session_token])
|
|
8
|
+
2. OIDC / web identity federation (role_arn + web_identity_token_file,
|
|
9
|
+
e.g. GitHub Actions, GitLab CI, EKS service account tokens)
|
|
10
|
+
3. Named CLI profile (includes SSO profiles)
|
|
11
|
+
4. Default chain: env vars, EC2/ECS instance role (inherited role),
|
|
12
|
+
container credentials, SSO cache
|
|
13
|
+
Then, optionally on top of any of the above:
|
|
14
|
+
5. STS AssumeRole into role_arn or accounts[]/role_name, with optional
|
|
15
|
+
external_id (the standard third-party auditor pattern)
|
|
16
|
+
|
|
17
|
+
Azure (build_azure_credential):
|
|
18
|
+
1. Workload identity federation (tenant_id + client_id +
|
|
19
|
+
federated_token_file — AKS / GitHub OIDC)
|
|
20
|
+
2. Service principal with client secret
|
|
21
|
+
3. Service principal with client certificate
|
|
22
|
+
4. Managed identity (system- or user-assigned — inherited identity)
|
|
23
|
+
5. DefaultAzureCredential chain (env, CLI, PowerShell, managed identity)
|
|
24
|
+
|
|
25
|
+
GCP (build_gcp_credentials):
|
|
26
|
+
1. Explicit credentials file: service account key JSON or a workload
|
|
27
|
+
identity federation (external_account) config JSON
|
|
28
|
+
2. Application Default Credentials: GOOGLE_APPLICATION_CREDENTIALS,
|
|
29
|
+
gcloud user creds, or the GCE/GKE metadata server (inherited identity)
|
|
30
|
+
Then, optionally on top of either:
|
|
31
|
+
3. Service account impersonation (impersonate_service_account)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import logging
|
|
37
|
+
import os
|
|
38
|
+
from dataclasses import dataclass
|
|
39
|
+
from typing import Any
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
_GCP_SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class AWSCredentials:
|
|
48
|
+
"""Resolved AWS credentials."""
|
|
49
|
+
|
|
50
|
+
session: Any # boto3.Session
|
|
51
|
+
account_id: str | None = None
|
|
52
|
+
region: str = "us-east-1"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class AzureCredentials:
|
|
57
|
+
"""Resolved Azure credentials."""
|
|
58
|
+
|
|
59
|
+
credential: Any # azure.identity credential object
|
|
60
|
+
subscription_id: str
|
|
61
|
+
tenant_id: str | None = None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class GCPCredentials:
|
|
66
|
+
"""Resolved GCP credentials."""
|
|
67
|
+
|
|
68
|
+
credentials: Any # google.auth.credentials.Credentials
|
|
69
|
+
project_id: str
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
73
|
+
# AWS
|
|
74
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_aws_session(
|
|
78
|
+
cfg: Any,
|
|
79
|
+
region: str,
|
|
80
|
+
account_id: str | None = None,
|
|
81
|
+
) -> Any:
|
|
82
|
+
"""Build a boto3 Session from an AWSConfig, honouring all auth methods.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
cfg: AWSConfig (or any object with the same credential fields).
|
|
86
|
+
region: Target region for the session.
|
|
87
|
+
account_id: Target account for cross-account AssumeRole via
|
|
88
|
+
cfg.role_name (ignored when cfg.role_arn is set).
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
boto3.Session ready for API calls.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ImportError: If boto3 is not installed.
|
|
95
|
+
RuntimeError: If a requested role assumption fails.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
import boto3
|
|
99
|
+
except ImportError as exc:
|
|
100
|
+
raise ImportError(
|
|
101
|
+
"boto3 is required for AWS. Install with: pip install cloudg[aws]"
|
|
102
|
+
) from exc
|
|
103
|
+
|
|
104
|
+
session_kwargs: dict[str, str] = {"region_name": region}
|
|
105
|
+
session_name = getattr(cfg, "role_session_name", None) or "cloudg-scan"
|
|
106
|
+
role_arn = getattr(cfg, "role_arn", None)
|
|
107
|
+
token_file = getattr(cfg, "web_identity_token_file", None) or os.environ.get(
|
|
108
|
+
"AWS_WEB_IDENTITY_TOKEN_FILE"
|
|
109
|
+
)
|
|
110
|
+
external_id = getattr(cfg, "external_id", None)
|
|
111
|
+
|
|
112
|
+
assumed_via_oidc = False
|
|
113
|
+
|
|
114
|
+
if cfg.access_key_id and cfg.secret_access_key:
|
|
115
|
+
# 1. Direct access keys
|
|
116
|
+
session_kwargs["aws_access_key_id"] = cfg.access_key_id
|
|
117
|
+
session_kwargs["aws_secret_access_key"] = cfg.secret_access_key
|
|
118
|
+
if getattr(cfg, "session_token", None):
|
|
119
|
+
session_kwargs["aws_session_token"] = cfg.session_token
|
|
120
|
+
logger.info("AWS auth: direct access keys (region %s)", region)
|
|
121
|
+
session = boto3.Session(**session_kwargs)
|
|
122
|
+
elif role_arn and token_file:
|
|
123
|
+
# 2. OIDC / web identity federation
|
|
124
|
+
logger.info("AWS auth: web identity federation into %s", role_arn)
|
|
125
|
+
with open(token_file) as f:
|
|
126
|
+
token = f.read().strip()
|
|
127
|
+
sts = boto3.client("sts", region_name=region)
|
|
128
|
+
try:
|
|
129
|
+
resp = sts.assume_role_with_web_identity(
|
|
130
|
+
RoleArn=role_arn,
|
|
131
|
+
RoleSessionName=session_name,
|
|
132
|
+
WebIdentityToken=token,
|
|
133
|
+
)
|
|
134
|
+
except Exception as exc:
|
|
135
|
+
raise RuntimeError(f"AssumeRoleWithWebIdentity failed for {role_arn}: {exc}") from exc
|
|
136
|
+
creds = resp["Credentials"]
|
|
137
|
+
session = boto3.Session(
|
|
138
|
+
aws_access_key_id=creds["AccessKeyId"],
|
|
139
|
+
aws_secret_access_key=creds["SecretAccessKey"],
|
|
140
|
+
aws_session_token=creds["SessionToken"],
|
|
141
|
+
region_name=region,
|
|
142
|
+
)
|
|
143
|
+
assumed_via_oidc = True
|
|
144
|
+
elif getattr(cfg, "profile", None):
|
|
145
|
+
# 3. Named profile (plain or SSO)
|
|
146
|
+
session_kwargs["profile_name"] = cfg.profile
|
|
147
|
+
logger.info("AWS auth: profile '%s' (region %s)", cfg.profile, region)
|
|
148
|
+
session = boto3.Session(**session_kwargs)
|
|
149
|
+
else:
|
|
150
|
+
# 4. Default chain: env vars, instance/task role, SSO cache
|
|
151
|
+
logger.info("AWS auth: default credential chain (region %s)", region)
|
|
152
|
+
session = boto3.Session(**session_kwargs)
|
|
153
|
+
|
|
154
|
+
# 5. Optional role assumption on top of the base credentials
|
|
155
|
+
target_role: str | None = None
|
|
156
|
+
if role_arn and not assumed_via_oidc:
|
|
157
|
+
target_role = role_arn
|
|
158
|
+
elif account_id and getattr(cfg, "role_name", None):
|
|
159
|
+
target_role = f"arn:aws:iam::{account_id}:role/{cfg.role_name}"
|
|
160
|
+
|
|
161
|
+
if target_role:
|
|
162
|
+
logger.info("AWS auth: assuming role %s", target_role)
|
|
163
|
+
assume_kwargs: dict[str, Any] = {
|
|
164
|
+
"RoleArn": target_role,
|
|
165
|
+
"RoleSessionName": session_name,
|
|
166
|
+
"DurationSeconds": 3600,
|
|
167
|
+
}
|
|
168
|
+
if external_id:
|
|
169
|
+
assume_kwargs["ExternalId"] = external_id
|
|
170
|
+
try:
|
|
171
|
+
assumed = session.client("sts").assume_role(**assume_kwargs)
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
raise RuntimeError(f"AssumeRole failed for {target_role}: {exc}") from exc
|
|
174
|
+
creds = assumed["Credentials"]
|
|
175
|
+
session = boto3.Session(
|
|
176
|
+
aws_access_key_id=creds["AccessKeyId"],
|
|
177
|
+
aws_secret_access_key=creds["SecretAccessKey"],
|
|
178
|
+
aws_session_token=creds["SessionToken"],
|
|
179
|
+
region_name=region,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return session
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
186
|
+
# Azure
|
|
187
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def build_azure_credential(cfg: Any) -> Any:
|
|
191
|
+
"""Build an azure.identity credential from an AzureConfig.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
A TokenCredential usable with all Azure mgmt SDK clients.
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
ImportError: If azure-identity is not installed.
|
|
198
|
+
"""
|
|
199
|
+
try:
|
|
200
|
+
from azure import identity
|
|
201
|
+
except ImportError as exc:
|
|
202
|
+
raise ImportError(
|
|
203
|
+
"azure-identity is required for Azure. Install with: pip install cloudg[azure]"
|
|
204
|
+
) from exc
|
|
205
|
+
|
|
206
|
+
tenant_id = getattr(cfg, "tenant_id", None) or os.environ.get("AZURE_TENANT_ID")
|
|
207
|
+
client_id = getattr(cfg, "client_id", None) or os.environ.get("AZURE_CLIENT_ID")
|
|
208
|
+
client_secret = getattr(cfg, "client_secret", None) or os.environ.get("AZURE_CLIENT_SECRET")
|
|
209
|
+
certificate_path = getattr(cfg, "certificate_path", None)
|
|
210
|
+
federated_token_file = getattr(cfg, "federated_token_file", None) or os.environ.get(
|
|
211
|
+
"AZURE_FEDERATED_TOKEN_FILE"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# 1. Workload identity federation (AKS, GitHub Actions OIDC)
|
|
215
|
+
if tenant_id and client_id and federated_token_file:
|
|
216
|
+
logger.info("Azure auth: workload identity federation (client %s)", client_id)
|
|
217
|
+
return identity.WorkloadIdentityCredential(
|
|
218
|
+
tenant_id=tenant_id,
|
|
219
|
+
client_id=client_id,
|
|
220
|
+
token_file_path=federated_token_file,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# 2. Service principal with secret
|
|
224
|
+
if tenant_id and client_id and client_secret:
|
|
225
|
+
logger.info("Azure auth: service principal secret (client %s)", client_id)
|
|
226
|
+
return identity.ClientSecretCredential(
|
|
227
|
+
tenant_id=tenant_id,
|
|
228
|
+
client_id=client_id,
|
|
229
|
+
client_secret=client_secret,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# 3. Service principal with certificate
|
|
233
|
+
if tenant_id and client_id and certificate_path:
|
|
234
|
+
logger.info("Azure auth: service principal certificate (client %s)", client_id)
|
|
235
|
+
return identity.CertificateCredential(
|
|
236
|
+
tenant_id=tenant_id,
|
|
237
|
+
client_id=client_id,
|
|
238
|
+
certificate_path=certificate_path,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# 4. Managed identity (inherited from the VM / App Service / AKS node)
|
|
242
|
+
if getattr(cfg, "use_managed_identity", False):
|
|
243
|
+
mi_client_id = getattr(cfg, "managed_identity_client_id", None)
|
|
244
|
+
logger.info(
|
|
245
|
+
"Azure auth: managed identity (%s)",
|
|
246
|
+
mi_client_id or "system-assigned",
|
|
247
|
+
)
|
|
248
|
+
if mi_client_id:
|
|
249
|
+
return identity.ManagedIdentityCredential(client_id=mi_client_id)
|
|
250
|
+
return identity.ManagedIdentityCredential()
|
|
251
|
+
|
|
252
|
+
# 5. Default chain (env, managed identity, CLI, PowerShell)
|
|
253
|
+
logger.info("Azure auth: DefaultAzureCredential chain")
|
|
254
|
+
return identity.DefaultAzureCredential()
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
258
|
+
# GCP
|
|
259
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def build_gcp_credentials(cfg: Any) -> tuple[Any, str | None]:
|
|
263
|
+
"""Build google-auth credentials from a GCPConfig.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
(credentials, default_project_id) — project may be None when the
|
|
267
|
+
auth method carries no project (e.g. impersonation).
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
ImportError: If google-auth is not installed.
|
|
271
|
+
"""
|
|
272
|
+
try:
|
|
273
|
+
import google.auth
|
|
274
|
+
except ImportError as exc:
|
|
275
|
+
raise ImportError(
|
|
276
|
+
"google-auth is required for GCP. Install with: pip install cloudg[gcp]"
|
|
277
|
+
) from exc
|
|
278
|
+
|
|
279
|
+
credentials_file = getattr(cfg, "credentials_file", None) or os.environ.get(
|
|
280
|
+
"GOOGLE_APPLICATION_CREDENTIALS"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
if credentials_file and getattr(cfg, "credentials_file", None):
|
|
284
|
+
# 1. Explicit file: service account key OR workload identity
|
|
285
|
+
# federation (external_account) config — load handles both.
|
|
286
|
+
logger.info("GCP auth: credentials file %s", credentials_file)
|
|
287
|
+
credentials, project = google.auth.load_credentials_from_file(
|
|
288
|
+
credentials_file, scopes=_GCP_SCOPES
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
# 2. Application Default Credentials (env var file, gcloud user
|
|
292
|
+
# credentials, or GCE/GKE metadata server)
|
|
293
|
+
logger.info("GCP auth: application default credentials")
|
|
294
|
+
credentials, project = google.auth.default(scopes=_GCP_SCOPES)
|
|
295
|
+
|
|
296
|
+
# 3. Optional service account impersonation
|
|
297
|
+
impersonate = getattr(cfg, "impersonate_service_account", None)
|
|
298
|
+
if impersonate:
|
|
299
|
+
from google.auth import impersonated_credentials
|
|
300
|
+
|
|
301
|
+
logger.info("GCP auth: impersonating %s", impersonate)
|
|
302
|
+
credentials = impersonated_credentials.Credentials(
|
|
303
|
+
source_credentials=credentials,
|
|
304
|
+
target_principal=impersonate,
|
|
305
|
+
target_scopes=_GCP_SCOPES,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return credentials, project
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
312
|
+
# Resolver (thin wrapper kept for the `collect` command and library use)
|
|
313
|
+
# ─────────────────────────────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class CredentialResolver:
|
|
317
|
+
"""Resolves cloud provider credentials across all supported auth methods.
|
|
318
|
+
|
|
319
|
+
Thin convenience wrapper around build_aws_session /
|
|
320
|
+
build_azure_credential / build_gcp_credentials that also resolves the
|
|
321
|
+
account, subscription, or project the credentials belong to.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def resolve_aws(
|
|
325
|
+
self,
|
|
326
|
+
profile: str | None = None,
|
|
327
|
+
region: str | None = None,
|
|
328
|
+
config: Any = None,
|
|
329
|
+
) -> AWSCredentials:
|
|
330
|
+
"""Resolve AWS credentials.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
profile: AWS profile name (defaults to AWS_PROFILE env var).
|
|
334
|
+
region: AWS region (defaults to AWS_DEFAULT_REGION or us-east-1).
|
|
335
|
+
config: Optional AWSConfig for the full range of auth methods
|
|
336
|
+
(direct keys, OIDC, role assumption). Overrides `profile`.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
AWSCredentials with an active boto3 session.
|
|
340
|
+
"""
|
|
341
|
+
region = region or os.environ.get("AWS_DEFAULT_REGION", "us-east-1")
|
|
342
|
+
|
|
343
|
+
if config is not None:
|
|
344
|
+
session = build_aws_session(config, region)
|
|
345
|
+
else:
|
|
346
|
+
from types import SimpleNamespace
|
|
347
|
+
|
|
348
|
+
shim = SimpleNamespace(
|
|
349
|
+
access_key_id=None,
|
|
350
|
+
secret_access_key=None,
|
|
351
|
+
session_token=None,
|
|
352
|
+
profile=profile or os.environ.get("AWS_PROFILE"),
|
|
353
|
+
role_arn=None,
|
|
354
|
+
role_name=None,
|
|
355
|
+
external_id=None,
|
|
356
|
+
web_identity_token_file=None,
|
|
357
|
+
role_session_name=None,
|
|
358
|
+
)
|
|
359
|
+
session = build_aws_session(shim, region)
|
|
360
|
+
|
|
361
|
+
account_id = None
|
|
362
|
+
try:
|
|
363
|
+
identity = session.client("sts").get_caller_identity()
|
|
364
|
+
account_id = identity.get("Account")
|
|
365
|
+
logger.info("AWS credentials resolved for account %s", account_id)
|
|
366
|
+
except Exception:
|
|
367
|
+
logger.warning("Could not resolve AWS account ID via STS")
|
|
368
|
+
|
|
369
|
+
return AWSCredentials(session=session, account_id=account_id, region=region)
|
|
370
|
+
|
|
371
|
+
def resolve_azure(
|
|
372
|
+
self,
|
|
373
|
+
subscription_id: str | None = None,
|
|
374
|
+
tenant_id: str | None = None,
|
|
375
|
+
config: Any = None,
|
|
376
|
+
) -> AzureCredentials:
|
|
377
|
+
"""Resolve Azure credentials.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
subscription_id: Azure subscription (defaults to AZURE_SUBSCRIPTION_ID).
|
|
381
|
+
tenant_id: Azure tenant (defaults to AZURE_TENANT_ID).
|
|
382
|
+
config: Optional AzureConfig for service principal, workload
|
|
383
|
+
identity, or managed identity auth.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
AzureCredentials with a TokenCredential instance.
|
|
387
|
+
|
|
388
|
+
Raises:
|
|
389
|
+
ValueError: If subscription_id cannot be determined.
|
|
390
|
+
"""
|
|
391
|
+
subscription_id = subscription_id or os.environ.get("AZURE_SUBSCRIPTION_ID")
|
|
392
|
+
tenant_id = tenant_id or os.environ.get("AZURE_TENANT_ID")
|
|
393
|
+
|
|
394
|
+
if not subscription_id:
|
|
395
|
+
raise ValueError(
|
|
396
|
+
"Azure subscription ID is required. "
|
|
397
|
+
"Set AZURE_SUBSCRIPTION_ID env var or pass subscription_id."
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if config is not None:
|
|
401
|
+
credential = build_azure_credential(config)
|
|
402
|
+
else:
|
|
403
|
+
from types import SimpleNamespace
|
|
404
|
+
|
|
405
|
+
credential = build_azure_credential(SimpleNamespace(tenant_id=tenant_id))
|
|
406
|
+
logger.info("Azure credentials resolved for subscription %s", subscription_id)
|
|
407
|
+
|
|
408
|
+
return AzureCredentials(
|
|
409
|
+
credential=credential,
|
|
410
|
+
subscription_id=subscription_id,
|
|
411
|
+
tenant_id=tenant_id,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
def resolve_gcp(
|
|
415
|
+
self,
|
|
416
|
+
project_id: str | None = None,
|
|
417
|
+
config: Any = None,
|
|
418
|
+
) -> GCPCredentials:
|
|
419
|
+
"""Resolve GCP credentials.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
project_id: GCP project (defaults to GOOGLE_CLOUD_PROJECT).
|
|
423
|
+
config: Optional GCPConfig for key-file, federation, or
|
|
424
|
+
impersonation auth.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
GCPCredentials with active credentials.
|
|
428
|
+
|
|
429
|
+
Raises:
|
|
430
|
+
ValueError: If project_id cannot be determined.
|
|
431
|
+
"""
|
|
432
|
+
project_id = project_id or os.environ.get("GOOGLE_CLOUD_PROJECT")
|
|
433
|
+
|
|
434
|
+
if config is not None:
|
|
435
|
+
credentials, discovered_project = build_gcp_credentials(config)
|
|
436
|
+
else:
|
|
437
|
+
from types import SimpleNamespace
|
|
438
|
+
|
|
439
|
+
credentials, discovered_project = build_gcp_credentials(SimpleNamespace())
|
|
440
|
+
|
|
441
|
+
if not project_id:
|
|
442
|
+
project_id = discovered_project
|
|
443
|
+
|
|
444
|
+
if not project_id:
|
|
445
|
+
raise ValueError(
|
|
446
|
+
"GCP project ID is required. Set GOOGLE_CLOUD_PROJECT env var or pass project_id."
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
logger.info("GCP credentials resolved for project %s", project_id)
|
|
450
|
+
|
|
451
|
+
return GCPCredentials(credentials=credentials, project_id=project_id)
|