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.
Files changed (75) hide show
  1. cloudg/__init__.py +21 -0
  2. cloudg/api.py +613 -0
  3. cloudg/cli.py +934 -0
  4. cloudg/collectors/__init__.py +5 -0
  5. cloudg/collectors/aws.py +830 -0
  6. cloudg/collectors/azure.py +362 -0
  7. cloudg/collectors/base.py +44 -0
  8. cloudg/collectors/gcp.py +170 -0
  9. cloudg/collectors/multi.py +329 -0
  10. cloudg/config.py +261 -0
  11. cloudg/coverage.py +93 -0
  12. cloudg/credentials.py +451 -0
  13. cloudg/graph/__init__.py +6 -0
  14. cloudg/graph/builder.py +336 -0
  15. cloudg/graph/ontology.py +914 -0
  16. cloudg/graph/rag_export.py +533 -0
  17. cloudg/graph/reachability.py +237 -0
  18. cloudg/normaliser.py +390 -0
  19. cloudg/policies/custodian-aws-security.yml +139 -0
  20. cloudg/policies/custodian-azure.yml +95 -0
  21. cloudg/policies/custodian-gcp.yml +87 -0
  22. cloudg/policies/custodian.yml +131 -0
  23. cloudg/region_discovery.py +330 -0
  24. cloudg/registry.py +142 -0
  25. cloudg/renderers/__init__.py +1 -0
  26. cloudg/renderers/html_report.py +430 -0
  27. cloudg/renderers/json_export.py +65 -0
  28. cloudg/renderers/svg.py +689 -0
  29. cloudg/renderers/terraform_export.py +666 -0
  30. cloudg/retry.py +103 -0
  31. cloudg/rules/cis_aws_v3.yaml +148 -0
  32. cloudg/rules/cis_azure_v2.yaml +142 -0
  33. cloudg/rules/cis_gcp_v3.yaml +133 -0
  34. cloudg/rules/frameworks/aws_foundational_security_best_practices_aws.yaml +1286 -0
  35. cloudg/rules/frameworks/cis_5.0_aws.yaml +280 -0
  36. cloudg/rules/frameworks/cis_5.0_azure.yaml +475 -0
  37. cloudg/rules/frameworks/cis_5.0_gcp.yaml +327 -0
  38. cloudg/rules/frameworks/gdpr_aws.yaml +95 -0
  39. cloudg/rules/frameworks/hipaa_aws.yaml +461 -0
  40. cloudg/rules/frameworks/hipaa_azure.yaml +504 -0
  41. cloudg/rules/frameworks/hipaa_gcp.yaml +211 -0
  42. cloudg/rules/frameworks/iso27001_2022_aws.yaml +789 -0
  43. cloudg/rules/frameworks/iso27001_2022_azure.yaml +477 -0
  44. cloudg/rules/frameworks/iso27001_2022_gcp.yaml +240 -0
  45. cloudg/rules/frameworks/mitre_attack_aws.yaml +596 -0
  46. cloudg/rules/frameworks/mitre_attack_azure.yaml +418 -0
  47. cloudg/rules/frameworks/mitre_attack_gcp.yaml +300 -0
  48. cloudg/rules/frameworks/nist_800_53_revision_5_aws.yaml +3234 -0
  49. cloudg/rules/frameworks/nist_csf_2.0_aws.yaml +828 -0
  50. cloudg/rules/frameworks/pci_4.0_aws.yaml +7108 -0
  51. cloudg/rules/frameworks/pci_4.0_azure.yaml +4696 -0
  52. cloudg/rules/frameworks/pci_4.0_gcp.yaml +5405 -0
  53. cloudg/rules/frameworks/soc2_aws.yaml +421 -0
  54. cloudg/rules/frameworks/soc2_azure.yaml +477 -0
  55. cloudg/rules/frameworks/soc2_gcp.yaml +342 -0
  56. cloudg/rules/gdpr.yaml +56 -0
  57. cloudg/rules/hipaa_security_rule.yaml +83 -0
  58. cloudg/rules/iso_27001_2022.yaml +88 -0
  59. cloudg/rules/nist_800_53.yaml +134 -0
  60. cloudg/rules/pci_dss_v4.yaml +108 -0
  61. cloudg/rules/soc2_tsc.yaml +93 -0
  62. cloudg/scanners/__init__.py +1 -0
  63. cloudg/scanners/checkov.py +174 -0
  64. cloudg/scanners/iam_linter.py +200 -0
  65. cloudg/scanners/prowler.py +237 -0
  66. cloudg/scanners/scoutsuite.py +163 -0
  67. cloudg/scanners/trivy.py +339 -0
  68. cloudg/schema/__init__.py +25 -0
  69. cloudg/schema/models.py +260 -0
  70. cloudg/templates/report.html.j2 +828 -0
  71. cloudg-0.3.0.dist-info/METADATA +330 -0
  72. cloudg-0.3.0.dist-info/RECORD +75 -0
  73. cloudg-0.3.0.dist-info/WHEEL +4 -0
  74. cloudg-0.3.0.dist-info/entry_points.txt +14 -0
  75. 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)
@@ -0,0 +1,6 @@
1
+ """Graph engine package."""
2
+
3
+ from cloudg.graph.builder import GraphBuilder
4
+ from cloudg.graph.reachability import ReachabilityAnalyzer
5
+
6
+ __all__ = ["GraphBuilder", "ReachabilityAnalyzer"]