From e8487d0686b3f7a698f36bfe34494940cf276438 Mon Sep 17 00:00:00 2001 From: StylusFrost <43682773+StylusFrost@users.noreply.github.com> Date: Fri, 24 Apr 2026 18:01:47 +0200 Subject: [PATCH] fix(sdk): unwrap namespaced config for all built-in and external providers load_and_validate_config_file only detected the namespaced format for 5 hardcoded providers (aws, gcp, azure, kubernetes, m365). For every other built-in (github, nhn, vercel, cloudflare, iac, llm, image, mongodbatlas, oraclecloud, openstack, alibabacloud, googleworkspace) and for any external plug-in, the full YAML was returned wrapped instead of the provider's own block. Replace the hardcoded list with a dynamic check: if the file has a top-level key matching the provider and its value is a dict, unwrap it. Keep the legacy flat format for AWS only (historical, pre-multicloud) and identify it by the absence of nested-dict top-level values, which prevents cross-provider config leakage when a namespaced file has no section for the requested provider. --- prowler/CHANGELOG.md | 4 +++ prowler/config/config.py | 28 ++++++++++++------- tests/config/config_test.py | 26 +++++++++++++++++ .../fixtures/config_namespaced_external.yaml | 8 ++++++ 4 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 tests/config/fixtures/config_namespaced_external.yaml diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index df7dbbd09f..f18b270017 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -9,6 +9,10 @@ All notable changes to the **Prowler SDK** are documented in this file. - Support for external/custom providers, checks, and compliance frameworks without modifying core code [(#10700)](https://github.com/prowler-cloud/prowler/pull/10700) - SARIF output format for the IaC provider, enabling GitHub Code Scanning integration via `--output-formats sarif` [(#10626)](https://github.com/prowler-cloud/prowler/pull/10626) +### 🐞 Fixed + +- `load_and_validate_config_file` now unwraps namespaced config for every built-in and external provider, and no longer leaks the full file as the provider's config when the file is namespaced [(#10700)](https://github.com/prowler-cloud/prowler/pull/10700) + --- ## [5.24.3] (Prowler v5.24.3) diff --git a/prowler/config/config.py b/prowler/config/config.py index 7b26e81e8a..d262f1578b 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -239,18 +239,26 @@ def load_and_validate_config_file(provider: str, config_file_path: str) -> dict: with open(config_file_path, "r", encoding=encoding_format_utf_8) as f: config_file = yaml.safe_load(f) - # Not to introduce a breaking change, allow the old format config file without any provider keys - # and a new format with a key for each provider to include their configuration values within. - if any( - key in config_file - for key in ["aws", "gcp", "azure", "kubernetes", "m365"] + # Namespaced format: each provider has its own top-level key. + # Works for every built-in and every external plugin without a hardcoded list. + # Flat legacy format is AWS-only (historical, pre-multicloud). We identify it + # by the absence of nested-dict top-level values (namespaced files always + # have dict values; the legacy AWS format only has primitives/lists). + if ( + isinstance(config_file, dict) + and provider in config_file + and isinstance(config_file[provider], dict) ): - config = config_file.get(provider, {}) + config = config_file.get(provider, {}) or {} + elif ( + isinstance(config_file, dict) + and config_file + and provider == "aws" + and not any(isinstance(v, dict) for v in config_file.values()) + ): + config = config_file else: - config = config_file if config_file else {} - # Not to break Azure, K8s and GCP does not support or use the old config format - if provider in ["azure", "gcp", "kubernetes", "m365"]: - config = {} + config = {} return config diff --git a/tests/config/config_test.py b/tests/config/config_test.py index 08b1f7d5e0..631fda594c 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -465,6 +465,32 @@ class Test_Config: assert load_and_validate_config_file("azure", config_test_file) == {} assert load_and_validate_config_file("kubernetes", config_test_file) == {} + def test_load_and_validate_config_file_namespaced_non_listed_provider(self): + path = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) + config_test_file = f"{path}/fixtures/config_namespaced_external.yaml" + # github is a built-in not in the legacy hardcoded list; namespaced format must unwrap it. + assert load_and_validate_config_file("github", config_test_file) == { + "token": "abc", + "org": "prowler-cloud", + } + + def test_load_and_validate_config_file_namespaced_external_provider(self): + path = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) + config_test_file = f"{path}/fixtures/config_namespaced_external.yaml" + # External plug-in provider: namespaced format must unwrap its block. + assert load_and_validate_config_file("custom_plugin", config_test_file) == { + "setting": "value", + "nested": {"key": 42}, + } + + def test_load_and_validate_config_file_namespaced_missing_provider(self): + path = pathlib.Path(os.path.dirname(os.path.realpath(__file__))) + config_test_file = f"{path}/fixtures/config_namespaced_external.yaml" + # Provider with no section in a namespaced file must return empty config, + # not the full file (prevents cross-provider config leakage). + assert load_and_validate_config_file("aws", config_test_file) == {} + assert load_and_validate_config_file("gcp", config_test_file) == {} + def test_load_and_validate_config_file_invalid_config_file_path(self, caplog): provider = "aws" config_file_path = "invalid/path/to/fixer_config.yaml" diff --git a/tests/config/fixtures/config_namespaced_external.yaml b/tests/config/fixtures/config_namespaced_external.yaml new file mode 100644 index 0000000000..ec9f75c698 --- /dev/null +++ b/tests/config/fixtures/config_namespaced_external.yaml @@ -0,0 +1,8 @@ +# Namespaced config covering a non-listed built-in (github) and an external plugin. +github: + token: abc + org: prowler-cloud +custom_plugin: + setting: value + nested: + key: 42