diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 451313e595..683da84487 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -38,6 +38,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - OpenStack block storage service with 7 security checks [(#10120)](https://github.com/prowler-cloud/prowler/pull/10120) - OpenStack compute service with 7 security checks [(#9944)](https://github.com/prowler-cloud/prowler/pull/9944) - OpenStack image service with 6 security checks [(#10096)](https://github.com/prowler-cloud/prowler/pull/10096) +- IaC `--provider-uid` flag to specify the provider UID for the IaC provider [(#10233)](https://github.com/prowler-cloud/prowler/pull/10233) ### 🔄 Changed diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py index 860393a674..6d3ad007b6 100644 --- a/prowler/lib/cli/parser.py +++ b/prowler/lib/cli/parser.py @@ -221,7 +221,8 @@ Detailed documentation at https://docs.prowler.com action="store_true", help=( "Send OCSF output to Prowler Cloud ingestion endpoint. " - "Requires PROWLER_API_KEY environment variable." + "Requires PROWLER_API_KEY environment variable. " + "For the IaC provider, --provider-uid is also required." ), ) diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index c33ba6b20d..304a1857bf 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -331,8 +331,9 @@ class Finding(BaseModel): elif provider.type == "iac": output_data["auth_method"] = provider.auth_method - output_data["account_uid"] = "iac" - output_data["account_name"] = "iac" + provider_uid = getattr(provider, "provider_uid", None) + output_data["account_uid"] = provider_uid if provider_uid else "iac" + output_data["account_name"] = provider_uid if provider_uid else "iac" output_data["resource_name"] = getattr( check_output, "resource_name", "" ) diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 99729307de..0f028106bb 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -273,6 +273,7 @@ class Provider(ABC): github_username=arguments.github_username, personal_access_token=arguments.personal_access_token, oauth_app_token=arguments.oauth_app_token, + provider_uid=arguments.provider_uid, ) elif "llm" in provider_class_name.lower(): provider_class( diff --git a/prowler/providers/iac/iac_provider.py b/prowler/providers/iac/iac_provider.py index 7933488ab4..e7b83995a7 100644 --- a/prowler/providers/iac/iac_provider.py +++ b/prowler/providers/iac/iac_provider.py @@ -38,6 +38,7 @@ class IacProvider(Provider): github_username: str = None, personal_access_token: str = None, oauth_app_token: str = None, + provider_uid: str = None, ): logger.info("Instantiating IAC Provider...") @@ -47,6 +48,7 @@ class IacProvider(Provider): self.exclude_path = exclude_path self.region = "branch" self.audited_account = "local-iac" + self._provider_uid = provider_uid self._session = None self._identity = "prowler" self._auth_method = "No auth" @@ -146,6 +148,10 @@ class IacProvider(Provider): def fixer_config(self): return self._fixer_config + @property + def provider_uid(self): + return self._provider_uid + def __del__(self): """Cleanup temporary directory when provider is destroyed""" self.cleanup() diff --git a/prowler/providers/iac/lib/arguments/arguments.py b/prowler/providers/iac/lib/arguments/arguments.py index d7d04fa7e3..242138145e 100644 --- a/prowler/providers/iac/lib/arguments/arguments.py +++ b/prowler/providers/iac/lib/arguments/arguments.py @@ -1,3 +1,5 @@ +import re + SCANNERS_CHOICES = [ "vuln", "misconfig", @@ -68,6 +70,12 @@ def init_parser(self): default=None, help="GitHub OAuth app token for authenticated repository cloning. If not provided, will use GITHUB_OAUTH_APP_TOKEN env var.", ) + iac_scan_subparser.add_argument( + "--provider-uid", + dest="provider_uid", + default=None, + help="Unique identifier for the IaC provider. Required when using --export-ocsf.", + ) def validate_arguments(arguments): @@ -80,4 +88,19 @@ def validate_arguments(arguments): False, "--scan-path (-P) and --scan-repository-url (-R) are mutually exclusive. Please specify only one.", ) + export_ocsf = getattr(arguments, "export_ocsf", False) + provider_uid = getattr(arguments, "provider_uid", None) + if export_ocsf and not provider_uid: + return ( + False, + "--provider-uid is required when using --export-ocsf with the IAC provider.", + ) + if provider_uid and not re.match( + r"^(https?://|git@|ssh://)[^\s/]+[^\s]*\.git$|^(https?://)[^\s/]+[^\s]*$", + provider_uid, + ): + return ( + False, + "--provider-uid must be a valid repository URL (e.g., https://github.com/user/repo or https://github.com/user/repo.git).", + ) return (True, "") diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index 6756233322..566824c82f 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -689,6 +689,7 @@ class TestFinding: provider.type = "iac" provider.scan_repository_url = "https://github.com/user/repo" provider.auth_method = "No auth" + provider.provider_uid = None # Mock check result check_output = MagicMock() diff --git a/tests/providers/iac/lib/arguments/iac_arguments_test.py b/tests/providers/iac/lib/arguments/iac_arguments_test.py index d6b2e92420..f5a2e7ca17 100644 --- a/tests/providers/iac/lib/arguments/iac_arguments_test.py +++ b/tests/providers/iac/lib/arguments/iac_arguments_test.py @@ -1,11 +1,11 @@ import types +from prowler.providers.iac.lib.arguments import arguments as iac_arguments + +Args = types.SimpleNamespace + def test_validate_arguments_mutual_exclusion(): - from prowler.providers.iac.lib.arguments import arguments as iac_arguments - - Args = types.SimpleNamespace - # Only scan_path (default) args = Args(scan_path=".", scan_repository_url=None) valid, msg = iac_arguments.validate_arguments(args) @@ -31,3 +31,90 @@ def test_validate_arguments_mutual_exclusion(): valid, msg = iac_arguments.validate_arguments(args) assert valid assert msg == "" + + +def test_validate_arguments_export_ocsf_requires_provider_uid(): + # --export-ocsf without provider_uid should fail + args = Args( + scan_path=".", + scan_repository_url=None, + export_ocsf=True, + provider_uid=None, + ) + valid, msg = iac_arguments.validate_arguments(args) + assert not valid + assert "--provider-uid is required" in msg + + +def test_validate_arguments_export_ocsf_with_provider_uid_passes(): + # --export-ocsf with valid provider_uid should pass + args = Args( + scan_path=".", + scan_repository_url=None, + export_ocsf=True, + provider_uid="https://github.com/user/repo.git", + ) + valid, msg = iac_arguments.validate_arguments(args) + assert valid + assert msg == "" + + +def test_validate_arguments_no_export_ocsf_without_provider_uid_passes(): + # No --export-ocsf, no provider_uid — should pass + args = Args( + scan_path=".", + scan_repository_url=None, + export_ocsf=False, + provider_uid=None, + ) + valid, msg = iac_arguments.validate_arguments(args) + assert valid + assert msg == "" + + # No export_ocsf attr at all — should pass + args = Args(scan_path=".", scan_repository_url=None) + valid, msg = iac_arguments.validate_arguments(args) + assert valid + assert msg == "" + + +def test_validate_arguments_provider_uid_must_be_valid_url(): + # Invalid provider_uid should fail + args = Args( + scan_path=".", + scan_repository_url=None, + provider_uid="not-a-url", + ) + valid, msg = iac_arguments.validate_arguments(args) + assert not valid + assert "valid repository URL" in msg + + # HTTPS URL without .git should pass + args = Args( + scan_path=".", + scan_repository_url=None, + provider_uid="https://github.com/user/repo", + ) + valid, msg = iac_arguments.validate_arguments(args) + assert valid + assert msg == "" + + # HTTPS URL with .git should pass + args = Args( + scan_path=".", + scan_repository_url=None, + provider_uid="https://github.com/user/repo.git", + ) + valid, msg = iac_arguments.validate_arguments(args) + assert valid + assert msg == "" + + # SSH URL should pass + args = Args( + scan_path=".", + scan_repository_url=None, + provider_uid="git@github.com:user/repo.git", + ) + valid, msg = iac_arguments.validate_arguments(args) + assert valid + assert msg == ""