From 00fb71b9dbaf870ed1791f4d0b44bf0c29b762c0 Mon Sep 17 00:00:00 2001 From: Deepak Dalvi Date: Fri, 19 Jun 2026 22:35:33 +0530 Subject: [PATCH 1/8] e2e provider --- docs/developer-guide/e2e-details.mdx | 46 ++++ prowler/__main__.py | 5 + prowler/config/config.py | 1 + prowler/config/config.yaml | 5 + prowler/lib/check/models.py | 17 ++ prowler/lib/cli/parser.py | 8 +- .../compliance/universal/universal_output.py | 1 + prowler/lib/outputs/finding.py | 12 + prowler/lib/outputs/html/html.py | 39 ++++ prowler/lib/outputs/outputs.py | 2 + prowler/lib/outputs/summary_table.py | 3 + prowler/providers/common/provider.py | 10 + prowler/providers/e2e/__init__.py | 0 prowler/providers/e2e/e2e_provider.py | 219 ++++++++++++++++++ prowler/providers/e2e/exceptions/__init__.py | 0 .../providers/e2e/exceptions/exceptions.py | 24 ++ prowler/providers/e2e/lib/__init__.py | 0 prowler/providers/e2e/lib/api/__init__.py | 0 prowler/providers/e2e/lib/api/client.py | 97 ++++++++ .../providers/e2e/lib/arguments/__init__.py | 0 .../providers/e2e/lib/arguments/arguments.py | 66 ++++++ .../providers/e2e/lib/mutelist/__init__.py | 0 .../providers/e2e/lib/mutelist/mutelist.py | 14 ++ prowler/providers/e2e/lib/service/__init__.py | 0 prowler/providers/e2e/lib/service/service.py | 12 + prowler/providers/e2e/models.py | 43 ++++ prowler/providers/e2e/services/__init__.py | 0 .../e2e/services/loadbalancer/__init__.py | 0 .../__init__.py | 0 ...b_https_uses_ssl_certificate.metadata.json | 34 +++ ...balancer_alb_https_uses_ssl_certificate.py | 25 ++ .../__init__.py | 0 ...backend_health_check_enabled.metadata.json | 34 +++ ...adbalancer_backend_health_check_enabled.py | 25 ++ .../loadbalancer_bitninja_enabled/__init__.py | 0 ...oadbalancer_bitninja_enabled.metadata.json | 34 +++ .../loadbalancer_bitninja_enabled.py | 22 ++ .../loadbalancer/loadbalancer_client.py | 6 + .../loadbalancer/loadbalancer_service.py | 90 +++++++ .../providers/e2e/services/node/__init__.py | 0 .../__init__.py | 0 ...ccidental_protection_enabled.metadata.json | 34 +++ .../node_accidental_protection_enabled.py | 16 ++ .../node/node_compliance_enabled/__init__.py | 0 .../node_compliance_enabled.metadata.json | 34 +++ .../node_compliance_enabled.py | 16 ++ .../node/node_encryption_enabled/__init__.py | 0 .../node_encryption_enabled.metadata.json | 34 +++ .../node_encryption_enabled.py | 16 ++ .../node_public_ip_not_assigned/__init__.py | 0 .../node_public_ip_not_assigned.metadata.json | 34 +++ .../node_public_ip_not_assigned.py | 16 ++ .../node_rescue_mode_disabled/__init__.py | 0 .../node_rescue_mode_disabled.metadata.json | 34 +++ .../node_rescue_mode_disabled.py | 16 ++ .../node/node_vpc_attached/__init__.py | 0 .../node_vpc_attached.metadata.json | 34 +++ .../node_vpc_attached/node_vpc_attached.py | 16 ++ .../e2e/services/node/nodes_client.py | 4 + .../e2e/services/node/nodes_service.py | 105 +++++++++ .../e2e/services/securitygroup/__init__.py | 0 .../securitygroup/securitygroup_client.py | 6 + .../__init__.py | 0 ...itygroup_no_all_traffic_rule.metadata.json | 34 +++ .../securitygroup_no_all_traffic_rule.py | 22 ++ .../__init__.py | 0 ...oup_no_inbound_any_all_ports.metadata.json | 34 +++ .../securitygroup_no_inbound_any_all_ports.py | 30 +++ .../__init__.py | 0 ...itygroup_restrictive_default.metadata.json | 34 +++ .../securitygroup_restrictive_default.py | 42 ++++ .../securitygroup/securitygroup_service.py | 145 ++++++++++++ .../e2e/services/storage/__init__.py | 0 .../__init__.py | 0 ...ge_block_volume_not_orphaned.metadata.json | 34 +++ .../storage_block_volume_not_orphaned.py | 20 ++ .../__init__.py | 0 ...ge_bucket_encryption_enabled.metadata.json | 34 +++ .../storage_bucket_encryption_enabled.py | 20 ++ .../__init__.py | 0 ...ucket_public_access_disabled.metadata.json | 34 +++ .../storage_bucket_public_access_disabled.py | 20 ++ .../__init__.py | 0 ...ge_bucket_versioning_enabled.metadata.json | 34 +++ .../storage_bucket_versioning_enabled.py | 20 ++ .../e2e/services/storage/storage_client.py | 4 + .../e2e/services/storage/storage_service.py | 104 +++++++++ tests/providers/e2e/e2e_fixtures.py | 29 +++ tests/providers/e2e/e2e_provider_test.py | 81 +++++++ .../e2e/lib/arguments/arguments_test.py | 27 +++ .../node/node_public_ip_not_assigned_test.py | 69 ++++++ .../e2e/services/node/nodes_service_test.py | 69 ++++++ 92 files changed, 2246 insertions(+), 3 deletions(-) create mode 100644 docs/developer-guide/e2e-details.mdx create mode 100644 prowler/providers/e2e/__init__.py create mode 100644 prowler/providers/e2e/e2e_provider.py create mode 100644 prowler/providers/e2e/exceptions/__init__.py create mode 100644 prowler/providers/e2e/exceptions/exceptions.py create mode 100644 prowler/providers/e2e/lib/__init__.py create mode 100644 prowler/providers/e2e/lib/api/__init__.py create mode 100644 prowler/providers/e2e/lib/api/client.py create mode 100644 prowler/providers/e2e/lib/arguments/__init__.py create mode 100644 prowler/providers/e2e/lib/arguments/arguments.py create mode 100644 prowler/providers/e2e/lib/mutelist/__init__.py create mode 100644 prowler/providers/e2e/lib/mutelist/mutelist.py create mode 100644 prowler/providers/e2e/lib/service/__init__.py create mode 100644 prowler/providers/e2e/lib/service/service.py create mode 100644 prowler/providers/e2e/models.py create mode 100644 prowler/providers/e2e/services/__init__.py create mode 100644 prowler/providers/e2e/services/loadbalancer/__init__.py create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/__init__.py create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_client.py create mode 100644 prowler/providers/e2e/services/loadbalancer/loadbalancer_service.py create mode 100644 prowler/providers/e2e/services/node/__init__.py create mode 100644 prowler/providers/e2e/services/node/node_accidental_protection_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py create mode 100644 prowler/providers/e2e/services/node/node_compliance_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py create mode 100644 prowler/providers/e2e/services/node/node_encryption_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py create mode 100644 prowler/providers/e2e/services/node/node_public_ip_not_assigned/__init__.py create mode 100644 prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json create mode 100644 prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py create mode 100644 prowler/providers/e2e/services/node/node_rescue_mode_disabled/__init__.py create mode 100644 prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json create mode 100644 prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py create mode 100644 prowler/providers/e2e/services/node/node_vpc_attached/__init__.py create mode 100644 prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.metadata.json create mode 100644 prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py create mode 100644 prowler/providers/e2e/services/node/nodes_client.py create mode 100644 prowler/providers/e2e/services/node/nodes_service.py create mode 100644 prowler/providers/e2e/services/securitygroup/__init__.py create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_client.py create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/__init__.py create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/__init__.py create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/__init__.py create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py create mode 100644 prowler/providers/e2e/services/securitygroup/securitygroup_service.py create mode 100644 prowler/providers/e2e/services/storage/__init__.py create mode 100644 prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/__init__.py create mode 100644 prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json create mode 100644 prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/__init__.py create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.metadata.json create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.py create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py create mode 100644 prowler/providers/e2e/services/storage/storage_client.py create mode 100644 prowler/providers/e2e/services/storage/storage_service.py create mode 100644 tests/providers/e2e/e2e_fixtures.py create mode 100644 tests/providers/e2e/e2e_provider_test.py create mode 100644 tests/providers/e2e/lib/arguments/arguments_test.py create mode 100644 tests/providers/e2e/services/node/node_public_ip_not_assigned_test.py create mode 100644 tests/providers/e2e/services/node/nodes_service_test.py diff --git a/docs/developer-guide/e2e-details.mdx b/docs/developer-guide/e2e-details.mdx new file mode 100644 index 00000000000..bebb15c43fb --- /dev/null +++ b/docs/developer-guide/e2e-details.mdx @@ -0,0 +1,46 @@ +--- +title: 'E2E Cloud Provider' +--- + +This page documents the [E2E Cloud](https://www.e2enetworks.com/) provider implementation in Prowler SDK. + +E2E Cloud uses the MyAccount REST API documented at [E2E Cloud API](https://docs.e2enetworks.com/api/myaccount/compute/nodes/). Authentication requires both an API key and a Bearer auth token generated from the MyAccount portal. + +## Authentication + +Set the following environment variables before running scans: + +```bash +export E2E_API_KEY= +export E2E_AUTH_TOKEN= +export E2E_PROJECT_ID= +export E2E_LOCATION=Delhi +``` + +Optional CLI flags (`--e2e-api-key`, `--e2e-auth-token`, `--e2e-project-id`, `--e2e-location`) are available for backward compatibility, but environment variables are preferred. + +## Usage + +```bash +uv run python prowler-cli.py e2e --list-checks +uv run python prowler-cli.py e2e --service node --log-level DEBUG +``` + +## Services and Checks + +The initial release includes four services: + +- `node` — compute node posture (public IP, encryption, compliance, VPC attachment) +- `securitygroup` — security group rules and node attachments +- `loadbalancer` — appliance HTTPS, health checks, BitNinja protection +- `storage` — object storage buckets and block volumes + +Provider code lives under [`prowler/providers/e2e/`](https://github.com/prowler-cloud/prowler/tree/master/prowler/providers/e2e). + +## Architecture Notes + +- API client: [`prowler/providers/e2e/lib/api/client.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/e2e/lib/api/client.py) +- Provider class: [`prowler/providers/e2e/e2e_provider.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/providers/e2e/e2e_provider.py) +- Check reports use `CheckReportE2e` in [`prowler/lib/check/models.py`](https://github.com/prowler-cloud/prowler/blob/master/prowler/lib/check/models.py) + +Supported regions default to `Delhi` and `Chennai` per the E2E Cloud OpenAPI specification. diff --git a/prowler/__main__.py b/prowler/__main__.py index 533a7043590..2941b85b2c2 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -156,6 +156,7 @@ from prowler.providers.oraclecloud.models import OCIOutputOptions from prowler.providers.scaleway.models import ScalewayOutputOptions from prowler.providers.stackit.models import StackITOutputOptions +from prowler.providers.e2e.models import E2eOutputOptions from prowler.providers.vercel.models import VercelOutputOptions @@ -431,6 +432,10 @@ def prowler(): output_options = VercelOutputOptions( args, bulk_checks_metadata, global_provider.identity ) + elif provider == "e2e": + output_options = E2eOutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) elif provider == "okta": output_options = OktaOutputOptions( args, bulk_checks_metadata, global_provider.identity diff --git a/prowler/config/config.py b/prowler/config/config.py index 9e2b079da6b..430dea928ed 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -80,6 +80,7 @@ class Provider(str, Enum): VERCEL = "vercel" OKTA = "okta" STACKIT = "stackit" + E2E = "e2e" # Compliance diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 28f07c20516..0ce777be55a 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -675,3 +675,8 @@ okta: # Defaults to 15 per DISA STIG V-273187 (OKTA-APP-000025); raise it only # with an explicit risk acceptance. okta_admin_console_idle_timeout_max_minutes: 15 + +# E2E Cloud Configuration +e2e: + # load_balancer_bitninja_enabled + require_bitninja_on_load_balancers: false diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index f9155c68f76..e4507e61ca6 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -1255,6 +1255,23 @@ def __init__(self, metadata: Dict, resource: Any) -> None: self.location = getattr(resource, "location", "kr1") +@dataclass +class CheckReportE2e(Check_Report): + """Contains the E2E Cloud Check's finding information.""" + + resource_name: str + resource_id: str + location: str + + def __init__(self, metadata: Dict, resource: Any) -> None: + super().__init__(metadata, resource) + self.resource_name = getattr( + resource, "name", getattr(resource, "resource_name", "") + ) + self.resource_id = getattr(resource, "id", getattr(resource, "resource_id", "")) + self.location = getattr(resource, "location", "global") + + @dataclass class CheckReportStackIT(Check_Report): """Contains the StackIT Check's finding information.""" diff --git a/prowler/lib/cli/parser.py b/prowler/lib/cli/parser.py index 2848e9c7880..b70bac49d3e 100644 --- a/prowler/lib/cli/parser.py +++ b/prowler/lib/cli/parser.py @@ -48,6 +48,7 @@ def __init__(self): "nhn", "mongodbatlas", "vercel", + "e2e", "okta", "scaleway", "stackit", @@ -73,10 +74,10 @@ def __init__(self): self.parser = argparse.ArgumentParser( prog="prowler", formatter_class=RawTextHelpFormatter, - usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,dashboard,iac,image,llm{extra_providers_csv}}} ...", + usage=f"prowler [-h] [--version] {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,e2e,dashboard,iac,image,llm{extra_providers_csv}}} ...", epilog=f""" Available Cloud Providers: - {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel{extra_providers_csv}}} + {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,e2e{extra_providers_csv}}} aws AWS Provider azure Azure Provider gcp GCP Provider @@ -96,7 +97,8 @@ def __init__(self): nhn NHN Provider (Unofficial) mongodbatlas MongoDB Atlas Provider scaleway Scaleway Provider - vercel Vercel Provider{extra_providers_text} + vercel Vercel Provider + e2e E2E Cloud Provider{extra_providers_text} Available components: diff --git a/prowler/lib/outputs/compliance/universal/universal_output.py b/prowler/lib/outputs/compliance/universal/universal_output.py index a3cdb1389af..5ad5582da50 100644 --- a/prowler/lib/outputs/compliance/universal/universal_output.py +++ b/prowler/lib/outputs/compliance/universal/universal_output.py @@ -22,6 +22,7 @@ "oraclecloud": ("TenancyId", "account_uid", "Region", "region"), "alibabacloud": ("AccountId", "account_uid", "Region", "region"), "nhn": ("AccountId", "account_uid", "Region", "region"), + "e2e": ("ProjectId", "account_uid", "Location", "region"), } _DEFAULT_HEADERS = ("AccountId", "account_uid", "Region", "region") diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index 9f772d17c4a..13017de22f6 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -342,6 +342,18 @@ def generate_output( output_data["resource_uid"] = check_output.resource_id output_data["region"] = check_output.location + elif provider.type == "e2e": + output_data["auth_method"] = "api_key_and_bearer_token" + output_data["account_uid"] = str( + get_nested_attribute(provider, "identity.project_id") + ) + output_data["account_name"] = str( + get_nested_attribute(provider, "identity.project_id") + ) + output_data["resource_name"] = check_output.resource_name + output_data["resource_uid"] = check_output.resource_id + output_data["region"] = check_output.location + elif provider.type == "stackit": output_data["auth_method"] = getattr( provider, "auth_method", "api_token" diff --git a/prowler/lib/outputs/html/html.py b/prowler/lib/outputs/html/html.py index 545bcbc4569..c253441af8f 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -1396,6 +1396,45 @@ def get_googleworkspace_assessment_summary(provider: Provider) -> str: ) return "" + @staticmethod + def get_e2e_assessment_summary(provider: Provider) -> str: + """Get the HTML assessment summary for the E2E Cloud provider.""" + try: + locations = ", ".join(provider.identity.locations) + return f""" +
+
+
+ E2E Cloud Assessment Summary +
+
    +
  • + Project ID: {provider.identity.project_id} +
  • +
  • + Locations: {locations} +
  • +
+
+
+
+
+
+ E2E Cloud Credentials +
+
    +
  • + Authentication: API Key + Bearer Token +
  • +
+
+
""" + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + return "" + @staticmethod def get_vercel_assessment_summary(provider: Provider) -> str: """ diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index a1f37a9dfc0..31f0e5094bf 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -24,6 +24,8 @@ def stdout_report(finding, color, verbose, status, fix, provider=None): details = finding.location elif finding.check_metadata.Provider == "nhn": details = finding.location + elif finding.check_metadata.Provider == "e2e": + details = finding.location elif finding.check_metadata.Provider == "stackit": details = finding.location elif finding.check_metadata.Provider == "llm": diff --git a/prowler/lib/outputs/summary_table.py b/prowler/lib/outputs/summary_table.py index 43d4a547c18..d1fdf103ed5 100644 --- a/prowler/lib/outputs/summary_table.py +++ b/prowler/lib/outputs/summary_table.py @@ -70,6 +70,9 @@ def display_summary_table( elif provider.type == "nhn": entity_type = "Tenant Domain" audited_entities = provider.identity.tenant_domain + elif provider.type == "e2e": + entity_type = "Project" + audited_entities = str(provider.identity.project_id) elif provider.type == "stackit": if provider.identity.project_name: entity_type = "Project" diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 77ae9180a3c..0217e14863f 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -561,6 +561,16 @@ def init_global_provider(arguments: Namespace) -> None: mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, ) + elif arguments.provider == "e2e": + provider_class( + api_key=getattr(arguments, "e2e_api_key", None), + auth_token=getattr(arguments, "e2e_auth_token", None), + project_id=getattr(arguments, "e2e_project_id", None), + locations=getattr(arguments, "e2e_location", None), + config_path=arguments.config_file, + mutelist_path=arguments.mutelist_file, + fixer_config=fixer_config, + ) elif arguments.provider == "okta": provider_class( okta_org_domain=getattr(arguments, "okta_org_domain", ""), diff --git a/prowler/providers/e2e/__init__.py b/prowler/providers/e2e/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/e2e_provider.py b/prowler/providers/e2e/e2e_provider.py new file mode 100644 index 00000000000..a70676bc6d9 --- /dev/null +++ b/prowler/providers/e2e/e2e_provider.py @@ -0,0 +1,219 @@ +import os + +import requests +from colorama import Style + +from prowler.config.config import ( + default_config_file_path, + get_default_mute_file_path, + load_and_validate_config_file, +) +from prowler.lib.logger import logger +from prowler.lib.utils.utils import print_boxes +from prowler.providers.common.models import Audit_Metadata, Connection +from prowler.providers.common.provider import Provider +from prowler.providers.e2e.exceptions.exceptions import ( + E2eCredentialsError, + E2eSessionError, +) +from prowler.providers.e2e.lib.mutelist.mutelist import E2eMutelist +from prowler.providers.e2e.models import ( + E2E_DEFAULT_LOCATIONS, + E2eIdentityInfo, + E2eSession, +) + + +class E2eProvider(Provider): + """Provider class for E2E Cloud.""" + + _type: str = "e2e" + _session: E2eSession + _identity: E2eIdentityInfo + _audit_config: dict + _fixer_config: dict + _mutelist: E2eMutelist + audit_metadata: Audit_Metadata + + def __init__( + self, + api_key: str = None, + auth_token: str = None, + project_id: str | int = None, + locations: list[str] | None = None, + config_path: str = None, + fixer_config: dict = None, + mutelist_path: str = None, + mutelist_content: dict = None, + ): + logger.info("Initializing E2E Cloud Provider...") + + self._api_key = api_key or os.getenv("E2E_API_KEY") + self._auth_token = auth_token or os.getenv("E2E_AUTH_TOKEN") + project_value = project_id or os.getenv("E2E_PROJECT_ID") + self._project_id = int(project_value) if project_value else None + self._locations = self._resolve_locations(locations) + + if not self._api_key or not self._auth_token or self._project_id is None: + raise E2eCredentialsError( + "E2eProvider requires api_key, auth_token, and project_id." + ) + + self._fixer_config = fixer_config if fixer_config else {} + if not config_path: + config_path = default_config_file_path + self._audit_config = load_and_validate_config_file(self._type, config_path) + + if mutelist_content: + self._mutelist = E2eMutelist(mutelist_content=mutelist_content) + else: + if not mutelist_path: + mutelist_path = get_default_mute_file_path(self._type) + self._mutelist = E2eMutelist(mutelist_path=mutelist_path) + + self._session = E2eProvider.setup_session( + api_key=self._api_key, + auth_token=self._auth_token, + project_id=self._project_id, + locations=self._locations, + ) + self._identity = E2eIdentityInfo( + project_id=self._project_id, + locations=self._locations, + ) + + Provider.set_global_provider(self) + + @staticmethod + def _resolve_locations(locations: list[str] | None) -> list[str]: + if locations: + return locations + + env_location = os.getenv("E2E_LOCATION") + if env_location: + return [env_location] + + return list(E2E_DEFAULT_LOCATIONS) + + @property + def type(self) -> str: + return self._type + + @property + def session(self) -> E2eSession: + return self._session + + @property + def identity(self) -> E2eIdentityInfo: + return self._identity + + @property + def audit_config(self) -> dict: + return self._audit_config + + @property + def fixer_config(self) -> dict: + return self._fixer_config + + @property + def mutelist(self) -> E2eMutelist: + return self._mutelist + + @staticmethod + def setup_session( + api_key: str, + auth_token: str, + project_id: int, + locations: list[str], + ) -> E2eSession: + try: + http_session = requests.Session() + http_session.headers.update( + { + "Authorization": f"Bearer {auth_token}", + "Content-Type": "application/json", + } + ) + return E2eSession( + api_key=api_key, + auth_token=auth_token, + project_id=project_id, + locations=locations, + http_session=http_session, + ) + except Exception as error: + raise E2eSessionError( + message="Failed to initialize E2E Cloud session.", + original_exception=error, + ) from error + + def print_credentials(self) -> None: + masked_key = ( + f"{self._api_key[:4]}...{self._api_key[-4:]}" + if self._api_key and len(self._api_key) > 8 + else "****" + ) + report_lines = [ + f" Project ID: {self._project_id}", + f" Locations: {', '.join(self._locations)}", + f" API Key: {masked_key}", + ] + report_title = ( + f"{Style.BRIGHT}Using the E2E Cloud credentials below:{Style.RESET_ALL}" + ) + print_boxes(report_lines, report_title) + + @staticmethod + def test_connection( + api_key: str = None, + auth_token: str = None, + project_id: str | int = None, + locations: list[str] | None = None, + raise_on_exception: bool = True, + ) -> Connection: + try: + api_key = api_key or os.getenv("E2E_API_KEY") + auth_token = auth_token or os.getenv("E2E_AUTH_TOKEN") + project_value = project_id or os.getenv("E2E_PROJECT_ID") + project_id_int = int(project_value) if project_value else None + resolved_locations = locations or ( + [os.getenv("E2E_LOCATION")] + if os.getenv("E2E_LOCATION") + else list(E2E_DEFAULT_LOCATIONS) + ) + + if not api_key or not auth_token or project_id_int is None: + raise E2eCredentialsError( + "E2E Cloud test_connection requires api_key, auth_token, and project_id." + ) + + session = E2eProvider.setup_session( + api_key=api_key, + auth_token=auth_token, + project_id=project_id_int, + locations=resolved_locations, + ) + response = session.http_session.get( + f"{session.base_url}/nodes/", + params={ + "apikey": session.api_key, + "project_id": session.project_id, + "location": resolved_locations[0], + }, + timeout=30, + ) + if response.status_code != 200: + error_msg = ( + f"E2E Cloud connection failed with status {response.status_code}: " + f"{response.text}" + ) + raise E2eSessionError(message=error_msg) + + return Connection(is_connected=True) + except Exception as error: + logger.critical( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + if raise_on_exception: + raise error + return Connection(error=error) diff --git a/prowler/providers/e2e/exceptions/__init__.py b/prowler/providers/e2e/exceptions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/exceptions/exceptions.py b/prowler/providers/e2e/exceptions/exceptions.py new file mode 100644 index 00000000000..01411b4b011 --- /dev/null +++ b/prowler/providers/e2e/exceptions/exceptions.py @@ -0,0 +1,24 @@ +class E2eCredentialsError(Exception): + """Raised when E2E Cloud credentials are missing or invalid.""" + + def __init__(self, message: str): + self.message = message + super().__init__(message) + + +class E2eSessionError(Exception): + """Raised when the E2E Cloud session cannot be initialized.""" + + def __init__(self, message: str, original_exception: Exception | None = None): + self.message = message + self.original_exception = original_exception + super().__init__(message) + + +class E2eAPIError(Exception): + """Raised when an E2E Cloud API request fails.""" + + def __init__(self, message: str, original_exception: Exception | None = None): + self.message = message + self.original_exception = original_exception + super().__init__(message) diff --git a/prowler/providers/e2e/lib/__init__.py b/prowler/providers/e2e/lib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/lib/api/__init__.py b/prowler/providers/e2e/lib/api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/lib/api/client.py b/prowler/providers/e2e/lib/api/client.py new file mode 100644 index 00000000000..fe55a0edd64 --- /dev/null +++ b/prowler/providers/e2e/lib/api/client.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +from typing import Any + +import requests + +from prowler.lib.logger import logger +from prowler.providers.e2e.exceptions.exceptions import E2eAPIError +from prowler.providers.e2e.models import E2eSession + + +class E2eAPIClient: + """Shared HTTP client for E2E Cloud MyAccount API requests.""" + + def __init__(self, session: E2eSession): + self.session = session + + def _auth_params(self, location: str, extra: dict | None = None) -> dict[str, Any]: + params = { + "apikey": self.session.api_key, + "project_id": self.session.project_id, + "location": location, + } + if extra: + params.update(extra) + return params + + def get( + self, + path: str, + location: str, + params: dict | None = None, + timeout: int = 30, + ) -> dict: + """Perform a GET request against the E2E Cloud API.""" + url = f"{self.session.base_url}{path}" + query_params = self._auth_params(location, params) + try: + response = self.session.http_session.get( + url, + params=query_params, + timeout=timeout, + ) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + return {"data": payload} + return payload + except requests.exceptions.RequestException as error: + logger.error( + f"E2E API GET {path} failed: {error.__class__.__name__}: {error}" + ) + raise E2eAPIError( + message=f"GET {path} failed for location {location}", + original_exception=error, + ) from error + + def get_data( + self, + path: str, + location: str, + params: dict | None = None, + ) -> list | dict: + """Return the `data` field from a standard E2E API envelope.""" + payload = self.get(path, location=location, params=params) + return payload.get("data", []) + + def paginate( + self, + path: str, + location: str, + params: dict | None = None, + per_page: int = 100, + ) -> list: + """Iterate page_no/per_page style paginated list endpoints.""" + all_items: list = [] + page_no = 1 + total_pages = 1 + + while page_no <= total_pages: + page_params = {"page_no": page_no, "per_page": per_page} + if params: + page_params.update(params) + + payload = self.get(path, location=location, params=page_params) + data = payload.get("data", []) + if isinstance(data, list): + all_items.extend(data) + elif isinstance(data, dict): + return data + + total_pages = int(payload.get("total_page_number", page_no)) + if not data: + break + page_no += 1 + + return all_items diff --git a/prowler/providers/e2e/lib/arguments/__init__.py b/prowler/providers/e2e/lib/arguments/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/lib/arguments/arguments.py b/prowler/providers/e2e/lib/arguments/arguments.py new file mode 100644 index 00000000000..8a745123abe --- /dev/null +++ b/prowler/providers/e2e/lib/arguments/arguments.py @@ -0,0 +1,66 @@ +import os + +SENSITIVE_ARGUMENTS = frozenset({"--e2e-api-key", "--e2e-auth-token"}) + + +def init_parser(self): + """Init the E2E Cloud Provider CLI parser.""" + e2e_parser = self.subparsers.add_parser( + "e2e", + parents=[self.common_providers_parser], + help="E2E Cloud Provider", + ) + + auth_group = e2e_parser.add_argument_group("Authentication") + auth_group.add_argument( + "--e2e-api-key", + nargs="?", + default=None, + metavar="E2E_API_KEY", + help="E2E Cloud API key. Use E2E_API_KEY env var instead of passing directly.", + ) + auth_group.add_argument( + "--e2e-auth-token", + nargs="?", + default=None, + metavar="E2E_AUTH_TOKEN", + help="E2E Cloud auth token. Use E2E_AUTH_TOKEN env var instead of passing directly.", + ) + auth_group.add_argument( + "--e2e-project-id", + nargs="?", + default=None, + metavar="E2E_PROJECT_ID", + help="E2E Cloud project ID. Use E2E_PROJECT_ID env var instead of passing directly.", + ) + + scope_group = e2e_parser.add_argument_group("Scope") + scope_group.add_argument( + "--e2e-location", + "--e2e-locations", + nargs="*", + default=None, + metavar="LOCATION", + help="E2E Cloud region(s) to scan (e.g. Delhi Chennai). Defaults to E2E_LOCATION or Delhi.", + ) + + +def validate_arguments(arguments) -> tuple[bool, str]: + """Validate E2E Cloud provider CLI arguments.""" + api_key = arguments.e2e_api_key or os.getenv("E2E_API_KEY") + auth_token = arguments.e2e_auth_token or os.getenv("E2E_AUTH_TOKEN") + project_id = arguments.e2e_project_id or os.getenv("E2E_PROJECT_ID") + + if not api_key: + return False, "E2E Cloud provider requires an API key (E2E_API_KEY)." + if not auth_token: + return False, "E2E Cloud provider requires an auth token (E2E_AUTH_TOKEN)." + if not project_id: + return False, "E2E Cloud provider requires a project ID (E2E_PROJECT_ID)." + + try: + int(project_id) + except (TypeError, ValueError): + return False, "E2E Cloud project ID must be an integer." + + return True, "" diff --git a/prowler/providers/e2e/lib/mutelist/__init__.py b/prowler/providers/e2e/lib/mutelist/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/lib/mutelist/mutelist.py b/prowler/providers/e2e/lib/mutelist/mutelist.py new file mode 100644 index 00000000000..11a12a152fe --- /dev/null +++ b/prowler/providers/e2e/lib/mutelist/mutelist.py @@ -0,0 +1,14 @@ +from prowler.lib.check.models import CheckReportE2e +from prowler.lib.mutelist.mutelist import Mutelist +from prowler.lib.outputs.utils import unroll_dict, unroll_tags + + +class E2eMutelist(Mutelist): + def is_finding_muted(self, finding: CheckReportE2e) -> bool: + return self.is_muted( + finding.resource_id, + finding.check_metadata.CheckID, + finding.location, + finding.resource_name, + unroll_dict(unroll_tags(finding.resource_tags)), + ) diff --git a/prowler/providers/e2e/lib/service/__init__.py b/prowler/providers/e2e/lib/service/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/lib/service/service.py b/prowler/providers/e2e/lib/service/service.py new file mode 100644 index 00000000000..0f196f258ab --- /dev/null +++ b/prowler/providers/e2e/lib/service/service.py @@ -0,0 +1,12 @@ +from prowler.providers.e2e.lib.api.client import E2eAPIClient + + +class E2eService: + """Base class for E2E Cloud services.""" + + def __init__(self, service: str, provider): + self.provider = provider + self.audit_config = provider.audit_config + self.fixer_config = provider.fixer_config + self.service = service.lower() if not service.islower() else service + self.client = E2eAPIClient(provider.session) diff --git a/prowler/providers/e2e/models.py b/prowler/providers/e2e/models.py new file mode 100644 index 00000000000..8c6eeacc5a4 --- /dev/null +++ b/prowler/providers/e2e/models.py @@ -0,0 +1,43 @@ +from typing import Any, Optional + +from pydantic import BaseModel, Field + +from prowler.config.config import output_file_timestamp +from prowler.providers.common.models import ProviderOutputOptions + +E2E_DEFAULT_LOCATIONS = ("Delhi", "Chennai") +E2E_BASE_URL = "https://api.e2enetworks.com/myaccount/api/v1" + + +class E2eSession(BaseModel): + """E2E Cloud API session information.""" + + api_key: str = Field(exclude=True, repr=False) + auth_token: str = Field(exclude=True, repr=False) + project_id: int + locations: list[str] + base_url: str = E2E_BASE_URL + http_session: Any = Field(default=None, exclude=True) + + +class E2eIdentityInfo(BaseModel): + """E2E Cloud identity and scoping information.""" + + project_id: int + locations: list[str] + + +class E2eOutputOptions(ProviderOutputOptions): + """Customize output filenames for E2E Cloud scans.""" + + def __init__(self, arguments, bulk_checks_metadata, identity: E2eIdentityInfo): + super().__init__(arguments, bulk_checks_metadata) + if ( + not hasattr(arguments, "output_filename") + or arguments.output_filename is None + ): + self.output_filename = ( + f"prowler-output-e2e-{identity.project_id}-{output_file_timestamp}" + ) + else: + self.output_filename = arguments.output_filename diff --git a/prowler/providers/e2e/services/__init__.py b/prowler/providers/e2e/services/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/loadbalancer/__init__.py b/prowler/providers/e2e/services/loadbalancer/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/__init__.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json new file mode 100644 index 00000000000..80c6a7bc582 --- /dev/null +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "loadbalancer_alb_https_uses_ssl_certificate", + "CheckTitle": "Check if E2E Cloud ALB/HTTPS load balancers use an SSL certificate", + "CheckType": [], + "ServiceName": "loadbalancer", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "LoadBalancer", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud application load balancers serving HTTPS traffic have an SSL certificate configured.", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach a valid SSL certificate to HTTPS load balancers.", + "Url": "https://hub.prowler.com/check/loadbalancer_alb_https_uses_ssl_certificate" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py new file mode 100644 index 00000000000..8e3dcc27947 --- /dev/null +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.loadbalancer.loadbalancer_client import ( + loadbalancer_client, +) + + +class loadbalancer_alb_https_uses_ssl_certificate(Check): + def execute(self): + findings = [] + for lb in loadbalancer_client.load_balancers: + if not lb.is_alb_https: + continue + + report = CheckReportE2e(metadata=self.metadata(), resource=lb) + report.status = "PASS" + report.status_extended = ( + f"Load balancer {lb.name} uses an SSL certificate for HTTPS traffic." + ) + if not lb.ssl_certificate_id: + report.status = "FAIL" + report.status_extended = ( + f"Load balancer {lb.name} does not have an SSL certificate configured for HTTPS traffic." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/__init__.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json new file mode 100644 index 00000000000..66cc8208ebf --- /dev/null +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "loadbalancer_backend_health_check_enabled", + "CheckTitle": "Check if E2E Cloud ALB load balancers have backend health checks enabled", + "CheckType": [], + "ServiceName": "loadbalancer", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "LoadBalancer", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud application load balancers have HTTP health checks configured for backends.", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure HTTP health checks for ALB backends.", + "Url": "https://hub.prowler.com/check/loadbalancer_backend_health_check_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py new file mode 100644 index 00000000000..39872416d5d --- /dev/null +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.loadbalancer.loadbalancer_client import ( + loadbalancer_client, +) + + +class loadbalancer_backend_health_check_enabled(Check): + def execute(self): + findings = [] + for lb in loadbalancer_client.load_balancers: + if not lb.is_alb_https: + continue + + report = CheckReportE2e(metadata=self.metadata(), resource=lb) + report.status = "PASS" + report.status_extended = ( + f"Load balancer {lb.name} has backend health checks configured." + ) + if not lb.has_backend_health_check: + report.status = "FAIL" + report.status_extended = ( + f"Load balancer {lb.name} does not have backend health checks configured." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/__init__.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json new file mode 100644 index 00000000000..9b3da2db511 --- /dev/null +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "loadbalancer_bitninja_enabled", + "CheckTitle": "Check if E2E Cloud load balancers have BitNinja protection enabled", + "CheckType": [], + "ServiceName": "loadbalancer", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "LoadBalancer", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud load balancers have BitNinja protection enabled.", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable BitNinja protection on load balancer appliances.", + "Url": "https://hub.prowler.com/check/loadbalancer_bitninja_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py new file mode 100644 index 00000000000..a49f9886659 --- /dev/null +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.loadbalancer.loadbalancer_client import ( + loadbalancer_client, +) + + +class loadbalancer_bitninja_enabled(Check): + def execute(self): + findings = [] + for lb in loadbalancer_client.load_balancers: + report = CheckReportE2e(metadata=self.metadata(), resource=lb) + report.status = "PASS" + report.status_extended = ( + f"Load balancer {lb.name} has BitNinja protection enabled." + ) + if not lb.enable_bitninja: + report.status = "FAIL" + report.status_extended = ( + f"Load balancer {lb.name} does not have BitNinja protection enabled." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_client.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_client.py new file mode 100644 index 00000000000..0eb89ce6377 --- /dev/null +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2e.services.loadbalancer.loadbalancer_service import ( + LoadBalancers, +) + +loadbalancer_client = LoadBalancers(Provider.get_global_provider()) diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_service.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_service.py new file mode 100644 index 00000000000..28662d753a3 --- /dev/null +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_service.py @@ -0,0 +1,90 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2e.lib.service.service import E2eService + + +class LoadBalancers(E2eService): + """Service class for E2E Cloud load balancers.""" + + def __init__(self, provider): + super().__init__("loadbalancer", provider) + self.loadbalancers: list[LoadBalancer] = [] + self._fetch_loadbalancers() + + def _fetch_loadbalancers(self): + for location in self.provider.session.locations: + try: + appliances = self.client.paginate( + "/appliances/", + location=location, + ) + for item in appliances: + context = self._extract_context(item) + node_detail = item.get("node_detail", {}) or {} + self.loadbalancers.append( + LoadBalancer( + id=str(item.get("id", "")), + name=item.get("name", ""), + location=location, + status=item.get("status", ""), + lb_mode=context.get("lb_mode", ""), + lb_port=str(context.get("lb_port", "")), + enable_bitninja=bool(context.get("enable_bitninja", False)), + ssl_certificate_id=self._get_ssl_certificate_id(context), + backends=context.get("backends", []) or [], + public_ip=node_detail.get("public_ip", ""), + ) + ) + except Exception as error: + logger.error( + f"loadbalancer - Error fetching appliances in {location}: {error}" + ) + + @staticmethod + def _extract_context(item: dict) -> dict: + instances = item.get("appliance_instance", []) or [] + if not instances: + return {} + return instances[0].get("context", {}) or {} + + @staticmethod + def _get_ssl_certificate_id(context: dict) -> str | None: + ssl_context = context.get("ssl_context", {}) or {} + certificate_id = ssl_context.get("ssl_certificate_id") + if certificate_id in (None, "", 0): + return None + return str(certificate_id) + + +class LoadBalancer(BaseModel): + id: str + name: str + location: str + status: str = "" + lb_mode: str = "" + lb_port: str = "" + enable_bitninja: bool = False + ssl_certificate_id: str | None = None + backends: list = [] + public_ip: str = "" + + @property + def resource_id(self) -> str: + return self.id + + @property + def resource_name(self) -> str: + return self.name + + @property + def is_alb_https(self) -> bool: + mode = self.lb_mode.upper() + return mode in ("HTTP", "HTTPS", "BOTH") + + @property + def has_backend_health_check(self) -> bool: + for backend in self.backends: + if isinstance(backend, dict) and backend.get("http_check"): + return True + return False diff --git a/prowler/providers/e2e/services/node/__init__.py b/prowler/providers/e2e/services/node/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/__init__.py b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json new file mode 100644 index 00000000000..47a36a67ef9 --- /dev/null +++ b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "node_accidental_protection_enabled", + "CheckTitle": "Check if E2E Cloud nodes have accidental protection enabled", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "compute", + "Description": "Check if E2E Cloud nodes have accidental protection enabled", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud nodes have accidental protection enabled", + "Url": "https://hub.prowler.com/check/node_accidental_protection_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py new file mode 100644 index 00000000000..14172c6db38 --- /dev/null +++ b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.node.nodes_client import nodes_client + + +class node_accidental_protection_enabled(Check): + def execute(self): + findings = [] + for node in nodes_client.nodes: + report = CheckReportE2e(metadata=self.metadata(), resource=node) + report.status = "PASS" + report.status_extended = f"Node {node.name} has accidental protection enabled." + if getattr(node, "is_accidental_protection") != True: + report.status = "FAIL" + report.status_extended = f"Node {node.name} does not have accidental protection enabled." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/node/node_compliance_enabled/__init__.py b/prowler/providers/e2e/services/node/node_compliance_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json new file mode 100644 index 00000000000..3f2962ef517 --- /dev/null +++ b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "node_compliance_enabled", + "CheckTitle": "Check if E2E Cloud nodes have compliance mode enabled", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "compute", + "Description": "Check if E2E Cloud nodes have compliance mode enabled", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud nodes have compliance mode enabled", + "Url": "https://hub.prowler.com/check/node_compliance_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py new file mode 100644 index 00000000000..2a06a3156e5 --- /dev/null +++ b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.node.nodes_client import nodes_client + + +class node_compliance_enabled(Check): + def execute(self): + findings = [] + for node in nodes_client.nodes: + report = CheckReportE2e(metadata=self.metadata(), resource=node) + report.status = "PASS" + report.status_extended = f"Node {node.name} has compliance mode enabled." + if getattr(node, "is_node_compliance") != True: + report.status = "FAIL" + report.status_extended = f"Node {node.name} does not have compliance mode enabled." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/node/node_encryption_enabled/__init__.py b/prowler/providers/e2e/services/node/node_encryption_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json new file mode 100644 index 00000000000..e2e73a8aa76 --- /dev/null +++ b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "node_encryption_enabled", + "CheckTitle": "Check if E2E Cloud nodes have encryption enabled", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Other", + "ResourceGroup": "compute", + "Description": "Check if E2E Cloud nodes have encryption enabled", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud nodes have encryption enabled", + "Url": "https://hub.prowler.com/check/node_encryption_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py new file mode 100644 index 00000000000..6fa0ce6c197 --- /dev/null +++ b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.node.nodes_client import nodes_client + + +class node_encryption_enabled(Check): + def execute(self): + findings = [] + for node in nodes_client.nodes: + report = CheckReportE2e(metadata=self.metadata(), resource=node) + report.status = "PASS" + report.status_extended = f"Node {node.name} has encryption enabled." + if getattr(node, "is_encryption_enabled") != True: + report.status = "FAIL" + report.status_extended = f"Node {node.name} does not have encryption enabled." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/node/node_public_ip_not_assigned/__init__.py b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json new file mode 100644 index 00000000000..671d611e66f --- /dev/null +++ b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "node_public_ip_not_assigned", + "CheckTitle": "Check if E2E Cloud nodes do not have a public IP assigned", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "compute", + "Description": "Check if E2E Cloud nodes do not have a public IP assigned", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud nodes do not have a public IP assigned", + "Url": "https://hub.prowler.com/check/node_public_ip_not_assigned" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py new file mode 100644 index 00000000000..d49b38f8b93 --- /dev/null +++ b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.node.nodes_client import nodes_client + + +class node_public_ip_not_assigned(Check): + def execute(self): + findings = [] + for node in nodes_client.nodes: + report = CheckReportE2e(metadata=self.metadata(), resource=node) + report.status = "PASS" + report.status_extended = f"Node {node.name} does not have a public IP." + if getattr(node, "has_public_ip") != False: + report.status = "FAIL" + report.status_extended = f"Node {node.name} has a public IP assigned." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/node/node_rescue_mode_disabled/__init__.py b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json new file mode 100644 index 00000000000..b9604c1ed3e --- /dev/null +++ b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "node_rescue_mode_disabled", + "CheckTitle": "Check if E2E Cloud nodes do not have rescue mode enabled", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "compute", + "Description": "Check if E2E Cloud nodes do not have rescue mode enabled", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud nodes do not have rescue mode enabled", + "Url": "https://hub.prowler.com/check/node_rescue_mode_disabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py new file mode 100644 index 00000000000..16cc87c7f4d --- /dev/null +++ b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.node.nodes_client import nodes_client + + +class node_rescue_mode_disabled(Check): + def execute(self): + findings = [] + for node in nodes_client.nodes: + report = CheckReportE2e(metadata=self.metadata(), resource=node) + report.status = "PASS" + report.status_extended = f"Node {node.name} does not have rescue mode enabled." + if node.rescue_mode_status != "Disabled": + report.status = "FAIL" + report.status_extended = f"Node {node.name} has rescue mode enabled." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/node/node_vpc_attached/__init__.py b/prowler/providers/e2e/services/node/node_vpc_attached/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.metadata.json b/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.metadata.json new file mode 100644 index 00000000000..cfca9226735 --- /dev/null +++ b/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "node_vpc_attached", + "CheckTitle": "Check if E2E Cloud nodes are attached to a VPC", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "compute", + "Description": "Check if E2E Cloud nodes are attached to a VPC", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud nodes are attached to a VPC", + "Url": "https://hub.prowler.com/check/node_vpc_attached" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py b/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py new file mode 100644 index 00000000000..26746ec4d2a --- /dev/null +++ b/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.node.nodes_client import nodes_client + + +class node_vpc_attached(Check): + def execute(self): + findings = [] + for node in nodes_client.nodes: + report = CheckReportE2e(metadata=self.metadata(), resource=node) + report.status = "PASS" + report.status_extended = f"Node {node.name} is attached to a VPC." + if getattr(node, "is_vpc_attached") != True: + report.status = "FAIL" + report.status_extended = f"Node {node.name} is not attached to a VPC." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/node/nodes_client.py b/prowler/providers/e2e/services/node/nodes_client.py new file mode 100644 index 00000000000..cecc935104d --- /dev/null +++ b/prowler/providers/e2e/services/node/nodes_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2e.services.node.nodes_service import Nodes + +nodes_client = Nodes(Provider.get_global_provider()) diff --git a/prowler/providers/e2e/services/node/nodes_service.py b/prowler/providers/e2e/services/node/nodes_service.py new file mode 100644 index 00000000000..be5c3bfd38a --- /dev/null +++ b/prowler/providers/e2e/services/node/nodes_service.py @@ -0,0 +1,105 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2e.lib.service.service import E2eService + + +def _has_public_ip(public_ip_address: str | None) -> bool: + if not public_ip_address: + return False + value = str(public_ip_address).strip() + if not value or value in ("[]", "null", "None"): + return False + return True + + +class Nodes(E2eService): + """Service class for E2E Cloud compute nodes.""" + + def __init__(self, provider): + super().__init__("nodes", provider) + self.nodes: list[Node] = [] + self._fetch_nodes() + + def _fetch_nodes(self): + for location in self.provider.session.locations: + try: + node_list = self.client.get_data("/nodes/", location=location) + if not isinstance(node_list, list): + continue + + for item in node_list: + node_id = str(item.get("id", "")) + detail = self._get_node_detail(node_id, location) + merged = {**item, **detail} + + self.nodes.append( + Node( + id=node_id, + name=merged.get("name", ""), + status=merged.get("status", ""), + location=location, + vm_id=str(merged.get("vm_id", merged.get("id", ""))), + public_ip_address=merged.get("public_ip_address"), + private_ip_address=merged.get("private_ip_address", ""), + is_accidental_protection=bool( + merged.get("is_accidental_protection", False) + ), + is_encryption_enabled=bool( + merged.get("isEncryptionEnabled", False) + ), + is_locked=bool(merged.get("is_locked", False)), + rescue_mode_status=merged.get( + "rescue_mode_status", "Disabled" + ), + is_node_compliance=bool( + merged.get("is_node_compliance", False) + ), + is_vpc_attached=bool(merged.get("is_vpc_attached", False)), + has_public_ip=_has_public_ip( + merged.get("public_ip_address") + ), + ) + ) + except Exception as error: + logger.error( + f"nodes - Error fetching nodes in {location}: {error}" + ) + + def _get_node_detail(self, node_id: str, location: str) -> dict: + if not node_id: + return {} + try: + data = self.client.get_data( + f"/nodes/{node_id}/", + location=location, + ) + return data if isinstance(data, dict) else {} + except Exception as error: + logger.error(f"nodes - Error fetching node detail {node_id}: {error}") + return {} + + +class Node(BaseModel): + id: str + name: str + status: str + location: str + vm_id: str + public_ip_address: str | None = None + private_ip_address: str = "" + is_accidental_protection: bool = False + is_encryption_enabled: bool = False + is_locked: bool = False + rescue_mode_status: str = "Disabled" + is_node_compliance: bool = False + is_vpc_attached: bool = False + has_public_ip: bool = False + + @property + def resource_id(self) -> str: + return self.id + + @property + def resource_name(self) -> str: + return self.name diff --git a/prowler/providers/e2e/services/securitygroup/__init__.py b/prowler/providers/e2e/services/securitygroup/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_client.py b/prowler/providers/e2e/services/securitygroup/securitygroup_client.py new file mode 100644 index 00000000000..a8a9b86044d --- /dev/null +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2e.services.securitygroup.securitygroup_service import ( + SecurityGroups, +) + +securitygroup_client = SecurityGroups(Provider.get_global_provider()) diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/__init__.py b/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json b/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json new file mode 100644 index 00000000000..6aebc2438bb --- /dev/null +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "securitygroup_no_all_traffic_rule", + "CheckTitle": "Check if E2E Cloud security groups do not allow all traffic", + "CheckType": [], + "ServiceName": "securitygroup", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "SecurityGroup", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud security groups do not have an all-traffic rule enabled.", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict security group rules to required ports and sources only.", + "Url": "https://hub.prowler.com/check/securitygroup_no_all_traffic_rule" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py b/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py new file mode 100644 index 00000000000..97b524a0551 --- /dev/null +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.securitygroup.securitygroup_client import ( + securitygroup_client, +) + + +class securitygroup_no_all_traffic_rule(Check): + def execute(self): + findings = [] + for group in securitygroup_client.security_groups: + report = CheckReportE2e(metadata=self.metadata(), resource=group) + report.status = "PASS" + report.status_extended = ( + f"Security group {group.name} does not allow all traffic." + ) + if group.is_all_traffic_rule: + report.status = "FAIL" + report.status_extended = ( + f"Security group {group.name} allows all traffic." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/__init__.py b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json new file mode 100644 index 00000000000..f57030c0d20 --- /dev/null +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "securitygroup_no_inbound_any_all_ports", + "CheckTitle": "Check if E2E Cloud security groups do not allow inbound all-protocol traffic from any source", + "CheckType": [], + "ServiceName": "securitygroup", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "SecurityGroup", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud security groups do not allow inbound all-protocol traffic from any source.", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Replace overly permissive inbound rules with least-privilege access.", + "Url": "https://hub.prowler.com/check/securitygroup_no_inbound_any_all_ports" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py new file mode 100644 index 00000000000..a842807d834 --- /dev/null +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py @@ -0,0 +1,30 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.securitygroup.securitygroup_client import ( + securitygroup_client, +) + + +def _is_permissive_inbound(rule) -> bool: + return ( + rule.rule_type.lower() == "inbound" + and rule.protocol_name.lower() == "all" + and rule.network.lower() == "any" + ) + + +class securitygroup_no_inbound_any_all_ports(Check): + def execute(self): + findings = [] + for group in securitygroup_client.security_groups: + report = CheckReportE2e(metadata=self.metadata(), resource=group) + report.status = "PASS" + report.status_extended = ( + f"Security group {group.name} does not allow inbound all-protocol traffic from any source." + ) + if any(_is_permissive_inbound(rule) for rule in group.rules): + report.status = "FAIL" + report.status_extended = ( + f"Security group {group.name} allows inbound all-protocol traffic from any source." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/__init__.py b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json new file mode 100644 index 00000000000..8e3c715b325 --- /dev/null +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "securitygroup_restrictive_default", + "CheckTitle": "Check if E2E Cloud nodes do not rely on permissive default security groups", + "CheckType": [], + "ServiceName": "securitygroup", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "SecurityGroup", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud nodes do not use only default security groups with overly permissive inbound rules.", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach custom security groups with least-privilege rules to nodes.", + "Url": "https://hub.prowler.com/check/securitygroup_restrictive_default" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py new file mode 100644 index 00000000000..d3047e31885 --- /dev/null +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py @@ -0,0 +1,42 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.securitygroup.securitygroup_client import ( + securitygroup_client, +) + + +def _has_permissive_inbound(rules) -> bool: + for rule in rules: + if ( + rule.rule_type.lower() == "inbound" + and rule.protocol_name.lower() == "all" + and rule.network.lower() == "any" + ): + return True + return False + + +class securitygroup_restrictive_default(Check): + def execute(self): + findings = [] + node_groups: dict[str, list] = {} + for group in securitygroup_client.node_security_groups: + node_groups.setdefault(group.node_id, []).append(group) + + for node_id, groups in node_groups.items(): + resource = groups[0] + report = CheckReportE2e(metadata=self.metadata(), resource=resource) + report.status = "PASS" + report.status_extended = ( + f"Node {resource.node_name} does not rely on a permissive default security group." + ) + + default_groups = [group for group in groups if group.is_default] + if default_groups and len(groups) == len(default_groups): + if any(_has_permissive_inbound(group.rules) for group in default_groups): + report.status = "FAIL" + report.status_extended = ( + f"Node {resource.node_name} uses only default security groups with overly permissive inbound rules." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_service.py b/prowler/providers/e2e/services/securitygroup/securitygroup_service.py new file mode 100644 index 00000000000..70cbfa1bc20 --- /dev/null +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_service.py @@ -0,0 +1,145 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2e.lib.service.service import E2eService + + +class SecurityGroups(E2eService): + """Service class for E2E Cloud security groups.""" + + def __init__(self, provider): + super().__init__("securitygroup", provider) + self.security_groups: list[SecurityGroupResource] = [] + self.node_security_groups: list[NodeSecurityGroup] = [] + self._fetch_security_groups() + self._fetch_node_security_groups() + + def _fetch_security_groups(self): + for location in self.provider.session.locations: + try: + groups = self.client.get_data("/security_group/", location=location) + if not isinstance(groups, list): + continue + + for item in groups: + rules = [ + SecurityGroupRule( + id=str(rule.get("id", "")), + rule_type=rule.get("rule_type", ""), + protocol_name=rule.get("protocol_name", ""), + port_range=rule.get("port_range", ""), + network=rule.get("network", ""), + network_cidr=rule.get("network_cidr", ""), + ) + for rule in item.get("rules", []) + ] + self.security_groups.append( + SecurityGroupResource( + id=str(item.get("id", "")), + name=item.get("name", ""), + location=location, + description=item.get("description", ""), + is_default=bool(item.get("is_default", False)), + is_all_traffic_rule=bool( + item.get("is_all_traffic_rule", False) + ), + rules=rules, + ) + ) + except Exception as error: + logger.error( + f"securitygroup - Error fetching groups in {location}: {error}" + ) + + def _fetch_node_security_groups(self): + from prowler.providers.e2e.services.node.nodes_client import nodes_client + + for node in nodes_client.nodes: + if not node.vm_id: + continue + try: + attached = self.client.get_data( + f"/security_group/{node.vm_id}/attach/", + location=node.location, + ) + if not isinstance(attached, list): + continue + + for item in attached: + rules = [ + SecurityGroupRule( + id=str(rule.get("id", "")), + rule_type=rule.get("rule_type", ""), + protocol_name=rule.get("protocol_name", ""), + port_range=rule.get("port_range", ""), + network=rule.get("network", ""), + network_cidr=rule.get("network_cidr", ""), + ) + for rule in item.get("rules", []) + ] + self.node_security_groups.append( + NodeSecurityGroup( + node_id=node.id, + node_name=node.name, + vm_id=node.vm_id, + location=node.location, + security_group_id=str(item.get("id", "")), + name=item.get("name", ""), + is_default=bool(item.get("is_default", False)), + is_all_traffic_rule=bool( + item.get("is_all_traffic_rule", False) + ), + rules=rules, + ) + ) + except Exception as error: + logger.error( + f"securitygroup - Error fetching attached groups for node {node.id}: {error}" + ) + + +class SecurityGroupRule(BaseModel): + id: str + rule_type: str + protocol_name: str + port_range: str + network: str + network_cidr: str + + +class SecurityGroupResource(BaseModel): + id: str + name: str + location: str + description: str = "" + is_default: bool = False + is_all_traffic_rule: bool = False + rules: list[SecurityGroupRule] = [] + + @property + def resource_id(self) -> str: + return self.id + + @property + def resource_name(self) -> str: + return self.name + + +class NodeSecurityGroup(BaseModel): + node_id: str + node_name: str + vm_id: str + location: str + security_group_id: str + name: str + is_default: bool = False + is_all_traffic_rule: bool = False + rules: list[SecurityGroupRule] = [] + + @property + def resource_id(self) -> str: + return f"{self.node_id}:{self.security_group_id}" + + @property + def resource_name(self) -> str: + return f"{self.node_name}/{self.name}" diff --git a/prowler/providers/e2e/services/storage/__init__.py b/prowler/providers/e2e/services/storage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/__init__.py b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json new file mode 100644 index 00000000000..a403691146e --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "storage_block_volume_not_orphaned", + "CheckTitle": "Check if E2E Cloud block volumes are not orphaned", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "BlockVolume", + "ResourceGroup": "storage", + "Description": "Check if E2E Cloud block volumes in Available state are attached to a node.", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach available block volumes to nodes or delete unused volumes.", + "Url": "https://hub.prowler.com/check/storage_block_volume_not_orphaned" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py new file mode 100644 index 00000000000..42dd72f2838 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.storage.storage_client import storage_client + + +class storage_block_volume_not_orphaned(Check): + def execute(self): + findings = [] + for volume in storage_client.block_volumes: + report = CheckReportE2e(metadata=self.metadata(), resource=volume) + report.status = "PASS" + report.status_extended = ( + f"Block volume {volume.name} is attached or not in an available orphaned state." + ) + if volume.status == "Available" and not volume.is_attached: + report.status = "FAIL" + report.status_extended = ( + f"Block volume {volume.name} is available and not attached to any node." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/__init__.py b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.metadata.json b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.metadata.json new file mode 100644 index 00000000000..e9c315cb4d7 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "storage_bucket_encryption_enabled", + "CheckTitle": "Check if E2E Cloud object storage buckets have encryption enabled", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "ObjectStorageBucket", + "ResourceGroup": "storage", + "Description": "Check if E2E Cloud object storage buckets have encryption enabled.", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable encryption on object storage buckets.", + "Url": "https://hub.prowler.com/check/storage_bucket_encryption_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py new file mode 100644 index 00000000000..8db56b16285 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.storage.storage_client import storage_client + + +class storage_bucket_encryption_enabled(Check): + def execute(self): + findings = [] + for bucket in storage_client.buckets: + report = CheckReportE2e(metadata=self.metadata(), resource=bucket) + report.status = "PASS" + report.status_extended = ( + f"Object storage bucket {bucket.name} has encryption enabled." + ) + if not bucket.is_encryption_enabled: + report.status = "FAIL" + report.status_extended = ( + f"Object storage bucket {bucket.name} does not have encryption enabled." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/__init__.py b/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.metadata.json b/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.metadata.json new file mode 100644 index 00000000000..a9a21bbd155 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "storage_bucket_public_access_disabled", + "CheckTitle": "Check if E2E Cloud object storage buckets do not allow public access", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "ObjectStorageBucket", + "ResourceGroup": "storage", + "Description": "Check if E2E Cloud object storage buckets have public access disabled.", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable public access on object storage buckets.", + "Url": "https://hub.prowler.com/check/storage_bucket_public_access_disabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.py b/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.py new file mode 100644 index 00000000000..f1177a53ca1 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.storage.storage_client import storage_client + + +class storage_bucket_public_access_disabled(Check): + def execute(self): + findings = [] + for bucket in storage_client.buckets: + report = CheckReportE2e(metadata=self.metadata(), resource=bucket) + report.status = "PASS" + report.status_extended = ( + f"Object storage bucket {bucket.name} does not allow public access." + ) + if bucket.is_public_access_enabled: + report.status = "FAIL" + report.status_extended = ( + f"Object storage bucket {bucket.name} allows public access." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/__init__.py b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.metadata.json b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.metadata.json new file mode 100644 index 00000000000..a1732540905 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "storage_bucket_versioning_enabled", + "CheckTitle": "Check if E2E Cloud object storage buckets have versioning enabled", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "ObjectStorageBucket", + "ResourceGroup": "storage", + "Description": "Check if E2E Cloud object storage buckets have versioning enabled.", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable versioning on object storage buckets.", + "Url": "https://hub.prowler.com/check/storage_bucket_versioning_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ] +} diff --git a/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py new file mode 100644 index 00000000000..1a789168359 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.storage.storage_client import storage_client + + +class storage_bucket_versioning_enabled(Check): + def execute(self): + findings = [] + for bucket in storage_client.buckets: + report = CheckReportE2e(metadata=self.metadata(), resource=bucket) + report.status = "PASS" + report.status_extended = ( + f"Object storage bucket {bucket.name} has versioning enabled." + ) + if bucket.versioning_status != "Enabled": + report.status = "FAIL" + report.status_extended = ( + f"Object storage bucket {bucket.name} does not have versioning enabled." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/storage/storage_client.py b/prowler/providers/e2e/services/storage/storage_client.py new file mode 100644 index 00000000000..96adca9211d --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2e.services.storage.storage_service import Storage + +storage_client = Storage(Provider.get_global_provider()) diff --git a/prowler/providers/e2e/services/storage/storage_service.py b/prowler/providers/e2e/services/storage/storage_service.py new file mode 100644 index 00000000000..249efa53d80 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_service.py @@ -0,0 +1,104 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2e.lib.service.service import E2eService + + +class Storage(E2eService): + """Service class for E2E Cloud storage resources.""" + + def __init__(self, provider): + super().__init__("storage", provider) + self.block_volumes: list[BlockVolume] = [] + self.buckets: list[StorageBucket] = [] + self._fetch_block_volumes() + self._fetch_buckets() + + def _fetch_block_volumes(self): + for location in self.provider.session.locations: + try: + volumes = self.client.paginate( + "/block_storage/", + location=location, + ) + for item in volumes: + vm_detail = item.get("vm_detail", {}) or {} + self.block_volumes.append( + BlockVolume( + id=str(item.get("block_id", "")), + name=item.get("name", ""), + location=location, + status=item.get("status", ""), + size_string=item.get("size_string", ""), + is_attached=bool(vm_detail), + ) + ) + except Exception as error: + logger.error( + f"storage - Error fetching block volumes in {location}: {error}" + ) + + def _fetch_buckets(self): + for location in self.provider.session.locations: + try: + buckets = self.client.paginate( + "/storage/buckets/", + location=location, + ) + for item in buckets: + self.buckets.append( + StorageBucket( + id=str(item.get("id", "")), + name=item.get("name", ""), + location=location, + status=item.get("status", ""), + versioning_status=item.get("versioning_status", "Off"), + is_public_access_enabled=bool( + item.get("is_public_access_enabled", False) + ), + is_encryption_enabled=bool( + item.get("is_encryption_enabled", False) + ), + is_lock_enabled=bool(item.get("is_lock_enabled", False)), + ) + ) + except Exception as error: + logger.error( + f"storage - Error fetching buckets in {location}: {error}" + ) + + +class BlockVolume(BaseModel): + id: str + name: str + location: str + status: str = "" + size_string: str = "" + is_attached: bool = False + + @property + def resource_id(self) -> str: + return self.id + + @property + def resource_name(self) -> str: + return self.name + + +class StorageBucket(BaseModel): + id: str + name: str + location: str + status: str = "" + versioning_status: str = "Off" + is_public_access_enabled: bool = False + is_encryption_enabled: bool = False + is_lock_enabled: bool = False + + @property + def resource_id(self) -> str: + return self.id + + @property + def resource_name(self) -> str: + return self.name diff --git a/tests/providers/e2e/e2e_fixtures.py b/tests/providers/e2e/e2e_fixtures.py new file mode 100644 index 00000000000..59973b1324f --- /dev/null +++ b/tests/providers/e2e/e2e_fixtures.py @@ -0,0 +1,29 @@ +from unittest.mock import MagicMock + +from prowler.providers.e2e.e2e_provider import E2eProvider +from prowler.providers.e2e.models import E2eIdentityInfo, E2eSession + + +def set_mocked_e2e_provider( + project_id: int = 12345, + locations: list[str] | None = None, + audit_config: dict | None = None, + fixer_config: dict | None = None, +): + """Create a mocked E2E provider for tests without network calls.""" + provider = MagicMock(spec=E2eProvider) + provider.type = "e2e" + provider.audit_config = audit_config or {} + provider.fixer_config = fixer_config or {} + provider.identity = E2eIdentityInfo( + project_id=project_id, + locations=locations or ["Delhi"], + ) + provider.session = E2eSession( + api_key="test-api-key", + auth_token="test-auth-token", + project_id=project_id, + locations=locations or ["Delhi"], + http_session=MagicMock(), + ) + return provider diff --git a/tests/providers/e2e/e2e_provider_test.py b/tests/providers/e2e/e2e_provider_test.py new file mode 100644 index 00000000000..322281ed667 --- /dev/null +++ b/tests/providers/e2e/e2e_provider_test.py @@ -0,0 +1,81 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest + +from prowler.providers.common.models import Connection +from prowler.providers.e2e.e2e_provider import E2eProvider +from prowler.providers.e2e.exceptions.exceptions import E2eCredentialsError + + +class TestE2eProvider: + @patch("prowler.providers.e2e.e2e_provider.load_and_validate_config_file") + def test_e2e_provider_init_success(self, mock_load_config): + mock_load_config.return_value = {} + + provider = E2eProvider( + api_key="test-api-key", + auth_token="test-auth-token", + project_id="12345", + locations=["Delhi"], + ) + + assert provider.session.api_key == "test-api-key" + assert provider.session.auth_token == "test-auth-token" + assert provider.session.project_id == 12345 + assert provider.identity.project_id == 12345 + assert provider.session.locations == ["Delhi"] + assert provider.session.http_session.headers["Authorization"] == ( + "Bearer test-auth-token" + ) + + @patch("prowler.providers.e2e.e2e_provider.load_and_validate_config_file") + def test_e2e_provider_init_missing_credentials(self, mock_load_config): + mock_load_config.return_value = {} + + with pytest.raises(E2eCredentialsError): + E2eProvider(api_key="", auth_token="token", project_id="1") + + @patch.dict( + os.environ, + { + "E2E_API_KEY": "env-api-key", + "E2E_AUTH_TOKEN": "env-auth-token", + "E2E_PROJECT_ID": "99", + "E2E_LOCATION": "Chennai", + }, + clear=False, + ) + @patch("prowler.providers.e2e.e2e_provider.load_and_validate_config_file") + def test_e2e_provider_init_from_env(self, mock_load_config): + mock_load_config.return_value = {} + + provider = E2eProvider() + + assert provider.session.api_key == "env-api-key" + assert provider.session.project_id == 99 + assert provider.session.locations == ["Chennai"] + + @patch("prowler.providers.e2e.e2e_provider.E2eProvider.setup_session") + def test_e2e_test_connection_success(self, mock_setup_session): + mock_session = MagicMock() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_session.http_session.get.return_value = mock_response + mock_session.api_key = "key" + mock_session.project_id = 1 + mock_session.base_url = "https://api.e2enetworks.com/myaccount/api/v1" + mock_setup_session.return_value = mock_session + + result = E2eProvider.test_connection( + api_key="key", + auth_token="token", + project_id=1, + locations=["Delhi"], + ) + + assert result == Connection(is_connected=True) + + def test_e2e_test_connection_missing_credentials(self): + with pytest.raises(E2eCredentialsError): + E2eProvider.test_connection(api_key="", auth_token="", project_id=None) diff --git a/tests/providers/e2e/lib/arguments/arguments_test.py b/tests/providers/e2e/lib/arguments/arguments_test.py new file mode 100644 index 00000000000..d319a0fc0e2 --- /dev/null +++ b/tests/providers/e2e/lib/arguments/arguments_test.py @@ -0,0 +1,27 @@ +from unittest.mock import MagicMock + +from prowler.providers.e2e.lib.arguments.arguments import validate_arguments + + +class TestE2eArguments: + def test_validate_arguments_success(self): + arguments = MagicMock() + arguments.e2e_api_key = "key" + arguments.e2e_auth_token = "token" + arguments.e2e_project_id = "123" + + valid, message = validate_arguments(arguments) + + assert valid is True + assert message == "" + + def test_validate_arguments_missing_project_id(self): + arguments = MagicMock() + arguments.e2e_api_key = "key" + arguments.e2e_auth_token = "token" + arguments.e2e_project_id = None + + valid, message = validate_arguments(arguments) + + assert valid is False + assert "project ID" in message diff --git a/tests/providers/e2e/services/node/node_public_ip_not_assigned_test.py b/tests/providers/e2e/services/node/node_public_ip_not_assigned_test.py new file mode 100644 index 00000000000..fcd804c94e5 --- /dev/null +++ b/tests/providers/e2e/services/node/node_public_ip_not_assigned_test.py @@ -0,0 +1,69 @@ +from unittest import mock + +from prowler.providers.e2e.services.node.nodes_service import Node +from tests.providers.e2e.e2e_fixtures import set_mocked_e2e_provider + + +class TestNodePublicIpCheck: + def test_empty_nodes(self): + nodes_client = mock.MagicMock() + nodes_client.nodes = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2e_provider(), + ), + mock.patch( + "prowler.providers.e2e.services.node.node_public_ip_not_assigned.node_public_ip_not_assigned.nodes_client", + new=nodes_client, + ), + ): + from prowler.providers.e2e.services.node.node_public_ip_not_assigned.node_public_ip_not_assigned import ( + node_public_ip_not_assigned, + ) + + check = node_public_ip_not_assigned() + assert check.execute() == [] + + def test_pass_and_fail(self): + nodes_client = mock.MagicMock() + nodes_client.nodes = [ + Node( + id="1", + name="private-node", + status="Running", + location="Delhi", + vm_id="1", + has_public_ip=False, + ), + Node( + id="2", + name="public-node", + status="Running", + location="Delhi", + vm_id="2", + has_public_ip=True, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2e_provider(), + ), + mock.patch( + "prowler.providers.e2e.services.node.node_public_ip_not_assigned.node_public_ip_not_assigned.nodes_client", + new=nodes_client, + ), + ): + from prowler.providers.e2e.services.node.node_public_ip_not_assigned.node_public_ip_not_assigned import ( + node_public_ip_not_assigned, + ) + + check = node_public_ip_not_assigned() + findings = check.execute() + + assert len(findings) == 2 + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" diff --git a/tests/providers/e2e/services/node/nodes_service_test.py b/tests/providers/e2e/services/node/nodes_service_test.py new file mode 100644 index 00000000000..8e9f0fc5694 --- /dev/null +++ b/tests/providers/e2e/services/node/nodes_service_test.py @@ -0,0 +1,69 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.e2e.services.node.nodes_service import Node, Nodes + + +class TestNodesService: + @patch("prowler.providers.e2e.services.node.nodes_service.E2eService.__init__") + def test_fetch_nodes_enriches_detail(self, mock_super_init): + mock_super_init.return_value = None + + provider = MagicMock() + provider.session.locations = ["Delhi"] + service = Nodes.__new__(Nodes) + service.provider = provider + service.client = MagicMock() + service.nodes = [] + + service.client.get_data.side_effect = [ + [ + { + "id": 101, + "name": "node-1", + "status": "Running", + "public_ip_address": "1.2.3.4", + "is_accidental_protection": True, + "isEncryptionEnabled": True, + "is_locked": False, + "rescue_mode_status": "Disabled", + } + ], + { + "vm_id": 555, + "is_node_compliance": True, + "is_vpc_attached": True, + }, + ] + + service._fetch_nodes() + + assert len(service.nodes) == 1 + node = service.nodes[0] + assert node.id == "101" + assert node.vm_id == "555" + assert node.has_public_ip is True + assert node.is_node_compliance is True + assert node.is_vpc_attached is True + + +class TestNodeCheckLogic: + def test_node_public_ip_detection(self): + public_node = Node( + id="1", + name="public", + status="Running", + location="Delhi", + vm_id="1", + has_public_ip=True, + ) + private_node = Node( + id="2", + name="private", + status="Running", + location="Delhi", + vm_id="2", + has_public_ip=False, + ) + + assert public_node.has_public_ip is True + assert private_node.has_public_ip is False From e4834f0bf416de04904bc09139bde70ff139e116 Mon Sep 17 00:00:00 2001 From: Deepak Dalvi Date: Sat, 20 Jun 2026 22:23:58 +0530 Subject: [PATCH 2/8] e2e addon checks --- .../e2e/services/database/__init__.py | 0 .../e2e/services/database/database_client.py | 4 + .../__init__.py | 0 ...abase_cluster_backup_enabled.metadata.json | 34 ++++ .../database_cluster_backup_enabled.py | 16 ++ .../__init__.py | 0 ...uster_default_admin_username.metadata.json | 34 ++++ ...database_cluster_default_admin_username.py | 20 +++ .../__init__.py | 0 ...ster_ip_whitelist_configured.metadata.json | 34 ++++ ...atabase_cluster_ip_whitelist_configured.py | 22 +++ .../__init__.py | 0 ...uster_public_ip_not_assigned.metadata.json | 34 ++++ ...database_cluster_public_ip_not_assigned.py | 16 ++ .../database_cluster_running/__init__.py | 0 .../database_cluster_running.metadata.json | 34 ++++ .../database_cluster_running.py | 18 ++ .../database_cluster_ssl_enabled/__init__.py | 0 ...database_cluster_ssl_enabled.metadata.json | 34 ++++ .../database_cluster_ssl_enabled.py | 16 ++ .../__init__.py | 0 ...plica_public_ip_not_assigned.metadata.json | 34 ++++ ...database_replica_public_ip_not_assigned.py | 22 +++ .../e2e/services/database/database_service.py | 160 ++++++++++++++++++ .../e2e/services/network/__init__.py | 0 .../e2e/services/network/network_client.py | 4 + .../__init__.py | 0 ...rveip_floating_ip_unattached.metadata.json | 34 ++++ ...etwork_reserveip_floating_ip_unattached.py | 18 ++ .../__init__.py | 0 ...reserveip_orphaned_public_ip.metadata.json | 34 ++++ .../network_reserveip_orphaned_public_ip.py | 18 ++ .../e2e/services/network/network_service.py | 153 +++++++++++++++++ .../__init__.py | 0 ...twork_vpc_has_attached_nodes.metadata.json | 34 ++++ .../network_vpc_has_attached_nodes.py | 16 ++ .../network/network_vpc_is_active/__init__.py | 0 .../network_vpc_is_active.metadata.json | 34 ++++ .../network_vpc_is_active.py | 16 ++ .../__init__.py | 0 ...ering_external_peer_disabled.metadata.json | 34 ++++ ...work_vpc_peering_external_peer_disabled.py | 20 +++ .../__init__.py | 0 ..._bucket_lifecycle_configured.metadata.json | 34 ++++ .../storage_bucket_lifecycle_configured.py | 20 +++ .../storage_bucket_lock_enabled/__init__.py | 0 .../storage_bucket_lock_enabled.metadata.json | 34 ++++ .../storage_bucket_lock_enabled.py | 16 ++ .../storage_efs_backup_enabled/__init__.py | 0 .../storage_efs_backup_enabled.metadata.json | 34 ++++ .../storage_efs_backup_enabled.py | 16 ++ .../__init__.py | 0 ...ge_efs_vpc_access_restricted.metadata.json | 34 ++++ .../storage_efs_vpc_access_restricted.py | 20 +++ .../e2e/services/storage/storage_service.py | 105 ++++++++++++ 55 files changed, 1260 insertions(+) create mode 100644 prowler/providers/e2e/services/database/__init__.py create mode 100644 prowler/providers/e2e/services/database/database_client.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_backup_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_default_admin_username/__init__.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json create mode 100644 prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/__init__.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json create mode 100644 prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/__init__.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json create mode 100644 prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_running/__init__.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.metadata.json create mode 100644 prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_ssl_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py create mode 100644 prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/__init__.py create mode 100644 prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json create mode 100644 prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py create mode 100644 prowler/providers/e2e/services/database/database_service.py create mode 100644 prowler/providers/e2e/services/network/__init__.py create mode 100644 prowler/providers/e2e/services/network/network_client.py create mode 100644 prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/__init__.py create mode 100644 prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json create mode 100644 prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py create mode 100644 prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/__init__.py create mode 100644 prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json create mode 100644 prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py create mode 100644 prowler/providers/e2e/services/network/network_service.py create mode 100644 prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/__init__.py create mode 100644 prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json create mode 100644 prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py create mode 100644 prowler/providers/e2e/services/network/network_vpc_is_active/__init__.py create mode 100644 prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json create mode 100644 prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py create mode 100644 prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/__init__.py create mode 100644 prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json create mode 100644 prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/__init__.py create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.metadata.json create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py create mode 100644 prowler/providers/e2e/services/storage/storage_efs_backup_enabled/__init__.py create mode 100644 prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json create mode 100644 prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py create mode 100644 prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/__init__.py create mode 100644 prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json create mode 100644 prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py diff --git a/prowler/providers/e2e/services/database/__init__.py b/prowler/providers/e2e/services/database/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/database/database_client.py b/prowler/providers/e2e/services/database/database_client.py new file mode 100644 index 00000000000..b9cff1a32a1 --- /dev/null +++ b/prowler/providers/e2e/services/database/database_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2e.services.database.database_service import Database + +database_client = Database(Provider.get_global_provider()) diff --git a/prowler/providers/e2e/services/database/database_cluster_backup_enabled/__init__.py b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json new file mode 100644 index 00000000000..0df1878bbf0 --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "database_cluster_backup_enabled", + "CheckTitle": "Check if E2E Cloud database clusters have backups enabled", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "database", + "Description": "Check if E2E Cloud database clusters have backups enabled", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud database clusters have backups enabled", + "Url": "https://hub.prowler.com/check/database_cluster_backup_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py new file mode 100644 index 00000000000..358901b57b6 --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.database.database_client import database_client + + +class database_cluster_backup_enabled(Check): + def execute(self): + findings = [] + for cluster in database_client.clusters: + report = CheckReportE2e(metadata=self.metadata(), resource=cluster) + report.status = "PASS" + report.status_extended = f"Database cluster {cluster.name} has backups enabled." + if not cluster.backup_enabled: + report.status = "FAIL" + report.status_extended = f"Database cluster {cluster.name} does not have backups enabled." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/database/database_cluster_default_admin_username/__init__.py b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json new file mode 100644 index 00000000000..21a4bfd226f --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "database_cluster_default_admin_username", + "CheckTitle": "Check if E2E Cloud database clusters do not use the default admin username", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "database", + "Description": "Check if E2E Cloud database clusters do not use the default admin username", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud database clusters do not use the default admin username", + "Url": "https://hub.prowler.com/check/database_cluster_default_admin_username" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py new file mode 100644 index 00000000000..fca3e7e9f2c --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.database.database_client import database_client + + +class database_cluster_default_admin_username(Check): + def execute(self): + findings = [] + for cluster in database_client.clusters: + report = CheckReportE2e(metadata=self.metadata(), resource=cluster) + report.status = "PASS" + report.status_extended = ( + f"Database cluster {cluster.name} does not use the default admin username." + ) + if cluster.master_username.lower() == "admin": + report.status = "FAIL" + report.status_extended = ( + f"Database cluster {cluster.name} uses the default admin username." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/__init__.py b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json new file mode 100644 index 00000000000..d2d4fed4bc2 --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "database_cluster_ip_whitelist_configured", + "CheckTitle": "Check if E2E Cloud database clusters with public IPs have IP whitelisting configured", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Other", + "ResourceGroup": "database", + "Description": "Check if E2E Cloud database clusters with public IPs have IP whitelisting configured", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud database clusters with public IPs have IP whitelisting configured", + "Url": "https://hub.prowler.com/check/database_cluster_ip_whitelist_configured" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py new file mode 100644 index 00000000000..f89dce37514 --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.database.database_client import database_client + + +class database_cluster_ip_whitelist_configured(Check): + def execute(self): + findings = [] + for cluster in database_client.clusters: + if not cluster.master_has_public_ip: + continue + report = CheckReportE2e(metadata=self.metadata(), resource=cluster) + report.status = "PASS" + report.status_extended = ( + f"Database cluster {cluster.name} has IP whitelisting configured." + ) + if not cluster.whitelisted_ips: + report.status = "FAIL" + report.status_extended = ( + f"Database cluster {cluster.name} has a public IP but no whitelisted IPs." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/__init__.py b/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json b/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json new file mode 100644 index 00000000000..5f7904403e9 --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "database_cluster_public_ip_not_assigned", + "CheckTitle": "Check if E2E Cloud database clusters do not expose a public IP on the master node", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Other", + "ResourceGroup": "database", + "Description": "Check if E2E Cloud database clusters do not expose a public IP on the master node", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud database clusters do not expose a public IP on the master node", + "Url": "https://hub.prowler.com/check/database_cluster_public_ip_not_assigned" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py b/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py new file mode 100644 index 00000000000..dadbad4e6fc --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.database.database_client import database_client + + +class database_cluster_public_ip_not_assigned(Check): + def execute(self): + findings = [] + for cluster in database_client.clusters: + report = CheckReportE2e(metadata=self.metadata(), resource=cluster) + report.status = "PASS" + report.status_extended = f"Database cluster {cluster.name} master node does not have a public IP." + if cluster.master_has_public_ip: + report.status = "FAIL" + report.status_extended = f"Database cluster {cluster.name} master node has a public IP assigned." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/database/database_cluster_running/__init__.py b/prowler/providers/e2e/services/database/database_cluster_running/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.metadata.json b/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.metadata.json new file mode 100644 index 00000000000..1b94b1670e0 --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "database_cluster_running", + "CheckTitle": "Check if E2E Cloud database clusters are in RUNNING status", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "Other", + "ResourceGroup": "database", + "Description": "Check if E2E Cloud database clusters are in RUNNING status", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud database clusters are in RUNNING status", + "Url": "https://hub.prowler.com/check/database_cluster_running" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py b/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py new file mode 100644 index 00000000000..84650df55f1 --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py @@ -0,0 +1,18 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.database.database_client import database_client + + +class database_cluster_running(Check): + def execute(self): + findings = [] + for cluster in database_client.clusters: + report = CheckReportE2e(metadata=self.metadata(), resource=cluster) + report.status = "PASS" + report.status_extended = f"Database cluster {cluster.name} is running." + if cluster.status != "RUNNING": + report.status = "FAIL" + report.status_extended = ( + f"Database cluster {cluster.name} is not running (status: {cluster.status})." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/__init__.py b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json new file mode 100644 index 00000000000..dd1b61075fb --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "database_cluster_ssl_enabled", + "CheckTitle": "Check if E2E Cloud database clusters have SSL enabled on the master node", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Other", + "ResourceGroup": "database", + "Description": "Check if E2E Cloud database clusters have SSL enabled on the master node", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud database clusters have SSL enabled on the master node", + "Url": "https://hub.prowler.com/check/database_cluster_ssl_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py new file mode 100644 index 00000000000..7bde2dc41ff --- /dev/null +++ b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.database.database_client import database_client + + +class database_cluster_ssl_enabled(Check): + def execute(self): + findings = [] + for cluster in database_client.clusters: + report = CheckReportE2e(metadata=self.metadata(), resource=cluster) + report.status = "PASS" + report.status_extended = f"Database cluster {cluster.name} has SSL enabled on the master node." + if not cluster.master_ssl_enabled: + report.status = "FAIL" + report.status_extended = f"Database cluster {cluster.name} does not have SSL enabled on the master node." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/__init__.py b/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json b/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json new file mode 100644 index 00000000000..d3d82b37ffb --- /dev/null +++ b/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "database_replica_public_ip_not_assigned", + "CheckTitle": "Check if E2E Cloud database read replicas do not have a public IP assigned", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Other", + "ResourceGroup": "database", + "Description": "Check if E2E Cloud database read replicas do not have a public IP assigned", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud database read replicas do not have a public IP assigned", + "Url": "https://hub.prowler.com/check/database_replica_public_ip_not_assigned" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py b/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py new file mode 100644 index 00000000000..0cd4b5b8d2e --- /dev/null +++ b/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.database.database_client import database_client + + +class database_replica_public_ip_not_assigned(Check): + def execute(self): + findings = [] + for instance in database_client.instances: + if instance.role != "replica": + continue + report = CheckReportE2e(metadata=self.metadata(), resource=instance) + report.status = "PASS" + report.status_extended = ( + f"Database replica {instance.name} does not have a public IP assigned." + ) + if instance.has_public_ip: + report.status = "FAIL" + report.status_extended = ( + f"Database replica {instance.name} has a public IP assigned." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/database/database_service.py b/prowler/providers/e2e/services/database/database_service.py new file mode 100644 index 00000000000..611af05c80c --- /dev/null +++ b/prowler/providers/e2e/services/database/database_service.py @@ -0,0 +1,160 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2e.lib.service.service import E2eService + + +def _has_public_ip(public_ip_address: str | None) -> bool: + if not public_ip_address: + return False + value = str(public_ip_address).strip() + if not value or value.lower() in ("[]", "null", "none"): + return False + return True + + +class Database(E2eService): + """Service class for E2E Cloud DBaaS (RDS) resources.""" + + def __init__(self, provider): + super().__init__("database", provider) + self.clusters: list[DatabaseCluster] = [] + self.instances: list[DatabaseInstance] = [] + self._fetch_clusters() + + def _fetch_clusters(self): + for location in self.provider.session.locations: + try: + cluster_list = self.client.get_data("/rds/cluster/", location=location) + if not isinstance(cluster_list, list): + continue + + for item in cluster_list: + cluster_id = str(item.get("id", "")) + detail = self._get_cluster_detail(cluster_id, location) + merged = {**item, **detail} + + master_node = merged.get("master_node", {}) or {} + database_info = master_node.get("database", {}) or {} + software = merged.get("software", {}) or master_node.get( + "plan", {} + ).get("software", {}) or {} + + cluster = DatabaseCluster( + id=cluster_id, + name=merged.get("name", ""), + location=location, + status=merged.get("status", ""), + software_name=software.get("name", ""), + software_version=software.get("version", ""), + backup_enabled=bool(merged.get("backup_enabled", False)), + whitelisted_ips=merged.get("whitelisted_ips", []) or [], + master_ssl_enabled=bool(master_node.get("ssl", False)), + master_public_ip=master_node.get("public_ip_address"), + master_username=database_info.get("username", ""), + master_has_public_ip=_has_public_ip( + master_node.get("public_ip_address") + ), + ) + self.clusters.append(cluster) + + self.instances.append( + DatabaseInstance( + id=str(master_node.get("instance_id", cluster_id)), + name=master_node.get("node_name", merged.get("name", "")), + cluster_id=cluster_id, + cluster_name=cluster.name, + location=location, + role="master", + public_ip_address=master_node.get("public_ip_address"), + has_public_ip=_has_public_ip( + master_node.get("public_ip_address") + ), + ssl_enabled=bool(master_node.get("ssl", False)), + username=database_info.get("username", ""), + ) + ) + + for slave in merged.get("slave_nodes", []) or []: + if not isinstance(slave, dict): + continue + slave_db = slave.get("database", {}) or {} + self.instances.append( + DatabaseInstance( + id=str(slave.get("instance_id", "")), + name=slave.get("node_name", ""), + cluster_id=cluster_id, + cluster_name=cluster.name, + location=location, + role="replica", + public_ip_address=slave.get("public_ip_address"), + has_public_ip=_has_public_ip( + slave.get("public_ip_address") + ), + ssl_enabled=bool(slave.get("ssl", False)), + username=slave_db.get("username", ""), + ) + ) + except Exception as error: + logger.error( + f"database - Error fetching clusters in {location}: {error}" + ) + + def _get_cluster_detail(self, cluster_id: str, location: str) -> dict: + if not cluster_id: + return {} + try: + data = self.client.get_data( + f"/rds/cluster/{cluster_id}/", + location=location, + ) + return data if isinstance(data, dict) else {} + except Exception as error: + logger.error( + f"database - Error fetching cluster detail {cluster_id}: {error}" + ) + return {} + + +class DatabaseCluster(BaseModel): + id: str + name: str + location: str + status: str = "" + software_name: str = "" + software_version: str = "" + backup_enabled: bool = False + whitelisted_ips: list = [] + master_ssl_enabled: bool = False + master_public_ip: str | None = None + master_username: str = "" + master_has_public_ip: bool = False + + @property + def resource_id(self) -> str: + return self.id + + @property + def resource_name(self) -> str: + return self.name + + +class DatabaseInstance(BaseModel): + id: str + name: str + cluster_id: str + cluster_name: str + location: str + role: str + public_ip_address: str | None = None + has_public_ip: bool = False + ssl_enabled: bool = False + username: str = "" + + @property + def resource_id(self) -> str: + return self.id + + @property + def resource_name(self) -> str: + return self.name diff --git a/prowler/providers/e2e/services/network/__init__.py b/prowler/providers/e2e/services/network/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/network/network_client.py b/prowler/providers/e2e/services/network/network_client.py new file mode 100644 index 00000000000..92cf447c31a --- /dev/null +++ b/prowler/providers/e2e/services/network/network_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2e.services.network.network_service import Network + +network_client = Network(Provider.get_global_provider()) diff --git a/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/__init__.py b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json new file mode 100644 index 00000000000..4e1da776da2 --- /dev/null +++ b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "network_reserveip_floating_ip_unattached", + "CheckTitle": "Check if E2E Cloud floating IPs are attached to nodes", + "CheckType": [], + "ServiceName": "network", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud floating IPs are attached to nodes", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud floating IPs are attached to nodes", + "Url": "https://hub.prowler.com/check/network_reserveip_floating_ip_unattached" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py new file mode 100644 index 00000000000..d6fc6a8362c --- /dev/null +++ b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py @@ -0,0 +1,18 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.network.network_client import network_client + + +class network_reserveip_floating_ip_unattached(Check): + def execute(self): + findings = [] + for ip in network_client.reserved_ips: + if ip.reserved_type != "FloatingIP": + continue + report = CheckReportE2e(metadata=self.metadata(), resource=ip) + report.status = "PASS" + report.status_extended = f"Floating IP {ip.ip_address} is attached to node(s)." + if ip.status != "Attached" or ip.floating_ip_attached_nodes_count == 0: + report.status = "FAIL" + report.status_extended = f"Floating IP {ip.ip_address} is not attached to any node." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/__init__.py b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json new file mode 100644 index 00000000000..4cfc7da7b3b --- /dev/null +++ b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "network_reserveip_orphaned_public_ip", + "CheckTitle": "Check if E2E Cloud public or addon IPs are attached", + "CheckType": [], + "ServiceName": "network", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud public or addon IPs are attached", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud public or addon IPs are attached", + "Url": "https://hub.prowler.com/check/network_reserveip_orphaned_public_ip" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py new file mode 100644 index 00000000000..0dc0a282def --- /dev/null +++ b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py @@ -0,0 +1,18 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.network.network_client import network_client + + +class network_reserveip_orphaned_public_ip(Check): + def execute(self): + findings = [] + for ip in network_client.reserved_ips: + if ip.reserved_type not in ("PublicIP", "AddonIP"): + continue + report = CheckReportE2e(metadata=self.metadata(), resource=ip) + report.status = "PASS" + report.status_extended = f"Reserved IP {ip.ip_address} is attached to a resource." + if ip.status != "Attached" or ip.vm_id is None: + report.status = "FAIL" + report.status_extended = f"Reserved IP {ip.ip_address} is orphaned (status: {ip.status})." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/network/network_service.py b/prowler/providers/e2e/services/network/network_service.py new file mode 100644 index 00000000000..a9af61d2624 --- /dev/null +++ b/prowler/providers/e2e/services/network/network_service.py @@ -0,0 +1,153 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2e.lib.service.service import E2eService + + +class Network(E2eService): + """Service class for E2E Cloud network resources.""" + + def __init__(self, provider): + super().__init__("network", provider) + self.vpcs: list[Vpc] = [] + self.reserved_ips: list[ReservedIp] = [] + self.vpc_tunnels: list[VpcTunnel] = [] + self._fetch_vpcs() + self._fetch_reserved_ips() + self._fetch_vpc_tunnels() + + def _fetch_vpcs(self): + for location in self.provider.session.locations: + try: + vpcs = self.client.paginate("/vpc/list/", location=location) + for item in vpcs: + gateway_node = item.get("gateway_node", {}) or {} + self.vpcs.append( + Vpc( + network_id=str(item.get("network_id", "")), + name=item.get("name", ""), + location=location, + is_active=bool(item.get("is_active", False)), + state=item.get("state", ""), + ipv4_cidr=item.get("ipv4_cidr", ""), + vm_count=int(item.get("vm_count", 0)), + gateway_node_id=str(gateway_node.get("node_id", "")), + gateway_public_ip=gateway_node.get("ip_address_public", ""), + ) + ) + except Exception as error: + logger.error( + f"network - Error fetching VPCs in {location}: {error}" + ) + + def _fetch_reserved_ips(self): + for location in self.provider.session.locations: + try: + ips = self.client.get_data("/reserve_ips/", location=location) + if not isinstance(ips, list): + continue + + for item in ips: + attached_nodes = item.get("floating_ip_attached_nodes", []) or [] + self.reserved_ips.append( + ReservedIp( + reserve_id=str(item.get("reserve_id", "")), + ip_address=item.get("ip_address", ""), + location=location, + status=item.get("status", ""), + reserved_type=item.get("reserved_type", ""), + vm_id=item.get("vm_id"), + floating_ip_attached_nodes_count=len(attached_nodes), + ) + ) + except Exception as error: + logger.error( + f"network - Error fetching reserved IPs in {location}: {error}" + ) + + def _fetch_vpc_tunnels(self): + for vpc in self.vpcs: + if not vpc.network_id: + continue + try: + tunnels = self.client.get_data( + f"/vpc/tunnels/{vpc.network_id}/", + location=vpc.location, + ) + if not isinstance(tunnels, list): + continue + + for item in tunnels: + self.vpc_tunnels.append( + VpcTunnel( + id=str(item.get("id", "")), + name=item.get("name", ""), + location=vpc.location, + local_vpc_network_id=vpc.network_id, + local_vpc_name=vpc.name, + status=item.get("status", ""), + is_peer_vpc_external=bool( + item.get("is_peer_vpc_external", False) + ), + ) + ) + except Exception as error: + logger.error( + f"network - Error fetching tunnels for VPC {vpc.network_id}: {error}" + ) + + +class Vpc(BaseModel): + network_id: str + name: str + location: str + is_active: bool = False + state: str = "" + ipv4_cidr: str = "" + vm_count: int = 0 + gateway_node_id: str = "" + gateway_public_ip: str = "" + + @property + def resource_id(self) -> str: + return self.network_id + + @property + def resource_name(self) -> str: + return self.name + + +class ReservedIp(BaseModel): + reserve_id: str + ip_address: str + location: str + status: str = "" + reserved_type: str = "" + vm_id: int | None = None + floating_ip_attached_nodes_count: int = 0 + + @property + def resource_id(self) -> str: + return self.reserve_id + + @property + def resource_name(self) -> str: + return self.ip_address + + +class VpcTunnel(BaseModel): + id: str + name: str + location: str + local_vpc_network_id: str + local_vpc_name: str + status: str = "" + is_peer_vpc_external: bool = False + + @property + def resource_id(self) -> str: + return self.id + + @property + def resource_name(self) -> str: + return self.name diff --git a/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/__init__.py b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json new file mode 100644 index 00000000000..b9de3bf6899 --- /dev/null +++ b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "network_vpc_has_attached_nodes", + "CheckTitle": "Check if E2E Cloud VPCs have attached nodes", + "CheckType": [], + "ServiceName": "network", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "Other", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud VPCs have attached nodes", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud VPCs have attached nodes", + "Url": "https://hub.prowler.com/check/network_vpc_has_attached_nodes" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py new file mode 100644 index 00000000000..33ae6accad8 --- /dev/null +++ b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.network.network_client import network_client + + +class network_vpc_has_attached_nodes(Check): + def execute(self): + findings = [] + for vpc in network_client.vpcs: + report = CheckReportE2e(metadata=self.metadata(), resource=vpc) + report.status = "PASS" + report.status_extended = f"VPC {vpc.name} has {vpc.vm_count} attached node(s)." + if vpc.vm_count <= 0: + report.status = "FAIL" + report.status_extended = f"VPC {vpc.name} has no attached nodes." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/network/network_vpc_is_active/__init__.py b/prowler/providers/e2e/services/network/network_vpc_is_active/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json b/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json new file mode 100644 index 00000000000..1020e1eb6ce --- /dev/null +++ b/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "network_vpc_is_active", + "CheckTitle": "Check if E2E Cloud VPCs are active", + "CheckType": [], + "ServiceName": "network", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud VPCs are active", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud VPCs are active", + "Url": "https://hub.prowler.com/check/network_vpc_is_active" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py b/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py new file mode 100644 index 00000000000..b5f172365d7 --- /dev/null +++ b/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.network.network_client import network_client + + +class network_vpc_is_active(Check): + def execute(self): + findings = [] + for vpc in network_client.vpcs: + report = CheckReportE2e(metadata=self.metadata(), resource=vpc) + report.status = "PASS" + report.status_extended = f"VPC {vpc.name} is active." + if not vpc.is_active or vpc.state != "Active": + report.status = "FAIL" + report.status_extended = f"VPC {vpc.name} is not active (state: {vpc.state})." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/__init__.py b/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json b/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json new file mode 100644 index 00000000000..7ef120a7dce --- /dev/null +++ b/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "network_vpc_peering_external_peer_disabled", + "CheckTitle": "Check if E2E Cloud VPC peering does not use external peers", + "CheckType": [], + "ServiceName": "network", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "Other", + "ResourceGroup": "network", + "Description": "Check if E2E Cloud VPC peering does not use external peers", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud VPC peering does not use external peers", + "Url": "https://hub.prowler.com/check/network_vpc_peering_external_peer_disabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py b/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py new file mode 100644 index 00000000000..76838c57000 --- /dev/null +++ b/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.network.network_client import network_client + + +class network_vpc_peering_external_peer_disabled(Check): + def execute(self): + findings = [] + for tunnel in network_client.vpc_tunnels: + report = CheckReportE2e(metadata=self.metadata(), resource=tunnel) + report.status = "PASS" + report.status_extended = ( + f"VPC peering {tunnel.name} does not use an external peer VPC." + ) + if tunnel.is_peer_vpc_external: + report.status = "FAIL" + report.status_extended = ( + f"VPC peering {tunnel.name} uses an external peer VPC." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/__init__.py b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.metadata.json b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.metadata.json new file mode 100644 index 00000000000..20053f3c8a3 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "storage_bucket_lifecycle_configured", + "CheckTitle": "Check if E2E Cloud object storage buckets have lifecycle configuration enabled", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "Other", + "ResourceGroup": "storage", + "Description": "Check if E2E Cloud object storage buckets have lifecycle configuration enabled", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud object storage buckets have lifecycle configuration enabled", + "Url": "https://hub.prowler.com/check/storage_bucket_lifecycle_configured" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py new file mode 100644 index 00000000000..2f6307147cb --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.storage.storage_client import storage_client + + +class storage_bucket_lifecycle_configured(Check): + def execute(self): + findings = [] + for bucket in storage_client.buckets: + report = CheckReportE2e(metadata=self.metadata(), resource=bucket) + report.status = "PASS" + report.status_extended = ( + f"Object storage bucket {bucket.name} has lifecycle configuration enabled." + ) + if bucket.lifecycle_configuration_status != "Configured": + report.status = "FAIL" + report.status_extended = ( + f"Object storage bucket {bucket.name} does not have lifecycle configuration enabled." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/__init__.py b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.metadata.json b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.metadata.json new file mode 100644 index 00000000000..a0a4c941aba --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "storage_bucket_lock_enabled", + "CheckTitle": "Check if E2E Cloud object storage buckets have object lock enabled", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "storage", + "Description": "Check if E2E Cloud object storage buckets have object lock enabled", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud object storage buckets have object lock enabled", + "Url": "https://hub.prowler.com/check/storage_bucket_lock_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py new file mode 100644 index 00000000000..f8d5670fbd2 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.storage.storage_client import storage_client + + +class storage_bucket_lock_enabled(Check): + def execute(self): + findings = [] + for bucket in storage_client.buckets: + report = CheckReportE2e(metadata=self.metadata(), resource=bucket) + report.status = "PASS" + report.status_extended = f"Object storage bucket {bucket.name} has object lock enabled." + if not bucket.is_lock_enabled: + report.status = "FAIL" + report.status_extended = f"Object storage bucket {bucket.name} does not have object lock enabled." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/__init__.py b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json new file mode 100644 index 00000000000..4663c2f53d3 --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "storage_efs_backup_enabled", + "CheckTitle": "Check if E2E Cloud EFS volumes have backup enabled", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "storage", + "Description": "Check if E2E Cloud EFS volumes have backup enabled", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud EFS volumes have backup enabled", + "Url": "https://hub.prowler.com/check/storage_efs_backup_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py new file mode 100644 index 00000000000..5111eaa2a3c --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py @@ -0,0 +1,16 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.storage.storage_client import storage_client + + +class storage_efs_backup_enabled(Check): + def execute(self): + findings = [] + for volume in storage_client.efs_volumes: + report = CheckReportE2e(metadata=self.metadata(), resource=volume) + report.status = "PASS" + report.status_extended = f"EFS volume {volume.name} has backup enabled." + if not volume.is_backup_enabled: + report.status = "FAIL" + report.status_extended = f"EFS volume {volume.name} does not have backup enabled." + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/__init__.py b/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json b/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json new file mode 100644 index 00000000000..7b6d7eb8e5b --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2e", + "CheckID": "storage_efs_vpc_access_restricted", + "CheckTitle": "Check if E2E Cloud EFS volumes restrict VPC access", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Other", + "ResourceGroup": "storage", + "Description": "Check if E2E Cloud EFS volumes restrict VPC access", + "Risk": "", + "RelatedUrl": "", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Check if E2E Cloud EFS volumes restrict VPC access", + "Url": "https://hub.prowler.com/check/storage_efs_vpc_access_restricted" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + ] +} diff --git a/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py b/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py new file mode 100644 index 00000000000..0fdf1e25fef --- /dev/null +++ b/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2e +from prowler.providers.e2e.services.storage.storage_client import storage_client + + +class storage_efs_vpc_access_restricted(Check): + def execute(self): + findings = [] + for volume in storage_client.efs_volumes: + report = CheckReportE2e(metadata=self.metadata(), resource=volume) + report.status = "PASS" + report.status_extended = ( + f"EFS volume {volume.name} does not allow all VPC resources." + ) + if volume.is_all_vpc_resources_allowed: + report.status = "FAIL" + report.status_extended = ( + f"EFS volume {volume.name} allows access from all VPC resources." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2e/services/storage/storage_service.py b/prowler/providers/e2e/services/storage/storage_service.py index 249efa53d80..2314246f068 100644 --- a/prowler/providers/e2e/services/storage/storage_service.py +++ b/prowler/providers/e2e/services/storage/storage_service.py @@ -11,8 +11,12 @@ def __init__(self, provider): super().__init__("storage", provider) self.block_volumes: list[BlockVolume] = [] self.buckets: list[StorageBucket] = [] + self.efs_volumes: list[EfsVolume] = [] + self.epfs_volumes: list[EpfsVolume] = [] self._fetch_block_volumes() self._fetch_buckets() + self._fetch_efs_volumes() + self._fetch_epfs_volumes() def _fetch_block_volumes(self): for location in self.provider.session.locations: @@ -60,6 +64,9 @@ def _fetch_buckets(self): item.get("is_encryption_enabled", False) ), is_lock_enabled=bool(item.get("is_lock_enabled", False)), + lifecycle_configuration_status=item.get( + "lifecycle_configuration_status", "" + ), ) ) except Exception as error: @@ -67,6 +74,68 @@ def _fetch_buckets(self): f"storage - Error fetching buckets in {location}: {error}" ) + def _fetch_efs_volumes(self): + for location in self.provider.session.locations: + try: + volumes = self.client.paginate("/efs/", location=location) + for item in volumes: + self.efs_volumes.append( + EfsVolume( + id=str(item.get("id", "")), + name=item.get("name", ""), + location=location, + status=item.get("status", ""), + vpc_id=str(item.get("vpc_id", "")), + is_backup_enabled=bool( + item.get("is_backup_enabled", False) + ), + is_all_vpc_resources_allowed=bool( + item.get("is_all_vpc_resources_allowed", False) + ), + ) + ) + except Exception as error: + logger.error( + f"storage - Error fetching EFS volumes in {location}: {error}" + ) + + def _fetch_epfs_volumes(self): + for location in self.provider.session.locations: + try: + all_items: list = [] + page = 1 + total_pages = 1 + while page <= total_pages: + payload = self.client.get( + "/epfs/", + location=location, + params={"page": page, "page_size": 100}, + ) + data = payload.get("data", []) + if isinstance(data, list): + all_items.extend(data) + total_pages = int(payload.get("total_page_number", page)) + if not data: + break + page += 1 + + for item in all_items: + vpc = item.get("vpc", {}) or {} + self.epfs_volumes.append( + EpfsVolume( + id=str(item.get("id", "")), + name=item.get("name", ""), + location=location, + vpc_network_id=str(vpc.get("network_id", "")), + vpc_name=vpc.get("name", ""), + deleted=bool(item.get("deleted", False)), + ) + ) + except Exception as error: + logger.error( + f"storage - Error fetching EPFS volumes in {location}: {error}" + ) + class BlockVolume(BaseModel): id: str @@ -94,6 +163,42 @@ class StorageBucket(BaseModel): is_public_access_enabled: bool = False is_encryption_enabled: bool = False is_lock_enabled: bool = False + lifecycle_configuration_status: str = "" + + @property + def resource_id(self) -> str: + return self.id + + @property + def resource_name(self) -> str: + return self.name + + +class EfsVolume(BaseModel): + id: str + name: str + location: str + status: str = "" + vpc_id: str = "" + is_backup_enabled: bool = False + is_all_vpc_resources_allowed: bool = False + + @property + def resource_id(self) -> str: + return self.id + + @property + def resource_name(self) -> str: + return self.name + + +class EpfsVolume(BaseModel): + id: str + name: str + location: str + vpc_network_id: str = "" + vpc_name: str = "" + deleted: bool = False @property def resource_id(self) -> str: From 6d27d2c6fb962f666a2db25fd8d31c98e1cab4a4 Mon Sep 17 00:00:00 2001 From: Deepak Dalvi Date: Sat, 20 Jun 2026 22:32:19 +0530 Subject: [PATCH 3/8] e2e addon test cases --- .../node_accidental_protection_enabled.py | 2 +- .../node_compliance_enabled.py | 2 +- .../node_encryption_enabled.py | 2 +- .../node_public_ip_not_assigned.py | 2 +- .../node_vpc_attached/node_vpc_attached.py | 2 +- .../e2e/services/database/__init__.py | 0 .../database_cluster_ssl_enabled_test.py | 43 +++++++++++ .../database/database_service_test.py | 70 ++++++++++++++++++ .../e2e/services/network/__init__.py | 0 .../services/network/network_service_test.py | 72 +++++++++++++++++++ .../network/network_vpc_is_active_test.py | 45 ++++++++++++ .../e2e/services/storage/__init__.py | 0 .../storage_efs_backup_enabled_test.py | 43 +++++++++++ .../services/storage/storage_service_test.py | 49 +++++++++++++ 14 files changed, 327 insertions(+), 5 deletions(-) create mode 100644 tests/providers/e2e/services/database/__init__.py create mode 100644 tests/providers/e2e/services/database/database_cluster_ssl_enabled_test.py create mode 100644 tests/providers/e2e/services/database/database_service_test.py create mode 100644 tests/providers/e2e/services/network/__init__.py create mode 100644 tests/providers/e2e/services/network/network_service_test.py create mode 100644 tests/providers/e2e/services/network/network_vpc_is_active_test.py create mode 100644 tests/providers/e2e/services/storage/__init__.py create mode 100644 tests/providers/e2e/services/storage/storage_efs_backup_enabled_test.py create mode 100644 tests/providers/e2e/services/storage/storage_service_test.py diff --git a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py index 14172c6db38..5fe43ccc757 100644 --- a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py +++ b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py @@ -9,7 +9,7 @@ def execute(self): report = CheckReportE2e(metadata=self.metadata(), resource=node) report.status = "PASS" report.status_extended = f"Node {node.name} has accidental protection enabled." - if getattr(node, "is_accidental_protection") != True: + if not node.is_accidental_protection: report.status = "FAIL" report.status_extended = f"Node {node.name} does not have accidental protection enabled." findings.append(report) diff --git a/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py index 2a06a3156e5..3c392c50838 100644 --- a/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py +++ b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py @@ -9,7 +9,7 @@ def execute(self): report = CheckReportE2e(metadata=self.metadata(), resource=node) report.status = "PASS" report.status_extended = f"Node {node.name} has compliance mode enabled." - if getattr(node, "is_node_compliance") != True: + if not node.is_node_compliance: report.status = "FAIL" report.status_extended = f"Node {node.name} does not have compliance mode enabled." findings.append(report) diff --git a/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py index 6fa0ce6c197..2be78e83aa8 100644 --- a/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py +++ b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py @@ -9,7 +9,7 @@ def execute(self): report = CheckReportE2e(metadata=self.metadata(), resource=node) report.status = "PASS" report.status_extended = f"Node {node.name} has encryption enabled." - if getattr(node, "is_encryption_enabled") != True: + if not node.is_encryption_enabled: report.status = "FAIL" report.status_extended = f"Node {node.name} does not have encryption enabled." findings.append(report) diff --git a/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py index d49b38f8b93..8921e5ce70a 100644 --- a/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py +++ b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py @@ -9,7 +9,7 @@ def execute(self): report = CheckReportE2e(metadata=self.metadata(), resource=node) report.status = "PASS" report.status_extended = f"Node {node.name} does not have a public IP." - if getattr(node, "has_public_ip") != False: + if node.has_public_ip: report.status = "FAIL" report.status_extended = f"Node {node.name} has a public IP assigned." findings.append(report) diff --git a/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py b/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py index 26746ec4d2a..c99f3387e6a 100644 --- a/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py +++ b/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py @@ -9,7 +9,7 @@ def execute(self): report = CheckReportE2e(metadata=self.metadata(), resource=node) report.status = "PASS" report.status_extended = f"Node {node.name} is attached to a VPC." - if getattr(node, "is_vpc_attached") != True: + if not node.is_vpc_attached: report.status = "FAIL" report.status_extended = f"Node {node.name} is not attached to a VPC." findings.append(report) diff --git a/tests/providers/e2e/services/database/__init__.py b/tests/providers/e2e/services/database/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/providers/e2e/services/database/database_cluster_ssl_enabled_test.py b/tests/providers/e2e/services/database/database_cluster_ssl_enabled_test.py new file mode 100644 index 00000000000..b26a5fe9a06 --- /dev/null +++ b/tests/providers/e2e/services/database/database_cluster_ssl_enabled_test.py @@ -0,0 +1,43 @@ +from unittest import mock + +from prowler.providers.e2e.services.database.database_service import DatabaseCluster +from tests.providers.e2e.e2e_fixtures import set_mocked_e2e_provider + + +class TestDatabaseClusterSslEnabledCheck: + def test_pass_and_fail(self): + database_client = mock.MagicMock() + database_client.clusters = [ + DatabaseCluster( + id="1", + name="secure-db", + location="Delhi", + master_ssl_enabled=True, + ), + DatabaseCluster( + id="2", + name="insecure-db", + location="Delhi", + master_ssl_enabled=False, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2e_provider(), + ), + mock.patch( + "prowler.providers.e2e.services.database.database_cluster_ssl_enabled.database_cluster_ssl_enabled.database_client", + new=database_client, + ), + ): + from prowler.providers.e2e.services.database.database_cluster_ssl_enabled.database_cluster_ssl_enabled import ( + database_cluster_ssl_enabled, + ) + + findings = database_cluster_ssl_enabled().execute() + + assert len(findings) == 2 + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" diff --git a/tests/providers/e2e/services/database/database_service_test.py b/tests/providers/e2e/services/database/database_service_test.py new file mode 100644 index 00000000000..da97802eff8 --- /dev/null +++ b/tests/providers/e2e/services/database/database_service_test.py @@ -0,0 +1,70 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.e2e.services.database.database_service import Database + + +class TestDatabaseService: + @patch( + "prowler.providers.e2e.services.database.database_service.E2eService.__init__" + ) + def test_fetch_clusters_enriches_detail(self, mock_super_init): + mock_super_init.return_value = None + + provider = MagicMock() + provider.session.locations = ["Delhi"] + service = Database.__new__(Database) + service.provider = provider + service.client = MagicMock() + service.clusters = [] + service.instances = [] + + service.client.get_data.side_effect = [ + [ + { + "id": 5276, + "name": "E2E-DBaaS-1", + "status": "RUNNING", + "software": {"name": "MySQL", "version": "8.0"}, + "master_node": { + "instance_id": 10650, + "node_name": "E2E-DBaaS-1-Node-1", + "public_ip_address": "164.52.1.1", + "ssl": True, + "database": {"username": "dbadmin"}, + }, + } + ], + { + "id": 5276, + "name": "E2E-DBaaS-1", + "status": "RUNNING", + "backup_enabled": True, + "whitelisted_ips": ["203.0.113.0/24"], + "master_node": { + "instance_id": 10650, + "node_name": "E2E-DBaaS-1-Node-1", + "public_ip_address": "164.52.1.1", + "ssl": True, + "database": {"username": "dbadmin"}, + }, + "slave_nodes": [ + { + "instance_id": 10651, + "node_name": "E2E-DBaaS-1-Replica-1", + "public_ip_address": None, + "database": {"username": "dbadmin"}, + } + ], + }, + ] + + service._fetch_clusters() + + assert len(service.clusters) == 1 + cluster = service.clusters[0] + assert cluster.backup_enabled is True + assert cluster.master_ssl_enabled is True + assert cluster.master_has_public_ip is True + assert cluster.master_username == "dbadmin" + assert len(service.instances) == 2 + assert service.instances[1].role == "replica" diff --git a/tests/providers/e2e/services/network/__init__.py b/tests/providers/e2e/services/network/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/providers/e2e/services/network/network_service_test.py b/tests/providers/e2e/services/network/network_service_test.py new file mode 100644 index 00000000000..7dfebb1a814 --- /dev/null +++ b/tests/providers/e2e/services/network/network_service_test.py @@ -0,0 +1,72 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.e2e.services.network.network_service import Network, Vpc + + +class TestNetworkService: + @patch("prowler.providers.e2e.services.network.network_service.E2eService.__init__") + def test_fetch_vpcs_and_tunnels(self, mock_super_init): + mock_super_init.return_value = None + + provider = MagicMock() + provider.session.locations = ["Delhi"] + service = Network.__new__(Network) + service.provider = provider + service.client = MagicMock() + service.vpcs = [] + service.reserved_ips = [] + service.vpc_tunnels = [] + + service.client.paginate.return_value = [ + { + "network_id": 100, + "name": "VPC-100", + "is_active": True, + "state": "Active", + "ipv4_cidr": "10.0.0.0/23", + "vm_count": 2, + "gateway_node": {"node_id": 1, "ip_address_public": "1.2.3.4"}, + } + ] + service.client.get_data.side_effect = [ + [ + { + "reserve_id": 10, + "ip_address": "164.52.1.1", + "status": "Attached", + "reserved_type": "FloatingIP", + "vm_id": 55, + "floating_ip_attached_nodes": [{"id": 1}], + } + ], + [ + { + "id": 5, + "name": "peer-tunnel", + "status": "ACTIVE", + "is_peer_vpc_external": True, + } + ], + ] + + service._fetch_vpcs() + service._fetch_reserved_ips() + service._fetch_vpc_tunnels() + + assert len(service.vpcs) == 1 + assert service.vpcs[0].vm_count == 2 + assert len(service.reserved_ips) == 1 + assert service.reserved_ips[0].floating_ip_attached_nodes_count == 1 + assert len(service.vpc_tunnels) == 1 + assert service.vpc_tunnels[0].is_peer_vpc_external is True + + +class TestVpcModel: + def test_resource_properties(self): + vpc = Vpc( + network_id="100", + name="VPC-100", + location="Delhi", + ) + assert vpc.resource_id == "100" + assert vpc.resource_name == "VPC-100" diff --git a/tests/providers/e2e/services/network/network_vpc_is_active_test.py b/tests/providers/e2e/services/network/network_vpc_is_active_test.py new file mode 100644 index 00000000000..2c1fc71d779 --- /dev/null +++ b/tests/providers/e2e/services/network/network_vpc_is_active_test.py @@ -0,0 +1,45 @@ +from unittest import mock + +from prowler.providers.e2e.services.network.network_service import Vpc +from tests.providers.e2e.e2e_fixtures import set_mocked_e2e_provider + + +class TestNetworkVpcIsActiveCheck: + def test_pass_and_fail(self): + network_client = mock.MagicMock() + network_client.vpcs = [ + Vpc( + network_id="1", + name="active-vpc", + location="Delhi", + is_active=True, + state="Active", + ), + Vpc( + network_id="2", + name="inactive-vpc", + location="Delhi", + is_active=False, + state="Inactive", + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2e_provider(), + ), + mock.patch( + "prowler.providers.e2e.services.network.network_vpc_is_active.network_vpc_is_active.network_client", + new=network_client, + ), + ): + from prowler.providers.e2e.services.network.network_vpc_is_active.network_vpc_is_active import ( + network_vpc_is_active, + ) + + findings = network_vpc_is_active().execute() + + assert len(findings) == 2 + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" diff --git a/tests/providers/e2e/services/storage/__init__.py b/tests/providers/e2e/services/storage/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/providers/e2e/services/storage/storage_efs_backup_enabled_test.py b/tests/providers/e2e/services/storage/storage_efs_backup_enabled_test.py new file mode 100644 index 00000000000..4fd18a04c0b --- /dev/null +++ b/tests/providers/e2e/services/storage/storage_efs_backup_enabled_test.py @@ -0,0 +1,43 @@ +from unittest import mock + +from prowler.providers.e2e.services.storage.storage_service import EfsVolume +from tests.providers.e2e.e2e_fixtures import set_mocked_e2e_provider + + +class TestStorageEfsBackupEnabledCheck: + def test_pass_and_fail(self): + storage_client = mock.MagicMock() + storage_client.efs_volumes = [ + EfsVolume( + id="1", + name="efs-ok", + location="Delhi", + is_backup_enabled=True, + ), + EfsVolume( + id="2", + name="efs-bad", + location="Delhi", + is_backup_enabled=False, + ), + ] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2e_provider(), + ), + mock.patch( + "prowler.providers.e2e.services.storage.storage_efs_backup_enabled.storage_efs_backup_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.e2e.services.storage.storage_efs_backup_enabled.storage_efs_backup_enabled import ( + storage_efs_backup_enabled, + ) + + findings = storage_efs_backup_enabled().execute() + + assert len(findings) == 2 + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" diff --git a/tests/providers/e2e/services/storage/storage_service_test.py b/tests/providers/e2e/services/storage/storage_service_test.py new file mode 100644 index 00000000000..37529dc6e7e --- /dev/null +++ b/tests/providers/e2e/services/storage/storage_service_test.py @@ -0,0 +1,49 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.e2e.services.storage.storage_service import Storage + + +class TestStorageService: + @patch("prowler.providers.e2e.services.storage.storage_service.E2eService.__init__") + def test_fetch_efs_and_epfs(self, mock_super_init): + mock_super_init.return_value = None + + provider = MagicMock() + provider.session.locations = ["Delhi"] + service = Storage.__new__(Storage) + service.provider = provider + service.client = MagicMock() + service.block_volumes = [] + service.buckets = [] + service.efs_volumes = [] + service.epfs_volumes = [] + + service.client.paginate.return_value = [ + { + "id": 1396, + "name": "sfs-993", + "status": "Available", + "vpc_id": 6882, + "is_backup_enabled": True, + "is_all_vpc_resources_allowed": False, + } + ] + service.client.get.return_value = { + "data": [ + { + "id": 145, + "name": "epfs-1", + "deleted": False, + "vpc": {"network_id": 34872, "name": "VPC-717"}, + } + ], + "total_page_number": 1, + } + + service._fetch_efs_volumes() + service._fetch_epfs_volumes() + + assert len(service.efs_volumes) == 1 + assert service.efs_volumes[0].is_backup_enabled is True + assert len(service.epfs_volumes) == 1 + assert service.epfs_volumes[0].vpc_network_id == "34872" From 4ea7ed488f2579752ed1c6c75e7eb8f9a707be9c Mon Sep 17 00:00:00 2001 From: Deepak Dalvi Date: Sat, 20 Jun 2026 22:35:13 +0530 Subject: [PATCH 4/8] e2e fix code issues --- prowler/lib/check/models.py | 6 + prowler/providers/e2e/docs/schema.md | 368 ++++++++++++++++++ prowler/providers/e2e/e2e_provider.py | 42 +- prowler/providers/e2e/lib/api/client.py | 2 +- .../providers/e2e/lib/mutelist/mutelist.py | 12 + prowler/providers/e2e/lib/service/service.py | 9 +- prowler/providers/e2e/models.py | 16 +- ...b_https_uses_ssl_certificate.metadata.json | 6 +- ...balancer_alb_https_uses_ssl_certificate.py | 4 +- ...backend_health_check_enabled.metadata.json | 6 +- ...adbalancer_backend_health_check_enabled.py | 6 +- ...oadbalancer_bitninja_enabled.metadata.json | 6 +- .../loadbalancer_bitninja_enabled.py | 4 +- .../loadbalancer/loadbalancer_service.py | 14 +- ...ccidental_protection_enabled.metadata.json | 4 +- .../node_compliance_enabled.metadata.json | 4 +- .../node_encryption_enabled.metadata.json | 4 +- .../node_public_ip_not_assigned.metadata.json | 4 +- .../node_rescue_mode_disabled.metadata.json | 4 +- .../node_vpc_attached.metadata.json | 4 +- ...itygroup_no_all_traffic_rule.metadata.json | 6 +- .../securitygroup_no_all_traffic_rule.py | 4 +- ...oup_no_inbound_any_all_ports.metadata.json | 6 +- .../securitygroup_no_inbound_any_all_ports.py | 13 +- ...itygroup_restrictive_default.metadata.json | 11 +- .../securitygroup_restrictive_default.py | 10 +- .../securitygroup/securitygroup_service.py | 6 +- ...ge_block_volume_not_orphaned.metadata.json | 6 +- .../storage_block_volume_not_orphaned.py | 4 +- .../storage_bucket_encryption_enabled.py | 4 +- ...ucket_public_access_disabled.metadata.json | 6 +- .../storage_bucket_public_access_disabled.py | 4 +- ...ge_bucket_versioning_enabled.metadata.json | 6 +- .../storage_bucket_versioning_enabled.py | 4 +- .../e2e/services/storage/storage_service.py | 6 +- .../e2e/lib/arguments/arguments_test.py | 11 + .../e2e/services/node/nodes_service_test.py | 21 + 37 files changed, 585 insertions(+), 68 deletions(-) create mode 100644 prowler/providers/e2e/docs/schema.md diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index e4507e61ca6..a14fcae44a6 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -1264,6 +1264,12 @@ class CheckReportE2e(Check_Report): location: str def __init__(self, metadata: Dict, resource: Any) -> None: + """Initialize the E2E Cloud Check's finding information. + + Args: + metadata: The metadata of the check. + resource: Basic information about the E2E Cloud resource. + """ super().__init__(metadata, resource) self.resource_name = getattr( resource, "name", getattr(resource, "resource_name", "") diff --git a/prowler/providers/e2e/docs/schema.md b/prowler/providers/e2e/docs/schema.md new file mode 100644 index 00000000000..7d176821cc7 --- /dev/null +++ b/prowler/providers/e2e/docs/schema.md @@ -0,0 +1,368 @@ +## E2E Schema + +Representation of resources in [E2E Cloud MyAccount](https://docs.e2enetworks.com/api/myaccount/openapi.yaml). This schema mirrors the [Cartography module format](https://github.com/cartography-cncf/cartography/tree/master/docs/root/modules) for graph-based security analysis and Attack Paths query authoring. + +### E2eProject + +Representation of an E2E Cloud project (tenant scope for all MyAccount resources). + +> **Ontology Mapping**: This node has the extra label `Tenant` to enable cross-platform queries for organizational tenants across different systems (e.g., AWSAccount, GCPProject, AzureSubscription). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | E2E project ID (`project_id` query parameter) | +| locations | List of deployment regions (e.g. `Delhi`, `Chennai`) | + +#### Relationships + +- All E2E resources belong to an `E2eProject`. + + ```cypher + (:E2eProject)-[:RESOURCE]->(:E2eNode, + :E2eVpc, + :E2eVpcTunnel, + :E2eReservedIp, + :E2eSecurityGroup, + :E2eLoadBalancer, + :E2eBlockVolume, + :E2eStorageBucket, + :E2eEfs, + :E2eEpfs, + :E2eDatabaseCluster, + :E2eDatabaseInstance) + ``` + +### E2eNode + +Representation of an E2E Cloud compute node ([Nodes API](https://docs.e2enetworks.com/api/myaccount/compute/nodes/)). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | Node ID | +| name | Node display name | +| vm_id | Virtual machine identifier used by network and security group APIs | +| status | Node lifecycle status (e.g. `Running`) | +| location | Deployment region (`Delhi`, `Chennai`) | +| public_ip_address | Public IP address if assigned | +| private_ip_address | Private IP address | +| is_vpc_attached | Whether the node is attached to a VPC | +| is_encryption_enabled | Whether encryption at rest is enabled | +| is_accidental_protection | Whether accidental deletion protection is enabled | +| is_node_compliance | Whether compliance monitoring is enabled | +| rescue_mode_status | Rescue mode state (`Enabled` / `Disabled`) | + +#### Relationships + +- A compute node may be a member of a VPC. + + ```cypher + (:E2eNode)-[:MEMBER_OF_VPC]->(:E2eVpc) + ``` + +- A compute node uses one or more security groups. + + ```cypher + (:E2eNode)-[:USES]->(:E2eSecurityGroup) + ``` + +- Block volumes attach to compute nodes. + + ```cypher + (:E2eBlockVolume)-[:ATTACHED_TO]->(:E2eNode) + ``` + +- Reserved or floating IPs attach to compute nodes. + + ```cypher + (:E2eReservedIp)-[:ATTACHED_TO]->(:E2eNode) + ``` + +### E2eVpc + +Representation of an E2E Cloud VPC ([VPC API](https://docs.e2enetworks.com/api/myaccount/network/vpc/)). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | VPC network ID (`network_id`) | +| name | VPC name | +| location | Deployment region | +| ipv4_cidr | IPv4 CIDR block | +| is_active | Whether the VPC is active | +| state | VPC state (e.g. `Active`) | +| vm_count | Number of nodes attached to the VPC | +| gateway_node_id | Gateway node ID for the VPC | +| gateway_public_ip | Public IP of the VPC gateway node | + +#### Relationships + +- A VPC contains VPC peering tunnels. + + ```cypher + (:E2eVpc)-[:CONTAINS]->(:E2eVpcTunnel) + ``` + +- A VPC gateway is backed by a compute node. + + ```cypher + (:E2eVpc)-[:GATEWAY_NODE]->(:E2eNode) + ``` + +- EFS and EPFS volumes attach to VPCs. + + ```cypher + (:E2eEfs)-[:ATTACHED_TO_VPC]->(:E2eVpc) + (:E2eEpfs)-[:ATTACHED_TO_VPC]->(:E2eVpc) + ``` + +### E2eVpcTunnel + +Representation of an E2E Cloud VPC peering tunnel ([VPC Tunnels API](https://docs.e2enetworks.com/api/myaccount/network/vpc/)). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | Tunnel ID | +| name | Tunnel name | +| location | Deployment region | +| local_vpc_network_id | Local VPC network ID | +| status | Tunnel status (e.g. `CREATING`, `ACTIVE`) | +| is_peer_vpc_external | Whether the peer VPC is external to the account | + +#### Relationships + +- A VPC tunnel peers two VPCs. + + ```cypher + (:E2eVpcTunnel)-[:PEERS_WITH]->(:E2eVpc) + (:E2eVpc)-[:PEERS_WITH]->(:E2eVpc) + ``` + +### E2eReservedIp + +Representation of a reserved, public, addon, or floating IP ([Reserve IP API](https://docs.e2enetworks.com/api/myaccount/network/reserve-ip/)). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | Reserve IP ID | +| ip_address | IP address | +| location | Deployment region | +| status | Attachment status (e.g. `Attached`) | +| reserved_type | IP type (`FloatingIP`, `PublicIP`, `AddonIP`) | +| vm_id | Attached VM ID if applicable | + +#### Relationships + +- Reserved IPs attach to compute nodes. + + ```cypher + (:E2eReservedIp)-[:ATTACHED_TO]->(:E2eNode) + ``` + +### E2eSecurityGroup + +Representation of an E2E Cloud security group ([Security Group API](https://docs.e2enetworks.com/api/myaccount/network/security-group/)). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | Security group ID | +| name | Security group name | +| location | Deployment region | +| is_default | Whether this is the default security group | +| is_all_traffic_rule | Whether an allow-all-traffic rule is present | +| description | Security group description | + +#### Relationships + +- Security groups attach to compute nodes. + + ```cypher + (:E2eSecurityGroup)-[:ATTACHED_TO]->(:E2eNode) + ``` + +- Security groups may attach to load balancers. + + ```cypher + (:E2eSecurityGroup)-[:ATTACHED_TO]->(:E2eLoadBalancer) + ``` + +### E2eLoadBalancer + +Representation of an E2E Cloud load balancer appliance ([Appliances API](https://docs.e2enetworks.com/api/myaccount/compute/load-balancer/)). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | Load balancer appliance ID | +| name | Load balancer name | +| location | Deployment region | +| status | Appliance status | +| lb_mode | Load balancer mode (e.g. ALB HTTPS) | +| ssl_certificate_id | SSL certificate ID for HTTPS listeners | +| bitninja_enabled | Whether BitNinja protection is enabled | +| public_ip | Public IP address | + +#### Relationships + +- Load balancers route traffic to backend nodes. + + ```cypher + (:E2eLoadBalancer)-[:BACKEND]->(:E2eNode) + ``` + +### E2eBlockVolume + +Representation of an E2E Cloud block storage volume ([Block Storage API](https://docs.e2enetworks.com/api/myaccount/storage/block-storage/)). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | Block volume ID (`block_id`) | +| name | Volume name | +| location | Deployment region | +| status | Volume status (e.g. `Available`) | +| size_string | Human-readable volume size | +| is_attached | Whether the volume is attached to a node | + +#### Relationships + +- Block volumes attach to compute nodes. + + ```cypher + (:E2eBlockVolume)-[:ATTACHED_TO]->(:E2eNode) + ``` + +### E2eStorageBucket + +Representation of an E2E Cloud object storage bucket ([Object Storage API](https://docs.e2enetworks.com/api/myaccount/storage/object-storage/)). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | Bucket ID | +| name | Bucket name | +| location | Deployment region | +| status | Bucket status | +| versioning_status | Versioning state (`Off`, `Enabled`, `Suspended`) | +| is_public_access_enabled | Whether public access is enabled | +| is_encryption_enabled | Whether encryption is enabled | +| is_lock_enabled | Whether object lock is enabled | +| lifecycle_configuration_status | Lifecycle policy status | + +#### Relationships + +- Object storage buckets belong to a project. + + ```cypher + (:E2eProject)-[:RESOURCE]->(:E2eStorageBucket) + ``` + +### E2eEfs + +Representation of an E2E Cloud shared file system (SFS / EFS) ([SFS API](https://docs.e2enetworks.com/api/myaccount/storage/parallel-file-storage/)). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | SFS volume ID | +| name | Volume name | +| location | Deployment region | +| status | Volume status | +| vpc_id | Attached VPC ID | +| is_backup_enabled | Whether backup is enabled | +| is_all_vpc_resources_allowed | Whether all VPC resources can access the volume | + +#### Relationships + +- EFS volumes attach to VPCs. + + ```cypher + (:E2eEfs)-[:ATTACHED_TO_VPC]->(:E2eVpc) + ``` + +### E2eEpfs + +Representation of an E2E Cloud elastic parallel file system (EPFS). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | EPFS volume ID | +| name | Volume name | +| location | Deployment region | +| vpc_network_id | Attached VPC network ID | +| deleted | Whether the volume is marked deleted | + +#### Relationships + +- EPFS volumes attach to VPCs. + + ```cypher + (:E2eEpfs)-[:ATTACHED_TO_VPC]->(:E2eVpc) + ``` + +### E2eDatabaseCluster + +Representation of an E2E Cloud DBaaS cluster ([RDS API](https://docs.e2enetworks.com/api/myaccount/database/rds/)). + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | Cluster ID | +| name | Cluster name | +| location | Deployment region | +| status | Cluster status (e.g. `RUNNING`) | +| software_name | Database engine name (e.g. `MySQL`, `PostgreSQL`) | +| software_version | Database engine version | +| backup_enabled | Whether automated backup is enabled | +| master_ssl_enabled | Whether SSL is enabled on the master node | +| master_has_public_ip | Whether the master node has a public IP | + +#### Relationships + +- A database cluster has one or more database instances. + + ```cypher + (:E2eDatabaseCluster)-[:HAS_INSTANCE]->(:E2eDatabaseInstance) + ``` + +### E2eDatabaseInstance + +Representation of a master or replica instance within an E2E Cloud DBaaS cluster. + +| Field | Description | +|-------|-------------| +| firstseen | Timestamp of when a sync job discovered this node | +| lastupdated | Timestamp of the last time the node was updated | +| **id** | Instance ID | +| name | Instance node name | +| cluster_id | Parent cluster ID | +| location | Deployment region | +| role | Instance role (`master` or `replica`) | +| public_ip_address | Public IP address if assigned | +| ssl_enabled | Whether SSL is enabled | +| username | Database admin username | + +#### Relationships + +- Database instances belong to a cluster. + + ```cypher + (:E2eDatabaseCluster)-[:HAS_INSTANCE]->(:E2eDatabaseInstance) + ``` diff --git a/prowler/providers/e2e/e2e_provider.py b/prowler/providers/e2e/e2e_provider.py index a70676bc6d9..be33875db13 100644 --- a/prowler/providers/e2e/e2e_provider.py +++ b/prowler/providers/e2e/e2e_provider.py @@ -64,7 +64,7 @@ def __init__( config_path = default_config_file_path self._audit_config = load_and_validate_config_file(self._type, config_path) - if mutelist_content: + if mutelist_content is not None: self._mutelist = E2eMutelist(mutelist_content=mutelist_content) else: if not mutelist_path: @@ -86,6 +86,14 @@ def __init__( @staticmethod def _resolve_locations(locations: list[str] | None) -> list[str]: + """Resolve scan locations from CLI args, env vars, or defaults. + + Args: + locations: Optional list of location names from CLI arguments. + + Returns: + The resolved list of E2E Cloud locations to scan. + """ if locations: return locations @@ -126,6 +134,20 @@ def setup_session( project_id: int, locations: list[str], ) -> E2eSession: + """Create an authenticated E2E Cloud API session. + + Args: + api_key: E2E Cloud API key. + auth_token: Bearer auth token for the MyAccount API. + project_id: E2E Cloud project identifier. + locations: Locations included in the session scope. + + Returns: + A configured E2eSession with an HTTP client. + + Raises: + E2eSessionError: If session initialization fails. + """ try: http_session = requests.Session() http_session.headers.update( @@ -148,6 +170,7 @@ def setup_session( ) from error def print_credentials(self) -> None: + """Print masked E2E Cloud credentials and scan scope to stdout.""" masked_key = ( f"{self._api_key[:4]}...{self._api_key[-4:]}" if self._api_key and len(self._api_key) > 8 @@ -171,6 +194,23 @@ def test_connection( locations: list[str] | None = None, raise_on_exception: bool = True, ) -> Connection: + """Test connectivity to the E2E Cloud MyAccount API. + + Args: + api_key: E2E Cloud API key. Falls back to E2E_API_KEY. + auth_token: Bearer auth token. Falls back to E2E_AUTH_TOKEN. + project_id: Project identifier. Falls back to E2E_PROJECT_ID. + locations: Optional locations to use for the probe request. + raise_on_exception: Whether to re-raise caught exceptions. + + Returns: + Connection indicating success or containing the error. + + Raises: + E2eCredentialsError: If required credentials are missing. + E2eSessionError: If the API returns a non-200 response. + Exception: Any unexpected error when raise_on_exception is True. + """ try: api_key = api_key or os.getenv("E2E_API_KEY") auth_token = auth_token or os.getenv("E2E_AUTH_TOKEN") diff --git a/prowler/providers/e2e/lib/api/client.py b/prowler/providers/e2e/lib/api/client.py index fe55a0edd64..12f274aebb5 100644 --- a/prowler/providers/e2e/lib/api/client.py +++ b/prowler/providers/e2e/lib/api/client.py @@ -87,7 +87,7 @@ def paginate( if isinstance(data, list): all_items.extend(data) elif isinstance(data, dict): - return data + all_items.extend(data.values()) total_pages = int(payload.get("total_page_number", page_no)) if not data: diff --git a/prowler/providers/e2e/lib/mutelist/mutelist.py b/prowler/providers/e2e/lib/mutelist/mutelist.py index 11a12a152fe..3f53b40e1af 100644 --- a/prowler/providers/e2e/lib/mutelist/mutelist.py +++ b/prowler/providers/e2e/lib/mutelist/mutelist.py @@ -1,10 +1,22 @@ +"""E2E Cloud mutelist support for suppressing findings.""" + from prowler.lib.check.models import CheckReportE2e from prowler.lib.mutelist.mutelist import Mutelist from prowler.lib.outputs.utils import unroll_dict, unroll_tags class E2eMutelist(Mutelist): + """Mutelist implementation for E2E Cloud check findings.""" + def is_finding_muted(self, finding: CheckReportE2e) -> bool: + """Determine whether an E2E Cloud finding is muted. + + Args: + finding: The E2E Cloud check report to evaluate. + + Returns: + True if the finding matches a mutelist entry, otherwise False. + """ return self.is_muted( finding.resource_id, finding.check_metadata.CheckID, diff --git a/prowler/providers/e2e/lib/service/service.py b/prowler/providers/e2e/lib/service/service.py index 0f196f258ab..1b7e792cb3c 100644 --- a/prowler/providers/e2e/lib/service/service.py +++ b/prowler/providers/e2e/lib/service/service.py @@ -1,10 +1,17 @@ +from prowler.providers.e2e.e2e_provider import E2eProvider from prowler.providers.e2e.lib.api.client import E2eAPIClient class E2eService: """Base class for E2E Cloud services.""" - def __init__(self, service: str, provider): + def __init__(self, service: str, provider: E2eProvider): + """Initialize an E2E Cloud service client. + + Args: + service: Service name used for logging and configuration lookup. + provider: The active E2E Cloud provider instance. + """ self.provider = provider self.audit_config = provider.audit_config self.fixer_config = provider.fixer_config diff --git a/prowler/providers/e2e/models.py b/prowler/providers/e2e/models.py index 8c6eeacc5a4..27f9f38fe30 100644 --- a/prowler/providers/e2e/models.py +++ b/prowler/providers/e2e/models.py @@ -1,4 +1,4 @@ -from typing import Any, Optional +from typing import Any from pydantic import BaseModel, Field @@ -30,7 +30,19 @@ class E2eIdentityInfo(BaseModel): class E2eOutputOptions(ProviderOutputOptions): """Customize output filenames for E2E Cloud scans.""" - def __init__(self, arguments, bulk_checks_metadata, identity: E2eIdentityInfo): + def __init__( + self, + arguments: object, + bulk_checks_metadata: dict, + identity: E2eIdentityInfo, + ) -> None: + """Initialize E2E Cloud output options. + + Args: + arguments: Parsed CLI arguments for the scan. + bulk_checks_metadata: Loaded metadata for all checks in the scan. + identity: E2E Cloud identity information used in output filenames. + """ super().__init__(arguments, bulk_checks_metadata) if ( not hasattr(arguments, "output_filename") diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json index 80c6a7bc582..1ec896e3860 100644 --- a/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "LoadBalancer", "ResourceGroup": "network", "Description": "Check if E2E Cloud application load balancers serving HTTPS traffic have an SSL certificate configured.", - "Risk": "", + "Risk": "HTTPS load balancers without a valid SSL certificate expose traffic to interception and downgrade attacks, weakening confidentiality and integrity for client connections.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/appliances//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"ssl_context\":{\"ssl_certificate_id\":}}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + "https://docs.e2enetworks.com/api/myaccount/compute/load-balancer/" ] } diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py index 8e3dcc27947..294fa4f021f 100644 --- a/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py @@ -5,7 +5,9 @@ class loadbalancer_alb_https_uses_ssl_certificate(Check): - def execute(self): + """Ensure HTTPS load balancers have an SSL certificate configured.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for lb in loadbalancer_client.load_balancers: if not lb.is_alb_https: diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json index 66cc8208ebf..3e651ce9bc9 100644 --- a/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "LoadBalancer", "ResourceGroup": "network", "Description": "Check if E2E Cloud application load balancers have HTTP health checks configured for backends.", - "Risk": "", + "Risk": "Load balancers without backend health checks may route traffic to failed or unhealthy nodes, causing outages and masking active compromise on degraded backends.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/appliances//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"backends\":[{\"http_check\":true}]}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + "https://docs.e2enetworks.com/api/myaccount/compute/load-balancer/" ] } diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py index 39872416d5d..b1bb97af034 100644 --- a/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py @@ -5,10 +5,12 @@ class loadbalancer_backend_health_check_enabled(Check): - def execute(self): + """Ensure ALB load balancers have backend health checks configured.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for lb in loadbalancer_client.load_balancers: - if not lb.is_alb_https: + if not lb.is_alb: continue report = CheckReportE2e(metadata=self.metadata(), resource=lb) diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json index 9b3da2db511..c73f10caacb 100644 --- a/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "LoadBalancer", "ResourceGroup": "network", "Description": "Check if E2E Cloud load balancers have BitNinja protection enabled.", - "Risk": "", + "Risk": "Load balancers without BitNinja protection have reduced web application firewall coverage, increasing exposure to automated attacks and malicious traffic against public endpoints.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/appliances//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"enable_bitninja\":true}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + "https://docs.e2enetworks.com/api/myaccount/compute/load-balancer/" ] } diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py index a49f9886659..bd83fca7dda 100644 --- a/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py @@ -5,7 +5,9 @@ class loadbalancer_bitninja_enabled(Check): - def execute(self): + """Ensure load balancers have BitNinja protection enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for lb in loadbalancer_client.load_balancers: report = CheckReportE2e(metadata=self.metadata(), resource=lb) diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_service.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_service.py index 28662d753a3..18bae01b30a 100644 --- a/prowler/providers/e2e/services/loadbalancer/loadbalancer_service.py +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_service.py @@ -9,7 +9,7 @@ class LoadBalancers(E2eService): def __init__(self, provider): super().__init__("loadbalancer", provider) - self.loadbalancers: list[LoadBalancer] = [] + self.load_balancers: list[LoadBalancer] = [] self._fetch_loadbalancers() def _fetch_loadbalancers(self): @@ -22,7 +22,7 @@ def _fetch_loadbalancers(self): for item in appliances: context = self._extract_context(item) node_detail = item.get("node_detail", {}) or {} - self.loadbalancers.append( + self.load_balancers.append( LoadBalancer( id=str(item.get("id", "")), name=item.get("name", ""), @@ -38,7 +38,8 @@ def _fetch_loadbalancers(self): ) except Exception as error: logger.error( - f"loadbalancer - Error fetching appliances in {location}: {error}" + f"loadbalancer - Error fetching appliances in {location} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) @staticmethod @@ -78,10 +79,15 @@ def resource_name(self) -> str: return self.name @property - def is_alb_https(self) -> bool: + def is_alb(self) -> bool: mode = self.lb_mode.upper() return mode in ("HTTP", "HTTPS", "BOTH") + @property + def is_alb_https(self) -> bool: + mode = self.lb_mode.upper() + return mode in ("HTTPS", "BOTH") + @property def has_backend_health_check(self) -> bool: for backend in self.backends: diff --git a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json index 47a36a67ef9..0534fbbe52b 100644 --- a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json +++ b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "compute", "Description": "Check if E2E Cloud nodes have accidental protection enabled", - "Risk": "", + "Risk": "Nodes without accidental protection are easier to delete or modify unintentionally, increasing the risk of service disruption and unplanned exposure of compute resources.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/nodes//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"is_accidental_protection\":true}'", "NativeIaC": "", "Other": "", "Terraform": "" diff --git a/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json index 3f2962ef517..03faa11710e 100644 --- a/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json +++ b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "compute", "Description": "Check if E2E Cloud nodes have compliance mode enabled", - "Risk": "", + "Risk": "Nodes without compliance mode enabled may not enforce required security controls, increasing misconfiguration risk and weakening auditability of compute workloads.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/nodes//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"is_node_compliance\":true}'", "NativeIaC": "", "Other": "", "Terraform": "" diff --git a/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json index e2e73a8aa76..6ccfad5011a 100644 --- a/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json +++ b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "compute", "Description": "Check if E2E Cloud nodes have encryption enabled", - "Risk": "", + "Risk": "Unencrypted nodes increase the risk of data exposure if disks or snapshots are accessed outside normal operating controls.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/nodes//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"isEncryptionEnabled\":true}'", "NativeIaC": "", "Other": "", "Terraform": "" diff --git a/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json index 671d611e66f..33bc7cad015 100644 --- a/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json +++ b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "compute", "Description": "Check if E2E Cloud nodes do not have a public IP assigned", - "Risk": "", + "Risk": "Nodes with public IP addresses are directly reachable from the internet, expanding the attack surface and increasing exposure to unauthorized access attempts.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/nodes//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"public_ip_address\":null}'", "NativeIaC": "", "Other": "", "Terraform": "" diff --git a/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json index b9604c1ed3e..c60471fcd39 100644 --- a/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json +++ b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "compute", "Description": "Check if E2E Cloud nodes do not have rescue mode enabled", - "Risk": "", + "Risk": "Rescue mode provides elevated access to node filesystems and can bypass normal operating controls, increasing the risk of unauthorized data access or persistence.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X POST -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/nodes//disable-rescue/?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\"", "NativeIaC": "", "Other": "", "Terraform": "" diff --git a/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.metadata.json b/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.metadata.json index cfca9226735..01d2c3c020f 100644 --- a/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.metadata.json +++ b/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "compute", "Description": "Check if E2E Cloud nodes are attached to a VPC", - "Risk": "", + "Risk": "Nodes not attached to a VPC may lack network isolation controls, making traffic routing and segmentation harder to enforce.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/nodes//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"vpc_id\":}'", "NativeIaC": "", "Other": "", "Terraform": "" diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json b/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json index 6aebc2438bb..e403c614119 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "SecurityGroup", "ResourceGroup": "network", "Description": "Check if E2E Cloud security groups do not have an all-traffic rule enabled.", - "Risk": "", + "Risk": "Security groups that allow all traffic permit unrestricted network access, increasing exposure to lateral movement and data exfiltration.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/security_group//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"is_all_traffic_rule\":false}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + "https://docs.e2enetworks.com/api/myaccount/network/security-group/" ] } diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py b/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py index 97b524a0551..b591e480aae 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py @@ -5,7 +5,9 @@ class securitygroup_no_all_traffic_rule(Check): - def execute(self): + """Ensure security groups do not allow all traffic.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for group in securitygroup_client.security_groups: report = CheckReportE2e(metadata=self.metadata(), resource=group) diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json index f57030c0d20..dae8e94a839 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "SecurityGroup", "ResourceGroup": "network", "Description": "Check if E2E Cloud security groups do not allow inbound all-protocol traffic from any source.", - "Risk": "", + "Risk": "Inbound all-protocol rules from any source expose services to the entire internet and increase the likelihood of unauthorized access.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X DELETE -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" \"https://api.e2enetworks.com/myaccount/api/v1/security_group//rules//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\"", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + "https://docs.e2enetworks.com/api/myaccount/network/security-group/" ] } diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py index a842807d834..f9d6d578c78 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py @@ -4,12 +4,15 @@ ) +def _is_open_network(value: str) -> bool: + normalized = value.lower().strip() + return normalized in ("any", "0.0.0.0/0", "::/0") + + def _is_permissive_inbound(rule) -> bool: - return ( - rule.rule_type.lower() == "inbound" - and rule.protocol_name.lower() == "all" - and rule.network.lower() == "any" - ) + if rule.rule_type.lower() != "inbound" or rule.protocol_name.lower() != "all": + return False + return _is_open_network(rule.network) or _is_open_network(rule.network_cidr) class securitygroup_no_inbound_any_all_ports(Check): diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json index 8e3c715b325..3f59e0fea72 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "SecurityGroup", "ResourceGroup": "network", "Description": "Check if E2E Cloud nodes do not use only default security groups with overly permissive inbound rules.", - "Risk": "", + "Risk": "Nodes that rely only on permissive default security groups inherit broad inbound access, weakening network segmentation and trust boundaries.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X POST -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/security_group//attach/?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"vm_id\":}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -24,11 +24,14 @@ "Url": "https://hub.prowler.com/check/securitygroup_restrictive_default" } }, - "Categories": [], + "Categories": [ + "trust-boundaries", + "internet-exposed" + ], "DependsOn": [], "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + "https://docs.e2enetworks.com/api/myaccount/network/security-group/" ] } diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py index d3047e31885..0ba9e1f1a46 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py @@ -4,12 +4,20 @@ ) +def _is_open_network(value: str) -> bool: + normalized = value.lower().strip() + return normalized in ("any", "0.0.0.0/0", "::/0") + + def _has_permissive_inbound(rules) -> bool: for rule in rules: if ( rule.rule_type.lower() == "inbound" and rule.protocol_name.lower() == "all" - and rule.network.lower() == "any" + and ( + _is_open_network(rule.network) + or _is_open_network(rule.network_cidr) + ) ): return True return False diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_service.py b/prowler/providers/e2e/services/securitygroup/securitygroup_service.py index 70cbfa1bc20..0d94e7418de 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_service.py +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_service.py @@ -48,7 +48,8 @@ def _fetch_security_groups(self): ) except Exception as error: logger.error( - f"securitygroup - Error fetching groups in {location}: {error}" + f"securitygroup - Error fetching groups in {location} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) def _fetch_node_security_groups(self): @@ -94,7 +95,8 @@ def _fetch_node_security_groups(self): ) except Exception as error: logger.error( - f"securitygroup - Error fetching attached groups for node {node.id}: {error}" + f"securitygroup - Error fetching attached groups for node {node.id} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) diff --git a/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json index a403691146e..377554a999e 100644 --- a/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json +++ b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "BlockVolume", "ResourceGroup": "storage", "Description": "Check if E2E Cloud block volumes in Available state are attached to a node.", - "Risk": "", + "Risk": "Orphaned block volumes remain provisioned without an attached workload, increasing storage cost and the risk of retaining sensitive data without active ownership.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X POST -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/block_storage//attach/?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"vm_id\":}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + "https://docs.e2enetworks.com/api/myaccount/storage/block-storage/" ] } diff --git a/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py index 42dd72f2838..40319442d21 100644 --- a/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py +++ b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py @@ -3,7 +3,9 @@ class storage_block_volume_not_orphaned(Check): - def execute(self): + """Ensure available block volumes are attached to a node.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for volume in storage_client.block_volumes: report = CheckReportE2e(metadata=self.metadata(), resource=volume) diff --git a/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py index 8db56b16285..399e13146c0 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py +++ b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py @@ -3,7 +3,9 @@ class storage_bucket_encryption_enabled(Check): - def execute(self): + """Ensure object storage buckets have encryption enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for bucket in storage_client.buckets: report = CheckReportE2e(metadata=self.metadata(), resource=bucket) diff --git a/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.metadata.json b/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.metadata.json index a9a21bbd155..cad84b17b74 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.metadata.json +++ b/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "ObjectStorageBucket", "ResourceGroup": "storage", "Description": "Check if E2E Cloud object storage buckets have public access disabled.", - "Risk": "", + "Risk": "Public object storage buckets can expose sensitive data to anonymous internet access, weakening confidentiality and increasing data breach risk.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/storage/buckets//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"is_public_access_enabled\":false}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + "https://docs.e2enetworks.com/api/myaccount/storage/object-storage/" ] } diff --git a/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.py b/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.py index f1177a53ca1..1deba542b18 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.py +++ b/prowler/providers/e2e/services/storage/storage_bucket_public_access_disabled/storage_bucket_public_access_disabled.py @@ -3,7 +3,9 @@ class storage_bucket_public_access_disabled(Check): - def execute(self): + """Ensure object storage buckets do not allow public access.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for bucket in storage_client.buckets: report = CheckReportE2e(metadata=self.metadata(), resource=bucket) diff --git a/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.metadata.json b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.metadata.json index a1732540905..6717e22a154 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.metadata.json +++ b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "ObjectStorageBucket", "ResourceGroup": "storage", "Description": "Check if E2E Cloud object storage buckets have versioning enabled.", - "Risk": "", + "Risk": "Buckets without versioning cannot recover from accidental deletion or overwrite events, reducing data resilience and auditability.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/storage/buckets//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"versioning_status\":\"Enabled\"}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + "https://docs.e2enetworks.com/api/myaccount/storage/object-storage/" ] } diff --git a/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py index 1a789168359..b3100e163bb 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py +++ b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py @@ -3,7 +3,9 @@ class storage_bucket_versioning_enabled(Check): - def execute(self): + """Ensure object storage buckets have versioning enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for bucket in storage_client.buckets: report = CheckReportE2e(metadata=self.metadata(), resource=bucket) diff --git a/prowler/providers/e2e/services/storage/storage_service.py b/prowler/providers/e2e/services/storage/storage_service.py index 2314246f068..360de702063 100644 --- a/prowler/providers/e2e/services/storage/storage_service.py +++ b/prowler/providers/e2e/services/storage/storage_service.py @@ -39,7 +39,8 @@ def _fetch_block_volumes(self): ) except Exception as error: logger.error( - f"storage - Error fetching block volumes in {location}: {error}" + f"storage - Error fetching block volumes in {location} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) def _fetch_buckets(self): @@ -71,7 +72,8 @@ def _fetch_buckets(self): ) except Exception as error: logger.error( - f"storage - Error fetching buckets in {location}: {error}" + f"storage - Error fetching buckets in {location} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) def _fetch_efs_volumes(self): diff --git a/tests/providers/e2e/lib/arguments/arguments_test.py b/tests/providers/e2e/lib/arguments/arguments_test.py index d319a0fc0e2..807d0b0c562 100644 --- a/tests/providers/e2e/lib/arguments/arguments_test.py +++ b/tests/providers/e2e/lib/arguments/arguments_test.py @@ -25,3 +25,14 @@ def test_validate_arguments_missing_project_id(self): assert valid is False assert "project ID" in message + + def test_validate_arguments_invalid_project_id(self): + arguments = MagicMock() + arguments.e2e_api_key = "key" + arguments.e2e_auth_token = "token" + arguments.e2e_project_id = "abc" + + valid, message = validate_arguments(arguments) + + assert valid is False + assert "integer" in message diff --git a/tests/providers/e2e/services/node/nodes_service_test.py b/tests/providers/e2e/services/node/nodes_service_test.py index 8e9f0fc5694..c9deebba3f5 100644 --- a/tests/providers/e2e/services/node/nodes_service_test.py +++ b/tests/providers/e2e/services/node/nodes_service_test.py @@ -1,3 +1,5 @@ +import pytest + from unittest.mock import MagicMock, patch from prowler.providers.e2e.services.node.nodes_service import Node, Nodes @@ -67,3 +69,22 @@ def test_node_public_ip_detection(self): assert public_node.has_public_ip is True assert private_node.has_public_ip is False + + +class TestHasPublicIp: + @pytest.mark.parametrize( + "public_ip_address,expected", + [ + (None, False), + ("", False), + ("[]", False), + ("null", False), + ("None", False), + ("1.2.3.4", True), + (" 10.0.0.1 ", True), + ], + ) + def test_has_public_ip_normalization(self, public_ip_address, expected): + from prowler.providers.e2e.services.node.nodes_service import _has_public_ip + + assert _has_public_ip(public_ip_address) is expected From a9958cad0d97228cacb5592cb818d0cbc0917540 Mon Sep 17 00:00:00 2001 From: Deepak Dalvi Date: Sat, 20 Jun 2026 22:40:25 +0530 Subject: [PATCH 5/8] e2e fix code test cases --- prowler/CHANGELOG.md | 3 ++- tests/lib/cli/parser_test.py | 3 ++- .../lib/arguments/{arguments_test.py => e2e_arguments_test.py} | 0 .../{database_service_test.py => e2e_database_service_test.py} | 0 .../{network_service_test.py => e2e_network_service_test.py} | 0 .../{storage_service_test.py => e2e_storage_service_test.py} | 0 6 files changed, 4 insertions(+), 2 deletions(-) rename tests/providers/e2e/lib/arguments/{arguments_test.py => e2e_arguments_test.py} (100%) rename tests/providers/e2e/services/database/{database_service_test.py => e2e_database_service_test.py} (100%) rename tests/providers/e2e/services/network/{network_service_test.py => e2e_network_service_test.py} (100%) rename tests/providers/e2e/services/storage/{storage_service_test.py => e2e_storage_service_test.py} (100%) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 29b2fd9eccf..4add4d0424c 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -33,9 +33,10 @@ All notable changes to the **Prowler SDK** are documented in this file. - `entra_directory_sync_object_takeover_blocked` check for the M365 provider, verifying that hybrid Entra tenants block cloud object takeover through both soft-match and hard-match directory synchronization [(#11098)](https://github.com/prowler-cloud/prowler/pull/11098) - `entra_conditional_access_policy_no_deleted_object_references` check for M365 provider [(#11236)](https://github.com/prowler-cloud/prowler/pull/11236) - `aks_cluster_defender_enabled` check for Azure provider, verifying that AKS clusters have Microsoft Defender security monitoring enabled [(#11028)](https://github.com/prowler-cloud/prowler/pull/11028) -- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the GCP provider, mapping existing GCP checks across the five DORA pillars [(#11642)](https://github.com/prowler-cloud/prowler/pull/11642) +- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the GCP provider, mapping existing GCP checs across the five DORA pillars [(#11642)](https://github.com/prowler-cloud/prowler/pull/11642) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the Cloudflare provider, mapping existing Cloudflare edge/network checks across the applicable DORA pillars [(#11645)](https://github.com/prowler-cloud/prowler/pull/11645) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the AlibabaCloud provider, mapping existing AlibabaCloud checks across the applicable DORA pillars [(#11646)](https://github.com/prowler-cloud/prowler/pull/11646) +- E2E Cloud provider with `network`, `database`, and extended `storage` services (32 checks across compute, network, security groups, load balancers, storage, and DBaaS), plus a Cartography-style resource graph schema at `prowler/providers/e2e/docs/schema.md` ### 🔄 Changed diff --git a/tests/lib/cli/parser_test.py b/tests/lib/cli/parser_test.py index cd602a3a615..7b70fc5bb6d 100644 --- a/tests/lib/cli/parser_test.py +++ b/tests/lib/cli/parser_test.py @@ -17,7 +17,7 @@ # capsys # https://docs.pytest.org/en/7.1.x/how-to/capture-stdout-stderr.html -prowler_default_usage_error = "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,dashboard,iac,image,llm} ..." +prowler_default_usage_error = "usage: prowler [-h] [--version] {aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,e2e,dashboard,iac,image,llm} ..." def mock_get_available_providers(): @@ -39,6 +39,7 @@ def mock_get_available_providers(): "cloudflare", "openstack", "stackit", + "e2e", ] diff --git a/tests/providers/e2e/lib/arguments/arguments_test.py b/tests/providers/e2e/lib/arguments/e2e_arguments_test.py similarity index 100% rename from tests/providers/e2e/lib/arguments/arguments_test.py rename to tests/providers/e2e/lib/arguments/e2e_arguments_test.py diff --git a/tests/providers/e2e/services/database/database_service_test.py b/tests/providers/e2e/services/database/e2e_database_service_test.py similarity index 100% rename from tests/providers/e2e/services/database/database_service_test.py rename to tests/providers/e2e/services/database/e2e_database_service_test.py diff --git a/tests/providers/e2e/services/network/network_service_test.py b/tests/providers/e2e/services/network/e2e_network_service_test.py similarity index 100% rename from tests/providers/e2e/services/network/network_service_test.py rename to tests/providers/e2e/services/network/e2e_network_service_test.py diff --git a/tests/providers/e2e/services/storage/storage_service_test.py b/tests/providers/e2e/services/storage/e2e_storage_service_test.py similarity index 100% rename from tests/providers/e2e/services/storage/storage_service_test.py rename to tests/providers/e2e/services/storage/e2e_storage_service_test.py From d99c82156e9c01c37ab10c6801ef1b04848537b0 Mon Sep 17 00:00:00 2001 From: Deepak Dalvi Date: Sat, 20 Jun 2026 22:59:04 +0530 Subject: [PATCH 6/8] fix(sdk): address E2E provider PR review feedback - Populate check metadata with Risk and CLI remediation - Add docstrings and return types to check classes - Harden security group null handling and standardize logging --- prowler/CHANGELOG.md | 2 +- .../providers/e2e/lib/mutelist/mutelist.py | 5 +++-- ...abase_cluster_backup_enabled.metadata.json | 6 ++--- .../database_cluster_backup_enabled.py | 4 +++- ...uster_default_admin_username.metadata.json | 6 ++--- ...database_cluster_default_admin_username.py | 4 +++- ...ster_ip_whitelist_configured.metadata.json | 6 ++--- ...atabase_cluster_ip_whitelist_configured.py | 4 +++- ...uster_public_ip_not_assigned.metadata.json | 6 ++--- ...database_cluster_public_ip_not_assigned.py | 4 +++- .../database_cluster_running.metadata.json | 6 ++--- .../database_cluster_running.py | 4 +++- ...database_cluster_ssl_enabled.metadata.json | 6 ++--- .../database_cluster_ssl_enabled.py | 4 +++- ...plica_public_ip_not_assigned.metadata.json | 6 ++--- ...database_replica_public_ip_not_assigned.py | 4 +++- .../e2e/services/database/database_service.py | 6 +++-- ...rveip_floating_ip_unattached.metadata.json | 6 ++--- ...etwork_reserveip_floating_ip_unattached.py | 4 +++- ...reserveip_orphaned_public_ip.metadata.json | 6 ++--- .../network_reserveip_orphaned_public_ip.py | 4 +++- .../e2e/services/network/network_service.py | 11 +++++++--- ...twork_vpc_has_attached_nodes.metadata.json | 6 ++--- .../network_vpc_has_attached_nodes.py | 4 +++- .../network_vpc_is_active.metadata.json | 6 ++--- .../network_vpc_is_active.py | 4 +++- ...ering_external_peer_disabled.metadata.json | 6 ++--- ...work_vpc_peering_external_peer_disabled.py | 4 +++- .../node_accidental_protection_enabled.py | 4 +++- .../node_compliance_enabled.py | 4 +++- .../node_encryption_enabled.py | 4 +++- .../node_public_ip_not_assigned.py | 4 +++- .../node_rescue_mode_disabled.py | 4 +++- .../node_vpc_attached/node_vpc_attached.py | 4 +++- .../e2e/services/node/nodes_service.py | 8 +++++-- .../securitygroup_no_inbound_any_all_ports.py | 14 ++++++++---- .../securitygroup_restrictive_default.py | 14 +++++++----- ...ge_bucket_encryption_enabled.metadata.json | 6 ++--- ..._bucket_lifecycle_configured.metadata.json | 6 ++--- .../storage_bucket_lifecycle_configured.py | 4 +++- .../storage_bucket_lock_enabled.metadata.json | 6 ++--- .../storage_bucket_lock_enabled.py | 4 +++- .../storage_efs_backup_enabled.metadata.json | 6 ++--- .../storage_efs_backup_enabled.py | 4 +++- ...ge_efs_vpc_access_restricted.metadata.json | 6 ++--- .../storage_efs_vpc_access_restricted.py | 4 +++- .../e2e/services/storage/storage_service.py | 6 +++-- .../network/network_vpc_is_active_test.py | 22 +++++++++++++++++++ .../storage_efs_backup_enabled_test.py | 22 +++++++++++++++++++ 49 files changed, 206 insertions(+), 94 deletions(-) diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 4add4d0424c..7e3295c09de 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -33,7 +33,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - `entra_directory_sync_object_takeover_blocked` check for the M365 provider, verifying that hybrid Entra tenants block cloud object takeover through both soft-match and hard-match directory synchronization [(#11098)](https://github.com/prowler-cloud/prowler/pull/11098) - `entra_conditional_access_policy_no_deleted_object_references` check for M365 provider [(#11236)](https://github.com/prowler-cloud/prowler/pull/11236) - `aks_cluster_defender_enabled` check for Azure provider, verifying that AKS clusters have Microsoft Defender security monitoring enabled [(#11028)](https://github.com/prowler-cloud/prowler/pull/11028) -- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the GCP provider, mapping existing GCP checs across the five DORA pillars [(#11642)](https://github.com/prowler-cloud/prowler/pull/11642) +- DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the GCP provider, mapping existing GCP checks across the five DORA pillars [(#11642)](https://github.com/prowler-cloud/prowler/pull/11642) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the Cloudflare provider, mapping existing Cloudflare edge/network checks across the applicable DORA pillars [(#11645)](https://github.com/prowler-cloud/prowler/pull/11645) - DORA (Digital Operational Resilience Act, Regulation (EU) 2022/2554) compliance coverage for the AlibabaCloud provider, mapping existing AlibabaCloud checks across the applicable DORA pillars [(#11646)](https://github.com/prowler-cloud/prowler/pull/11646) - E2E Cloud provider with `network`, `database`, and extended `storage` services (32 checks across compute, network, security groups, load balancers, storage, and DBaaS), plus a Cartography-style resource graph schema at `prowler/providers/e2e/docs/schema.md` diff --git a/prowler/providers/e2e/lib/mutelist/mutelist.py b/prowler/providers/e2e/lib/mutelist/mutelist.py index 3f53b40e1af..135435fbc88 100644 --- a/prowler/providers/e2e/lib/mutelist/mutelist.py +++ b/prowler/providers/e2e/lib/mutelist/mutelist.py @@ -8,15 +8,16 @@ class E2eMutelist(Mutelist): """Mutelist implementation for E2E Cloud check findings.""" - def is_finding_muted(self, finding: CheckReportE2e) -> bool: + def is_finding_muted(self, **kwargs) -> bool: """Determine whether an E2E Cloud finding is muted. Args: - finding: The E2E Cloud check report to evaluate. + **kwargs: Keyword arguments; must include ``finding``. Returns: True if the finding matches a mutelist entry, otherwise False. """ + finding: CheckReportE2e = kwargs["finding"] return self.is_muted( finding.resource_id, finding.check_metadata.CheckID, diff --git a/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json index 0df1878bbf0..fb1233c2af8 100644 --- a/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json +++ b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "database", "Description": "Check if E2E Cloud database clusters have backups enabled", - "Risk": "", + "Risk": "Database clusters without backups are vulnerable to data loss from failures or accidental deletion.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/rds/clusters//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"backup_enabled\":true}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/database/rds/" ] } diff --git a/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py index 358901b57b6..285998173f5 100644 --- a/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py +++ b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py @@ -3,7 +3,9 @@ class database_cluster_backup_enabled(Check): - def execute(self): + """Check if E2E Cloud database clusters have backups enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for cluster in database_client.clusters: report = CheckReportE2e(metadata=self.metadata(), resource=cluster) diff --git a/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json index 21a4bfd226f..ce2ede0ec67 100644 --- a/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json +++ b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "database", "Description": "Check if E2E Cloud database clusters do not use the default admin username", - "Risk": "", + "Risk": "Default admin usernames increase exposure to credential guessing and brute-force attacks against database clusters.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/rds/clusters//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"username\":\"\"}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/database/rds/" ] } diff --git a/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py index fca3e7e9f2c..65d4634c143 100644 --- a/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py +++ b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py @@ -3,7 +3,9 @@ class database_cluster_default_admin_username(Check): - def execute(self): + """Check if E2E Cloud database clusters do not use the default admin username.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for cluster in database_client.clusters: report = CheckReportE2e(metadata=self.metadata(), resource=cluster) diff --git a/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json index d2d4fed4bc2..5b38627ae7d 100644 --- a/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json +++ b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "database", "Description": "Check if E2E Cloud database clusters with public IPs have IP whitelisting configured", - "Risk": "", + "Risk": "Database clusters without IP whitelisting allow broader network access and increase unauthorized connection risk.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/rds/clusters//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"whitelist_ips\":[\"\"]}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/database/rds/" ] } diff --git a/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py index f89dce37514..3f26a81dd98 100644 --- a/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py +++ b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py @@ -3,7 +3,9 @@ class database_cluster_ip_whitelist_configured(Check): - def execute(self): + """Check if E2E Cloud database clusters with public IPs have IP whitelisting configured.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for cluster in database_client.clusters: if not cluster.master_has_public_ip: diff --git a/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json b/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json index 5f7904403e9..2811b1389b7 100644 --- a/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json +++ b/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "database", "Description": "Check if E2E Cloud database clusters do not expose a public IP on the master node", - "Risk": "", + "Risk": "Public database endpoints expand attack surface and increase risk of unauthorized access over the internet.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/rds/clusters//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"public_ip\":null}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/database/rds/" ] } diff --git a/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py b/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py index dadbad4e6fc..2fdfa273c5f 100644 --- a/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py +++ b/prowler/providers/e2e/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py @@ -3,7 +3,9 @@ class database_cluster_public_ip_not_assigned(Check): - def execute(self): + """Check if E2E Cloud database clusters do not expose a public IP on the master node.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for cluster in database_client.clusters: report = CheckReportE2e(metadata=self.metadata(), resource=cluster) diff --git a/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.metadata.json b/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.metadata.json index 1b94b1670e0..a80979365e1 100644 --- a/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.metadata.json +++ b/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "database", "Description": "Check if E2E Cloud database clusters are in RUNNING status", - "Risk": "", + "Risk": "Non-running database clusters may indicate outages or misconfiguration that impacts availability and recovery.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X POST -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" \"https://api.e2enetworks.com/myaccount/api/v1/rds/clusters//start/?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\"", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/database/rds/" ] } diff --git a/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py b/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py index 84650df55f1..f96bfa1b1ac 100644 --- a/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py +++ b/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py @@ -3,7 +3,9 @@ class database_cluster_running(Check): - def execute(self): + """Check if E2E Cloud database clusters are in RUNNING status.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for cluster in database_client.clusters: report = CheckReportE2e(metadata=self.metadata(), resource=cluster) diff --git a/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json index dd1b61075fb..1b7620962f7 100644 --- a/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json +++ b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "database", "Description": "Check if E2E Cloud database clusters have SSL enabled on the master node", - "Risk": "", + "Risk": "Database clusters without SSL expose credentials and data in transit to interception.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/rds/clusters//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"is_ssl_enabled\":true}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/database/rds/" ] } diff --git a/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py index 7bde2dc41ff..f201dd9bd20 100644 --- a/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py +++ b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py @@ -3,7 +3,9 @@ class database_cluster_ssl_enabled(Check): - def execute(self): + """Check if E2E Cloud database clusters have SSL enabled on the master node.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for cluster in database_client.clusters: report = CheckReportE2e(metadata=self.metadata(), resource=cluster) diff --git a/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json b/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json index d3d82b37ffb..27bb4abf146 100644 --- a/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json +++ b/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "database", "Description": "Check if E2E Cloud database read replicas do not have a public IP assigned", - "Risk": "", + "Risk": "Read replicas with public IPs increase database exposure and unauthorized access risk.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/rds/replicas//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"public_ip\":null}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/database/rds/" ] } diff --git a/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py b/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py index 0cd4b5b8d2e..33b59276e70 100644 --- a/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py +++ b/prowler/providers/e2e/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py @@ -3,7 +3,9 @@ class database_replica_public_ip_not_assigned(Check): - def execute(self): + """Check if E2E Cloud database read replicas do not have a public IP assigned.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for instance in database_client.instances: if instance.role != "replica": diff --git a/prowler/providers/e2e/services/database/database_service.py b/prowler/providers/e2e/services/database/database_service.py index 611af05c80c..f5666921045 100644 --- a/prowler/providers/e2e/services/database/database_service.py +++ b/prowler/providers/e2e/services/database/database_service.py @@ -97,7 +97,8 @@ def _fetch_clusters(self): ) except Exception as error: logger.error( - f"database - Error fetching clusters in {location}: {error}" + f"database - Error fetching clusters in {location} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) def _get_cluster_detail(self, cluster_id: str, location: str) -> dict: @@ -111,7 +112,8 @@ def _get_cluster_detail(self, cluster_id: str, location: str) -> dict: return data if isinstance(data, dict) else {} except Exception as error: logger.error( - f"database - Error fetching cluster detail {cluster_id}: {error}" + f"database - Error fetching cluster detail {cluster_id} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) return {} diff --git a/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json index 4e1da776da2..7fc1bbb439d 100644 --- a/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json +++ b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "network", "Description": "Check if E2E Cloud floating IPs are attached to nodes", - "Risk": "", + "Risk": "Unattached floating IPs may be reassigned unexpectedly and disrupt secure routing assumptions.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/reserve_ips//attach/?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"vm_id\":}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/network/reserve-ip/" ] } diff --git a/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py index d6fc6a8362c..4406cc2fd95 100644 --- a/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py +++ b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py @@ -3,7 +3,9 @@ class network_reserveip_floating_ip_unattached(Check): - def execute(self): + """Check if E2E Cloud floating IPs are attached to nodes.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for ip in network_client.reserved_ips: if ip.reserved_type != "FloatingIP": diff --git a/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json index 4cfc7da7b3b..d97938a8be0 100644 --- a/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json +++ b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "network", "Description": "Check if E2E Cloud public or addon IPs are attached", - "Risk": "", + "Risk": "Orphaned public IPs remain internet-reachable resources that can be misused or reassigned without ownership controls.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X DELETE -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" \"https://api.e2enetworks.com/myaccount/api/v1/reserve_ips//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\"", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/network/reserve-ip/" ] } diff --git a/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py index 0dc0a282def..fff56e82a89 100644 --- a/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py +++ b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py @@ -3,7 +3,9 @@ class network_reserveip_orphaned_public_ip(Check): - def execute(self): + """Check if E2E Cloud public or addon IPs are attached.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for ip in network_client.reserved_ips: if ip.reserved_type not in ("PublicIP", "AddonIP"): diff --git a/prowler/providers/e2e/services/network/network_service.py b/prowler/providers/e2e/services/network/network_service.py index a9af61d2624..be2c1a91225 100644 --- a/prowler/providers/e2e/services/network/network_service.py +++ b/prowler/providers/e2e/services/network/network_service.py @@ -20,6 +20,8 @@ def _fetch_vpcs(self): for location in self.provider.session.locations: try: vpcs = self.client.paginate("/vpc/list/", location=location) + if not isinstance(vpcs, list): + continue for item in vpcs: gateway_node = item.get("gateway_node", {}) or {} self.vpcs.append( @@ -37,7 +39,8 @@ def _fetch_vpcs(self): ) except Exception as error: logger.error( - f"network - Error fetching VPCs in {location}: {error}" + f"network - Error fetching VPCs in {location} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) def _fetch_reserved_ips(self): @@ -62,7 +65,8 @@ def _fetch_reserved_ips(self): ) except Exception as error: logger.error( - f"network - Error fetching reserved IPs in {location}: {error}" + f"network - Error fetching reserved IPs in {location} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) def _fetch_vpc_tunnels(self): @@ -93,7 +97,8 @@ def _fetch_vpc_tunnels(self): ) except Exception as error: logger.error( - f"network - Error fetching tunnels for VPC {vpc.network_id}: {error}" + f"network - Error fetching tunnels for VPC {vpc.network_id} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) diff --git a/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json index b9de3bf6899..2ed96c9dc79 100644 --- a/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json +++ b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "network", "Description": "Check if E2E Cloud VPCs have attached nodes", - "Risk": "", + "Risk": "VPCs without attached nodes may indicate unused network segments or misconfiguration that weakens segmentation.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/nodes//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"vpc_id\":}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/network/vpc/" ] } diff --git a/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py index 33ae6accad8..47e4c7e03dc 100644 --- a/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py +++ b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py @@ -3,7 +3,9 @@ class network_vpc_has_attached_nodes(Check): - def execute(self): + """Check if E2E Cloud VPCs have attached nodes.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for vpc in network_client.vpcs: report = CheckReportE2e(metadata=self.metadata(), resource=vpc) diff --git a/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json b/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json index 1020e1eb6ce..b1dde659fa1 100644 --- a/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json +++ b/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "network", "Description": "Check if E2E Cloud VPCs are active", - "Risk": "", + "Risk": "Inactive VPCs can cause connectivity failures and may leave workloads without expected network isolation.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/vpc//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"is_active\":true}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/network/vpc/" ] } diff --git a/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py b/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py index b5f172365d7..476f954fb04 100644 --- a/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py +++ b/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py @@ -3,7 +3,9 @@ class network_vpc_is_active(Check): - def execute(self): + """Check if E2E Cloud VPCs are active.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for vpc in network_client.vpcs: report = CheckReportE2e(metadata=self.metadata(), resource=vpc) diff --git a/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json b/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json index 7ef120a7dce..98a236545e8 100644 --- a/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json +++ b/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "network", "Description": "Check if E2E Cloud VPC peering does not use external peers", - "Risk": "", + "Risk": "VPC peering with external peers can expand trust boundaries and expose private networks to third parties.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X DELETE -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" \"https://api.e2enetworks.com/myaccount/api/v1/vpc/tunnels//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\"", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/network/vpc/" ] } diff --git a/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py b/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py index 76838c57000..b13d9232952 100644 --- a/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py +++ b/prowler/providers/e2e/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py @@ -3,7 +3,9 @@ class network_vpc_peering_external_peer_disabled(Check): - def execute(self): + """Check if E2E Cloud VPC peering does not use external peers.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for tunnel in network_client.vpc_tunnels: report = CheckReportE2e(metadata=self.metadata(), resource=tunnel) diff --git a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py index 5fe43ccc757..b63b920b6c5 100644 --- a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py +++ b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py @@ -3,7 +3,9 @@ class node_accidental_protection_enabled(Check): - def execute(self): + """Check if E2E Cloud nodes have accidental protection enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for node in nodes_client.nodes: report = CheckReportE2e(metadata=self.metadata(), resource=node) diff --git a/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py index 3c392c50838..b43219c1351 100644 --- a/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py +++ b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py @@ -3,7 +3,9 @@ class node_compliance_enabled(Check): - def execute(self): + """Check if E2E Cloud nodes have compliance mode enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for node in nodes_client.nodes: report = CheckReportE2e(metadata=self.metadata(), resource=node) diff --git a/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py index 2be78e83aa8..c29bcb955d1 100644 --- a/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py +++ b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py @@ -3,7 +3,9 @@ class node_encryption_enabled(Check): - def execute(self): + """Check if E2E Cloud nodes have encryption enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for node in nodes_client.nodes: report = CheckReportE2e(metadata=self.metadata(), resource=node) diff --git a/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py index 8921e5ce70a..74e83862810 100644 --- a/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py +++ b/prowler/providers/e2e/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py @@ -3,7 +3,9 @@ class node_public_ip_not_assigned(Check): - def execute(self): + """Check if E2E Cloud nodes do not have a public IP assigned.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for node in nodes_client.nodes: report = CheckReportE2e(metadata=self.metadata(), resource=node) diff --git a/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py index 16cc87c7f4d..723f8424fe9 100644 --- a/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py +++ b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py @@ -3,7 +3,9 @@ class node_rescue_mode_disabled(Check): - def execute(self): + """Check if E2E Cloud nodes do not have rescue mode enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for node in nodes_client.nodes: report = CheckReportE2e(metadata=self.metadata(), resource=node) diff --git a/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py b/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py index c99f3387e6a..990bea40b9f 100644 --- a/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py +++ b/prowler/providers/e2e/services/node/node_vpc_attached/node_vpc_attached.py @@ -3,7 +3,9 @@ class node_vpc_attached(Check): - def execute(self): + """Check if E2E Cloud nodes are attached to a VPC.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for node in nodes_client.nodes: report = CheckReportE2e(metadata=self.metadata(), resource=node) diff --git a/prowler/providers/e2e/services/node/nodes_service.py b/prowler/providers/e2e/services/node/nodes_service.py index be5c3bfd38a..0dc19522cd9 100644 --- a/prowler/providers/e2e/services/node/nodes_service.py +++ b/prowler/providers/e2e/services/node/nodes_service.py @@ -63,7 +63,8 @@ def _fetch_nodes(self): ) except Exception as error: logger.error( - f"nodes - Error fetching nodes in {location}: {error}" + f"nodes - Error fetching nodes in {location} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) def _get_node_detail(self, node_id: str, location: str) -> dict: @@ -76,7 +77,10 @@ def _get_node_detail(self, node_id: str, location: str) -> dict: ) return data if isinstance(data, dict) else {} except Exception as error: - logger.error(f"nodes - Error fetching node detail {node_id}: {error}") + logger.error( + f"nodes - Error fetching node detail {node_id} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) return {} diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py index f9d6d578c78..38756466384 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py @@ -4,19 +4,25 @@ ) -def _is_open_network(value: str) -> bool: - normalized = value.lower().strip() +def _is_open_network(value: str | None) -> bool: + if value is None: + return False + normalized = str(value).lower().strip() return normalized in ("any", "0.0.0.0/0", "::/0") def _is_permissive_inbound(rule) -> bool: - if rule.rule_type.lower() != "inbound" or rule.protocol_name.lower() != "all": + if (rule.rule_type or "").lower() != "inbound": + return False + if (rule.protocol_name or "").lower() != "all": return False return _is_open_network(rule.network) or _is_open_network(rule.network_cidr) class securitygroup_no_inbound_any_all_ports(Check): - def execute(self): + """Check if E2E Cloud security groups do not allow inbound all-protocol traffic from any source.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for group in securitygroup_client.security_groups: report = CheckReportE2e(metadata=self.metadata(), resource=group) diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py index 0ba9e1f1a46..3a0960500b7 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py @@ -4,16 +4,18 @@ ) -def _is_open_network(value: str) -> bool: - normalized = value.lower().strip() +def _is_open_network(value: str | None) -> bool: + if value is None: + return False + normalized = str(value).lower().strip() return normalized in ("any", "0.0.0.0/0", "::/0") def _has_permissive_inbound(rules) -> bool: for rule in rules: if ( - rule.rule_type.lower() == "inbound" - and rule.protocol_name.lower() == "all" + (rule.rule_type or "").lower() == "inbound" + and (rule.protocol_name or "").lower() == "all" and ( _is_open_network(rule.network) or _is_open_network(rule.network_cidr) @@ -24,7 +26,9 @@ def _has_permissive_inbound(rules) -> bool: class securitygroup_restrictive_default(Check): - def execute(self): + """Check if E2E Cloud nodes do not rely on permissive default security groups.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] node_groups: dict[str, list] = {} for group in securitygroup_client.node_security_groups: diff --git a/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.metadata.json b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.metadata.json index e9c315cb4d7..44ab4fa48a8 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.metadata.json +++ b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "ObjectStorageBucket", "ResourceGroup": "storage", "Description": "Check if E2E Cloud object storage buckets have encryption enabled.", - "Risk": "", + "Risk": "Unencrypted object storage buckets increase risk of data exposure if storage access controls fail.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/storage/buckets//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"is_encryption_enabled\":true}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + "https://docs.e2enetworks.com/api/myaccount/storage/object-storage/" ] } diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.metadata.json b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.metadata.json index 20053f3c8a3..9dc0e29b2e0 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.metadata.json +++ b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "storage", "Description": "Check if E2E Cloud object storage buckets have lifecycle configuration enabled", - "Risk": "", + "Risk": "Buckets without lifecycle rules may retain sensitive data longer than required and increase breach impact.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/storage/buckets//lifecycle/?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"status\":\"Configured\"}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/storage/object-storage/" ] } diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py index 2f6307147cb..f189c0f8071 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py +++ b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py @@ -3,7 +3,9 @@ class storage_bucket_lifecycle_configured(Check): - def execute(self): + """Check if E2E Cloud object storage buckets have lifecycle configuration enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for bucket in storage_client.buckets: report = CheckReportE2e(metadata=self.metadata(), resource=bucket) diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.metadata.json b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.metadata.json index a0a4c941aba..488208538ca 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.metadata.json +++ b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "storage", "Description": "Check if E2E Cloud object storage buckets have object lock enabled", - "Risk": "", + "Risk": "Buckets without object lock are more vulnerable to destructive overwrites and ransomware-style deletion.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/storage/buckets//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"is_lock_enabled\":true}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/storage/object-storage/" ] } diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py index f8d5670fbd2..fcb4595df84 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py +++ b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py @@ -3,7 +3,9 @@ class storage_bucket_lock_enabled(Check): - def execute(self): + """Check if E2E Cloud object storage buckets have object lock enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for bucket in storage_client.buckets: report = CheckReportE2e(metadata=self.metadata(), resource=bucket) diff --git a/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json index 4663c2f53d3..80d71acf937 100644 --- a/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json +++ b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "storage", "Description": "Check if E2E Cloud EFS volumes have backup enabled", - "Risk": "", + "Risk": "Shared file systems without backups increase data-loss risk during failures or accidental changes.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/efs//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"is_backup_enabled\":true}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/storage/parallel-file-storage/" ] } diff --git a/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py index 5111eaa2a3c..14ce42a28aa 100644 --- a/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py +++ b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py @@ -3,7 +3,9 @@ class storage_efs_backup_enabled(Check): - def execute(self): + """Check if E2E Cloud EFS volumes have backup enabled.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for volume in storage_client.efs_volumes: report = CheckReportE2e(metadata=self.metadata(), resource=volume) diff --git a/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json b/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json index 7b6d7eb8e5b..f179380e6a9 100644 --- a/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json +++ b/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json @@ -10,11 +10,11 @@ "ResourceType": "Other", "ResourceGroup": "storage", "Description": "Check if E2E Cloud EFS volumes restrict VPC access", - "Risk": "", + "Risk": "EFS volumes allowing all VPC resources broaden access beyond least privilege within the network.", "RelatedUrl": "", "Remediation": { "Code": { - "CLI": "", + "CLI": "curl -X PUT -H \"Authorization: Bearer $E2E_AUTH_TOKEN\" -H \"Content-Type: application/json\" \"https://api.e2enetworks.com/myaccount/api/v1/efs//?apikey=$E2E_API_KEY&project_id=$E2E_PROJECT_ID&location=\" -d '{\"is_all_vpc_resources_allowed\":false}'", "NativeIaC": "", "Other": "", "Terraform": "" @@ -29,6 +29,6 @@ "RelatedTo": [], "Notes": "", "AdditionalURLs": [ - "https://docs.e2enetworks.com/api/myaccount/openapi.yaml" + "https://docs.e2enetworks.com/api/myaccount/storage/parallel-file-storage/" ] } diff --git a/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py b/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py index 0fdf1e25fef..f8deeb4f0f7 100644 --- a/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py +++ b/prowler/providers/e2e/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py @@ -3,7 +3,9 @@ class storage_efs_vpc_access_restricted(Check): - def execute(self): + """Check if E2E Cloud EFS volumes restrict VPC access.""" + + def execute(self) -> list[CheckReportE2e]: findings = [] for volume in storage_client.efs_volumes: report = CheckReportE2e(metadata=self.metadata(), resource=volume) diff --git a/prowler/providers/e2e/services/storage/storage_service.py b/prowler/providers/e2e/services/storage/storage_service.py index 360de702063..cd4ef992811 100644 --- a/prowler/providers/e2e/services/storage/storage_service.py +++ b/prowler/providers/e2e/services/storage/storage_service.py @@ -98,7 +98,8 @@ def _fetch_efs_volumes(self): ) except Exception as error: logger.error( - f"storage - Error fetching EFS volumes in {location}: {error}" + f"storage - Error fetching EFS volumes in {location} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) def _fetch_epfs_volumes(self): @@ -135,7 +136,8 @@ def _fetch_epfs_volumes(self): ) except Exception as error: logger.error( - f"storage - Error fetching EPFS volumes in {location}: {error}" + f"storage - Error fetching EPFS volumes in {location} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) diff --git a/tests/providers/e2e/services/network/network_vpc_is_active_test.py b/tests/providers/e2e/services/network/network_vpc_is_active_test.py index 2c1fc71d779..84d05851e80 100644 --- a/tests/providers/e2e/services/network/network_vpc_is_active_test.py +++ b/tests/providers/e2e/services/network/network_vpc_is_active_test.py @@ -43,3 +43,25 @@ def test_pass_and_fail(self): assert len(findings) == 2 assert findings[0].status == "PASS" assert findings[1].status == "FAIL" + + def test_no_resources(self): + network_client = mock.MagicMock() + network_client.vpcs = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2e_provider(), + ), + mock.patch( + "prowler.providers.e2e.services.network.network_vpc_is_active.network_vpc_is_active.network_client", + new=network_client, + ), + ): + from prowler.providers.e2e.services.network.network_vpc_is_active.network_vpc_is_active import ( + network_vpc_is_active, + ) + + findings = network_vpc_is_active().execute() + + assert findings == [] diff --git a/tests/providers/e2e/services/storage/storage_efs_backup_enabled_test.py b/tests/providers/e2e/services/storage/storage_efs_backup_enabled_test.py index 4fd18a04c0b..ff15a687922 100644 --- a/tests/providers/e2e/services/storage/storage_efs_backup_enabled_test.py +++ b/tests/providers/e2e/services/storage/storage_efs_backup_enabled_test.py @@ -41,3 +41,25 @@ def test_pass_and_fail(self): assert len(findings) == 2 assert findings[0].status == "PASS" assert findings[1].status == "FAIL" + + def test_no_resources(self): + storage_client = mock.MagicMock() + storage_client.efs_volumes = [] + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2e_provider(), + ), + mock.patch( + "prowler.providers.e2e.services.storage.storage_efs_backup_enabled.storage_efs_backup_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.e2e.services.storage.storage_efs_backup_enabled.storage_efs_backup_enabled import ( + storage_efs_backup_enabled, + ) + + findings = storage_efs_backup_enabled().execute() + + assert findings == [] From f346d2a871f20023c56a18aa7fca5dde4af9346e Mon Sep 17 00:00:00 2001 From: Deepak Dalvi Date: Sat, 20 Jun 2026 23:12:16 +0530 Subject: [PATCH 7/8] fix: foramt and lint --- .../database_cluster_backup_enabled.py | 8 ++++++-- .../database_cluster_default_admin_username.py | 4 +--- .../database_cluster_ip_whitelist_configured.py | 4 +--- .../database_cluster_running.py | 4 +--- .../database_cluster_ssl_enabled.py | 4 +++- .../e2e/services/database/database_service.py | 8 +++++--- ...adbalancer_alb_https_uses_ssl_certificate.py | 4 +--- ...loadbalancer_backend_health_check_enabled.py | 4 +--- .../loadbalancer_bitninja_enabled.py | 4 +--- .../network_reserveip_floating_ip_unattached.py | 8 ++++++-- .../network_reserveip_orphaned_public_ip.py | 8 ++++++-- .../network_vpc_has_attached_nodes.py | 4 +++- .../network_vpc_is_active.py | 4 +++- .../node_accidental_protection_enabled.py | 8 ++++++-- .../node_compliance_enabled.py | 4 +++- .../node_encryption_enabled.py | 4 +++- .../node_rescue_mode_disabled.py | 4 +++- .../securitygroup_no_inbound_any_all_ports.py | 8 ++------ .../securitygroup_restrictive_default.py | 17 ++++++----------- .../storage_block_volume_not_orphaned.py | 8 ++------ .../storage_bucket_encryption_enabled.py | 4 +--- .../storage_bucket_lifecycle_configured.py | 8 ++------ .../storage_bucket_lock_enabled.py | 4 +++- .../storage_bucket_versioning_enabled.py | 4 +--- .../storage_efs_backup_enabled.py | 4 +++- 25 files changed, 73 insertions(+), 72 deletions(-) diff --git a/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py index 285998173f5..4db2ef83ca8 100644 --- a/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py +++ b/prowler/providers/e2e/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py @@ -10,9 +10,13 @@ def execute(self) -> list[CheckReportE2e]: for cluster in database_client.clusters: report = CheckReportE2e(metadata=self.metadata(), resource=cluster) report.status = "PASS" - report.status_extended = f"Database cluster {cluster.name} has backups enabled." + report.status_extended = ( + f"Database cluster {cluster.name} has backups enabled." + ) if not cluster.backup_enabled: report.status = "FAIL" - report.status_extended = f"Database cluster {cluster.name} does not have backups enabled." + report.status_extended = ( + f"Database cluster {cluster.name} does not have backups enabled." + ) findings.append(report) return findings diff --git a/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py index 65d4634c143..52b8797336c 100644 --- a/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py +++ b/prowler/providers/e2e/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py @@ -10,9 +10,7 @@ def execute(self) -> list[CheckReportE2e]: for cluster in database_client.clusters: report = CheckReportE2e(metadata=self.metadata(), resource=cluster) report.status = "PASS" - report.status_extended = ( - f"Database cluster {cluster.name} does not use the default admin username." - ) + report.status_extended = f"Database cluster {cluster.name} does not use the default admin username." if cluster.master_username.lower() == "admin": report.status = "FAIL" report.status_extended = ( diff --git a/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py index 3f26a81dd98..b43332ace64 100644 --- a/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py +++ b/prowler/providers/e2e/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py @@ -17,8 +17,6 @@ def execute(self) -> list[CheckReportE2e]: ) if not cluster.whitelisted_ips: report.status = "FAIL" - report.status_extended = ( - f"Database cluster {cluster.name} has a public IP but no whitelisted IPs." - ) + report.status_extended = f"Database cluster {cluster.name} has a public IP but no whitelisted IPs." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py b/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py index f96bfa1b1ac..6f0e7f3b891 100644 --- a/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py +++ b/prowler/providers/e2e/services/database/database_cluster_running/database_cluster_running.py @@ -13,8 +13,6 @@ def execute(self) -> list[CheckReportE2e]: report.status_extended = f"Database cluster {cluster.name} is running." if cluster.status != "RUNNING": report.status = "FAIL" - report.status_extended = ( - f"Database cluster {cluster.name} is not running (status: {cluster.status})." - ) + report.status_extended = f"Database cluster {cluster.name} is not running (status: {cluster.status})." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py index f201dd9bd20..e407019535c 100644 --- a/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py +++ b/prowler/providers/e2e/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py @@ -10,7 +10,9 @@ def execute(self) -> list[CheckReportE2e]: for cluster in database_client.clusters: report = CheckReportE2e(metadata=self.metadata(), resource=cluster) report.status = "PASS" - report.status_extended = f"Database cluster {cluster.name} has SSL enabled on the master node." + report.status_extended = ( + f"Database cluster {cluster.name} has SSL enabled on the master node." + ) if not cluster.master_ssl_enabled: report.status = "FAIL" report.status_extended = f"Database cluster {cluster.name} does not have SSL enabled on the master node." diff --git a/prowler/providers/e2e/services/database/database_service.py b/prowler/providers/e2e/services/database/database_service.py index f5666921045..42eca29d83b 100644 --- a/prowler/providers/e2e/services/database/database_service.py +++ b/prowler/providers/e2e/services/database/database_service.py @@ -36,9 +36,11 @@ def _fetch_clusters(self): master_node = merged.get("master_node", {}) or {} database_info = master_node.get("database", {}) or {} - software = merged.get("software", {}) or master_node.get( - "plan", {} - ).get("software", {}) or {} + software = ( + merged.get("software", {}) + or master_node.get("plan", {}).get("software", {}) + or {} + ) cluster = DatabaseCluster( id=cluster_id, diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py index 294fa4f021f..7ea6dad2ef3 100644 --- a/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py @@ -20,8 +20,6 @@ def execute(self) -> list[CheckReportE2e]: ) if not lb.ssl_certificate_id: report.status = "FAIL" - report.status_extended = ( - f"Load balancer {lb.name} does not have an SSL certificate configured for HTTPS traffic." - ) + report.status_extended = f"Load balancer {lb.name} does not have an SSL certificate configured for HTTPS traffic." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py index b1bb97af034..a650072409e 100644 --- a/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py @@ -20,8 +20,6 @@ def execute(self) -> list[CheckReportE2e]: ) if not lb.has_backend_health_check: report.status = "FAIL" - report.status_extended = ( - f"Load balancer {lb.name} does not have backend health checks configured." - ) + report.status_extended = f"Load balancer {lb.name} does not have backend health checks configured." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py index bd83fca7dda..b417ec6a482 100644 --- a/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py +++ b/prowler/providers/e2e/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py @@ -17,8 +17,6 @@ def execute(self) -> list[CheckReportE2e]: ) if not lb.enable_bitninja: report.status = "FAIL" - report.status_extended = ( - f"Load balancer {lb.name} does not have BitNinja protection enabled." - ) + report.status_extended = f"Load balancer {lb.name} does not have BitNinja protection enabled." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py index 4406cc2fd95..2ace7d09429 100644 --- a/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py +++ b/prowler/providers/e2e/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py @@ -12,9 +12,13 @@ def execute(self) -> list[CheckReportE2e]: continue report = CheckReportE2e(metadata=self.metadata(), resource=ip) report.status = "PASS" - report.status_extended = f"Floating IP {ip.ip_address} is attached to node(s)." + report.status_extended = ( + f"Floating IP {ip.ip_address} is attached to node(s)." + ) if ip.status != "Attached" or ip.floating_ip_attached_nodes_count == 0: report.status = "FAIL" - report.status_extended = f"Floating IP {ip.ip_address} is not attached to any node." + report.status_extended = ( + f"Floating IP {ip.ip_address} is not attached to any node." + ) findings.append(report) return findings diff --git a/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py index fff56e82a89..113e23983d3 100644 --- a/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py +++ b/prowler/providers/e2e/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py @@ -12,9 +12,13 @@ def execute(self) -> list[CheckReportE2e]: continue report = CheckReportE2e(metadata=self.metadata(), resource=ip) report.status = "PASS" - report.status_extended = f"Reserved IP {ip.ip_address} is attached to a resource." + report.status_extended = ( + f"Reserved IP {ip.ip_address} is attached to a resource." + ) if ip.status != "Attached" or ip.vm_id is None: report.status = "FAIL" - report.status_extended = f"Reserved IP {ip.ip_address} is orphaned (status: {ip.status})." + report.status_extended = ( + f"Reserved IP {ip.ip_address} is orphaned (status: {ip.status})." + ) findings.append(report) return findings diff --git a/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py index 47e4c7e03dc..c5ed7f4f1eb 100644 --- a/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py +++ b/prowler/providers/e2e/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py @@ -10,7 +10,9 @@ def execute(self) -> list[CheckReportE2e]: for vpc in network_client.vpcs: report = CheckReportE2e(metadata=self.metadata(), resource=vpc) report.status = "PASS" - report.status_extended = f"VPC {vpc.name} has {vpc.vm_count} attached node(s)." + report.status_extended = ( + f"VPC {vpc.name} has {vpc.vm_count} attached node(s)." + ) if vpc.vm_count <= 0: report.status = "FAIL" report.status_extended = f"VPC {vpc.name} has no attached nodes." diff --git a/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py b/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py index 476f954fb04..3104a8fd2d3 100644 --- a/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py +++ b/prowler/providers/e2e/services/network/network_vpc_is_active/network_vpc_is_active.py @@ -13,6 +13,8 @@ def execute(self) -> list[CheckReportE2e]: report.status_extended = f"VPC {vpc.name} is active." if not vpc.is_active or vpc.state != "Active": report.status = "FAIL" - report.status_extended = f"VPC {vpc.name} is not active (state: {vpc.state})." + report.status_extended = ( + f"VPC {vpc.name} is not active (state: {vpc.state})." + ) findings.append(report) return findings diff --git a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py index b63b920b6c5..8ddaeabc7c1 100644 --- a/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py +++ b/prowler/providers/e2e/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py @@ -10,9 +10,13 @@ def execute(self) -> list[CheckReportE2e]: for node in nodes_client.nodes: report = CheckReportE2e(metadata=self.metadata(), resource=node) report.status = "PASS" - report.status_extended = f"Node {node.name} has accidental protection enabled." + report.status_extended = ( + f"Node {node.name} has accidental protection enabled." + ) if not node.is_accidental_protection: report.status = "FAIL" - report.status_extended = f"Node {node.name} does not have accidental protection enabled." + report.status_extended = ( + f"Node {node.name} does not have accidental protection enabled." + ) findings.append(report) return findings diff --git a/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py index b43219c1351..c726eb177e9 100644 --- a/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py +++ b/prowler/providers/e2e/services/node/node_compliance_enabled/node_compliance_enabled.py @@ -13,6 +13,8 @@ def execute(self) -> list[CheckReportE2e]: report.status_extended = f"Node {node.name} has compliance mode enabled." if not node.is_node_compliance: report.status = "FAIL" - report.status_extended = f"Node {node.name} does not have compliance mode enabled." + report.status_extended = ( + f"Node {node.name} does not have compliance mode enabled." + ) findings.append(report) return findings diff --git a/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py index c29bcb955d1..cdb89e26b61 100644 --- a/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py +++ b/prowler/providers/e2e/services/node/node_encryption_enabled/node_encryption_enabled.py @@ -13,6 +13,8 @@ def execute(self) -> list[CheckReportE2e]: report.status_extended = f"Node {node.name} has encryption enabled." if not node.is_encryption_enabled: report.status = "FAIL" - report.status_extended = f"Node {node.name} does not have encryption enabled." + report.status_extended = ( + f"Node {node.name} does not have encryption enabled." + ) findings.append(report) return findings diff --git a/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py index 723f8424fe9..6f7d8fb46a5 100644 --- a/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py +++ b/prowler/providers/e2e/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py @@ -10,7 +10,9 @@ def execute(self) -> list[CheckReportE2e]: for node in nodes_client.nodes: report = CheckReportE2e(metadata=self.metadata(), resource=node) report.status = "PASS" - report.status_extended = f"Node {node.name} does not have rescue mode enabled." + report.status_extended = ( + f"Node {node.name} does not have rescue mode enabled." + ) if node.rescue_mode_status != "Disabled": report.status = "FAIL" report.status_extended = f"Node {node.name} has rescue mode enabled." diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py index 38756466384..22694a0e376 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py @@ -27,13 +27,9 @@ def execute(self) -> list[CheckReportE2e]: for group in securitygroup_client.security_groups: report = CheckReportE2e(metadata=self.metadata(), resource=group) report.status = "PASS" - report.status_extended = ( - f"Security group {group.name} does not allow inbound all-protocol traffic from any source." - ) + report.status_extended = f"Security group {group.name} does not allow inbound all-protocol traffic from any source." if any(_is_permissive_inbound(rule) for rule in group.rules): report.status = "FAIL" - report.status_extended = ( - f"Security group {group.name} allows inbound all-protocol traffic from any source." - ) + report.status_extended = f"Security group {group.name} allows inbound all-protocol traffic from any source." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py index 3a0960500b7..7a056519c56 100644 --- a/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py +++ b/prowler/providers/e2e/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py @@ -16,10 +16,7 @@ def _has_permissive_inbound(rules) -> bool: if ( (rule.rule_type or "").lower() == "inbound" and (rule.protocol_name or "").lower() == "all" - and ( - _is_open_network(rule.network) - or _is_open_network(rule.network_cidr) - ) + and (_is_open_network(rule.network) or _is_open_network(rule.network_cidr)) ): return True return False @@ -38,17 +35,15 @@ def execute(self) -> list[CheckReportE2e]: resource = groups[0] report = CheckReportE2e(metadata=self.metadata(), resource=resource) report.status = "PASS" - report.status_extended = ( - f"Node {resource.node_name} does not rely on a permissive default security group." - ) + report.status_extended = f"Node {resource.node_name} does not rely on a permissive default security group." default_groups = [group for group in groups if group.is_default] if default_groups and len(groups) == len(default_groups): - if any(_has_permissive_inbound(group.rules) for group in default_groups): + if any( + _has_permissive_inbound(group.rules) for group in default_groups + ): report.status = "FAIL" - report.status_extended = ( - f"Node {resource.node_name} uses only default security groups with overly permissive inbound rules." - ) + report.status_extended = f"Node {resource.node_name} uses only default security groups with overly permissive inbound rules." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py index 40319442d21..34e0865e0a1 100644 --- a/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py +++ b/prowler/providers/e2e/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py @@ -10,13 +10,9 @@ def execute(self) -> list[CheckReportE2e]: for volume in storage_client.block_volumes: report = CheckReportE2e(metadata=self.metadata(), resource=volume) report.status = "PASS" - report.status_extended = ( - f"Block volume {volume.name} is attached or not in an available orphaned state." - ) + report.status_extended = f"Block volume {volume.name} is attached or not in an available orphaned state." if volume.status == "Available" and not volume.is_attached: report.status = "FAIL" - report.status_extended = ( - f"Block volume {volume.name} is available and not attached to any node." - ) + report.status_extended = f"Block volume {volume.name} is available and not attached to any node." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py index 399e13146c0..f1597e95342 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py +++ b/prowler/providers/e2e/services/storage/storage_bucket_encryption_enabled/storage_bucket_encryption_enabled.py @@ -15,8 +15,6 @@ def execute(self) -> list[CheckReportE2e]: ) if not bucket.is_encryption_enabled: report.status = "FAIL" - report.status_extended = ( - f"Object storage bucket {bucket.name} does not have encryption enabled." - ) + report.status_extended = f"Object storage bucket {bucket.name} does not have encryption enabled." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py index f189c0f8071..f5e9476b7cb 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py +++ b/prowler/providers/e2e/services/storage/storage_bucket_lifecycle_configured/storage_bucket_lifecycle_configured.py @@ -10,13 +10,9 @@ def execute(self) -> list[CheckReportE2e]: for bucket in storage_client.buckets: report = CheckReportE2e(metadata=self.metadata(), resource=bucket) report.status = "PASS" - report.status_extended = ( - f"Object storage bucket {bucket.name} has lifecycle configuration enabled." - ) + report.status_extended = f"Object storage bucket {bucket.name} has lifecycle configuration enabled." if bucket.lifecycle_configuration_status != "Configured": report.status = "FAIL" - report.status_extended = ( - f"Object storage bucket {bucket.name} does not have lifecycle configuration enabled." - ) + report.status_extended = f"Object storage bucket {bucket.name} does not have lifecycle configuration enabled." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py index fcb4595df84..9a0a44f24f5 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py +++ b/prowler/providers/e2e/services/storage/storage_bucket_lock_enabled/storage_bucket_lock_enabled.py @@ -10,7 +10,9 @@ def execute(self) -> list[CheckReportE2e]: for bucket in storage_client.buckets: report = CheckReportE2e(metadata=self.metadata(), resource=bucket) report.status = "PASS" - report.status_extended = f"Object storage bucket {bucket.name} has object lock enabled." + report.status_extended = ( + f"Object storage bucket {bucket.name} has object lock enabled." + ) if not bucket.is_lock_enabled: report.status = "FAIL" report.status_extended = f"Object storage bucket {bucket.name} does not have object lock enabled." diff --git a/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py index b3100e163bb..5193b50e661 100644 --- a/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py +++ b/prowler/providers/e2e/services/storage/storage_bucket_versioning_enabled/storage_bucket_versioning_enabled.py @@ -15,8 +15,6 @@ def execute(self) -> list[CheckReportE2e]: ) if bucket.versioning_status != "Enabled": report.status = "FAIL" - report.status_extended = ( - f"Object storage bucket {bucket.name} does not have versioning enabled." - ) + report.status_extended = f"Object storage bucket {bucket.name} does not have versioning enabled." findings.append(report) return findings diff --git a/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py index 14ce42a28aa..34fece3b0b3 100644 --- a/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py +++ b/prowler/providers/e2e/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py @@ -13,6 +13,8 @@ def execute(self) -> list[CheckReportE2e]: report.status_extended = f"EFS volume {volume.name} has backup enabled." if not volume.is_backup_enabled: report.status = "FAIL" - report.status_extended = f"EFS volume {volume.name} does not have backup enabled." + report.status_extended = ( + f"EFS volume {volume.name} does not have backup enabled." + ) findings.append(report) return findings From 671a743b6a0e7e5943ef94ef5ab731bf66b6dc71 Mon Sep 17 00:00:00 2001 From: Deepak Dalvi Date: Sat, 20 Jun 2026 23:32:23 +0530 Subject: [PATCH 8/8] fix: codecov issues --- .github/workflows/sdk-tests.yml | 24 + tests/lib/outputs/finding_test.py | 34 + tests/lib/outputs/html/html_test.py | 17 + tests/lib/outputs/outputs_test.py | 20 + tests/lib/outputs/summary_table_test.py | 28 + tests/providers/e2e/e2e_checks_test.py | 844 ++++++++++++++++++++++++ tests/providers/e2e/e2e_fixtures.py | 25 +- 7 files changed, 991 insertions(+), 1 deletion(-) create mode 100644 tests/providers/e2e/e2e_checks_test.py diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 2952ebc2d50..cbf75b6e551 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -590,6 +590,30 @@ jobs: flags: prowler-py${{ matrix.python-version }}-stackit files: ./stackit_coverage.xml + # E2E Cloud Provider + - name: Check if E2E Cloud files changed + if: steps.check-changes.outputs.any_changed == 'true' + id: changed-e2e + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + files: | + ./prowler/**/e2e/** + ./tests/**/e2e/** + ./uv.lock + + - name: Run E2E Cloud tests + if: steps.changed-e2e.outputs.any_changed == 'true' + run: uv run pytest -n auto --cov=./prowler/providers/e2e --cov-report=xml:e2e_coverage.xml tests/providers/e2e + + - name: Upload E2E Cloud coverage to Codecov + if: steps.changed-e2e.outputs.any_changed == 'true' + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + flags: prowler-py${{ matrix.python-version }}-e2e + files: ./e2e_coverage.xml + # External Provider (dynamic loading) - name: Check if External Provider files changed if: steps.check-changes.outputs.any_changed == 'true' diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index 0f1cb14e258..168e4185119 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -1546,6 +1546,40 @@ def test_generate_output_stackit(self): assert finding_output.status == Status.PASS assert finding_output.muted is False + @patch( + "prowler.lib.outputs.finding.get_check_compliance", + new=mock_get_check_compliance, + ) + def test_generate_output_e2e(self): + provider = MagicMock() + provider.type = "e2e" + provider.identity.project_id = 12345 + + check_output = MagicMock() + check_output.resource_id = "test_resource_id" + check_output.resource_name = "test_resource_name" + check_output.resource_details = "" + check_output.location = "Delhi" + check_output.status = Status.PASS + check_output.status_extended = "mock_status_extended" + check_output.muted = False + check_output.check_metadata = mock_check_metadata(provider="e2e") + check_output.resource = {} + check_output.compliance = {} + + output_options = MagicMock() + output_options.unix_timestamp = True + + finding_output = Finding.generate_output(provider, check_output, output_options) + + assert isinstance(finding_output, Finding) + assert finding_output.auth_method == "api_key_and_bearer_token" + assert finding_output.account_uid == "12345" + assert finding_output.account_name == "12345" + assert finding_output.resource_name == "test_resource_name" + assert finding_output.resource_uid == "test_resource_id" + assert finding_output.region == "Delhi" + def test_transform_api_finding_stackit(self): provider = MagicMock() provider.type = "stackit" diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py index 536e40f8086..9e9b652474e 100644 --- a/tests/lib/outputs/html/html_test.py +++ b/tests/lib/outputs/html/html_test.py @@ -999,6 +999,23 @@ def test_stackit_get_assessment_summary_without_project_name(self): assert "Project ID: f033ea6d-8697-40eb-a60e-acfa9128480d" in summary assert "Project Name:" not in summary + def test_e2e_get_assessment_summary(self): + """Test E2E Cloud HTML assessment summary shows project and locations.""" + findings = [generate_finding_output()] + output = HTML(findings) + + provider = MagicMock() + provider.type = "e2e" + provider.identity.project_id = 12345 + provider.identity.locations = ["Delhi", "Chennai"] + + summary = output.get_assessment_summary(provider) + + assert "E2E Cloud Assessment Summary" in summary + assert "Project ID: 12345" in summary + assert "Locations: Delhi, Chennai" in summary + assert "API Key + Bearer Token" in summary + def test_process_markdown_bold_text(self): """Test that **text** is converted to text""" test_text = "This is **bold text** and this is **also bold**" diff --git a/tests/lib/outputs/outputs_test.py b/tests/lib/outputs/outputs_test.py index ca187178470..f5d8ae9cda3 100644 --- a/tests/lib/outputs/outputs_test.py +++ b/tests/lib/outputs/outputs_test.py @@ -1293,3 +1293,23 @@ def test_report_with_stackit_provider_fail(self): with mock.patch("builtins.print") as mocked_print: report([finding], provider, output_options) mocked_print.assert_called() + + def test_report_with_e2e_provider(self): + finding = MagicMock() + finding.status = "PASS" + finding.muted = False + finding.location = "Delhi" + finding.check_metadata.Provider = "e2e" + finding.status_extended = "Node has no public IP assigned" + + output_options = MagicMock() + output_options.verbose = True + output_options.status = ["PASS", "FAIL"] + output_options.fixer = False + + provider = MagicMock() + provider.type = "e2e" + + with mock.patch("builtins.print") as mocked_print: + report([finding], provider, output_options) + mocked_print.assert_called() diff --git a/tests/lib/outputs/summary_table_test.py b/tests/lib/outputs/summary_table_test.py index 0c842225cce..0d968834771 100644 --- a/tests/lib/outputs/summary_table_test.py +++ b/tests/lib/outputs/summary_table_test.py @@ -72,6 +72,34 @@ def test_stackit_summary_with_project_name(self, capsys): assert "Project" in captured.out assert "my-prod-env" in captured.out + def test_e2e_summary(self, capsys): + provider = SimpleNamespace( + type="e2e", + identity=SimpleNamespace(project_id=12345), + ) + output_options = SimpleNamespace( + output_directory="out", + output_filename="report", + output_modes=[], + ) + findings = [ + SimpleNamespace( + status="PASS", + muted=False, + check_metadata=SimpleNamespace( + ServiceName="node", + Provider="e2e", + Severity="high", + ), + ) + ] + + display_summary_table(findings, provider, output_options) + + captured = capsys.readouterr() + assert "Project" in captured.out + assert "12345" in captured.out + def test_stackit_summary_with_project_id_only(self, capsys): provider = SimpleNamespace( type="stackit", diff --git a/tests/providers/e2e/e2e_checks_test.py b/tests/providers/e2e/e2e_checks_test.py new file mode 100644 index 00000000000..fd9b702d2d8 --- /dev/null +++ b/tests/providers/e2e/e2e_checks_test.py @@ -0,0 +1,844 @@ +from prowler.providers.e2e.services.database.database_service import ( + DatabaseCluster, + DatabaseInstance, +) +from prowler.providers.e2e.services.loadbalancer.loadbalancer_service import LoadBalancer +from prowler.providers.e2e.services.network.network_service import ( + ReservedIp, + Vpc, + VpcTunnel, +) +from prowler.providers.e2e.services.node.nodes_service import Node +from prowler.providers.e2e.services.securitygroup.securitygroup_service import ( + NodeSecurityGroup, + SecurityGroupResource, + SecurityGroupRule, +) +from prowler.providers.e2e.services.storage.storage_service import ( + BlockVolume, + EfsVolume, + StorageBucket, +) +from tests.providers.e2e.e2e_fixtures import run_e2e_check + + +def _database_client_path(check_name: str) -> str: + return ( + f"prowler.providers.e2e.services.database.{check_name}.{check_name}" + ".database_client" + ) + + +def _network_client_path(check_name: str) -> str: + return ( + f"prowler.providers.e2e.services.network.{check_name}.{check_name}" + ".network_client" + ) + + +def _node_client_path(check_name: str) -> str: + return ( + f"prowler.providers.e2e.services.node.{check_name}.{check_name}.nodes_client" + ) + + +def _storage_client_path(check_name: str) -> str: + return ( + f"prowler.providers.e2e.services.storage.{check_name}.{check_name}" + ".storage_client" + ) + + +def _loadbalancer_client_path(check_name: str) -> str: + return ( + f"prowler.providers.e2e.services.loadbalancer.{check_name}.{check_name}" + ".loadbalancer_client" + ) + + +def _securitygroup_client_path(check_name: str) -> str: + return ( + f"prowler.providers.e2e.services.securitygroup.{check_name}.{check_name}" + ".securitygroup_client" + ) + + +class TestDatabaseChecks: + def test_database_cluster_backup_enabled(self): + check = "database_cluster_backup_enabled" + resources = [ + DatabaseCluster( + id="1", + name="ok", + location="Delhi", + backup_enabled=True, + ), + DatabaseCluster( + id="2", + name="bad", + location="Delhi", + backup_enabled=False, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.database.{check}.{check}", + _database_client_path(check), + "clusters", + resources, + ) + assert len(findings) == 2 + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_database_cluster_default_admin_username(self): + check = "database_cluster_default_admin_username" + resources = [ + DatabaseCluster( + id="1", + name="ok", + location="Delhi", + master_username="dbadmin", + ), + DatabaseCluster( + id="2", + name="bad", + location="Delhi", + master_username="admin", + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.database.{check}.{check}", + _database_client_path(check), + "clusters", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_database_cluster_ip_whitelist_configured(self): + check = "database_cluster_ip_whitelist_configured" + resources = [ + DatabaseCluster( + id="1", + name="ok", + location="Delhi", + master_has_public_ip=True, + whitelisted_ips=["203.0.113.0/24"], + ), + DatabaseCluster( + id="2", + name="bad", + location="Delhi", + master_has_public_ip=True, + whitelisted_ips=[], + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.database.{check}.{check}", + _database_client_path(check), + "clusters", + resources, + ) + assert len(findings) == 2 + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_database_cluster_public_ip_not_assigned(self): + check = "database_cluster_public_ip_not_assigned" + resources = [ + DatabaseCluster( + id="1", + name="ok", + location="Delhi", + master_has_public_ip=False, + ), + DatabaseCluster( + id="2", + name="bad", + location="Delhi", + master_has_public_ip=True, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.database.{check}.{check}", + _database_client_path(check), + "clusters", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_database_cluster_running(self): + check = "database_cluster_running" + resources = [ + DatabaseCluster( + id="1", + name="ok", + location="Delhi", + status="RUNNING", + ), + DatabaseCluster( + id="2", + name="bad", + location="Delhi", + status="STOPPED", + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.database.{check}.{check}", + _database_client_path(check), + "clusters", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_database_replica_public_ip_not_assigned(self): + check = "database_replica_public_ip_not_assigned" + resources = [ + DatabaseInstance( + id="1", + name="ok", + cluster_id="c1", + cluster_name="cluster", + location="Delhi", + role="replica", + has_public_ip=False, + ), + DatabaseInstance( + id="2", + name="bad", + cluster_id="c1", + cluster_name="cluster", + location="Delhi", + role="replica", + has_public_ip=True, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.database.{check}.{check}", + _database_client_path(check), + "instances", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + +class TestNetworkChecks: + def test_network_reserveip_floating_ip_unattached(self): + check = "network_reserveip_floating_ip_unattached" + resources = [ + ReservedIp( + reserve_id="1", + ip_address="1.2.3.4", + location="Delhi", + reserved_type="FloatingIP", + status="Attached", + floating_ip_attached_nodes_count=1, + ), + ReservedIp( + reserve_id="2", + ip_address="5.6.7.8", + location="Delhi", + reserved_type="FloatingIP", + status="Available", + floating_ip_attached_nodes_count=0, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.network.{check}.{check}", + _network_client_path(check), + "reserved_ips", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_network_reserveip_orphaned_public_ip(self): + check = "network_reserveip_orphaned_public_ip" + resources = [ + ReservedIp( + reserve_id="1", + ip_address="1.2.3.4", + location="Delhi", + reserved_type="PublicIP", + status="Attached", + vm_id=123, + ), + ReservedIp( + reserve_id="2", + ip_address="5.6.7.8", + location="Delhi", + reserved_type="PublicIP", + status="Available", + vm_id=None, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.network.{check}.{check}", + _network_client_path(check), + "reserved_ips", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_network_vpc_has_attached_nodes(self): + check = "network_vpc_has_attached_nodes" + resources = [ + Vpc(network_id="1", name="ok", location="Delhi", vm_count=2), + Vpc(network_id="2", name="bad", location="Delhi", vm_count=0), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.network.{check}.{check}", + _network_client_path(check), + "vpcs", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_network_vpc_peering_external_peer_disabled(self): + check = "network_vpc_peering_external_peer_disabled" + resources = [ + VpcTunnel( + id="1", + name="ok", + location="Delhi", + local_vpc_network_id="vpc-1", + local_vpc_name="vpc", + is_peer_vpc_external=False, + ), + VpcTunnel( + id="2", + name="bad", + location="Delhi", + local_vpc_network_id="vpc-2", + local_vpc_name="vpc", + is_peer_vpc_external=True, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.network.{check}.{check}", + _network_client_path(check), + "vpc_tunnels", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + +class TestNodeChecks: + def test_node_accidental_protection_enabled(self): + check = "node_accidental_protection_enabled" + resources = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + is_accidental_protection=True, + ), + Node( + id="2", + name="bad", + status="Running", + location="Delhi", + vm_id="2", + is_accidental_protection=False, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.node.{check}.{check}", + _node_client_path(check), + "nodes", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_node_compliance_enabled(self): + check = "node_compliance_enabled" + resources = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + is_node_compliance=True, + ), + Node( + id="2", + name="bad", + status="Running", + location="Delhi", + vm_id="2", + is_node_compliance=False, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.node.{check}.{check}", + _node_client_path(check), + "nodes", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_node_encryption_enabled(self): + check = "node_encryption_enabled" + resources = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + is_encryption_enabled=True, + ), + Node( + id="2", + name="bad", + status="Running", + location="Delhi", + vm_id="2", + is_encryption_enabled=False, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.node.{check}.{check}", + _node_client_path(check), + "nodes", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_node_rescue_mode_disabled(self): + check = "node_rescue_mode_disabled" + resources = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + rescue_mode_status="Disabled", + ), + Node( + id="2", + name="bad", + status="Running", + location="Delhi", + vm_id="2", + rescue_mode_status="Enabled", + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.node.{check}.{check}", + _node_client_path(check), + "nodes", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_node_vpc_attached(self): + check = "node_vpc_attached" + resources = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + is_vpc_attached=True, + ), + Node( + id="2", + name="bad", + status="Running", + location="Delhi", + vm_id="2", + is_vpc_attached=False, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.node.{check}.{check}", + _node_client_path(check), + "nodes", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + +class TestStorageChecks: + def test_storage_block_volume_not_orphaned(self): + check = "storage_block_volume_not_orphaned" + resources = [ + BlockVolume( + id="1", + name="ok", + location="Delhi", + is_attached=True, + ), + BlockVolume( + id="2", + name="bad", + location="Delhi", + status="Available", + is_attached=False, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.storage.{check}.{check}", + _storage_client_path(check), + "block_volumes", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_storage_bucket_encryption_enabled(self): + check = "storage_bucket_encryption_enabled" + resources = [ + StorageBucket( + id="1", + name="ok", + location="Delhi", + is_encryption_enabled=True, + ), + StorageBucket( + id="2", + name="bad", + location="Delhi", + is_encryption_enabled=False, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.storage.{check}.{check}", + _storage_client_path(check), + "buckets", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_storage_bucket_lifecycle_configured(self): + check = "storage_bucket_lifecycle_configured" + resources = [ + StorageBucket( + id="1", + name="ok", + location="Delhi", + lifecycle_configuration_status="Configured", + ), + StorageBucket( + id="2", + name="bad", + location="Delhi", + lifecycle_configuration_status="Disabled", + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.storage.{check}.{check}", + _storage_client_path(check), + "buckets", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_storage_bucket_lock_enabled(self): + check = "storage_bucket_lock_enabled" + resources = [ + StorageBucket( + id="1", + name="ok", + location="Delhi", + is_lock_enabled=True, + ), + StorageBucket( + id="2", + name="bad", + location="Delhi", + is_lock_enabled=False, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.storage.{check}.{check}", + _storage_client_path(check), + "buckets", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_storage_bucket_public_access_disabled(self): + check = "storage_bucket_public_access_disabled" + resources = [ + StorageBucket( + id="1", + name="ok", + location="Delhi", + is_public_access_enabled=False, + ), + StorageBucket( + id="2", + name="bad", + location="Delhi", + is_public_access_enabled=True, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.storage.{check}.{check}", + _storage_client_path(check), + "buckets", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_storage_bucket_versioning_enabled(self): + check = "storage_bucket_versioning_enabled" + resources = [ + StorageBucket( + id="1", + name="ok", + location="Delhi", + versioning_status="Enabled", + ), + StorageBucket( + id="2", + name="bad", + location="Delhi", + versioning_status="Off", + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.storage.{check}.{check}", + _storage_client_path(check), + "buckets", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_storage_efs_vpc_access_restricted(self): + check = "storage_efs_vpc_access_restricted" + resources = [ + EfsVolume( + id="1", + name="ok", + location="Delhi", + is_all_vpc_resources_allowed=False, + ), + EfsVolume( + id="2", + name="bad", + location="Delhi", + is_all_vpc_resources_allowed=True, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.storage.{check}.{check}", + _storage_client_path(check), + "efs_volumes", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + +class TestLoadBalancerChecks: + def test_loadbalancer_alb_https_uses_ssl_certificate(self): + check = "loadbalancer_alb_https_uses_ssl_certificate" + resources = [ + LoadBalancer( + id="1", + name="ok", + location="Delhi", + lb_mode="HTTPS", + ssl_certificate_id="cert-1", + ), + LoadBalancer( + id="2", + name="bad", + location="Delhi", + lb_mode="HTTPS", + ssl_certificate_id=None, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.loadbalancer.{check}.{check}", + _loadbalancer_client_path(check), + "load_balancers", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_loadbalancer_backend_health_check_enabled(self): + check = "loadbalancer_backend_health_check_enabled" + resources = [ + LoadBalancer( + id="1", + name="ok", + location="Delhi", + lb_mode="HTTP", + backends=[{"http_check": True}], + ), + LoadBalancer( + id="2", + name="bad", + location="Delhi", + lb_mode="HTTP", + backends=[{}], + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.loadbalancer.{check}.{check}", + _loadbalancer_client_path(check), + "load_balancers", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_loadbalancer_bitninja_enabled(self): + check = "loadbalancer_bitninja_enabled" + resources = [ + LoadBalancer( + id="1", + name="ok", + location="Delhi", + enable_bitninja=True, + ), + LoadBalancer( + id="2", + name="bad", + location="Delhi", + enable_bitninja=False, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.loadbalancer.{check}.{check}", + _loadbalancer_client_path(check), + "load_balancers", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + +class TestSecurityGroupChecks: + def test_securitygroup_no_all_traffic_rule(self): + check = "securitygroup_no_all_traffic_rule" + resources = [ + SecurityGroupResource( + id="1", + name="ok", + location="Delhi", + is_all_traffic_rule=False, + ), + SecurityGroupResource( + id="2", + name="bad", + location="Delhi", + is_all_traffic_rule=True, + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.securitygroup.{check}.{check}", + _securitygroup_client_path(check), + "security_groups", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_securitygroup_no_inbound_any_all_ports(self): + check = "securitygroup_no_inbound_any_all_ports" + permissive_rule = SecurityGroupRule( + id="1", + rule_type="inbound", + protocol_name="all", + port_range="", + network="any", + network_cidr="", + ) + safe_rule = SecurityGroupRule( + id="2", + rule_type="inbound", + protocol_name="tcp", + port_range="443", + network="203.0.113.0/24", + network_cidr="203.0.113.0/24", + ) + resources = [ + SecurityGroupResource( + id="1", + name="ok", + location="Delhi", + rules=[safe_rule], + ), + SecurityGroupResource( + id="2", + name="bad", + location="Delhi", + rules=[permissive_rule], + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.securitygroup.{check}.{check}", + _securitygroup_client_path(check), + "security_groups", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" + + def test_securitygroup_restrictive_default(self): + check = "securitygroup_restrictive_default" + permissive_rule = SecurityGroupRule( + id="1", + rule_type="inbound", + protocol_name="all", + port_range="", + network="any", + network_cidr="", + ) + resources = [ + NodeSecurityGroup( + node_id="1", + node_name="ok-node", + vm_id="vm-1", + location="Delhi", + security_group_id="sg-1", + name="custom", + is_default=False, + rules=[], + ), + NodeSecurityGroup( + node_id="2", + node_name="bad-node", + vm_id="vm-2", + location="Delhi", + security_group_id="sg-2", + name="default", + is_default=True, + rules=[permissive_rule], + ), + ] + findings = run_e2e_check( + f"prowler.providers.e2e.services.securitygroup.{check}.{check}", + _securitygroup_client_path(check), + "node_security_groups", + resources, + ) + assert findings[0].status == "PASS" + assert findings[1].status == "FAIL" diff --git a/tests/providers/e2e/e2e_fixtures.py b/tests/providers/e2e/e2e_fixtures.py index 59973b1324f..dadf6917bde 100644 --- a/tests/providers/e2e/e2e_fixtures.py +++ b/tests/providers/e2e/e2e_fixtures.py @@ -1,9 +1,32 @@ -from unittest.mock import MagicMock +import importlib +import sys +from unittest.mock import MagicMock, patch from prowler.providers.e2e.e2e_provider import E2eProvider from prowler.providers.e2e.models import E2eIdentityInfo, E2eSession +def run_e2e_check( + check_module_path: str, client_patch_path: str, client_attr: str, resources: list +): + """Execute an E2E check with mocked client resources.""" + check_class_name = check_module_path.rsplit(".", 1)[-1] + client = MagicMock() + setattr(client, client_attr, resources) + + with patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2e_provider(), + ): + if check_module_path in sys.modules: + module = importlib.reload(sys.modules[check_module_path]) + else: + module = importlib.import_module(check_module_path) + + with patch(client_patch_path, new=client): + return getattr(module, check_class_name)().execute() + + def set_mocked_e2e_provider( project_id: int = 12345, locations: list[str] | None = None,