cloudg 0.3.0__tar.gz → 0.3.1__tar.gz

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 (85) hide show
  1. {cloudg-0.3.0 → cloudg-0.3.1}/PKG-INFO +2 -5
  2. {cloudg-0.3.0 → cloudg-0.3.1}/README.md +1 -4
  3. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/__init__.py +1 -1
  4. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/api.py +59 -8
  5. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/cli.py +31 -14
  6. {cloudg-0.3.0 → cloudg-0.3.1}/pyproject.toml +1 -1
  7. {cloudg-0.3.0 → cloudg-0.3.1}/tests/test_scanner_orchestration.py +40 -0
  8. {cloudg-0.3.0 → cloudg-0.3.1}/.gitignore +0 -0
  9. {cloudg-0.3.0 → cloudg-0.3.1}/LICENSE +0 -0
  10. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/collectors/__init__.py +0 -0
  11. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/collectors/aws.py +0 -0
  12. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/collectors/azure.py +0 -0
  13. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/collectors/base.py +0 -0
  14. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/collectors/gcp.py +0 -0
  15. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/collectors/multi.py +0 -0
  16. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/config.py +0 -0
  17. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/coverage.py +0 -0
  18. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/credentials.py +0 -0
  19. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/graph/__init__.py +0 -0
  20. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/graph/builder.py +0 -0
  21. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/graph/ontology.py +0 -0
  22. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/graph/rag_export.py +0 -0
  23. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/graph/reachability.py +0 -0
  24. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/normaliser.py +0 -0
  25. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/policies/custodian-aws-security.yml +0 -0
  26. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/policies/custodian-azure.yml +0 -0
  27. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/policies/custodian-gcp.yml +0 -0
  28. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/policies/custodian.yml +0 -0
  29. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/region_discovery.py +0 -0
  30. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/registry.py +0 -0
  31. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/renderers/__init__.py +0 -0
  32. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/renderers/html_report.py +0 -0
  33. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/renderers/json_export.py +0 -0
  34. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/renderers/svg.py +0 -0
  35. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/renderers/terraform_export.py +0 -0
  36. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/retry.py +0 -0
  37. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/cis_aws_v3.yaml +0 -0
  38. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/cis_azure_v2.yaml +0 -0
  39. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/cis_gcp_v3.yaml +0 -0
  40. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/aws_foundational_security_best_practices_aws.yaml +0 -0
  41. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/cis_5.0_aws.yaml +0 -0
  42. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/cis_5.0_azure.yaml +0 -0
  43. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/cis_5.0_gcp.yaml +0 -0
  44. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/gdpr_aws.yaml +0 -0
  45. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/hipaa_aws.yaml +0 -0
  46. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/hipaa_azure.yaml +0 -0
  47. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/hipaa_gcp.yaml +0 -0
  48. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/iso27001_2022_aws.yaml +0 -0
  49. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/iso27001_2022_azure.yaml +0 -0
  50. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/iso27001_2022_gcp.yaml +0 -0
  51. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/mitre_attack_aws.yaml +0 -0
  52. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/mitre_attack_azure.yaml +0 -0
  53. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/mitre_attack_gcp.yaml +0 -0
  54. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/nist_800_53_revision_5_aws.yaml +0 -0
  55. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/nist_csf_2.0_aws.yaml +0 -0
  56. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/pci_4.0_aws.yaml +0 -0
  57. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/pci_4.0_azure.yaml +0 -0
  58. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/pci_4.0_gcp.yaml +0 -0
  59. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/soc2_aws.yaml +0 -0
  60. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/soc2_azure.yaml +0 -0
  61. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/frameworks/soc2_gcp.yaml +0 -0
  62. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/gdpr.yaml +0 -0
  63. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/hipaa_security_rule.yaml +0 -0
  64. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/iso_27001_2022.yaml +0 -0
  65. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/nist_800_53.yaml +0 -0
  66. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/pci_dss_v4.yaml +0 -0
  67. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/rules/soc2_tsc.yaml +0 -0
  68. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/scanners/__init__.py +0 -0
  69. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/scanners/checkov.py +0 -0
  70. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/scanners/iam_linter.py +0 -0
  71. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/scanners/prowler.py +0 -0
  72. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/scanners/scoutsuite.py +0 -0
  73. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/scanners/trivy.py +0 -0
  74. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/schema/__init__.py +0 -0
  75. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/schema/models.py +0 -0
  76. {cloudg-0.3.0 → cloudg-0.3.1}/cloudg/templates/report.html.j2 +0 -0
  77. {cloudg-0.3.0 → cloudg-0.3.1}/tests/__init__.py +0 -0
  78. {cloudg-0.3.0 → cloudg-0.3.1}/tests/conftest.py +0 -0
  79. {cloudg-0.3.0 → cloudg-0.3.1}/tests/test_api.py +0 -0
  80. {cloudg-0.3.0 → cloudg-0.3.1}/tests/test_collectors.py +0 -0
  81. {cloudg-0.3.0 → cloudg-0.3.1}/tests/test_normaliser.py +0 -0
  82. {cloudg-0.3.0 → cloudg-0.3.1}/tests/test_ontology.py +0 -0
  83. {cloudg-0.3.0 → cloudg-0.3.1}/tests/test_rag_export.py +0 -0
  84. {cloudg-0.3.0 → cloudg-0.3.1}/tests/test_region_discovery.py +0 -0
  85. {cloudg-0.3.0 → cloudg-0.3.1}/tests/test_terraform_export.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cloudg
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: cloudg (cloud graphing) — multi-cloud infrastructure mapping, knowledge graphs and security intelligence for AWS, Azure and GCP
5
5
  Project-URL: Homepage, https://github.com/morpheuslord/cloudg
