From d874fc573d6791172168e72c7689a4ff456b65c4 Mon Sep 17 00:00:00 2001 From: UniCode Date: Wed, 8 Jul 2026 17:51:27 +0530 Subject: [PATCH] feat(e2e): provider for e2e cloud (#11654) Co-authored-by: Daniel Barranquero --- .github/workflows/sdk-tests.yml | 24 ++ README.md | 1 + docs/docs.json | 7 + docs/introduction.mdx | 1 + .../providers/e2enetworks/authentication.mdx | 99 +++++++ .../getting-started-e2enetworks.mdx | 66 +++++ prowler/__main__.py | 5 + .../changelog.d/e2enetworks-provider.added.md | 1 + prowler/config/config.py | 1 + prowler/config/config.yaml | 5 + .../config/e2enetworks_mutelist_example.yaml | 23 ++ prowler/config/schema/e2enetworks.py | 22 ++ prowler/config/schema/registry.py | 2 + prowler/lib/check/models.py | 23 ++ prowler/lib/cli/parser.py | 8 +- .../compliance/universal/universal_output.py | 1 + prowler/lib/outputs/finding.py | 12 + prowler/lib/outputs/html/html.py | 41 +++ prowler/lib/outputs/outputs.py | 2 + prowler/lib/outputs/summary_table.py | 3 + prowler/providers/common/provider.py | 10 + prowler/providers/e2enetworks/__init__.py | 0 .../e2enetworks/e2enetworks_provider.py | 258 ++++++++++++++++++ .../e2enetworks/exceptions/__init__.py | 0 .../e2enetworks/exceptions/exceptions.py | 93 +++++++ prowler/providers/e2enetworks/lib/__init__.py | 0 .../providers/e2enetworks/lib/api/__init__.py | 0 .../providers/e2enetworks/lib/api/client.py | 109 ++++++++ .../e2enetworks/lib/arguments/__init__.py | 0 .../e2enetworks/lib/arguments/arguments.py | 68 +++++ .../e2enetworks/lib/mutelist/__init__.py | 0 .../e2enetworks/lib/mutelist/mutelist.py | 27 ++ .../e2enetworks/lib/service/__init__.py | 0 .../e2enetworks/lib/service/service.py | 19 ++ prowler/providers/e2enetworks/models.py | 53 ++++ .../e2enetworks/services/__init__.py | 0 .../e2enetworks/services/database/__init__.py | 0 .../services/database/database_client.py | 4 + .../__init__.py | 0 ...abase_cluster_backup_enabled.metadata.json | 36 +++ .../database_cluster_backup_enabled.py | 24 ++ .../__init__.py | 0 ...uster_default_admin_username.metadata.json | 34 +++ ...database_cluster_default_admin_username.py | 22 ++ .../__init__.py | 0 ...ster_ip_whitelist_configured.metadata.json | 36 +++ ...atabase_cluster_ip_whitelist_configured.py | 24 ++ .../__init__.py | 0 ...uster_public_ip_not_assigned.metadata.json | 36 +++ ...database_cluster_public_ip_not_assigned.py | 20 ++ .../database_cluster_running/__init__.py | 0 .../database_cluster_running.metadata.json | 34 +++ .../database_cluster_running.py | 20 ++ .../database_cluster_ssl_enabled/__init__.py | 0 ...database_cluster_ssl_enabled.metadata.json | 36 +++ .../database_cluster_ssl_enabled.py | 22 ++ .../__init__.py | 0 ...plica_public_ip_not_assigned.metadata.json | 36 +++ ...database_replica_public_ip_not_assigned.py | 26 ++ .../services/database/database_service.py | 164 +++++++++++ .../services/loadbalancer/__init__.py | 0 .../__init__.py | 0 ...b_https_uses_ssl_certificate.metadata.json | 36 +++ ...balancer_alb_https_uses_ssl_certificate.py | 25 ++ .../__init__.py | 0 ...backend_health_check_enabled.metadata.json | 36 +++ ...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 | 96 +++++++ .../e2enetworks/services/network/__init__.py | 0 .../services/network/network_client.py | 4 + .../__init__.py | 0 ...rveip_floating_ip_unattached.metadata.json | 34 +++ ...etwork_reserveip_floating_ip_unattached.py | 24 ++ .../__init__.py | 0 ...reserveip_orphaned_public_ip.metadata.json | 36 +++ .../network_reserveip_orphaned_public_ip.py | 24 ++ .../services/network/network_service.py | 158 +++++++++++ .../__init__.py | 0 ...twork_vpc_has_attached_nodes.metadata.json | 34 +++ .../network_vpc_has_attached_nodes.py | 20 ++ .../network/network_vpc_is_active/__init__.py | 0 .../network_vpc_is_active.metadata.json | 34 +++ .../network_vpc_is_active.py | 20 ++ .../__init__.py | 0 ...ering_external_peer_disabled.metadata.json | 36 +++ ...work_vpc_peering_external_peer_disabled.py | 22 ++ .../e2enetworks/services/node/__init__.py | 0 .../__init__.py | 0 ...ccidental_protection_enabled.metadata.json | 36 +++ .../node_accidental_protection_enabled.py | 22 ++ .../e2enetworks/services/node/node_client.py | 4 + .../node/node_compliance_enabled/__init__.py | 0 .../node_compliance_enabled.metadata.json | 34 +++ .../node_compliance_enabled.py | 20 ++ .../node/node_encryption_enabled/__init__.py | 0 .../node_encryption_enabled.metadata.json | 36 +++ .../node_encryption_enabled.py | 20 ++ .../node_public_ip_not_assigned/__init__.py | 0 .../node_public_ip_not_assigned.metadata.json | 36 +++ .../node_public_ip_not_assigned.py | 18 ++ .../node_rescue_mode_disabled/__init__.py | 0 .../node_rescue_mode_disabled.metadata.json | 34 +++ .../node_rescue_mode_disabled.py | 20 ++ .../e2enetworks/services/node/node_service.py | 109 ++++++++ .../node/node_vpc_attached/__init__.py | 0 .../node_vpc_attached.metadata.json | 34 +++ .../node_vpc_attached/node_vpc_attached.py | 18 ++ .../services/securitygroup/__init__.py | 0 .../securitygroup/securitygroup_client.py | 6 + .../__init__.py | 0 ...itygroup_no_all_traffic_rule.metadata.json | 36 +++ .../securitygroup_no_all_traffic_rule.py | 24 ++ .../__init__.py | 0 ...oup_no_inbound_any_all_ports.metadata.json | 36 +++ .../securitygroup_no_inbound_any_all_ports.py | 35 +++ .../__init__.py | 0 ...itygroup_restrictive_default.metadata.json | 37 +++ .../securitygroup_restrictive_default.py | 49 ++++ .../securitygroup/securitygroup_service.py | 147 ++++++++++ .../e2enetworks/services/storage/__init__.py | 0 .../__init__.py | 0 ...ge_block_volume_not_orphaned.metadata.json | 34 +++ .../storage_block_volume_not_orphaned.py | 18 ++ .../services/storage/storage_client.py | 4 + .../storage_efs_backup_enabled/__init__.py | 0 .../storage_efs_backup_enabled.metadata.json | 36 +++ .../storage_efs_backup_enabled.py | 20 ++ .../__init__.py | 0 ...ge_efs_vpc_access_restricted.metadata.json | 34 +++ .../storage_efs_vpc_access_restricted.py | 22 ++ .../services/storage/storage_service.py | 158 +++++++++++ .../schema/other_providers_schema_test.py | 12 + tests/lib/cli/parser_test.py | 3 +- tests/lib/outputs/finding_test.py | 34 +++ tests/lib/outputs/html/html_test.py | 32 +++ tests/lib/outputs/outputs_test.py | 20 ++ tests/lib/outputs/summary_table_test.py | 28 ++ .../e2enetworks/e2enetworks_fixtures.py | 55 ++++ .../e2enetworks/e2enetworks_metadata_test.py | 37 +++ .../e2enetworks/e2enetworks_provider_test.py | 93 +++++++ .../lib/api/e2enetworks_api_client_test.py | 43 +++ .../arguments/e2enetworks_arguments_test.py | 44 +++ .../lib/mutelist/e2enetworks_mutelist_test.py | 65 +++++ .../database_cluster_backup_enabled_test.py | 68 +++++ ...ase_cluster_default_admin_username_test.py | 72 +++++ ...se_cluster_ip_whitelist_configured_test.py | 80 ++++++ ...ase_cluster_public_ip_not_assigned_test.py | 72 +++++ .../database_cluster_running_test.py | 68 +++++ .../database_cluster_ssl_enabled_test.py | 72 +++++ ...ase_replica_public_ip_not_assigned_test.py | 84 ++++++ .../e2enetworks_database_service_test.py | 70 +++++ .../e2enetworks_loadbalancer_service_test.py | 81 ++++++ ...cer_alb_https_uses_ssl_certificate_test.py | 80 ++++++ ...ancer_backend_health_check_enabled_test.py | 76 ++++++ .../loadbalancer_bitninja_enabled_test.py | 68 +++++ .../e2enetworks_network_service_test.py | 74 +++++ ...k_reserveip_floating_ip_unattached_test.py | 82 ++++++ ...twork_reserveip_orphaned_public_ip_test.py | 82 ++++++ .../network_vpc_has_attached_nodes_test.py | 68 +++++ .../network_vpc_is_active_test.py | 80 ++++++ ...vpc_peering_external_peer_disabled_test.py | 82 ++++++ .../node/e2enetworks_node_service_test.py | 94 +++++++ ...node_accidental_protection_enabled_test.py | 82 ++++++ .../node_compliance_enabled_test.py | 82 ++++++ .../node_encryption_enabled_test.py | 82 ++++++ .../node_public_ip_not_assigned_test.py | 82 ++++++ .../node_rescue_mode_disabled_test.py | 82 ++++++ .../node_vpc_attached_test.py | 82 ++++++ .../e2enetworks_securitygroup_service_test.py | 71 +++++ .../securitygroup_no_all_traffic_rule_test.py | 72 +++++ ...ritygroup_no_inbound_any_all_ports_test.py | 97 +++++++ .../securitygroup_restrictive_default_test.py | 96 +++++++ .../e2enetworks_storage_service_test.py | 50 ++++ .../storage_block_volume_not_orphaned_test.py | 74 +++++ .../storage_efs_backup_enabled_test.py | 68 +++++ .../storage_efs_vpc_access_restricted_test.py | 72 +++++ 180 files changed, 6428 insertions(+), 4 deletions(-) create mode 100644 docs/user-guide/providers/e2enetworks/authentication.mdx create mode 100644 docs/user-guide/providers/e2enetworks/getting-started-e2enetworks.mdx create mode 100644 prowler/changelog.d/e2enetworks-provider.added.md create mode 100644 prowler/config/e2enetworks_mutelist_example.yaml create mode 100644 prowler/config/schema/e2enetworks.py create mode 100644 prowler/providers/e2enetworks/__init__.py create mode 100644 prowler/providers/e2enetworks/e2enetworks_provider.py create mode 100644 prowler/providers/e2enetworks/exceptions/__init__.py create mode 100644 prowler/providers/e2enetworks/exceptions/exceptions.py create mode 100644 prowler/providers/e2enetworks/lib/__init__.py create mode 100644 prowler/providers/e2enetworks/lib/api/__init__.py create mode 100644 prowler/providers/e2enetworks/lib/api/client.py create mode 100644 prowler/providers/e2enetworks/lib/arguments/__init__.py create mode 100644 prowler/providers/e2enetworks/lib/arguments/arguments.py create mode 100644 prowler/providers/e2enetworks/lib/mutelist/__init__.py create mode 100644 prowler/providers/e2enetworks/lib/mutelist/mutelist.py create mode 100644 prowler/providers/e2enetworks/lib/service/__init__.py create mode 100644 prowler/providers/e2enetworks/lib/service/service.py create mode 100644 prowler/providers/e2enetworks/models.py create mode 100644 prowler/providers/e2enetworks/services/__init__.py create mode 100644 prowler/providers/e2enetworks/services/database/__init__.py create mode 100644 prowler/providers/e2enetworks/services/database/database_client.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/__init__.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_default_admin_username/__init__.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/__init__.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/__init__.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_running/__init__.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running.metadata.json create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_ssl_enabled/__init__.py create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json create mode 100644 prowler/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py create mode 100644 prowler/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/__init__.py create mode 100644 prowler/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json create mode 100644 prowler/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py create mode 100644 prowler/providers/e2enetworks/services/database/database_service.py create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/__init__.py create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/__init__.py create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/__init__.py create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/__init__.py create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_client.py create mode 100644 prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_service.py create mode 100644 prowler/providers/e2enetworks/services/network/__init__.py create mode 100644 prowler/providers/e2enetworks/services/network/network_client.py create mode 100644 prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/__init__.py create mode 100644 prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json create mode 100644 prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py create mode 100644 prowler/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/__init__.py create mode 100644 prowler/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json create mode 100644 prowler/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py create mode 100644 prowler/providers/e2enetworks/services/network/network_service.py create mode 100644 prowler/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/__init__.py create mode 100644 prowler/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json create mode 100644 prowler/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py create mode 100644 prowler/providers/e2enetworks/services/network/network_vpc_is_active/__init__.py create mode 100644 prowler/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json create mode 100644 prowler/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active.py create mode 100644 prowler/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/__init__.py create mode 100644 prowler/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json create mode 100644 prowler/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py create mode 100644 prowler/providers/e2enetworks/services/node/__init__.py create mode 100644 prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/__init__.py create mode 100644 prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json create mode 100644 prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py create mode 100644 prowler/providers/e2enetworks/services/node/node_client.py create mode 100644 prowler/providers/e2enetworks/services/node/node_compliance_enabled/__init__.py create mode 100644 prowler/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json create mode 100644 prowler/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled.py create mode 100644 prowler/providers/e2enetworks/services/node/node_encryption_enabled/__init__.py create mode 100644 prowler/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json create mode 100644 prowler/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled.py create mode 100644 prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/__init__.py create mode 100644 prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json create mode 100644 prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py create mode 100644 prowler/providers/e2enetworks/services/node/node_rescue_mode_disabled/__init__.py create mode 100644 prowler/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json create mode 100644 prowler/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py create mode 100644 prowler/providers/e2enetworks/services/node/node_service.py create mode 100644 prowler/providers/e2enetworks/services/node/node_vpc_attached/__init__.py create mode 100644 prowler/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached.metadata.json create mode 100644 prowler/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached.py create mode 100644 prowler/providers/e2enetworks/services/securitygroup/__init__.py create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_client.py create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/__init__.py create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/__init__.py create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/__init__.py create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py create mode 100644 prowler/providers/e2enetworks/services/securitygroup/securitygroup_service.py create mode 100644 prowler/providers/e2enetworks/services/storage/__init__.py create mode 100644 prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/__init__.py create mode 100644 prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json create mode 100644 prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py create mode 100644 prowler/providers/e2enetworks/services/storage/storage_client.py create mode 100644 prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/__init__.py create mode 100644 prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json create mode 100644 prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py create mode 100644 prowler/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/__init__.py create mode 100644 prowler/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json create mode 100644 prowler/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py create mode 100644 prowler/providers/e2enetworks/services/storage/storage_service.py create mode 100644 tests/providers/e2enetworks/e2enetworks_fixtures.py create mode 100644 tests/providers/e2enetworks/e2enetworks_metadata_test.py create mode 100644 tests/providers/e2enetworks/e2enetworks_provider_test.py create mode 100644 tests/providers/e2enetworks/lib/api/e2enetworks_api_client_test.py create mode 100644 tests/providers/e2enetworks/lib/arguments/e2enetworks_arguments_test.py create mode 100644 tests/providers/e2enetworks/lib/mutelist/e2enetworks_mutelist_test.py create mode 100644 tests/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled_test.py create mode 100644 tests/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username_test.py create mode 100644 tests/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured_test.py create mode 100644 tests/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned_test.py create mode 100644 tests/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running_test.py create mode 100644 tests/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled_test.py create mode 100644 tests/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned_test.py create mode 100644 tests/providers/e2enetworks/services/database/e2enetworks_database_service_test.py create mode 100644 tests/providers/e2enetworks/services/loadbalancer/e2enetworks_loadbalancer_service_test.py create mode 100644 tests/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate_test.py create mode 100644 tests/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled_test.py create mode 100644 tests/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled_test.py create mode 100644 tests/providers/e2enetworks/services/network/e2enetworks_network_service_test.py create mode 100644 tests/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached_test.py create mode 100644 tests/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip_test.py create mode 100644 tests/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes_test.py create mode 100644 tests/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active_test.py create mode 100644 tests/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled_test.py create mode 100644 tests/providers/e2enetworks/services/node/e2enetworks_node_service_test.py create mode 100644 tests/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled_test.py create mode 100644 tests/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled_test.py create mode 100644 tests/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled_test.py create mode 100644 tests/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned_test.py create mode 100644 tests/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled_test.py create mode 100644 tests/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached_test.py create mode 100644 tests/providers/e2enetworks/services/securitygroup/e2enetworks_securitygroup_service_test.py create mode 100644 tests/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule_test.py create mode 100644 tests/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports_test.py create mode 100644 tests/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default_test.py create mode 100644 tests/providers/e2enetworks/services/storage/e2enetworks_storage_service_test.py create mode 100644 tests/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned_test.py create mode 100644 tests/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled_test.py create mode 100644 tests/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted_test.py diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 2fe6b1d536..3f312e8f0b 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -615,6 +615,30 @@ jobs: flags: prowler-py${{ matrix.python-version }}-linode files: ./linode_coverage.xml + # E2E Networks Provider + - name: Check if E2E Networks files changed + if: steps.check-changes.outputs.any_changed == 'true' + id: changed-e2enetworks + uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47.0.6 + with: + files: | + ./prowler/**/e2enetworks/** + ./tests/**/e2enetworks/** + ./uv.lock + + - name: Run E2E Networks tests + if: steps.changed-e2enetworks.outputs.any_changed == 'true' + run: uv run pytest -n auto --cov=./prowler/providers/e2enetworks --cov-report=xml:e2enetworks_coverage.xml tests/providers/e2enetworks + + - name: Upload E2E Networks coverage to Codecov + if: steps.changed-e2enetworks.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 }}-e2enetworks + files: ./e2enetworks_coverage.xml + # External Provider (dynamic loading) - name: Check if External Provider files changed if: steps.check-changes.outputs.any_changed == 'true' diff --git a/README.md b/README.md index e2314d8a69..50df95484f 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,7 @@ Every AWS provider scan will enqueue an Attack Paths ingestion job automatically | Vercel | 26 | 6 | 1 | 8 | Official | UI, API, CLI | | Okta | 29 | 8 | 2 | 2 | Official | UI, API, CLI | | Linode [Contact us](https://prowler.com/contact) | 10 | 3 | 1 | 4 | Unofficial | CLI | +| E2E Networks [Contact us](https://prowler.com/contact) | 27 | 6 | 0 | 2 | Unofficial | CLI | | Scaleway [Contact us](https://prowler.com/contact) | 1 | 1 | 1 | 1 | Unofficial | CLI | | StackIT [Contact us](https://prowler.com/contact) | 7 | 2 | 1 | 3 | Unofficial | CLI | | NHN | 6 | 2 | 2 | 0 | Unofficial | CLI | diff --git a/docs/docs.json b/docs/docs.json index 3cb0b3dec9..8c5f2d87fb 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -378,6 +378,13 @@ "user-guide/providers/linode/getting-started-linode", "user-guide/providers/linode/authentication" ] + }, + { + "group": "E2E Networks", + "pages": [ + "user-guide/providers/e2enetworks/getting-started-e2enetworks", + "user-guide/providers/e2enetworks/authentication" + ] } ] }, diff --git a/docs/introduction.mdx b/docs/introduction.mdx index 51a27eb555..c31ec2c864 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -31,6 +31,7 @@ Prowler supports a wide range of providers organized by category: | [AWS](/user-guide/providers/aws/getting-started-aws) | Official | Accounts | UI, API, CLI | | [Azure](/user-guide/providers/azure/getting-started-azure) | Official | Subscriptions | UI, API, CLI | | [Cloudflare](/user-guide/providers/cloudflare/getting-started-cloudflare) | Official | Accounts | UI, API, CLI | +| [E2E Networks](/user-guide/providers/e2enetworks/getting-started-e2enetworks) | [Contact us](https://prowler.com/contact) | Projects | CLI | | [Google Cloud](/user-guide/providers/gcp/getting-started-gcp) | Official | Projects | UI, API, CLI | | [Linode](/user-guide/providers/linode/getting-started-linode) | [Contact us](https://prowler.com/contact) | Accounts | CLI | | **NHN** | [Contact us](https://prowler.com/contact) | Tenants | CLI | diff --git a/docs/user-guide/providers/e2enetworks/authentication.mdx b/docs/user-guide/providers/e2enetworks/authentication.mdx new file mode 100644 index 0000000000..67d031861d --- /dev/null +++ b/docs/user-guide/providers/e2enetworks/authentication.mdx @@ -0,0 +1,99 @@ +--- +title: "E2E Networks Authentication in Prowler" +--- + +import { VersionBadge } from "/snippets/version-badge.mdx" + + + +Prowler for E2E Networks authenticates against the E2E Networks MyAccount API using an **API key**, an **auth token**, and a numeric **project ID**. Set the API key and auth token as environment variables to avoid exposing secrets in shell history or process listings. Compatibility CLI flags are still accepted, but Prowler warns when secret values are passed directly on the command line. + +## Required Credentials + +Prowler requires read access to the E2E Networks project. The following values are needed: + +| Credential | Environment Variable | Description | +|------------|----------------------|-------------| +| API Key | `E2E_NETWORKS_API_KEY` | Identifies the MyAccount API key used for requests | +| Auth Token | `E2E_NETWORKS_AUTH_TOKEN` | Bearer token authorizing the API key | +| Project ID | `E2E_NETWORKS_PROJECT_ID` | Numeric ID of the project to scan | +| Region | `E2E_NETWORKS_REGION` | Optional region to scan (defaults to all regions: Delhi, Chennai) | + + +The project ID must be an integer. A missing or non-numeric project ID stops the scan before any resource is read. + + +--- + +## API Credentials + +### Step 1: Create the API Key and Auth Token + +1. Log into the [E2E Networks MyAccount portal](https://myaccount.e2enetworks.com). +2. Open the **API Tokens** section under the account settings. +3. Create a new API token and copy both the API key and the auth token. +4. Copy the values immediately — the auth token is not shown again. + +### Step 2: Find the Project ID + +Select the target project in MyAccount and copy its numeric project ID from the project settings or the API context. + +### Step 3: Configure Authentication + +Export the credentials as environment variables: + +```bash +export E2E_NETWORKS_API_KEY="your-api-key" +export E2E_NETWORKS_AUTH_TOKEN="your-auth-token" +export E2E_NETWORKS_PROJECT_ID="your-project-id" +``` + +Then run Prowler: + +```bash +prowler e2enetworks +``` + +--- + +## Verifying Authentication + +To confirm that Prowler can reach the E2E Networks project, run a scan against a single location: + +```bash +prowler e2enetworks --region Delhi +``` + +A successful run reports findings for the discovered resources. A failed run displays an error message indicating the credential or connectivity issue. + +--- + +## CI/CD Integration + +For automated pipelines, set the credentials as secret environment variables: + +**GitHub Actions:** + +```yaml +env: + E2E_NETWORKS_API_KEY: ${{ secrets.E2E_NETWORKS_API_KEY }} + E2E_NETWORKS_AUTH_TOKEN: ${{ secrets.E2E_NETWORKS_AUTH_TOKEN }} + E2E_NETWORKS_PROJECT_ID: ${{ secrets.E2E_NETWORKS_PROJECT_ID }} + +steps: + - name: Run Prowler + run: prowler e2enetworks +``` + +**GitLab CI:** + +```yaml +variables: + E2E_NETWORKS_API_KEY: $E2E_NETWORKS_API_KEY + E2E_NETWORKS_AUTH_TOKEN: $E2E_NETWORKS_AUTH_TOKEN + E2E_NETWORKS_PROJECT_ID: $E2E_NETWORKS_PROJECT_ID + +prowler_scan: + script: + - prowler e2enetworks +``` diff --git a/docs/user-guide/providers/e2enetworks/getting-started-e2enetworks.mdx b/docs/user-guide/providers/e2enetworks/getting-started-e2enetworks.mdx new file mode 100644 index 0000000000..6b67f4e32c --- /dev/null +++ b/docs/user-guide/providers/e2enetworks/getting-started-e2enetworks.mdx @@ -0,0 +1,66 @@ +--- +title: 'Getting Started With E2E Networks on Prowler' +--- + +import { VersionBadge } from "/snippets/version-badge.mdx" + + + +Prowler for E2E Networks scans E2E Networks infrastructure for security misconfigurations across compute nodes, networking, security groups, load balancers, storage, and managed databases. + + +E2E Networks support in Prowler is community-maintained. For commercial support or to request additional service coverage, [contact us](https://prowler.com/contact). + + +## Prerequisites + +Set up authentication for E2E Networks with the [E2E Networks Authentication](/user-guide/providers/e2enetworks/authentication) guide before starting: + +- Create an E2E Networks API key and auth token from the MyAccount API Tokens section +- Note the numeric project ID of the project to scan + +## Prowler CLI + +### Run Prowler for E2E Networks + +Once authenticated, export the credentials as environment variables and run Prowler for E2E Networks. Environment variables keep secrets out of shell history and process listings: + +```bash +export E2E_NETWORKS_API_KEY="your-api-key" +export E2E_NETWORKS_AUTH_TOKEN="your-auth-token" +export E2E_NETWORKS_PROJECT_ID="your-project-id" +prowler e2enetworks +``` + +### Run Specific Checks + +```bash +prowler e2enetworks --checks node_encryption_enabled storage_block_volume_not_orphaned +``` + +### Run a Specific Service + +```bash +prowler e2enetworks --services node +``` + +### Scan Specific Regions + +Use `--region` (aliases `--filter-region` and `-f`) to limit the scan to one or more E2E Networks regions. When the flag is omitted, Prowler reads `E2E_NETWORKS_REGION` and otherwise scans all regions (Delhi, Chennai). + +```bash +prowler e2enetworks --region Delhi Chennai +``` + +## Available Services + +Prowler for E2E Networks currently supports the following services: + +| Service | Description | +|---------|-------------| +| `database` | Managed database clusters and their backup, network, and encryption settings | +| `loadbalancer` | Application load balancers and their TLS, health-check, and protection settings | +| `network` | Virtual Private Clouds, peering, and reserved public IP addresses | +| `node` | Compute nodes and their encryption, protection, and network configuration | +| `securitygroup` | Security groups and their inbound and default traffic rules | +| `storage` | Block volumes and file storage (EFS/EPFS) with their protection settings | diff --git a/prowler/__main__.py b/prowler/__main__.py index d4c925f74f..12f4b24cac 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -140,6 +140,7 @@ from prowler.providers.azure.models import AzureOutputOptions from prowler.providers.cloudflare.models import CloudflareOutputOptions from prowler.providers.common.provider import Provider from prowler.providers.common.quick_inventory import run_provider_quick_inventory +from prowler.providers.e2enetworks.models import E2eNetworksOutputOptions from prowler.providers.gcp.models import GCPOutputOptions from prowler.providers.github.models import GithubOutputOptions from prowler.providers.googleworkspace.models import GoogleWorkspaceOutputOptions @@ -432,6 +433,10 @@ def prowler(): output_options = VercelOutputOptions( args, bulk_checks_metadata, global_provider.identity ) + elif provider == "e2enetworks": + output_options = E2eNetworksOutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) elif provider == "okta": output_options = OktaOutputOptions( args, bulk_checks_metadata, global_provider.identity diff --git a/prowler/changelog.d/e2enetworks-provider.added.md b/prowler/changelog.d/e2enetworks-provider.added.md new file mode 100644 index 0000000000..9185c9d51c --- /dev/null +++ b/prowler/changelog.d/e2enetworks-provider.added.md @@ -0,0 +1 @@ +E2E Networks provider with 27 checks across compute nodes, networking, security groups, load balancers, block/file storage, and managed databases diff --git a/prowler/config/config.py b/prowler/config/config.py index b98c11cbad..5bec954def 100644 --- a/prowler/config/config.py +++ b/prowler/config/config.py @@ -81,6 +81,7 @@ class Provider(str, Enum): OKTA = "okta" STACKIT = "stackit" LINODE = "linode" + E2ENETWORKS = "e2enetworks" # Compliance diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 3a059e11d0..6d2803be56 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -801,3 +801,8 @@ openstack: # Regex patterns whose matches are excluded from secret scanning of # resource metadata. secrets_ignore_patterns: [] + +# E2E Networks Configuration +e2enetworks: + # load_balancer_bitninja_enabled + require_bitninja_on_load_balancers: false diff --git a/prowler/config/e2enetworks_mutelist_example.yaml b/prowler/config/e2enetworks_mutelist_example.yaml new file mode 100644 index 0000000000..40443287a6 --- /dev/null +++ b/prowler/config/e2enetworks_mutelist_example.yaml @@ -0,0 +1,23 @@ +### Account, Check and/or Region can be * to apply for all the cases. +### Account == * (E2E Networks findings are matched by resource, so use * to apply to all) +### Region == or * for all locations +### Resources and tags are lists that can have either Regex or Keywords. +### Tags is an optional list that matches on tuples of 'key=value' and are "ANDed" together. +### Use an alternation Regex to match one of multiple tags with "ORed" logic. +### For each check you can except Accounts, Regions, Resources and/or Tags. +########################### MUTELIST EXAMPLE ########################### +Mutelist: + Accounts: + "*": + Checks: + "node_public_ip_not_assigned": + Regions: + - "Delhi" + Resources: + - "example-node-id" + - "bastion-.*" + "storage_bucket_public_access_disabled": + Regions: + - "*" + Resources: + - "public-assets-bucket" diff --git a/prowler/config/schema/e2enetworks.py b/prowler/config/schema/e2enetworks.py new file mode 100644 index 0000000000..57e29e32d6 --- /dev/null +++ b/prowler/config/schema/e2enetworks.py @@ -0,0 +1,22 @@ +"""E2E Networks provider config schema.""" + +from typing import Optional + +from pydantic import Field + +from prowler.config.schema.base import ProviderConfigBase + + +class E2eNetworksProviderConfig(ProviderConfigBase): + """E2E Networks provider configuration schema. + + Defines optional configuration parameters for E2E Networks security checks. + """ + + require_bitninja_on_load_balancers: Optional[bool] = Field( + default=None, + description=( + "Whether BitNinja protection is required on load balancers for the " + "loadbalancer_bitninja_enabled check." + ), + ) diff --git a/prowler/config/schema/registry.py b/prowler/config/schema/registry.py index d34a9b866a..074ccc264d 100644 --- a/prowler/config/schema/registry.py +++ b/prowler/config/schema/registry.py @@ -9,6 +9,7 @@ from prowler.config.schema.aws import AWSProviderConfig from prowler.config.schema.azure import AzureProviderConfig from prowler.config.schema.base import ProviderConfigBase from prowler.config.schema.cloudflare import CloudflareProviderConfig +from prowler.config.schema.e2enetworks import E2eNetworksProviderConfig from prowler.config.schema.gcp import GCPProviderConfig from prowler.config.schema.github import GitHubProviderConfig from prowler.config.schema.kubernetes import KubernetesProviderConfig @@ -27,6 +28,7 @@ SCHEMAS: dict[str, type[ProviderConfigBase]] = { "github": GitHubProviderConfig, "mongodbatlas": MongoDBAtlasProviderConfig, "cloudflare": CloudflareProviderConfig, + "e2enetworks": E2eNetworksProviderConfig, "vercel": VercelProviderConfig, "okta": OktaProviderConfig, "alibabacloud": AlibabaCloudProviderConfig, diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index aa16d7969b..5f92ecf481 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -1286,6 +1286,29 @@ class CheckReportNHN(Check_Report): self.location = getattr(resource, "location", "kr1") +@dataclass +class CheckReportE2eNetworks(Check_Report): + """Contains the E2E Networks Check's finding information.""" + + resource_name: str + resource_id: str + location: str + + def __init__(self, metadata: Dict, resource: Any) -> None: + """Initialize the E2E Networks Check's finding information. + + Args: + metadata: The metadata of the check. + resource: Basic information about the E2E Networks resource. + """ + 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 67a7ea2824..38661e6445 100644 --- a/prowler/lib/cli/parser.py +++ b/prowler/lib/cli/parser.py @@ -48,6 +48,7 @@ class ProwlerArgumentParser: "nhn", "mongodbatlas", "vercel", + "e2enetworks", "okta", "scaleway", "stackit", @@ -74,10 +75,10 @@ class ProwlerArgumentParser: 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,linode,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,linode,e2enetworks,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,linode{extra_providers_csv}}} + {{aws,azure,gcp,kubernetes,m365,github,googleworkspace,okta,iac,llm,image,nhn,mongodbatlas,oraclecloud,alibabacloud,cloudflare,openstack,scaleway,stackit,vercel,linode,e2enetworks{extra_providers_csv}}} aws AWS Provider azure Azure Provider gcp GCP Provider @@ -98,7 +99,8 @@ Available Cloud Providers: mongodbatlas MongoDB Atlas Provider scaleway Scaleway Provider vercel Vercel Provider - linode Linode Provider{extra_providers_text} + linode Linode Provider + e2enetworks E2E Networks 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 59afd1fa31..b1b0d9409b 100644 --- a/prowler/lib/outputs/compliance/universal/universal_output.py +++ b/prowler/lib/outputs/compliance/universal/universal_output.py @@ -26,6 +26,7 @@ PROVIDER_HEADER_MAP = { "oraclecloud": ("TenancyId", "account_uid", "Region", "region"), "alibabacloud": ("AccountId", "account_uid", "Region", "region"), "nhn": ("AccountId", "account_uid", "Region", "region"), + "e2enetworks": ("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 ed8bda4039..7231572571 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -342,6 +342,18 @@ class Finding(BaseModel): output_data["resource_uid"] = check_output.resource_id output_data["region"] = check_output.location + elif provider.type == "e2enetworks": + 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 18dd4b0f3d..3463014232 100644 --- a/prowler/lib/outputs/html/html.py +++ b/prowler/lib/outputs/html/html.py @@ -2,6 +2,7 @@ import sys from io import TextIOWrapper import markdown +from markupsafe import escape from prowler.config.config import ( html_logo_url, @@ -1396,6 +1397,46 @@ class HTML(Output): ) return "" + @staticmethod + def get_e2enetworks_assessment_summary(provider: Provider) -> str: + """Get the HTML assessment summary for the E2E Networks provider.""" + try: + locations = escape(", ".join(provider.identity.locations)) + project_id = escape(str(provider.identity.project_id)) + return f""" +
+
+
+ E2E Networks Assessment Summary +
+
    +
  • + Project ID: {project_id} +
  • +
  • + Locations: {locations} +
  • +
+
+
+
+
+
+ E2E Networks 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 40dc4635ba..05421ac584 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 == "e2enetworks": + 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 77b4c2725a..2d8da80597 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 == "e2enetworks": + 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 981d11fe00..d4793adbe7 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -625,6 +625,16 @@ class Provider(ABC): mutelist_path=arguments.mutelist_file, fixer_config=fixer_config, ) + elif arguments.provider == "e2enetworks": + provider_class( + api_key=getattr(arguments, "e2e_networks_api_key", None), + auth_token=getattr(arguments, "e2e_networks_auth_token", None), + project_id=getattr(arguments, "e2e_networks_project_id", None), + locations=getattr(arguments, "region", 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/e2enetworks/__init__.py b/prowler/providers/e2enetworks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/e2enetworks_provider.py b/prowler/providers/e2enetworks/e2enetworks_provider.py new file mode 100644 index 0000000000..480f5ee4a8 --- /dev/null +++ b/prowler/providers/e2enetworks/e2enetworks_provider.py @@ -0,0 +1,258 @@ +import os + +import requests +from colorama import Fore, 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.e2enetworks.exceptions.exceptions import ( + E2eNetworksCredentialsError, + E2eNetworksSessionError, +) +from prowler.providers.e2enetworks.lib.mutelist.mutelist import E2eNetworksMutelist +from prowler.providers.e2enetworks.models import ( + E2E_DEFAULT_LOCATIONS, + E2eNetworksIdentityInfo, + E2eNetworksSession, +) + + +class E2enetworksProvider(Provider): + """Provider class for E2E Networks.""" + + _type: str = "e2enetworks" + _session: E2eNetworksSession + _identity: E2eNetworksIdentityInfo + _audit_config: dict + _fixer_config: dict + _mutelist: E2eNetworksMutelist + 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 Networks Provider...") + + self._api_key = api_key or os.getenv("E2E_NETWORKS_API_KEY") + self._auth_token = auth_token or os.getenv("E2E_NETWORKS_AUTH_TOKEN") + project_value = project_id or os.getenv("E2E_NETWORKS_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 E2eNetworksCredentialsError( + message="E2enetworksProvider 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 is not None: + self._mutelist = E2eNetworksMutelist(mutelist_content=mutelist_content) + else: + if not mutelist_path: + mutelist_path = get_default_mute_file_path(self._type) + self._mutelist = E2eNetworksMutelist(mutelist_path=mutelist_path) + + self._session = E2enetworksProvider.setup_session( + api_key=self._api_key, + auth_token=self._auth_token, + project_id=self._project_id, + locations=self._locations, + ) + self._identity = E2eNetworksIdentityInfo( + project_id=self._project_id, + locations=self._locations, + ) + + Provider.set_global_provider(self) + + @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 Networks locations to scan. + """ + if locations: + return locations + + env_region = os.getenv("E2E_NETWORKS_REGION") + if env_region: + return [env_region] + + return list(E2E_DEFAULT_LOCATIONS) + + @property + def type(self) -> str: + return self._type + + @property + def session(self) -> E2eNetworksSession: + return self._session + + @property + def identity(self) -> E2eNetworksIdentityInfo: + 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) -> E2eNetworksMutelist: + return self._mutelist + + @staticmethod + def setup_session( + api_key: str, + auth_token: str, + project_id: int, + locations: list[str], + ) -> E2eNetworksSession: + """Create an authenticated E2E Networks API session. + + Args: + api_key: E2E Networks API key. + auth_token: Bearer auth token for the MyAccount API. + project_id: E2E Networks project identifier. + locations: Locations included in the session scope. + + Returns: + A configured E2eNetworksSession with an HTTP client. + + Raises: + E2eNetworksSessionError: If session initialization fails. + """ + try: + http_session = requests.Session() + http_session.headers.update( + { + "Authorization": f"Bearer {auth_token}", + "Content-Type": "application/json", + } + ) + return E2eNetworksSession( + api_key=api_key, + auth_token=auth_token, + project_id=project_id, + locations=locations, + http_session=http_session, + ) + except Exception as error: + raise E2eNetworksSessionError( + message="Failed to initialize E2E Networks session.", + original_exception=error, + ) from error + + def print_credentials(self) -> None: + """Print the E2E Networks scan scope to stdout. + + The API key and auth token are never printed, matching the behavior of + the other Prowler providers, which only report identity and scope. + """ + report_lines = [ + f" Authentication: {Fore.YELLOW}API Key{Style.RESET_ALL}", + f" Project ID: {self._project_id}", + f" Locations: {', '.join(self._locations)}", + ] + report_title = ( + f"{Style.BRIGHT}Using the E2E Networks 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: + """Test connectivity to the E2E Networks MyAccount API. + + Args: + api_key: E2E Networks API key. Falls back to E2E_NETWORKS_API_KEY. + auth_token: Bearer auth token. Falls back to E2E_NETWORKS_AUTH_TOKEN. + project_id: Project identifier. Falls back to E2E_NETWORKS_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: + E2eNetworksCredentialsError: If required credentials are missing. + E2eNetworksSessionError: 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_NETWORKS_API_KEY") + auth_token = auth_token or os.getenv("E2E_NETWORKS_AUTH_TOKEN") + project_value = project_id or os.getenv("E2E_NETWORKS_PROJECT_ID") + project_id_int = int(project_value) if project_value else None + resolved_locations = locations or ( + [os.getenv("E2E_NETWORKS_REGION")] + if os.getenv("E2E_NETWORKS_REGION") + else list(E2E_DEFAULT_LOCATIONS) + ) + + if not api_key or not auth_token or project_id_int is None: + raise E2eNetworksCredentialsError( + message="E2E Networks test_connection requires api_key, auth_token, and project_id." + ) + + session = E2enetworksProvider.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 Networks connection failed with status {response.status_code}: " + f"{response.text}" + ) + raise E2eNetworksSessionError(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/e2enetworks/exceptions/__init__.py b/prowler/providers/e2enetworks/exceptions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/exceptions/exceptions.py b/prowler/providers/e2enetworks/exceptions/exceptions.py new file mode 100644 index 0000000000..e01179bbc4 --- /dev/null +++ b/prowler/providers/e2enetworks/exceptions/exceptions.py @@ -0,0 +1,93 @@ +from prowler.exceptions.exceptions import ProwlerException + + +# Exceptions codes from 19000 to 19999 are reserved for E2E Networks exceptions +class E2eNetworksBaseException(ProwlerException): + """Base class for E2E Networks errors.""" + + E2E_NETWORKS_ERROR_CODES = { + (19000, "E2eNetworksCredentialsError"): { + "message": "E2E Networks credentials not found or invalid", + "remediation": "Provide a valid API key, auth token and project id via the E2E_NETWORKS_API_KEY, E2E_NETWORKS_AUTH_TOKEN and E2E_NETWORKS_PROJECT_ID environment variables.", + }, + (19001, "E2eNetworksAuthenticationError"): { + "message": "E2E Networks authentication failed", + "remediation": "Verify the E2E Networks API key and auth token and ensure the project id is correct and accessible.", + }, + (19002, "E2eNetworksSessionError"): { + "message": "E2E Networks session setup failed", + "remediation": "Review the E2E Networks credentials and network connectivity to the MyAccount API.", + }, + (19003, "E2eNetworksAPIError"): { + "message": "E2E Networks API request failed", + "remediation": "Check the E2E Networks MyAccount API status and that the credentials grant access to the requested resource.", + }, + (19004, "E2eNetworksIdentityError"): { + "message": "Unable to retrieve E2E Networks identity or project information", + "remediation": "Ensure the credentials allow access to the E2E Networks project and account APIs.", + }, + } + + def __init__(self, code, file=None, original_exception=None, message=None): + provider = "E2E" + error_info = self.E2E_NETWORKS_ERROR_CODES.get((code, self.__class__.__name__)) + if error_info is None: + error_info = { + "message": message or "Unknown E2E Networks error", + "remediation": "Check the E2E Networks MyAccount API documentation for more details.", + } + elif message: + error_info = error_info.copy() + error_info["message"] = message + super().__init__( + code=code, + source=provider, + file=file, + original_exception=original_exception, + error_info=error_info, + ) + + +class E2eNetworksCredentialsError(E2eNetworksBaseException): + """Exception for E2E Networks credential errors.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 19000, file=file, original_exception=original_exception, message=message + ) + + +class E2eNetworksAuthenticationError(E2eNetworksBaseException): + """Exception for E2E Networks authentication errors.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 19001, file=file, original_exception=original_exception, message=message + ) + + +class E2eNetworksSessionError(E2eNetworksBaseException): + """Exception for E2E Networks session setup errors.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 19002, file=file, original_exception=original_exception, message=message + ) + + +class E2eNetworksAPIError(E2eNetworksBaseException): + """Exception for E2E Networks API request errors.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 19003, file=file, original_exception=original_exception, message=message + ) + + +class E2eNetworksIdentityError(E2eNetworksBaseException): + """Exception for E2E Networks identity errors.""" + + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 19004, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/e2enetworks/lib/__init__.py b/prowler/providers/e2enetworks/lib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/lib/api/__init__.py b/prowler/providers/e2enetworks/lib/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/lib/api/client.py b/prowler/providers/e2enetworks/lib/api/client.py new file mode 100644 index 0000000000..1025bd815e --- /dev/null +++ b/prowler/providers/e2enetworks/lib/api/client.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import re +from typing import Any + +import requests + +from prowler.lib.logger import logger +from prowler.providers.e2enetworks.exceptions.exceptions import E2eNetworksAPIError +from prowler.providers.e2enetworks.models import E2eNetworksSession + + +def _redact_sensitive_values(message: str) -> str: + """Redact E2E Networks secrets from request error messages.""" + redacted = re.sub(r"(?i)(apikey=)[^&\s]+", r"\1REDACTED", message) + return re.sub( + r"(?i)(Authorization:\s*Bearer\s+|Bearer\s+)[^\s,;]+", + r"\1REDACTED", + redacted, + ) + + +class E2eNetworksAPIClient: + """Shared HTTP client for E2E Networks MyAccount API requests.""" + + def __init__(self, session: E2eNetworksSession): + 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 Networks 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: + redacted_error = _redact_sensitive_values(str(error)) + logger.error( + f"E2E API GET {path} failed: {error.__class__.__name__}: {redacted_error}" + ) + raise E2eNetworksAPIError( + message=f"GET {path} failed for location {location}", + original_exception=Exception(redacted_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): + all_items.extend(data.values()) + + 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/e2enetworks/lib/arguments/__init__.py b/prowler/providers/e2enetworks/lib/arguments/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/lib/arguments/arguments.py b/prowler/providers/e2enetworks/lib/arguments/arguments.py new file mode 100644 index 0000000000..fde5ff6355 --- /dev/null +++ b/prowler/providers/e2enetworks/lib/arguments/arguments.py @@ -0,0 +1,68 @@ +import os + +SENSITIVE_ARGUMENTS = frozenset({"--e2e-networks-api-key", "--e2e-networks-auth-token"}) + + +def init_parser(self): + """Init the E2E Networks Provider CLI parser.""" + e2enetworks_parser = self.subparsers.add_parser( + "e2enetworks", + parents=[self.common_providers_parser], + help="E2E Networks Provider", + ) + + auth_group = e2enetworks_parser.add_argument_group("Authentication") + auth_group.add_argument( + "--e2e-networks-api-key", + nargs="?", + default=None, + metavar="E2E_NETWORKS_API_KEY", + help="E2E Networks API key. Use E2E_NETWORKS_API_KEY env var instead of passing directly.", + ) + auth_group.add_argument( + "--e2e-networks-auth-token", + nargs="?", + default=None, + metavar="E2E_NETWORKS_AUTH_TOKEN", + help="E2E Networks auth token. Use E2E_NETWORKS_AUTH_TOKEN env var instead of passing directly.", + ) + auth_group.add_argument( + "--e2e-networks-project-id", + nargs="?", + default=None, + metavar="E2E_NETWORKS_PROJECT_ID", + help="E2E Networks project ID. Use E2E_NETWORKS_PROJECT_ID env var instead of passing directly.", + ) + + regions_subparser = e2enetworks_parser.add_argument_group("Regions") + regions_subparser.add_argument( + "--region", + "--filter-region", + "-f", + nargs="+", + default=None, + metavar="REGION", + help="E2E Networks region(s) to scan (e.g. Delhi Chennai). If omitted, " + "uses E2E_NETWORKS_REGION or scans all regions (Delhi, Chennai).", + ) + + +def validate_arguments(arguments) -> tuple[bool, str]: + """Validate E2E Networks provider CLI arguments. + + Only argument consistency is checked here so that listing operations such as + ``--list-checks`` and ``--list-services`` run without credentials. Credential + presence (API key, auth token and project ID) is enforced later at provider + initialization, when an actual scan is performed. + """ + project_id = arguments.e2e_networks_project_id or os.getenv( + "E2E_NETWORKS_PROJECT_ID" + ) + + if project_id is not None: + try: + int(project_id) + except (TypeError, ValueError): + return False, "E2E Networks project ID must be an integer." + + return True, "" diff --git a/prowler/providers/e2enetworks/lib/mutelist/__init__.py b/prowler/providers/e2enetworks/lib/mutelist/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/lib/mutelist/mutelist.py b/prowler/providers/e2enetworks/lib/mutelist/mutelist.py new file mode 100644 index 0000000000..6468169e17 --- /dev/null +++ b/prowler/providers/e2enetworks/lib/mutelist/mutelist.py @@ -0,0 +1,27 @@ +"""E2E Networks mutelist support for suppressing findings.""" + +from prowler.lib.check.models import CheckReportE2eNetworks +from prowler.lib.mutelist.mutelist import Mutelist +from prowler.lib.outputs.utils import unroll_dict, unroll_tags + + +class E2eNetworksMutelist(Mutelist): + """Mutelist implementation for E2E Networks check findings.""" + + def is_finding_muted(self, **kwargs) -> bool: + """Determine whether an E2E Networks finding is muted. + + Args: + **kwargs: Keyword arguments; must include ``finding``. + + Returns: + True if the finding matches a mutelist entry, otherwise False. + """ + finding: CheckReportE2eNetworks = kwargs["finding"] + 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/e2enetworks/lib/service/__init__.py b/prowler/providers/e2enetworks/lib/service/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/lib/service/service.py b/prowler/providers/e2enetworks/lib/service/service.py new file mode 100644 index 0000000000..7d5b51f463 --- /dev/null +++ b/prowler/providers/e2enetworks/lib/service/service.py @@ -0,0 +1,19 @@ +from prowler.providers.e2enetworks.e2enetworks_provider import E2enetworksProvider +from prowler.providers.e2enetworks.lib.api.client import E2eNetworksAPIClient + + +class E2eNetworksService: + """Base class for E2E Networks services.""" + + def __init__(self, service: str, provider: E2enetworksProvider): + """Initialize an E2E Networks service client. + + Args: + service: Service name used for logging and configuration lookup. + provider: The active E2E Networks provider instance. + """ + 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 = E2eNetworksAPIClient(provider.session) diff --git a/prowler/providers/e2enetworks/models.py b/prowler/providers/e2enetworks/models.py new file mode 100644 index 0000000000..84d6bf4e57 --- /dev/null +++ b/prowler/providers/e2enetworks/models.py @@ -0,0 +1,53 @@ +from typing import Any + +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 E2eNetworksSession(BaseModel): + """E2E Networks 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 E2eNetworksIdentityInfo(BaseModel): + """E2E Networks identity and scoping information.""" + + project_id: int + locations: list[str] + + +class E2eNetworksOutputOptions(ProviderOutputOptions): + """Customize output filenames for E2E Networks scans.""" + + def __init__( + self, + arguments: object, + bulk_checks_metadata: dict, + identity: E2eNetworksIdentityInfo, + ) -> None: + """Initialize E2E Networks output options. + + Args: + arguments: Parsed CLI arguments for the scan. + bulk_checks_metadata: Loaded metadata for all checks in the scan. + identity: E2E Networks identity information used in output filenames. + """ + super().__init__(arguments, bulk_checks_metadata) + if ( + not hasattr(arguments, "output_filename") + or arguments.output_filename is None + ): + self.output_filename = f"prowler-output-e2enetworks-{identity.project_id}-{output_file_timestamp}" + else: + self.output_filename = arguments.output_filename diff --git a/prowler/providers/e2enetworks/services/__init__.py b/prowler/providers/e2enetworks/services/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/database/__init__.py b/prowler/providers/e2enetworks/services/database/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/database/database_client.py b/prowler/providers/e2enetworks/services/database/database_client.py new file mode 100644 index 0000000000..5eaf4d6bda --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2enetworks.services.database.database_service import Database + +database_client = Database(Provider.get_global_provider()) diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/__init__.py b/prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json b/prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json new file mode 100644 index 0000000000..1604d0e5f7 --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "database_cluster_backup_enabled", + "CheckTitle": "E2E Networks database clusters have automated backups enabled", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "database", + "Description": "**E2E Networks database clusters** should have automated backups enabled so data can be restored after failures, corruption, or accidental deletion. Automated backups capture point-in-time copies of the cluster on a recurring schedule.", + "Risk": "Database clusters without backups are vulnerable to permanent **data loss** from hardware failures, corruption, or accidental deletion, with no way to recover to a prior state.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/database/rds/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **automated backups** on every database cluster that does not currently have them configured, and validate that backups can be restored.", + "Url": "https://hub.prowler.com/check/database_cluster_backup_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py b/prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py new file mode 100644 index 0000000000..553c05f8ff --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.database.database_client import ( + database_client, +) + + +class database_cluster_backup_enabled(Check): + """Check if E2E Networks database clusters have backups enabled.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for cluster in database_client.clusters: + report = CheckReportE2eNetworks(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/e2enetworks/services/database/database_cluster_default_admin_username/__init__.py b/prowler/providers/e2enetworks/services/database/database_cluster_default_admin_username/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json b/prowler/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json new file mode 100644 index 0000000000..83d2d160c9 --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "database_cluster_default_admin_username", + "CheckTitle": "E2E Networks database clusters do not use the default admin username", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "database", + "Description": "**E2E Networks database clusters** should be configured with a custom administrative username instead of the well-known default, reducing the predictability of privileged credentials.", + "Risk": "Default admin usernames are publicly known, so they increase exposure to **credential guessing** and **brute-force attacks** against the privileged database account.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/database/rds/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Replace the default administrative username on database clusters with a unique, non-default account name and rotate the associated credentials.", + "Url": "https://hub.prowler.com/check/database_cluster_default_admin_username" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py b/prowler/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py new file mode 100644 index 0000000000..8a5de20d0e --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.database.database_client import ( + database_client, +) + + +class database_cluster_default_admin_username(Check): + """Check if E2E Networks database clusters do not use the default admin username.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for cluster in database_client.clusters: + report = CheckReportE2eNetworks(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/e2enetworks/services/database/database_cluster_ip_whitelist_configured/__init__.py b/prowler/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json b/prowler/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json new file mode 100644 index 0000000000..eebe425046 --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "database_cluster_ip_whitelist_configured", + "CheckTitle": "E2E Networks database clusters with public IPs have IP whitelisting configured", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "database", + "Description": "**E2E Networks database clusters** that expose a public IP should restrict connectivity through an **IP whitelist** instead of accepting connections from any address (`0.0.0.0/0`).", + "Risk": "Publicly exposed database clusters without IP whitelisting accept connections from **any source on the internet**, greatly increasing the risk of unauthorized access and data exfiltration.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/database/rds/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure an **IP whitelist** on publicly exposed database clusters to allow only trusted source ranges, or move the cluster onto private networking.", + "Url": "https://hub.prowler.com/check/database_cluster_ip_whitelist_configured" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py b/prowler/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py new file mode 100644 index 0000000000..0537f6f9e9 --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.database.database_client import ( + database_client, +) + + +class database_cluster_ip_whitelist_configured(Check): + """Check if E2E Networks database clusters with public IPs have IP whitelisting configured.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for cluster in database_client.clusters: + if not cluster.master_has_public_ip: + continue + report = CheckReportE2eNetworks(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/e2enetworks/services/database/database_cluster_public_ip_not_assigned/__init__.py b/prowler/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json b/prowler/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json new file mode 100644 index 0000000000..a965ef84f7 --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "database_cluster_public_ip_not_assigned", + "CheckTitle": "E2E Networks database cluster master nodes do not expose a public IP", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "database", + "Description": "The **master node** of each **E2E Networks database cluster** should be reachable only over private networking and must not have a public IP address assigned.", + "Risk": "A **public database endpoint** is directly reachable from the internet, expanding the attack surface and increasing the risk of unauthorized access, brute-force attempts, and data exposure.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/database/rds/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Remove the **public IP** from the master node of database clusters and access them through private networking such as a VPC.", + "Url": "https://hub.prowler.com/check/database_cluster_public_ip_not_assigned" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py b/prowler/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py new file mode 100644 index 0000000000..f6116d9e68 --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.database.database_client import ( + database_client, +) + + +class database_cluster_public_ip_not_assigned(Check): + """Check if E2E Networks database clusters do not expose a public IP on the master node.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for cluster in database_client.clusters: + report = CheckReportE2eNetworks(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/e2enetworks/services/database/database_cluster_running/__init__.py b/prowler/providers/e2enetworks/services/database/database_cluster_running/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running.metadata.json b/prowler/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running.metadata.json new file mode 100644 index 0000000000..8de420a54b --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "database_cluster_running", + "CheckTitle": "E2E Networks database clusters are in RUNNING status", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "NotDefined", + "ResourceGroup": "database", + "Description": "**E2E Networks database clusters** should be in the `RUNNING` state, confirming they are operational rather than stopped, failed, or stuck in a transitional status.", + "Risk": "Database clusters that are not `RUNNING` may indicate outages, failed provisioning, or misconfiguration that impacts **availability** and the ability to recover data.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/database/rds/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Start database clusters that are not in the `RUNNING` state and investigate the root cause of any stopped or failed clusters.", + "Url": "https://hub.prowler.com/check/database_cluster_running" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running.py b/prowler/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running.py new file mode 100644 index 0000000000..3322f85863 --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.database.database_client import ( + database_client, +) + + +class database_cluster_running(Check): + """Check if E2E Networks database clusters are in RUNNING status.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for cluster in database_client.clusters: + report = CheckReportE2eNetworks(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/e2enetworks/services/database/database_cluster_ssl_enabled/__init__.py b/prowler/providers/e2enetworks/services/database/database_cluster_ssl_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json b/prowler/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json new file mode 100644 index 0000000000..f459ffd40b --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "database_cluster_ssl_enabled", + "CheckTitle": "E2E Networks database clusters have SSL enabled on the master node", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "database", + "Description": "**E2E Networks database clusters** should require `SSL/TLS` on the master node so that client connections and credentials are encrypted **in transit**.", + "Risk": "Without `SSL/TLS`, database credentials and query data travel in **plaintext** and can be intercepted through man-in-the-middle or network eavesdropping attacks.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/database/rds/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable `SSL/TLS` on the master node of database clusters to encrypt all client connections in transit.", + "Url": "https://hub.prowler.com/check/database_cluster_ssl_enabled" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py b/prowler/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py new file mode 100644 index 0000000000..7def7a8c26 --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.database.database_client import ( + database_client, +) + + +class database_cluster_ssl_enabled(Check): + """Check if E2E Networks database clusters have SSL enabled on the master node.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for cluster in database_client.clusters: + report = CheckReportE2eNetworks(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/e2enetworks/services/database/database_replica_public_ip_not_assigned/__init__.py b/prowler/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json b/prowler/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json new file mode 100644 index 0000000000..268ae1c6ad --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "database_replica_public_ip_not_assigned", + "CheckTitle": "E2E Networks database read replicas do not have a public IP assigned", + "CheckType": [], + "ServiceName": "database", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "database", + "Description": "**E2E Networks database read replicas** should be reachable only over private networking and must not have a public IP address assigned.", + "Risk": "Read replicas with **public IPs** are directly reachable from the internet, expanding database exposure and increasing the risk of unauthorized access to replicated data.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/database/rds/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Remove the **public IP** from database read replicas and route access through private networking.", + "Url": "https://hub.prowler.com/check/database_replica_public_ip_not_assigned" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py b/prowler/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py new file mode 100644 index 0000000000..0dadc211a6 --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned.py @@ -0,0 +1,26 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.database.database_client import ( + database_client, +) + + +class database_replica_public_ip_not_assigned(Check): + """Check if E2E Networks database read replicas do not have a public IP assigned.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for instance in database_client.instances: + if instance.role != "replica": + continue + report = CheckReportE2eNetworks(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/e2enetworks/services/database/database_service.py b/prowler/providers/e2enetworks/services/database/database_service.py new file mode 100644 index 0000000000..703c2871d7 --- /dev/null +++ b/prowler/providers/e2enetworks/services/database/database_service.py @@ -0,0 +1,164 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2enetworks.lib.service.service import E2eNetworksService + + +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(E2eNetworksService): + """Service class for E2E Networks 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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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/e2enetworks/services/loadbalancer/__init__.py b/prowler/providers/e2enetworks/services/loadbalancer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/__init__.py b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json new file mode 100644 index 0000000000..641554a2cc --- /dev/null +++ b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "loadbalancer_alb_https_uses_ssl_certificate", + "CheckTitle": "E2E Networks ALB HTTPS load balancers use an SSL certificate", + "CheckType": [], + "ServiceName": "loadbalancer", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks application load balancers** serving `HTTPS` traffic should have a valid **SSL certificate** configured so client connections are encrypted and authenticated.", + "Risk": "`HTTPS` load balancers without a valid **SSL certificate** expose traffic to interception and downgrade attacks, weakening **confidentiality** and **integrity** for client connections.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/load-balancer/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach a valid **SSL certificate** to every load balancer that serves `HTTPS` traffic.", + "Url": "https://hub.prowler.com/check/loadbalancer_alb_https_uses_ssl_certificate" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate.py new file mode 100644 index 0000000000..2ed10c1c14 --- /dev/null +++ b/prowler/providers/e2enetworks/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, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_client import ( + loadbalancer_client, +) + + +class loadbalancer_alb_https_uses_ssl_certificate(Check): + """Check that HTTPS load balancers have an SSL certificate configured.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for lb in loadbalancer_client.load_balancers: + if not lb.is_alb_https: + continue + + report = CheckReportE2eNetworks(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/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/__init__.py b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json new file mode 100644 index 0000000000..b4c6a710c7 --- /dev/null +++ b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "loadbalancer_backend_health_check_enabled", + "CheckTitle": "E2E Networks ALB load balancers have backend health checks enabled", + "CheckType": [], + "ServiceName": "loadbalancer", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks application load balancers** should have `HTTP` **health checks** configured for their backends so traffic is only routed to healthy nodes.", + "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": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/load-balancer/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure `HTTP` **health checks** for ALB backends so unhealthy nodes are automatically removed from rotation.", + "Url": "https://hub.prowler.com/check/loadbalancer_backend_health_check_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py new file mode 100644 index 0000000000..8602797e5f --- /dev/null +++ b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_client import ( + loadbalancer_client, +) + + +class loadbalancer_backend_health_check_enabled(Check): + """Check that ALB load balancers have backend health checks configured.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for lb in loadbalancer_client.load_balancers: + if not lb.is_alb: + continue + + report = CheckReportE2eNetworks(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/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/__init__.py b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json new file mode 100644 index 0000000000..8ecae9db08 --- /dev/null +++ b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "loadbalancer_bitninja_enabled", + "CheckTitle": "E2E Networks load balancers have BitNinja protection enabled", + "CheckType": [], + "ServiceName": "loadbalancer", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks load balancers** should have **BitNinja** protection enabled to provide web application firewall coverage on public endpoints.", + "Risk": "Load balancers without **BitNinja** protection have reduced **WAF** coverage, increasing exposure to automated attacks and malicious traffic against public endpoints.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/load-balancer/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **BitNinja** protection on load balancer appliances to strengthen web application firewall coverage.", + "Url": "https://hub.prowler.com/check/loadbalancer_bitninja_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py new file mode 100644 index 0000000000..c08ebef982 --- /dev/null +++ b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_client import ( + loadbalancer_client, +) + + +class loadbalancer_bitninja_enabled(Check): + """Check that load balancers have BitNinja protection enabled.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for lb in loadbalancer_client.load_balancers: + report = CheckReportE2eNetworks(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/e2enetworks/services/loadbalancer/loadbalancer_client.py b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_client.py new file mode 100644 index 0000000000..88330ec3a8 --- /dev/null +++ b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_service import ( + LoadBalancers, +) + +loadbalancer_client = LoadBalancers(Provider.get_global_provider()) diff --git a/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_service.py b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_service.py new file mode 100644 index 0000000000..e572a2f614 --- /dev/null +++ b/prowler/providers/e2enetworks/services/loadbalancer/loadbalancer_service.py @@ -0,0 +1,96 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2enetworks.lib.service.service import E2eNetworksService + + +class LoadBalancers(E2eNetworksService): + """Service class for E2E Networks load balancers.""" + + def __init__(self, provider): + super().__init__("loadbalancer", provider) + self.load_balancers: 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.load_balancers.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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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(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: + if isinstance(backend, dict) and backend.get("http_check"): + return True + return False diff --git a/prowler/providers/e2enetworks/services/network/__init__.py b/prowler/providers/e2enetworks/services/network/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/network/network_client.py b/prowler/providers/e2enetworks/services/network/network_client.py new file mode 100644 index 0000000000..d7d16095d3 --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2enetworks.services.network.network_service import Network + +network_client = Network(Provider.get_global_provider()) diff --git a/prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/__init__.py b/prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json b/prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json new file mode 100644 index 0000000000..a9fecbfb0a --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "network_reserveip_floating_ip_unattached", + "CheckTitle": "E2E Networks floating IPs are attached to nodes", + "CheckType": [], + "ServiceName": "network", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks floating (reserved) IP addresses** should be attached to a node instead of being left unassigned, since idle floating IPs can be reassigned unexpectedly.", + "Risk": "**Unattached floating IPs** may be reassigned unexpectedly to other resources, disrupting secure routing assumptions and potentially exposing services.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/network/reserve-ip/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach unassigned **floating IPs** to the intended node, or release them if they are no longer needed.", + "Url": "https://hub.prowler.com/check/network_reserveip_floating_ip_unattached" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py b/prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py new file mode 100644 index 0000000000..c24a270804 --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.network.network_client import network_client + + +class network_reserveip_floating_ip_unattached(Check): + """Check if E2E Networks floating IPs are attached to nodes.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for ip in network_client.reserved_ips: + if ip.reserved_type != "FloatingIP": + continue + report = CheckReportE2eNetworks(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/e2enetworks/services/network/network_reserveip_orphaned_public_ip/__init__.py b/prowler/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json b/prowler/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json new file mode 100644 index 0000000000..b8c21b4b6b --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "network_reserveip_orphaned_public_ip", + "CheckTitle": "E2E Networks public or addon IPs are attached to a resource", + "CheckType": [], + "ServiceName": "network", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks public and addon reserved IP addresses** should be attached to a resource, identifying orphaned addresses that remain internet-reachable without an owner.", + "Risk": "**Orphaned public IPs** remain internet-reachable resources that can be misused or reassigned without ownership controls, expanding the attack surface.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/network/reserve-ip/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach orphaned **public or addon IPs** to a resource, or delete them to remove unused internet-reachable addresses.", + "Url": "https://hub.prowler.com/check/network_reserveip_orphaned_public_ip" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py b/prowler/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py new file mode 100644 index 0000000000..bb4a67066d --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.network.network_client import network_client + + +class network_reserveip_orphaned_public_ip(Check): + """Check if E2E Networks public or addon IPs are attached.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for ip in network_client.reserved_ips: + if ip.reserved_type not in ("PublicIP", "AddonIP"): + continue + report = CheckReportE2eNetworks(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/e2enetworks/services/network/network_service.py b/prowler/providers/e2enetworks/services/network/network_service.py new file mode 100644 index 0000000000..327749d22c --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_service.py @@ -0,0 +1,158 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2enetworks.lib.service.service import E2eNetworksService + + +class Network(E2eNetworksService): + """Service class for E2E Networks 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) + if not isinstance(vpcs, list): + continue + 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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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/e2enetworks/services/network/network_vpc_has_attached_nodes/__init__.py b/prowler/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json b/prowler/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json new file mode 100644 index 0000000000..59b94ba76a --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "network_vpc_has_attached_nodes", + "CheckTitle": "E2E Networks VPCs have attached nodes", + "CheckType": [], + "ServiceName": "network", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks VPCs** should have at least one node attached, identifying unused network segments that may indicate misconfiguration or forgotten resources.", + "Risk": "**VPCs without attached nodes** may indicate unused network segments or misconfiguration that weakens **network segmentation** and increases management overhead.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/network/vpc/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach nodes to VPCs that are in use, or remove empty VPCs that are no longer required.", + "Url": "https://hub.prowler.com/check/network_vpc_has_attached_nodes" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py b/prowler/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py new file mode 100644 index 0000000000..3b453e188a --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.network.network_client import network_client + + +class network_vpc_has_attached_nodes(Check): + """Check if E2E Networks VPCs have attached nodes.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for vpc in network_client.vpcs: + report = CheckReportE2eNetworks(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/e2enetworks/services/network/network_vpc_is_active/__init__.py b/prowler/providers/e2enetworks/services/network/network_vpc_is_active/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json b/prowler/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json new file mode 100644 index 0000000000..8843bdf1e7 --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "network_vpc_is_active", + "CheckTitle": "E2E Networks VPCs are active", + "CheckType": [], + "ServiceName": "network", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks VPCs** should be in the `active` state so that attached workloads retain expected connectivity and network isolation.", + "Risk": "**Inactive VPCs** can cause connectivity failures and may leave workloads without the expected **network isolation** boundaries.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/network/vpc/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Activate VPCs that are `inactive` so dependent workloads keep their expected connectivity and isolation.", + "Url": "https://hub.prowler.com/check/network_vpc_is_active" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active.py b/prowler/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active.py new file mode 100644 index 0000000000..d95027d9ae --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.network.network_client import network_client + + +class network_vpc_is_active(Check): + """Check if E2E Networks VPCs are active.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for vpc in network_client.vpcs: + report = CheckReportE2eNetworks(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/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/__init__.py b/prowler/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json b/prowler/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json new file mode 100644 index 0000000000..9adbfab90e --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "network_vpc_peering_external_peer_disabled", + "CheckTitle": "E2E Networks VPC peering does not use external peers", + "CheckType": [], + "ServiceName": "network", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks VPC peering connections** should be established only with internal networks and must not extend trust to external peers outside the account.", + "Risk": "**VPC peering with external peers** expands trust boundaries and can expose private networks to third parties, enabling lateral movement across accounts.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/network/vpc/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Remove **VPC peering** tunnels that connect to external peers to keep private networks within trusted boundaries.", + "Url": "https://hub.prowler.com/check/network_vpc_peering_external_peer_disabled" + } + }, + "Categories": [ + "trust-boundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py b/prowler/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py new file mode 100644 index 0000000000..d47fa0a6df --- /dev/null +++ b/prowler/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.network.network_client import network_client + + +class network_vpc_peering_external_peer_disabled(Check): + """Check if E2E Networks VPC peering does not use external peers.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for tunnel in network_client.vpc_tunnels: + report = CheckReportE2eNetworks(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/e2enetworks/services/node/__init__.py b/prowler/providers/e2enetworks/services/node/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/__init__.py b/prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json b/prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json new file mode 100644 index 0000000000..6229383147 --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "node_accidental_protection_enabled", + "CheckTitle": "E2E Networks nodes have accidental protection enabled", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "compute", + "Description": "**E2E Networks nodes** should have **accidental protection** enabled to guard against unintended deletion or modification of compute instances.", + "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": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **accidental protection** on nodes to prevent unintended deletion or modification.", + "Url": "https://hub.prowler.com/check/node_accidental_protection_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py b/prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py new file mode 100644 index 0000000000..49a6ff411d --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.node.node_client import node_client + + +class node_accidental_protection_enabled(Check): + """Check if E2E Networks nodes have accidental protection enabled.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for node in node_client.nodes: + report = CheckReportE2eNetworks(metadata=self.metadata(), resource=node) + report.status = "PASS" + 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." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2enetworks/services/node/node_client.py b/prowler/providers/e2enetworks/services/node/node_client.py new file mode 100644 index 0000000000..a1c9981373 --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2enetworks.services.node.node_service import Nodes + +node_client = Nodes(Provider.get_global_provider()) diff --git a/prowler/providers/e2enetworks/services/node/node_compliance_enabled/__init__.py b/prowler/providers/e2enetworks/services/node/node_compliance_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json b/prowler/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json new file mode 100644 index 0000000000..bb3b5f30ef --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "node_compliance_enabled", + "CheckTitle": "E2E Networks nodes have compliance mode enabled", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "compute", + "Description": "**E2E Networks nodes** should run with **compliance mode** enabled so required security controls are enforced and workload activity remains auditable.", + "Risk": "Nodes without **compliance mode** may not enforce required security controls, increasing misconfiguration risk and weakening the **auditability** of compute workloads.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **compliance mode** on nodes to enforce required security controls and maintain auditability.", + "Url": "https://hub.prowler.com/check/node_compliance_enabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled.py b/prowler/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled.py new file mode 100644 index 0000000000..bae2b16184 --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.node.node_client import node_client + + +class node_compliance_enabled(Check): + """Check if E2E Networks nodes have compliance mode enabled.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for node in node_client.nodes: + report = CheckReportE2eNetworks(metadata=self.metadata(), resource=node) + report.status = "PASS" + 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." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2enetworks/services/node/node_encryption_enabled/__init__.py b/prowler/providers/e2enetworks/services/node/node_encryption_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json b/prowler/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json new file mode 100644 index 0000000000..6af8ca2479 --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "node_encryption_enabled", + "CheckTitle": "E2E Networks nodes have disk encryption enabled", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "compute", + "Description": "**E2E Networks nodes** should have **disk encryption** enabled so data stored on node volumes and snapshots is protected **at rest**.", + "Risk": "**Unencrypted nodes** increase the risk of **data exposure** if disks or snapshots are accessed outside normal operating controls.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **disk encryption** on nodes to protect data at rest on volumes and snapshots.", + "Url": "https://hub.prowler.com/check/node_encryption_enabled" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled.py b/prowler/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled.py new file mode 100644 index 0000000000..65f65feb83 --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.node.node_client import node_client + + +class node_encryption_enabled(Check): + """Check if E2E Networks nodes have encryption enabled.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for node in node_client.nodes: + report = CheckReportE2eNetworks(metadata=self.metadata(), resource=node) + report.status = "PASS" + 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." + ) + findings.append(report) + return findings diff --git a/prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/__init__.py b/prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json b/prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json new file mode 100644 index 0000000000..addcf3feb0 --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "node_public_ip_not_assigned", + "CheckTitle": "E2E Networks nodes do not have a public IP assigned", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "compute", + "Description": "**E2E Networks nodes** should not be assigned a public IP address, keeping compute instances off the public internet unless inbound access is explicitly required.", + "Risk": "Nodes with **public IP addresses** are directly reachable from the internet, expanding the attack surface and increasing exposure to unauthorized access attempts.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Remove **public IP addresses** from nodes that do not require inbound internet access and reach them over private networking.", + "Url": "https://hub.prowler.com/check/node_public_ip_not_assigned" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py b/prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py new file mode 100644 index 0000000000..a95bd4552d --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned.py @@ -0,0 +1,18 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.node.node_client import node_client + + +class node_public_ip_not_assigned(Check): + """Check if E2E Networks nodes do not have a public IP assigned.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for node in node_client.nodes: + report = CheckReportE2eNetworks(metadata=self.metadata(), resource=node) + report.status = "PASS" + report.status_extended = f"Node {node.name} does not have a public IP." + if node.has_public_ip: + 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/e2enetworks/services/node/node_rescue_mode_disabled/__init__.py b/prowler/providers/e2enetworks/services/node/node_rescue_mode_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json b/prowler/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json new file mode 100644 index 0000000000..8336bc12cb --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "node_rescue_mode_disabled", + "CheckTitle": "E2E Networks nodes do not have rescue mode enabled", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "compute", + "Description": "**E2E Networks nodes** should not be running in **rescue mode**, which grants elevated filesystem access and can bypass normal operating controls.", + "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": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable **rescue mode** on nodes once recovery tasks are complete to restore normal operating controls.", + "Url": "https://hub.prowler.com/check/node_rescue_mode_disabled" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py b/prowler/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py new file mode 100644 index 0000000000..bde2da993d --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.node.node_client import node_client + + +class node_rescue_mode_disabled(Check): + """Check if E2E Networks nodes do not have rescue mode enabled.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for node in node_client.nodes: + report = CheckReportE2eNetworks(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/e2enetworks/services/node/node_service.py b/prowler/providers/e2enetworks/services/node/node_service.py new file mode 100644 index 0000000000..03e39be1b9 --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_service.py @@ -0,0 +1,109 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2enetworks.lib.service.service import E2eNetworksService + + +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(E2eNetworksService): + """Service class for E2E Networks compute nodes.""" + + def __init__(self, provider): + super().__init__("node", 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"node - 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: + 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"node - Error fetching node detail {node_id} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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/e2enetworks/services/node/node_vpc_attached/__init__.py b/prowler/providers/e2enetworks/services/node/node_vpc_attached/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached.metadata.json b/prowler/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached.metadata.json new file mode 100644 index 0000000000..59822e48e7 --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "node_vpc_attached", + "CheckTitle": "E2E Networks nodes are attached to a VPC", + "CheckType": [], + "ServiceName": "node", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "compute", + "Description": "**E2E Networks nodes** should be attached to a **VPC** so their traffic benefits from network isolation and segmentation controls.", + "Risk": "Nodes not attached to a **VPC** may lack network isolation controls, making traffic routing and **segmentation** harder to enforce.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/compute/nodes/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach nodes to a **VPC** to enforce network isolation and segmentation for their traffic.", + "Url": "https://hub.prowler.com/check/node_vpc_attached" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached.py b/prowler/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached.py new file mode 100644 index 0000000000..5318461d3c --- /dev/null +++ b/prowler/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached.py @@ -0,0 +1,18 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.node.node_client import node_client + + +class node_vpc_attached(Check): + """Check if E2E Networks nodes are attached to a VPC.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for node in node_client.nodes: + report = CheckReportE2eNetworks(metadata=self.metadata(), resource=node) + report.status = "PASS" + report.status_extended = f"Node {node.name} is attached to a VPC." + 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) + return findings diff --git a/prowler/providers/e2enetworks/services/securitygroup/__init__.py b/prowler/providers/e2enetworks/services/securitygroup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/securitygroup/securitygroup_client.py b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_client.py new file mode 100644 index 0000000000..4c938fbb2a --- /dev/null +++ b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_client.py @@ -0,0 +1,6 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2enetworks.services.securitygroup.securitygroup_service import ( + SecurityGroups, +) + +securitygroup_client = SecurityGroups(Provider.get_global_provider()) diff --git a/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/__init__.py b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json new file mode 100644 index 0000000000..23d765050a --- /dev/null +++ b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "securitygroup_no_all_traffic_rule", + "CheckTitle": "E2E Networks security groups do not allow all traffic", + "CheckType": [], + "ServiceName": "securitygroup", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks security groups** should not have an **all-traffic rule**, which permits unrestricted inbound and outbound network access.", + "Risk": "Security groups that allow **all traffic** permit unrestricted network access, increasing exposure to **lateral movement** and **data exfiltration**.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/network/security-group/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict security group rules to the required ports and sources only, removing any all-traffic rule.", + "Url": "https://hub.prowler.com/check/securitygroup_no_all_traffic_rule" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py new file mode 100644 index 0000000000..5230211d6f --- /dev/null +++ b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.securitygroup.securitygroup_client import ( + securitygroup_client, +) + + +class securitygroup_no_all_traffic_rule(Check): + """Check that security groups do not allow all traffic.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for group in securitygroup_client.security_groups: + report = CheckReportE2eNetworks(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/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/__init__.py b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json new file mode 100644 index 0000000000..ebd43c61cc --- /dev/null +++ b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "securitygroup_no_inbound_any_all_ports", + "CheckTitle": "E2E Networks security groups do not allow inbound all-protocol traffic from any source", + "CheckType": [], + "ServiceName": "securitygroup", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks security groups** should not allow inbound **all-protocol** traffic from any source (`0.0.0.0/0`), which opens every port to the internet.", + "Risk": "Inbound **all-protocol** rules from any source expose services to the entire internet and greatly increase the likelihood of unauthorized access.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/network/security-group/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Replace overly permissive inbound rules with **least-privilege** rules scoped to specific ports and trusted sources.", + "Url": "https://hub.prowler.com/check/securitygroup_no_inbound_any_all_ports" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py new file mode 100644 index 0000000000..ee3c38015f --- /dev/null +++ b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports.py @@ -0,0 +1,35 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.securitygroup.securitygroup_client import ( + securitygroup_client, +) + + +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 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): + """Check if E2E Networks security groups do not allow inbound all-protocol traffic from any source.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for group in securitygroup_client.security_groups: + report = CheckReportE2eNetworks(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/e2enetworks/services/securitygroup/securitygroup_restrictive_default/__init__.py b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json new file mode 100644 index 0000000000..15202d4a86 --- /dev/null +++ b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.metadata.json @@ -0,0 +1,37 @@ +{ + "Provider": "e2enetworks", + "CheckID": "securitygroup_restrictive_default", + "CheckTitle": "E2E Networks nodes do not rely on permissive default security groups", + "CheckType": [], + "ServiceName": "securitygroup", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "network", + "Description": "**E2E Networks nodes** should not rely solely on **default security groups** that have overly permissive inbound rules.", + "Risk": "Nodes that rely only on **permissive default security groups** inherit broad inbound access, weakening **network segmentation** and trust boundaries.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/network/security-group/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach custom security groups with **least-privilege** rules to nodes instead of relying on permissive defaults.", + "Url": "https://hub.prowler.com/check/securitygroup_restrictive_default" + } + }, + "Categories": [ + "trust-boundaries", + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py new file mode 100644 index 0000000000..39d750db6b --- /dev/null +++ b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default.py @@ -0,0 +1,49 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.securitygroup.securitygroup_client import ( + securitygroup_client, +) + + +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 or "").lower() == "inbound" + and (rule.protocol_name or "").lower() == "all" + and (_is_open_network(rule.network) or _is_open_network(rule.network_cidr)) + ): + return True + return False + + +class securitygroup_restrictive_default(Check): + """Check if E2E Networks nodes do not rely on permissive default security groups.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + 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 = CheckReportE2eNetworks(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/e2enetworks/services/securitygroup/securitygroup_service.py b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_service.py new file mode 100644 index 0000000000..7a874d85ae --- /dev/null +++ b/prowler/providers/e2enetworks/services/securitygroup/securitygroup_service.py @@ -0,0 +1,147 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2enetworks.lib.service.service import E2eNetworksService + + +class SecurityGroups(E2eNetworksService): + """Service class for E2E Networks 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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _fetch_node_security_groups(self): + from prowler.providers.e2enetworks.services.node.node_client import node_client + + for node in node_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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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/e2enetworks/services/storage/__init__.py b/prowler/providers/e2enetworks/services/storage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/__init__.py b/prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json b/prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json new file mode 100644 index 0000000000..9c3b0b3391 --- /dev/null +++ b/prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "storage_block_volume_not_orphaned", + "CheckTitle": "E2E Networks block volumes are not orphaned", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "low", + "ResourceType": "NotDefined", + "ResourceGroup": "storage", + "Description": "**E2E Networks block volumes** in the `Available` state should be attached to a node rather than left provisioned without an owner.", + "Risk": "**Orphaned block volumes** remain provisioned without an attached workload, increasing storage cost and the risk of retaining **sensitive data** without active ownership.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/storage/block-storage/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Attach `Available` block volumes to nodes, or delete unused volumes after confirming the data is no longer needed.", + "Url": "https://hub.prowler.com/check/storage_block_volume_not_orphaned" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py b/prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py new file mode 100644 index 0000000000..0bbf7d488a --- /dev/null +++ b/prowler/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned.py @@ -0,0 +1,18 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.storage.storage_client import storage_client + + +class storage_block_volume_not_orphaned(Check): + """Check that available block volumes are attached to a node.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for volume in storage_client.block_volumes: + report = CheckReportE2eNetworks(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/e2enetworks/services/storage/storage_client.py b/prowler/providers/e2enetworks/services/storage/storage_client.py new file mode 100644 index 0000000000..b71905e363 --- /dev/null +++ b/prowler/providers/e2enetworks/services/storage/storage_client.py @@ -0,0 +1,4 @@ +from prowler.providers.common.provider import Provider +from prowler.providers.e2enetworks.services.storage.storage_service import Storage + +storage_client = Storage(Provider.get_global_provider()) diff --git a/prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/__init__.py b/prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json b/prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json new file mode 100644 index 0000000000..7be4fdafe0 --- /dev/null +++ b/prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "e2enetworks", + "CheckID": "storage_efs_backup_enabled", + "CheckTitle": "E2E Networks EFS volumes have backup enabled", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "storage", + "Description": "**E2E Networks EFS shared file systems** should have **backups** enabled so data can be recovered after failures or accidental changes.", + "Risk": "Shared file systems without **backups** increase the risk of permanent **data loss** during failures or accidental changes.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/storage/parallel-file-storage/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable **backups** on EFS shared file systems to allow recovery after failures or accidental changes.", + "Url": "https://hub.prowler.com/check/storage_efs_backup_enabled" + } + }, + "Categories": [ + "resilience" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py b/prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py new file mode 100644 index 0000000000..0e8272b338 --- /dev/null +++ b/prowler/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.storage.storage_client import storage_client + + +class storage_efs_backup_enabled(Check): + """Check if E2E Networks EFS volumes have backup enabled.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for volume in storage_client.efs_volumes: + report = CheckReportE2eNetworks(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/e2enetworks/services/storage/storage_efs_vpc_access_restricted/__init__.py b/prowler/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json b/prowler/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json new file mode 100644 index 0000000000..1fd4d54e63 --- /dev/null +++ b/prowler/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "e2enetworks", + "CheckID": "storage_efs_vpc_access_restricted", + "CheckTitle": "E2E Networks EFS volumes restrict VPC access", + "CheckType": [], + "ServiceName": "storage", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "NotDefined", + "ResourceGroup": "storage", + "Description": "**E2E Networks EFS volumes** should restrict access to specific VPC resources instead of allowing every resource in the **VPC** to connect.", + "Risk": "EFS volumes that allow **all VPC resources** broaden access beyond **least privilege** within the network, increasing the blast radius of a compromised resource.", + "RelatedUrl": "", + "AdditionalURLs": [ + "https://docs.e2enetworks.com/api/myaccount/storage/parallel-file-storage/" + ], + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Restrict EFS volume access to specific VPC resources instead of allowing all resources in the VPC.", + "Url": "https://hub.prowler.com/check/storage_efs_vpc_access_restricted" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py b/prowler/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py new file mode 100644 index 0000000000..c3bd715fa6 --- /dev/null +++ b/prowler/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, CheckReportE2eNetworks +from prowler.providers.e2enetworks.services.storage.storage_client import storage_client + + +class storage_efs_vpc_access_restricted(Check): + """Check if E2E Networks EFS volumes restrict VPC access.""" + + def execute(self) -> list[CheckReportE2eNetworks]: + findings = [] + for volume in storage_client.efs_volumes: + report = CheckReportE2eNetworks(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/e2enetworks/services/storage/storage_service.py b/prowler/providers/e2enetworks/services/storage/storage_service.py new file mode 100644 index 0000000000..9038d727f1 --- /dev/null +++ b/prowler/providers/e2enetworks/services/storage/storage_service.py @@ -0,0 +1,158 @@ +from pydantic.v1 import BaseModel + +from prowler.lib.logger import logger +from prowler.providers.e2enetworks.lib.service.service import E2eNetworksService + + +class Storage(E2eNetworksService): + """Service class for E2E Networks storage resources.""" + + def __init__(self, provider): + super().__init__("storage", provider) + self.block_volumes: list[BlockVolume] = [] + self.efs_volumes: list[EfsVolume] = [] + self.epfs_volumes: list[EpfsVolume] = [] + self._fetch_block_volumes() + self._fetch_efs_volumes() + self._fetch_epfs_volumes() + + 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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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} -- " + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {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 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: + return self.id + + @property + def resource_name(self) -> str: + return self.name diff --git a/tests/config/schema/other_providers_schema_test.py b/tests/config/schema/other_providers_schema_test.py index 5cf13918be..0406776f57 100644 --- a/tests/config/schema/other_providers_schema_test.py +++ b/tests/config/schema/other_providers_schema_test.py @@ -262,3 +262,15 @@ class Test_OpenStack_Schema: def test_zero_threshold_dropped(self): assert _validate("openstack", {"image_sharing_threshold": 0}) == {} + + +class Test_E2ENetworks_Schema: + def test_valid_values_round_trip(self): + raw = {"require_bitninja_on_load_balancers": True} + assert _validate("e2enetworks", raw) == raw + + def test_non_bool_value_dropped(self): + assert ( + _validate("e2enetworks", {"require_bitninja_on_load_balancers": "maybe"}) + == {} + ) diff --git a/tests/lib/cli/parser_test.py b/tests/lib/cli/parser_test.py index 94b6ad2b4d..549ca17727 100644 --- a/tests/lib/cli/parser_test.py +++ b/tests/lib/cli/parser_test.py @@ -17,7 +17,7 @@ prowler_command = "prowler" # 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,linode,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,linode,e2enetworks,dashboard,iac,image,llm} ..." def mock_get_available_providers(): @@ -40,6 +40,7 @@ def mock_get_available_providers(): "openstack", "stackit", "linode", + "e2enetworks", ] diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index f843cb8f00..1315970201 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -1627,6 +1627,40 @@ class TestFinding: 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_e2enetworks(self): + provider = MagicMock() + provider.type = "e2enetworks" + 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="e2enetworks") + 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 536e40f808..3eccc8ba8d 100644 --- a/tests/lib/outputs/html/html_test.py +++ b/tests/lib/outputs/html/html_test.py @@ -999,6 +999,38 @@ class TestHTML: assert "Project ID: f033ea6d-8697-40eb-a60e-acfa9128480d" in summary assert "Project Name:" not in summary + def test_e2enetworks_get_assessment_summary(self): + """Test E2E Networks HTML assessment summary shows project and locations.""" + findings = [generate_finding_output()] + output = HTML(findings) + + provider = MagicMock() + provider.type = "e2enetworks" + provider.identity.project_id = 12345 + provider.identity.locations = ["Delhi", "Chennai"] + + summary = output.get_assessment_summary(provider) + + assert "E2E Networks 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_e2enetworks_get_assessment_summary_escapes_locations(self): + """Test E2E Networks HTML assessment summary escapes user-controlled locations.""" + findings = [generate_finding_output()] + output = HTML(findings) + + provider = MagicMock() + provider.type = "e2enetworks" + provider.identity.project_id = 12345 + provider.identity.locations = ['Delhi">'] + + summary = output.get_assessment_summary(provider) + + assert "" not in summary + assert "Delhi"><script>alert(1)</script>" 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 ca18717847..fad6f4d286 100644 --- a/tests/lib/outputs/outputs_test.py +++ b/tests/lib/outputs/outputs_test.py @@ -1293,3 +1293,23 @@ class TestReport: with mock.patch("builtins.print") as mocked_print: report([finding], provider, output_options) mocked_print.assert_called() + + def test_report_with_e2enetworks_provider(self): + finding = MagicMock() + finding.status = "PASS" + finding.muted = False + finding.location = "Delhi" + finding.check_metadata.Provider = "e2enetworks" + 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 = "e2enetworks" + + 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 0c842225cc..57e39efb94 100644 --- a/tests/lib/outputs/summary_table_test.py +++ b/tests/lib/outputs/summary_table_test.py @@ -72,6 +72,34 @@ class TestDisplaySummaryTable: assert "Project" in captured.out assert "my-prod-env" in captured.out + def test_e2enetworks_summary(self, capsys): + provider = SimpleNamespace( + type="e2enetworks", + 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="e2enetworks", + 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/e2enetworks/e2enetworks_fixtures.py b/tests/providers/e2enetworks/e2enetworks_fixtures.py new file mode 100644 index 0000000000..1f493d3bb8 --- /dev/null +++ b/tests/providers/e2enetworks/e2enetworks_fixtures.py @@ -0,0 +1,55 @@ +import importlib +import sys +from unittest.mock import MagicMock, patch + +from prowler.providers.e2enetworks.e2enetworks_provider import E2enetworksProvider +from prowler.providers.e2enetworks.models import ( + E2eNetworksIdentityInfo, + E2eNetworksSession, +) + + +def run_e2enetworks_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_e2enetworks_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_e2enetworks_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=E2enetworksProvider) + provider.type = "e2enetworks" + provider.audit_config = audit_config or {} + provider.fixer_config = fixer_config or {} + provider.identity = E2eNetworksIdentityInfo( + project_id=project_id, + locations=locations or ["Delhi"], + ) + provider.session = E2eNetworksSession( + 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/e2enetworks/e2enetworks_metadata_test.py b/tests/providers/e2enetworks/e2enetworks_metadata_test.py new file mode 100644 index 0000000000..54639bbf2c --- /dev/null +++ b/tests/providers/e2enetworks/e2enetworks_metadata_test.py @@ -0,0 +1,37 @@ +from pathlib import Path + +import pytest + +from prowler.lib.check.models import CheckMetadata + +EXPECTED_SERVICE_NAMES = { + "database": "database", + "loadbalancer": "loadbalancer", + "network": "network", + "node": "node", + "securitygroup": "securitygroup", + "storage": "storage", +} + + +@pytest.mark.parametrize( + "metadata_file", + sorted(Path("prowler/providers/e2enetworks").glob("services/**/*.metadata.json")), +) +def test_e2enetworks_check_metadata_is_valid(metadata_file): + metadata = CheckMetadata.parse_file(metadata_file) + assert metadata.Provider == "e2enetworks" + assert metadata.CheckID == metadata_file.stem.replace(".metadata", "") + + +@pytest.mark.parametrize( + "metadata_file", + sorted(Path("prowler/providers/e2enetworks").glob("services/**/*.metadata.json")), +) +def test_e2enetworks_check_metadata_uses_service_folder_names(metadata_file): + metadata = CheckMetadata.parse_file(metadata_file) + service_folder = metadata_file.relative_to( + Path("prowler/providers/e2enetworks/services") + ).parts[0] + + assert metadata.ServiceName == EXPECTED_SERVICE_NAMES[service_folder] diff --git a/tests/providers/e2enetworks/e2enetworks_provider_test.py b/tests/providers/e2enetworks/e2enetworks_provider_test.py new file mode 100644 index 0000000000..a8743f38d8 --- /dev/null +++ b/tests/providers/e2enetworks/e2enetworks_provider_test.py @@ -0,0 +1,93 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest + +from prowler.providers.common.models import Connection +from prowler.providers.e2enetworks.e2enetworks_provider import E2enetworksProvider +from prowler.providers.e2enetworks.exceptions.exceptions import ( + E2eNetworksCredentialsError, +) + + +class TestE2enetworksProvider: + @patch( + "prowler.providers.e2enetworks.e2enetworks_provider.load_and_validate_config_file" + ) + def test_e2enetworks_provider_init_success(self, mock_load_config): + mock_load_config.return_value = {} + + provider = E2enetworksProvider( + 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.e2enetworks.e2enetworks_provider.load_and_validate_config_file" + ) + def test_e2enetworks_provider_init_missing_credentials(self, mock_load_config): + mock_load_config.return_value = {} + + with pytest.raises(E2eNetworksCredentialsError): + E2enetworksProvider(api_key="", auth_token="token", project_id="1") + + @patch.dict( + os.environ, + { + "E2E_NETWORKS_API_KEY": "env-api-key", + "E2E_NETWORKS_AUTH_TOKEN": "env-auth-token", + "E2E_NETWORKS_PROJECT_ID": "99", + "E2E_NETWORKS_REGION": "Chennai", + }, + clear=False, + ) + @patch( + "prowler.providers.e2enetworks.e2enetworks_provider.load_and_validate_config_file" + ) + def test_e2enetworks_provider_init_from_env(self, mock_load_config): + mock_load_config.return_value = {} + + provider = E2enetworksProvider() + + assert provider.session.api_key == "env-api-key" + assert provider.session.project_id == 99 + assert provider.session.locations == ["Chennai"] + + @patch( + "prowler.providers.e2enetworks.e2enetworks_provider.E2enetworksProvider.setup_session" + ) + def test_e2enetworks_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 = E2enetworksProvider.test_connection( + api_key="key", + auth_token="token", + project_id=1, + locations=["Delhi"], + ) + + assert result == Connection(is_connected=True) + + def test_e2enetworks_test_connection_missing_credentials(self): + with pytest.raises(E2eNetworksCredentialsError): + E2enetworksProvider.test_connection( + api_key="", auth_token="", project_id=None + ) diff --git a/tests/providers/e2enetworks/lib/api/e2enetworks_api_client_test.py b/tests/providers/e2enetworks/lib/api/e2enetworks_api_client_test.py new file mode 100644 index 0000000000..e9a3f960e7 --- /dev/null +++ b/tests/providers/e2enetworks/lib/api/e2enetworks_api_client_test.py @@ -0,0 +1,43 @@ +from unittest.mock import MagicMock, patch + +import pytest +import requests + +from prowler.providers.e2enetworks.exceptions.exceptions import E2eNetworksAPIError +from prowler.providers.e2enetworks.lib.api.client import E2eNetworksAPIClient +from prowler.providers.e2enetworks.models import E2eNetworksSession + + +class TestE2eNetworksAPIClient: + def test_get_redacts_api_key_from_errors(self): + session = E2eNetworksSession( + api_key="secret-api-key", + auth_token="secret-auth-token", + project_id=123, + locations=["Delhi"], + http_session=MagicMock(), + ) + session.http_session.get.side_effect = requests.exceptions.HTTPError( + "404 Client Error: Not Found for url: " + "https://api.e2enetworks.com/myaccount/api/v1/nodes/" + "?apikey=secret-api-key&project_id=123&location=Delhi " + "Authorization: Bearer secret-auth-token" + ) + + client = E2eNetworksAPIClient(session) + + with patch( + "prowler.providers.e2enetworks.lib.api.client.logger.error" + ) as mock_logger: + with pytest.raises(E2eNetworksAPIError) as error: + client.get("/nodes/", location="Delhi") + + logged_message = mock_logger.call_args.args[0] + assert "secret-api-key" not in logged_message + assert "secret-auth-token" not in logged_message + assert "apikey=REDACTED" in logged_message + assert "Bearer REDACTED" in logged_message + assert "secret-api-key" not in str(error.value) + assert "secret-auth-token" not in str(error.value) + assert "apikey=REDACTED" in str(error.value) + assert "Bearer REDACTED" in str(error.value) diff --git a/tests/providers/e2enetworks/lib/arguments/e2enetworks_arguments_test.py b/tests/providers/e2enetworks/lib/arguments/e2enetworks_arguments_test.py new file mode 100644 index 0000000000..22fa0beb46 --- /dev/null +++ b/tests/providers/e2enetworks/lib/arguments/e2enetworks_arguments_test.py @@ -0,0 +1,44 @@ +from unittest.mock import MagicMock + +from prowler.providers.e2enetworks.lib.arguments.arguments import validate_arguments + + +class TestE2eArguments: + def test_validate_arguments_success(self): + arguments = MagicMock() + arguments.e2e_networks_api_key = "key" + arguments.e2e_networks_auth_token = "token" + arguments.e2e_networks_project_id = "123" + + valid, message = validate_arguments(arguments) + + assert valid is True + assert message == "" + + def test_validate_arguments_missing_credentials_allowed(self, monkeypatch): + # Listing operations must work without credentials; presence is enforced + # later at provider initialization, not here. + monkeypatch.delenv("E2E_NETWORKS_API_KEY", raising=False) + monkeypatch.delenv("E2E_NETWORKS_AUTH_TOKEN", raising=False) + monkeypatch.delenv("E2E_NETWORKS_PROJECT_ID", raising=False) + + arguments = MagicMock() + arguments.e2e_networks_api_key = None + arguments.e2e_networks_auth_token = None + arguments.e2e_networks_project_id = None + + valid, message = validate_arguments(arguments) + + assert valid is True + assert message == "" + + def test_validate_arguments_invalid_project_id(self): + arguments = MagicMock() + arguments.e2e_networks_api_key = "key" + arguments.e2e_networks_auth_token = "token" + arguments.e2e_networks_project_id = "abc" + + valid, message = validate_arguments(arguments) + + assert valid is False + assert "integer" in message diff --git a/tests/providers/e2enetworks/lib/mutelist/e2enetworks_mutelist_test.py b/tests/providers/e2enetworks/lib/mutelist/e2enetworks_mutelist_test.py new file mode 100644 index 0000000000..968822c253 --- /dev/null +++ b/tests/providers/e2enetworks/lib/mutelist/e2enetworks_mutelist_test.py @@ -0,0 +1,65 @@ +from unittest.mock import MagicMock + +from prowler.providers.e2enetworks.lib.mutelist.mutelist import E2eNetworksMutelist + + +def _build_finding(check_id: str, location: str, resource_id: str, resource_name: str): + finding = MagicMock() + finding.check_metadata = MagicMock() + finding.check_metadata.CheckID = check_id + finding.status = "FAIL" + finding.location = location + finding.resource_id = resource_id + finding.resource_name = resource_name + finding.resource_tags = [] + return finding + + +class Test_e2enetworks_mutelist: + def test_is_finding_muted(self): + mutelist_content = { + "Accounts": { + "*": { + "Checks": { + "node_public_ip_not_assigned": { + "Regions": ["Delhi"], + "Resources": ["example-node-id"], + } + } + } + } + } + + mutelist = E2eNetworksMutelist(mutelist_content=mutelist_content) + finding = _build_finding( + "node_public_ip_not_assigned", + "Delhi", + "example-node-id", + "example-node-id", + ) + + assert mutelist.is_finding_muted(finding=finding) is True + + def test_is_finding_not_muted_wrong_location(self): + mutelist_content = { + "Accounts": { + "*": { + "Checks": { + "node_public_ip_not_assigned": { + "Regions": ["Delhi"], + "Resources": ["example-node-id"], + } + } + } + } + } + + mutelist = E2eNetworksMutelist(mutelist_content=mutelist_content) + finding = _build_finding( + "node_public_ip_not_assigned", + "Mumbai", + "example-node-id", + "example-node-id", + ) + + assert mutelist.is_finding_muted(finding=finding) is False diff --git a/tests/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled_test.py b/tests/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled_test.py new file mode 100644 index 0000000000..acfa9e85ed --- /dev/null +++ b/tests/providers/e2enetworks/services/database/database_cluster_backup_enabled/database_cluster_backup_enabled_test.py @@ -0,0 +1,68 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.database.database_service import ( + DatabaseCluster, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.database.database_cluster_backup_enabled.database_cluster_backup_enabled.database_client" + + +class Test_database_cluster_backup_enabled: + def test_no_clusters(self): + client = mock.MagicMock() + client.clusters = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_backup_enabled.database_cluster_backup_enabled import ( + database_cluster_backup_enabled, + ) + + assert database_cluster_backup_enabled().execute() == [] + + def test_database_cluster_backup_enabled_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster(id="1", name="ok", location="Delhi", backup_enabled=True), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_backup_enabled.database_cluster_backup_enabled import ( + database_cluster_backup_enabled, + ) + + findings = database_cluster_backup_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_database_cluster_backup_enabled_non_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster(id="2", name="bad", location="Delhi", backup_enabled=False), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_backup_enabled.database_cluster_backup_enabled import ( + database_cluster_backup_enabled, + ) + + findings = database_cluster_backup_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username_test.py b/tests/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username_test.py new file mode 100644 index 0000000000..80a37f4d97 --- /dev/null +++ b/tests/providers/e2enetworks/services/database/database_cluster_default_admin_username/database_cluster_default_admin_username_test.py @@ -0,0 +1,72 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.database.database_service import ( + DatabaseCluster, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.database.database_cluster_default_admin_username.database_cluster_default_admin_username.database_client" + + +class Test_database_cluster_default_admin_username: + def test_no_clusters(self): + client = mock.MagicMock() + client.clusters = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_default_admin_username.database_cluster_default_admin_username import ( + database_cluster_default_admin_username, + ) + + assert database_cluster_default_admin_username().execute() == [] + + def test_database_cluster_default_admin_username_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster( + id="1", name="ok", location="Delhi", master_username="dbadmin" + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_default_admin_username.database_cluster_default_admin_username import ( + database_cluster_default_admin_username, + ) + + findings = database_cluster_default_admin_username().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_database_cluster_default_admin_username_non_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster( + id="2", name="bad", location="Delhi", master_username="admin" + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_default_admin_username.database_cluster_default_admin_username import ( + database_cluster_default_admin_username, + ) + + findings = database_cluster_default_admin_username().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured_test.py b/tests/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured_test.py new file mode 100644 index 0000000000..76adbbc5c2 --- /dev/null +++ b/tests/providers/e2enetworks/services/database/database_cluster_ip_whitelist_configured/database_cluster_ip_whitelist_configured_test.py @@ -0,0 +1,80 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.database.database_service import ( + DatabaseCluster, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.database.database_cluster_ip_whitelist_configured.database_cluster_ip_whitelist_configured.database_client" + + +class Test_database_cluster_ip_whitelist_configured: + def test_no_clusters(self): + client = mock.MagicMock() + client.clusters = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_ip_whitelist_configured.database_cluster_ip_whitelist_configured import ( + database_cluster_ip_whitelist_configured, + ) + + assert database_cluster_ip_whitelist_configured().execute() == [] + + def test_database_cluster_ip_whitelist_configured_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster( + id="1", + name="ok", + location="Delhi", + master_has_public_ip=True, + whitelisted_ips=["203.0.113.0/24"], + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_ip_whitelist_configured.database_cluster_ip_whitelist_configured import ( + database_cluster_ip_whitelist_configured, + ) + + findings = database_cluster_ip_whitelist_configured().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_database_cluster_ip_whitelist_configured_non_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster( + id="2", + name="bad", + location="Delhi", + master_has_public_ip=True, + whitelisted_ips=[], + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_ip_whitelist_configured.database_cluster_ip_whitelist_configured import ( + database_cluster_ip_whitelist_configured, + ) + + findings = database_cluster_ip_whitelist_configured().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned_test.py b/tests/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned_test.py new file mode 100644 index 0000000000..4d8e4cbb27 --- /dev/null +++ b/tests/providers/e2enetworks/services/database/database_cluster_public_ip_not_assigned/database_cluster_public_ip_not_assigned_test.py @@ -0,0 +1,72 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.database.database_service import ( + DatabaseCluster, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.database.database_cluster_public_ip_not_assigned.database_cluster_public_ip_not_assigned.database_client" + + +class Test_database_cluster_public_ip_not_assigned: + def test_no_clusters(self): + client = mock.MagicMock() + client.clusters = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_public_ip_not_assigned.database_cluster_public_ip_not_assigned import ( + database_cluster_public_ip_not_assigned, + ) + + assert database_cluster_public_ip_not_assigned().execute() == [] + + def test_database_cluster_public_ip_not_assigned_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster( + id="1", name="ok", location="Delhi", master_has_public_ip=False + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_public_ip_not_assigned.database_cluster_public_ip_not_assigned import ( + database_cluster_public_ip_not_assigned, + ) + + findings = database_cluster_public_ip_not_assigned().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_database_cluster_public_ip_not_assigned_non_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster( + id="2", name="bad", location="Delhi", master_has_public_ip=True + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_public_ip_not_assigned.database_cluster_public_ip_not_assigned import ( + database_cluster_public_ip_not_assigned, + ) + + findings = database_cluster_public_ip_not_assigned().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running_test.py b/tests/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running_test.py new file mode 100644 index 0000000000..c76b4cb7bd --- /dev/null +++ b/tests/providers/e2enetworks/services/database/database_cluster_running/database_cluster_running_test.py @@ -0,0 +1,68 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.database.database_service import ( + DatabaseCluster, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.database.database_cluster_running.database_cluster_running.database_client" + + +class Test_database_cluster_running: + def test_no_clusters(self): + client = mock.MagicMock() + client.clusters = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_running.database_cluster_running import ( + database_cluster_running, + ) + + assert database_cluster_running().execute() == [] + + def test_database_cluster_running_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster(id="1", name="ok", location="Delhi", status="RUNNING"), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_running.database_cluster_running import ( + database_cluster_running, + ) + + findings = database_cluster_running().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_database_cluster_running_non_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster(id="2", name="bad", location="Delhi", status="STOPPED"), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_running.database_cluster_running import ( + database_cluster_running, + ) + + findings = database_cluster_running().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled_test.py b/tests/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled_test.py new file mode 100644 index 0000000000..e1e247fa48 --- /dev/null +++ b/tests/providers/e2enetworks/services/database/database_cluster_ssl_enabled/database_cluster_ssl_enabled_test.py @@ -0,0 +1,72 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.database.database_service import ( + DatabaseCluster, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.database.database_cluster_ssl_enabled.database_cluster_ssl_enabled.database_client" + + +class Test_database_cluster_ssl_enabled: + def test_no_clusters(self): + client = mock.MagicMock() + client.clusters = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_ssl_enabled.database_cluster_ssl_enabled import ( + database_cluster_ssl_enabled, + ) + + assert database_cluster_ssl_enabled().execute() == [] + + def test_database_cluster_ssl_enabled_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster( + id="1", name="ok", location="Delhi", master_ssl_enabled=True + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_ssl_enabled.database_cluster_ssl_enabled import ( + database_cluster_ssl_enabled, + ) + + findings = database_cluster_ssl_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_database_cluster_ssl_enabled_non_compliant(self): + client = mock.MagicMock() + client.clusters = [ + DatabaseCluster( + id="2", name="bad", location="Delhi", master_ssl_enabled=False + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_cluster_ssl_enabled.database_cluster_ssl_enabled import ( + database_cluster_ssl_enabled, + ) + + findings = database_cluster_ssl_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned_test.py b/tests/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned_test.py new file mode 100644 index 0000000000..068dd857b7 --- /dev/null +++ b/tests/providers/e2enetworks/services/database/database_replica_public_ip_not_assigned/database_replica_public_ip_not_assigned_test.py @@ -0,0 +1,84 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.database.database_service import ( + DatabaseInstance, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.database.database_replica_public_ip_not_assigned.database_replica_public_ip_not_assigned.database_client" + + +class Test_database_replica_public_ip_not_assigned: + def test_no_instances(self): + client = mock.MagicMock() + client.instances = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_replica_public_ip_not_assigned.database_replica_public_ip_not_assigned import ( + database_replica_public_ip_not_assigned, + ) + + assert database_replica_public_ip_not_assigned().execute() == [] + + def test_database_replica_public_ip_not_assigned_compliant(self): + client = mock.MagicMock() + client.instances = [ + DatabaseInstance( + id="1", + name="ok", + cluster_id="c1", + cluster_name="cluster", + location="Delhi", + role="replica", + has_public_ip=False, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_replica_public_ip_not_assigned.database_replica_public_ip_not_assigned import ( + database_replica_public_ip_not_assigned, + ) + + findings = database_replica_public_ip_not_assigned().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_database_replica_public_ip_not_assigned_non_compliant(self): + client = mock.MagicMock() + client.instances = [ + DatabaseInstance( + id="2", + name="bad", + cluster_id="c1", + cluster_name="cluster", + location="Delhi", + role="replica", + has_public_ip=True, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.database.database_replica_public_ip_not_assigned.database_replica_public_ip_not_assigned import ( + database_replica_public_ip_not_assigned, + ) + + findings = database_replica_public_ip_not_assigned().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/database/e2enetworks_database_service_test.py b/tests/providers/e2enetworks/services/database/e2enetworks_database_service_test.py new file mode 100644 index 0000000000..58697e86c8 --- /dev/null +++ b/tests/providers/e2enetworks/services/database/e2enetworks_database_service_test.py @@ -0,0 +1,70 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.e2enetworks.services.database.database_service import Database + + +class TestDatabaseService: + @patch( + "prowler.providers.e2enetworks.services.database.database_service.E2eNetworksService.__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/e2enetworks/services/loadbalancer/e2enetworks_loadbalancer_service_test.py b/tests/providers/e2enetworks/services/loadbalancer/e2enetworks_loadbalancer_service_test.py new file mode 100644 index 0000000000..6f7885c48d --- /dev/null +++ b/tests/providers/e2enetworks/services/loadbalancer/e2enetworks_loadbalancer_service_test.py @@ -0,0 +1,81 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_service import ( + LoadBalancers, +) + + +class TestLoadBalancerService: + @patch( + "prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_service.E2eNetworksService.__init__" + ) + def test_fetch_loadbalancers_parses_context(self, mock_super_init): + mock_super_init.return_value = None + + provider = MagicMock() + provider.session.locations = ["Delhi"] + service = LoadBalancers.__new__(LoadBalancers) + service.provider = provider + service.client = MagicMock() + service.load_balancers = [] + + service.client.paginate.return_value = [ + { + "id": 42, + "name": "web-alb", + "status": "running", + "node_detail": {"public_ip": "164.52.2.2"}, + "appliance_instance": [ + { + "context": { + "lb_mode": "HTTPS", + "lb_port": 443, + "enable_bitninja": True, + "ssl_context": {"ssl_certificate_id": 7}, + "backends": [{"http_check": True}], + } + } + ], + } + ] + + service._fetch_loadbalancers() + + assert len(service.load_balancers) == 1 + lb = service.load_balancers[0] + assert lb.id == "42" + assert lb.name == "web-alb" + assert lb.location == "Delhi" + assert lb.lb_mode == "HTTPS" + assert lb.enable_bitninja is True + assert lb.ssl_certificate_id == "7" + assert lb.public_ip == "164.52.2.2" + assert lb.is_alb is True + assert lb.is_alb_https is True + assert lb.has_backend_health_check is True + + @patch( + "prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_service.E2eNetworksService.__init__" + ) + def test_fetch_loadbalancers_handles_missing_context(self, mock_super_init): + mock_super_init.return_value = None + + provider = MagicMock() + provider.session.locations = ["Delhi"] + service = LoadBalancers.__new__(LoadBalancers) + service.provider = provider + service.client = MagicMock() + service.load_balancers = [] + + service.client.paginate.return_value = [ + {"id": 1, "name": "tcp-lb", "status": "running"} + ] + + service._fetch_loadbalancers() + + assert len(service.load_balancers) == 1 + lb = service.load_balancers[0] + assert lb.enable_bitninja is False + assert lb.ssl_certificate_id is None + assert lb.is_alb is False + assert lb.has_backend_health_check is False diff --git a/tests/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate_test.py b/tests/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate_test.py new file mode 100644 index 0000000000..58c5469ec3 --- /dev/null +++ b/tests/providers/e2enetworks/services/loadbalancer/loadbalancer_alb_https_uses_ssl_certificate/loadbalancer_alb_https_uses_ssl_certificate_test.py @@ -0,0 +1,80 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_service import ( + LoadBalancer, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_alb_https_uses_ssl_certificate.loadbalancer_alb_https_uses_ssl_certificate.loadbalancer_client" + + +class Test_loadbalancer_alb_https_uses_ssl_certificate: + def test_no_load_balancers(self): + client = mock.MagicMock() + client.load_balancers = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_alb_https_uses_ssl_certificate.loadbalancer_alb_https_uses_ssl_certificate import ( + loadbalancer_alb_https_uses_ssl_certificate, + ) + + assert loadbalancer_alb_https_uses_ssl_certificate().execute() == [] + + def test_loadbalancer_alb_https_uses_ssl_certificate_compliant(self): + client = mock.MagicMock() + client.load_balancers = [ + LoadBalancer( + id="1", + name="ok", + location="Delhi", + lb_mode="HTTPS", + ssl_certificate_id="cert-1", + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_alb_https_uses_ssl_certificate.loadbalancer_alb_https_uses_ssl_certificate import ( + loadbalancer_alb_https_uses_ssl_certificate, + ) + + findings = loadbalancer_alb_https_uses_ssl_certificate().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_loadbalancer_alb_https_uses_ssl_certificate_non_compliant(self): + client = mock.MagicMock() + client.load_balancers = [ + LoadBalancer( + id="2", + name="bad", + location="Delhi", + lb_mode="HTTPS", + ssl_certificate_id=None, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_alb_https_uses_ssl_certificate.loadbalancer_alb_https_uses_ssl_certificate import ( + loadbalancer_alb_https_uses_ssl_certificate, + ) + + findings = loadbalancer_alb_https_uses_ssl_certificate().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled_test.py b/tests/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled_test.py new file mode 100644 index 0000000000..5fc23155a3 --- /dev/null +++ b/tests/providers/e2enetworks/services/loadbalancer/loadbalancer_backend_health_check_enabled/loadbalancer_backend_health_check_enabled_test.py @@ -0,0 +1,76 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_service import ( + LoadBalancer, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_backend_health_check_enabled.loadbalancer_backend_health_check_enabled.loadbalancer_client" + + +class Test_loadbalancer_backend_health_check_enabled: + def test_no_load_balancers(self): + client = mock.MagicMock() + client.load_balancers = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_backend_health_check_enabled.loadbalancer_backend_health_check_enabled import ( + loadbalancer_backend_health_check_enabled, + ) + + assert loadbalancer_backend_health_check_enabled().execute() == [] + + def test_loadbalancer_backend_health_check_enabled_compliant(self): + client = mock.MagicMock() + client.load_balancers = [ + LoadBalancer( + id="1", + name="ok", + location="Delhi", + lb_mode="HTTP", + backends=[{"http_check": True}], + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_backend_health_check_enabled.loadbalancer_backend_health_check_enabled import ( + loadbalancer_backend_health_check_enabled, + ) + + findings = loadbalancer_backend_health_check_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_loadbalancer_backend_health_check_enabled_non_compliant(self): + client = mock.MagicMock() + client.load_balancers = [ + LoadBalancer( + id="2", name="bad", location="Delhi", lb_mode="HTTP", backends=[{}] + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_backend_health_check_enabled.loadbalancer_backend_health_check_enabled import ( + loadbalancer_backend_health_check_enabled, + ) + + findings = loadbalancer_backend_health_check_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled_test.py b/tests/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled_test.py new file mode 100644 index 0000000000..f521ec4397 --- /dev/null +++ b/tests/providers/e2enetworks/services/loadbalancer/loadbalancer_bitninja_enabled/loadbalancer_bitninja_enabled_test.py @@ -0,0 +1,68 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_service import ( + LoadBalancer, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_bitninja_enabled.loadbalancer_bitninja_enabled.loadbalancer_client" + + +class Test_loadbalancer_bitninja_enabled: + def test_no_load_balancers(self): + client = mock.MagicMock() + client.load_balancers = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_bitninja_enabled.loadbalancer_bitninja_enabled import ( + loadbalancer_bitninja_enabled, + ) + + assert loadbalancer_bitninja_enabled().execute() == [] + + def test_loadbalancer_bitninja_enabled_compliant(self): + client = mock.MagicMock() + client.load_balancers = [ + LoadBalancer(id="1", name="ok", location="Delhi", enable_bitninja=True), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_bitninja_enabled.loadbalancer_bitninja_enabled import ( + loadbalancer_bitninja_enabled, + ) + + findings = loadbalancer_bitninja_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_loadbalancer_bitninja_enabled_non_compliant(self): + client = mock.MagicMock() + client.load_balancers = [ + LoadBalancer(id="2", name="bad", location="Delhi", enable_bitninja=False), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.loadbalancer.loadbalancer_bitninja_enabled.loadbalancer_bitninja_enabled import ( + loadbalancer_bitninja_enabled, + ) + + findings = loadbalancer_bitninja_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/network/e2enetworks_network_service_test.py b/tests/providers/e2enetworks/services/network/e2enetworks_network_service_test.py new file mode 100644 index 0000000000..29a3ab1e5e --- /dev/null +++ b/tests/providers/e2enetworks/services/network/e2enetworks_network_service_test.py @@ -0,0 +1,74 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.e2enetworks.services.network.network_service import Network, Vpc + + +class TestNetworkService: + @patch( + "prowler.providers.e2enetworks.services.network.network_service.E2eNetworksService.__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/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached_test.py b/tests/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached_test.py new file mode 100644 index 0000000000..b3624b72c6 --- /dev/null +++ b/tests/providers/e2enetworks/services/network/network_reserveip_floating_ip_unattached/network_reserveip_floating_ip_unattached_test.py @@ -0,0 +1,82 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.network.network_service import ( + ReservedIp, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.network.network_reserveip_floating_ip_unattached.network_reserveip_floating_ip_unattached.network_client" + + +class Test_network_reserveip_floating_ip_unattached: + def test_no_reserved_ips(self): + client = mock.MagicMock() + client.reserved_ips = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_reserveip_floating_ip_unattached.network_reserveip_floating_ip_unattached import ( + network_reserveip_floating_ip_unattached, + ) + + assert network_reserveip_floating_ip_unattached().execute() == [] + + def test_network_reserveip_floating_ip_unattached_compliant(self): + client = mock.MagicMock() + client.reserved_ips = [ + ReservedIp( + reserve_id="1", + ip_address="1.2.3.4", + location="Delhi", + reserved_type="FloatingIP", + status="Attached", + floating_ip_attached_nodes_count=1, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_reserveip_floating_ip_unattached.network_reserveip_floating_ip_unattached import ( + network_reserveip_floating_ip_unattached, + ) + + findings = network_reserveip_floating_ip_unattached().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_network_reserveip_floating_ip_unattached_non_compliant(self): + client = mock.MagicMock() + client.reserved_ips = [ + ReservedIp( + reserve_id="2", + ip_address="5.6.7.8", + location="Delhi", + reserved_type="FloatingIP", + status="Available", + floating_ip_attached_nodes_count=0, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_reserveip_floating_ip_unattached.network_reserveip_floating_ip_unattached import ( + network_reserveip_floating_ip_unattached, + ) + + findings = network_reserveip_floating_ip_unattached().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip_test.py b/tests/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip_test.py new file mode 100644 index 0000000000..48cbed3691 --- /dev/null +++ b/tests/providers/e2enetworks/services/network/network_reserveip_orphaned_public_ip/network_reserveip_orphaned_public_ip_test.py @@ -0,0 +1,82 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.network.network_service import ( + ReservedIp, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.network.network_reserveip_orphaned_public_ip.network_reserveip_orphaned_public_ip.network_client" + + +class Test_network_reserveip_orphaned_public_ip: + def test_no_reserved_ips(self): + client = mock.MagicMock() + client.reserved_ips = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_reserveip_orphaned_public_ip.network_reserveip_orphaned_public_ip import ( + network_reserveip_orphaned_public_ip, + ) + + assert network_reserveip_orphaned_public_ip().execute() == [] + + def test_network_reserveip_orphaned_public_ip_compliant(self): + client = mock.MagicMock() + client.reserved_ips = [ + ReservedIp( + reserve_id="1", + ip_address="1.2.3.4", + location="Delhi", + reserved_type="PublicIP", + status="Attached", + vm_id=123, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_reserveip_orphaned_public_ip.network_reserveip_orphaned_public_ip import ( + network_reserveip_orphaned_public_ip, + ) + + findings = network_reserveip_orphaned_public_ip().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_network_reserveip_orphaned_public_ip_non_compliant(self): + client = mock.MagicMock() + client.reserved_ips = [ + ReservedIp( + reserve_id="2", + ip_address="5.6.7.8", + location="Delhi", + reserved_type="PublicIP", + status="Available", + vm_id=None, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_reserveip_orphaned_public_ip.network_reserveip_orphaned_public_ip import ( + network_reserveip_orphaned_public_ip, + ) + + findings = network_reserveip_orphaned_public_ip().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes_test.py b/tests/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes_test.py new file mode 100644 index 0000000000..5e677a3c11 --- /dev/null +++ b/tests/providers/e2enetworks/services/network/network_vpc_has_attached_nodes/network_vpc_has_attached_nodes_test.py @@ -0,0 +1,68 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.network.network_service import ( + Vpc, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.network.network_vpc_has_attached_nodes.network_vpc_has_attached_nodes.network_client" + + +class Test_network_vpc_has_attached_nodes: + def test_no_vpcs(self): + client = mock.MagicMock() + client.vpcs = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_vpc_has_attached_nodes.network_vpc_has_attached_nodes import ( + network_vpc_has_attached_nodes, + ) + + assert network_vpc_has_attached_nodes().execute() == [] + + def test_network_vpc_has_attached_nodes_compliant(self): + client = mock.MagicMock() + client.vpcs = [ + Vpc(network_id="1", name="ok", location="Delhi", vm_count=2), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_vpc_has_attached_nodes.network_vpc_has_attached_nodes import ( + network_vpc_has_attached_nodes, + ) + + findings = network_vpc_has_attached_nodes().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_network_vpc_has_attached_nodes_non_compliant(self): + client = mock.MagicMock() + client.vpcs = [ + Vpc(network_id="2", name="bad", location="Delhi", vm_count=0), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_vpc_has_attached_nodes.network_vpc_has_attached_nodes import ( + network_vpc_has_attached_nodes, + ) + + findings = network_vpc_has_attached_nodes().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active_test.py b/tests/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active_test.py new file mode 100644 index 0000000000..d38d4802ab --- /dev/null +++ b/tests/providers/e2enetworks/services/network/network_vpc_is_active/network_vpc_is_active_test.py @@ -0,0 +1,80 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.network.network_service import ( + Vpc, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.network.network_vpc_is_active.network_vpc_is_active.network_client" + + +class Test_network_vpc_is_active: + def test_no_vpcs(self): + client = mock.MagicMock() + client.vpcs = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_vpc_is_active.network_vpc_is_active import ( + network_vpc_is_active, + ) + + assert network_vpc_is_active().execute() == [] + + def test_network_vpc_is_active_compliant(self): + client = mock.MagicMock() + client.vpcs = [ + Vpc( + network_id="1", + name="ok", + location="Delhi", + is_active=True, + state="Active", + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_vpc_is_active.network_vpc_is_active import ( + network_vpc_is_active, + ) + + findings = network_vpc_is_active().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_network_vpc_is_active_non_compliant(self): + client = mock.MagicMock() + client.vpcs = [ + Vpc( + network_id="2", + name="bad", + location="Delhi", + is_active=False, + state="Inactive", + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_vpc_is_active.network_vpc_is_active import ( + network_vpc_is_active, + ) + + findings = network_vpc_is_active().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled_test.py b/tests/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled_test.py new file mode 100644 index 0000000000..eb7503c419 --- /dev/null +++ b/tests/providers/e2enetworks/services/network/network_vpc_peering_external_peer_disabled/network_vpc_peering_external_peer_disabled_test.py @@ -0,0 +1,82 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.network.network_service import ( + VpcTunnel, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.network.network_vpc_peering_external_peer_disabled.network_vpc_peering_external_peer_disabled.network_client" + + +class Test_network_vpc_peering_external_peer_disabled: + def test_no_vpc_tunnels(self): + client = mock.MagicMock() + client.vpc_tunnels = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_vpc_peering_external_peer_disabled.network_vpc_peering_external_peer_disabled import ( + network_vpc_peering_external_peer_disabled, + ) + + assert network_vpc_peering_external_peer_disabled().execute() == [] + + def test_network_vpc_peering_external_peer_disabled_compliant(self): + client = mock.MagicMock() + client.vpc_tunnels = [ + VpcTunnel( + id="1", + name="ok", + location="Delhi", + local_vpc_network_id="vpc-1", + local_vpc_name="vpc", + is_peer_vpc_external=False, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_vpc_peering_external_peer_disabled.network_vpc_peering_external_peer_disabled import ( + network_vpc_peering_external_peer_disabled, + ) + + findings = network_vpc_peering_external_peer_disabled().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_network_vpc_peering_external_peer_disabled_non_compliant(self): + client = mock.MagicMock() + client.vpc_tunnels = [ + VpcTunnel( + id="2", + name="bad", + location="Delhi", + local_vpc_network_id="vpc-2", + local_vpc_name="vpc", + is_peer_vpc_external=True, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.network.network_vpc_peering_external_peer_disabled.network_vpc_peering_external_peer_disabled import ( + network_vpc_peering_external_peer_disabled, + ) + + findings = network_vpc_peering_external_peer_disabled().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/node/e2enetworks_node_service_test.py b/tests/providers/e2enetworks/services/node/e2enetworks_node_service_test.py new file mode 100644 index 0000000000..84bbb576ef --- /dev/null +++ b/tests/providers/e2enetworks/services/node/e2enetworks_node_service_test.py @@ -0,0 +1,94 @@ +from unittest.mock import MagicMock, patch + +import pytest + +from prowler.providers.e2enetworks.services.node.node_service import Node, Nodes + + +class TestNodesService: + @patch( + "prowler.providers.e2enetworks.services.node.node_service.E2eNetworksService.__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 + + +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.e2enetworks.services.node.node_service import ( + _has_public_ip, + ) + + assert _has_public_ip(public_ip_address) is expected diff --git a/tests/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled_test.py b/tests/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled_test.py new file mode 100644 index 0000000000..4335c8fa6a --- /dev/null +++ b/tests/providers/e2enetworks/services/node/node_accidental_protection_enabled/node_accidental_protection_enabled_test.py @@ -0,0 +1,82 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.node.node_service import ( + Node, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.node.node_accidental_protection_enabled.node_accidental_protection_enabled.node_client" + + +class Test_node_accidental_protection_enabled: + def test_no_nodes(self): + client = mock.MagicMock() + client.nodes = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_accidental_protection_enabled.node_accidental_protection_enabled import ( + node_accidental_protection_enabled, + ) + + assert node_accidental_protection_enabled().execute() == [] + + def test_node_accidental_protection_enabled_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + is_accidental_protection=True, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_accidental_protection_enabled.node_accidental_protection_enabled import ( + node_accidental_protection_enabled, + ) + + findings = node_accidental_protection_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_node_accidental_protection_enabled_non_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="2", + name="bad", + status="Running", + location="Delhi", + vm_id="2", + is_accidental_protection=False, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_accidental_protection_enabled.node_accidental_protection_enabled import ( + node_accidental_protection_enabled, + ) + + findings = node_accidental_protection_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled_test.py b/tests/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled_test.py new file mode 100644 index 0000000000..b6f8a7a9ae --- /dev/null +++ b/tests/providers/e2enetworks/services/node/node_compliance_enabled/node_compliance_enabled_test.py @@ -0,0 +1,82 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.node.node_service import ( + Node, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.node.node_compliance_enabled.node_compliance_enabled.node_client" + + +class Test_node_compliance_enabled: + def test_no_nodes(self): + client = mock.MagicMock() + client.nodes = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_compliance_enabled.node_compliance_enabled import ( + node_compliance_enabled, + ) + + assert node_compliance_enabled().execute() == [] + + def test_node_compliance_enabled_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + is_node_compliance=True, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_compliance_enabled.node_compliance_enabled import ( + node_compliance_enabled, + ) + + findings = node_compliance_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_node_compliance_enabled_non_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="2", + name="bad", + status="Running", + location="Delhi", + vm_id="2", + is_node_compliance=False, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_compliance_enabled.node_compliance_enabled import ( + node_compliance_enabled, + ) + + findings = node_compliance_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled_test.py b/tests/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled_test.py new file mode 100644 index 0000000000..698cfe008f --- /dev/null +++ b/tests/providers/e2enetworks/services/node/node_encryption_enabled/node_encryption_enabled_test.py @@ -0,0 +1,82 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.node.node_service import ( + Node, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.node.node_encryption_enabled.node_encryption_enabled.node_client" + + +class Test_node_encryption_enabled: + def test_no_nodes(self): + client = mock.MagicMock() + client.nodes = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_encryption_enabled.node_encryption_enabled import ( + node_encryption_enabled, + ) + + assert node_encryption_enabled().execute() == [] + + def test_node_encryption_enabled_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + is_encryption_enabled=True, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_encryption_enabled.node_encryption_enabled import ( + node_encryption_enabled, + ) + + findings = node_encryption_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_node_encryption_enabled_non_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="2", + name="bad", + status="Running", + location="Delhi", + vm_id="2", + is_encryption_enabled=False, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_encryption_enabled.node_encryption_enabled import ( + node_encryption_enabled, + ) + + findings = node_encryption_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned_test.py b/tests/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned_test.py new file mode 100644 index 0000000000..ea7d8a3ccb --- /dev/null +++ b/tests/providers/e2enetworks/services/node/node_public_ip_not_assigned/node_public_ip_not_assigned_test.py @@ -0,0 +1,82 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.node.node_service import ( + Node, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.node.node_public_ip_not_assigned.node_public_ip_not_assigned.node_client" + + +class Test_node_public_ip_not_assigned: + def test_no_nodes(self): + client = mock.MagicMock() + client.nodes = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_public_ip_not_assigned.node_public_ip_not_assigned import ( + node_public_ip_not_assigned, + ) + + assert node_public_ip_not_assigned().execute() == [] + + def test_node_public_ip_not_assigned_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + has_public_ip=False, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_public_ip_not_assigned.node_public_ip_not_assigned import ( + node_public_ip_not_assigned, + ) + + findings = node_public_ip_not_assigned().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_node_public_ip_not_assigned_non_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="2", + name="bad", + 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_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_public_ip_not_assigned.node_public_ip_not_assigned import ( + node_public_ip_not_assigned, + ) + + findings = node_public_ip_not_assigned().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled_test.py b/tests/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled_test.py new file mode 100644 index 0000000000..8c4887a85d --- /dev/null +++ b/tests/providers/e2enetworks/services/node/node_rescue_mode_disabled/node_rescue_mode_disabled_test.py @@ -0,0 +1,82 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.node.node_service import ( + Node, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.node.node_rescue_mode_disabled.node_rescue_mode_disabled.node_client" + + +class Test_node_rescue_mode_disabled: + def test_no_nodes(self): + client = mock.MagicMock() + client.nodes = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_rescue_mode_disabled.node_rescue_mode_disabled import ( + node_rescue_mode_disabled, + ) + + assert node_rescue_mode_disabled().execute() == [] + + def test_node_rescue_mode_disabled_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + rescue_mode_status="Disabled", + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_rescue_mode_disabled.node_rescue_mode_disabled import ( + node_rescue_mode_disabled, + ) + + findings = node_rescue_mode_disabled().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_node_rescue_mode_disabled_non_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="2", + name="bad", + status="Running", + location="Delhi", + vm_id="2", + rescue_mode_status="Enabled", + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_rescue_mode_disabled.node_rescue_mode_disabled import ( + node_rescue_mode_disabled, + ) + + findings = node_rescue_mode_disabled().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached_test.py b/tests/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached_test.py new file mode 100644 index 0000000000..283fbddfc6 --- /dev/null +++ b/tests/providers/e2enetworks/services/node/node_vpc_attached/node_vpc_attached_test.py @@ -0,0 +1,82 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.node.node_service import ( + Node, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.node.node_vpc_attached.node_vpc_attached.node_client" + + +class Test_node_vpc_attached: + def test_no_nodes(self): + client = mock.MagicMock() + client.nodes = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_vpc_attached.node_vpc_attached import ( + node_vpc_attached, + ) + + assert node_vpc_attached().execute() == [] + + def test_node_vpc_attached_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="1", + name="ok", + status="Running", + location="Delhi", + vm_id="1", + is_vpc_attached=True, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_vpc_attached.node_vpc_attached import ( + node_vpc_attached, + ) + + findings = node_vpc_attached().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_node_vpc_attached_non_compliant(self): + client = mock.MagicMock() + client.nodes = [ + Node( + id="2", + name="bad", + status="Running", + location="Delhi", + vm_id="2", + is_vpc_attached=False, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.node.node_vpc_attached.node_vpc_attached import ( + node_vpc_attached, + ) + + findings = node_vpc_attached().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/securitygroup/e2enetworks_securitygroup_service_test.py b/tests/providers/e2enetworks/services/securitygroup/e2enetworks_securitygroup_service_test.py new file mode 100644 index 0000000000..f9ad45d732 --- /dev/null +++ b/tests/providers/e2enetworks/services/securitygroup/e2enetworks_securitygroup_service_test.py @@ -0,0 +1,71 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.e2enetworks.services.securitygroup.securitygroup_service import ( + SecurityGroups, +) + + +class TestSecurityGroupService: + @patch( + "prowler.providers.e2enetworks.services.securitygroup.securitygroup_service.E2eNetworksService.__init__" + ) + def test_fetch_security_groups_parses_rules(self, mock_super_init): + mock_super_init.return_value = None + + provider = MagicMock() + provider.session.locations = ["Delhi"] + service = SecurityGroups.__new__(SecurityGroups) + service.provider = provider + service.client = MagicMock() + service.security_groups = [] + + service.client.get_data.return_value = [ + { + "id": 10, + "name": "default", + "description": "Default security group", + "is_default": True, + "is_all_traffic_rule": True, + "rules": [ + { + "id": 1, + "rule_type": "Inbound", + "protocol_name": "All", + "port_range": "1-65535", + "network": "0.0.0.0", + "network_cidr": "0", + } + ], + } + ] + + service._fetch_security_groups() + + assert len(service.security_groups) == 1 + group = service.security_groups[0] + assert group.id == "10" + assert group.name == "default" + assert group.location == "Delhi" + assert group.is_default is True + assert group.is_all_traffic_rule is True + assert len(group.rules) == 1 + assert group.rules[0].rule_type == "Inbound" + + @patch( + "prowler.providers.e2enetworks.services.securitygroup.securitygroup_service.E2eNetworksService.__init__" + ) + def test_fetch_security_groups_ignores_non_list_response(self, mock_super_init): + mock_super_init.return_value = None + + provider = MagicMock() + provider.session.locations = ["Delhi"] + service = SecurityGroups.__new__(SecurityGroups) + service.provider = provider + service.client = MagicMock() + service.security_groups = [] + + service.client.get_data.return_value = {"error": "unexpected"} + + service._fetch_security_groups() + + assert service.security_groups == [] diff --git a/tests/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule_test.py b/tests/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule_test.py new file mode 100644 index 0000000000..e992bc1a3c --- /dev/null +++ b/tests/providers/e2enetworks/services/securitygroup/securitygroup_no_all_traffic_rule/securitygroup_no_all_traffic_rule_test.py @@ -0,0 +1,72 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.securitygroup.securitygroup_service import ( + SecurityGroupResource, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.securitygroup.securitygroup_no_all_traffic_rule.securitygroup_no_all_traffic_rule.securitygroup_client" + + +class Test_securitygroup_no_all_traffic_rule: + def test_no_security_groups(self): + client = mock.MagicMock() + client.security_groups = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.securitygroup.securitygroup_no_all_traffic_rule.securitygroup_no_all_traffic_rule import ( + securitygroup_no_all_traffic_rule, + ) + + assert securitygroup_no_all_traffic_rule().execute() == [] + + def test_securitygroup_no_all_traffic_rule_compliant(self): + client = mock.MagicMock() + client.security_groups = [ + SecurityGroupResource( + id="1", name="ok", location="Delhi", is_all_traffic_rule=False + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.securitygroup.securitygroup_no_all_traffic_rule.securitygroup_no_all_traffic_rule import ( + securitygroup_no_all_traffic_rule, + ) + + findings = securitygroup_no_all_traffic_rule().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_securitygroup_no_all_traffic_rule_non_compliant(self): + client = mock.MagicMock() + client.security_groups = [ + SecurityGroupResource( + id="2", name="bad", location="Delhi", is_all_traffic_rule=True + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.securitygroup.securitygroup_no_all_traffic_rule.securitygroup_no_all_traffic_rule import ( + securitygroup_no_all_traffic_rule, + ) + + findings = securitygroup_no_all_traffic_rule().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports_test.py b/tests/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports_test.py new file mode 100644 index 0000000000..3634aa8aec --- /dev/null +++ b/tests/providers/e2enetworks/services/securitygroup/securitygroup_no_inbound_any_all_ports/securitygroup_no_inbound_any_all_ports_test.py @@ -0,0 +1,97 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.securitygroup.securitygroup_service import ( + SecurityGroupResource, + SecurityGroupRule, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.securitygroup.securitygroup_no_inbound_any_all_ports.securitygroup_no_inbound_any_all_ports.securitygroup_client" + + +class Test_securitygroup_no_inbound_any_all_ports: + def test_no_security_groups(self): + client = mock.MagicMock() + client.security_groups = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.securitygroup.securitygroup_no_inbound_any_all_ports.securitygroup_no_inbound_any_all_ports import ( + securitygroup_no_inbound_any_all_ports, + ) + + assert securitygroup_no_inbound_any_all_ports().execute() == [] + + def test_securitygroup_no_inbound_any_all_ports_compliant(self): + client = mock.MagicMock() + client.security_groups = [ + SecurityGroupResource( + id="1", + name="ok", + location="Delhi", + rules=[ + 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", + ) + ], + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.securitygroup.securitygroup_no_inbound_any_all_ports.securitygroup_no_inbound_any_all_ports import ( + securitygroup_no_inbound_any_all_ports, + ) + + findings = securitygroup_no_inbound_any_all_ports().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_securitygroup_no_inbound_any_all_ports_non_compliant(self): + client = mock.MagicMock() + client.security_groups = [ + SecurityGroupResource( + id="2", + name="bad", + location="Delhi", + rules=[ + SecurityGroupRule( + id="1", + rule_type="inbound", + protocol_name="all", + port_range="", + network="any", + network_cidr="", + ) + ], + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.securitygroup.securitygroup_no_inbound_any_all_ports.securitygroup_no_inbound_any_all_ports import ( + securitygroup_no_inbound_any_all_ports, + ) + + findings = securitygroup_no_inbound_any_all_ports().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default_test.py b/tests/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default_test.py new file mode 100644 index 0000000000..fdb35608df --- /dev/null +++ b/tests/providers/e2enetworks/services/securitygroup/securitygroup_restrictive_default/securitygroup_restrictive_default_test.py @@ -0,0 +1,96 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.securitygroup.securitygroup_service import ( + NodeSecurityGroup, + SecurityGroupRule, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.securitygroup.securitygroup_restrictive_default.securitygroup_restrictive_default.securitygroup_client" + + +class Test_securitygroup_restrictive_default: + def test_no_node_security_groups(self): + client = mock.MagicMock() + client.node_security_groups = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.securitygroup.securitygroup_restrictive_default.securitygroup_restrictive_default import ( + securitygroup_restrictive_default, + ) + + assert securitygroup_restrictive_default().execute() == [] + + def test_securitygroup_restrictive_default_compliant(self): + client = mock.MagicMock() + client.node_security_groups = [ + 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=[], + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.securitygroup.securitygroup_restrictive_default.securitygroup_restrictive_default import ( + securitygroup_restrictive_default, + ) + + findings = securitygroup_restrictive_default().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_securitygroup_restrictive_default_non_compliant(self): + client = mock.MagicMock() + client.node_security_groups = [ + 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=[ + SecurityGroupRule( + id="1", + rule_type="inbound", + protocol_name="all", + port_range="", + network="any", + network_cidr="", + ) + ], + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.securitygroup.securitygroup_restrictive_default.securitygroup_restrictive_default import ( + securitygroup_restrictive_default, + ) + + findings = securitygroup_restrictive_default().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/storage/e2enetworks_storage_service_test.py b/tests/providers/e2enetworks/services/storage/e2enetworks_storage_service_test.py new file mode 100644 index 0000000000..9c30d2e2c7 --- /dev/null +++ b/tests/providers/e2enetworks/services/storage/e2enetworks_storage_service_test.py @@ -0,0 +1,50 @@ +from unittest.mock import MagicMock, patch + +from prowler.providers.e2enetworks.services.storage.storage_service import Storage + + +class TestStorageService: + @patch( + "prowler.providers.e2enetworks.services.storage.storage_service.E2eNetworksService.__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.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" diff --git a/tests/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned_test.py b/tests/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned_test.py new file mode 100644 index 0000000000..85c51fcd73 --- /dev/null +++ b/tests/providers/e2enetworks/services/storage/storage_block_volume_not_orphaned/storage_block_volume_not_orphaned_test.py @@ -0,0 +1,74 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.storage.storage_service import ( + BlockVolume, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.storage.storage_block_volume_not_orphaned.storage_block_volume_not_orphaned.storage_client" + + +class Test_storage_block_volume_not_orphaned: + def test_no_block_volumes(self): + client = mock.MagicMock() + client.block_volumes = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.storage.storage_block_volume_not_orphaned.storage_block_volume_not_orphaned import ( + storage_block_volume_not_orphaned, + ) + + assert storage_block_volume_not_orphaned().execute() == [] + + def test_storage_block_volume_not_orphaned_compliant(self): + client = mock.MagicMock() + client.block_volumes = [ + BlockVolume(id="1", name="ok", location="Delhi", is_attached=True), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.storage.storage_block_volume_not_orphaned.storage_block_volume_not_orphaned import ( + storage_block_volume_not_orphaned, + ) + + findings = storage_block_volume_not_orphaned().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_storage_block_volume_not_orphaned_non_compliant(self): + client = mock.MagicMock() + client.block_volumes = [ + BlockVolume( + id="2", + name="bad", + location="Delhi", + status="Available", + is_attached=False, + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.storage.storage_block_volume_not_orphaned.storage_block_volume_not_orphaned import ( + storage_block_volume_not_orphaned, + ) + + findings = storage_block_volume_not_orphaned().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled_test.py b/tests/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled_test.py new file mode 100644 index 0000000000..7606097ca7 --- /dev/null +++ b/tests/providers/e2enetworks/services/storage/storage_efs_backup_enabled/storage_efs_backup_enabled_test.py @@ -0,0 +1,68 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.storage.storage_service import ( + EfsVolume, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.storage.storage_efs_backup_enabled.storage_efs_backup_enabled.storage_client" + + +class Test_storage_efs_backup_enabled: + def test_no_efs_volumes(self): + client = mock.MagicMock() + client.efs_volumes = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.storage.storage_efs_backup_enabled.storage_efs_backup_enabled import ( + storage_efs_backup_enabled, + ) + + assert storage_efs_backup_enabled().execute() == [] + + def test_storage_efs_backup_enabled_compliant(self): + client = mock.MagicMock() + client.efs_volumes = [ + EfsVolume(id="1", name="ok", location="Delhi", is_backup_enabled=True), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.storage.storage_efs_backup_enabled.storage_efs_backup_enabled import ( + storage_efs_backup_enabled, + ) + + findings = storage_efs_backup_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_storage_efs_backup_enabled_non_compliant(self): + client = mock.MagicMock() + client.efs_volumes = [ + EfsVolume(id="2", name="bad", location="Delhi", is_backup_enabled=False), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.storage.storage_efs_backup_enabled.storage_efs_backup_enabled import ( + storage_efs_backup_enabled, + ) + + findings = storage_efs_backup_enabled().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL" diff --git a/tests/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted_test.py b/tests/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted_test.py new file mode 100644 index 0000000000..b0c06bcf65 --- /dev/null +++ b/tests/providers/e2enetworks/services/storage/storage_efs_vpc_access_restricted/storage_efs_vpc_access_restricted_test.py @@ -0,0 +1,72 @@ +from unittest import mock + +from prowler.providers.e2enetworks.services.storage.storage_service import ( + EfsVolume, +) +from tests.providers.e2enetworks.e2enetworks_fixtures import ( + set_mocked_e2enetworks_provider, +) + +CLIENT_PATH = "prowler.providers.e2enetworks.services.storage.storage_efs_vpc_access_restricted.storage_efs_vpc_access_restricted.storage_client" + + +class Test_storage_efs_vpc_access_restricted: + def test_no_efs_volumes(self): + client = mock.MagicMock() + client.efs_volumes = [] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.storage.storage_efs_vpc_access_restricted.storage_efs_vpc_access_restricted import ( + storage_efs_vpc_access_restricted, + ) + + assert storage_efs_vpc_access_restricted().execute() == [] + + def test_storage_efs_vpc_access_restricted_compliant(self): + client = mock.MagicMock() + client.efs_volumes = [ + EfsVolume( + id="1", name="ok", location="Delhi", is_all_vpc_resources_allowed=False + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.storage.storage_efs_vpc_access_restricted.storage_efs_vpc_access_restricted import ( + storage_efs_vpc_access_restricted, + ) + + findings = storage_efs_vpc_access_restricted().execute() + assert len(findings) == 1 + assert findings[0].status == "PASS" + + def test_storage_efs_vpc_access_restricted_non_compliant(self): + client = mock.MagicMock() + client.efs_volumes = [ + EfsVolume( + id="2", name="bad", location="Delhi", is_all_vpc_resources_allowed=True + ), + ] + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_e2enetworks_provider(), + ), + mock.patch(CLIENT_PATH, new=client), + ): + from prowler.providers.e2enetworks.services.storage.storage_efs_vpc_access_restricted.storage_efs_vpc_access_restricted import ( + storage_efs_vpc_access_restricted, + ) + + findings = storage_efs_vpc_access_restricted().execute() + assert len(findings) == 1 + assert findings[0].status == "FAIL"