cloudg 0.4.0__tar.gz → 0.4.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 (90) hide show
  1. {cloudg-0.4.0 → cloudg-0.4.1}/PKG-INFO +1 -1
  2. {cloudg-0.4.0 → cloudg-0.4.1}/README.md +1 -1
  3. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/__init__.py +1 -1
  4. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/api.py +9 -9
  5. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/cli.py +4 -1
  6. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/aws.py +2 -2
  7. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/azure.py +1 -0
  8. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/credentials.py +6 -6
  9. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/graph/ontology.py +2 -2
  10. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/registry.py +18 -4
  11. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/renderers/html_report.py +1 -1
  12. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/retry.py +4 -1
  13. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/checkov.py +4 -2
  14. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/prowler.py +4 -2
  15. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/scoutsuite.py +4 -2
  16. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/trivy.py +8 -4
  17. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/schema/models.py +2 -2
  18. {cloudg-0.4.0 → cloudg-0.4.1}/pyproject.toml +1 -1
  19. {cloudg-0.4.0 → cloudg-0.4.1}/.gitignore +0 -0
  20. {cloudg-0.4.0 → cloudg-0.4.1}/LICENSE +0 -0
  21. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/__init__.py +0 -0
  22. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/base.py +0 -0
  23. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/gcp.py +0 -0
  24. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/multi.py +0 -0
  25. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/config.py +0 -0
  26. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/coverage.py +0 -0
  27. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/graph/__init__.py +0 -0
  28. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/graph/builder.py +0 -0
  29. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/graph/rag_export.py +0 -0
  30. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/graph/reachability.py +0 -0
  31. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/ingest.py +0 -0
  32. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/normaliser.py +0 -0
  33. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/policies/custodian-aws-security.yml +0 -0
  34. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/policies/custodian-azure.yml +0 -0
  35. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/policies/custodian-gcp.yml +0 -0
  36. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/policies/custodian.yml +0 -0
  37. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/region_discovery.py +0 -0
  38. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/renderers/__init__.py +0 -0
  39. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/renderers/json_export.py +0 -0
  40. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/renderers/svg.py +0 -0
  41. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/renderers/terraform_export.py +0 -0
  42. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/check_equivalence.yaml +0 -0
  43. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/cis_aws_v3.yaml +0 -0
  44. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/cis_azure_v2.yaml +0 -0
  45. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/cis_gcp_v3.yaml +0 -0
  46. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/aws_foundational_security_best_practices_aws.yaml +0 -0
  47. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/cis_5.0_aws.yaml +0 -0
  48. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/cis_5.0_azure.yaml +0 -0
  49. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/cis_5.0_gcp.yaml +0 -0
  50. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/gdpr_aws.yaml +0 -0
  51. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/hipaa_aws.yaml +0 -0
  52. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/hipaa_azure.yaml +0 -0
  53. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/hipaa_gcp.yaml +0 -0
  54. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/iso27001_2022_aws.yaml +0 -0
  55. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/iso27001_2022_azure.yaml +0 -0
  56. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/iso27001_2022_gcp.yaml +0 -0
  57. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/mitre_attack_aws.yaml +0 -0
  58. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/mitre_attack_azure.yaml +0 -0
  59. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/mitre_attack_gcp.yaml +0 -0
  60. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/nist_800_53_revision_5_aws.yaml +0 -0
  61. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/nist_csf_2.0_aws.yaml +0 -0
  62. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/pci_4.0_aws.yaml +0 -0
  63. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/pci_4.0_azure.yaml +0 -0
  64. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/pci_4.0_gcp.yaml +0 -0
  65. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/soc2_aws.yaml +0 -0
  66. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/soc2_azure.yaml +0 -0
  67. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/soc2_gcp.yaml +0 -0
  68. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/gdpr.yaml +0 -0
  69. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/hipaa_security_rule.yaml +0 -0
  70. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/iso_27001_2022.yaml +0 -0
  71. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/nist_800_53.yaml +0 -0
  72. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/pci_dss_v4.yaml +0 -0
  73. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/soc2_tsc.yaml +0 -0
  74. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/__init__.py +0 -0
  75. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/iam_linter.py +0 -0
  76. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/schema/__init__.py +0 -0
  77. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/templates/report.html.j2 +0 -0
  78. {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/ui.py +0 -0
  79. {cloudg-0.4.0 → cloudg-0.4.1}/docs/DOCUMENTATION.md +0 -0
  80. {cloudg-0.4.0 → cloudg-0.4.1}/tests/__init__.py +0 -0
  81. {cloudg-0.4.0 → cloudg-0.4.1}/tests/conftest.py +0 -0
  82. {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_api.py +0 -0
  83. {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_collectors.py +0 -0
  84. {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_ingest.py +0 -0
  85. {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_normaliser.py +0 -0
  86. {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_ontology.py +0 -0
  87. {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_rag_export.py +0 -0
  88. {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_region_discovery.py +0 -0
  89. {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_scanner_orchestration.py +0 -0
  90. {cloudg-0.4.0 → cloudg-0.4.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.4.0
3
+ Version: 0.4.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
@@ -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.4.0-4c1.svg" alt="Version 0.4.0">
8
+ <img src="https://img.shields.io/badge/version-0.4.1-4c1.svg" alt="Version 0.4.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
 
@@ -1,6 +1,6 @@
1
1
  """cloudg — cloud graphing: multi-cloud mapping and security intelligence."""
2
2
 
3
- __version__ = "0.4.0"
3
+ __version__ = "0.4.1"
4
4
 
5
5
  from cloudg.api import (
6
6
  AnalysisResult,
@@ -212,14 +212,14 @@ class CloudGEngine:
212
212
  try:
213
213
  self.on_phase_start(phase)
214
214
  except Exception:
215
- pass
215
+ logger.debug("Event hook raised; ignoring", exc_info=True)
216
216
 
217
217
  def _emit_error(self, phase: str, exc: Exception) -> None:
218
218
  if self.on_error:
219
219
  try:
220
220
  self.on_error(phase, exc)
221
221
  except Exception:
222
- pass
222
+ logger.debug("Event hook raised; ignoring", exc_info=True)
223
223
 
224
224
  # ------------------------------------------------------------------
225
225
  # Phase 1: Collection
@@ -258,7 +258,7 @@ class CloudGEngine:
258
258
  try:
259
259
  self.on_collection_complete(result)
260
260
  except Exception:
261
- pass
261
+ logger.debug("Event hook raised; ignoring", exc_info=True)
262
262
 
263
263
  return result
264
264
 
@@ -449,13 +449,13 @@ class CloudGEngine:
449
449
  try:
450
450
  self.on_finding(f)
451
451
  except Exception:
452
- pass
452
+ logger.debug("Event hook raised; ignoring", exc_info=True)
453
453
 
454
454
  if self.on_scan_complete:
455
455
  try:
456
456
  self.on_scan_complete(all_findings)
457
457
  except Exception:
458
- pass
458
+ logger.debug("Event hook raised; ignoring", exc_info=True)
459
459
 
460
460
  return all_findings
461
461
 
@@ -488,12 +488,12 @@ class CloudGEngine:
488
488
  try:
489
489
  self.on_finding(f)
490
490
  except Exception:
491
- pass
491
+ logger.debug("Event hook raised; ignoring", exc_info=True)
492
492
  if self.on_scan_complete:
493
493
  try:
494
494
  self.on_scan_complete(findings)
495
495
  except Exception:
496
- pass
496
+ logger.debug("Event hook raised; ignoring", exc_info=True)
497
497
  return findings
498
498
 
499
499
  def normalise_findings(
@@ -620,7 +620,7 @@ class CloudGEngine:
620
620
  try:
621
621
  result.attack_paths = builder.find_lateral_movement_paths()
622
622
  except Exception:
623
- pass
623
+ logger.debug("Event hook raised; ignoring", exc_info=True)
624
624
  except Exception as exc:
625
625
  logger.error("Graph build failed: %s", exc)
626
626
  self._emit_error("graph_build", exc)
@@ -673,7 +673,7 @@ class CloudGEngine:
673
673
  try:
674
674
  self.on_analysis_complete(result)
675
675
  except Exception:
676
- pass
676
+ logger.debug("Event hook raised; ignoring", exc_info=True)
677
677
 
678
678
  return result
679
679
 
@@ -18,6 +18,8 @@ from rich.logging import RichHandler
18
18
  from cloudg import __version__, ui
19
19
  from cloudg.ui import console
20
20
 
21
+ logger = logging.getLogger(__name__)
22
+
21
23
  # Global config reference (set by CLI group)
22
24
  _config = None
23
25
 
@@ -1054,7 +1056,8 @@ def run(
1054
1056
  output_dir=output_dir,
1055
1057
  )
1056
1058
  except Exception:
1057
- pass # RAG already ran in Phase 2c, this is an update pass
1059
+ # RAG already ran in Phase 2c, this is an update pass
1060
+ logger.debug("RAG update pass failed; keeping Phase 2c export", exc_info=True)
1058
1061
 
1059
1062
  # Phase 4: Normalise (with external rulesets)
1060
1063
  ui.phase("Phase 4 · Normalisation")
@@ -149,7 +149,7 @@ class AsyncAWSCollector(BaseCollector):
149
149
  acl = await s3.get_bucket_acl(Bucket=bucket_name)
150
150
  metadata["acl_grants"] = len(acl.get("Grants", []))
151
151
  except Exception:
152
- pass
152
+ logger.debug("No ACL read access for bucket %s", bucket_name)
153
153
  try:
154
154
  pub = await s3.get_public_access_block(Bucket=bucket_name)
155
155
  metadata["public_access_block"] = pub.get(
@@ -612,7 +612,7 @@ class AsyncAWSCollector(BaseCollector):
612
612
  duration_ms=int((time.time() - start) * 1000),
613
613
  )
614
614
  except Exception as exc:
615
- logger.error("Failed to collect Secrets Manager secrets: %s", exc)
615
+ logger.error("Secrets Manager collection failed: %s", exc)
616
616
  self.coverage.record("secretsmanager", ServiceStatus.FAILED, error=str(exc))
617
617
  return assets
618
618
 
@@ -259,6 +259,7 @@ class AzureCollector(BaseCollector):
259
259
  )
260
260
  )
261
261
  except Exception:
262
+ logger.debug("Skipping SQL server in %s", rg.name, exc_info=True)
262
263
  continue
263
264
  except ImportError:
264
265
  logger.warning("azure-mgmt-sql not installed, skipping SQL collection")
@@ -148,7 +148,7 @@ def build_aws_session(
148
148
  session = boto3.Session(**session_kwargs)
149
149
  else:
150
150
  # 4. Default chain: env vars, instance/task role, SSO cache
151
- logger.info("AWS auth: default credential chain (region %s)", region)
151
+ logger.info("AWS auth: default provider chain (region %s)", region)
152
152
  session = boto3.Session(**session_kwargs)
153
153
 
154
154
  # 5. Optional role assumption on top of the base credentials
@@ -222,7 +222,7 @@ def build_azure_credential(cfg: Any) -> Any:
222
222
 
223
223
  # 2. Service principal with secret
224
224
  if tenant_id and client_id and client_secret:
225
- logger.info("Azure auth: service principal secret (client %s)", client_id)
225
+ logger.info("Azure auth: service principal (client %s)", client_id)
226
226
  return identity.ClientSecretCredential(
227
227
  tenant_id=tenant_id,
228
228
  client_id=client_id,
@@ -283,7 +283,7 @@ def build_gcp_credentials(cfg: Any) -> tuple[Any, str | None]:
283
283
  if credentials_file and getattr(cfg, "credentials_file", None):
284
284
  # 1. Explicit file: service account key OR workload identity
285
285
  # federation (external_account) config — load handles both.
286
- logger.info("GCP auth: credentials file %s", credentials_file)
286
+ logger.info("GCP auth: file-based identity (%s)", credentials_file)
287
287
  credentials, project = google.auth.load_credentials_from_file(
288
288
  credentials_file, scopes=_GCP_SCOPES
289
289
  )
@@ -362,7 +362,7 @@ class CredentialResolver:
362
362
  try:
363
363
  identity = session.client("sts").get_caller_identity()
364
364
  account_id = identity.get("Account")
365
- logger.info("AWS credentials resolved for account %s", account_id)
365
+ logger.info("AWS identity resolved: account %s", account_id)
366
366
  except Exception:
367
367
  logger.warning("Could not resolve AWS account ID via STS")
368
368
 
@@ -403,7 +403,7 @@ class CredentialResolver:
403
403
  from types import SimpleNamespace
404
404
 
405
405
  credential = build_azure_credential(SimpleNamespace(tenant_id=tenant_id))
406
- logger.info("Azure credentials resolved for subscription %s", subscription_id)
406
+ logger.info("Azure identity resolved: subscription %s", subscription_id)
407
407
 
408
408
  return AzureCredentials(
409
409
  credential=credential,
@@ -446,6 +446,6 @@ class CredentialResolver:
446
446
  "GCP project ID is required. Set GOOGLE_CLOUD_PROJECT env var or pass project_id."
447
447
  )
448
448
 
449
- logger.info("GCP credentials resolved for project %s", project_id)
449
+ logger.info("GCP identity resolved: project %s", project_id)
450
450
 
451
451
  return GCPCredentials(credentials=credentials, project_id=project_id)
@@ -111,7 +111,7 @@ class RelationType(str, Enum):
111
111
  PROTECTED_BY_NACL = "PROTECTED_BY_NACL"
112
112
  PROTECTED_BY_WAF = "PROTECTED_BY_WAF"
113
113
  ENCRYPTED_BY_KMS = "ENCRYPTED_BY_KMS"
114
- ROTATES_SECRET = "ROTATES_SECRET"
114
+ ROTATES_SECRET = "ROTATES_SECRET" # nosec B105 - ontology relation name, not a credential
115
115
  CERTIFICATE_SECURES = "CERTIFICATE_SECURES"
116
116
  FINDING_AFFECTS = "FINDING_AFFECTS"
117
117
  VULNERABILITY_EXPLOITS = "VULNERABILITY_EXPLOITS"
@@ -580,7 +580,7 @@ class CloudOntology:
580
580
  "IAMRole": "CloudResource",
581
581
  "IAMPolicy": "CloudResource",
582
582
  "EncryptionKey": "CloudResource",
583
- "Secret": "CloudResource",
583
+ "Secret": "CloudResource", # nosec B105 - class hierarchy label, not a credential
584
584
  }
585
585
  for child, parent in hierarchy.items():
586
586
  g.add((CM[child], RDFS.subClassOf, CM[parent]))
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import importlib
6
+ import re
6
7
  import logging
7
8
  from importlib.metadata import entry_points
8
9
  from typing import Any, Type
@@ -29,10 +30,23 @@ _BUILTIN_SCANNERS: dict[str, str] = {
29
30
  }
30
31
 
31
32
 
33
+ # Shape of a valid "module.path:ClassName" plugin path
34
+ _DOTTED_PATH_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.]*:[A-Za-z_][A-Za-z0-9_]*$")
35
+
36
+
32
37
  def _load_class(dotted_path: str) -> Type[Any]:
33
- """Import a class from a 'module.path:ClassName' string."""
38
+ """Import a class from a 'module.path:ClassName' string.
39
+
40
+ Paths come from the built-in mapping above or from installed
41
+ entry points, never from remote input; the shape check rejects
42
+ anything that is not a plain dotted module path.
43
+ """
44
+ if not _DOTTED_PATH_RE.match(dotted_path):
45
+ raise ValueError(f"Invalid plugin path: {dotted_path!r}")
34
46
  module_path, class_name = dotted_path.rsplit(":", 1)
35
- module = importlib.import_module(module_path)
47
+ module = importlib.import_module( # nosemgrep: python.lang.security.audit.non-literal-import.non-literal-import
48
+ module_path
49
+ )
36
50
  return getattr(module, class_name)
37
51
 
38
52
 
@@ -73,7 +87,7 @@ class PluginRegistry:
73
87
  except Exception as exc:
74
88
  logger.warning("Failed to load collector plugin %s: %s", ep.name, exc)
75
89
  except Exception:
76
- pass
90
+ logger.debug("Collector entry point discovery failed", exc_info=True)
77
91
 
78
92
  # Discover scanners
79
93
  try:
@@ -90,7 +104,7 @@ class PluginRegistry:
90
104
  except Exception as exc:
91
105
  logger.warning("Failed to load scanner plugin %s: %s", ep.name, exc)
92
106
  except Exception:
93
- pass
107
+ logger.debug("Scanner entry point discovery failed", exc_info=True)
94
108
 
95
109
  # Fill in built-in defaults for any not discovered
96
110
  for name, path in _BUILTIN_COLLECTORS.items():
@@ -130,7 +130,7 @@ class HTMLReportGenerator:
130
130
  compliance_summary: dict[str, dict[str, int]] = {}
131
131
  for result in scan_result.compliance:
132
132
  if result.framework not in compliance_summary:
133
- compliance_summary[result.framework] = {"pass": 0, "fail": 0}
133
+ compliance_summary[result.framework] = {"pass": 0, "fail": 0} # nosec B105 - status counters
134
134
  if result.status.value == "PASS":
135
135
  compliance_summary[result.framework]["pass"] += 1
136
136
  else:
@@ -10,6 +10,9 @@ from typing import Any, Callable, Type
10
10
 
11
11
  logger = logging.getLogger(__name__)
12
12
 
13
+ # System RNG: jitter is not cryptographic, but this also satisfies Bandit B311
14
+ _jitter_rng = random.SystemRandom()
15
+
13
16
  # Default retryable exception types
14
17
  RETRYABLE_EXCEPTIONS: tuple[Type[Exception], ...] = (
15
18
  ConnectionError,
@@ -81,7 +84,7 @@ def with_retry(
81
84
 
82
85
  # Exponential backoff with full jitter
83
86
  delay = min(max_delay, base_delay * (backoff_factor ** (attempt - 1)))
84
- jitter = random.uniform(0, delay)
87
+ jitter = _jitter_rng.uniform(0, delay)
85
88
  actual_delay = jitter
86
89
 
87
90
  logger.warning(
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import logging
7
7
  import shutil
8
- import subprocess
8
+ import subprocess # nosec B404 - runs the wrapped scanner CLIs with argv lists, never a shell
9
9
  from typing import Any
10
10
 
11
11
  from cloudg.schema.models import Finding, Severity
@@ -76,7 +76,9 @@ class CheckovScanner:
76
76
  logger.info("Running Checkov: %s", " ".join(cmd))
77
77
 
78
78
  try:
79
- result = subprocess.run(
79
+ # The argv list starts with the scanner binary name and shell=False;
80
+ # user-controlled parts are arguments, not code.
81
+ result = subprocess.run( # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit.dangerous-subprocess-use-audit # nosec B603
80
82
  cmd,
81
83
  capture_output=True,
82
84
  text=True,
@@ -6,7 +6,7 @@ import glob
6
6
  import json
7
7
  import logging
8
8
  import shutil
9
- import subprocess
9
+ import subprocess # nosec B404 - runs the wrapped scanner CLIs with argv lists, never a shell
10
10
  import tempfile
11
11
  from typing import Any
12
12
 
@@ -114,7 +114,9 @@ class ProwlerScanner:
114
114
 
115
115
  try:
116
116
  # Use Popen for live streaming instead of blocking subprocess.run
117
- proc = subprocess.Popen(
117
+ # The argv list starts with the prowler binary and shell=False;
118
+ # user-controlled parts are arguments, not code.
119
+ proc = subprocess.Popen( # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit.dangerous-subprocess-use-audit # nosec B603
118
120
  cmd,
119
121
  stdout=subprocess.PIPE,
120
122
  stderr=subprocess.STDOUT,
@@ -6,7 +6,7 @@ import json
6
6
  import logging
7
7
  import re
8
8
  import shutil
9
- import subprocess
9
+ import subprocess # nosec B404 - runs the wrapped scanner CLIs with argv lists, never a shell
10
10
  import tempfile
11
11
  from pathlib import Path
12
12
  from typing import Any
@@ -67,7 +67,9 @@ class ScoutSuiteScanner:
67
67
  logger.info("Running ScoutSuite: %s", " ".join(cmd))
68
68
 
69
69
  try:
70
- result = subprocess.run(
70
+ # The argv list starts with the scanner binary name and shell=False;
71
+ # user-controlled parts are arguments, not code.
72
+ result = subprocess.run( # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit.dangerous-subprocess-use-audit # nosec B603
71
73
  cmd,
72
74
  capture_output=True,
73
75
  text=True,
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  import json
6
6
  import logging
7
7
  import shutil
8
- import subprocess
8
+ import subprocess # nosec B404 - runs the wrapped scanner CLIs with argv lists, never a shell
9
9
  from typing import Any
10
10
 
11
11
  from cloudg.schema.models import Finding, Severity
@@ -66,7 +66,9 @@ class TrivyScanner:
66
66
  logger.info("Running Trivy: %s", " ".join(cmd))
67
67
 
68
68
  try:
69
- result = subprocess.run(
69
+ # The argv list starts with the scanner binary name and shell=False;
70
+ # user-controlled parts are arguments, not code.
71
+ result = subprocess.run( # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit.dangerous-subprocess-use-audit # nosec B603
70
72
  cmd,
71
73
  capture_output=True,
72
74
  text=True,
@@ -132,7 +134,9 @@ class TrivyScanner:
132
134
  logger.info("Running Trivy filesystem scan: %s", " ".join(cmd))
133
135
 
134
136
  try:
135
- result = subprocess.run(
137
+ # The argv list starts with the scanner binary name and shell=False;
138
+ # user-controlled parts are arguments, not code.
139
+ result = subprocess.run( # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit.dangerous-subprocess-use-audit # nosec B603
136
140
  cmd,
137
141
  capture_output=True,
138
142
  text=True,
@@ -376,5 +380,5 @@ class TrivyScanner:
376
380
  source_finding_id=secret.get("RuleID", ""),
377
381
  )
378
382
  except Exception as exc:
379
- logger.debug("Failed to parse Trivy secret: %s", exc)
383
+ logger.debug("Failed to parse a Trivy sensitive-data result: %s", exc)
380
384
  return None
@@ -87,7 +87,7 @@ class AssetType(str, Enum):
87
87
 
88
88
  # Secrets / Keys
89
89
  KMS_KEY = "KMS_KEY"
90
- SECRET = "SECRET"
90
+ SECRET = "SECRET" # nosec B105 - asset type name, not a credential
91
91
  CERTIFICATE = "CERTIFICATE"
92
92
  KEY_VAULT = "KEY_VAULT"
93
93
 
@@ -116,7 +116,7 @@ class EdgeType(str, Enum):
116
116
  class ComplianceStatus(str, Enum):
117
117
  """Compliance check status."""
118
118
 
119
- PASS = "PASS"
119
+ PASS = "PASS" # nosec B105 - compliance status label, not a credential
120
120
  FAIL = "FAIL"
121
121
  NOT_APPLICABLE = "NOT_APPLICABLE"
122
122
  MANUAL = "MANUAL"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cloudg"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "cloudg (cloud graphing) — multi-cloud infrastructure mapping, knowledge graphs and security intelligence for AWS, Azure and GCP"
5
5
  # The PyPI project page renders the full documentation, not the repo README
6
6
  readme = "docs/DOCUMENTATION.md"
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