6
6
  Project-URL: Repository, https://github.com/morpheuslord/cloudg
@@ -75,7 +75,7 @@ Description-Content-Type: text/markdown
75
75
  <p align="center">
76
76
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT"></a>
77
77
  <img src="https://img.shields.io/badge/python-3.11%2B-blue.svg" alt="Python 3.11+">
78
- <img src="https://img.shields.io/badge/version-0.3.0-4c1.svg" alt="Version 0.3.0">
78
+ <img src="https://img.shields.io/badge/version-0.3.1-4c1.svg" alt="Version 0.3.1">
79
79
  <a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv"></a>
80
80
  </p>
81
81
 
@@ -325,6 +325,3 @@ Python 3.11 or newer. The moto/aiobotocore incompatibility around async response
325
325
 
326
326
  ---
327
327
 
328
- ## License
329
-
330
- MIT
@@ -5,7 +5,7 @@
5
5
  <p align="center">
6
6
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT"></a>
7
7
  <img src="https://img.shields.io/badge/python-3.11%2B-blue.svg" alt="Python 3.11+">
8
- <img src="https://img.shields.io/badge/version-0.3.0-4c1.svg" alt="Version 0.3.0">
8
+ <img src="https://img.shields.io/badge/version-0.3.1-4c1.svg" alt="Version 0.3.1">
9
9
  <a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv"></a>
10
10
  </p>
11
11
 
@@ -255,6 +255,3 @@ Python 3.11 or newer. The moto/aiobotocore incompatibility around async response
255
255
 
256
256
  ---
257
257
 
