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.
- {cloudg-0.4.0 → cloudg-0.4.1}/PKG-INFO +1 -1
- {cloudg-0.4.0 → cloudg-0.4.1}/README.md +1 -1
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/__init__.py +1 -1
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/api.py +9 -9
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/cli.py +4 -1
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/aws.py +2 -2
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/azure.py +1 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/credentials.py +6 -6
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/graph/ontology.py +2 -2
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/registry.py +18 -4
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/renderers/html_report.py +1 -1
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/retry.py +4 -1
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/checkov.py +4 -2
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/prowler.py +4 -2
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/scoutsuite.py +4 -2
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/trivy.py +8 -4
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/schema/models.py +2 -2
- {cloudg-0.4.0 → cloudg-0.4.1}/pyproject.toml +1 -1
- {cloudg-0.4.0 → cloudg-0.4.1}/.gitignore +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/LICENSE +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/__init__.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/base.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/gcp.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/collectors/multi.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/config.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/coverage.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/graph/__init__.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/graph/builder.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/graph/rag_export.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/graph/reachability.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/ingest.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/normaliser.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/policies/custodian-aws-security.yml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/policies/custodian-azure.yml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/policies/custodian-gcp.yml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/policies/custodian.yml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/region_discovery.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/renderers/__init__.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/renderers/json_export.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/renderers/svg.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/renderers/terraform_export.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/check_equivalence.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/cis_aws_v3.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/cis_azure_v2.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/cis_gcp_v3.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/aws_foundational_security_best_practices_aws.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/cis_5.0_aws.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/cis_5.0_azure.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/cis_5.0_gcp.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/gdpr_aws.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/hipaa_aws.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/hipaa_azure.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/hipaa_gcp.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/iso27001_2022_aws.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/iso27001_2022_azure.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/iso27001_2022_gcp.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/mitre_attack_aws.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/mitre_attack_azure.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/mitre_attack_gcp.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/nist_800_53_revision_5_aws.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/nist_csf_2.0_aws.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/pci_4.0_aws.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/pci_4.0_azure.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/pci_4.0_gcp.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/soc2_aws.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/soc2_azure.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/frameworks/soc2_gcp.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/gdpr.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/hipaa_security_rule.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/iso_27001_2022.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/nist_800_53.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/pci_dss_v4.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/rules/soc2_tsc.yaml +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/__init__.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/scanners/iam_linter.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/schema/__init__.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/templates/report.html.j2 +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/cloudg/ui.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/docs/DOCUMENTATION.md +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/tests/__init__.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/tests/conftest.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_api.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_collectors.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_ingest.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_normaliser.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_ontology.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_rag_export.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_region_discovery.py +0 -0
- {cloudg-0.4.0 → cloudg-0.4.1}/tests/test_scanner_orchestration.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
|
@@ -212,14 +212,14 @@ class CloudGEngine:
|
|
|
212
212
|
try:
|
|
213
213
|
self.on_phase_start(phase)
|
|
214
214
|
except Exception:
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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("
|
|
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
|
|
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|