Repeated lookups for unknown provider names (built-ins, typos, names
with no registered entry point) re-iterated entry_points() on every
call because only hits were cached. importlib.metadata.entry_points()
walks the metadata of every installed Python package, so the cost is
proportional to the size of the venv, not just to Prowler.
Caches None on miss so subsequent lookups hit the existing
`if name in Provider._ep_providers` short-circuit and return
immediately. Aligns Provider._ep_providers with the symmetric cache
in tool_wrapper._ep_class_cache, which already had this behavior.
Includes a regression test that mocks importlib.metadata.entry_points,
calls _load_ep_provider("nonexistent") twice, and asserts entry_points
is invoked exactly once.
Also underscore-prefix the remaining unused parameters on the abstract
Provider stubs (get_output_options, get_stdout_detail,
generate_compliance_output, display_compliance_table) so vulture stops
flagging them now that the file is in the diff. Same pattern applied
in bbe3a7dbf for the previous batch.
The entry-point loop in get_available_compliance_frameworks appended
framework names without checking whether the same name was already
provided by a built-in. The loader (Compliance.get_bulk) already
respects "built-in wins" via an explicit guard, but the listing
function did not, causing --list-compliance and
--list-compliance-requirements to print the same name twice on
collision with a built-in.
Adds the same dedup guard already used by the universal frameworks
loop a few lines above, restoring symmetry across the three
compliance discovery layers.
Includes a regression test that registers a fake entry point with
a name colliding with a known built-in (cis_2.0_aws) and asserts
the framework appears exactly once in the resulting list.
Two unrelated lint blockers that CI surfaced once the file was in scope
of `black --check .` and `vulture` on the changed-file path:
- Reformat one if/and conditional that black wanted on a single line.
- Underscore-prefix unused parameter names on FakeProvider stub methods
(from_cli_args, get_output_options, display_compliance_table,
generate_compliance_output, FakePureContractProvider.from_cli_args)
and the throwaway **kw in a MagicMock side_effect lambda. These are
test fixtures whose method bodies don't reference those args; the
signatures must match the abstract contract on Provider, so we keep
positions/types and just rename to indicate intentional non-use.
No logic change.
Calling importlib.util.find_spec on prowler.providers.{provider}.services
for an external provider propagates ModuleNotFoundError when the parent
package prowler.providers.{provider} does not exist, instead of returning
None. This caused recover_checks_from_provider, _resolve_check_module and
Scan.scan to fail with "No module named 'prowler.providers.{external}'"
even though the plug-in registered its checks via entry points correctly.
Gate the built-in branch on Provider.is_builtin (which already wraps the
find_spec in try/except) and reuse _resolve_check_module from Scan.scan
so external providers fall through to the entry-point lookup.
Conflict in prowler/config/config.py resolved by combining both branches:
- HEAD: external compliance discovery via entry points (PROWLER-1391)
- master: multi-provider framework JSONs scanned at top-level compliance/ (#10300)
Order: built-in per-provider -> built-in multi-provider -> external entry points.
Built-ins first so they win on name collisions against external registrations.
Supporting external plug-ins to register multi-provider frameworks is tracked
in PROWLER-1444.
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.