258
- ## License
259
-
260
- MIT
@@ -1,6 +1,6 @@
1
1
  """cloudg — cloud graphing: multi-cloud mapping and security intelligence."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.1"
4
4
 
5
5
  from cloudg.api import (
6
6
  AnalysisResult,
@@ -43,6 +43,34 @@ from cloudg.schema.models import (
43
43
  logger = logging.getLogger(__name__)
44
44
 
45
45
 
46
+ def resolve_iac_dirs(
47
+ iac_dir: str | None,
48
+ config_dirs: list[str] | None,
49
+ terraform_dir: str | Path | None = None,
50
+ ) -> tuple[list[str], str | None]:
51
+ """Resolve which directories the IaC scanners (Checkov, Trivy fs) target.
52
+
53
+ Priority: explicit dir -> configured dirs -> the generated Terraform
54
+ recreation of the live infrastructure. There is deliberately no fallback
55
+ to "." any more: scanning whatever directory cloudg happens to run from
56
+ is not a scan of the cloud, and its zero findings read like a clean bill
57
+ of health.
58
+
59
+ Returns:
60
+ (dirs, source) where source is "cli", "config", "terraform", or
61
+ None when there is nothing for the IaC scanners to target.
62
+ """
63
+ if iac_dir:
64
+ return [iac_dir], "cli"
65
+ if config_dirs:
66
+ return list(config_dirs), "config"
67
+ if terraform_dir:
68
+ tf_path = Path(terraform_dir)
69
+ if tf_path.is_dir() and any(tf_path.glob("*.tf.json")):
70
+ return [str(tf_path)], "terraform"
71
+ return [], None
72
+
73
+
46
74
  # ---------------------------------------------------------------------------
47
75
  # Result dataclasses
48
76
  # ---------------------------------------------------------------------------
@@ -285,14 +313,37 @@ class CloudGEngine:
285
313
  logger.error("Graph analysis failed: %s", exc)
286
314
  self._emit_error("graph_analysis", exc)
287
315
 
288
- # Resolve IaC directories
289
- resolved_iac_dirs: list[str] = []
290
- if iac_dir:
291
- resolved_iac_dirs = [iac_dir]
292
- elif self.config.scanners.iac_directories:
293
- resolved_iac_dirs = list(self.config.scanners.iac_directories)
294
- else:
295
- resolved_iac_dirs = ["."]
316
+ # Resolve IaC directories. When nothing is configured but Terraform
317
+ # recreation is enabled, generate it now and scan that: the IaC
318
+ # scanners then audit the live infrastructure via its Terraform
319
+ # representation instead of an unrelated local directory.
320
+ tf_dir: str | None = None
321
+ if (
322
+ not iac_dir
323
+ and not self.config.scanners.iac_directories
324
+ and self.config.terraform.enabled
325
+ ):
326
+ tf_dir = self.config.terraform.output_dir or str(out / "terraform")
327
+ try:
328
+ from cloudg.renderers.terraform_export import TerraformExporter
329
+
330
+ TerraformExporter(output_dir=tf_dir).export(assets, edges)
331
+ except Exception as exc:
332
+ logger.error("Terraform export for IaC scanning failed: %s", exc)
333
+ self._emit_error("terraform", exc)
334
+ tf_dir = None
335
+
336
+ resolved_iac_dirs, iac_source = resolve_iac_dirs(
337
+ iac_dir, self.config.scanners.iac_directories, tf_dir
338
+ )
339
+ if iac_source == "terraform":
340
+ logger.info("IaC scanners target the Terraform recreation at %s", resolved_iac_dirs[0])
341
+ elif not resolved_iac_dirs and "checkov" in scanner_list:
342
+ logger.warning(
343
+ "Skipping Checkov: no IaC directory configured. "
344
+ "Pass iac_dir, set scanners.iac_directories, or enable terraform "
345
+ "so the recreated infrastructure can be scanned."
346
+ )
296
347
 
297
348
  # Resolve images
298
349
  resolved_images: list[str] = images or list(self.config.scanners.trivy_images)
@@ -567,15 +567,6 @@ def run(
567
567
  if "gcp" in cfg.providers:
568
568
  console.print(f"[bold]GCP regions:[/] {cfg.gcp.regions}")
569
569
 
570
- # ── Resolve IaC directories: CLI flag → config → default "." ──
571
- resolved_iac_dirs: list[str] = []
572
- if iac_dir:
573
- resolved_iac_dirs = [iac_dir]
574
- elif cfg.scanners.iac_directories:
575
- resolved_iac_dirs = list(cfg.scanners.iac_directories)
576
- else:
577
- resolved_iac_dirs = ["."]
578
-
579
570
  # ── Resolve container images: CLI flag → config ──
580
571
  resolved_images: list[str] = []
581
572
  if images:
@@ -653,6 +644,7 @@ def run(
653
644
  console.print(f" [red]✗ RAG export failed: {exc}[/]")
654
645
 
655
646
  # Phase 2d: Terraform Recreation
647
+ tf_dir: str | None = None
656
648
  if terraform or cfg.terraform.enabled:
657
649
  console.print("\n[bold]Phase 2d: Terraform Recreation[/]")
658
650
  try:
@@ -673,6 +665,7 @@ def run(
673
665
  console.print(f" [green]✓ Import: {tf_paths['import_commands']}[/]")
674
666
  except Exception as exc:
675
667
  console.print(f" [red]✗ Terraform export failed: {exc}[/]")
668
+ tf_dir = None
676
669
 
677
670
  import concurrent.futures
678
671
 
@@ -680,6 +673,18 @@ def run(
680
673
  console.print("\n[bold]Phase 3: Security Scanning[/bold] (Running in parallel)")
681
674
  scanner_findings: list[Any] = []
682
675
 
676
+ # ── Resolve IaC scan targets: CLI flag → config → Terraform recreation ──
677
+ # No fallback to "." — scanning the directory cloudg runs from is not a
678
+ # scan of the cloud, and its zero findings look like a clean result.
679
+ from cloudg.api import resolve_iac_dirs
680
+
681
+ resolved_iac_dirs, iac_source = resolve_iac_dirs(iac_dir, cfg.scanners.iac_directories, tf_dir)
682
+ if iac_source == "terraform":
683
+ console.print(
684
+ " [dim]IaC scanners target the Terraform recreation of the live "
685
+ f"infrastructure ({resolved_iac_dirs[0]})[/dim]"
686
+ )
687
+
683
688
  def run_prowler(prov: str) -> list[Any]:
684
689
  from cloudg.scanners.prowler import ProwlerScanner
685
690
 
@@ -780,24 +785,36 @@ def run(
780
785
  else:
781
786
  console.print(" [dim]⊘ ScoutSuite: not enabled[/dim]")
782
787
 
783
- # Checkov — always runs against resolved IaC directories
788
+ # Checkov — runs against resolved IaC directories
784
789
  if "checkov" in scanner_list:
785
- for d in resolved_iac_dirs:
786
- future_to_scanner[executor.submit(run_checkov, d)] = f"Checkov ({d})"
790
+ if resolved_iac_dirs:
791
+ for d in resolved_iac_dirs:
792
+ future_to_scanner[executor.submit(run_checkov, d)] = f"Checkov ({d})"
793
+ else:
794
+ console.print(
795
+ " [yellow]⊘ Checkov: nothing to scan — pass --iac-dir, set "
796
+ "scanners.iac_directories, or enable --terraform to scan the "
797
+ "recreated infrastructure[/yellow]"
798
+ )
787
799
  else:
788
800
  console.print(" [dim]⊘ Checkov: not enabled[/dim]")
789
801
 
790
- # Trivy — runs against container images if available, otherwise falls back to filesystem scan
802
+ # Trivy — scans container images if configured, else the IaC targets
791
803
  if "trivy" in scanner_list:
792
804
  if resolved_images:
793
805
  future_to_scanner[executor.submit(run_trivy, resolved_images)] = "Trivy (images)"
794
- else:
806
+ elif resolved_iac_dirs:
795
807
  console.print(
796
808
  " [yellow]⊘ Trivy: no images configured, falling back to filesystem scan[/yellow]"
797
809
  )
798
810
  future_to_scanner[executor.submit(run_trivy_fs, resolved_iac_dirs)] = (
799
811
  "Trivy (filesystem)"
800
812
  )
813
+ else:
814
+ console.print(
815
+ " [yellow]⊘ Trivy: nothing to scan — configure --images, "
816
+ "--iac-dir, or enable --terraform[/yellow]"
817
+ )
801
818
  else:
802
819
  console.print(" [dim]⊘ Trivy: not enabled[/dim]")
803
820
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cloudg"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "cloudg (cloud graphing) — multi-cloud infrastructure mapping, knowledge graphs and security intelligence for AWS, Azure and GCP"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -252,3 +252,43 @@ class TestIaCDirResolution:
252
252
  resolved = ["."]
253
253
 
254
254
  assert resolved == ["."]
255
+
256
+
257
+ class TestResolveIacDirs:
258
+ """IaC target resolution: CLI dir -> config dirs -> terraform recreation, never '.'."""
259
+
260
+ def test_cli_dir_wins(self, tmp_path):
261
+ from cloudg.api import resolve_iac_dirs
262
+
263
+ dirs, source = resolve_iac_dirs("./infra", ["./other"], str(tmp_path))
264
+ assert dirs == ["./infra"]
265
+ assert source == "cli"
266
+
267
+ def test_config_dirs_used_when_no_cli_dir(self):
268
+ from cloudg.api import resolve_iac_dirs
269
+
270
+ dirs, source = resolve_iac_dirs(None, ["./a", "./b"], None)
271
+ assert dirs == ["./a", "./b"]
272
+ assert source == "config"
273
+
274
+ def test_terraform_recreation_used_as_fallback(self, tmp_path):
275
+ from cloudg.api import resolve_iac_dirs
276
+
277
+ (tmp_path / "main.tf.json").write_text("{}")
278
+ dirs, source = resolve_iac_dirs(None, [], tmp_path)
279
+ assert dirs == [str(tmp_path)]
280
+ assert source == "terraform"
281
+
282
+ def test_terraform_dir_without_tf_json_is_ignored(self, tmp_path):
283
+ from cloudg.api import resolve_iac_dirs
284
+
285
+ dirs, source = resolve_iac_dirs(None, [], tmp_path)
286
+ assert dirs == []
287
+ assert source is None
288
+
289
+ def test_no_targets_means_skip_not_cwd(self):
290
+ from cloudg.api import resolve_iac_dirs
291
+
292
+ dirs, source = resolve_iac_dirs(None, [], None)
293
+ assert dirs == []
294
+ assert source is None
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes