From 6ff0f1c176a9762992a629088bafc25132e25383 Mon Sep 17 00:00:00 2001 From: codydouglasBC <157752937+codydouglasBC@users.noreply.github.com> Date: Wed, 26 Jun 2024 08:28:48 -0700 Subject: [PATCH] Initial commit of config-modules code. (#4) Include desired state schema and 77 controllers across 5 products. Also include configuration support for 2 VC Profile components. Add unit-tests covering 89% of total code. Co-authored-by: Russell Jew <171984014+rjew-bc@users.noreply.github.com> Co-authored-by: Balavignesh Ravichandran <147883382+balavigneshVMware@users.noreply.github.com> Co-authored-by: Ravi Pratap Singh <172073177+ravi-pratap-s@users.noreply.github.com> Co-authored-by: Sam Huang <170547249+sam-huang-bc@users.noreply.github.com> Co-authored-by: Guangsong Huang <172428411+huanggs001@users.noreply.github.com> Co-authored-by: Sudipto Mukhopadhyay <167139009+sudipto674@users.noreply.github.com> --- .dockerignore | 1 + .pre-commit-config.yaml | 31 + .pylintrc | 391 ++ CHANGELOG.md | 10 + CONTRIBUTING.md | 117 + Dockerfile | 15 + MANIFEST.in | 4 + README.md | 48 + config_modules_vmware/__init__.py | 6 + config_modules_vmware/app.py | 56 + config_modules_vmware/controllers/__init__.py | 0 .../controllers/base_controller.py | 136 + .../controllers/esxi/__init__.py | 0 .../esxi/account_unlock_time_interval.py | 85 + .../esxi/bridge_protocol_data_unit_filter.py | 85 + .../controllers/esxi/cim_service_policy.py | 88 + .../controllers/esxi/dcui_login_banner.py | 85 + .../esxi/firewall_rulesets_config.py | 456 ++ .../esxi/hyperthread_warning_policy.py | 85 + .../esxi/managed_object_browser.py | 85 + .../esxi/max_failed_login_attempts.py | 85 + .../controllers/esxi/ntp_service_config.py | 88 + .../esxi/ntp_service_startup_policy.py | 88 + .../esxi/password_max_lifetime_policy.py | 85 + .../esxi/password_reuse_restriction_policy.py | 85 + .../controllers/esxi/shell_service_policy.py | 88 + .../controllers/esxi/slp_service_policy.py | 87 + .../controllers/esxi/snmp_service_policy.py | 88 + .../esxi/ssh_ignore_rhosts_policy.py | 102 + .../controllers/esxi/ssh_login_banner.py | 85 + .../esxi/ssh_port_forwarding_policy.py | 102 + .../controllers/esxi/tls_version.py | 98 + .../controllers/esxi/utils/__init__.py | 0 .../utils/esxi_advanced_settings_utils.py | 47 + .../esxi/utils/esxi_ssh_config_utils.py | 49 + .../controllers/esxi/utils/service_utils.py | 66 + .../controllers/nsxt_edge/__init__.py | 0 .../controllers/nsxt_edge/ntp_config.py | 96 + .../controllers/nsxt_manager/__init__.py | 0 .../controllers/nsxt_manager/ntp_config.py | 176 + .../controllers/sample/__init__.py | 0 .../controllers/sample/sample_controller.py | 211 + .../controllers/sddc_manager/__init__.py | 0 .../sddc_manager/auto_rotate_schedule.py | 210 + .../controllers/sddc_manager/backup.py | 269 ++ .../controllers/sddc_manager/cert_config.py | 165 + .../controllers/sddc_manager/depot_config.py | 108 + .../controllers/sddc_manager/dns_config.py | 151 + .../controllers/sddc_manager/fips_config.py | 89 + .../controllers/sddc_manager/ntp_config.py | 135 + .../controllers/sddc_manager/proxy_config.py | 270 ++ .../sddc_manager/users_groups_roles_config.py | 94 + .../controllers/vcenter/__init__.py | 0 .../alarm_remote_syslog_failure_config.py | 166 + .../controllers/vcenter/alarm_sso_config.py | 165 + .../vcenter/backup_schedule_config.py | 295 ++ .../controllers/vcenter/cert_config.py | 136 + .../datastore_transit_encryption_config.py | 297 ++ .../vcenter/datastore_unique_name_policy.py | 150 + .../controllers/vcenter/dns_config.py | 131 + .../vcenter/dv_pg_forged_transmits_policy.py | 294 ++ .../dv_pg_mac_address_change_policy.py | 301 ++ .../dv_pg_native_vlan_exclusion_policy.py | 271 ++ .../vcenter/dv_pg_promiscuous_mode_policy.py | 300 ++ .../dv_pg_reserved_vlan_exclusion_policy.py | 270 ++ .../vcenter/dv_pg_vlan_trunking_authorized.py | 281 ++ .../vcenter/dvs_health_check_config.py | 304 ++ .../vcenter/dvs_network_io_control_policy.py | 305 ++ .../h5_client_session_timeout_config.py | 180 + .../vcenter/ldap_identity_source_config.py | 111 + .../vcenter/logon_banner_config.py | 185 + .../controllers/vcenter/ntp_config.py | 225 + .../controllers/vcenter/snmp_v3_config.py | 186 + ..._active_directory_authentication_policy.py | 86 + ...o_active_directory_ldaps_enabled_config.py | 162 + .../vcenter/sso_auto_unlock_interval.py | 82 + ...so_bash_shell_authorized_members_config.py | 165 + .../sso_failed_login_attempt_interval.py | 82 + .../vcenter/sso_max_failed_login_attempts.py | 84 + .../sso_password_max_lifetime_policy.py | 83 + ...password_min_lowercase_character_policy.py | 83 + ...o_password_min_numeric_character_policy.py | 86 + ...o_password_min_special_character_policy.py | 83 + ...password_min_uppercase_character_policy.py | 83 + .../sso_password_minimum_length_policy.py | 83 + .../sso_password_reuse_restriction_policy.py | 82 + ...rusted_admins_authorized_members_config.py | 162 + .../controllers/vcenter/syslog_config.py | 152 + .../task_and_event_retention_policy.py | 191 + .../controllers/vcenter/tls_version_config.py | 262 + .../vcenter/users_groups_roles_config.py | 82 + .../controllers/vcenter/utils/__init__.py | 0 .../vcenter/utils/vc_alarms_utils.py | 298 ++ .../vcenter/utils/vc_port_group_utils.py | 101 + .../vcenter/utils/vc_profile_utils.py | 242 + .../controllers/vcenter/vc_profile.py | 361 ++ .../vcenter/vm_migrate_encryption_policy.py | 352 ++ .../vcenter/vmotion_port_group_config.py | 469 ++ .../vcenter/vpx_log_level_config.py | 87 + ...vpx_sddc_deployed_compliance_kit_config.py | 86 + .../vcenter/vpx_syslog_enablement_config.py | 85 + .../vpx_user_host_password_length_policy.py | 100 + .../vpx_user_password_expiration_policy.py | 93 + .../vcenter/vsan_hcl_proxy_config.py | 284 ++ .../vsan_iscsi_targets_mchap_config.py | 174 + .../controllers/vrslcm/__init__.py | 0 .../controllers/vrslcm/dns_config.py | 187 + config_modules_vmware/framework/__init__.py | 0 .../framework/auth/__init__.py | 0 .../framework/auth/contexts/__init__.py | 0 .../framework/auth/contexts/base_context.py | 82 + .../framework/auth/contexts/esxi_context.py | 143 + .../auth/contexts/sddc_manager_context.py | 96 + .../framework/auth/contexts/vc_context.py | 143 + .../framework/auth/contexts/vrslcm_context.py | 16 + .../framework/clients/__init__.py | 0 .../framework/clients/aria_suite/__init__.py | 0 .../framework/clients/aria_suite/aria_auth.py | 39 + .../clients/aria_suite/aria_consts.py | 3 + .../framework/clients/common/__init__.py | 0 .../framework/clients/common/consts.py | 54 + .../framework/clients/common/rest_client.py | 398 ++ .../framework/clients/common/vmomi_client.py | 232 + .../framework/clients/esxi/__init__.py | 0 .../framework/clients/esxi/esx_cli_client.py | 67 + .../clients/sddc_manager/__init__.py | 0 .../sddc_manager/sddc_manager_consts.py | 27 + .../sddc_manager/sddc_manager_rest_client.py | 256 + .../framework/clients/vcenter/__init__.py | 0 .../clients/vcenter/dependencies/__init__.py | 0 .../vcenter/dependencies/pyVim/__init__.py | 10 + .../clients/vcenter/dependencies/pyVim/sso.py | 1274 +++++ .../vcenter/dependencies/pyVmomi/CoreTypes.py | 196 + .../vcenter/dependencies/pyVmomi/Iso8601.py | 332 ++ .../dependencies/pyVmomi/QueryTypes.py | 540 +++ .../dependencies/pyVmomi/SoapAdapter.py | 1860 ++++++++ .../vcenter/dependencies/pyVmomi/SsoTypes.py | 4243 +++++++++++++++++ .../pyVmomi/StubAdapterAccessorImpl.py | 50 + .../vcenter/dependencies/pyVmomi/Version.py | 77 + .../dependencies/pyVmomi/VmomiSupport.py | 1984 ++++++++ .../dependencies/pyVmomi/__bindings.py | 211 + .../vcenter/dependencies/pyVmomi/__init__.py | 56 + ...ware Development Kit License Agreement.pdf | Bin 0 -> 63687 bytes .../dependencies/vsan_management/__init__.py | 0 .../vsan_management/vsanmgmtObjects.py | 1336 ++++++ .../framework/clients/vcenter/vc_consts.py | 55 + .../clients/vcenter/vc_rest_client.py | 408 ++ .../clients/vcenter/vc_vmomi_client.py | 573 +++ .../clients/vcenter/vc_vmomi_sso_client.py | 410 ++ .../clients/vcenter/vc_vsan_vmomi_client.py | 368 ++ .../framework/logging/__init__.py | 0 .../framework/logging/logger_adapter.py | 53 + .../framework/logging/logging_context.py | 108 + .../framework/models/__init__.py | 0 .../models/controller_models/__init__.py | 0 .../models/controller_models/metadata.py | 263 + .../models/output_models/__init__.py | 0 .../output_models/compliance_response.py | 88 + .../configuration_drift_response.py | 499 ++ .../output_models/get_current_response.py | 88 + .../models/output_models/output_response.py | 51 + .../output_models/remediate_response.py | 88 + .../framework/utils/__init__.py | 0 .../framework/utils/comparator.py | 315 ++ .../framework/utils/target_enum.py | 12 + config_modules_vmware/framework/utils/task.py | 169 + .../framework/utils/utils.py | 156 + config_modules_vmware/interfaces/__init__.py | 0 .../interfaces/controller_interface.py | 561 +++ .../interfaces/metadata_interface.py | 55 + config_modules_vmware/log_config.yml | 37 + config_modules_vmware/schemas/__init__.py | 0 .../schemas/compliance_reference_schema.json | 3055 ++++++++++++ .../schemas/schema_utility.py | 43 + config_modules_vmware/services/__init__.py | 0 .../services/apis/__init__.py | 0 .../services/apis/controllers/__init__.py | 0 .../services/apis/controllers/consts.py | 14 + .../services/apis/controllers/misc.py | 27 + .../services/apis/controllers/vcenter.py | 265 + .../services/apis/models/__init__.py | 0 .../services/apis/models/about.py | 13 + .../services/apis/models/drift_payload.py | 66 + .../services/apis/models/error_model.py | 32 + .../apis/models/get_config_payload.py | 34 + .../services/apis/models/healthcheck.py | 8 + .../services/apis/models/openapi_examples.py | 158 + .../services/apis/models/request_payload.py | 19 + .../services/apis/models/target_model.py | 40 + config_modules_vmware/services/config.ini | 99 + config_modules_vmware/services/config.py | 63 + .../services/mapper/__init__.py | 0 .../mapper/configuration_mapping.json | 5 + .../mapper/control_config_mapping.json | 95 + .../services/mapper/mapper_utils.py | 35 + .../services/workflows/__init__.py | 0 .../workflows/compliance_operations.py | 504 ++ .../workflows/configuration_operations.py | 132 + .../workflows/operations_interface.py | 69 + config_modules_vmware/tests/README.md | 22 + config_modules_vmware/tests/__init__.py | 0 config_modules_vmware/tests/config.ini | 9 + .../tests/controllers/__init__.py | 0 .../tests/controllers/esxi/__init__.py | 0 .../esxi/test_account_unlock_time_interval.py | 58 + .../test_bridge_protocol_data_unit_filter.py | 58 + .../esxi/test_cim_service_policy.py | 99 + .../esxi/test_dcui_login_banner.py | 58 + .../esxi/test_firewall_rulesets_config.py | 233 + .../esxi/test_managed_object_browser.py | 58 + .../esxi/test_max_failed_login_attempts.py | 58 + .../esxi/test_ntp_service_config.py | 100 + .../esxi/test_ntp_service_startup_policy.py | 99 + .../esxi/test_password_max_lifetime_policy.py | 171 + .../test_password_reuse_restriction_policy.py | 58 + .../esxi/test_shell_service_policy.py | 99 + .../esxi/test_slp_service_policy.py | 99 + .../esxi/test_snmp_service_policy.py | 99 + .../esxi/test_ssh_ignore_rhosts_policy.py | 78 + .../controllers/esxi/test_ssh_login_banner.py | 58 + .../esxi/test_ssh_port_forwarding_policy.py | 78 + .../esxi/test_suppress_hyperthread_warning.py | 58 + .../controllers/esxi/test_tls_version.py | 111 + .../tests/controllers/esxi/utils/__init__.py | 0 .../esxi/utils/test_esxi_ssh_config_utils.py | 41 + .../tests/controllers/nsxt/__init__.py | 0 .../tests/controllers/nsxt/test_ntp_config.py | 107 + .../tests/controllers/sample/__init__.py | 0 .../sample/test_sample_controller.py | 204 + .../controllers/sddc_manager/__init__.py | 0 .../sddc_manager/test_auto_rotate_config.py | 473 ++ .../sddc_manager/test_backup_config.py | 409 ++ .../sddc_manager/test_cert_config.py | 231 + .../sddc_manager/test_depot_config.py | 191 + .../sddc_manager/test_dns_config.py | 313 ++ .../sddc_manager/test_fips_config.py | 79 + .../sddc_manager/test_ntp_config.py | 274 ++ .../sddc_manager/test_proxy_config.py | 296 ++ .../test_users_groups_roles_config.py | 228 + .../tests/controllers/vcenter/__init__.py | 0 ...test_alarm_remote_syslog_failure_config.py | 177 + .../vcenter/test_alarm_sso_config.py | 191 + .../vcenter/test_backup_schedule_config.py | 307 ++ .../controllers/vcenter/test_cert_config.py | 148 + ...est_datastore_transit_encryption_config.py | 337 ++ .../test_datastore_unique_name_policy.py | 176 + .../controllers/vcenter/test_dns_config.py | 220 + .../test_dv_pg_forged_transmits_policy.py | 213 + .../test_dv_pg_mac_address_change_policy.py | 258 + ...test_dv_pg_native_vlan_exclusion_policy.py | 315 ++ .../test_dv_pg_promiscuous_mode_policy.py | 207 + ...st_dv_pg_reserved_vlan_exclusion_policy.py | 315 ++ .../test_dv_pg_vlan_trunking_authorized.py | 270 ++ .../vcenter/test_dvs_health_check_config.py | 257 + .../test_dvs_network_io_control_policy.py | 243 + .../test_h5_client_session_timeout_config.py | 245 + .../test_ldap_identity_source_config.py | 197 + .../vcenter/test_login_banner_config.py | 139 + .../controllers/vcenter/test_ntp_config.py | 233 + .../vcenter/test_snmp_v3_config.py | 257 + ..._active_directory_authentication_policy.py | 144 + ...o_active_directory_ldaps_enabled_config.py | 194 + .../vcenter/test_sso_auto_unlock_interval.py | 156 + ...so_bash_shell_authorized_members_config.py | 194 + .../test_sso_failed_login_attempt_interval.py | 157 + .../test_sso_max_failed_login_attempts.py | 157 + .../test_sso_password_max_lifetime_policy.py | 157 + ...password_min_lowercase_character_policy.py | 157 + ...o_password_min_numeric_character_policy.py | 158 + ...o_password_min_special_character_policy.py | 157 + ...password_min_uppercase_character_policy.py | 157 + ...test_sso_password_minimum_length_policy.py | 157 + ...t_sso_password_reuse_restriction_policy.py | 157 + ...rusted_admins_authorized_members_config.py | 195 + .../controllers/vcenter/test_syslog_config.py | 254 + .../test_task_and_event_retention_policy.py | 198 + .../vcenter/test_tls_version_config.py | 308 ++ .../vcenter/test_users_groups_roles_config.py | 86 + .../controllers/vcenter/test_vc_profile.py | 547 +++ .../test_vm_migrate_encryption_policy.py | 307 ++ .../vcenter/test_vmotion_port_group_config.py | 298 ++ .../vcenter/test_vpx_log_level_config.py | 156 + ...vpx_sddc_deployed_compliance_kit_config.py | 156 + .../test_vpx_syslog_enablement_config.py | 156 + ...st_vpx_user_host_password_length_policy.py | 158 + ...est_vpx_user_password_expiration_policy.py | 160 + .../vcenter/test_vsan_hcl_proxy_config.py | 283 ++ .../test_vsan_iscsi_targets_mchap_config.py | 224 + .../controllers/vcenter/utils/__init__.py | 0 .../vcenter/utils/test_vc_profile_utils.py | 261 + .../tests/controllers/vrslcm/__init__.py | 0 .../controllers/vrslcm/test_dns_config.py | 107 + .../tests/framework/__init__.py | 0 .../tests/framework/auth/__init__.py | 0 .../tests/framework/auth/contexts/__init__.py | 0 .../auth/contexts/test_base_context.py | 22 + .../auth/contexts/test_esxi_context.py | 93 + .../contexts/test_sddc_manager_context.py | 49 + .../auth/contexts/test_vc_context.py | 91 + .../tests/framework/clients/__init__.py | 0 .../framework/clients/aria_suite/__init__.py | 0 .../clients/aria_suite/test_aria_auth.py | 19 + .../tests/framework/clients/esxi/__init__.py | 0 .../clients/esxi/test_esx_cli_client.py | 41 + .../framework/clients/vcenter/__init__.py | 0 .../clients/vcenter/test_vc_vmomi_client.py | 46 + .../tests/framework/logging/__init__.py | 0 .../framework/logging/test_logger_adapter.py | 115 + .../framework/logging/test_logging_context.py | 100 + .../tests/framework/models/__init__.py | 0 .../models/controller_models/__init__.py | 0 .../models/controller_models/test_metadata.py | 229 + .../models/output_models/__init__.py | 0 .../output_models/test_compliance_response.py | 37 + .../test_get_current_response.py | 37 + .../output_models/test_output_response.py | 29 + .../output_models/test_remediate_response.py | 37 + .../tests/framework/utils/__init__.py | 0 .../tests/framework/utils/test_comparator.py | 282 ++ .../tests/framework/utils/test_task.py | 105 + .../tests/framework/utils/test_utils.py | 152 + .../tests/interfaces/__init__.py | 0 .../interfaces/test_controller_interface.py | 287 ++ .../interfaces/test_metadata_interface.py | 203 + .../tests/schemas/__init__.py | 0 .../tests/schemas/test_schema_utility.py | 55 + .../tests/services/__init__.py | 0 .../tests/services/apis/__init__.py | 0 .../services/apis/controllers/__init__.py | 0 .../services/apis/controllers/test_misc.py | 31 + .../services/apis/controllers/test_vcenter.py | 648 +++ .../tests/services/mapper/__init__.py | 0 .../services/mapper/test_mapper_utils.py | 52 + .../tests/services/test_config.py | 49 + .../tests/services/workflows/__init__.py | 0 .../workflows/test_compliance_operations.py | 1212 +++++ .../test_configuration_operations.py | 195 + .../workflows/test_operations_interface.py | 40 + devops/release/library/version.yml | 4 + devops/scripts/build.sh | 17 + devops/scripts/generate_markdown_docs.sh | 19 + devops/scripts/generate_openapi_spec.py | 15 + devops/scripts/run_formatting.sh | 12 + devops/scripts/run_functional_tests.sh | 18 + devops/scripts/run_reorder_imports.sh | 25 + devops/scripts/run_security_analysis.sh | 8 + devops/scripts/run_static_code_analysis.sh | 10 + devops/scripts/start_api_server.sh | 13 + docs/api-service.md | 26 + docs/controllers/conf.py | 60 + .../esxi.account_unlock_time_interval.rst | 4 + .../esxi.bridge_protocol_data_unit_filter.rst | 4 + .../esxi/esxi.cim_service_policy.rst | 4 + .../esxi/esxi.dcui_login_banner.rst | 4 + .../esxi/esxi.firewall_rulesets_config.rst | 4 + .../esxi/esxi.hyperthread_warning_policy.rst | 4 + .../esxi/esxi.managed_object_browser.rst | 4 + .../esxi/esxi.max_failed_login_attempts.rst | 4 + .../esxi/esxi.ntp_service_config.rst | 4 + .../esxi/esxi.ntp_service_startup_policy.rst | 4 + .../esxi.password_max_lifetime_policy.rst | 4 + ...esxi.password_reuse_restriction_policy.rst | 4 + docs/controllers/esxi/esxi.rst | 33 + .../esxi/esxi.shell_service_policy.rst | 4 + .../esxi/esxi.slp_service_policy.rst | 4 + .../esxi/esxi.snmp_service_policy.rst | 4 + .../esxi/esxi.ssh_ignore_rhosts_policy.rst | 4 + .../esxi/esxi.ssh_login_banner.rst | 4 + .../esxi/esxi.ssh_port_forwarding_policy.rst | 4 + docs/controllers/esxi/esxi.tls_version.rst | 4 + docs/controllers/index.rst | 15 + .../esxi/esxi.account_unlock_time_interval.md | 55 + .../esxi.bridge_protocol_data_unit_filter.md | 55 + .../markdown/esxi/esxi.cim_service_policy.md | 55 + .../markdown/esxi/esxi.dcui_login_banner.md | 55 + .../esxi/esxi.firewall_rulesets_config.md | 146 + .../esxi/esxi.hyperthread_warning_policy.md | 55 + .../esxi/esxi.managed_object_browser.md | 55 + .../esxi/esxi.max_failed_login_attempts.md | 55 + docs/controllers/markdown/esxi/esxi.md | 63 + .../markdown/esxi/esxi.ntp_service_config.md | 55 + .../esxi/esxi.ntp_service_startup_policy.md | 55 + .../esxi/esxi.password_max_lifetime_policy.md | 55 + .../esxi.password_reuse_restriction_policy.md | 55 + .../esxi/esxi.shell_service_policy.md | 55 + .../markdown/esxi/esxi.slp_service_policy.md | 55 + .../markdown/esxi/esxi.snmp_service_policy.md | 55 + .../esxi/esxi.ssh_ignore_rhosts_policy.md | 55 + .../markdown/esxi/esxi.ssh_login_banner.md | 55 + .../esxi/esxi.ssh_port_forwarding_policy.md | 55 + .../markdown/esxi/esxi.tls_version.md | 55 + docs/controllers/markdown/index.md | 99 + .../markdown/nsxt_edge/nsxt_edge.md | 7 + .../nsxt_edge/nsxt_edge.ntp_config.md | 74 + .../markdown/nsxt_manager/nsxt_manager.md | 12 + .../nsxt_manager/nsxt_manager.ntp_config.md | 129 + docs/controllers/markdown/sample/sample.md | 10 + .../sample/sample.sample_controller.md | 129 + .../sddc_manager.auto_rotate_schedule.md | 76 + .../sddc_manager/sddc_manager.backup.md | 64 + .../sddc_manager/sddc_manager.cert_config.md | 80 + .../sddc_manager/sddc_manager.depot_config.md | 53 + .../sddc_manager/sddc_manager.dns_config.md | 62 + .../sddc_manager/sddc_manager.fips_config.md | 50 + .../markdown/sddc_manager/sddc_manager.md | 38 + .../sddc_manager/sddc_manager.ntp_config.md | 62 + .../sddc_manager/sddc_manager.proxy_config.md | 64 + .../sddc_manager.users_groups_roles_config.md | 50 + ...nter.alarm_remote_syslog_failure_config.md | 69 + .../vcenter/vcenter.alarm_sso_config.md | 69 + .../vcenter/vcenter.backup_schedule_config.md | 119 + .../markdown/vcenter/vcenter.cert_config.md | 82 + ...ter.datastore_transit_encryption_config.md | 112 + .../vcenter.datastore_unique_name_policy.md | 73 + .../markdown/vcenter/vcenter.dns_config.md | 97 + .../vcenter.dv_pg_forged_transmits_policy.md | 137 + ...vcenter.dv_pg_mac_address_change_policy.md | 137 + ...nter.dv_pg_native_vlan_exclusion_policy.md | 97 + .../vcenter.dv_pg_promiscuous_mode_policy.md | 137 + ...er.dv_pg_reserved_vlan_exclusion_policy.md | 97 + .../vcenter.dv_pg_vlan_trunking_authorized.md | 104 + .../vcenter.dvs_health_check_config.md | 132 + .../vcenter.dvs_network_io_control_policy.md | 131 + ...center.h5_client_session_timeout_config.md | 62 + .../vcenter.ldap_identity_source_config.md | 59 + .../vcenter/vcenter.logon_banner_config.md | 96 + docs/controllers/markdown/vcenter/vcenter.md | 187 + .../markdown/vcenter/vcenter.ntp_config.md | 97 + .../vcenter/vcenter.snmp_v3_config.md | 80 + ..._active_directory_authentication_policy.md | 59 + ...o_active_directory_ldaps_enabled_config.md | 78 + .../vcenter.sso_auto_unlock_interval.md | 55 + ...so_bash_shell_authorized_members_config.md | 87 + ...enter.sso_failed_login_attempt_interval.md | 55 + .../vcenter.sso_max_failed_login_attempts.md | 55 + ...center.sso_password_max_lifetime_policy.md | 55 + ...password_min_lowercase_character_policy.md | 55 + ...o_password_min_numeric_character_policy.md | 55 + ...o_password_min_special_character_policy.md | 55 + ...password_min_uppercase_character_policy.md | 55 + ...nter.sso_password_minimum_length_policy.md | 55 + ...r.sso_password_reuse_restriction_policy.md | 55 + ...rusted_admins_authorized_members_config.md | 87 + .../markdown/vcenter/vcenter.syslog_config.md | 117 + ...vcenter.task_and_event_retention_policy.md | 86 + .../vcenter/vcenter.tls_version_config.md | 85 + .../vcenter.users_groups_roles_config.md | 51 + .../markdown/vcenter/vcenter.vc_profile.md | 70 + .../vcenter.vm_migrate_encryption_policy.md | 174 + .../vcenter.vmotion_port_group_config.md | 209 + .../vcenter/vcenter.vpx_log_level_config.md | 60 + ...vpx_sddc_deployed_compliance_kit_config.md | 58 + .../vcenter.vpx_syslog_enablement_config.md | 57 + ...er.vpx_user_host_password_length_policy.md | 67 + ...ter.vpx_user_password_expiration_policy.md | 65 + .../vcenter/vcenter.vsan_hcl_proxy_config.md | 136 + ...vcenter.vsan_iscsi_targets_mchap_config.md | 81 + .../markdown/vrslcm/vrslcm.dns_config.md | 98 + docs/controllers/markdown/vrslcm/vrslcm.md | 10 + .../nsxt_edge/nsxt_edge.ntp_config.rst | 4 + docs/controllers/nsxt_edge/nsxt_edge.rst | 15 + .../nsxt_manager/nsxt_manager.ntp_config.rst | 4 + .../controllers/nsxt_manager/nsxt_manager.rst | 15 + docs/controllers/sample/sample.rst | 15 + .../sample/sample.sample_controller.rst | 4 + .../sddc_manager.auto_rotate_schedule.rst | 4 + .../sddc_manager/sddc_manager.backup.rst | 4 + .../sddc_manager/sddc_manager.cert_config.rst | 4 + .../sddc_manager.depot_config.rst | 4 + .../sddc_manager/sddc_manager.dns_config.rst | 4 + .../sddc_manager/sddc_manager.fips_config.rst | 4 + .../sddc_manager/sddc_manager.ntp_config.rst | 4 + .../sddc_manager.proxy_config.rst | 4 + .../controllers/sddc_manager/sddc_manager.rst | 23 + ...sddc_manager.users_groups_roles_config.rst | 4 + ...ter.alarm_remote_syslog_failure_config.rst | 4 + .../vcenter/vcenter.alarm_sso_config.rst | 4 + .../vcenter.backup_schedule_config.rst | 4 + .../vcenter/vcenter.cert_config.rst | 4 + ...er.datastore_transit_encryption_config.rst | 4 + .../vcenter.datastore_unique_name_policy.rst | 4 + .../vcenter/vcenter.dns_config.rst | 4 + .../vcenter.dv_pg_forged_transmits_policy.rst | 4 + ...center.dv_pg_mac_address_change_policy.rst | 4 + ...ter.dv_pg_native_vlan_exclusion_policy.rst | 4 + .../vcenter.dv_pg_promiscuous_mode_policy.rst | 4 + ...r.dv_pg_reserved_vlan_exclusion_policy.rst | 4 + ...vcenter.dv_pg_vlan_trunking_authorized.rst | 4 + .../vcenter.dvs_health_check_config.rst | 4 + .../vcenter.dvs_network_io_control_policy.rst | 4 + ...enter.h5_client_session_timeout_config.rst | 4 + .../vcenter.ldap_identity_source_config.rst | 4 + .../vcenter/vcenter.logon_banner_config.rst | 4 + .../vcenter/vcenter.ntp_config.rst | 4 + docs/controllers/vcenter/vcenter.rst | 62 + .../vcenter/vcenter.snmp_v3_config.rst | 4 + ...active_directory_authentication_policy.rst | 4 + ..._active_directory_ldaps_enabled_config.rst | 4 + .../vcenter.sso_auto_unlock_interval.rst | 4 + ...o_bash_shell_authorized_members_config.rst | 4 + ...nter.sso_failed_login_attempt_interval.rst | 4 + .../vcenter.sso_max_failed_login_attempts.rst | 4 + ...enter.sso_password_max_lifetime_policy.rst | 4 + ...assword_min_lowercase_character_policy.rst | 4 + ..._password_min_numeric_character_policy.rst | 4 + ..._password_min_special_character_policy.rst | 4 + ...assword_min_uppercase_character_policy.rst | 4 + ...ter.sso_password_minimum_length_policy.rst | 4 + ....sso_password_reuse_restriction_policy.rst | 4 + ...usted_admins_authorized_members_config.rst | 4 + .../vcenter/vcenter.syslog_config.rst | 4 + ...center.task_and_event_retention_policy.rst | 4 + .../vcenter/vcenter.tls_version_config.rst | 4 + .../vcenter.users_groups_roles_config.rst | 4 + .../vcenter/vcenter.vc_profile.rst | 4 + .../vcenter.vm_migrate_encryption_policy.rst | 4 + .../vcenter.vmotion_port_group_config.rst | 4 + .../vcenter/vcenter.vpx_log_level_config.rst | 4 + ...px_sddc_deployed_compliance_kit_config.rst | 4 + .../vcenter.vpx_syslog_enablement_config.rst | 4 + ...r.vpx_user_host_password_length_policy.rst | 4 + ...er.vpx_user_password_expiration_policy.rst | 4 + .../vcenter/vcenter.vsan_hcl_proxy_config.rst | 4 + ...center.vsan_iscsi_targets_mchap_config.rst | 4 + docs/controllers/vrslcm/vrslcm.dns_config.rst | 4 + docs/controllers/vrslcm/vrslcm.rst | 15 + docs/docker-instructions.md | 46 + .../instructions-to-create-new-controllers.md | 244 + docs/interface-consumption.md | 31 + docs/openapi.json | 932 ++++ .../saltext-changes_to_support_new_product.md | 80 + docs/testing-controllers.md | 444 ++ functional_tests/README.md | 189 + functional_tests/__init__.py | 0 functional_tests/central_test/__init__.py | 0 functional_tests/central_test/central_test.py | 59 + functional_tests/central_test/conftest.py | 151 + .../control_compliance_template.py | 240 + functional_tests/central_test/pytest.ini | 3 + .../central_test/validate_test.py | 116 + functional_tests/jenkins/build.sh | 44 + .../jenkins/remote_test_script.sh | 64 + functional_tests/local_test/__init__.py | 0 functional_tests/local_test/conftest.py | 73 + functional_tests/local_test/local_test.py | 11 + functional_tests/local_test/pytest.ini | 3 + functional_tests/local_test/ssh_command.py | 46 + functional_tests/local_test/ssh_tester.py | 118 + functional_tests/utils/__init__.py | 0 functional_tests/utils/constants.py | 31 + functional_tests/utils/control_util.py | 153 + .../utils/credential_api_client.py | 252 + functional_tests/utils/file_util.py | 21 + functional_tests/utils/racetrack.py | 263 + .../sample_nimbus_compliance_values.yaml | 340 ++ .../values/sample_nimbus_drift_values.yaml | 348 ++ requirements.txt | 5 + requirements/api-requirements.txt | 13 + requirements/dev-requirements.txt | 8 + requirements/functional-test-requirements.txt | 17 + requirements/prod-requirements.txt | 13 + requirements/unit-test-requirements.txt | 21 + setup.cfg | 29 + setup.py | 33 + 564 files changed, 68846 insertions(+) create mode 100644 .dockerignore create mode 100644 .pre-commit-config.yaml create mode 100644 .pylintrc create mode 100755 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100755 Dockerfile create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 config_modules_vmware/__init__.py create mode 100644 config_modules_vmware/app.py create mode 100644 config_modules_vmware/controllers/__init__.py create mode 100644 config_modules_vmware/controllers/base_controller.py create mode 100644 config_modules_vmware/controllers/esxi/__init__.py create mode 100644 config_modules_vmware/controllers/esxi/account_unlock_time_interval.py create mode 100644 config_modules_vmware/controllers/esxi/bridge_protocol_data_unit_filter.py create mode 100644 config_modules_vmware/controllers/esxi/cim_service_policy.py create mode 100644 config_modules_vmware/controllers/esxi/dcui_login_banner.py create mode 100644 config_modules_vmware/controllers/esxi/firewall_rulesets_config.py create mode 100644 config_modules_vmware/controllers/esxi/hyperthread_warning_policy.py create mode 100644 config_modules_vmware/controllers/esxi/managed_object_browser.py create mode 100644 config_modules_vmware/controllers/esxi/max_failed_login_attempts.py create mode 100644 config_modules_vmware/controllers/esxi/ntp_service_config.py create mode 100644 config_modules_vmware/controllers/esxi/ntp_service_startup_policy.py create mode 100644 config_modules_vmware/controllers/esxi/password_max_lifetime_policy.py create mode 100644 config_modules_vmware/controllers/esxi/password_reuse_restriction_policy.py create mode 100644 config_modules_vmware/controllers/esxi/shell_service_policy.py create mode 100644 config_modules_vmware/controllers/esxi/slp_service_policy.py create mode 100644 config_modules_vmware/controllers/esxi/snmp_service_policy.py create mode 100644 config_modules_vmware/controllers/esxi/ssh_ignore_rhosts_policy.py create mode 100644 config_modules_vmware/controllers/esxi/ssh_login_banner.py create mode 100644 config_modules_vmware/controllers/esxi/ssh_port_forwarding_policy.py create mode 100644 config_modules_vmware/controllers/esxi/tls_version.py create mode 100644 config_modules_vmware/controllers/esxi/utils/__init__.py create mode 100644 config_modules_vmware/controllers/esxi/utils/esxi_advanced_settings_utils.py create mode 100644 config_modules_vmware/controllers/esxi/utils/esxi_ssh_config_utils.py create mode 100644 config_modules_vmware/controllers/esxi/utils/service_utils.py create mode 100644 config_modules_vmware/controllers/nsxt_edge/__init__.py create mode 100644 config_modules_vmware/controllers/nsxt_edge/ntp_config.py create mode 100644 config_modules_vmware/controllers/nsxt_manager/__init__.py create mode 100644 config_modules_vmware/controllers/nsxt_manager/ntp_config.py create mode 100644 config_modules_vmware/controllers/sample/__init__.py create mode 100644 config_modules_vmware/controllers/sample/sample_controller.py create mode 100644 config_modules_vmware/controllers/sddc_manager/__init__.py create mode 100644 config_modules_vmware/controllers/sddc_manager/auto_rotate_schedule.py create mode 100644 config_modules_vmware/controllers/sddc_manager/backup.py create mode 100644 config_modules_vmware/controllers/sddc_manager/cert_config.py create mode 100644 config_modules_vmware/controllers/sddc_manager/depot_config.py create mode 100644 config_modules_vmware/controllers/sddc_manager/dns_config.py create mode 100644 config_modules_vmware/controllers/sddc_manager/fips_config.py create mode 100644 config_modules_vmware/controllers/sddc_manager/ntp_config.py create mode 100644 config_modules_vmware/controllers/sddc_manager/proxy_config.py create mode 100644 config_modules_vmware/controllers/sddc_manager/users_groups_roles_config.py create mode 100644 config_modules_vmware/controllers/vcenter/__init__.py create mode 100644 config_modules_vmware/controllers/vcenter/alarm_remote_syslog_failure_config.py create mode 100644 config_modules_vmware/controllers/vcenter/alarm_sso_config.py create mode 100644 config_modules_vmware/controllers/vcenter/backup_schedule_config.py create mode 100644 config_modules_vmware/controllers/vcenter/cert_config.py create mode 100644 config_modules_vmware/controllers/vcenter/datastore_transit_encryption_config.py create mode 100644 config_modules_vmware/controllers/vcenter/datastore_unique_name_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/dns_config.py create mode 100644 config_modules_vmware/controllers/vcenter/dv_pg_forged_transmits_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/dv_pg_mac_address_change_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/dv_pg_native_vlan_exclusion_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/dv_pg_promiscuous_mode_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/dv_pg_reserved_vlan_exclusion_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/dv_pg_vlan_trunking_authorized.py create mode 100644 config_modules_vmware/controllers/vcenter/dvs_health_check_config.py create mode 100644 config_modules_vmware/controllers/vcenter/dvs_network_io_control_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/h5_client_session_timeout_config.py create mode 100644 config_modules_vmware/controllers/vcenter/ldap_identity_source_config.py create mode 100644 config_modules_vmware/controllers/vcenter/logon_banner_config.py create mode 100644 config_modules_vmware/controllers/vcenter/ntp_config.py create mode 100644 config_modules_vmware/controllers/vcenter/snmp_v3_config.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_active_directory_authentication_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_active_directory_ldaps_enabled_config.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_auto_unlock_interval.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_bash_shell_authorized_members_config.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_failed_login_attempt_interval.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_max_failed_login_attempts.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_password_max_lifetime_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_password_min_lowercase_character_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_password_min_numeric_character_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_password_min_special_character_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_password_min_uppercase_character_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_password_minimum_length_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_password_reuse_restriction_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/sso_trusted_admins_authorized_members_config.py create mode 100644 config_modules_vmware/controllers/vcenter/syslog_config.py create mode 100644 config_modules_vmware/controllers/vcenter/task_and_event_retention_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/tls_version_config.py create mode 100644 config_modules_vmware/controllers/vcenter/users_groups_roles_config.py create mode 100644 config_modules_vmware/controllers/vcenter/utils/__init__.py create mode 100644 config_modules_vmware/controllers/vcenter/utils/vc_alarms_utils.py create mode 100644 config_modules_vmware/controllers/vcenter/utils/vc_port_group_utils.py create mode 100644 config_modules_vmware/controllers/vcenter/utils/vc_profile_utils.py create mode 100644 config_modules_vmware/controllers/vcenter/vc_profile.py create mode 100644 config_modules_vmware/controllers/vcenter/vm_migrate_encryption_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/vmotion_port_group_config.py create mode 100644 config_modules_vmware/controllers/vcenter/vpx_log_level_config.py create mode 100644 config_modules_vmware/controllers/vcenter/vpx_sddc_deployed_compliance_kit_config.py create mode 100644 config_modules_vmware/controllers/vcenter/vpx_syslog_enablement_config.py create mode 100644 config_modules_vmware/controllers/vcenter/vpx_user_host_password_length_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/vpx_user_password_expiration_policy.py create mode 100644 config_modules_vmware/controllers/vcenter/vsan_hcl_proxy_config.py create mode 100644 config_modules_vmware/controllers/vcenter/vsan_iscsi_targets_mchap_config.py create mode 100644 config_modules_vmware/controllers/vrslcm/__init__.py create mode 100644 config_modules_vmware/controllers/vrslcm/dns_config.py create mode 100644 config_modules_vmware/framework/__init__.py create mode 100644 config_modules_vmware/framework/auth/__init__.py create mode 100644 config_modules_vmware/framework/auth/contexts/__init__.py create mode 100644 config_modules_vmware/framework/auth/contexts/base_context.py create mode 100644 config_modules_vmware/framework/auth/contexts/esxi_context.py create mode 100644 config_modules_vmware/framework/auth/contexts/sddc_manager_context.py create mode 100644 config_modules_vmware/framework/auth/contexts/vc_context.py create mode 100644 config_modules_vmware/framework/auth/contexts/vrslcm_context.py create mode 100644 config_modules_vmware/framework/clients/__init__.py create mode 100644 config_modules_vmware/framework/clients/aria_suite/__init__.py create mode 100644 config_modules_vmware/framework/clients/aria_suite/aria_auth.py create mode 100644 config_modules_vmware/framework/clients/aria_suite/aria_consts.py create mode 100644 config_modules_vmware/framework/clients/common/__init__.py create mode 100644 config_modules_vmware/framework/clients/common/consts.py create mode 100644 config_modules_vmware/framework/clients/common/rest_client.py create mode 100644 config_modules_vmware/framework/clients/common/vmomi_client.py create mode 100644 config_modules_vmware/framework/clients/esxi/__init__.py create mode 100644 config_modules_vmware/framework/clients/esxi/esx_cli_client.py create mode 100644 config_modules_vmware/framework/clients/sddc_manager/__init__.py create mode 100644 config_modules_vmware/framework/clients/sddc_manager/sddc_manager_consts.py create mode 100644 config_modules_vmware/framework/clients/sddc_manager/sddc_manager_rest_client.py create mode 100644 config_modules_vmware/framework/clients/vcenter/__init__.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/__init__.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVim/__init__.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVim/sso.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVmomi/CoreTypes.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVmomi/Iso8601.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVmomi/QueryTypes.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVmomi/SoapAdapter.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVmomi/SsoTypes.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVmomi/StubAdapterAccessorImpl.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVmomi/Version.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVmomi/VmomiSupport.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVmomi/__bindings.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/pyVmomi/__init__.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/vsan_management/VMware vSAN Management Software Development Kit License Agreement.pdf create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/vsan_management/__init__.py create mode 100644 config_modules_vmware/framework/clients/vcenter/dependencies/vsan_management/vsanmgmtObjects.py create mode 100644 config_modules_vmware/framework/clients/vcenter/vc_consts.py create mode 100644 config_modules_vmware/framework/clients/vcenter/vc_rest_client.py create mode 100644 config_modules_vmware/framework/clients/vcenter/vc_vmomi_client.py create mode 100644 config_modules_vmware/framework/clients/vcenter/vc_vmomi_sso_client.py create mode 100644 config_modules_vmware/framework/clients/vcenter/vc_vsan_vmomi_client.py create mode 100644 config_modules_vmware/framework/logging/__init__.py create mode 100644 config_modules_vmware/framework/logging/logger_adapter.py create mode 100644 config_modules_vmware/framework/logging/logging_context.py create mode 100644 config_modules_vmware/framework/models/__init__.py create mode 100644 config_modules_vmware/framework/models/controller_models/__init__.py create mode 100644 config_modules_vmware/framework/models/controller_models/metadata.py create mode 100644 config_modules_vmware/framework/models/output_models/__init__.py create mode 100644 config_modules_vmware/framework/models/output_models/compliance_response.py create mode 100644 config_modules_vmware/framework/models/output_models/configuration_drift_response.py create mode 100644 config_modules_vmware/framework/models/output_models/get_current_response.py create mode 100644 config_modules_vmware/framework/models/output_models/output_response.py create mode 100644 config_modules_vmware/framework/models/output_models/remediate_response.py create mode 100644 config_modules_vmware/framework/utils/__init__.py create mode 100644 config_modules_vmware/framework/utils/comparator.py create mode 100644 config_modules_vmware/framework/utils/target_enum.py create mode 100644 config_modules_vmware/framework/utils/task.py create mode 100644 config_modules_vmware/framework/utils/utils.py create mode 100644 config_modules_vmware/interfaces/__init__.py create mode 100644 config_modules_vmware/interfaces/controller_interface.py create mode 100644 config_modules_vmware/interfaces/metadata_interface.py create mode 100644 config_modules_vmware/log_config.yml create mode 100644 config_modules_vmware/schemas/__init__.py create mode 100644 config_modules_vmware/schemas/compliance_reference_schema.json create mode 100644 config_modules_vmware/schemas/schema_utility.py create mode 100644 config_modules_vmware/services/__init__.py create mode 100644 config_modules_vmware/services/apis/__init__.py create mode 100644 config_modules_vmware/services/apis/controllers/__init__.py create mode 100644 config_modules_vmware/services/apis/controllers/consts.py create mode 100644 config_modules_vmware/services/apis/controllers/misc.py create mode 100644 config_modules_vmware/services/apis/controllers/vcenter.py create mode 100644 config_modules_vmware/services/apis/models/__init__.py create mode 100644 config_modules_vmware/services/apis/models/about.py create mode 100644 config_modules_vmware/services/apis/models/drift_payload.py create mode 100644 config_modules_vmware/services/apis/models/error_model.py create mode 100644 config_modules_vmware/services/apis/models/get_config_payload.py create mode 100644 config_modules_vmware/services/apis/models/healthcheck.py create mode 100644 config_modules_vmware/services/apis/models/openapi_examples.py create mode 100644 config_modules_vmware/services/apis/models/request_payload.py create mode 100644 config_modules_vmware/services/apis/models/target_model.py create mode 100644 config_modules_vmware/services/config.ini create mode 100644 config_modules_vmware/services/config.py create mode 100644 config_modules_vmware/services/mapper/__init__.py create mode 100644 config_modules_vmware/services/mapper/configuration_mapping.json create mode 100644 config_modules_vmware/services/mapper/control_config_mapping.json create mode 100644 config_modules_vmware/services/mapper/mapper_utils.py create mode 100644 config_modules_vmware/services/workflows/__init__.py create mode 100644 config_modules_vmware/services/workflows/compliance_operations.py create mode 100644 config_modules_vmware/services/workflows/configuration_operations.py create mode 100644 config_modules_vmware/services/workflows/operations_interface.py create mode 100644 config_modules_vmware/tests/README.md create mode 100644 config_modules_vmware/tests/__init__.py create mode 100644 config_modules_vmware/tests/config.ini create mode 100644 config_modules_vmware/tests/controllers/__init__.py create mode 100644 config_modules_vmware/tests/controllers/esxi/__init__.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_account_unlock_time_interval.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_bridge_protocol_data_unit_filter.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_cim_service_policy.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_dcui_login_banner.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_firewall_rulesets_config.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_managed_object_browser.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_max_failed_login_attempts.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_ntp_service_config.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_ntp_service_startup_policy.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_password_max_lifetime_policy.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_password_reuse_restriction_policy.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_shell_service_policy.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_slp_service_policy.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_snmp_service_policy.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_ssh_ignore_rhosts_policy.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_ssh_login_banner.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_ssh_port_forwarding_policy.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_suppress_hyperthread_warning.py create mode 100644 config_modules_vmware/tests/controllers/esxi/test_tls_version.py create mode 100644 config_modules_vmware/tests/controllers/esxi/utils/__init__.py create mode 100644 config_modules_vmware/tests/controllers/esxi/utils/test_esxi_ssh_config_utils.py create mode 100644 config_modules_vmware/tests/controllers/nsxt/__init__.py create mode 100644 config_modules_vmware/tests/controllers/nsxt/test_ntp_config.py create mode 100644 config_modules_vmware/tests/controllers/sample/__init__.py create mode 100644 config_modules_vmware/tests/controllers/sample/test_sample_controller.py create mode 100644 config_modules_vmware/tests/controllers/sddc_manager/__init__.py create mode 100644 config_modules_vmware/tests/controllers/sddc_manager/test_auto_rotate_config.py create mode 100644 config_modules_vmware/tests/controllers/sddc_manager/test_backup_config.py create mode 100644 config_modules_vmware/tests/controllers/sddc_manager/test_cert_config.py create mode 100644 config_modules_vmware/tests/controllers/sddc_manager/test_depot_config.py create mode 100644 config_modules_vmware/tests/controllers/sddc_manager/test_dns_config.py create mode 100644 config_modules_vmware/tests/controllers/sddc_manager/test_fips_config.py create mode 100644 config_modules_vmware/tests/controllers/sddc_manager/test_ntp_config.py create mode 100644 config_modules_vmware/tests/controllers/sddc_manager/test_proxy_config.py create mode 100644 config_modules_vmware/tests/controllers/sddc_manager/test_users_groups_roles_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/__init__.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_alarm_remote_syslog_failure_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_alarm_sso_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_backup_schedule_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_cert_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_datastore_transit_encryption_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_datastore_unique_name_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_dns_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_dv_pg_forged_transmits_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_dv_pg_mac_address_change_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_dv_pg_native_vlan_exclusion_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_dv_pg_promiscuous_mode_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_dv_pg_reserved_vlan_exclusion_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_dv_pg_vlan_trunking_authorized.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_dvs_health_check_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_dvs_network_io_control_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_h5_client_session_timeout_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_ldap_identity_source_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_login_banner_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_ntp_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_snmp_v3_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_active_directory_authentication_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_active_directory_ldaps_enabled_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_auto_unlock_interval.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_bash_shell_authorized_members_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_failed_login_attempt_interval.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_max_failed_login_attempts.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_password_max_lifetime_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_lowercase_character_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_numeric_character_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_special_character_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_password_min_uppercase_character_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_password_minimum_length_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_password_reuse_restriction_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_sso_trusted_admins_authorized_members_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_syslog_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_task_and_event_retention_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_tls_version_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_users_groups_roles_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_vc_profile.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_vm_migrate_encryption_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_vmotion_port_group_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_vpx_log_level_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_vpx_sddc_deployed_compliance_kit_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_vpx_syslog_enablement_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_vpx_user_host_password_length_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_vpx_user_password_expiration_policy.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_vsan_hcl_proxy_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/test_vsan_iscsi_targets_mchap_config.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/utils/__init__.py create mode 100644 config_modules_vmware/tests/controllers/vcenter/utils/test_vc_profile_utils.py create mode 100644 config_modules_vmware/tests/controllers/vrslcm/__init__.py create mode 100644 config_modules_vmware/tests/controllers/vrslcm/test_dns_config.py create mode 100644 config_modules_vmware/tests/framework/__init__.py create mode 100644 config_modules_vmware/tests/framework/auth/__init__.py create mode 100644 config_modules_vmware/tests/framework/auth/contexts/__init__.py create mode 100644 config_modules_vmware/tests/framework/auth/contexts/test_base_context.py create mode 100644 config_modules_vmware/tests/framework/auth/contexts/test_esxi_context.py create mode 100644 config_modules_vmware/tests/framework/auth/contexts/test_sddc_manager_context.py create mode 100644 config_modules_vmware/tests/framework/auth/contexts/test_vc_context.py create mode 100644 config_modules_vmware/tests/framework/clients/__init__.py create mode 100644 config_modules_vmware/tests/framework/clients/aria_suite/__init__.py create mode 100644 config_modules_vmware/tests/framework/clients/aria_suite/test_aria_auth.py create mode 100644 config_modules_vmware/tests/framework/clients/esxi/__init__.py create mode 100644 config_modules_vmware/tests/framework/clients/esxi/test_esx_cli_client.py create mode 100644 config_modules_vmware/tests/framework/clients/vcenter/__init__.py create mode 100644 config_modules_vmware/tests/framework/clients/vcenter/test_vc_vmomi_client.py create mode 100644 config_modules_vmware/tests/framework/logging/__init__.py create mode 100644 config_modules_vmware/tests/framework/logging/test_logger_adapter.py create mode 100644 config_modules_vmware/tests/framework/logging/test_logging_context.py create mode 100644 config_modules_vmware/tests/framework/models/__init__.py create mode 100644 config_modules_vmware/tests/framework/models/controller_models/__init__.py create mode 100644 config_modules_vmware/tests/framework/models/controller_models/test_metadata.py create mode 100644 config_modules_vmware/tests/framework/models/output_models/__init__.py create mode 100644 config_modules_vmware/tests/framework/models/output_models/test_compliance_response.py create mode 100644 config_modules_vmware/tests/framework/models/output_models/test_get_current_response.py create mode 100644 config_modules_vmware/tests/framework/models/output_models/test_output_response.py create mode 100644 config_modules_vmware/tests/framework/models/output_models/test_remediate_response.py create mode 100644 config_modules_vmware/tests/framework/utils/__init__.py create mode 100644 config_modules_vmware/tests/framework/utils/test_comparator.py create mode 100644 config_modules_vmware/tests/framework/utils/test_task.py create mode 100644 config_modules_vmware/tests/framework/utils/test_utils.py create mode 100644 config_modules_vmware/tests/interfaces/__init__.py create mode 100644 config_modules_vmware/tests/interfaces/test_controller_interface.py create mode 100644 config_modules_vmware/tests/interfaces/test_metadata_interface.py create mode 100644 config_modules_vmware/tests/schemas/__init__.py create mode 100644 config_modules_vmware/tests/schemas/test_schema_utility.py create mode 100644 config_modules_vmware/tests/services/__init__.py create mode 100644 config_modules_vmware/tests/services/apis/__init__.py create mode 100644 config_modules_vmware/tests/services/apis/controllers/__init__.py create mode 100644 config_modules_vmware/tests/services/apis/controllers/test_misc.py create mode 100644 config_modules_vmware/tests/services/apis/controllers/test_vcenter.py create mode 100644 config_modules_vmware/tests/services/mapper/__init__.py create mode 100644 config_modules_vmware/tests/services/mapper/test_mapper_utils.py create mode 100644 config_modules_vmware/tests/services/test_config.py create mode 100644 config_modules_vmware/tests/services/workflows/__init__.py create mode 100644 config_modules_vmware/tests/services/workflows/test_compliance_operations.py create mode 100644 config_modules_vmware/tests/services/workflows/test_configuration_operations.py create mode 100644 config_modules_vmware/tests/services/workflows/test_operations_interface.py create mode 100644 devops/release/library/version.yml create mode 100755 devops/scripts/build.sh create mode 100755 devops/scripts/generate_markdown_docs.sh create mode 100755 devops/scripts/generate_openapi_spec.py create mode 100755 devops/scripts/run_formatting.sh create mode 100755 devops/scripts/run_functional_tests.sh create mode 100755 devops/scripts/run_reorder_imports.sh create mode 100755 devops/scripts/run_security_analysis.sh create mode 100755 devops/scripts/run_static_code_analysis.sh create mode 100755 devops/scripts/start_api_server.sh create mode 100644 docs/api-service.md create mode 100644 docs/controllers/conf.py create mode 100644 docs/controllers/esxi/esxi.account_unlock_time_interval.rst create mode 100644 docs/controllers/esxi/esxi.bridge_protocol_data_unit_filter.rst create mode 100644 docs/controllers/esxi/esxi.cim_service_policy.rst create mode 100644 docs/controllers/esxi/esxi.dcui_login_banner.rst create mode 100644 docs/controllers/esxi/esxi.firewall_rulesets_config.rst create mode 100644 docs/controllers/esxi/esxi.hyperthread_warning_policy.rst create mode 100644 docs/controllers/esxi/esxi.managed_object_browser.rst create mode 100644 docs/controllers/esxi/esxi.max_failed_login_attempts.rst create mode 100644 docs/controllers/esxi/esxi.ntp_service_config.rst create mode 100644 docs/controllers/esxi/esxi.ntp_service_startup_policy.rst create mode 100644 docs/controllers/esxi/esxi.password_max_lifetime_policy.rst create mode 100644 docs/controllers/esxi/esxi.password_reuse_restriction_policy.rst create mode 100644 docs/controllers/esxi/esxi.rst create mode 100644 docs/controllers/esxi/esxi.shell_service_policy.rst create mode 100644 docs/controllers/esxi/esxi.slp_service_policy.rst create mode 100644 docs/controllers/esxi/esxi.snmp_service_policy.rst create mode 100644 docs/controllers/esxi/esxi.ssh_ignore_rhosts_policy.rst create mode 100644 docs/controllers/esxi/esxi.ssh_login_banner.rst create mode 100644 docs/controllers/esxi/esxi.ssh_port_forwarding_policy.rst create mode 100644 docs/controllers/esxi/esxi.tls_version.rst create mode 100644 docs/controllers/index.rst create mode 100644 docs/controllers/markdown/esxi/esxi.account_unlock_time_interval.md create mode 100644 docs/controllers/markdown/esxi/esxi.bridge_protocol_data_unit_filter.md create mode 100644 docs/controllers/markdown/esxi/esxi.cim_service_policy.md create mode 100644 docs/controllers/markdown/esxi/esxi.dcui_login_banner.md create mode 100644 docs/controllers/markdown/esxi/esxi.firewall_rulesets_config.md create mode 100644 docs/controllers/markdown/esxi/esxi.hyperthread_warning_policy.md create mode 100644 docs/controllers/markdown/esxi/esxi.managed_object_browser.md create mode 100644 docs/controllers/markdown/esxi/esxi.max_failed_login_attempts.md create mode 100644 docs/controllers/markdown/esxi/esxi.md create mode 100644 docs/controllers/markdown/esxi/esxi.ntp_service_config.md create mode 100644 docs/controllers/markdown/esxi/esxi.ntp_service_startup_policy.md create mode 100644 docs/controllers/markdown/esxi/esxi.password_max_lifetime_policy.md create mode 100644 docs/controllers/markdown/esxi/esxi.password_reuse_restriction_policy.md create mode 100644 docs/controllers/markdown/esxi/esxi.shell_service_policy.md create mode 100644 docs/controllers/markdown/esxi/esxi.slp_service_policy.md create mode 100644 docs/controllers/markdown/esxi/esxi.snmp_service_policy.md create mode 100644 docs/controllers/markdown/esxi/esxi.ssh_ignore_rhosts_policy.md create mode 100644 docs/controllers/markdown/esxi/esxi.ssh_login_banner.md create mode 100644 docs/controllers/markdown/esxi/esxi.ssh_port_forwarding_policy.md create mode 100644 docs/controllers/markdown/esxi/esxi.tls_version.md create mode 100644 docs/controllers/markdown/index.md create mode 100644 docs/controllers/markdown/nsxt_edge/nsxt_edge.md create mode 100644 docs/controllers/markdown/nsxt_edge/nsxt_edge.ntp_config.md create mode 100644 docs/controllers/markdown/nsxt_manager/nsxt_manager.md create mode 100644 docs/controllers/markdown/nsxt_manager/nsxt_manager.ntp_config.md create mode 100644 docs/controllers/markdown/sample/sample.md create mode 100644 docs/controllers/markdown/sample/sample.sample_controller.md create mode 100644 docs/controllers/markdown/sddc_manager/sddc_manager.auto_rotate_schedule.md create mode 100644 docs/controllers/markdown/sddc_manager/sddc_manager.backup.md create mode 100644 docs/controllers/markdown/sddc_manager/sddc_manager.cert_config.md create mode 100644 docs/controllers/markdown/sddc_manager/sddc_manager.depot_config.md create mode 100644 docs/controllers/markdown/sddc_manager/sddc_manager.dns_config.md create mode 100644 docs/controllers/markdown/sddc_manager/sddc_manager.fips_config.md create mode 100644 docs/controllers/markdown/sddc_manager/sddc_manager.md create mode 100644 docs/controllers/markdown/sddc_manager/sddc_manager.ntp_config.md create mode 100644 docs/controllers/markdown/sddc_manager/sddc_manager.proxy_config.md create mode 100644 docs/controllers/markdown/sddc_manager/sddc_manager.users_groups_roles_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.alarm_remote_syslog_failure_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.alarm_sso_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.backup_schedule_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.cert_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.datastore_transit_encryption_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.datastore_unique_name_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.dns_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.dv_pg_forged_transmits_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.dv_pg_mac_address_change_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.dv_pg_native_vlan_exclusion_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.dv_pg_promiscuous_mode_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.dv_pg_reserved_vlan_exclusion_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.dv_pg_vlan_trunking_authorized.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.dvs_health_check_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.dvs_network_io_control_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.h5_client_session_timeout_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.ldap_identity_source_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.logon_banner_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.ntp_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.snmp_v3_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_active_directory_authentication_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_active_directory_ldaps_enabled_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_auto_unlock_interval.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_bash_shell_authorized_members_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_failed_login_attempt_interval.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_max_failed_login_attempts.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_password_max_lifetime_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_password_min_lowercase_character_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_password_min_numeric_character_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_password_min_special_character_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_password_min_uppercase_character_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_password_minimum_length_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_password_reuse_restriction_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.sso_trusted_admins_authorized_members_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.syslog_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.task_and_event_retention_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.tls_version_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.users_groups_roles_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.vc_profile.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.vm_migrate_encryption_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.vmotion_port_group_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.vpx_log_level_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.vpx_sddc_deployed_compliance_kit_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.vpx_syslog_enablement_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.vpx_user_host_password_length_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.vpx_user_password_expiration_policy.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.vsan_hcl_proxy_config.md create mode 100644 docs/controllers/markdown/vcenter/vcenter.vsan_iscsi_targets_mchap_config.md create mode 100644 docs/controllers/markdown/vrslcm/vrslcm.dns_config.md create mode 100644 docs/controllers/markdown/vrslcm/vrslcm.md create mode 100644 docs/controllers/nsxt_edge/nsxt_edge.ntp_config.rst create mode 100644 docs/controllers/nsxt_edge/nsxt_edge.rst create mode 100644 docs/controllers/nsxt_manager/nsxt_manager.ntp_config.rst create mode 100644 docs/controllers/nsxt_manager/nsxt_manager.rst create mode 100644 docs/controllers/sample/sample.rst create mode 100644 docs/controllers/sample/sample.sample_controller.rst create mode 100644 docs/controllers/sddc_manager/sddc_manager.auto_rotate_schedule.rst create mode 100644 docs/controllers/sddc_manager/sddc_manager.backup.rst create mode 100644 docs/controllers/sddc_manager/sddc_manager.cert_config.rst create mode 100644 docs/controllers/sddc_manager/sddc_manager.depot_config.rst create mode 100644 docs/controllers/sddc_manager/sddc_manager.dns_config.rst create mode 100644 docs/controllers/sddc_manager/sddc_manager.fips_config.rst create mode 100644 docs/controllers/sddc_manager/sddc_manager.ntp_config.rst create mode 100644 docs/controllers/sddc_manager/sddc_manager.proxy_config.rst create mode 100644 docs/controllers/sddc_manager/sddc_manager.rst create mode 100644 docs/controllers/sddc_manager/sddc_manager.users_groups_roles_config.rst create mode 100644 docs/controllers/vcenter/vcenter.alarm_remote_syslog_failure_config.rst create mode 100644 docs/controllers/vcenter/vcenter.alarm_sso_config.rst create mode 100644 docs/controllers/vcenter/vcenter.backup_schedule_config.rst create mode 100644 docs/controllers/vcenter/vcenter.cert_config.rst create mode 100644 docs/controllers/vcenter/vcenter.datastore_transit_encryption_config.rst create mode 100644 docs/controllers/vcenter/vcenter.datastore_unique_name_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.dns_config.rst create mode 100644 docs/controllers/vcenter/vcenter.dv_pg_forged_transmits_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.dv_pg_mac_address_change_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.dv_pg_native_vlan_exclusion_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.dv_pg_promiscuous_mode_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.dv_pg_reserved_vlan_exclusion_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.dv_pg_vlan_trunking_authorized.rst create mode 100644 docs/controllers/vcenter/vcenter.dvs_health_check_config.rst create mode 100644 docs/controllers/vcenter/vcenter.dvs_network_io_control_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.h5_client_session_timeout_config.rst create mode 100644 docs/controllers/vcenter/vcenter.ldap_identity_source_config.rst create mode 100644 docs/controllers/vcenter/vcenter.logon_banner_config.rst create mode 100644 docs/controllers/vcenter/vcenter.ntp_config.rst create mode 100644 docs/controllers/vcenter/vcenter.rst create mode 100644 docs/controllers/vcenter/vcenter.snmp_v3_config.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_active_directory_authentication_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_active_directory_ldaps_enabled_config.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_auto_unlock_interval.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_bash_shell_authorized_members_config.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_failed_login_attempt_interval.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_max_failed_login_attempts.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_password_max_lifetime_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_password_min_lowercase_character_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_password_min_numeric_character_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_password_min_special_character_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_password_min_uppercase_character_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_password_minimum_length_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_password_reuse_restriction_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.sso_trusted_admins_authorized_members_config.rst create mode 100644 docs/controllers/vcenter/vcenter.syslog_config.rst create mode 100644 docs/controllers/vcenter/vcenter.task_and_event_retention_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.tls_version_config.rst create mode 100644 docs/controllers/vcenter/vcenter.users_groups_roles_config.rst create mode 100644 docs/controllers/vcenter/vcenter.vc_profile.rst create mode 100644 docs/controllers/vcenter/vcenter.vm_migrate_encryption_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.vmotion_port_group_config.rst create mode 100644 docs/controllers/vcenter/vcenter.vpx_log_level_config.rst create mode 100644 docs/controllers/vcenter/vcenter.vpx_sddc_deployed_compliance_kit_config.rst create mode 100644 docs/controllers/vcenter/vcenter.vpx_syslog_enablement_config.rst create mode 100644 docs/controllers/vcenter/vcenter.vpx_user_host_password_length_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.vpx_user_password_expiration_policy.rst create mode 100644 docs/controllers/vcenter/vcenter.vsan_hcl_proxy_config.rst create mode 100644 docs/controllers/vcenter/vcenter.vsan_iscsi_targets_mchap_config.rst create mode 100644 docs/controllers/vrslcm/vrslcm.dns_config.rst create mode 100644 docs/controllers/vrslcm/vrslcm.rst create mode 100644 docs/docker-instructions.md create mode 100644 docs/instructions-to-create-new-controllers.md create mode 100644 docs/interface-consumption.md create mode 100644 docs/openapi.json create mode 100644 docs/saltext-changes_to_support_new_product.md create mode 100644 docs/testing-controllers.md create mode 100644 functional_tests/README.md create mode 100644 functional_tests/__init__.py create mode 100644 functional_tests/central_test/__init__.py create mode 100644 functional_tests/central_test/central_test.py create mode 100644 functional_tests/central_test/conftest.py create mode 100644 functional_tests/central_test/control_compliance_template.py create mode 100644 functional_tests/central_test/pytest.ini create mode 100644 functional_tests/central_test/validate_test.py create mode 100755 functional_tests/jenkins/build.sh create mode 100644 functional_tests/jenkins/remote_test_script.sh create mode 100644 functional_tests/local_test/__init__.py create mode 100644 functional_tests/local_test/conftest.py create mode 100644 functional_tests/local_test/local_test.py create mode 100644 functional_tests/local_test/pytest.ini create mode 100644 functional_tests/local_test/ssh_command.py create mode 100644 functional_tests/local_test/ssh_tester.py create mode 100644 functional_tests/utils/__init__.py create mode 100644 functional_tests/utils/constants.py create mode 100644 functional_tests/utils/control_util.py create mode 100644 functional_tests/utils/credential_api_client.py create mode 100644 functional_tests/utils/file_util.py create mode 100644 functional_tests/utils/racetrack.py create mode 100644 functional_tests/values/sample_nimbus_compliance_values.yaml create mode 100644 functional_tests/values/sample_nimbus_drift_values.yaml create mode 100644 requirements.txt create mode 100644 requirements/api-requirements.txt create mode 100644 requirements/dev-requirements.txt create mode 100644 requirements/functional-test-requirements.txt create mode 100644 requirements/prod-requirements.txt create mode 100644 requirements/unit-test-requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6988e72 --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +**/tests \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b6eaef2 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,31 @@ +default_language_version: + python: python3 +fail_fast: true +repos: +- repo: local + hooks: + - id: reorder_imports + name: Reorder Imports. + entry: ./devops/scripts/run_reorder_imports.sh + language: script + pass_filenames: true + - id: formatting + name: Fix formatting with Black. + entry: ./devops/scripts/run_formatting.sh + language: script + pass_filenames: false + - id: security_analysis + name: Run security analysis with Bandit. + entry: ./devops/scripts/run_security_analysis.sh + language: script + pass_filenames: false + - id: static_code_analysis + name: Run static code analysis with pylint. + entry: ./devops/scripts/run_static_code_analysis.sh + language: script + pass_filenames: false + - id: generate_docs + name: Generate documentation. + entry: ./devops/scripts/generate_markdown_docs.sh + language: script + pass_filenames: false diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..b5a8337 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,391 @@ +[MASTER] + +# Specify a configuration file. +rcfile=.pylintrc + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=tests,requirements.txt,api-requirements.txt,log_config.yml,unit-test-requirements.txt,functional-test-requirements.txt,prod-requirements.txt,dev-requirements.txt +# Add directories to the blacklist based on paths +ignore-paths=config_modules_vmware/framework/clients/vcenter/dependencies/ + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Use multiple processes to speed up Pylint. +jobs=1 + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code +extension-pkg-whitelist= + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED +confidence= + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +#disable= +disable=R, + C, + I0011, + I1101, + I0013, + C0330, + W1203, + W0719, + W0212, + W0221, + W0718, + W0104, + wrong-import-position, + wrong-import-order, + no-name-in-module, + +# Disabled: +# R* [refactoring suggestions & reports] +# I0011 (locally-disabling) +# I0012 (locally-enabling) +# I0013 (file-ignored) +# C0330 (bad-continuation) Wrong hanging indentation before block (add 4 spaces). +# E8116 PEP8 E116: unexpected indentation (comment) +# E812* All PEP8 E12* + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[SPELLING] + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file=.pylint-spelling-words + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins=__opts__, + __utils__, + __salt__, + __pillar__, + __grains__, + __context__, + __runner__, + __ret__, + __env__, + __low__, + __states__, + __lowstate__, + __running__, + __active_provider_name__, + __master_opts__, + __jid_event__, + __instance_id__, + __salt_system_encoding__, + __proxy__, + __serializers__, + __reg__, + __executors__, + __events__ + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_,_cb + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[BASIC] + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_,log,pytest_plugins,__opts__,__context__ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Naming style matching correct function names. +function-naming-style = snake_case + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,60}$ + +# Naming style matching correct variable names. +variable-naming-style = snake_case + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,60}$ + +# Naming style matching correct constant names. +const-naming-style = UPPER_CASE + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming style matching correct attribute names. +attr-naming-style = snake_case + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,60}$ + +# Naming style matching correct argument names. +argument-naming-style = snake_case + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,60}$ + +# Naming style matching correct class attribute names. +class-attribute-naming-style = any + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,60}|(__.*__))$ + +# Naming style matching correct inline iteration names. +inlinevar-naming-style = any + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming style matching correct class names. +class-naming-style = PascalCase + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming style matching correct module names. +module-naming-style = snake_case + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,60}$ + +# Naming style matching correct method names. +method-naming-style = snake_case + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=120 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format=LF + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis +ignored-modules= + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes= + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members= + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=builtins.Exception diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 0000000..5bc59f4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# CURRENTLY-IN-DEVELOPMENT +- Initial Open Source Release! +- Support for two controller types (Compliance and Configuration) + - Compliance has support for 5 products and 77 Controllers + - ESXi - 19 Controllers + - NSX-T - 1 Controller + - SDDC Manager - 9 Controllers + - VCSA - 47 Controllers + - VRSLCM - 1 Controller + - Configuration has support for 2 VC Profile components - AuthManagement and Appliance. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4621128 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,117 @@ +# Contributing to `vmware-config-modules` + +We welcome contributions from the community and first want to thank you for taking the time to contribute! + +Please familiarize yourself with the [Code of Conduct](https://github.com/vmware/.github/blob/main/CODE_OF_CONDUCT.md) before contributing. + +__Before you start working with `vmware-config-modules`, please read our [Developer Certificate of Origin](https://cla.vmware.com/dco).__ + +__All contributions to this repository must be signed as described on that page.__ + +## Ways to contribute + +We welcome many different types of contributions and not all of them need a Pull request. Contributions may include: + +* New features and proposals +* Documentation +* Bug fixes +* Issue Triage +* Answering questions and giving feedback +* Helping to onboard new contributors +* Other related activities + +## Getting started + +### Setting up your dev environment +We recommend setting up a Python virtual environment to avoid having conflicts with local dependencies: +``` +# virtual env setup +python3 -m venv env +source env/bin/activate +python3 -m pip install -r requirements/dev-requirements.txt +``` + +### Building from source +From the repository root directory run the below commands. +```shell +python3 -m pip install build +python3 -m build +``` + The .whl package will be under the `./dist` directory. That built whl package can be installed locally with +```shell +python3 -m pip install dist/config_modules-*-py3-none-any.whl --force-reinstall +``` +### Running the unit tests +To simply run the unit tests, run the following commands. +```shell +python3 -m pip install -r requirements/unit-test-requirements.txt +pytest +``` +There is also a script available that will run the tests and generate a coverage report for you which can be invoked with +```shell +./devops/scripts/run_functional_tests.sh +``` +### Set up pre-commit +We use pre-commit to give early feedback on code quality and formatting. +Once pre-commit is installed, it automatically runs whenever a developer tries to do a git commit with new changes. Only if all pre-commit hooks are passed can a developer commit the changes. +Pre-commit can be installed into your virtual environment from above and set up with the following commands: +```shell +python3 -m pip install pre-commit +pre-commit install +``` +Sample pre-commit response: +``` +(env) ➜ config-poc git:(test_branch) ✗ git commit -m "test changes" +Fix formatting with Black................................................Passed +Reorder Imports..........................................................Passed +Run security analysis with Bandit........................................Passed +Run static code analysis with pylint.....................................Passed +Generate documentation...................................................Passed +[test_branch 1620b1c] test changes + 1 file changed, 1 insertion(+), 1 deletion(-) +``` + +Any files modified or generated as part of pre-commit needs to be added to the commit using `git add` before committing. + +Individual scripts are also provided in the [./devops/scripts](./devops/scripts) directory in case a developer wants to execute them individually: +- [Code Formatter](./devops/scripts/run_formatting.sh) +- [Re-Order imports](./devops/scripts/run_reorder_imports.sh) +- [Check for CWE violations](./devops/scripts/run_security_analysis.sh) +- [pyLint static analysis](./devops/scripts/run_static_code_analysis.sh) + +If the changes are in the API layer, developer needs to generate the openapi spec (using the below steps) and add it to the git commit. +- Build and install config-module with the changes. +- Use the script to generate openapi spec - [Generate OpenAPI specification](./devops/scripts/generate_openapi_spec.py) + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +* Make a fork of the repository within your GitHub account +* Create a topic branch in your fork from where you want to base your work +* Make commits of logical units +* Make sure your commit messages are in the proper format (see below) +* Push your changes to the topic branch in your fork +* Create a pull request containing that commit + +### Pull Request Checklist +Before submitting your pull request, we advise you to use the following: + +1. Check if your code changes will pass both code linting checks and unit tests. +2. Ensure your commit messages are descriptive. Suggestions of information to include as appropriate: + 1. A short summary. + 2. Any related GitHub issue references in the commit message. See [GFM syntax](https://guides.github.com/features/mastering-markdown/#GitHub-flavored-markdown) for referencing issues and commits. + 3. Detailed description + 4. Specify any desired state spec changes. + 5. Product, Category, Component + 6. Compliance Scope (PCI, NIST, VCF Compliance kit, etc.) + 7. Testing performed +3. Check the commits and commits messages and ensure they are free from typos. +4. Code changes have appropriate unit tests. + 1. Code coverage is expected __to be > 80%__. + +## Reporting Bugs and Creating Issues + +For specifics on what to include in your report, please follow the guidelines in the issue and pull request templates when available. + +Issues should have Minimum Complete Verifiable Example (MCVE) that someone would need to be able to reproduce the error. This also means including versions of Python and dependencies, OS and any other relevant information. diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..630e003 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM photon:4.0-20240507 + +WORKDIR /usr/lib/config-modules + +COPY ./config_modules_vmware ./config_modules_vmware +COPY ./devops/scripts ./devops/scripts +COPY ./requirements ./requirements + +# Install Python and dependencies. +RUN tdnf install -y python3 python3-pip &&\ + python3 -m pip install -r requirements/api-requirements.txt + +EXPOSE 443 + +ENTRYPOINT ["./devops/scripts/start_api_server.sh"] diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..65d8030 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +recursive-include * *.txt +recursive-include * *.ini +recursive-include * *.yml +recursive-include * *.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b5982b --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# Config Modules + +Config-modules is a library that can be utilized by services written in python to run compliance checks (audit and remediation) for multiple products. + +## Why Config Modules + +1. **Consistent Interface**: Config-modules design helps achieve a consistent interface to invoke compliance workflows, no matter the differences in product level implementation. +2. **Simplified Implementation**: Using a schema based approach helps achieve a simplified controller implementation. Schema serves as the contract between the customer input and the controller logic. +3. **Future-proof integration**: The config module design enables products to catch up any missing APIs without disrupting existing upstream implementations. For example, VC Profile API is under development, we are working with respective product owners to close the gap in future versions. +4. **Adaptability**: The config module is highly reusable across various automation integrations, offering versatility in different deployment scenarios. +5. **Independent development and Testing**: Config module design enables controller owners to independently develop and test product specific drift and remediation. +6. **Seamless SaltStack Integration**: Leveraging the strengths of SaltStack, config-modules seamlessly integrates as a library for Salt extensions, enhancing exisitng workflowless. + +# Documentation Index + +| Document | +|-----------------------------------------------------------------------------------------------| +| 1. [Contributing and Getting Started](CONTRIBUTING.md) | +| 2. [Instructions to Create New Controllers](docs/instructions-to-create-new-controllers.md) | +| 3. [Testing Controllers](docs/testing-controllers.md) | +| 4. [Controller Documentation](docs/controllers/markdown/index.md) | +| 5. [Interface Consumption](docs/interface-consumption.md) | +| 6. [Functional Test](functional_tests/README.md) | +| 7. [API Service Documentation](docs/api-service.md) | +| 8. [Building and Running in Docker](docs/docker-instructions.md) | + +## Directory Structure + +``` +|--config_modules_vmware +| |--controllers <---- logic for controllers +| | |--vcenter +| | |--sddc_manager +| | |--.... +| |--framework <---- framework related classes +| | |--auth +| | | |--contexts <---- product specific context +| | |--clients <---- product specific client connections +| | |--models <---- model class folder +| | |--utils <---- utils +| |--interfaces <---- user interfaces and APIs to call. +| |--schemas <---- schemas and related utility functions +| |--services <---- service classes (mapper, operations, etc) +| |--tests <---- unit test folder +|--functional_tests <---- functional test folder +|--docs <---- useful documentation +|--devops <---- scripts for CI/CD +``` diff --git a/config_modules_vmware/__init__.py b/config_modules_vmware/__init__.py new file mode 100644 index 0000000..e747930 --- /dev/null +++ b/config_modules_vmware/__init__.py @@ -0,0 +1,6 @@ +# Copyright 2024 Broadcom. All Rights Reserved. + +version: str = "0.0.12" +name: str = "config_modules_vmware" +author: str = "Broadcom" +description: str = "VMware Unified Config Modules" diff --git a/config_modules_vmware/app.py b/config_modules_vmware/app.py new file mode 100644 index 0000000..428b32a --- /dev/null +++ b/config_modules_vmware/app.py @@ -0,0 +1,56 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging.config +import os + +import yaml +from fastapi import FastAPI +from fastapi.openapi.utils import get_openapi + +from config_modules_vmware.services.apis.controllers.consts import DOCS_ENDPOINT +from config_modules_vmware.services.apis.controllers.consts import REDOC_ENDPOINT +from config_modules_vmware.services.apis.controllers.misc import misc_router +from config_modules_vmware.services.apis.controllers.vcenter import vcenter_router +from config_modules_vmware.services.config import Config + + +def custom_openapi(): + """This creates the openapi specification file using FastAPI utilities.""" + if app.openapi_schema: + return app.openapi_schema + openapi_schema = get_openapi( + title="Config-modules APIs", + version="1.0.0", + summary="List of APIs exposed by Config-modules", + routes=app.routes, + ) + app.openapi_schema = openapi_schema + return app.openapi_schema + + +def setup_logging(): + """This sets up logging configuration that is applicable when run both through gunicorn and as FastAPI server.""" + log_config_path = os.path.join(os.path.dirname(__file__), "log_config.yml") + with open(log_config_path, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) + # Set file handler config using app config + logging_variables = Config.get_section("service.logging.file") + log_dir = os.environ.get("CONFIG_MODULES_DOCKER_LOG_DIR", logging_variables.get("LogFileDir")) + os.makedirs(log_dir, exist_ok=True) + config["handlers"]["file"]["filename"] = os.path.join(log_dir, logging_variables.get("FileName")) + config["handlers"]["file"]["maxBytes"] = logging_variables.getint("FileSize") + config["handlers"]["file"]["backupCount"] = logging_variables.getint("MaxCount") + config["handlers"]["file"]["level"] = logging_variables.get("LogLevel") + + # Set log level for default handler + logging_variables = Config.get_section("service.logging.console") + config["handlers"]["default"]["level"] = logging_variables.get("LogLevel") + + logging.config.dictConfig(config) + + +# Start the app and register routes +app = FastAPI(title="Config Modules", docs_url=DOCS_ENDPOINT, redoc_url=REDOC_ENDPOINT) +app.include_router(misc_router, tags=["misc"]) +app.include_router(vcenter_router, tags=["vcenter"]) +app.openapi = custom_openapi +setup_logging() diff --git a/config_modules_vmware/controllers/__init__.py b/config_modules_vmware/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_modules_vmware/controllers/base_controller.py b/config_modules_vmware/controllers/base_controller.py new file mode 100644 index 0000000..18b9872 --- /dev/null +++ b/config_modules_vmware/controllers/base_controller.py @@ -0,0 +1,136 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from abc import ABC +from abc import abstractmethod +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils.comparator import Comparator +from config_modules_vmware.framework.utils.comparator import ComparatorOptionForList + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class BaseController(ABC): + def __init__(self): + self.comparator_option = ComparatorOptionForList.COMPARE_AFTER_SORT + self.instance_key = "name" + + def __init_subclass__(cls, **kwargs): + if not hasattr(cls, "metadata"): + err_str = f'Can\'t instantiate controller class "{cls.__name__}". ' f'"metadata" attribute is not defined' + logger.error(f"Metadata not created for controller {cls.__name__}") + raise TypeError(err_str) + controller_metadata = getattr(cls, "metadata") + if not isinstance(controller_metadata, ControllerMetadata) or not controller_metadata.validate(): + err_str = ( + f'Can\'t instantiate controller class "{cls.__name__}". ' + f'"metadata" attribute does not have all required fields' + ) + logger.error(err_str) + raise TypeError(err_str) + return super().__init_subclass__(**kwargs) + + @abstractmethod + def get(self, context: BaseContext, template: dict = None) -> Tuple[Any, List[str]]: + pass + + @abstractmethod + def set(self, context: BaseContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + pass + + def check_compliance(self, context: BaseContext, desired_values: Any) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: BaseContext + :param desired_values: Desired values for the specified configuration. + :type desired_values: Any + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + current_value, errors = self.get(context=context) + + if errors: + if len(errors) == 1 and errors[0] == consts.SKIPPED: + return {consts.STATUS: ComplianceStatus.SKIPPED} + # If errors are seen during get, return "FAILED" status with errors. + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + # If no errors seen, compare the current and desired value. If not same, return "NON_COMPLIANT" with values. + # Otherwise, return "COMPLIANT". + current_non_compliant_configs, desired_non_compliant_configs = Comparator.get_non_compliant_configs( + current_value, desired_values, comparator_option=self.comparator_option, instance_key=self.instance_key + ) + if current_non_compliant_configs or desired_non_compliant_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_non_compliant_configs, + consts.DESIRED: desired_non_compliant_configs, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result + + def remediate(self, context: BaseContext, desired_values: Any) -> Dict: + """Remediate current configuration drifts. + + :param context: Product context instance. + :type context: BaseContext + :param desired_values: Desired values for the specified configuration. + :type desired_values: Any + :return: Dict of status and old/new values(for success) or errors (for failure). + :rtype: dict + """ + logger.debug("Running remediation.") + + # Call check compliance and check for current compliance status. + compliance_response = self.check_compliance(context=context, desired_values=desired_values) + + if ( + compliance_response.get(consts.STATUS) == ComplianceStatus.FAILED + or compliance_response.get(consts.STATUS) == ComplianceStatus.ERROR + ): + # For compliance_status as "FAILED" or "ERROR", return FAILED with errors. + return {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: compliance_response.get(consts.ERRORS, [])} + + elif compliance_response.get(consts.STATUS) == ComplianceStatus.COMPLIANT: + # For compliant case, return SUCCESS. + return {consts.STATUS: RemediateStatus.SUCCESS} + + elif compliance_response.get(consts.STATUS) == ComplianceStatus.SKIPPED: + return {consts.STATUS: RemediateStatus.SKIPPED} + + elif compliance_response.get(consts.STATUS) != ComplianceStatus.NON_COMPLIANT: + # Raise exception for unexpected compliance status (other than FAILED, COMPLIANT, NON_COMPLIANT). + raise Exception("Error during remediation. Unexpected compliant status found.") + + # Configs are non_compliant, call set to remediate them. + status, errors = self.set(context=context, desired_values=desired_values) + if not errors: + result = { + consts.STATUS: status, + consts.OLD: compliance_response.get(consts.CURRENT), + consts.NEW: compliance_response.get(consts.DESIRED), + } + else: + if status == RemediateStatus.SKIPPED: + # ADD SKIPPED RESPONSE + result = { + consts.STATUS: RemediateStatus.SKIPPED, + consts.ERRORS: errors, + consts.CURRENT: compliance_response.get(consts.CURRENT), + consts.DESIRED: compliance_response.get(consts.DESIRED), + } + else: + result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + return result diff --git a/config_modules_vmware/controllers/esxi/__init__.py b/config_modules_vmware/controllers/esxi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_modules_vmware/controllers/esxi/account_unlock_time_interval.py b/config_modules_vmware/controllers/esxi/account_unlock_time_interval.py new file mode 100644 index 0000000..5b1f6e5 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/account_unlock_time_interval.py @@ -0,0 +1,85 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "Security.AccountUnlockTime" + + +class AccountUnlockTimeInterval(BaseController): + """For ESXi, a user's account to be locked for time interval before account can be unlocked. + + | Config Id - 165 + | Config Title - The ESXi host must enforce an unlock timeout of certain defined minutes after a user account is locked out. + """ + + metadata = ControllerMetadata( + name="account_unlock_time_interval", # controller name + path_in_schema="compliance_config.esxi.account_unlock_time_interval", + # path in the schema to this controller's definition. + configuration_id="165", # configuration id as defined in compliance kit. + title="The ESXi host must enforce an unlock timeout of certain defined minutes after a user account is locked out", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[int, List[str]]: + """Get unlock time interval for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of an integer for the account unlock time interval and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting account unlock time interval configuration for esxi.") + errors = [] + account_unlock_interval_time = -1 + try: + # Fetch account unlock interval time. + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + account_unlock_interval_time = result[0].value + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors = [str(e)] + return account_unlock_interval_time, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set unlock time interval for esxi host + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value of account unlock time interval. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting account unlock time interval configuration for esxi.") + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=desired_values) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/bridge_protocol_data_unit_filter.py b/config_modules_vmware/controllers/esxi/bridge_protocol_data_unit_filter.py new file mode 100644 index 0000000..974f948 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/bridge_protocol_data_unit_filter.py @@ -0,0 +1,85 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "Net.BlockGuestBPDU" + + +class BridgeProtocolDataUnitFilter(BaseController): + """ESXi controller to get and set bridge protocol data unit filter. + + | Config Id - 43 + | Config Title - Enable the Bridge Protocol Data Unit (BPDU) filter. + """ + + metadata = ControllerMetadata( + name="bridge_protocol_data_unit_filter", # controller name + path_in_schema="compliance_config.esxi.bridge_protocol_data_unit_filter", + # path in the schema to this controller's definition. + configuration_id="43", # configuration id as defined in compliance kit. + title="Enable the Bridge Protocol Data Unit (BPDU) filter", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[int, List[str]]: + """Get bridge protocol data unit filter. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of int (1 to enable, 0 to disable) and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting BPDU filter value for esxi.") + errors = [] + is_bpdu_filter_enabled = -1 + try: + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + is_bpdu_filter_enabled = result[0].value + + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors = [str(e)] + return is_bpdu_filter_enabled, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set bridge protocol data unit filter + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: 1 to enable and 0 to disable BPDU filter. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting BPDU filter value for esxi.") + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=desired_values) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/cim_service_policy.py b/config_modules_vmware/controllers/esxi/cim_service_policy.py new file mode 100644 index 0000000..ef3f851 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/cim_service_policy.py @@ -0,0 +1,88 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils.service_utils import HostServiceUtil +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ESXI_SERVICE_CIM = "sfcbd-watchdog" +SERVICE_RUNNING = "service_running" +SERVICE_POLICY = "service_policy" + + +class CimServicePolicy(BaseController): + """ESXi controller to start/stop/update cim service. + + | Config Id - 1126 + | Config Title - The ESXi CIM service must be disabled. + + """ + + metadata = ControllerMetadata( + name="cim_service_policy", # controller name + path_in_schema="compliance_config.esxi.cim_service_policy", + # path in the schema to this controller's definition. + configuration_id="1126", # configuration id as defined in compliance kit. + title="The ESXi CIM service must be disabled.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[dict, List[str]]: + """Get cim service status for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of dict such as {"service_running": True, "service_policy": "off"} and a list of errors. + :rtype: Tuple + """ + logger.info("Getting cim service status for esxi.") + errors = [] + cim_service_status = {} + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + cim_service_status, errors = util.get_service_status(ESXI_SERVICE_CIM) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return cim_service_status, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Start/stop cim service for esxi host based on desired value. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: dict of { "service_running": True, "service_policy": "off"} to start/stop service or update policy. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting cim service policy for esxi") + errors = [] + status = RemediateStatus.SUCCESS + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + util.start_stop_service(ESXI_SERVICE_CIM, desired_values.get(SERVICE_RUNNING)) + util.update_service_policy(ESXI_SERVICE_CIM, desired_values.get(SERVICE_POLICY)) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/dcui_login_banner.py b/config_modules_vmware/controllers/esxi/dcui_login_banner.py new file mode 100644 index 0000000..8fb5f1e --- /dev/null +++ b/config_modules_vmware/controllers/esxi/dcui_login_banner.py @@ -0,0 +1,85 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "Annotations.WelcomeMessage" + + +class DcuiLoginBanner(BaseController): + """ESXi controller for DCUI login banner. + + | Config Id - 122 + | Config Title - Configure the login banner for the DCUI of the ESXi host. + """ + + metadata = ControllerMetadata( + name="dcui_login_banner", # controller name + path_in_schema="compliance_config.esxi.dcui_login_banner", + # path in the schema to this controller's definition. + configuration_id="122", # configuration id as defined in compliance kit. + title="Configure the login banner for the DCUI of the ESXi host", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[int, List[str]]: + """Get dcui login banner for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of dcui login banner string and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh login banner for esxi.") + errors = [] + login_banner = "" + try: + # Fetch dcui login banner. + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + login_banner = result[0].value + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return login_banner, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set dcui login banner for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value of dcui login banner. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting dcui login banner for esxi.") + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=desired_values) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/firewall_rulesets_config.py b/config_modules_vmware/controllers/esxi/firewall_rulesets_config.py new file mode 100644 index 0000000..4722dc7 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/firewall_rulesets_config.py @@ -0,0 +1,456 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from itertools import dropwhile +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils.comparator import ComparatorOptionForList + +ALLOW_ALL_IP_KEY = "allow_all_ip" +NAME_KEY = "name" +ENABLED_KEY = "enabled" +ALLOWED_IPS_KEY = "allowed_ips" +RULES_KEY = "rules" +PORT_KEY = "port" +DIRECTION_KEY = "direction" +PROTOCOL_KEY = "protocol" +END_PORT_KEY = "end_port" +ADDRESS_KEY = "address" +NETWORK_KEY = "network" +NETWORK_CIDR_FORMAT = "{}/{}" + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class FirewallRulesetsConfig(BaseController): + """ESXi Firewall Rulesets configuration. + + | Config Id - 28 + | Config Title - Configure the ESXi hosts firewall to only allow traffic from the authorized networks. + """ + + metadata = ControllerMetadata( + name="firewall_rulesets", # controller name + path_in_schema="compliance_config.esxi.firewall_rulesets", + # path in the schema to this controller's definition. + configuration_id="28", # configuration id as defined in compliance kit. + title="Configure the ESXi hosts firewall to only allow traffic from the authorized networks.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def __init__(self): + super().__init__() + self.comparator_option = ComparatorOptionForList.IDENTIFIER_BASED_COMPARISON + self.instance_key = NAME_KEY + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set firewall ruleset configs in ESXi. + + :param context: ESXHostContext product instance. + :type context: HostContext + :param desired_values: Desired values for rulesets. List of Dict. + :type desired_values: list + :return: Tuple of a status (from the RemediateStatus enum) and a list of errors encountered if any. + :rtype: tuple + """ + pass # pylint: disable=unnecessary-pass + + def remediate(self, context: HostContext, desired_values: Any) -> Dict: + """Remediate current rulesets configuration drifts. + + | Sample output + + .. code-block:: json + + { + "status": "SUCCESS", + "old": [ + { + "enabled": true, + "allow_all_ip": false, + "allowed_ips": { + "address": [ + "192.168.0.1" + ], + "network": [ + "192.168.121.0/8" + ] + }, + "name": "ruleset_name" + } + ], + "new": [ + { + "enabled": false, + "allow_all_ip": true, + "allowed_ips": { + "address": [ + "192.168.0.2" + ], + "network": [ + "192.168.121.0/8", + "192.168.0.0/16" + ] + }, + "name": "ruleset_name" + } + ] + } + + :param context: ESXContext product instance. + :type context: EsxContext + :param desired_values: Desired values for rulesets. List of Dict. + :type desired_values: list + :return: Dict of status and list of old/new values(for success) and/or errors (for failure and partial). + :rtype: dict + """ + logger.debug("Running remediation.") + + # validate for duplicate keys + self._validate_input(desired_values) + + compliance_response = self.check_compliance(context, desired_values) + if compliance_response.get(consts.STATUS) == ComplianceStatus.FAILED: + # For compliance_status as "FAILED", return FAILED with errors. + return {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: compliance_response.get(consts.ERRORS, [])} + + elif compliance_response.get(consts.STATUS) == ComplianceStatus.COMPLIANT: + # For compliant case, return SUCCESS. + return {consts.STATUS: RemediateStatus.SUCCESS} + else: + # Check for non-compliant items and iterate through each of drifts and invoke remediation. + # Scenario 1. Handle addition/removal of ruleset in desired config + # Scenario 2: Handle drifts in ruleset. + desired_non_compliant_configs = compliance_response.get(consts.DESIRED) + current_non_compliant_configs = compliance_response.get(consts.CURRENT) + logger.debug( + f"check compliance response. " + f"non_compliant_configs_desired = {desired_non_compliant_configs}" + f"non_compliant_configs_current = {current_non_compliant_configs}" + ) + old_values = [] + new_values = [] + errors = [] + desired_iter, current_iter = iter(desired_non_compliant_configs), iter(current_non_compliant_configs) + for non_compliant_desired_config in desired_iter: + non_compliant_current_config = next(current_iter) + + # Scenario 1. Handle addition/removal of ruleset in desired config + if non_compliant_desired_config is None or non_compliant_current_config is None: + # Ruleset is added/removed in desired config. + # ESXi don't support creation or deletion of rulesets. + ruleset_name = ( + non_compliant_desired_config.get(NAME_KEY) + if non_compliant_desired_config is not None + else non_compliant_current_config.get(NAME_KEY) + ) + if non_compliant_current_config is not None: + errors.append( + f"Manual intervention required. Ruleset [{ruleset_name}] exists in host " + f"but not defined in desired input spec. spec={non_compliant_current_config}" + ) + else: + errors.append( + f"Manual intervention required. Ruleset [{ruleset_name}] not found in host. " + f"spec={non_compliant_desired_config}" + ) + else: + # Scenario 2: Handle drifts in ruleset. + # Exclude name(unique identifier) as it is identical in current and desired. + # ruleset_name = current_config.pop(NAME_KEY) + # desired_config.pop(NAME_KEY) + + self._set_ruleset_config( + context, + non_compliant_desired_config, + non_compliant_current_config, + desired_values, + new_values, + old_values, + errors, + ) + + # create remediation output. + # set status to success if all remediation passed for all rulesets. + # set status to failed if all remediation failed for all rulesets. + # set status to partial if remediation succeeded for few rulesets and failed for few. + remediation_result = {} + if len(new_values) > 0: + remediation_result[consts.NEW] = new_values + remediation_result[consts.OLD] = old_values + remediation_result[consts.STATUS] = RemediateStatus.SUCCESS + if len(errors) > 0: + remediation_result[consts.ERRORS] = errors + remediation_result[consts.STATUS] = RemediateStatus.FAILED + if len(new_values) > 0: + remediation_result[consts.STATUS] = RemediateStatus.PARTIAL + return remediation_result + + def _set_ruleset_config( + self, + context, + non_compliant_desired_config, + non_compliant_current_config, + desired_values, + new_values, + old_values, + errors, + ): + new = {} + old = {} + firewall_config = context.host_ref.configManager.firewallSystem + ruleset_name = non_compliant_desired_config.get(NAME_KEY) + allow_all_ip, allowed_ips = None, None + # desired_config dict contains only non-compliant config. This operation requires other keys that are + # compliant as well. So get full desired config from input desired_values. + desired_config_full = next(filter(lambda config: config[NAME_KEY] == ruleset_name, desired_values)) + + # ESXi vapi allow changes to + # 1. Enable/Disable ruleset + # 2. Enable/Disable allow_all_ip flag + # 3. Make changes to list of IP and Networks allowed + # Check for allowed keys present in desired_config and invoke vim API to remediate drift. + # For set not allowed keys in desired_config, add them to error. + + for config_key in non_compliant_desired_config.keys(): + if config_key == NAME_KEY: + continue + if config_key == ENABLED_KEY: + # Handle Enable/Disable Ruleset configuration. + config = non_compliant_desired_config.get(ENABLED_KEY) + self._toggle_ruleset( + firewall_config, ruleset_name, config, new, old, errors, non_compliant_current_config + ) + elif config_key == ALLOW_ALL_IP_KEY or config_key == ALLOWED_IPS_KEY: + # create tuples with drift and full desired configs. + allow_all_ip = ( + desired_config_full.get(ALLOW_ALL_IP_KEY), + non_compliant_desired_config.get(ALLOW_ALL_IP_KEY), + ) + allowed_ips = ( + desired_config_full.get(ALLOWED_IPS_KEY), + non_compliant_desired_config.get(ALLOWED_IPS_KEY), + ) + else: + # Add unsupported config keys to errors. + errors.append( + f"Manual intervention required for ruleset [{ruleset_name}]. " + f"Remediation not supported for configuration [{config_key}]." + f"{non_compliant_desired_config.get(config_key)}" + ) + # Handle allow_all_ip flag and allowed_ips configurations. + if allow_all_ip or allowed_ips: + self._set_allowed_ips( + allow_all_ip, + allowed_ips, + firewall_config, + ruleset_name, + old, + new, + errors, + non_compliant_current_config, + ) + # Set ruleset name to dict to uniquely identify result. + if len(new) > 0: + new[NAME_KEY] = ruleset_name + old[NAME_KEY] = ruleset_name + new_values.append(new) + old_values.append(old) + + def _toggle_ruleset(self, firewall_config, name, config, new, old, errors, current_config): + try: + if config: + firewall_config.EnableRuleset(id=name) + else: + firewall_config.DisableRuleset(id=name) + new[ENABLED_KEY] = config + old[ENABLED_KEY] = current_config.get(ENABLED_KEY) + except Exception as e: + logger.error(f"Ruleset {name}.Exception in remediating {ENABLED_KEY}={config}.{e}") + errors.append(f"Exception remediating ruleset [{name}], {str(e)}") + + def _set_allowed_ips( + self, allow_all_ip, allowed_ips, firewall_config, ruleset_name, old, new, errors, current_config + ): + # firewall_config.UpdateRuleset() API overwrites entire object whenever update is made to allowedHosts. + # So any update to firewall_rulespec.allowedHosts requires all 3 attributes + # (allIp, ipNetwork, ipAddress) to be set even when drift is found in one of them. + try: + firewall_rulespec = vim.host.Ruleset.RulesetSpec() + firewall_rulespec.allowedHosts = vim.host.Ruleset.IpList() + + firewall_rulespec.allowedHosts.allIp = allow_all_ip[0] + firewall_rulespec.allowedHosts.ipAddress = allowed_ips[0].get(ADDRESS_KEY, []) + + ip_networks = [ + self._create_ip_network_spec(ip_network) for ip_network in allowed_ips[0].get(NETWORK_KEY, []) + ] + firewall_rulespec.allowedHosts.ipNetwork = ip_networks + logger.info( + f"Remediate ruleset {ruleset_name}. {ALLOW_ALL_IP_KEY}={allow_all_ip[0]}. {ALLOWED_IPS_KEY}={allowed_ips[0]}" + ) + firewall_config.UpdateRuleset(id=ruleset_name, spec=firewall_rulespec) + # Check if allow_all_ip is changed + if allow_all_ip[1] is not None: + new[ALLOW_ALL_IP_KEY] = allow_all_ip[1] + old[ALLOW_ALL_IP_KEY] = current_config.get(ALLOW_ALL_IP_KEY) + # Check if allowed_ips is changed + if allowed_ips[1] is not None: + new[ALLOWED_IPS_KEY] = {} + old[ALLOWED_IPS_KEY] = {} + # set to new values if key is found to be non-compliant. + # desired_config dict contains non-compliant keys only. + if ADDRESS_KEY in allowed_ips[1]: + new[ALLOWED_IPS_KEY][ADDRESS_KEY] = allowed_ips[1][ADDRESS_KEY] + old[ALLOWED_IPS_KEY][ADDRESS_KEY] = current_config.get(ALLOWED_IPS_KEY)[ADDRESS_KEY] + if NETWORK_KEY in allowed_ips[1]: + new[ALLOWED_IPS_KEY][NETWORK_KEY] = allowed_ips[1][NETWORK_KEY] + old[ALLOWED_IPS_KEY][NETWORK_KEY] = current_config.get(ALLOWED_IPS_KEY)[NETWORK_KEY] + except Exception as e: + logger.error(f"Ruleset {ruleset_name}.Exception in remediation {e}") + errors.append(f"Exception remediating ruleset [{ruleset_name}], {str(e)}") + + def get(self, context: HostContext) -> Tuple[List[dict], List[str]]: + """Get Firewall rulesets configured in ESXi. + + | Sample output + + .. code-block:: json + + [ + { + "allow_all_ip": false, + "name": "test_ruleset", + "enabled": true, + "allowed_ips": { + "address": [ + "192.168.0.1" + ], + "network": [ + "192.168.121.0/8" + ] + }, + "rules": [ + { + "port": 8080, + "direction": "inbound", + "protocol": "tcp", + "end_port": 9090 + } + ] + } + ] + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of list of Dict containing rulesets and list of error messages. + :rtype: Tuple + """ + logger.info("Getting firewall rulesets for audit.") + errors = None + try: + # Fetch all firewall rulesets. Any failure will throw an exception. + rulesets_configured = context.host_ref.configManager.firewallSystem.firewallInfo.ruleset + + # Process each ruleset config and create ruleset json. + rulesets = [self._to_ruleset_dict(ruleset_config) for ruleset_config in rulesets_configured] + except Exception as e: + logger.exception(f"Exception retrieving firewall ruleset configuration - {e}") + errors = [str(e)] + rulesets = [] + return rulesets, errors + + def check_compliance(self, context: HostContext, desired_values: Any) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: ESX context instance. + :type context: HostContext + :param desired_values: Desired values for rulesets. + :type desired_values: Any + :return: Dict of status and list of current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + # Add custom validation on input config. + FirewallRulesetsConfig._validate_input(self, desired_values) + return super().check_compliance(context, desired_values) + + def _find_first_non_unique_value(self, values): + # Predicate function to validate for non-unique values + unique_values = set() + predicate = lambda value: value[NAME_KEY] not in unique_values and unique_values.add(value[NAME_KEY]) is None + # Iterator to check and return first non-unique value in list + return next(dropwhile(predicate, values), None) + + def _validate_input(self, desired_values): + # Validate if any rule sets are using same name. + # Raise an exception when first such occurrence is found. + non_unique_value = self._find_first_non_unique_value(desired_values) + if non_unique_value: + raise ValueError(f"Found duplicate entries for ruleset. {NAME_KEY}={non_unique_value[NAME_KEY]}") + + def _to_ruleset_dict(self, ruleset_config): + # Create rule set + ruleset = { + ALLOW_ALL_IP_KEY: ruleset_config.allowedHosts.allIp, + NAME_KEY: ruleset_config.key, + ENABLED_KEY: ruleset_config.enabled, + } + + # Get allowed ip addresses and networks + allowed_ips = self._to_allowed_ips_dict(ruleset_config.allowedHosts) + ruleset[ALLOWED_IPS_KEY] = allowed_ips + + # Get all firewall rules + ruleset[RULES_KEY] = [self._to_rule_dict(rule_config) for rule_config in ruleset_config.rule] + + return ruleset + + def _to_rule_dict(self, rule_config): + rule = { + PORT_KEY: rule_config.port, + DIRECTION_KEY: rule_config.direction, + PROTOCOL_KEY: rule_config.protocol, + } + if rule_config.endPort is not None: + rule[END_PORT_KEY] = rule_config.endPort + return rule + + def _to_allowed_ips_dict(self, allowed_hosts_config): + allowed_ips = { + ADDRESS_KEY: list(allowed_hosts_config.ipAddress), + # Network format should follow standard networking CIDR. + NETWORK_KEY: [ + self._to_network_cidr_format(network_config) for network_config in list(allowed_hosts_config.ipNetwork) + ], + } + return allowed_ips + + def _to_network_cidr_format(self, network_config): + return NETWORK_CIDR_FORMAT.format(network_config.network, network_config.prefixLength) + + def _create_ip_network_spec(self, ip_network): + ip, prefix = ip_network.split("/", 1) + ip_network_spec = vim.host.Ruleset.IpNetwork() + ip_network_spec.network = ip + ip_network_spec.prefixLength = int(prefix) + return ip_network_spec diff --git a/config_modules_vmware/controllers/esxi/hyperthread_warning_policy.py b/config_modules_vmware/controllers/esxi/hyperthread_warning_policy.py new file mode 100644 index 0000000..3b3db48 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/hyperthread_warning_policy.py @@ -0,0 +1,85 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "UserVars.SuppressHyperthreadWarning" + + +class HyperthreadWarningPolicy(BaseController): + """ESXi controller to enable/disable suppressing of hyperthread warning. + + | Config Id - 1110 + | Config Title - The ESXi host must not suppress warning about unmitigated hyperthreading vulnerabilities. + + """ + + metadata = ControllerMetadata( + name="suppress_hyperthread_warning", # controller name + path_in_schema="compliance_config.esxi.suppress_hyperthread_warning", + # path in the schema to this controller's definition. + configuration_id="1110", # configuration id as defined in compliance kit. + title="The ESXi host must not suppress warning about unmitigated hyperthreading vulnerabilities", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[int, List[str]]: + """Get suppress hyperthread warning settings for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of int (1 to enable suppress and 0 to disable suppress) and a list of error messages + :rtype: Tuple + """ + logger.info("Getting suppress hyperthread warning policy for esxi.") + errors = [] + suppress_hyperthread_warning = -1 + try: + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + suppress_hyperthread_warning = result[0].value + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors = [str(e)] + return suppress_hyperthread_warning, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set suppress hyperthread warning settings for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: 1 to enable suppress and 0 to disable suppress hyperthread warning. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting suppress hyperthread warning policy for esxi") + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=desired_values) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/managed_object_browser.py b/config_modules_vmware/controllers/esxi/managed_object_browser.py new file mode 100644 index 0000000..8d86f0d --- /dev/null +++ b/config_modules_vmware/controllers/esxi/managed_object_browser.py @@ -0,0 +1,85 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "Config.HostAgent.plugins.solo.enableMob" + + +class ManagedObjectBrowser(BaseController): + """ESXi controller class to configure managed object browser settings. + + | Config Id - 166 + | Config Title - The ESXi host must disable the Managed Object Browser (MOB). + """ + + metadata = ControllerMetadata( + name="managed_object_browser", # controller name + path_in_schema="compliance_config.esxi.managed_object_browser", + # path in the schema to this controller's definition. + configuration_id="166", # configuration id as defined in compliance kit. + title="The ESXi host must disable the Managed Object Browser (MOB).", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[int, List[str]]: + """Get managed object browser setting for ESXi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of boolean for the managed object browser flag and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting managed object browser setting for esxi.") + errors = [] + mob_flag = False + try: + # Fetch password max lifetime. + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + mob_flag = result[0].value + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return mob_flag, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set managed object browser setting for ESXi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value of managed object browser flag + :type desired_values: boolean + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting managed object browser setting for esxi.") + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=desired_values) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/max_failed_login_attempts.py b/config_modules_vmware/controllers/esxi/max_failed_login_attempts.py new file mode 100644 index 0000000..78fca85 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/max_failed_login_attempts.py @@ -0,0 +1,85 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "Security.AccountLockFailures" + + +class MaxFailedLoginAttempts(BaseController): + """For ESXi, max failed login attempts before account is locked. + + | Config Id - 34 + | Config Title - Set the maximum number of failed login attempts before an account is locked. + """ + + metadata = ControllerMetadata( + name="max_failed_login_attempts", # controller name + path_in_schema="compliance_config.esxi.max_failed_login_attempts", + # path in the schema to this controller's definition. + configuration_id="34", # configuration id as defined in compliance kit. + title="Set the maximum number of failed login attempts before an account is locked", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[int, List[str]]: + """Get max failed login attempts for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of an integer for the max failed login attempts and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting max failed login attempts configuration for esxi.") + errors = [] + max_failed_login_attempts = -1 + try: + # Fetch max failed login attempts. + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + max_failed_login_attempts = result[0].value + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors = [str(e)] + return max_failed_login_attempts, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set max failed login attempts for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value of max failed login attempts. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting max failed login attempts configuration for esxi.") + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=desired_values) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/ntp_service_config.py b/config_modules_vmware/controllers/esxi/ntp_service_config.py new file mode 100644 index 0000000..1f6ec76 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ntp_service_config.py @@ -0,0 +1,88 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils.service_utils import HostServiceUtil +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ESXI_SERVICE_NTP = "ntpd" +SERVICE_RUNNING = "service_running" + + +class NtpServiceConfig(BaseController): + """ESXi controller to start/stop ntp service.. + + | Config Id - 149 + | Config Title - Start NTP service on the ESXi host. + + """ + + metadata = ControllerMetadata( + name="ntp_service_config", # controller name + path_in_schema="compliance_config.esxi.ntp_service_config", + # path in the schema to this controller's definition. + configuration_id="149", # configuration id as defined in compliance kit. + title="Start NTP service on the ESXi host.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[dict, List[str]]: + """Get ntp service running status for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of dict ({"service_running": True}) and a list of errors. + :rtype: Tuple + """ + logger.info("Getting ntp service running status for esxi.") + ntp_service_running_status = {} + errors = [] + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + ntp_service_status, errors = util.get_service_status(ESXI_SERVICE_NTP) + if not errors: + ntp_service_running_status = {SERVICE_RUNNING: ntp_service_status.get(SERVICE_RUNNING)} + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return ntp_service_running_status, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Start/stop ntp service for esxi host based on desired value. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: dict of { "service_running": True" } to start/stop service. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ntp service running configiuration for esxi") + errors = [] + status = RemediateStatus.SUCCESS + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + util.start_stop_service(ESXI_SERVICE_NTP, desired_values.get(SERVICE_RUNNING)) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/ntp_service_startup_policy.py b/config_modules_vmware/controllers/esxi/ntp_service_startup_policy.py new file mode 100644 index 0000000..2c77812 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ntp_service_startup_policy.py @@ -0,0 +1,88 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils.service_utils import HostServiceUtil +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ESXI_SERVICE_NTP = "ntpd" +SERVICE_POLICY = "service_policy" + + +class NtpServiceStartupPolicy(BaseController): + """ESXi controller to get/update ntp startup policy. + + | Config Id - 148 + | Config Title - The ESXi host must configure NTP Service startup policy. + + """ + + metadata = ControllerMetadata( + name="ntp_service_startup_policy", # controller name + path_in_schema="compliance_config.esxi.ntp_service_startup_policy", + # path in the schema to this controller's definition. + configuration_id="148", # configuration id as defined in compliance kit. + title="The ESXi host must configure NTP Service startup policy.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[dict, List[str]]: + """Get ntp service startup policy status for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of dict ({"startup_policy": "on"}) and list of errors. + :rtype: Tuple + """ + logger.info("Getting ntp service startup policy status for esxi.") + ntp_startup_policy_status = {} + errors = [] + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + ntp_service_status, errors = util.get_service_status(ESXI_SERVICE_NTP) + if not errors: + ntp_startup_policy_status = {SERVICE_POLICY: ntp_service_status.get(SERVICE_POLICY)} + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return ntp_startup_policy_status, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Update ntp startup policy for esxi host based on desired value. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: dict of { "service_policy": "off"} to update policy. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ntp service startup policy for esxi") + errors = [] + status = RemediateStatus.SUCCESS + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + util.update_service_policy(ESXI_SERVICE_NTP, desired_values.get(SERVICE_POLICY)) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/password_max_lifetime_policy.py b/config_modules_vmware/controllers/esxi/password_max_lifetime_policy.py new file mode 100644 index 0000000..9db10e4 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/password_max_lifetime_policy.py @@ -0,0 +1,85 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "Security.PasswordMaxDays" + + +class PasswordMaxLifetimePolicy(BaseController): + """ESXi Password max lifetime configuration. + + | Config Id - 1123 + | Config Title - The ESXi host must be configured with an appropriate maximum password age. + """ + + metadata = ControllerMetadata( + name="password_max_lifetime", # controller name + path_in_schema="compliance_config.esxi.password_max_lifetime", + # path in the schema to this controller's definition. + configuration_id="1123", # configuration id as defined in compliance kit. + title="The ESXi host must be configured with an appropriate maximum password age.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[int, List[str]]: + """Get max password lifetime policy for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of an integer for the max password lifetime in days and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting password lifetime policy for esxi.") + errors = [] + max_lifetime_days = -1 + try: + # Fetch password max lifetime. + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + max_lifetime_days = result[0].value + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors = [str(e)] + return max_lifetime_days, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set max password lifetime policy for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value of max password lifetime in days. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting password lifetime policy for esxi.") + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=desired_values) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/password_reuse_restriction_policy.py b/config_modules_vmware/controllers/esxi/password_reuse_restriction_policy.py new file mode 100644 index 0000000..015955a --- /dev/null +++ b/config_modules_vmware/controllers/esxi/password_reuse_restriction_policy.py @@ -0,0 +1,85 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "Security.PasswordHistory" + + +class PasswordReuseRestrictionPolicy(BaseController): + """ESXi Password history setting configuration for restricting password reuse. + + | Config Id - 109 + | Config Title - Configure the password history setting to restrict the reuse of passwords. + """ + + metadata = ControllerMetadata( + name="password_reuse_restriction", # controller name + path_in_schema="compliance_config.esxi.password_reuse_restriction", + # path in the schema to this controller's definition. + configuration_id="109", # configuration id as defined in compliance kit. + title="Configure the password history setting to restrict the reuse of passwords", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[int, List[str]]: + """Get password history setting for esxi host to restrict the reuse of passwords. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of an integer for the number of previous passwords restricted and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting password reuse restriction policy for esxi.") + errors = [] + password_history_reuse_count = -1 + try: + # Fetch password history settings. + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + password_history_reuse_count = result[0].value + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors = [str(e)] + return password_history_reuse_count, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set password history setting for esxi host to restrict the reuse of passwords. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value of number of previous passwords restricted. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting password reuse restriction policy for esxi.") + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=desired_values) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/shell_service_policy.py b/config_modules_vmware/controllers/esxi/shell_service_policy.py new file mode 100644 index 0000000..1649e9a --- /dev/null +++ b/config_modules_vmware/controllers/esxi/shell_service_policy.py @@ -0,0 +1,88 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils.service_utils import HostServiceUtil +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ESXI_SERVICE_SHELL = "TSM" +SERVICE_RUNNING = "service_running" +SERVICE_POLICY = "service_policy" + + +class ShellServicePolicy(BaseController): + """ESXi controller to start/stop/update shell service. + + | Config Id - 112 + | Config Title - Stop the ESXi shell service and set the startup policy. + + """ + + metadata = ControllerMetadata( + name="shell_service_policy", # controller name + path_in_schema="compliance_config.esxi.shell_service_policy", + # path in the schema to this controller's definition. + configuration_id="112", # configuration id as defined in compliance kit. + title="Stop the ESXi shell service and set the startup policy.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[dict, List[str]]: + """Get shell service status for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of dict such as {"service_running": True, "service_policy": "off"} and list of errors. + :rtype: Tuple + """ + logger.info("Getting shell service status for esxi.") + errors = [] + shell_service_status = {} + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + shell_service_status, errors = util.get_service_status(ESXI_SERVICE_SHELL) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return shell_service_status, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Start/stop shell service for esxi host based on desired value. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: dict of { "service_running": True, "service_policy": "off"} to start/stop service or update policy. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting shell service policy for esxi") + errors = [] + status = RemediateStatus.SUCCESS + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + util.start_stop_service(ESXI_SERVICE_SHELL, desired_values.get(SERVICE_RUNNING)) + util.update_service_policy(ESXI_SERVICE_SHELL, desired_values.get(SERVICE_POLICY)) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/slp_service_policy.py b/config_modules_vmware/controllers/esxi/slp_service_policy.py new file mode 100644 index 0000000..a4f9b3d --- /dev/null +++ b/config_modules_vmware/controllers/esxi/slp_service_policy.py @@ -0,0 +1,87 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils.service_utils import HostServiceUtil +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ESXI_SERVICE_SLP = "slpd" +SERVICE_RUNNING = "service_running" +SERVICE_POLICY = "service_policy" + + +class SlpServicePolicy(BaseController): + """ESXi controller to start/stop slp service.. + + | Config Id - 1112 + | Config Title - Disable the OpenSLP service on the host. + + """ + + metadata = ControllerMetadata( + name="slp_service_policy", # controller name + path_in_schema="compliance_config.esxi.slp_service_policy", + # path in the schema to this controller's definition. + configuration_id="1112", # configuration id as defined in compliance kit. + title="Disable the OpenSLP service on the host.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[dict, List[str]]: + """Get slp service status for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of dict such as {"service_running": True, "service_policy": "off"} and list of errors + :rtype: Tuple + """ + logger.info("Getting slp service status for esxi.") + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + slp_service_status, errors = util.get_service_status(ESXI_SERVICE_SLP) + except Exception as e: + logger.exception(f"An error occurred: {e}") + slp_service_status = {} + errors = [str(e)] + return slp_service_status, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Start/stop slp service for esxi host based on desired value. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: dict of { "service_running": True, "service_policy": "off"} to start/stop service or update policy. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting slp service policy for esxi") + errors = [] + status = RemediateStatus.SUCCESS + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + util.start_stop_service(ESXI_SERVICE_SLP, desired_values.get(SERVICE_RUNNING)) + util.update_service_policy(ESXI_SERVICE_SLP, desired_values.get(SERVICE_POLICY)) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/snmp_service_policy.py b/config_modules_vmware/controllers/esxi/snmp_service_policy.py new file mode 100644 index 0000000..7dec42b --- /dev/null +++ b/config_modules_vmware/controllers/esxi/snmp_service_policy.py @@ -0,0 +1,88 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils.service_utils import HostServiceUtil +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ESXI_SERVICE_SNMP = "snmpd" +SERVICE_RUNNING = "service_running" +SERVICE_POLICY = "service_policy" + + +class SnmpServicePolicy(BaseController): + """ESXi controller to start/stop snmp service.. + + | Config Id - 1128 + | Config Title - Configure or disable SNMP. + + """ + + metadata = ControllerMetadata( + name="snmp_service_policy", # controller name + path_in_schema="compliance_config.esxi.snmp_service_policy", + # path in the schema to this controller's definition. + configuration_id="1128", # configuration id as defined in compliance kit. + title="Configure or disable SNMP", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[dict, List[str]]: + """Get snmp service status for esxi host. + + :param context: ESXi context instance. + :type context: HostContext + :return: Tuple of dict such as {"service_running": True, "service_policy": "off"} and a list of errors. + :rtype: Tuple + """ + logger.info("Getting snmp service status for esxi.") + errors = [] + snmp_service_status = {} + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + snmp_service_status, errors = util.get_service_status(ESXI_SERVICE_SNMP) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return snmp_service_status, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Start/stop snmp service for esxi host based on desired value. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: dict of { "service_running": True, "service_policy": "off"} to start/stop service or update policy. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting snmp service policy for esxi") + errors = [] + status = RemediateStatus.SUCCESS + try: + host_service = context.host_ref.configManager.serviceSystem + util = HostServiceUtil(host_service) + util.start_stop_service(ESXI_SERVICE_SNMP, desired_values.get(SERVICE_RUNNING)) + util.update_service_policy(ESXI_SERVICE_SNMP, desired_values.get(SERVICE_POLICY)) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/ssh_ignore_rhosts_policy.py b/config_modules_vmware/controllers/esxi/ssh_ignore_rhosts_policy.py new file mode 100644 index 0000000..c9e3041 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_ignore_rhosts_policy.py @@ -0,0 +1,102 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_ssh_config_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CONFIG_KEY = "ignorerhosts" + + +class SshIgnoreRHostsPolicy(BaseController): + """ESXi ignore ssh rhosts configuration. + + | Config Id - 3 + | Config Title - The ESXi host Secure Shell (SSH) daemon must ignore .rhosts files. + """ + + metadata = ControllerMetadata( + name="ssh_ignore_rhosts", # controller name + path_in_schema="compliance_config.esxi.ssh_ignore_rhosts", + # path in the schema to this controller's definition. + configuration_id="3", # configuration id as defined in compliance kit. + title="The ESXi host Secure Shell (SSH) daemon must ignore .rhosts files.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[str, List[str]]: + """Get ssh ignore rhosts policy for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of str for 'IgnoreRhosts' value and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh ignore rhosts policy for esxi.") + major_version = utils.get_product_major_version(context.product_version) + if major_version and major_version >= 8: + return self._get_v8(context) + else: + return self._get_skipped() + + def _get_skipped(self) -> Tuple[str, List[str]]: + return "", [consts.SKIPPED] + + def _get_v8(self, context: HostContext) -> Tuple[str, List[str]]: + errors = [] + ssh_ignore_rhosts = "" + try: + ssh_ignore_rhosts = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return ssh_ignore_rhosts, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh ignore rhosts policy for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value for 'IgnoreRhosts' property. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh ignore rhosts policy for esxi.") + major_version = utils.get_product_major_version(context.product_version) + if major_version and major_version >= 8: + return self._set_v8(context, desired_values) + else: + return self._set_skipped() + + def _set_skipped(self) -> Tuple[RemediateStatus, List[str]]: + return RemediateStatus.SKIPPED, [] + + def _set_v8(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_ssh_config_utils.set_ssh_config_value(context, CONFIG_KEY, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/ssh_login_banner.py b/config_modules_vmware/controllers/esxi/ssh_login_banner.py new file mode 100644 index 0000000..63d2076 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_login_banner.py @@ -0,0 +1,85 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "Config.Etc.issue" + + +class SshLoginBanner(BaseController): + """ESXi controller for SSH login banner. + + | Config Id - 123 + | Config Title - Configure the login banner for the SSH connections. + """ + + metadata = ControllerMetadata( + name="ssh_login_banner", # controller name + path_in_schema="compliance_config.esxi.ssh_login_banner", + # path in the schema to this controller's definition. + configuration_id="123", # configuration id as defined in compliance kit. + title="Configure the login banner for the SSH connections", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[int, List[str]]: + """Get ssh login banner for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of ssh login banner string and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh login banner for esxi.") + errors = [] + login_banner = "" + try: + # Fetch ssh login banner. + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + login_banner = result[0].value + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return login_banner, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh login banner for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value of ssh login banner. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh login banner for esxi") + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=desired_values) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/ssh_port_forwarding_policy.py b/config_modules_vmware/controllers/esxi/ssh_port_forwarding_policy.py new file mode 100644 index 0000000..bf8c460 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/ssh_port_forwarding_policy.py @@ -0,0 +1,102 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_ssh_config_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CONFIG_KEY = "allowtcpforwarding" + + +class SshPortForwardingPolicy(BaseController): + """ESXi ssh port forwarding configuration. + + | Config Id - 1111 + | Config Title - The ESXi host Secure Shell (SSH) daemon must disable port forwarding. + """ + + metadata = ControllerMetadata( + name="ssh_port_forwarding", # controller name + path_in_schema="compliance_config.esxi.ssh_port_forwarding", + # path in the schema to this controller's definition. + configuration_id="1111", # configuration id as defined in compliance kit. + title="The ESXi host Secure Shell (SSH) daemon must disable port forwarding.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[str, List[str]]: + """Get ssh port forwarding policy for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of str for 'AllowTcpForwarding' value and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting ssh port forwarding policy for esxi.") + major_version = utils.get_product_major_version(context.product_version) + if major_version and major_version >= 8: + return self._get_v8(context) + else: + return self._get_skipped() + + def _get_skipped(self) -> Tuple[str, List[str]]: + return "", [consts.SKIPPED] + + def _get_v8(self, context: HostContext) -> Tuple[str, List[str]]: + errors = [] + ssh_port_forwarding_enabled = "" + try: + ssh_port_forwarding_enabled = esxi_ssh_config_utils.get_ssh_config_value(context, CONFIG_KEY) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return ssh_port_forwarding_enabled, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set ssh port forwarding policy for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value for 'AllowTcpForwarding' property. + :type desired_values: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting ssh port forwarding policy for esxi.") + major_version = utils.get_product_major_version(context.product_version) + if major_version and major_version >= 8: + return self._set_v8(context, desired_values) + else: + return self._set_skipped() + + def _set_skipped(self) -> Tuple[RemediateStatus, List[str]]: + return RemediateStatus.SKIPPED, [] + + def _set_v8(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_ssh_config_utils.set_ssh_config_value(context, CONFIG_KEY, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/tls_version.py b/config_modules_vmware/controllers/esxi/tls_version.py new file mode 100644 index 0000000..3d2639a --- /dev/null +++ b/config_modules_vmware/controllers/esxi/tls_version.py @@ -0,0 +1,98 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.esxi.utils import esxi_advanced_settings_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SETTINGS_NAME = "UserVars.ESXiVPsDisabledProtocols" + +SUPPORTED_PROTOCOLS_SET = {"sslv3", "tlsv1", "tlsv1.1", "tlsv1.2"} + + +class TlsVersion(BaseController): + """ESXi controller class to get/set/check compliance/remediate tls protocols on esxi hosts. + + | Config Id - 1107 + | Config Title - The ESXi host must exclusively enable TLS 1.2 for all endpoints. + """ + + metadata = ControllerMetadata( + name="tls_version", # controller name + path_in_schema="compliance_config.esxi.tls_version", + # path in the schema to this controller's definition. + configuration_id="1107", # configuration id as defined in compliance kit. + title="The ESXi host must exclusively enable TLS 1.2 for all endpoints", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.ESXI], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: HostContext) -> Tuple[int, List[str]]: + """Get tls protocols enabled for esxi host. + + :param context: ESX context instance. + :type context: HostContext + :return: Tuple of list of enabled tls/ssl protocols and a list of error messages. + :rtype: Tuple + """ + logger.info("Getting tls versions enabled for esxi.") + errors = [] + enabled_protocols = [] + try: + # Fetch disabled tls protocols and create a list of enabled protocols + result = esxi_advanced_settings_utils.invoke_advanced_option_query(context.host_ref, prefix=SETTINGS_NAME) + disabled_protocols_str = result[0].value + logger.debug(f"Getting {SETTINGS_NAME} value: {disabled_protocols_str}") + + # Product allows duplicates values in the input disabled protocols string like "tlsv1,tlsv1" + # So, using set to remove the duplicates. Create sorted list of enabled protocols + disabled_protocols = {protocol.strip() for protocol in disabled_protocols_str.split(",")} + enabled_protocols = sorted(list(SUPPORTED_PROTOCOLS_SET - disabled_protocols)) + logger.debug(f"List of TLS/SSL protocols enabled: {enabled_protocols}") + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return enabled_protocols, errors + + def set(self, context: HostContext, desired_values) -> Tuple[RemediateStatus, List[str]]: + """Set tls protocols enabled for esxi host. + + :param context: Esxi context instance. + :type context: HostContext + :param desired_values: Desired value of tls/ssl protocols to be enabled. + :type desired_values: list + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting tls versions to be enabled for esxi.") + disabled_protocols = list(SUPPORTED_PROTOCOLS_SET - set(desired_values)) + disabled_protocols_str = ",".join(disabled_protocols) + logger.debug(f"Setting {SETTINGS_NAME} with protocols: {disabled_protocols_str}") + + host_option = vim.option.OptionValue(key=SETTINGS_NAME, value=disabled_protocols_str) + errors = [] + status = RemediateStatus.SUCCESS + try: + esxi_advanced_settings_utils.update_advanced_option(context.host_ref, host_option=host_option) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/esxi/utils/__init__.py b/config_modules_vmware/controllers/esxi/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_modules_vmware/controllers/esxi/utils/esxi_advanced_settings_utils.py b/config_modules_vmware/controllers/esxi/utils/esxi_advanced_settings_utils.py new file mode 100644 index 0000000..010678d --- /dev/null +++ b/config_modules_vmware/controllers/esxi/utils/esxi_advanced_settings_utils.py @@ -0,0 +1,47 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging + +from pyVmomi import vim + +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +def invoke_advanced_option_query(host_ref, prefix): + """ + Query config manager advanced option using prefix + :param host_ref: Host object reference of type vim.HostSystem + :param prefix: str query + :return: List of option values of type vim.option.OptionValue + """ + + try: + options = host_ref.configManager.advancedOption.QueryOptions(prefix) + if options is None or not isinstance(options, list): + raise Exception("Invalid returned options") + except vim.fault.InvalidName as e: + error_message = f"Invalid query param: {prefix} for advanced options for host: {host_ref.name}" + # pylint disabled on the following line. vim.fault.InvalidName is an Exception. + raise Exception(error_message) from e # pylint: disable=E0705 + except Exception as e: + error_message = ( + f"Exception on querying advanced options: {prefix} for host: {host_ref.name} with error msg: {e}" + ) + raise Exception(error_message) from e + return options + + +def update_advanced_option(host_ref, host_option): + """ + Update config manager advanced option using prefix + :param host_ref: Host object reference of type vim.HostSystem + :param host_option: list of option values of type vim.option.OptionValue + :return: error_message or empty error_message + """ + + try: + host_ref.configManager.advancedOption.UpdateOptions(changedValue=[host_option]) + except Exception as e: + error_message = f"Exception on updating advanced options for host: {host_ref.name} with error msg: {e}" + raise Exception(error_message) from e diff --git a/config_modules_vmware/controllers/esxi/utils/esxi_ssh_config_utils.py b/config_modules_vmware/controllers/esxi/utils/esxi_ssh_config_utils.py new file mode 100644 index 0000000..69ae7e4 --- /dev/null +++ b/config_modules_vmware/controllers/esxi/utils/esxi_ssh_config_utils.py @@ -0,0 +1,49 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging + +from config_modules_vmware.framework.auth.contexts.esxi_context import HostContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +def get_ssh_config_value(context: HostContext, config_key: str) -> str: + """ + Retrieve the ssh configuration value for the given key. + :param context: Esxi context instance. + :type context: HostContext + :param config_key: SSH config key to retrieve + :type config_key: str + :return: The configuration value + :rtype: str + """ + esx_cli_command = f"system ssh server config list -k {config_key}" + stdout, _, _ = context.esx_cli_client().run_esx_cli_cmd(context.hostname, esx_cli_command) + + try: + # Example command output: + # Key Value + # ------------------ ----- + # allowtcpforwarding no + last_line = stdout.splitlines()[-1] + key, val = last_line.split() + if key != config_key: + raise Exception(f"ssh config key was {key}, not {config_key}") + except ValueError as e: + err_msg = f"Could not find key in ssh config: '{config_key}'" + raise Exception(err_msg) from e + return val + + +def set_ssh_config_value(context: HostContext, config_key: str, config_val: str): + """ + Set the ssh configuration value for the given key. + :param context: Esxi context instance. + :type context: HostContext + :param config_key: SSH config key to retrieve + :type config_key: str + :param config_val: The configuration value to set + :type config_val: str + """ + esx_cli_command = f"system ssh server config set -k {config_key} -v {config_val}" + context.esx_cli_client().run_esx_cli_cmd(context.hostname, esx_cli_command) diff --git a/config_modules_vmware/controllers/esxi/utils/service_utils.py b/config_modules_vmware/controllers/esxi/utils/service_utils.py new file mode 100644 index 0000000..ffef1ed --- /dev/null +++ b/config_modules_vmware/controllers/esxi/utils/service_utils.py @@ -0,0 +1,66 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging + +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SERVICE_RUNNING = "service_running" +SERVICE_POLICY = "service_policy" + + +class HostServiceUtil: + def __init__(self, host_service): + """ + :param host_service: Host service object + :typpe host_service: vim.host.ServiceSystem + """ + self.host_service = host_service + self.service_map = self._build_service_map() + + def _build_service_map(self): + service_map = {} + for service in self.host_service.serviceInfo.service: + service_map[service.key] = service + return service_map + + def get_service_status(self, service_name): + """ + Query ESXi config manager service system for specific service status + :param service_name: service name + :type service_name: str + :return: Dict of service status and possible errors + """ + errors = [] + service_status = {} + service = self.service_map.get(service_name, None) + if service is None: + errors.append("service not found") + else: + service_status = {SERVICE_RUNNING: service.running, SERVICE_POLICY: service.policy} + return service_status, errors + + def start_stop_service(self, service_name, service_running): + """ + Start/stop ESXi service by config manager service system for specific service + :param service_name: service name + :type service_name: str + :param service_running: desired service running status + :type service_running: boolean + """ + if not service_running: + self.host_service.StopService(id=service_name) + else: + self.host_service.StartService(id=service_name) + + def update_service_policy(self, service_name, service_policy): + """ + Update ESXi service policy by config manager service system for specific service + :param service_name: service name + :type service_name: str + :param service_policy: desired service policy + :type service_policy: string of ("on", "off") + :return: Tuple of service status and possible errors + """ + # update service policy based on desired state + self.host_service.UpdateServicePolicy(id=service_name, policy=service_policy) diff --git a/config_modules_vmware/controllers/nsxt_edge/__init__.py b/config_modules_vmware/controllers/nsxt_edge/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_modules_vmware/controllers/nsxt_edge/ntp_config.py b/config_modules_vmware/controllers/nsxt_edge/ntp_config.py new file mode 100644 index 0000000..64c2e76 --- /dev/null +++ b/config_modules_vmware/controllers/nsxt_edge/ntp_config.py @@ -0,0 +1,96 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.nsxt_manager.ntp_config import NsxtNtpCommon +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.clients.common.consts import STATUS +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class NtpConfig(BaseController): + """Manage Ntp config with get and set methods.. + + | Config Id - 1401 + | Config Title - Synchronize system clocks to an authoritative time source. + + """ + + metadata = ControllerMetadata( + name="ntp", # controller name + path_in_schema="compliance_config.nsxt_edge.ntp", # path in the schema to this controller's definition. + configuration_id="1401", # configuration id as defined in compliance kit. + title="Configure NTP servers for the NSX-T edge.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[ + BaseContext.ProductEnum.NSXT_EDGE, + ], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + functional_test_targets=["nsxt_edge"], # location where functional tests are run. + ) + + def get(self, context: BaseContext) -> Tuple[Dict, List[Any]]: + """ + Get NTP config from NSXT edge. + + | Sample get output + + .. code-block:: json + + { + "servers": ["time.vmware.com", "time.google.com"] + } + + :param context: BaseContext, since this controller doesn't require product specific context. + :type context: BaseContext + :return: Tuple of Dict containing ntp servers and a list of error messages. + :rtype: Tuple + """ + return NsxtNtpCommon.get_ntp(context=context) + + def set(self, context: BaseContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Set NTP config in NSXT edge. + Also post set, check_compliance is run again to validate that the values are set. + + | Sample desired state for NTP. + + .. code-block:: json + + { + "servers": ["time.vmware.com", "time.google.com"] + } + + :param context: Product context instance. + :type context: BaseContext + :param desired_values: Desired value for the NTP config. Dict with keys "servers". + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting NTP control config for audit.") + errors = [] + try: + NsxtNtpCommon.set_ntp(context, desired_values) + if self.check_compliance(context, desired_values).get(STATUS) != ComplianceStatus.COMPLIANT: + raise Exception("Failed to update NTP servers") + status = RemediateStatus.SUCCESS + except Exception as e: + logger.exception(f"Exception setting ntp value - {e}") + status = RemediateStatus.FAILED + errors.append(str(e)) + return status, errors diff --git a/config_modules_vmware/controllers/nsxt_manager/__init__.py b/config_modules_vmware/controllers/nsxt_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_modules_vmware/controllers/nsxt_manager/ntp_config.py b/config_modules_vmware/controllers/nsxt_manager/ntp_config.py new file mode 100644 index 0000000..35c019c --- /dev/null +++ b/config_modules_vmware/controllers/nsxt_manager/ntp_config.py @@ -0,0 +1,176 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.clients.common.consts import STATUS +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class NsxtNtpCommon: + """Manage Ntp config with get and set methods.""" + + @staticmethod + def add_ntp(server: str) -> bool: + """ + Add NTP server. + :param server: NTP server. + :type server: str + :return: True + :rtype: bool + """ + logger.info(f"Adding ntp server {server}") + utils.run_shell_cmd(f"su -c 'set ntp-server {server}' admin") + return True + + @staticmethod + def del_ntp(server: str) -> bool: + """ + Delete NTP server + :param server: NTP server. + :type server: str + :return: True + :rtype: bool + """ + logger.info(f"Deleting ntp server {server}") + utils.run_shell_cmd(f"su -c 'del ntp-server {server}' admin") + return True + + @staticmethod + def set_ntp(context: BaseContext, desired_values: Dict) -> None: + """ + Set NTP config in NSXT. + Also post set, check_compliance is run again to validate that the values are set. + + | Sample desired state for NTP. + + .. code-block:: json + + { + "servers": ["time.vmware.com", "time.google.com"] + } + + :param context: Product context instance. + :type context: BaseContext + :param desired_values: Desired value for the NTP config. Dict with keys "servers". + :raises Exception: If there is an exception when trying to get NTP + """ + logger.info(f"Setting NTP control config for {context.product_category.value}.") + current_ntp_servers, get_errors = NsxtNtpCommon.get_ntp(context) + if get_errors: + raise Exception(f"Exception getting current NTP servers: {get_errors[0]}") + + current_ntp_servers = current_ntp_servers.get("servers", []) + desired_ntp_servers = desired_values.get("servers", []) + + logger.debug(f"Current NTP servers: {current_ntp_servers}") + logger.debug(f"Desired NTP servers: {desired_ntp_servers}") + + for ntp_server in set(desired_ntp_servers) - set(current_ntp_servers): + NsxtNtpCommon.add_ntp(ntp_server) + + for ntp_server in set(current_ntp_servers) - set(desired_ntp_servers): + NsxtNtpCommon.del_ntp(ntp_server) + return None + + @staticmethod + def get_ntp(context): + logger.info(f"Getting NTP servers for {context.product_category.value}") + errors = [] + try: + command_output = utils.run_shell_cmd("su -c 'get ntp-servers' admin")[0] + ntp_servers = list(command_output.strip().split("\n")) + except Exception as e: + logger.exception(f"Exception retrieving ntp value - {e}") + errors.append(str(e)) + ntp_servers = [] + return {"servers": ntp_servers}, errors + + +class NtpConfig(BaseController): + """Manage Ntp config with get and set methods. + + | Config Id - 1401 + | Config Title - Synchronize system clocks to an authoritative time source. + + """ + + metadata = ControllerMetadata( + name="ntp", # controller name + path_in_schema="compliance_config.nsxt_manager.ntp", # path in the schema to this controller's definition. + configuration_id="1401", # configuration id as defined in compliance kit. + title="Configure NTP servers for the NSX-T manager.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[ + BaseContext.ProductEnum.NSXT_MANAGER, + ], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + functional_test_targets=["nsxt_manager"], # location where functional tests are run. + ) + + def get(self, context: BaseContext) -> Tuple[Dict, List[Any]]: + """ + Get NTP config from NSXT manager. + + | Sample get output + + .. code-block:: json + + { + "servers": ["time.vmware.com", "time.google.com"] + } + + :param context: BaseContext, since this controller doesn't require product specific context. + :type context: BaseContext + :return: Tuple of Dict containing ntp servers and a list of error messages. + :rtype: Tuple + """ + return NsxtNtpCommon.get_ntp(context) + + def set(self, context: BaseContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Set NTP config in NSXT manager. + Also post set, check_compliance is run again to validate that the values are set. + + | Sample desired state for NTP. + + .. code-block:: json + + { + "servers": ["time.vmware.com", "time.google.com"] + } + + :param context: Product context instance. + :type context: BaseContext + :param desired_values: Desired value for the NTP config. Dict with keys "servers". + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting NTP control config for audit.") + errors = [] + try: + NsxtNtpCommon.set_ntp(context, desired_values) + if self.check_compliance(context, desired_values).get(STATUS) != ComplianceStatus.COMPLIANT: + raise Exception("Failed to update NTP servers") + status = RemediateStatus.SUCCESS + except Exception as e: + logger.exception(f"Exception setting ntp value - {e}") + status = RemediateStatus.FAILED + errors.append(str(e)) + return status, errors diff --git a/config_modules_vmware/controllers/sample/__init__.py b/config_modules_vmware/controllers/sample/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_modules_vmware/controllers/sample/sample_controller.py b/config_modules_vmware/controllers/sample/sample_controller.py new file mode 100644 index 0000000..3ffa7cb --- /dev/null +++ b/config_modules_vmware/controllers/sample/sample_controller.py @@ -0,0 +1,211 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils.comparator import ComparatorOptionForList + +# Any logging should be done using a logger which is created like this. +logger = LoggerAdapter(logging.getLogger(__name__)) + + +# Every Controller should extend the BaseController class. +# Pay attention to the docstrings formatting. They are used to autogenerate documentation. +# After writing the controller, you should run ./devops/scripts/generate_markdown_docs.sh. +# It will generate markdown documentation and put it in the api_docs directory. +class SampleController(BaseController): + """A one line summary of the controller. + + After a blank line, any more details about the controller can be given. + + """ + + # This init method is OPTIONAL. + def __init__(self): + """ + Based on the configuration data, one can choose different comparator using ComparatorOptionForList enum. + Please refer Comparator Class and ComparatorOptionForList enum under utils for more details. + BaseController class has already init method with below values but if controller wants, it can + override those. These values are used during check_compliance/remediation. + """ + super().__init__() + self.comparator_option = ComparatorOptionForList.COMPARE_AFTER_SORT + self.instance_key = "name" + + metadata = ControllerMetadata( + name="sample_controller_name", # controller name + path_in_schema="compliance_config.sample_product.sample_controller_name", # path in the schema to this controller's definition. + configuration_id="-1", # configuration id as defined in compliance kit. + title="sample config title defined in compliance kit", # controller title as defined in compliance kit. + tags=["sample", "test"], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.DISABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA., + type=ControllerMetadata.ControllerType.COMPLIANCE, # controller type i.e. compliance control or whole product configuration + ) + + # This method must be implemented. + def get(self, context): + """One line summary of the what is retrieved. + + | Optionally, after a blank line, more details may be given. For example, how the content is retrieved or + what format it will be in. + | Also describe any dependencies that may exist on other configurations or external libraries. + | Vertical bars can be used to add line blocks. + | Below is an example of a code block for a JSON object. + + .. code-block:: json + + { + "servers": [ + { + "hostname": "8.8.4.4", + "port": 90, + "protocol": "TLS" + }, + { + "hostname": "8.8.1.8", + "port": 90, + "protocol": "TLS" + } + ] + } + + :param context: Product context instance. + :type context: BaseContext + :return: Tuple of current control value and a list of error messages if any. + NOTE that the control value should be in the same format as the schema for this control. + :rtype: tuple + """ + logger.debug("Getting Sample control config.") + # Use context to connect to product and retrieve control value. + # The below code is only for use in the unit tests. + # It uses a VC Context for an example but does not call actual APIs. + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + "/test_url/sample_control" + errors = [] + try: + sample_control = vc_rest_client.get_helper(url) + except Exception as e: + errors.append(str(e)) + sample_control = -1 + return sample_control, errors + + # This method must be implemented. + def set(self, context, desired_values): + """One line summary of what is being set. + + More details about what the expected desired_values being passed in should be. Maybe an example if appropriate. + This should also describe any pre-requisite or post-requisite required for setting this value. + i.e. if a host needs to be in maintenance mode first, or needs to be restarted after. + + :param context: The type of context, i.e. VcenterContext. + :type context: BaseContext + :param desired_values: The value that is to be set (dict, string, int, etc.). + :type desired_values: The expected type + :return: Tuple of a status (from the RemediateStatus enum) and a list of errors encountered if any. + :rtype: tuple + """ + logger.debug("Setting sample control to new value.") + # Use context to connect to product and set control value. + # The below code is only for use in the unit tests. + # It uses a VC Context for an example but does not call actual APIs. + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + "/test_url/sample_control" + payload = {"sample_control": 123} + errors = [] + status = RemediateStatus.SUCCESS + try: + vc_rest_client.put_helper(url, body=payload, raise_for_status=True) + except Exception as e: + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def check_compliance(self, context, desired_values) -> Dict: + """Check compliance of current configuration against provided desired values. + + This function has a default implementation in the BaseController. If needed, you can overwrite it with your + own implementation. It should check the current value of the control against the provided desired_values and + return a dict with key "status" and a status from the ComplianceStatus enum. If + the control is NON_COMPLIANT, it should also include keys "current" and "desired" with their respective values. + If the operation failed, it should include a key "errors" and a list of the error messages. + + :param context: Product context instance. + :type context: BaseContext + :param desired_values: Desired values for this control. + :type desired_values: Any + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + # do the compliance check. + current_value, errors = self.get(context=context) + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + if current_value == desired_values: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + else: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_value, + consts.DESIRED: desired_values, + } + return result + + def remediate(self, context, desired_values) -> Dict: + """Remediate current configuration drifts. + + This function has a default implementation in the BaseController. If needed, you can overwrite it with your + own implementation. It should run check compliance and set it to the desired value if it is non-compliant. + This function should return a dict with key "status" and a status from the RemediateStatus enum. + If the value was changed it should also include keys "old" and "new" with their respective values. + If the operation failed, it should include a key "errors" and a list of the error messages. + + :param context: Product context instance. + :type context: BaseContext + :param desired_values: Desired values for the specified configuration. + :type desired_values: Any + :return: Dict of status and old/new values(for success) or errors (for failure). + :rtype: dict + """ + logger.debug("Running remediation") + + # Call check compliance and check for current compliance status. + compliance_response = self.check_compliance(context=context, desired_values=desired_values) + + if compliance_response.get(consts.STATUS) == ComplianceStatus.FAILED: + # For compliance_status as "FAILED", return FAILED with errors. + return {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: compliance_response.get(consts.ERRORS, [])} + + elif compliance_response.get(consts.STATUS) == ComplianceStatus.COMPLIANT: + # For compliant case, return SUCCESS. + return {consts.STATUS: RemediateStatus.SUCCESS} + + elif compliance_response.get(consts.STATUS) != ComplianceStatus.NON_COMPLIANT: + # Raise exception for unexpected compliance status (other than FAILED, COMPLIANT, NON_COMPLIANT). + raise Exception("Error during remediation. Unexpected compliant status found.") + + # Configs are non_compliant, call set to remediate them. + status, errors = self.set(context=context, desired_values=desired_values) + if not errors: + result = { + consts.STATUS: status, + consts.OLD: compliance_response.get(consts.CURRENT), + consts.NEW: compliance_response.get(consts.DESIRED), + } + else: + result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + + return result diff --git a/config_modules_vmware/controllers/sddc_manager/__init__.py b/config_modules_vmware/controllers/sddc_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_modules_vmware/controllers/sddc_manager/auto_rotate_schedule.py b/config_modules_vmware/controllers/sddc_manager/auto_rotate_schedule.py new file mode 100644 index 0000000..7fea5a5 --- /dev/null +++ b/config_modules_vmware/controllers/sddc_manager/auto_rotate_schedule.py @@ -0,0 +1,210 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Dict + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.sddc_manager_context import SDDCManagerContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.sddc_manager import sddc_manager_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) +TASK_ID = "id" +ESXI = "ESXI" +PSC = "PSC" +AUTO_ROTATE_CREDENTIALS = "credentials" +AUTO_ROTATE_FREQUENCY = "frequency" +RESOURCE_TYPE = "resource_type" +RESOURCE_NAME = "resource_name" +CREDENTIAL_TYPE = "credential_type" +USERNAME = "username" +FREQUENCY = "frequency" +RESOURCE = "resource" +AUTO_ROTATE_POLICY = "autoRotatePolicy" + + +class AutoRotateScheduleConfig(BaseController): + """ + Class for AutoRotateSchedule config with get and set methods. + | ConfigID - 1609 + | ConfigTitle - SDDC Manager must schedule automatic password rotation. + """ + + metadata = ControllerMetadata( + name="credential_auto_rotate_policy", # controller name + path_in_schema="compliance_config.sddc_manager.credential_auto_rotate_policy", + # path in the schema to this controller's definition. + configuration_id="1609", # configuration id as defined in compliance kit. + title="SDDC Manager must schedule automatic password rotation.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.SDDC_MANAGER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def __get_non_compliant_credentials(self, current_value, desired_value): + non_compliant_credentials = { + AUTO_ROTATE_CREDENTIALS: [ + { + RESOURCE_TYPE: element.get(RESOURCE_TYPE), + RESOURCE_NAME: element.get(RESOURCE_NAME), + CREDENTIAL_TYPE: element.get(CREDENTIAL_TYPE), + USERNAME: element.get(USERNAME), + FREQUENCY: element.get(FREQUENCY), + } + for element in current_value.get(AUTO_ROTATE_CREDENTIALS, []) + if element.get(FREQUENCY) != desired_value.get(FREQUENCY) + ] + } + return non_compliant_credentials + + def get(self, context: SDDCManagerContext): + """ + Get AutoRotateSchedule config of credentials. + + :param context: SddcManagerContext. + :type context: SddcManagerContext + :return: Tuple of dict with key "credentials" and list of error messages. + :rtype: tuple + """ + logger.info("Getting AutoRotateSchedule control config for audit.") + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.CREDENTIALS_URL + errors = [] + try: + get_response = sddc_manager_rest_client.get_helper(url) + auto_rotate_schedule_credentials = { + AUTO_ROTATE_CREDENTIALS: [ + { + RESOURCE_TYPE: element.get(RESOURCE).get("resourceType"), + RESOURCE_NAME: element.get(RESOURCE).get("resourceName"), + CREDENTIAL_TYPE: element.get("credentialType"), + USERNAME: element.get(USERNAME), + FREQUENCY: ( + element.get(AUTO_ROTATE_POLICY).get("frequencyInDays") + if (AUTO_ROTATE_POLICY in element) + else "" + ), + } + for element in get_response.get("elements", {}) + if ESXI not in element.get(RESOURCE, {}).get("resourceType") + ] + } + except Exception as e: + errors.append(str(e)) + auto_rotate_schedule_credentials = {} + return auto_rotate_schedule_credentials, errors + + def set(self, context, desired_values): + """ + Set AutoRotateSchedule config for the audit control. + + :param context: SddcManagerContext. + :type context: SddcManagerContext. + :param desired_values: Desired values for the AutoRotateSchedule config + :type desired_values: dict + :return: Tuple of "status" and list of error messages + :rtype: tuple + """ + logger.info("Setting AutoRotateSchedule control config for audit.") + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.CREDENTIALS_URL + response, errors = self.get(context=context) + payload = { + "operationType": "UPDATE_AUTO_ROTATE_POLICY", + "elements": [ + { + "resourceName": element.get("resource_name"), + "resourceType": element.get("resource_type"), + "credentials": [ + {"credentialType": element.get("credential_type"), "username": element.get("username")} + ], + } + for element in response.get("credentials", {}) + if PSC not in element.get("resource_type") + ], + "autoRotatePolicy": { + "frequencyInDays": desired_values.get(AUTO_ROTATE_FREQUENCY), + "enableAutoRotatePolicy": "true", + }, + } + errors = [] + status = RemediateStatus.SUCCESS + try: + task_info = sddc_manager_rest_client.patch_helper(url, body=payload) + logger.info(f"Remediation Task ID {task_info.get(TASK_ID)}") + task_status = sddc_manager_rest_client.monitor_task(task_info.get(TASK_ID)) + if not task_status: + raise Exception(f"Remediation failed for task: {task_info.get(TASK_ID)} check log for details") + except Exception as e: + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def check_compliance(self, context, desired_values: Dict) -> Dict: + """ + Check compliance of auto rotate configuration. + + :param context: Product context instance. + :type context: SDDCManagerContext + :param desired_values: Desired values for the specified configuration. + :type desired_values: Dict + :return: Dict of status and current/desired value( for non_compliant) or errors ( for failure). + :rtype: tuple + """ + logger.info("Checking compliance.") + current_value, errors = self.get(context=context) + # If errors are seen during get, return "FAILED" status with errors. + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_credentials = self.__get_non_compliant_credentials(current_value, desired_values) + if non_compliant_credentials.get(AUTO_ROTATE_CREDENTIALS): + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_credentials, + consts.DESIRED: desired_values, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result + + def remediate(self, context, desired_values: Dict) -> Dict: + """ + Remediate configuration drifts. + + :param context: Product context instance. + :type context: SDDCManagerContext + :param desired_values: Desired values for the specified configuration. + :type desired_values: Dict + :return: Dict of status and old/new values(for success) or errors (for failure). + :rtype: dict + """ + logger.info("Running remediation") + current_value, errors = self.get(context=context) + # Errors seen during get, return "FAILED" status with errors. + if errors: + return {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + + desired_value = desired_values + non_compliant_credentials = self.__get_non_compliant_credentials(current_value, desired_value) + if non_compliant_credentials.get(AUTO_ROTATE_CREDENTIALS): + status, errors = self.set(context=context, desired_values=desired_value) + else: + return {consts.STATUS: RemediateStatus.SUCCESS} + + if not errors: + result = {consts.STATUS: status, consts.OLD: non_compliant_credentials, consts.NEW: desired_value} + else: + result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + + return result diff --git a/config_modules_vmware/controllers/sddc_manager/backup.py b/config_modules_vmware/controllers/sddc_manager/backup.py new file mode 100644 index 0000000..6ba4960 --- /dev/null +++ b/config_modules_vmware/controllers/sddc_manager/backup.py @@ -0,0 +1,269 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.sddc_manager_context import SDDCManagerContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.sddc_manager import sddc_manager_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils +from config_modules_vmware.framework.utils.comparator import Comparator + +logger = LoggerAdapter(logging.getLogger(__name__)) +ENCRYPTION = "encryption" # nosec +PASSPHRASE = "passphrase" # nosec +SFTP = "SFTP" # nosec +PORT_NUMBER = 22 +SERVER = "server" +PORT = "port" +PROTOCOL = "protocol" +USERNAME = "username" # nosec +PASSWORD = "password" # nosec +DIRECTORY_PATH = "directory_path" +BACKUP_LOCATIONS = "backup_locations" +BACKUP_SCHEDULES = "backup_schedules" +RESOURCE_TYPE = "resource_type" +TAKE_SCHEDULED_BACKUPS = "take_scheduled_backups" +FREQUENCY = "frequency" +DAYS_OF_WEEK = "days_of_week" +HOUR_OF_DAY = "hour_of_day" +MINUTE_OF_HOUR = "minute_of_hour" +TAKE_BACKUP_ON_STATE_CHANGE = "take_backup_on_state_change" +RETENTION_POLICY = "retention_policy" +NUMBER_OF_MOST_RECENT_BACKUPS = "number_of_most_recent_backups" +NUMBER_OF_DAYS_OF_HOURLY_BACKUPS = "number_of_days_of_hourly_backups" +NUMBER_OF_DAYS_OF_DAILY_BACKUPS = "number_of_days_of_daily_backups" +SDDC_MANAGER = "SDDC_MANAGER" +ID = "id" +STATUS = "status" +CUSTOM_EXCEPTION = "Check for valid backup location and backup schedule and retry!!" + + +class BackupConfig(BaseController): + """ + Class for backupSettings config with get and set methods. + | ConfigID - 1600 + | ConfigTitle - Verify SDDC Manager backup. + """ + + metadata = ControllerMetadata( + name="backup", # controller name + path_in_schema="compliance_config.sddc_manager.backup", + # path in the schema to this controller's definition. + configuration_id="1600", # configuration id as defined in compliance kit. + title="Verify SDDC Manager backup.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.SDDC_MANAGER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def __gen_ssh_fingerprint(self, backup_server): + ssh_keyscan_command = f"ssh-keyscan -t rsa {backup_server}" + ssh_keyscan_std_out, _, _ = utils.run_shell_cmd(ssh_keyscan_command) + ssh_keygen_command = "ssh-keygen -lf -" + ssh_keygen_output, _, _ = utils.run_shell_cmd(ssh_keygen_command, input_to_stdin=ssh_keyscan_std_out) + ssh_fingerprint = ssh_keygen_output.split()[1] + return ssh_fingerprint + + def get(self, context: SDDCManagerContext) -> Tuple[Dict, List[Any]]: + """ + Get Backup config for audit control. + + :param context: SDDCManagerContext. + :type context: SDDCManagerContext + :return: Tuple of dict and list of error messages. + :rtype: tuple + """ + logger.info("Getting BackupSettings control config for audit.") + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.BACKUP_URL + errors = [] + try: + current_backup_response = sddc_manager_rest_client.get_helper(url) + backup_response = { + BACKUP_LOCATIONS: [ + { + SERVER: element.get(SERVER), + PORT: element.get(PORT), + PROTOCOL: element.get(PROTOCOL), + USERNAME: element.get(USERNAME), + DIRECTORY_PATH: element.get("directoryPath"), + } + for element in current_backup_response.get("backupLocations", {}) + ], + BACKUP_SCHEDULES: [ + { + RESOURCE_TYPE: element.get("resourceType"), + TAKE_SCHEDULED_BACKUPS: element.get("takeScheduledBackups"), + FREQUENCY: element.get(FREQUENCY), + DAYS_OF_WEEK: element.get("daysOfWeek"), + HOUR_OF_DAY: element.get("hourOfDay"), + MINUTE_OF_HOUR: element.get("minuteOfHour"), + TAKE_BACKUP_ON_STATE_CHANGE: element.get("takeBackupOnStateChange"), + RETENTION_POLICY: { + NUMBER_OF_MOST_RECENT_BACKUPS: element.get("retentionPolicy", {}).get( + "numberOfMostRecentBackups" + ), + NUMBER_OF_DAYS_OF_HOURLY_BACKUPS: element.get("retentionPolicy", {}).get( + "numberOfDaysOfHourlyBackups" + ), + NUMBER_OF_DAYS_OF_DAILY_BACKUPS: element.get("retentionPolicy", {}).get( + "numberOfDaysOfDailyBackups" + ), + }, + } + for element in current_backup_response.get("backupSchedules", {}) + ], + } + except Exception as e: + errors.append(f"Error retrieving backup settings: {str(e)}") + backup_response = {} + return backup_response, errors + + def set(self, context: SDDCManagerContext, desired_values: Dict) -> Tuple: + """ + Set Backup config for the audit control. + + :param context: SDDCManagerContext. + :type context: SDDCManagerContext. + :param desired_values: Desired values for the DepotSettings config + :type desired_values: dict + :return: Tuple of "status" and list of error messages + :rtype: tuple + """ + logger.info("Setting BackupSettings control config for audit.") + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.BACKUP_URL + + payload = { + "encryption": {"passphrase": desired_values.get(ENCRYPTION).get(PASSPHRASE)}, + "backupLocations": [ + { + SERVER: location.get(SERVER), + "port": PORT_NUMBER, + "protocol": SFTP, + USERNAME: location.get(USERNAME), + PASSWORD: location.get(PASSWORD), + "directoryPath": location.get(DIRECTORY_PATH), + "sshFingerprint": self.__gen_ssh_fingerprint(location.get(SERVER)), + } + for location in desired_values.get(BACKUP_LOCATIONS) + ], + "backupSchedules": [ + { + "resourceType": SDDC_MANAGER, + "takeScheduledBackups": schedule.get(TAKE_SCHEDULED_BACKUPS), + FREQUENCY: schedule.get(FREQUENCY), + "daysOfWeek": schedule.get(DAYS_OF_WEEK), + "hourOfDay": schedule.get(HOUR_OF_DAY), + "minuteOfHour": schedule.get(MINUTE_OF_HOUR), + "takeBackupOnStateChange": schedule.get(TAKE_BACKUP_ON_STATE_CHANGE), + "retentionPolicy": { + "numberOfMostRecentBackups": schedule.get(RETENTION_POLICY).get(NUMBER_OF_MOST_RECENT_BACKUPS), + "numberOfDaysOfHourlyBackups": schedule.get(RETENTION_POLICY).get( + NUMBER_OF_DAYS_OF_HOURLY_BACKUPS + ), + "numberOfDaysOfDailyBackups": schedule.get(RETENTION_POLICY).get( + NUMBER_OF_DAYS_OF_DAILY_BACKUPS + ), + }, + } + for schedule in desired_values.get(BACKUP_SCHEDULES) + ], + } + errors = [] + status = RemediateStatus.SUCCESS + try: + task_info = sddc_manager_rest_client.put_helper(url, body=payload) + logger.info(f"Remediation Task ID {task_info.get(ID)}") + task_status = sddc_manager_rest_client.monitor_task(task_info.get(ID)) + if not task_status: + raise Exception(f"Remediation failed for task: {task_info.get(ID)} check log for details") + except Exception as e: + errors.append(CUSTOM_EXCEPTION + ":" + str(e)) + status = RemediateStatus.FAILED + return status, errors + + def check_compliance(self, context: SDDCManagerContext, desired_values: Dict) -> Dict: + """Check compliance of current backup configuration in SDDC Manager. + + :param context: Product context instance. + :type context: SDDCManagerContext + :param desired_values: Desired values for backup config. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.info("Checking compliance.") + current_value, errors = self.get(context=context) + + # If errors are seen during get, return "FAILED" status with errors. + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + # Update the desired value to match the format of current_value + desired_value = { + BACKUP_LOCATIONS: [ + { + SERVER: element.get(SERVER), + PORT: PORT_NUMBER, + PROTOCOL: SFTP, + USERNAME: element.get(USERNAME), + DIRECTORY_PATH: element.get(DIRECTORY_PATH), + } + for element in desired_values.get(BACKUP_LOCATIONS, {}) + ], + BACKUP_SCHEDULES: [ + { + RESOURCE_TYPE: element.get(RESOURCE_TYPE), + TAKE_SCHEDULED_BACKUPS: element.get(TAKE_SCHEDULED_BACKUPS), + FREQUENCY: element.get(FREQUENCY), + DAYS_OF_WEEK: element.get(DAYS_OF_WEEK), + HOUR_OF_DAY: element.get(HOUR_OF_DAY), + MINUTE_OF_HOUR: element.get(MINUTE_OF_HOUR), + TAKE_BACKUP_ON_STATE_CHANGE: element.get(TAKE_BACKUP_ON_STATE_CHANGE), + RETENTION_POLICY: { + NUMBER_OF_MOST_RECENT_BACKUPS: element.get(RETENTION_POLICY, {}).get( + NUMBER_OF_MOST_RECENT_BACKUPS + ), + NUMBER_OF_DAYS_OF_HOURLY_BACKUPS: element.get(RETENTION_POLICY, {}).get( + NUMBER_OF_DAYS_OF_HOURLY_BACKUPS + ), + NUMBER_OF_DAYS_OF_DAILY_BACKUPS: element.get(RETENTION_POLICY, {}).get( + NUMBER_OF_DAYS_OF_DAILY_BACKUPS + ), + }, + } + for element in desired_values.get(BACKUP_SCHEDULES, {}) + ], + } + + # If no errors seen, compare the current and desired value. If not same, return "NON_COMPLIANT" with values. + # Otherwise, return "COMPLIANT". + + current_non_compliant_configs, desired_non_compliant_configs = Comparator.get_non_compliant_configs( + current_value, desired_value + ) + if current_non_compliant_configs or desired_non_compliant_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_non_compliant_configs, + consts.DESIRED: desired_non_compliant_configs, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT.name} + return result diff --git a/config_modules_vmware/controllers/sddc_manager/cert_config.py b/config_modules_vmware/controllers/sddc_manager/cert_config.py new file mode 100644 index 0000000..0ed551d --- /dev/null +++ b/config_modules_vmware/controllers/sddc_manager/cert_config.py @@ -0,0 +1,165 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +import socket +import ssl +from typing import Any +from typing import Dict +from typing import List +from typing import Optional +from typing import Tuple + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.sddc_manager_context import SDDCManagerContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.sddc_manager import sddc_manager_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ISSUER = "issuer" + + +class CertConfig(BaseController): + """ + Class for cert config with get and set methods. + + | Config Id - 1603 + | Config Title - Use an SSL certificate issued by a trusted certificate authority on the SDDC Manager. + + """ + + metadata = ControllerMetadata( + name="cert_config", # controller name + path_in_schema="compliance_config.sddc_manager.cert_config", + # path in the schema to this controller's definition. + configuration_id="1603", # configuration id as defined in compliance kit. + title="Use an SSL certificate issued by a trusted certificate authority on the SDDC Manager.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.SDDC_MANAGER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, + # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: SDDCManagerContext) -> Tuple[Dict, List[Any]]: + """ + Function to get certificate details of sddc manager for audit. + + :param context: SDDCManagerContext. + :type context: SDDCManagerContext + :return: Details of the certificate issuer + :rtype: tuple + """ + logger.info("Getting Certificate details for audit.") + errors = [] + sddc_manager_rest_client = context.sddc_manager_rest_client() + result = {} + try: + # Get SDDC manager IP + sddc_manager_url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.SDDC_MANAGER_URL + api_response = sddc_manager_rest_client.get_helper(sddc_manager_url) + sddc_manager_ip = api_response["elements"][0]["ipAddress"] + + issuer = self.__get_certificate_issuer(hostname=sddc_manager_ip, port=443) + if issuer: + result[ISSUER] = issuer + else: + raise Exception("Unable to fetch issuer details from cert") + except Exception as e: + logger.exception(f"Unable to fetch certificate details {e}") + errors.append(str(e)) + + return result, errors + + def __get_certificate_issuer(self, hostname: str, port: int = 443) -> Optional[str]: + """Fetch and parse the SSL certificate details from a given hostname and port and retrieve the issuer. + + :param hostname: Hostname or IP address of the server + :param port: Port number (default is 443 for HTTPS) + :return: Certificate Issuer + :rtype: str or None + """ + try: + ssl_ctx = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2) + ssl_ctx.verify_mode = ssl.CERT_NONE + with socket.create_connection((hostname, port), timeout=10) as sock: + with ssl_ctx.wrap_socket(sock, server_hostname=hostname) as ssock: + cert = ssock.getpeercert(True) + pem_certificate = ssl.DER_cert_to_PEM_cert(cert) + cert_obj = x509.load_pem_x509_certificate(pem_certificate.encode(), default_backend()) + issuer = cert_obj.issuer.rfc4514_string() + logger.info(f"Published by issuer: {issuer}") + return issuer + except Exception as e: + logger.exception(f"An error occurred while fetching certificate details: {e}") + return None + + def set(self, context: SDDCManagerContext, desired_values) -> Tuple: + """ + Set is not implemented as this control requires manual intervention. + + :param context: SDDCManagerContext. + :type context: SDDCManagerContext. + :param desired_values: Desired value for the certificate authority + :type desired_values: String or list of strings + :return: Tuple of status (RemediateStatus.SKIPPED) and errors if any + :rtype: tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors + + def check_compliance(self, context, desired_values) -> Dict: + """ + + Check compliance of configured certificate authority in SDDC Manager. Certificate issuer details needs + to be provided as shown in the below sample format (can provide multiple certs too).The method will check if the + current certificate details is available in the desired_values and return the compliance + status accordingly. + + | Sample desired_values spec + + .. code-block:: json + + { + "certificate_issuer": + ["OU=VMware Engineering,O=vcenter-1.vrack.vsphere.local,ST=California,C=US,DC=local,DC=vsphere,CN=CB", + "OU=VMware Engineering,O=vcenter-1.vrack.vsphere.local,ST=California,C=US,DC=local,DC=vsphere,CN=CA"] + } + + :param context: Product context instance + :type context: SDDCManagerContext + :param desired_values: Desired value for the certificate authority. + :type desired_values: Dictionary + :return: Dict of status and current/desired value or errors (for failure). + :rtype: dict + """ + logger.info("Checking compliance") + cert_info, errors = self.get(context=context) + current_value = cert_info.get(ISSUER) + + # If errors are seen during get, return "FAILED" status with errors. + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + if current_value in desired_values["certificate_issuer"]: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + else: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_value, + consts.DESIRED: desired_values, + } + return result diff --git a/config_modules_vmware/controllers/sddc_manager/depot_config.py b/config_modules_vmware/controllers/sddc_manager/depot_config.py new file mode 100644 index 0000000..f4bc5c5 --- /dev/null +++ b/config_modules_vmware/controllers/sddc_manager/depot_config.py @@ -0,0 +1,108 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.sddc_manager_context import SDDCManagerContext +from config_modules_vmware.framework.clients.sddc_manager import sddc_manager_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +VMWARE_ACCOUNT = "vmware_account" +VMWARE_ACCOUNT_CAMEL_CASE = "vmwareAccount" +USERNAME = "username" # nosec +PASSWORD = "password" # nosec +ERROR_CODE = "errorCode" +CUSTOM_EXCEPTION = "Check for valid depot credentials and retry!!" + + +class DepotConfig(BaseController): + """Class for Depot config with get and set methods. + | ConfigID - 1607 + | ConfigTitle - Dedicate an account for downloading updates and patches in SDDC Manager. + """ + + metadata = ControllerMetadata( + name="depot_config", # controller name + path_in_schema="compliance_config.sddc_manager.depot_config", + # path in the schema to this controller's definition. + configuration_id="1607", # configuration id as defined in compliance kit. + title="Dedicate an account for downloading updates and patches in SDDC Manager.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.SDDC_MANAGER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: SDDCManagerContext) -> Tuple[Dict, List[Any]]: + """Get Depot Configuration from SDDC Manager. + Validation is not done to check if dedicated account is passed. Upto the customer to pass the dedicated account. + + :param context: Product context instance. + :type context: SDDCManagerContext + :return: Tuple of dict with key "vmware_account" and list of error messages. + :rtype: tuple + """ + logger.info("Getting Depot Configuration.") + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.DEPOT_URL + current_value = {} + + errors = [] + try: + depot_settings = sddc_manager_rest_client.get_helper(url) + if VMWARE_ACCOUNT_CAMEL_CASE in depot_settings: + current_value[VMWARE_ACCOUNT] = { + USERNAME: depot_settings.get(VMWARE_ACCOUNT_CAMEL_CASE, {}).get(USERNAME, None), + PASSWORD: depot_settings.get(VMWARE_ACCOUNT_CAMEL_CASE, {}).get(PASSWORD, None), + } + except Exception as e: + errors.append(str(e)) + logger.error(f"An Exception occurred: {e}") + current_value = {} + return current_value, errors + + def set(self, context: SDDCManagerContext, desired_values) -> Tuple: + """Set Depot Configuration in SDDC Manager. + + :param context: Product context instance. + :type context: SDDCManagerContext + :param desired_values: Desired value for the Depot config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + + logger.info("Setting Depot config for audit.") + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.DEPOT_URL + + # Update the key names with Camel case for the payload + payload = { + VMWARE_ACCOUNT_CAMEL_CASE: { + USERNAME: desired_values.get(VMWARE_ACCOUNT, {}).get(USERNAME, None), + PASSWORD: desired_values.get(VMWARE_ACCOUNT, {}).get(PASSWORD, None), + } + } + errors = [] + status = RemediateStatus.SUCCESS + try: + sddc_manager_rest_client.put_helper(url, body=payload, raise_for_status=True) + + except Exception as e: + errors.append(CUSTOM_EXCEPTION + ":" + str(e)) + logger.error(f"An Exception occurred: {e}") + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/sddc_manager/dns_config.py b/config_modules_vmware/controllers/sddc_manager/dns_config.py new file mode 100644 index 0000000..a45eb0b --- /dev/null +++ b/config_modules_vmware/controllers/sddc_manager/dns_config.py @@ -0,0 +1,151 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import json +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.sddc_manager_context import SDDCManagerContext +from config_modules_vmware.framework.clients.common import rest_client +from config_modules_vmware.framework.clients.sddc_manager import sddc_manager_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class DnsConfig(BaseController): + """Operations for Dns config in SDDC Manager.""" + + metadata = ControllerMetadata( + name="dns", # controller name + path_in_schema="compliance_config.sddc_manager.dns", # path in the schema to this controller's definition. + configuration_id="1612", # configuration id as defined in compliance kit. + title="DNS should be configured to a global value that is enforced by SDDC Manager", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.SDDC_MANAGER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: SDDCManagerContext) -> Tuple[List[Dict], List[Any]]: + """Get DNS config from SDDC Manager. + + :param context: Product context instance. + :type context: SDDCManagerContext + :return: Tuple of dict with key "servers" and list of error messages. + :rtype: tuple + """ + + logger.info("Getting DNS control config for audit.") + + # Get using public API + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.DNS_URL + errors = [] + try: + dns_server_resp = sddc_manager_rest_client.get_helper(url) + dns_servers = [server["ipAddress"] for server in dns_server_resp.get("dnsServers", [])] + except Exception as e: + errors.append(str(e)) + dns_servers = [] + return {"servers": dns_servers}, errors + + def _set_dns_using_local_url(self, desired_values): + """Set DNS config in SDDC Manager using local API. + + :param desired_values: Desired values for the DNS config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + errors = [] + status = RemediateStatus.SUCCESS + try: + url = sddc_manager_consts.LOCAL_DNS_URL + payload = {} + if len(desired_values.get("servers")) >= 2: + payload = { + "primaryDnsServer": desired_values.get("servers")[0], + "secondaryDnsServer": desired_values.get("servers")[1], + } + elif len(desired_values.get("servers")) == 1: + payload = {"primaryDnsServer": desired_values.get("servers")[0]} + rest_headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + smart_rest_client_obj = rest_client.get_smart_rest_client() + dns_patch_response = smart_rest_client_obj.patch( + url=url, timeout=60, headers=rest_headers, body=json.dumps(payload) + ) + smart_rest_client_obj.raise_for_status(dns_patch_response, url) + + except Exception as e: + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def set(self, context, desired_values: Dict) -> Tuple[str, List[Any]]: + """Set DNS config in SDDC Manager. + + :param context: Product context instance. + :type context: SDDCManagerContext + :param desired_values: Desired values for the DNS config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + + logger.info("Setting DNS control config for audit.") + + # If 'is_local' flag is not present in desired spec or is set as True, then configure using local url. + if desired_values.get("is_local", True): + return self._set_dns_using_local_url(desired_values=desired_values) + + # Set using public API to propagate configuration to all components managed by SDDC-Manager + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.DNS_URL + payload = {} + if len(desired_values.get("servers")) >= 2: + payload = { + "dnsServers": [ + {"ipAddress": desired_values.get("servers")[0], "isPrimary": "true"}, + {"ipAddress": desired_values.get("servers")[1], "isPrimary": "false"}, + ] + } + elif len(desired_values.get("servers")) == 1: + payload = {"dnsServers": [{"ipAddress": desired_values.get("servers")[0], "isPrimary": "true"}]} + errors = [] + status = RemediateStatus.SUCCESS + try: + task_info = sddc_manager_rest_client.put_helper(url, body=payload, raise_for_status=True) + logger.info(f'Remediation Task ID {task_info.get("id")}') + if not sddc_manager_rest_client.monitor_task(task_info.get("id")): + raise Exception(f'Remediation failed for task {task_info.get("id")} check log for details') + except Exception as e: + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def check_compliance(self, context: SDDCManagerContext, desired_values: Dict) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: SDDCManagerContext + :param desired_values: Desired values for the specified configuration. + :type desired_values: Any + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + dns_desired_value = {"servers": desired_values.get("servers", [])} + return super().check_compliance(context, desired_values=dns_desired_value) diff --git a/config_modules_vmware/controllers/sddc_manager/fips_config.py b/config_modules_vmware/controllers/sddc_manager/fips_config.py new file mode 100644 index 0000000..8cc3ec7 --- /dev/null +++ b/config_modules_vmware/controllers/sddc_manager/fips_config.py @@ -0,0 +1,89 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.sddc_manager import sddc_manager_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class FipsConfig(BaseController): + """ + Class for Fips config with get method. + """ + + metadata = ControllerMetadata( + name="fips_mode_enabled", # controller name + path_in_schema="compliance_config.sddc_manager.fips_mode_enabled", # path in the schema to this controller's definition. + configuration_id="1608", # configuration id as defined in compliance kit. + title="SDDC Manager must be deployed with FIPS mode enabled", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.SDDC_MANAGER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context): + """ + Get FIPS mode status. + + :param context: SDDCManagerContext. + :type context: SDDCManagerContext + :return: Tuple of fips mode status (as a boolean data type) and list of error messages + :rtype: tuple + """ + + logger.info("Getting FIPS mode details for audit.") + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.FIPS_URL + errors = [] + try: + fips_resp = sddc_manager_rest_client.get_helper(url) + + if not isinstance(fips_resp, dict): + raise TypeError("FIPS response is not a dictionary") + logger.info(fips_resp) + fips_mode = fips_resp["enabled"] + + except TypeError as te: + errors.append(str(te)) + fips_mode = None + logger.error(f"Type error: {te}") + + except KeyError as ke: + errors.append(str(ke)) + fips_mode = None + logger.error("Enabled key not found in the API response.") + + except Exception as e: + errors.append(str(e)) + fips_mode = None + + return fips_mode, errors + + def set(self, context, desired_values): + """ + Set will not be implemented as in current VCF versions we can't change FIPS mode post bringup. + + :param context: SDDCManagerContext. + :type context: SDDCManagerContext + :param desired_values: True + :type desired_values: boolean + :return: Tuple of status and list of error messages + :rtype: tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + logger.info("Remediate is not implemented as it is not possible to change FIPS mode post bringup") + + return status, errors diff --git a/config_modules_vmware/controllers/sddc_manager/ntp_config.py b/config_modules_vmware/controllers/sddc_manager/ntp_config.py new file mode 100644 index 0000000..68f3853 --- /dev/null +++ b/config_modules_vmware/controllers/sddc_manager/ntp_config.py @@ -0,0 +1,135 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import json +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.sddc_manager_context import SDDCManagerContext +from config_modules_vmware.framework.clients.common import rest_client +from config_modules_vmware.framework.clients.sddc_manager import sddc_manager_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class NtpConfig(BaseController): + """Operations for Ntp config in SDDC Manager.""" + + metadata = ControllerMetadata( + name="ntp", # controller name + path_in_schema="compliance_config.sddc_manager.ntp", # path in the schema to this controller's definition. + configuration_id="1601", # configuration id as defined in compliance kit. + title="SDDC Manager components must use an authoritative time source [NTP]", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.SDDC_MANAGER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: SDDCManagerContext) -> Tuple[List[Dict], List[Any]]: + """Get list of NTP Servers from SDDC Manager. + + :param context: Product context instance. + :type context: SDDCManagerContext + :return: Tuple of dict with key "servers" and list of error messages. + :rtype: tuple + """ + logger.info("Getting NTP servers.") + + # Get using public API + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.NTP_URL + + errors = [] + try: + ntp_servers_resp = sddc_manager_rest_client.get_helper(url) + ntp_servers = [server["ipAddress"] for server in ntp_servers_resp.get("ntpServers", [])] + except Exception as e: + errors.append(str(e)) + ntp_servers = [] + return {"servers": ntp_servers}, errors + + def _set_ntp_using_local_url(self, desired_values): + """Set NTP config in SDDC Manager using local API. + + :param desired_values: Desired values for the NTP config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + errors = [] + status = RemediateStatus.SUCCESS + try: + url = sddc_manager_consts.LOCAL_NTP_URL + payload = {"ntpServers": desired_values.get("servers", [])} + rest_headers = { + "Accept": "application/json", + "Content-Type": "application/json", + } + smart_rest_client_obj = rest_client.get_smart_rest_client() + ntp_patch_response = smart_rest_client_obj.patch( + url=url, timeout=60, headers=rest_headers, body=json.dumps(payload) + ) + smart_rest_client_obj.raise_for_status(ntp_patch_response, url) + except Exception as e: + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def set(self, context, desired_values) -> Tuple[str, List[Any]]: + """Set NTP config in SDDC Manager. + + :param context: Product context instance. + :type context: SDDCManagerContext + :param desired_values: Desired value for the NTP config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + + logger.info("Setting NTP control config for audit.") + + # If 'is_local' flag is not present in desired spec or is set as True, then configure using local url. + if desired_values.get("is_local", True): + return self._set_ntp_using_local_url(desired_values=desired_values) + + # Set using public API to propagate configuration to all components managed by SDDC-Manager + sddc_manager_rest_client = context.sddc_manager_rest_client() + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.NTP_URL + payload = {"ntpServers": [{"ipAddress": server} for server in desired_values.get("servers", [])]} + errors = [] + status = RemediateStatus.SUCCESS + try: + task_info = sddc_manager_rest_client.put_helper(url, body=payload, raise_for_status=True) + logger.info(f'Remediation Task ID {task_info.get("id")}') + task_status = sddc_manager_rest_client.monitor_task(task_info.get("id")) + if not task_status: + raise Exception(f'Remediation failed for task: {task_info.get("id")} check log for details') + except Exception as e: + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def check_compliance(self, context: SDDCManagerContext, desired_values: Dict) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: SDDCManagerContext + :param desired_values: Desired values for the specified configuration. + :type desired_values: Any + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + ntp_desired_value = {"servers": desired_values.get("servers", [])} + return super().check_compliance(context, desired_values=ntp_desired_value) diff --git a/config_modules_vmware/controllers/sddc_manager/proxy_config.py b/config_modules_vmware/controllers/sddc_manager/proxy_config.py new file mode 100644 index 0000000..9cdd834 --- /dev/null +++ b/config_modules_vmware/controllers/sddc_manager/proxy_config.py @@ -0,0 +1,270 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import fcntl +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.sddc_manager_context import SDDCManagerContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.sddc_manager import sddc_manager_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +PROXY_HOST = "host" +PROXY_CONFIGURED = "isConfigured" +PROXY_ENABLED = "isEnabled" +PROXY_ENABLED_DESIRED_VALUE = "proxy_enabled" +PROXY_PORT = "port" +LCM_APP_PROPERTY_FILE = "/opt/vmware/vcf/lcm/lcm-app/conf/application-prod.properties" +LCM_DEPOT_PROXY_ENABLED = "lcm.depot.adapter.proxyEnabled=" +LCM_DEPOT_PROXY_HOST = "lcm.depot.adapter.proxyHost=" +LCM_DEPOT_PROXY_PORT = "lcm.depot.adapter.proxyPort=" +LCM_SERVICE_RESTART = "systemctl restart lcm" + + +class ProxyConfig(BaseController): + """Class for Proxy config with get and set methods. + | ConfigID - 1604 + | ConfigTitle - Enable/Disable lcm proxy configuration. + """ + + metadata = ControllerMetadata( + name="proxy_config", # controller name + path_in_schema="compliance_config.sddc_manager.proxy_config", + # path in the schema to this controller's definition. + configuration_id="1604", # configuration id as defined in compliance kit. + title="Enable/Disable lcm proxy configuration", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.SDDC_MANAGER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def _get_proxy_setting_by_api(self, sddc_manager_rest_client) -> Tuple[Dict, List[Any]]: + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.PROXY_URL + current_value = {} + + errors = [] + try: + proxy_settings = sddc_manager_rest_client.get_helper(url) + logger.info(f"Proxy Configuration: {proxy_settings}") + current_value = { + PROXY_ENABLED_DESIRED_VALUE: proxy_settings.get(PROXY_ENABLED, False), + PROXY_HOST: proxy_settings.get(PROXY_HOST), + PROXY_PORT: proxy_settings.get(PROXY_PORT), + } + except Exception as e: + errors.append(str(e)) + logger.error(f"An Exception occurred: {str(e)}") + current_value = {} + return current_value, errors + + def _set_proxy_setting_by_api(self, sddc_manager_rest_client, desired_values) -> Tuple: + """Set Proxy Configuration from SDDC Manager by api. + :param sddc_manager_rest_client: SDDC Manager API client. + :type sddc_manager_rest_client: API client + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Set proxy config by API.") + url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.PROXY_URL + + # Update the key names with Camel case for the payload + # There are 4 parameters used in proxy config api: + # isEnabled - to enable proxy + # isConfigured - to configure proxy parameters such as host/port. Initially, this flag is False, + # however, once it is enabled and host/port configured, this flag will always be True. + # host - proxy server IP address or fqdn + # port - proxy server port number + payload = { + PROXY_HOST: desired_values.get(PROXY_HOST), + PROXY_CONFIGURED: desired_values.get(PROXY_ENABLED_DESIRED_VALUE), + PROXY_ENABLED: desired_values.get(PROXY_ENABLED_DESIRED_VALUE), + PROXY_PORT: desired_values.get(PROXY_PORT), + } + errors = [] + status = RemediateStatus.SUCCESS + try: + sddc_manager_rest_client.patch_helper(url, body=payload, raise_for_status=True) + + except Exception as e: + errors.append(str(e)) + logger.error(f"An Exception occurred: {str(e)}") + status = RemediateStatus.FAILED + return status, errors + + def _get_proxy_setting_by_file(self, lcm_app_property_file) -> Tuple[Dict, List[Any]]: + """Get Proxy Configuration from SDDC Manager lcm app property file. + :param lcm_app_property_file: LCM app property file. + :type lcm_app_property_file: str + :return: Tuple of dict with proxy configuration. + :rtype: Tuple + """ + errors = [] + proxy_settings = {} + try: + # Read the lcm app property file file + with open(lcm_app_property_file, "r", encoding="UTF-8") as f: + lines = f.readlines() + logger.debug(f"Readlines from file: {lines}") + + for line in lines: + if line.startswith(LCM_DEPOT_PROXY_ENABLED): + key, value = PROXY_ENABLED_DESIRED_VALUE, line.strip().split("=", 1)[1].strip().lower() in ( + "true", + "1", + "yes", + ) + elif line.startswith(LCM_DEPOT_PROXY_HOST): + key, value = PROXY_HOST, line.strip().split("=", 1)[1].strip() + elif line.startswith(LCM_DEPOT_PROXY_PORT): + key, value = PROXY_PORT, int(line.strip().split("=", 1)[1].strip()) + else: + continue + proxy_settings[key] = value + + except Exception as e: + errors.append(str(e)) + + return proxy_settings, errors + + def _set_proxy_setting_by_file(self, lcm_app_property_file, desired_values) -> Tuple: + """Set Proxy Configuration from SDDC Manager by modifying lcm app property file. + :param lcm_app_property_file: LCM app property file. + :type lcm_app_property_file: str + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting Proxy config from LCM file.") + errors = [] + status = RemediateStatus.SUCCESS + try: + proxy_enabled_value = "true" if desired_values.get(PROXY_ENABLED_DESIRED_VALUE) else "false" + key_to_new_lines = {LCM_DEPOT_PROXY_ENABLED: f"{LCM_DEPOT_PROXY_ENABLED}{proxy_enabled_value}\n"} + if desired_values.get(PROXY_HOST) is not None: + key_to_new_lines[LCM_DEPOT_PROXY_HOST] = f"{LCM_DEPOT_PROXY_HOST}{desired_values.get(PROXY_HOST)}\n" + if desired_values.get(PROXY_PORT) is not None: + key_to_new_lines[LCM_DEPOT_PROXY_PORT] = f"{LCM_DEPOT_PROXY_PORT}{desired_values.get(PROXY_PORT)}\n" + + with open(lcm_app_property_file, "r+", encoding="UTF-8") as f: + # Acquire an exclusive lock on the file + fcntl.flock(f, fcntl.LOCK_EX) + + # Read the lines + lines = f.readlines() + + updated_lines = [] + for line in lines: + if line.startswith(LCM_DEPOT_PROXY_ENABLED): + updated_lines.append(key_to_new_lines.pop(LCM_DEPOT_PROXY_ENABLED)) + elif line.startswith(LCM_DEPOT_PROXY_HOST) and LCM_DEPOT_PROXY_HOST in key_to_new_lines: + updated_lines.append(key_to_new_lines.pop(LCM_DEPOT_PROXY_HOST)) + elif line.startswith(LCM_DEPOT_PROXY_PORT) and LCM_DEPOT_PROXY_PORT in key_to_new_lines: + updated_lines.append(key_to_new_lines.pop(LCM_DEPOT_PROXY_PORT)) + else: + updated_lines.append(line) + + # Add any remaining new lines + for _, new_line in key_to_new_lines.items(): + updated_lines.append(new_line) + + # Move the file pointer to the beginning of the file + f.seek(0) + # Write the updated lines + f.writelines(updated_lines) + # Truncate the file to the new length + f.truncate() + + # Release the lock + fcntl.flock(f, fcntl.LOCK_UN) + + # Restart LCM service + _, _, _ = utils.run_shell_cmd(LCM_SERVICE_RESTART) + + except Exception as e: + errors.append(str(e)) + status = RemediateStatus.FAILED + + return status, errors + + def get(self, context: SDDCManagerContext) -> Tuple[Dict, List[Any]]: + """Get Proxy Configuration from SDDC Manager. + + :param context: Product context instance. + :type context: SDDCManagerContext + :return: Tuple of dict with proxy configuration. + :rtype: Tuple + """ + logger.info("Getting Proxy Configuration.") + sddc_manager_rest_client = context.sddc_manager_rest_client() + # if VCF version 4.5.0.0 and above, use api to get proxy setting + if utils.is_newer_or_same_version(context.product_version, sddc_manager_consts.SDDC_MANAGER_VERSION_4_5_0_0): + return self._get_proxy_setting_by_api(sddc_manager_rest_client) + + # if VCF version lower than 4.5.0.0, get proxy setting from lcp app property file. + return self._get_proxy_setting_by_file(LCM_APP_PROPERTY_FILE) + + def set(self, context: SDDCManagerContext, desired_values) -> Tuple: + """Set Proxy Configuration in SDDC Manager. + + :param context: Product context instance. + :type context: SDDCManagerContext + :param desired_values: Desired value for the Proxy config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + + logger.info("Setting Proxy config for audit.") + sddc_manager_rest_client = context.sddc_manager_rest_client() + # if VCF version 4.5.0.0 and above, use api to get proxy setting + if utils.is_newer_or_same_version(context.product_version, sddc_manager_consts.SDDC_MANAGER_VERSION_4_5_0_0): + return self._set_proxy_setting_by_api(sddc_manager_rest_client, desired_values) + + # if VCF version lower than 4.5.0.0, get proxy setting from lcp app property file. + return self._set_proxy_setting_by_file(LCM_APP_PROPERTY_FILE, desired_values) + + def check_compliance(self, context: SDDCManagerContext, desired_values: Dict) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Product context instance. + :type context: SDDCManagerContext + :param desired_values: Desired values for the specified configuration. + :type desired_values: Any + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.info("Checking compliance.") + + proxy_enabled = desired_values.get(PROXY_ENABLED_DESIRED_VALUE) + if proxy_enabled: + return super().check_compliance(context, desired_values) + + current_values, errors = self.get(context=context) + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + if not current_values.get(PROXY_ENABLED_DESIRED_VALUE): + status = ComplianceStatus.COMPLIANT + else: + status = ComplianceStatus.NON_COMPLIANT + result = { + consts.STATUS: status, + consts.CURRENT: current_values, + consts.DESIRED: desired_values, + } + + return result diff --git a/config_modules_vmware/controllers/sddc_manager/users_groups_roles_config.py b/config_modules_vmware/controllers/sddc_manager/users_groups_roles_config.py new file mode 100644 index 0000000..9f7e117 --- /dev/null +++ b/config_modules_vmware/controllers/sddc_manager/users_groups_roles_config.py @@ -0,0 +1,94 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.sddc_manager_context import SDDCManagerContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.sddc_manager import sddc_manager_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) +ID = "id" +NAME = "name" +ELEMENTS = "elements" +TYPE = "type" +ROLE = "role" +USERS_GROUPS_ROLES_INFO = "users_groups_roles_info" + + +class UsersGroupsRolesConfig(BaseController): + """Class for UsersGroupsRolesSettings config with get and set methods. + | ConfigId - 1605 + | ConfigTitle - Assign least privileges to users and service accounts in SDDC Manager. + """ + + metadata = ControllerMetadata( + name="users_groups_roles", # controller name + path_in_schema="compliance_config.sddc_manager.users_groups_roles", # path in the schema to this controller's definition. + configuration_id="1605", # configuration id as defined in compliance kit. + title="Assign least privileges to users and service accounts in SDDC Manager.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.SDDC_MANAGER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: SDDCManagerContext) -> Tuple[Dict, List[Any]]: + """ + Get UsersGroupsRolesSettings config for audit control. + + :param context: SDDCManagerContext. + :type context: SDDCManagerContext + :return: Tuple of dict with key "users_groups_roles_info" and list of error messages. + :rtype: tuple + """ + logger.debug("Getting UsersGroupsRolesSettings control config for audit.") + sddc_manager_rest_client = context.sddc_manager_rest_client() + users_url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.USERS_URL + roles_url = sddc_manager_rest_client.get_base_url() + sddc_manager_consts.ROLES_URL + errors = [] + try: + users_response = sddc_manager_rest_client.get_helper(users_url) + roles_response = sddc_manager_rest_client.get_helper(roles_url) + + users_groups_roles_response = { + USERS_GROUPS_ROLES_INFO: [ + { + NAME: user.get(NAME), + TYPE: user.get(TYPE), + ROLE: next( + ( + role[NAME] + for role in roles_response.get(ELEMENTS, {}) + if role[ID] == user.get(ROLE).get(ID) + ), + None, + ), + } + for user in users_response.get(ELEMENTS, {}) + ] + } + except Exception as e: + errors.append(str(e)) + users_groups_roles_response = {} + return users_groups_roles_response, errors + + def set(self, context: SDDCManagerContext, desired_values: Dict) -> Tuple: + """ + Set method is not implemented as this control requires user intervention to remediate. + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/__init__.py b/config_modules_vmware/controllers/vcenter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config_modules_vmware/controllers/vcenter/alarm_remote_syslog_failure_config.py b/config_modules_vmware/controllers/vcenter/alarm_remote_syslog_failure_config.py new file mode 100644 index 0000000..4eb2645 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/alarm_remote_syslog_failure_config.py @@ -0,0 +1,166 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.vcenter.utils import vc_alarms_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils.comparator import Comparator +from config_modules_vmware.framework.utils.comparator import ComparatorOptionForList + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ESX_REMOTE_SYSLOG_FAILURE_EVENT = "esx.problem.vmsyslogd.remote.failure" + + +class AlarmRemoteSyslogFailureConfig(BaseController): + """Manage esxi remote syslog failure alarm config with get and set methods. + + | Config Id - 0000 + | Config Title - Configure an alarm to alert on ESXi remote syslog connection. + + Remediation is supported only for creation of alarm. Any update/delete in an alarm is not supported. + """ + + def __init__(self): + super().__init__() + self.comparator_option = ComparatorOptionForList.IDENTIFIER_BASED_COMPARISON + self.instance_key = vc_alarms_utils.ALARM_NAME + + metadata = ControllerMetadata( + name="alarm_esx_remote_syslog_failure", # controller name + path_in_schema="compliance_config.vcenter.alarm_esx_remote_syslog_failure", # path in the schema to this controller's definition. + configuration_id="0000", # configuration id as defined in compliance kit. + title="Configure an alert if an error occurs with the ESXi remote syslog connection.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """ + Get alarms for alarm_esx_remote_syslog_failure event type on vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of list of alarm info and a list of error messages if any. + :rtype: tuple + """ + errors = [] + result = [] + try: + logger.info(f"Get all the alarms with eventId {ESX_REMOTE_SYSLOG_FAILURE_EVENT}.") + content = context.vc_vmomi_client().content + alarm_manager = content.alarmManager + + # Get all alarm definitions (currently there is no support to query an alarm for specific event) + alarm_definitions = alarm_manager.GetAlarm(content.rootFolder) + alarms = [] + + # Fetch details of all the alarms for which any expression within an alarm has eventId : + # ESX_REMOTE_SYSLOG_FAILURE_EVENT + for alarm_def in alarm_definitions: + for expression in alarm_def.info.expression.expression: + if isinstance(expression, vim.alarm.EventAlarmExpression): + if expression.eventTypeId == ESX_REMOTE_SYSLOG_FAILURE_EVENT: + target_type = vc_alarms_utils.get_target_type(expression.objectType) + alarms.append(vc_alarms_utils.get_alarm_details(alarm_def, target_type)) + result = alarms + + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return result, errors + + def set(self, context: VcenterContext, desired_values: List[Dict]) -> Tuple[str, List[Any]]: + """ + Set alarms for alarm_esx_remote_syslog_failure event type on vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: List of dict objects with alarm info for the esx syslog failure alarm configuration. + :type desired_values: list[dict] + :return: Tuple of remediation status and a list of error messages if any. + :rtype: tuple + """ + errors = [] + status = RemediateStatus.SUCCESS + logger.info(f"Set the desired alarms for eventId {ESX_REMOTE_SYSLOG_FAILURE_EVENT}.") + # Iterate over all the alarm in desired list of alarms and create an alarm for each of them. + for desired_alarm_value in desired_values: + alarm_name = desired_alarm_value.get(vc_alarms_utils.ALARM_NAME) + logger.info(f"Create the alarm with name: {alarm_name}.") + try: + content = context.vc_vmomi_client().content + spec = vc_alarms_utils.create_alarm_spec(desired_alarm_value, ESX_REMOTE_SYSLOG_FAILURE_EVENT) + content.alarmManager.CreateAlarm(content.rootFolder, spec) + except vim.fault.DuplicateName: + logger.exception(f"Error creating duplicate alarm {alarm_name}") + status = RemediateStatus.FAILED + errors = [ + f"An alarm with same name '{alarm_name}' already exists.", + "Manual remediation required for an update or deletion.", + "Please either update/delete that alarm manually or choose different alarm name.", + ] + except Exception as e: + logger.exception(f"An error occurred: {e}") + status = RemediateStatus.FAILED + errors = [f"Error during {alarm_name} creation with error {str(e)}."] + return status, errors + + def check_compliance(self, context: VcenterContext, desired_values: List[Dict]) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Vcenter context instance. + :type context: VcenterContext + :param desired_values: List of dict objects with info for the esx remote syslog failure alarm configuration. + :type desired_values: list[dict] + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + current_value, errors = self.get(context=context) + # If errors are seen during get, return "FAILED" status with errors. + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + # Use case is to have at least one alarm with that eventTypeId. + # Numerous alarms can be configured with same eventId. Check compliance should be run against only on the + # list of alarms which are part of desired spec to avoid showing non_compliant due to non-interested alarms. + # Can remove this filtering if there is a use case where user wants to run check compliance for all the alarms. + desired_alarm_names = {item[vc_alarms_utils.ALARM_NAME] for item in desired_values} + filtered_current_values = [ + item for item in current_value if item[vc_alarms_utils.ALARM_NAME] in desired_alarm_names + ] + + # If no errors seen, compare the current and desired value. If not same, return "NON_COMPLIANT" with values. + # Otherwise, return "COMPLIANT". + current_non_compliant_configs, desired_non_compliant_configs = Comparator.get_non_compliant_configs( + filtered_current_values, desired_values, self.comparator_option, self.instance_key + ) + if current_non_compliant_configs or desired_non_compliant_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_non_compliant_configs, + consts.DESIRED: desired_non_compliant_configs, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/vcenter/alarm_sso_config.py b/config_modules_vmware/controllers/vcenter/alarm_sso_config.py new file mode 100644 index 0000000..419c700 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/alarm_sso_config.py @@ -0,0 +1,165 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.vcenter.utils import vc_alarms_utils +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils.comparator import Comparator +from config_modules_vmware.framework.utils.comparator import ComparatorOptionForList + +logger = LoggerAdapter(logging.getLogger(__name__)) + +SSO_EVENT_ID = "com.vmware.sso.PrincipalManagement" + + +class AlarmSSOConfig(BaseController): + """Manage SSO account actions alarm config with get and set methods. + + | Config Id - 1219 + | Config Title - Configure an alert to the appropriate personnel about SSO account actions. + + Remediation is supported only for creation of alarm. Any update/delete in an alarm is not supported. + """ + + def __init__(self): + super().__init__() + self.comparator_option = ComparatorOptionForList.IDENTIFIER_BASED_COMPARISON + self.instance_key = vc_alarms_utils.ALARM_NAME + + metadata = ControllerMetadata( + name="alarm_sso_account_actions", # controller name + path_in_schema="compliance_config.vcenter.alarm_sso_account_actions", # path in the schema to this controller's definition. + configuration_id="1219", # configuration id as defined in compliance kit. + title="Configure an alert to the appropriate personnel about SSO account actions.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """ + Get alarms for SSO account actions on vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of list of alarm info and a list of error messages if any. + :rtype: tuple + """ + errors = [] + result = [] + try: + logger.info(f"Get all the alarms with eventId {SSO_EVENT_ID}.") + content = context.vc_vmomi_client().content + alarm_manager = content.alarmManager + + # Get all alarm definitions (currently there is no support to query an alarm for specific event) + alarm_definitions = alarm_manager.GetAlarm(content.rootFolder) + alarms = [] + + # Fetch details of all the alarms for which any expression within an alarm has eventId : SSO_EVENT_ID + for alarm_def in alarm_definitions: + for expression in alarm_def.info.expression.expression: + if isinstance(expression, vim.alarm.EventAlarmExpression): + if expression.eventTypeId == SSO_EVENT_ID: + target_type = vc_alarms_utils.get_target_type(expression.objectType) + alarms.append(vc_alarms_utils.get_alarm_details(alarm_def, target_type)) + result = alarms + + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return result, errors + + def set(self, context: VcenterContext, desired_values: List[Dict]) -> Tuple[str, List[Any]]: + """ + Set alarms for SSO account actions on vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: List of dict objects with alarm info for the sso account actions alarm configuration. + :type desired_values: list[dict] + :return: Tuple of remediation status and a list of error messages if any. + :rtype: tuple + """ + errors = [] + status = RemediateStatus.SUCCESS + + # Iterate over all the alarm in desired list of alarms and create an alarm for each of them. + for desired_alarm_value in desired_values: + alarm_name = desired_alarm_value.get(vc_alarms_utils.ALARM_NAME) + logger.info(f"Create the alarm with name: {alarm_name}.") + try: + content = context.vc_vmomi_client().content + spec = vc_alarms_utils.create_alarm_spec(desired_alarm_value, SSO_EVENT_ID) + content.alarmManager.CreateAlarm(content.rootFolder, spec) + except vim.fault.DuplicateName: + logger.exception(f"Error creating duplicate alarm {alarm_name}") + status = RemediateStatus.FAILED + errors = [ + f"An alarm with same name '{alarm_name}' already exists.", + "Manual remediation required for an update or deletion.", + "Please either update/delete that alarm manually or choose different alarm name.", + ] + except Exception as e: + logger.exception(f"An error occurred: {e}") + status = RemediateStatus.FAILED + errors = [f"Error during {alarm_name} creation with error {str(e)}."] + return status, errors + + def check_compliance(self, context: VcenterContext, desired_values: List[Dict]) -> Dict: + """Check compliance of current configuration against provided desired values. + + :param context: Vcenter context instance. + :type context: VcenterContext + :param desired_values: List of dict objects with alarm info for the sso account actions alarm configuration. + :type desired_values: list[dict] + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + logger.debug("Checking compliance.") + current_value, errors = self.get(context=context) + # If errors are seen during get, return "FAILED" status with errors. + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + # Use case is to have at least one alarm with that eventTypeId. + # Numerous alarms can be configured with same eventId. Check compliance should be run against only on the + # list of alarms which are part of desired spec to avoid showing non_compliant due to non-interested alarms. + # Can remove this filtering if there is a use case where user wants to run check compliance for all the alarms. + desired_alarm_names = {item[vc_alarms_utils.ALARM_NAME] for item in desired_values} + filtered_current_values = [ + item for item in current_value if item[vc_alarms_utils.ALARM_NAME] in desired_alarm_names + ] + + # If no errors seen, compare the current and desired value. If not same, return "NON_COMPLIANT" with values. + # Otherwise, return "COMPLIANT". + current_non_compliant_configs, desired_non_compliant_configs = Comparator.get_non_compliant_configs( + filtered_current_values, desired_values, self.comparator_option, self.instance_key + ) + if current_non_compliant_configs or desired_non_compliant_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_non_compliant_configs, + consts.DESIRED: desired_non_compliant_configs, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/vcenter/backup_schedule_config.py b/config_modules_vmware/controllers/vcenter/backup_schedule_config.py new file mode 100644 index 0000000..9043585 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/backup_schedule_config.py @@ -0,0 +1,295 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from enum import Enum +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple +from typing import Union + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter import vc_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils +from config_modules_vmware.framework.utils.comparator import Comparator + +logger = LoggerAdapter(logging.getLogger(__name__)) + +DESIRED_KEYS_FOR_AUDIT = [ + "backup_schedule_name", + "enable_backup_schedule", + "backup_location_url", + "backup_server_username", + "backup_parts", + "recurrence_info", + "retention_info", +] + + +class RecurrenceType(Enum): + """Manage recurrence types. + + :meta private: + """ + + DAILY = "DAILY" + WEEKLY = "WEEKLY" + CUSTOM = "CUSTOM" + + +class BackupScheduleConfig(BaseController): + """Manage vCenter back schedule config with get and set methods. + + | Config Id - 1220 + | Config Title - The vCenter Server configuration must be backed up on a regular basis. + + """ + + metadata = ControllerMetadata( + name="backup_schedule_config", # controller name + path_in_schema="compliance_config.vcenter.backup_schedule_config", # path in the schema to this controller's definition. + configuration_id="1220", # configuration id as defined in compliance kit. + title="The vCenter Server configuration must be backed up on a regular basis.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[Dict, List[Any]]: + """Get backup schedule config from vCenter. + + | sample get call output + + .. code-block:: json + + { + "backup_schedule_name": "DailyBackup", + "enable_backup_schedule": true, + "backup_location_url": "sftp://10.0.0.250:/root/backups", + "backup_server_username": "root", + "backup_parts": [ + "seat", + "common" + ], + "recurrence_info": { + "hour": 1, + "minute": 0, + "recurrence_type": "DAILY" + }, + "retention_info": { + "max_count": 5 + } + } + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of dict with key "servers" and list of error messages. + :rtype: tuple + """ + logger.info("Getting Backup schedule config.") + errors = [] + try: + backup_schedule_config = self.__get_backup_schedule(context) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + backup_schedule_config = {} + return backup_schedule_config, errors + + def __get_backup_schedule(self, context: VcenterContext) -> Dict: + """Get backup schedule configs from vCenter + + :param context: Product context instance. + :type context: VcenterContext + :return: Dict of backup schedule config + :rtype: Dict + """ + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + vc_consts.BACKUP_SCHEDULE_URL + backup_schedule_response = vc_rest_client.get_helper(url) + + if not backup_schedule_response: + return {} + + schedule_name, schedule_config = list(backup_schedule_response.items())[0] + backup_schedule_config = { + "backup_schedule_name": schedule_name, + "enable_backup_schedule": schedule_config.get("enable"), + "backup_location_url": schedule_config.get("location"), + "backup_server_username": schedule_config.get("location_user"), + "backup_parts": schedule_config.get("parts"), + "recurrence_info": schedule_config.get("recurrence_info"), + "retention_info": schedule_config.get("retention_info"), + } + # Set recurrence type based on recurrence info + backup_schedule_config["recurrence_info"]["recurrence_type"] = self.__get_recurrence_type( + schedule_config.get("recurrence_info") + ) + return backup_schedule_config + + @staticmethod + def __get_recurrence_type(recurrence_info: Dict) -> Union[str, None]: + """ + Get recurrence type from recurrence info. + + :param recurrence_info: Dict containing recurrence info. + :type recurrence_info: Dict + :return: Recurrence type which can be any of ['DAILY', 'WEEKLY', 'CUSTOM'] + :rtype: str or None + """ + days = recurrence_info.get("days", []) + + if len(days) == 1: + return RecurrenceType.WEEKLY.value + elif len(days) == 0: + return RecurrenceType.DAILY.value + else: + return RecurrenceType.CUSTOM.value + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """Set Backup schedule config for vCenter. + + | Sample desired state for Backup schedule config + + .. code-block:: json + + { + "backup_schedule_name": "DailyBackup", + "enable_backup_schedule": true, + "backup_location_url": "sftp://10.0.0.250:/root/backups", + "backup_server_username": "root", + "backup_server_password": "HFKMo18wrwBh.k.H", + "backup_encryption_password": "HFKMo18wrwBh.k.H", + "backup_parts": [ + "seat", + "common" + ], + "recurrence_info": { + "recurrence_type": "DAILY", + "hour": 1, + "minute": 0 + }, + "retention_info": { + "max_count": 5 + } + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the vCenter backup schedule config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + logger.info("Setting Backup schedule config for remediation.") + + errors = [] + status = RemediateStatus.SUCCESS + try: + self.__set_backup_schedule(context, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def __delete_backup_schedule(self, context: VcenterContext): + """Delete a backup schedule if exists. + + :param context: + :return: + """ + vc_rest_client = context.vc_rest_client() + get_url = vc_rest_client.get_base_url() + vc_consts.BACKUP_SCHEDULE_URL + backup_schedule_config = vc_rest_client.get_helper(get_url) + + if not backup_schedule_config: + return + + if backup_schedule_config and isinstance(backup_schedule_config, dict) and len(backup_schedule_config) == 1: + schedule_name, _ = list(backup_schedule_config.items())[0] + delete_url = vc_rest_client.get_base_url() + vc_consts.BACKUP_SCHEDULE_BY_NAME_URL.format(schedule_name) + vc_rest_client.delete_helper(delete_url) + + def __set_backup_schedule(self, context: VcenterContext, desired_values: Dict): + """Set backup schedule configs in vCenter + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values as dict. + :type desired_values: Dict + :return: None + """ + vc_rest_client = context.vc_rest_client() + # cleanup existing schedules + self.__delete_backup_schedule(context) + + payload = {"schedule": desired_values.get("backup_schedule_name"), "spec": {}} + backup_password = desired_values.get("backup_encryption_password") + if backup_password: + payload["spec"]["backup_password"] = backup_password + + payload["spec"]["enable"] = desired_values.get("enable_backup_schedule") + payload["spec"]["location"] = desired_values.get("backup_location_url") + payload["spec"]["location_user"] = desired_values.get("backup_server_username") + + location_password = desired_values.get("backup_server_password") + if location_password: + payload["spec"]["location_password"] = location_password + + payload["spec"]["parts"] = desired_values.get("backup_parts") + payload["spec"]["recurrence_info"] = desired_values.get("recurrence_info") + del payload["spec"]["recurrence_info"]["recurrence_type"] + payload["spec"]["retention_info"] = desired_values.get("retention_info") + + # create a new schedule + create_schedule_url = vc_rest_client.get_base_url() + vc_consts.BACKUP_SCHEDULE_URL + vc_rest_client.post_helper(create_schedule_url, body=payload) + + def check_compliance(self, context: VcenterContext, desired_values: Dict) -> Dict: + """ + Check compliance of vCenter backup schedules. + + | Password is not considered during compliance check. + | Due to security restrictions we cannot get the current password. But it is still used for remediation. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for vCenter backup schedule config. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + backup_schedule_config, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + filtered_desired_values = utils.filter_dict_keys(desired_values, DESIRED_KEYS_FOR_AUDIT) + + current_non_compliant_configs, desired_configs = Comparator.get_non_compliant_configs( + current_config=backup_schedule_config, desired_config=filtered_desired_values + ) + + if current_non_compliant_configs or desired_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_non_compliant_configs, + consts.DESIRED: desired_configs, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/vcenter/cert_config.py b/config_modules_vmware/controllers/vcenter/cert_config.py new file mode 100644 index 0000000..0bb7ec0 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/cert_config.py @@ -0,0 +1,136 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter import vc_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ISSUER = "issuer" +ISSUER_DN = "issuer_dn" +CERTIFICATE_ISSUER = "certificate_issuer" + + +class CertConfig(BaseController): + """ + Class for cert config with get and set methods. + + | Config Id - 1205 + | Config Title - The vCenter Server Machine SSL certificate must be issued by an appropriate + | certificate authority. + + """ + + metadata = ControllerMetadata( + name="cert_config", # controller name + path_in_schema="compliance_config.vcenter.cert_config", # path in the schema to this controller's definition. + configuration_id="1205", # configuration id as defined in compliance kit. + title="The vCenter Server Machine SSL certificate must be issued by " + "an appropriate certificate authority", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, + # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[Dict, List[Any]]: + """ + Get certificate details of vcenter server for audit. + + :param context: Product context instance. + :type context: VcenterContext + :return: Details of the certificate issuer + :rtype: tuple + """ + logger.info("Getting Certificate details for audit.") + errors = [] + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + vc_consts.CERT_CA_URL + result = {} + try: + # api "api/vcenter/certificate-management/vcenter/tls" to get cert + cert_ca = vc_rest_client.get_helper(url) + # extract issuer info from cert. + issuer = cert_ca.get(ISSUER_DN) + if issuer: + result[ISSUER] = issuer + else: + raise Exception("Unable to fetch issuer details from cert") + except Exception as e: + logger.exception(f"Unable to fetch certificate details {e}") + errors.append(str(e)) + + return result, errors + + def set(self, context: VcenterContext, desired_values) -> Tuple: + """ + Set is not implemented as this control requires manual intervention. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired value for the certificate authority + :type desired_values: String or list of strings + :return: Tuple of status (RemediateStatus.SKIPPED) and errors if any + :rtype: tuple + """ + errors = ["Set is not implemented as this control requires manual intervention"] + status = RemediateStatus.SKIPPED + return status, errors + + def check_compliance(self, context, desired_values) -> Dict: + """ + + Check compliance of configured certificate authority in vCenter server. Certificate issuer details needs + to be provided as shown in the below sample format (can provide multiple certs too).The method will check + if the current certificate details is available in the desired_values and return the compliance + status accordingly. + + | Sample desired_values spec + + .. code-block:: json + + { + "certificate_issuer": + ["OU=VMware Engineering,O=vcenter-1.vrack.vsphere.local,ST=California,C=US,DC=local,DC=vsphere,CN=CB", + "OU=VMware Engineering,O=vcenter-1.vrack.vsphere.local,ST=California,C=US,DC=local,DC=vsphere,CN=CA"] + } + + :param context: Product context instance. + :param desired_values: Desired value for the certificate authority. + :return: Dict of status and current/desired value or errors (for failure). + :rtype: dict + """ + logger.info("Checking compliance") + cert_info, errors = self.get(context=context) + current_value = cert_info.get(ISSUER) + + # If errors are seen during get, return "FAILED" status with errors. + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + if current_value.replace(", ", ",") in desired_values[CERTIFICATE_ISSUER]: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result + + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: current_value, + consts.DESIRED: desired_values, + } + return result diff --git a/config_modules_vmware/controllers/vcenter/datastore_transit_encryption_config.py b/config_modules_vmware/controllers/vcenter/datastore_transit_encryption_config.py new file mode 100644 index 0000000..9dd352f --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/datastore_transit_encryption_config.py @@ -0,0 +1,297 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +DATA_IN_TRANSIT_ENCRYPTION_CONFIG_PYVMOMI_KEY = "dataInTransitEncryptionConfig" +REKEY_INTERVAL_PYVMOMI_KEY = "rekeyInterval" +ENABLED_PYVMOMI_KEY = "enabled" +CLUSTER_NAME_KEY = "cluster_name" +TRANSIT_ENCRYPTION_ENABLED = "transit_encryption_enabled" +REKEY_INTERVAL_KEY = "rekey_interval" +DATA_CENTER_NAME_KEY = "datacenter_name" +PER_HOST_TASK_TIMEOUT = 10 +BASE_TASK_TIMEOUT = 30 + + +class DatastoreTransitEncryptionPolicy(BaseController): + """Manage data in transit encryption policy for vSAN clusters with get and set method. + + | Config Id - 0000 + | Config Title - Configure Data in Transit Encryption Keys to be re-issued at regular intervals + for the vSAN Data in Transit encryption enabled clusters. + + """ + + metadata = ControllerMetadata( + name="vsan_datastore_transit_encryption_config", # controller name + path_in_schema="compliance_config.vcenter.vsan_datastore_transit_encryption_config", + # path in the schema to this controller's definition. + configuration_id="0000", # configuration id as defined in compliance kit. + title="Configure Data in Transit Encryption Keys to be re-issued at regular intervals " + "for the vSAN Data in Transit encryption enabled clusters.", # controller title. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """Get transit encryption policy for all encrypted vSAN based clusters. + + | Note: This control currently operates only on VCF 4411 due to vModl changes between versions 4411 and 5000. + Support for version 5xxx will be added soon. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of List of dicts with transit encryption policy and a list of error messages. + :rtype: Tuple + """ + errors = [] + try: + result = self.__get_rekey_interval_encryption_policy_for_vsan_clusters(context) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def __get_transit_encryption_config_for_clusters(self, context: VcenterContext) -> List[Tuple]: + """Helper method to get all encrypted vSAN clusters and their data in transit encryption configuration. + + :param context: VC product context instance. + :type context: VcenterContext + :return: List of tuple of cluster refs and their transit encryption configurations. + :rtype: List + """ + data_in_transit_encryption_configs = [] + vc_vsan_vmomi_client = context.vc_vsan_vmomi_client() + + vsan_clusters = vc_vsan_vmomi_client.get_all_vsan_enabled_clusters() + logger.info(f"Retrieved all vSAN enabled clusters {vsan_clusters}") + + vsan_ccs = vc_vsan_vmomi_client.get_vsan_cluster_config_system() + logger.info(f"Retrieved vSAN cluster config system {vsan_ccs}") + + for cluster_ref in vsan_clusters: + vsan_cluster_config = vsan_ccs.VsanClusterGetConfig(cluster_ref) + data_in_transit_encryption_config = getattr( + vsan_cluster_config, DATA_IN_TRANSIT_ENCRYPTION_CONFIG_PYVMOMI_KEY, None + ) + if data_in_transit_encryption_config: + data_in_transit_encryption_configs.append((cluster_ref, data_in_transit_encryption_config)) + + logger.debug( + f"Retrieved transit encryption configs for vSAN data in transit encryption enabled" + f" clusters {data_in_transit_encryption_configs}" + ) + return data_in_transit_encryption_configs + + def __get_rekey_interval_encryption_policy_for_vsan_clusters(self, context: VcenterContext) -> List[Dict]: + """Get transit encryption policy for all encryption enabled vSAN clusters. + + :param context: VC product context instance. + :type context: VcenterContext + :return: List of dicts with transit encryption policies for all vSAN encryption enabled clusters. + :rtype: List + """ + all_cluster_transit_encryption_configs = [] + data_in_transit_encryption_configs = self.__get_transit_encryption_config_for_clusters(context) + vc_vmomi_client = context.vc_vmomi_client() + + for cluster_ref, transit_encryption_config in data_in_transit_encryption_configs: + cluster_transit_encryption_config = {} + is_enabled = getattr(transit_encryption_config, ENABLED_PYVMOMI_KEY, None) + rekey_interval = getattr(transit_encryption_config, REKEY_INTERVAL_PYVMOMI_KEY, None) + datacenter_obj = vc_vmomi_client.find_datacenter_for_obj(cluster_ref) + data_center_name = getattr(datacenter_obj, "name", "") + cluster_transit_encryption_config[DATA_CENTER_NAME_KEY] = data_center_name + cluster_transit_encryption_config[CLUSTER_NAME_KEY] = cluster_ref.name + cluster_transit_encryption_config[TRANSIT_ENCRYPTION_ENABLED] = is_enabled + cluster_transit_encryption_config[REKEY_INTERVAL_KEY] = rekey_interval + all_cluster_transit_encryption_configs.append(cluster_transit_encryption_config) + + logger.info( + f"Retrieved transit encryption configs for all vSAN data in transit encryption enabled" + f" clusters {all_cluster_transit_encryption_configs}" + ) + return all_cluster_transit_encryption_configs + + def __set_transit_encryption_config_for_vsan_clusters(self, context: VcenterContext, desired_values: dict) -> None: + """Set transit encryption policy for all vSAN enabled clusters. + + :param context: VC product context. + :type context: VcenterContext + :param desired_values: Desired values for transit encryption policy. + :type desired_values: dict + :return: None. + """ + vc_vsan_vmomi_client = context.vc_vsan_vmomi_client() + vc_vmomi_client = context.vc_vmomi_client() + + data_in_transit_encryption_configs = self.__get_transit_encryption_config_for_clusters(context) + vsan_ccs = vc_vsan_vmomi_client.get_vsan_cluster_config_system() + + for cluster_ref, transit_encryption_config in data_in_transit_encryption_configs: + current_is_enabled = getattr(transit_encryption_config, ENABLED_PYVMOMI_KEY, None) + current_rekey_interval = getattr(transit_encryption_config, REKEY_INTERVAL_PYVMOMI_KEY, None) + + desired_is_enabled = desired_values.get(TRANSIT_ENCRYPTION_ENABLED) + desired_rekey_interval = desired_values.get(REKEY_INTERVAL_KEY) + if ( + current_is_enabled is not None + and current_rekey_interval is not None + and current_is_enabled == desired_is_enabled + and current_rekey_interval == desired_rekey_interval + ): + logger.info( + f"Cluster {cluster_ref.name} already has desired transmit encryption configs" + f" is_enabled {current_is_enabled} rekey_interval {current_rekey_interval}" + ) + else: + # Reconfig spec + cluster_reconfig_spec = vim.vsan.ReconfigSpec() + cluster_reconfig_spec.dataInTransitEncryptionConfig = vim.vsan.DataInTransitEncryptionConfig() + cluster_reconfig_spec.dataInTransitEncryptionConfig.enabled = desired_is_enabled + cluster_reconfig_spec.dataInTransitEncryptionConfig.rekeyInterval = desired_rekey_interval + # reconfigure vSAN cluster + encryption_config_task = vsan_ccs.ReconfigureEx(cluster_ref, cluster_reconfig_spec) + vc_task = vc_vsan_vmomi_client.convert_vsan_to_vc_task(encryption_config_task) + # Set timeout based on number of hosts in cluster + task_timeout = len(cluster_ref.host) * PER_HOST_TASK_TIMEOUT if cluster_ref.host else BASE_TASK_TIMEOUT + vc_vmomi_client.wait_for_task(vc_task, timeout=task_timeout) + + def set(self, context: VcenterContext, desired_values: dict) -> Tuple[str, List[Any]]: + """Set transit encryption policy for encryption enabled vSAN clusters. + + | Note: This control currently operates only on VCF 4411 due to vModl changes between versions 4411 and 5000. + Support for version 5xxx will be added soon. + + | Sample desired state for transit encryption policy. Rekey interval range lies + between 30 minutes - 10080 (7 days). + + .. code-block:: json + + { + "rekey_interval": 30, + "transit_encryption_enabled": true + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for transit encryption policy. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + errors = [] + status = RemediateStatus.SUCCESS + try: + self.__set_transit_encryption_config_for_vsan_clusters(context, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def check_compliance(self, context: VcenterContext, desired_values: dict) -> Dict: + """Check transit encryption policy compliance for encrypted vSAN enabled clusters. + + | Note: This control currently operates only on VCF 4411 due to vModl changes between versions 4411 and 5000. + Support for version 5xxx will be added soon. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for transit encryption policy. + :type desired_values: dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + cluster_rekey_policies, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_configs = [ + config + for config in cluster_rekey_policies + if config.get(REKEY_INTERVAL_KEY) != desired_values.get(REKEY_INTERVAL_KEY) + or config.get(TRANSIT_ENCRYPTION_ENABLED) != desired_values.get(TRANSIT_ENCRYPTION_ENABLED) + ] + + if non_compliant_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_configs, + consts.DESIRED: desired_values, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result + + def remediate(self, context: VcenterContext, desired_values: dict) -> Dict: + """Remediate transit encryption policy drifts on encryption enabled vSAN based clusters. + + | Note: This control currently operates only on VCF 4411 due to vModl changes between versions 4411 and 5000. + Support for version 5xxx will be added soon. + + | Sample desired state for transit encryption policy. Rekey interval range + lies between 30 minutes - 10080 (7 days). + + .. code-block:: json + + { + "rekey_interval": 30, + "transit_encryption_enabled": true + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for transit encryption policy. + :type desired_values: dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Running remediation") + cluster_rekey_policies, errors = self.get(context=context) + + if errors: + return {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + + non_compliant_configs = [ + config + for config in cluster_rekey_policies + if config.get(REKEY_INTERVAL_KEY) != desired_values.get(REKEY_INTERVAL_KEY) + or config.get(TRANSIT_ENCRYPTION_ENABLED) != desired_values.get(TRANSIT_ENCRYPTION_ENABLED) + ] + + if not non_compliant_configs: + return {consts.STATUS: RemediateStatus.SUCCESS} + + status, errors = self.set(context=context, desired_values=desired_values) + + if not errors: + result = {consts.STATUS: status, consts.OLD: non_compliant_configs, consts.NEW: desired_values} + else: + result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + return result diff --git a/config_modules_vmware/controllers/vcenter/datastore_unique_name_policy.py b/config_modules_vmware/controllers/vcenter/datastore_unique_name_policy.py new file mode 100644 index 0000000..cc09308 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/datastore_unique_name_policy.py @@ -0,0 +1,150 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_client import VcVmomiClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +DATASTORE_TYPE = "vsan" +NON_COMPLIANT_DATASTORE_NAME = "vsanDatastore" +DATASTORE_NAME_KEY = "datastore_name" +CLUSTER_NAME_KEY = "cluster_name" +DATACENTER_NAME_KEY = "datacenter_name" + + +class DatastoreUniqueNamePolicy(BaseController): + """Manage vSAN datastore name uniqueness with get and set methods. + + | Config Id - 420 + | Config Title - The vCenter Server must configure the vSAN Datastore name to a unique name. + + """ + + metadata = ControllerMetadata( + name="vsan_datastore_naming_policy", # controller name + path_in_schema="compliance_config.vcenter.vsan_datastore_naming_policy", # path in the schema to this controller's definition. + configuration_id="420", # configuration id as defined in compliance kit. + title="The vCenter Server must configure the vSAN Datastore name to a unique name.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """Get all vSAN datastore info. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of List of dicts with vSAN datastore info and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + try: + result = self.__get_all_vsan_enabled_datastore_config(vc_vmomi_client) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + @staticmethod + def __get_all_vsan_enabled_datastore_config(vc_vmomi_client: VcVmomiClient) -> List[Dict]: + """Get all vSAN enabled datastore info from vCenter. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of dicts with datastore configs. + :rtype: List + """ + vsan_datastore_configs = [] + all_datacenter_refs = vc_vmomi_client.get_objects_by_vimtype(vim.Datacenter) + logger.info(f"All datacenter mo-refs in vCenter {all_datacenter_refs}") + + for datacenter in all_datacenter_refs: + if hasattr(datacenter, "datastore"): + for datastore in datacenter.datastore: + if hasattr(datastore, "summary") and getattr(datastore, "summary"): + if hasattr(datastore.summary, "type") and getattr(datastore.summary, "type") == DATASTORE_TYPE: + if hasattr(datastore, "host") and datastore.host: + vsan_datastore_configs.append( + { + DATACENTER_NAME_KEY: getattr(datacenter, "name", ""), + CLUSTER_NAME_KEY: getattr(datastore.host[0].key.parent, "name", ""), + DATASTORE_NAME_KEY: getattr(datastore, "name", ""), + } + ) + logger.info(f"Retrieved vSAN enabled cluster configs {vsan_datastore_configs}") + return vsan_datastore_configs + + def set(self, context: VcenterContext, desired_values: bool) -> Tuple[str, List[Any]]: + """Set will not be implemented until we have a proper remediation workflow is in place with fail safes + and rollback mechanism. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: When set to true (the only allowed value), the audit process flags a datastore + as non-compliant only if its name is 'vsanDatastore.' No other names are checked for compliance. + :type desired_values: bool + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors + + def check_compliance(self, context: VcenterContext, desired_values: bool) -> Dict: + """Check compliance of datastore names among all vSAN-based datastores. + + | The audit process flags a datastore as non-compliant only if its name is 'vsanDatastore.' + No other names are checked for compliance. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: When set to true (the only allowed value), the audit process flags a datastore as + non-compliant only if its name is 'vsanDatastore.' No other names are checked for compliance. + :type desired_values: bool + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + vsan_datastore_configs, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_datastore_configs = [ + config + for config in vsan_datastore_configs + if config.get(DATASTORE_NAME_KEY) == NON_COMPLIANT_DATASTORE_NAME + ] + + if non_compliant_datastore_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_datastore_configs, + consts.DESIRED: desired_values, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/vcenter/dns_config.py b/config_modules_vmware/controllers/vcenter/dns_config.py new file mode 100644 index 0000000..8457d1b --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/dns_config.py @@ -0,0 +1,131 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.vcenter import vc_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class DnsConfig(BaseController): + """Manage DNS config with get and set methods. + + | Config Id - 1271 + | Config Title - DNS should be configured to a global value that is enforced by vCenter. + + """ + + metadata = ControllerMetadata( + name="dns", # controller name + path_in_schema="compliance_config.vcenter.dns", # path in the schema to this controller's definition. + configuration_id="1271", # configuration id as defined in compliance kit. + title="DNS should be configured to a global value that is enforced by vCenter.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """ + Get DNS config from vCenter. + + | Sample get call output + + .. code-block:: json + + { + "mode": "is_static", + "servers": ["8.8.8.8", "1.1.1.1"] + } + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of dict with keys "servers" and "mode" and a list of error messages if any. + :rtype: tuple + """ + logger.info("Getting DNS control config for audit.") + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + vc_consts.DNS_URL + errors = [] + try: + dns_servers = vc_rest_client.get_helper(url) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + dns_servers = {"servers": [], "mode": ""} + return dns_servers, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Sets list of servers and DNS mode. + + | Sample desired state for DNS + + .. code-block:: json + + { + "mode": "is_static", + "servers": ["8.8.8.8", "1.1.1.1"] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the DNS config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting DNS control config for audit.") + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + vc_consts.DNS_URL + payload = {"mode": desired_values.get("mode"), "servers": desired_values.get("servers")} + errors = [] + status = RemediateStatus.SUCCESS + try: + vc_rest_client.put_helper(url, body=payload, raise_for_status=True) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def check_compliance(self, context, desired_values: Dict) -> Dict: + """Check compliance of current DNS configuration in vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the DNS config. + :type desired_values: dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + dns_desired_value = {"servers": desired_values.get("servers", []), "mode": desired_values.get("mode")} + return super().check_compliance(context, desired_values=dns_desired_value) + + def remediate(self, context: BaseContext, desired_values: Dict) -> Dict: + """Remediate DNS configuration drifts in vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the DNS config. + :type desired_values: Any + :return: Dict of status and old/new values(for success) or errors (for failure). + :rtype: dict + """ + dns_desired_value = {"servers": desired_values.get("servers", []), "mode": desired_values.get("mode")} + return super().remediate(context, desired_values=dns_desired_value) diff --git a/config_modules_vmware/controllers/vcenter/dv_pg_forged_transmits_policy.py b/config_modules_vmware/controllers/vcenter/dv_pg_forged_transmits_policy.py new file mode 100644 index 0000000..b48a89a --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/dv_pg_forged_transmits_policy.py @@ -0,0 +1,294 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.vcenter.utils.vc_port_group_utils import ( + get_all_non_uplink_non_nsx_port_group_and_security_configs, +) +from config_modules_vmware.controllers.vcenter.utils.vc_port_group_utils import ( + get_non_compliant_security_policy_configs, +) +from config_modules_vmware.controllers.vcenter.utils.vc_port_group_utils import PortGroupSecurityConfigEnum +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_client import VcVmomiClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +# constants +DESIRED_KEY = "allow_forged_transmits" +SWITCH_NAME = "switch_name" +PORT_GROUP_NAME = "port_group_name" +NSX_BACKING_TYPE = "nsx" +GLOBAL = "__GLOBAL__" +OVERRIDES = "__OVERRIDES__" + + +class DVPortGroupForgedTransmitsPolicy(BaseController): + """Manage DV Port group Forged Transmits policy with get and set methods. + + | Config Id - 450 + | Config Title - The vCenter Server must set the distributed port group Forged Transmits policy to reject. + + """ + + metadata = ControllerMetadata( + name="dvpg_forged_transmits_policy", # controller name + path_in_schema="compliance_config.vcenter.dvpg_forged_transmits_policy", # path in the schema to this controller's definition. + configuration_id="450", # configuration id as defined in compliance kit. + title="The vCenter Server must set the distributed port group " + "Forged Transmits policy to reject.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """ + Get DV Port group Forged Transmits policy for all port groups. + + | Sample get call output + + .. code-block:: json + + [ + { + "switch_name": "SwitchB", + "port_group_name": "dv_pg_PortGroup3", + "allow_forged_transmits": false + }, + { + "switch_name": "SwitchC", + "port_group_name": "dv_pg_PortGroup1", + "allow_forged_transmits": true + }, + { + "switch_name": "SwitchA", + "port_group_name": "dv_pg_PortGroup2", + "allow_forged_transmits": false + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of list of port group and their Forged Transmits policy and a list of error messages. + :rtype: tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + try: + result = self.__get_all_dv_port_forged_transmit_policy(vc_vmomi_client) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple: + """Set DV Port group Forged Transmits policy for all port groups except uplink port groups. + + | Recommended DV port group Forged Transmits policy: false | reject. + | Sample desired state + + .. code-block:: json + + { + "__GLOBAL__": { + "allow_forged_transmits": false + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch-A", + "port_group_name": "dv_pg_PortGroup1", + "allow_forged_transmits": true + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the enabling or disabling Forged Transmits policy on port groups. + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + self.__set_forged_transmit_policy_for_non_compliant_dv_port_groups(vc_vmomi_client, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def __get_all_dv_port_forged_transmit_policy(self, vc_vmomi_client: VcVmomiClient) -> List: + """Get all non-uplink DV Port groups and their Forged Transmits policies. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of Forged transmit policies for all DV port groups. + :rtype: List + """ + non_uplink_non_nsx_dv_pgs = get_all_non_uplink_non_nsx_port_group_and_security_configs( + vc_vmomi_client, PortGroupSecurityConfigEnum.FORGED_TRANSMITS + ) + + forged_transmit_policies = [] + for dv_pg, forged_transmits_config in non_uplink_non_nsx_dv_pgs: + has_switch_name_config = hasattr(dv_pg.config, "distributedVirtualSwitch") and hasattr( + dv_pg.config.distributedVirtualSwitch, "name" + ) + forged_transmit_policies.append( + { + SWITCH_NAME: dv_pg.config.distributedVirtualSwitch.name if has_switch_name_config else "", + PORT_GROUP_NAME: dv_pg.name, + DESIRED_KEY: forged_transmits_config, + } + ) + return forged_transmit_policies + + def __set_forged_transmit_policy_for_non_compliant_dv_port_groups( + self, vc_vmomi_client: VcVmomiClient, desired_values: Dict + ) -> None: + """Set Forged Transmits policy for non-compliant DV port groups, skipping all uplink port groups. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :param desired_values: Desired values for Forged Transmits policy. + :type desired_values: Dict + :return: + :rtype: None + """ + desired_global_forged_transmit_policy_value = desired_values.get(GLOBAL, {}).get(DESIRED_KEY) + overrides = desired_values.get(OVERRIDES, []) + non_uplink_non_nsx_dv_pgs = get_all_non_uplink_non_nsx_port_group_and_security_configs( + vc_vmomi_client, PortGroupSecurityConfigEnum.FORGED_TRANSMITS + ) + + for dv_pg, current_forged_transmits_config in non_uplink_non_nsx_dv_pgs: + dv_switch_name = getattr(dv_pg.config.distributedVirtualSwitch, "name") + port_group_name = getattr(dv_pg, "name") + + # Check if there are overrides for the current DV Port group + override_forged_transmit_policy = next( + ( + override.get(DESIRED_KEY) + for override in overrides + if override[SWITCH_NAME] == dv_switch_name and override[PORT_GROUP_NAME] == port_group_name + ), + None, + ) + desired_forged_transmit_policy = ( + override_forged_transmit_policy + if override_forged_transmit_policy is not None + else desired_global_forged_transmit_policy_value + ) + # re-configure only if current and desired values are not equal + if current_forged_transmits_config != desired_forged_transmit_policy: + config_spec = vim.dvs.DistributedVirtualPortgroup.ConfigSpec() + config_spec.configVersion = dv_pg.config.configVersion + config_spec.defaultPortConfig = vim.dvs.VmwareDistributedVirtualSwitch.VmwarePortConfigPolicy() + config_spec.defaultPortConfig.securityPolicy = vim.dvs.VmwareDistributedVirtualSwitch.SecurityPolicy() + config_spec.defaultPortConfig.securityPolicy.forgedTransmits = vim.BoolPolicy( + value=desired_forged_transmit_policy + ) + logger.info( + f"Setting Forged transmits policy {desired_forged_transmit_policy}" f" on port group {dv_pg.name}" + ) + task = dv_pg.ReconfigureDVPortgroup_Task(spec=config_spec) + vc_vmomi_client.wait_for_task(task=task) + + def check_compliance(self, context: VcenterContext, desired_values: Dict) -> Dict: + """Check compliance of all non-uplink dv port groups Forged Transmits policy. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for Forged Transmits policy. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + forged_transmits_policy_configs, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_configs, desired_configs = get_non_compliant_security_policy_configs( + forged_transmits_policy_configs, desired_values, DESIRED_KEY + ) + + if non_compliant_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_configs, + consts.DESIRED: desired_configs, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result + + def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: + """Remediate configuration drifts on all non-uplink DV port groups. + + | Sample desired state for remediation. + + .. code-block:: json + + { + "__GLOBAL__": { + "allow_forged_transmits": false + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch-A", + "port_group_name": "dv_pg_PortGroup1", + "allow_forged_transmits": true + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for Forged Transmits policy. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Running remediation") + result = self.check_compliance(context, desired_values) + + if result[consts.STATUS] == ComplianceStatus.COMPLIANT: + return {consts.STATUS: RemediateStatus.SUCCESS} + elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: + non_compliant_configs = result[consts.CURRENT] + desired_configs = result[consts.DESIRED] + else: + errors = result[consts.ERRORS] + return {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + + status, errors = self.set(context=context, desired_values=desired_values) + + if not errors: + result = {consts.STATUS: status, consts.OLD: non_compliant_configs, consts.NEW: desired_configs} + else: + result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + return result diff --git a/config_modules_vmware/controllers/vcenter/dv_pg_mac_address_change_policy.py b/config_modules_vmware/controllers/vcenter/dv_pg_mac_address_change_policy.py new file mode 100644 index 0000000..ddafddd --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/dv_pg_mac_address_change_policy.py @@ -0,0 +1,301 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.vcenter.utils.vc_port_group_utils import ( + get_all_non_uplink_non_nsx_port_group_and_security_configs, +) +from config_modules_vmware.controllers.vcenter.utils.vc_port_group_utils import ( + get_non_compliant_security_policy_configs, +) +from config_modules_vmware.controllers.vcenter.utils.vc_port_group_utils import PortGroupSecurityConfigEnum +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_client import VcVmomiClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +# constants +DESIRED_KEY = "allow_mac_address_change" +NSX_BACKING_TYPE = "nsx" +SWITCH_NAME = "switch_name" +PORT_GROUP_NAME = "port_group_name" +GLOBAL = "__GLOBAL__" +OVERRIDES = "__OVERRIDES__" + + +class DVPortGroupMacAddressChangePolicy(BaseController): + """Manage DV Port group MAC address change policy with get and set methods. + + | Config Id - 407 + | Config Title - The vCenter Server must set the distributed port group MAC Address Change policy to reject. + + """ + + metadata = ControllerMetadata( + name="dvpg_mac_address_change_policy", # controller name + path_in_schema="compliance_config.vcenter.dvpg_mac_address_change_policy", # path in the schema to this controller's definition. + configuration_id="407", # configuration id as defined in compliance kit. + title="The vCenter Server must set the distributed port group MAC Address Change policy to reject.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """ + Get DV Port group MAC address change policy for all port groups. + + | Sample get call output + + .. code-block:: json + + [ + { + "switch_name": "SwitchB", + "port_group_name": "dv_pg_PortGroup3", + "allow_mac_address_change": false + }, + { + "switch_name": "SwitchC", + "port_group_name": "dv_pg_PortGroup1", + "allow_mac_address_change": true + }, + { + "switch_name": "SwitchA", + "port_group_name": "dv_pg_PortGroup2", + "allow_mac_address_change": false + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of list of port group and their MAC address change policy and a list of error messages. + :rtype: tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + try: + result = self.__get_all_dv_port_mac_address_change_policy(vc_vmomi_client) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple: + """ + Set DV Port group MAC address change policy for all port groups. + + | Recommended DV port group MAC address change policy: false | reject. + | Sample desired state + + .. code-block:: json + + { + "__GLOBAL__": { + "allow_mac_address_change": false + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch-A", + "port_group_name": "dv_pg_PortGroup1", + "allow_mac_address_change": true + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the enabling or disabling MAC address change policy on port groups. + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + self.__set_mac_address_change_policy_for_non_compliant_dv_port_groups(vc_vmomi_client, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def __get_all_dv_port_mac_address_change_policy(self, vc_vmomi_client: VcVmomiClient) -> List: + """ + Get all DV Port groups and their MAC address change policies. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of MAC address change policies for all DV port groups. + :rtype: List + """ + all_dv_port_groups = get_all_non_uplink_non_nsx_port_group_and_security_configs( + vc_vmomi_client, PortGroupSecurityConfigEnum.MAC_CHANGES + ) + + mac_address_change_policies = [] + for dv_pg, mac_address_change_config in all_dv_port_groups: + has_switch_name_config = hasattr(dv_pg.config, "distributedVirtualSwitch") and hasattr( + dv_pg.config.distributedVirtualSwitch, "name" + ) + mac_address_change_policies.append( + { + SWITCH_NAME: dv_pg.config.distributedVirtualSwitch.name if has_switch_name_config else "", + PORT_GROUP_NAME: dv_pg.name, + DESIRED_KEY: mac_address_change_config, + } + ) + + return mac_address_change_policies + + def __set_mac_address_change_policy_for_non_compliant_dv_port_groups( + self, vc_vmomi_client: VcVmomiClient, desired_values: Dict + ) -> None: + """ + Set MAC address change policy for all non-compliant DV port groups. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :param desired_values: Desired values for MAC address change policy. + :type desired_values: Dict + :return: + :rtype: None + """ + desired_global_mac_address_change_value = desired_values.get(GLOBAL, {}).get(DESIRED_KEY) + overrides = desired_values.get(OVERRIDES, []) + all_dv_port_groups = get_all_non_uplink_non_nsx_port_group_and_security_configs( + vc_vmomi_client, PortGroupSecurityConfigEnum.MAC_CHANGES + ) + + for dv_pg, current_mac_address_change_policy in all_dv_port_groups: + dv_switch_name = getattr(dv_pg.config.distributedVirtualSwitch, "name") + port_group_name = getattr(dv_pg, "name") + + # Check if there are overrides for the current DV Port group + override_mac_address_change = next( + ( + override.get(DESIRED_KEY) + for override in overrides + if override[SWITCH_NAME] == dv_switch_name and override[PORT_GROUP_NAME] == port_group_name + ), + None, + ) + desired_mac_address_change_policy = ( + override_mac_address_change + if override_mac_address_change is not None + else desired_global_mac_address_change_value + ) + # re-configure only if current and desired values are not equal + if current_mac_address_change_policy != desired_mac_address_change_policy: + config_spec = vim.dvs.DistributedVirtualPortgroup.ConfigSpec() + config_spec.configVersion = dv_pg.config.configVersion + config_spec.defaultPortConfig = vim.dvs.VmwareDistributedVirtualSwitch.VmwarePortConfigPolicy() + config_spec.defaultPortConfig.securityPolicy = vim.dvs.VmwareDistributedVirtualSwitch.SecurityPolicy() + config_spec.defaultPortConfig.securityPolicy.macChanges = vim.BoolPolicy( + value=desired_mac_address_change_policy + ) + logger.info( + f"Setting MAC address change policy {desired_mac_address_change_policy}" + f" on port group {dv_pg.name}" + ) + task = dv_pg.ReconfigureDVPortgroup_Task(spec=config_spec) + vc_vmomi_client.wait_for_task(task=task) + + def check_compliance(self, context: VcenterContext, desired_values: Dict) -> Dict: + """ + Check compliance of all dv port group's MAC address change policy. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for MAC address change policy. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + mac_address_change_policy_configs, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_configs, desired_configs = get_non_compliant_security_policy_configs( + mac_address_change_policy_configs, desired_values, DESIRED_KEY + ) + + if non_compliant_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_configs, + consts.DESIRED: desired_configs, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result + + def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: + """ + Remediate configuration drifts. + + | Sample desired state for remediation. + + .. code-block:: json + + { + "__GLOBAL__": { + "allow_mac_address_change": false + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch-A", + "port_group_name": "dv_pg_PortGroup1", + "allow_mac_address_change": true + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for MAC address change policy. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Running remediation") + result = self.check_compliance(context, desired_values) + + if result[consts.STATUS] == ComplianceStatus.COMPLIANT: + return {consts.STATUS: RemediateStatus.SUCCESS} + elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: + non_compliant_configs = result[consts.CURRENT] + desired_configs = result[consts.DESIRED] + else: + errors = result[consts.ERRORS] + return {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + + status, errors = self.set(context=context, desired_values=desired_values) + + if not errors: + result = {consts.STATUS: status, consts.OLD: non_compliant_configs, consts.NEW: desired_configs} + else: + result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + return result diff --git a/config_modules_vmware/controllers/vcenter/dv_pg_native_vlan_exclusion_policy.py b/config_modules_vmware/controllers/vcenter/dv_pg_native_vlan_exclusion_policy.py new file mode 100644 index 0000000..a6e821a --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/dv_pg_native_vlan_exclusion_policy.py @@ -0,0 +1,271 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_client import VcVmomiClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +# constants +VLAN_KEY = "vlan" +DESIRED_KEY = "native_vlan_id_to_exclude" +SWITCH_NAME_KEY = "switch_name" +PORT_GROUP_NAME_KEY = "port_group_name" +NSX_BACKING_TYPE = "nsx" + + +class DVPortGroupNativeVlanExclusionConfig(BaseController): + """Manage DV Port group Native Vlan exclusion config with get and set methods. + + | Config Id - 1201 + | Config Title - Configure all port groups to a value different from the value of the native VLAN. + + """ + + metadata = ControllerMetadata( + name="dvpg_excluded_native_vlan_policy", # controller name + path_in_schema="compliance_config.vcenter.dvpg_excluded_native_vlan_policy", # path in the schema to this + # controller's definition. + configuration_id="1201", # configuration id as defined in compliance kit. + title="Configure all port groups to a value different from the value of the native VLAN.", # controller title + # as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in + # ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """Get DV Port group Native Vlan exclusion config for all applicable port groups. + + | Sample get call output + + .. code-block:: json + + [ + { + "switch_name": "DSwitch-test", + "port_group_name": "DPortGroup-test", + "vlan": 1 + }, + { + "switch_name": "DSwitch-test", + "port_group_name": "DPortGroup", + "vlan": ["1-100", "105", "200-250"] + }, + { + "switch_name": "SDDC-Dswitch-Private", + "port_group_name": "SDDC-DPortGroup-vMotion", + "vlan": 1 + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of list of port group and their vlan configs and a list of error messages. + :rtype: tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + try: + result = self.__get_all_dv_port_vlan_configs(vc_vmomi_client) + logger.debug( + f"Retrieved DV Port group Native Vlan exclusion config for all applicable port groups" f" {result}" + ) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple: + """Set vlan config for DV port groups excluding native vlan in the configuration. + + | Sample desired state + + .. code-block:: json + + { + "native_vlan_id_to_exclude": 1 + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values containing native vlan id to be excluded from port group configurations. + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + logger.info(consts.REMEDIATION_SKIPPED_MESSAGE) + status = RemediateStatus.SKIPPED + return status, errors + + def __get_vlan_config_for_non_nsx_non_uplink_dv_port_groups(self, vc_vmomi_client: VcVmomiClient) -> List[Tuple]: + """Helper function to retrieve vlan configurations for non-nsx and non-uplink dv port groups. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of tuple with non-nsx, non-uplink dv_pg_refs and their vlan configurations. + :rtype: List + """ + vlan_config_non_nsx_non_uplink_dv_port_group_refs = [] + all_dv_port_group_refs = vc_vmomi_client.get_objects_by_vimtype(vim.DistributedVirtualPortgroup) + logger.info(f"Retrieved DV port groups {all_dv_port_group_refs}") + + for dv_pg in all_dv_port_group_refs: + is_uplink_port_group = hasattr(dv_pg.config, "uplink") and getattr(dv_pg.config, "uplink") + is_nsx_backed = getattr(dv_pg.config, "backingType", "") == NSX_BACKING_TYPE + # skip all uplink and nsx backed port groups + if not is_uplink_port_group and not is_nsx_backed: + vlan_config = getattr(getattr(dv_pg.config, "defaultPortConfig", None), "vlan", None) + vlan_config_non_nsx_non_uplink_dv_port_group_refs.append((dv_pg, vlan_config)) + return vlan_config_non_nsx_non_uplink_dv_port_group_refs + + def __is_native_vlan_in_range(self, vlan_range: vim.NumericRange, native_vlan_id): + """Check if a given native vlan value lies in range of vlan trunk range. + + :param vlan_range: vLan range configured for a dv port group. + :type vlan_range:vim.NumericRange + :param native_vlan_id: native vlan id to check if it lies within given vlan range + :type native_vlan_id: int + :return: + """ + logger.info(f"Check if native vlan is part of vlan range {vlan_range}") + start = vlan_range.start + end = vlan_range.end + if start <= native_vlan_id <= end: + return True + return False + + def __get_all_dv_port_vlan_configs(self, vc_vmomi_client: VcVmomiClient) -> List: + """Get all non-nsx, non-uplink DV Port groups and their vlan configurations. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of tuple of + :rtype: List + """ + non_nsx_non_uplink_dv_port_groups = self.__get_vlan_config_for_non_nsx_non_uplink_dv_port_groups( + vc_vmomi_client + ) + logger.info(f"Retrieved Non-NSX & Non-uplink port group refs {non_nsx_non_uplink_dv_port_groups}") + + dv_pg_vlan_configs = [] + for dv_pg_ref, vlan_config in non_nsx_non_uplink_dv_port_groups: + port_group_vlan_config = {} + has_switch_name_config = hasattr(dv_pg_ref.config, "distributedVirtualSwitch") and hasattr( + dv_pg_ref.config.distributedVirtualSwitch, "name" + ) + port_group_vlan_config[SWITCH_NAME_KEY] = ( + dv_pg_ref.config.distributedVirtualSwitch.name if has_switch_name_config else "" + ) + port_group_vlan_config[PORT_GROUP_NAME_KEY] = dv_pg_ref.name + + # check vlan type + is_vlan_type = isinstance(vlan_config, vim.dvs.VmwareDistributedVirtualSwitch.VlanIdSpec) + is_vlan_trunk_type = isinstance(vlan_config, vim.dvs.VmwareDistributedVirtualSwitch.TrunkVlanSpec) + is_pvt_vlan_type = isinstance(vlan_config, vim.dvs.VmwareDistributedVirtualSwitch.PvlanSpec) + + if is_vlan_type: + port_group_vlan_config[VLAN_KEY] = getattr(vlan_config, "vlanId") + elif is_vlan_trunk_type: + trunk_vlan_range = [] + vlan_ranges = vlan_config.vlanId + for vlan_range in vlan_ranges: + start = vlan_range.start + end = vlan_range.end + if start == end: + trunk_vlan_range.append(str(start)) + else: + trunk_vlan_range.append(f"{start}-{end}") + port_group_vlan_config[VLAN_KEY] = trunk_vlan_range + elif is_pvt_vlan_type: + port_group_vlan_config[VLAN_KEY] = getattr(vlan_config, "pvlanId") + dv_pg_vlan_configs.append(port_group_vlan_config) + logger.info(f"Retrieved vlan configs for non-nsx & non-uplink port groups {dv_pg_vlan_configs}") + return dv_pg_vlan_configs + + def __get_non_compliant_vlan_configs(self, dv_pg_vlan_configs: List, desired_values: dict) -> List: + """Get list of dv port groups with vlan configurations overlapping with native vlan id. + + :param dv_pg_vlan_configs: List of non-uplink, non-nsx port groups. + :type dv_pg_vlan_configs: List + :param desired_values: Dict containing Native VLAN ID to be excluded from port group configurations. + :type desired_values: Dict + :return: List of non-compliant vlan configs + :rtype: List + """ + non_compliant_dv_port_group_configs = [] + native_vlan_to_exclude = desired_values.get(DESIRED_KEY) + for dv_pg_vlan_config in dv_pg_vlan_configs: + vlan_config = dv_pg_vlan_config[VLAN_KEY] + # vlan trunk spec will be in list format Ex:["1-200", "205", "300-350"] + if isinstance(vlan_config, List): + for vlan_range in vlan_config: + # Trunk vlan can have ranges like "1-100" + if isinstance(vlan_range, str) and "-" in vlan_range: + ranges = vlan_range.split("-") + start = int(ranges[0]) + end = int(ranges[1]) + numeric_range = vim.NumericRange(start=start, end=end) + if self.__is_native_vlan_in_range(numeric_range, native_vlan_to_exclude): + non_compliant_dv_port_group_configs.append(dv_pg_vlan_config) + break + # Trunk ranges might also have single numeric values like "200" + else: + if int(vlan_range) == native_vlan_to_exclude: + non_compliant_dv_port_group_configs.append(dv_pg_vlan_config) + break + elif isinstance(vlan_config, int): + if native_vlan_to_exclude == vlan_config: + non_compliant_dv_port_group_configs.append(dv_pg_vlan_config) + logger.debug(f"Non-Compliant vlan configs {non_compliant_dv_port_group_configs}") + return non_compliant_dv_port_group_configs + + def check_compliance(self, context: VcenterContext, desired_values: dict) -> Dict: + """Check compliance of all dv port groups against native vlan id to be excluded from configuration. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Dict containing Native VLAN ID to be excluded from port group configurations. + :type desired_values: dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + all_dv_pg_vlan_configs, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_port_groups = self.__get_non_compliant_vlan_configs(all_dv_pg_vlan_configs, desired_values) + logger.info(f"Non compliant port groups {non_compliant_port_groups}") + + if non_compliant_port_groups: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_port_groups, + consts.DESIRED: desired_values, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/vcenter/dv_pg_promiscuous_mode_policy.py b/config_modules_vmware/controllers/vcenter/dv_pg_promiscuous_mode_policy.py new file mode 100644 index 0000000..bd2f93b --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/dv_pg_promiscuous_mode_policy.py @@ -0,0 +1,300 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.controllers.vcenter.utils.vc_port_group_utils import ( + get_all_non_uplink_non_nsx_port_group_and_security_configs, +) +from config_modules_vmware.controllers.vcenter.utils.vc_port_group_utils import ( + get_non_compliant_security_policy_configs, +) +from config_modules_vmware.controllers.vcenter.utils.vc_port_group_utils import PortGroupSecurityConfigEnum +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_client import VcVmomiClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +# Constants +DESIRED_KEY = "promiscuous_mode" +SWITCH_NAME = "switch_name" +PORT_GROUP_NAME = "port_group_name" +NSX_BACKING_TYPE = "nsx" +GLOBAL = "__GLOBAL__" +OVERRIDES = "__OVERRIDES__" + + +class DVPortGroupPromiscuousModePolicy(BaseController): + """Class for managing DV Port group promiscuous mode policy with get and set methods. + + | Config Id - 405 + | Config Title - The vCenter Server must set the distributed port group Promiscuous Mode policy to reject. + + """ + + metadata = ControllerMetadata( + name="dvpg_promiscuous_mode_policy", # controller name + path_in_schema="compliance_config.vcenter.dvpg_promiscuous_mode_policy", # path in the schema to this controller's definition. + configuration_id="405", # configuration id as defined in compliance kit. + title="The vCenter Server must set the distributed port group Promiscuous Mode policy to reject.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """ + Get DV Port group promiscuous mode policy for all port groups. + + | Sample get call output for remediation. + + .. code-block:: json + + [ + { + "switch_name": "SwitchB", + "port_group_name": "dv_pg_PortGroup3", + "promiscuous_mode": false + }, + { + "switch_name": "SwitchC", + "port_group_name": "dv_pg_PortGroup1", + "promiscuous_mode": true + }, + { + "switch_name": "SwitchA", + "port_group_name": "dv_pg_PortGroup2", + "promiscuous_mode": false + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of list of port group and their promiscuous mode policy and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + try: + result = self.__get_all_dv_port_group_promiscuous_mode_policy(vc_vmomi_client) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Set DV Port group promiscuous mode policy for all port groups. + + | Sample desired state + + .. code-block:: json + + { + "__GLOBAL__": { + "promiscuous_mode": false + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch-A", + "port_group_name": "dv_pg_PortGroup1", + "promiscuous_mode": true + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the enabling or disabling promiscuous mode on port groups. + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + self.__set_promiscuous_mode_policy_for_non_compliant_dv_port_groups(vc_vmomi_client, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def __get_all_dv_port_group_promiscuous_mode_policy(self, vc_vmomi_client: VcVmomiClient) -> List[Dict]: + """ + Get promiscuous mode policies for all dv port groups + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of Promiscuous mode policy for all dv port groups. + :rtype: List + """ + non_uplink_non_nsx_dv_pgs = get_all_non_uplink_non_nsx_port_group_and_security_configs( + vc_vmomi_client, PortGroupSecurityConfigEnum.ALLOW_PROMISCUOUS + ) + promiscuous_mode_configs = [] + for dv_pg, promiscuous_mode_config in non_uplink_non_nsx_dv_pgs: + has_switch_name_config = hasattr(dv_pg.config, "distributedVirtualSwitch") and hasattr( + dv_pg.config.distributedVirtualSwitch, "name" + ) + promiscuous_mode_configs.append( + { + SWITCH_NAME: dv_pg.config.distributedVirtualSwitch.name if has_switch_name_config else "", + PORT_GROUP_NAME: dv_pg.name, + DESIRED_KEY: promiscuous_mode_config, + } + ) + return promiscuous_mode_configs + + def __set_promiscuous_mode_policy_for_non_compliant_dv_port_groups( + self, vc_vmomi_client: VcVmomiClient, desired_values: Dict + ) -> None: + """ + Set promiscuous mode policy for all DV port groups. + + | Recommended promiscuous mode policy: false | reject + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :param desired_values: Desired values for Promiscuous mode policy. + :type desired_values: Dict + :return: + :rtype: None + """ + desired_global_promiscuous_mode_value = desired_values.get(GLOBAL, {}).get(DESIRED_KEY) + overrides = desired_values.get(OVERRIDES, []) + non_uplink_non_nsx_dv_pgs = get_all_non_uplink_non_nsx_port_group_and_security_configs( + vc_vmomi_client, PortGroupSecurityConfigEnum.ALLOW_PROMISCUOUS + ) + + for dv_pg, current_promiscuous_mode_config in non_uplink_non_nsx_dv_pgs: + dv_switch_name = getattr(dv_pg.config.distributedVirtualSwitch, "name") + port_group_name = getattr(dv_pg, "name") + + # Check if there are overrides for the current DV Port group + override_promiscuous_mode_policy = next( + ( + override.get(DESIRED_KEY) + for override in overrides + if override[SWITCH_NAME] == dv_switch_name and override[PORT_GROUP_NAME] == port_group_name + ), + None, + ) + desired_promiscuous_mode_policy = ( + override_promiscuous_mode_policy + if override_promiscuous_mode_policy is not None + else desired_global_promiscuous_mode_value + ) + # re-configure only if current and desired values are not equal + if current_promiscuous_mode_config != desired_promiscuous_mode_policy: + config_spec = vim.dvs.DistributedVirtualPortgroup.ConfigSpec() + config_spec.configVersion = dv_pg.config.configVersion + config_spec.defaultPortConfig = vim.dvs.VmwareDistributedVirtualSwitch.VmwarePortConfigPolicy() + config_spec.defaultPortConfig.securityPolicy = vim.dvs.VmwareDistributedVirtualSwitch.SecurityPolicy() + config_spec.defaultPortConfig.securityPolicy.allowPromiscuous = vim.BoolPolicy( + value=desired_promiscuous_mode_policy + ) + logger.info( + f"Setting Promiscuous mode policy {desired_promiscuous_mode_policy}" f" on port group {dv_pg.name}" + ) + task = dv_pg.ReconfigureDVPortgroup_Task(spec=config_spec) + vc_vmomi_client.wait_for_task(task=task) + + def check_compliance(self, context: VcenterContext, desired_values: Dict) -> Dict: + """ + Check compliance of all dv port group's promiscuous mode policy. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for promiscuous mode policy. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + dv_port_group_promiscuous_mode_policy_configs, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_configs, desired_configs = get_non_compliant_security_policy_configs( + dv_port_group_promiscuous_mode_policy_configs, desired_values, DESIRED_KEY + ) + + if non_compliant_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_configs, + consts.DESIRED: desired_configs, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result + + def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: + """ + Remediate configuration drifts by applying desired values. + + | Recommended promiscuous mode policy: false | reject + | Sample desired state for remdiation. + + .. code-block:: json + + { + "__GLOBAL__": { + "promiscuous_mode": false + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch-A", + "port_group_name": "dv_pg_PortGroup1", + "promiscuous_mode": true + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for promiscuous mode policy. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Running remediation") + result = self.check_compliance(context, desired_values) + + if result[consts.STATUS] == ComplianceStatus.COMPLIANT: + return {consts.STATUS: RemediateStatus.SUCCESS} + elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: + non_compliant_configs = result[consts.CURRENT] + desired_configs = result[consts.DESIRED] + else: + errors = result[consts.ERRORS] + return {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + + status, errors = self.set(context=context, desired_values=desired_values) + + if not errors: + result = {consts.STATUS: status, consts.OLD: non_compliant_configs, consts.NEW: desired_configs} + else: + result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + return result diff --git a/config_modules_vmware/controllers/vcenter/dv_pg_reserved_vlan_exclusion_policy.py b/config_modules_vmware/controllers/vcenter/dv_pg_reserved_vlan_exclusion_policy.py new file mode 100644 index 0000000..884bf3d --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/dv_pg_reserved_vlan_exclusion_policy.py @@ -0,0 +1,270 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_client import VcVmomiClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +# constants +VLAN_KEY = "vlan" +DESIRED_KEY = "reserved_vlan_id_to_exclude" +SWITCH_NAME_KEY = "switch_name" +PORT_GROUP_NAME_KEY = "port_group_name" +NSX_BACKING_TYPE = "nsx" + + +class DVPortGroupReservedVlanExclusionConfig(BaseController): + """Manage DV Port group Reserved Vlan exclusion config with get and set methods. + + | Config Id - 1202 + | Config Title - Configure all port groups to VLAN values not reserved by upstream physical switches. + + """ + + metadata = ControllerMetadata( + name="dvpg_excluded_reserved_vlan_policy", # controller name + path_in_schema="compliance_config.vcenter.dvpg_excluded_reserved_vlan_policy", # path in the schema to this + # controller's definition. + configuration_id="1202", # configuration id as defined in compliance kit. + title="Configure all port groups to VLAN values not reserved by upstream physical switches.", # controller title + # as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in + # ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """Get DV Port group Reserved Vlan exclusion config for all applicable port groups. + + | Sample get call output + + .. code-block:: json + + [ + { + "switch_name": "DSwitch-test", + "port_group_name": "DPortGroup-test", + "vlan": 1 + }, + { + "switch_name": "DSwitch-test", + "port_group_name": "DPortGroup", + "vlan": ["1-100", "105", "200-250"] + }, + { + "switch_name": "SDDC-Dswitch-Private", + "port_group_name": "SDDC-DPortGroup-vMotion", + "vlan": 1 + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of list of port group and their vlan configs and a list of error messages. + :rtype: tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + try: + result = self.__get_all_dv_port_vlan_configs(vc_vmomi_client) + logger.debug( + f"Retrieved DV Port group Reserved Vlan exclusion config for all applicable port groups" f" {result}" + ) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple: + """Set vlan config for DV port groups excluding reserved vlan in the configuration. + + | Sample desired state + + .. code-block:: json + + { + "reserved_vlan_id_to_exclude": 1 + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values containing reserved vlan id to be excluded from port group configurations. + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + logger.info(consts.REMEDIATION_SKIPPED_MESSAGE) + status = RemediateStatus.SKIPPED + return status, errors + + def __get_vlan_config_for_non_nsx_non_uplink_dv_port_groups(self, vc_vmomi_client: VcVmomiClient) -> List[Tuple]: + """Helper function to retrieve vlan configurations for non-nsx and non-uplink dv port groups. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of tuple with non-nsx, non-uplink dv_pg_refs and their vlan configurations. + :rtype: List + """ + vlan_config_non_nsx_non_uplink_dv_port_group_refs = [] + all_dv_port_group_refs = vc_vmomi_client.get_objects_by_vimtype(vim.DistributedVirtualPortgroup) + logger.info(f"Retrieved DV port groups {all_dv_port_group_refs}") + + for dv_pg in all_dv_port_group_refs: + is_uplink_port_group = hasattr(dv_pg.config, "uplink") and getattr(dv_pg.config, "uplink") + is_nsx_backed = getattr(dv_pg.config, "backingType", "") == NSX_BACKING_TYPE + # skip all uplink and nsx backed port groups + if not is_uplink_port_group and not is_nsx_backed: + vlan_config = getattr(getattr(dv_pg.config, "defaultPortConfig", None), "vlan", None) + vlan_config_non_nsx_non_uplink_dv_port_group_refs.append((dv_pg, vlan_config)) + return vlan_config_non_nsx_non_uplink_dv_port_group_refs + + def __is_reserved_vlan_in_range(self, vlan_range: vim.NumericRange, reserved_vlan_id: int): + """Check if a given reserved vlan value lies in range of vlan trunk range. + + :param vlan_range: vLan range configured for a dv port group. + :type vlan_range:vim.NumericRange + :param reserved_vlan_id: reserved vlan id to check if it lies within given vlan range + :type reserved_vlan_id: int + :return: + """ + logger.info(f"Check if reserved vlan is part of vlan range {vlan_range}") + start = vlan_range.start + end = vlan_range.end + if start <= reserved_vlan_id <= end: + return True + return False + + def __get_all_dv_port_vlan_configs(self, vc_vmomi_client: VcVmomiClient) -> List: + """Get all non-nsx, non-uplink DV Port groups and their vlan configurations. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of tuple of + :rtype: List + """ + non_nsx_non_uplink_dv_port_groups = self.__get_vlan_config_for_non_nsx_non_uplink_dv_port_groups( + vc_vmomi_client + ) + logger.debug(f"Retrieved Non-NSX & Non-uplink port group refs {non_nsx_non_uplink_dv_port_groups}") + + dv_pg_vlan_configs = [] + for dv_pg_ref, vlan_config in non_nsx_non_uplink_dv_port_groups: + port_group_vlan_config = {} + has_switch_name_config = hasattr(dv_pg_ref.config, "distributedVirtualSwitch") and hasattr( + dv_pg_ref.config.distributedVirtualSwitch, "name" + ) + port_group_vlan_config[SWITCH_NAME_KEY] = ( + dv_pg_ref.config.distributedVirtualSwitch.name if has_switch_name_config else "" + ) + port_group_vlan_config[PORT_GROUP_NAME_KEY] = dv_pg_ref.name + + # check vlan type + is_vlan_type = isinstance(vlan_config, vim.dvs.VmwareDistributedVirtualSwitch.VlanIdSpec) + is_vlan_trunk_type = isinstance(vlan_config, vim.dvs.VmwareDistributedVirtualSwitch.TrunkVlanSpec) + is_pvt_vlan_type = isinstance(vlan_config, vim.dvs.VmwareDistributedVirtualSwitch.PvlanSpec) + + if is_vlan_type: + port_group_vlan_config[VLAN_KEY] = getattr(vlan_config, "vlanId") + elif is_vlan_trunk_type: + trunk_vlan_range = [] + vlan_ranges = vlan_config.vlanId + for vlan_range in vlan_ranges: + start = vlan_range.start + end = vlan_range.end + if start == end: + trunk_vlan_range.append(str(start)) + else: + trunk_vlan_range.append(f"{start}-{end}") + port_group_vlan_config[VLAN_KEY] = trunk_vlan_range + elif is_pvt_vlan_type: + port_group_vlan_config[VLAN_KEY] = getattr(vlan_config, "pvlanId") + dv_pg_vlan_configs.append(port_group_vlan_config) + logger.info(f"Retrieved vlan configs for non-nsx & non-uplink port groups {dv_pg_vlan_configs}") + return dv_pg_vlan_configs + + def __get_non_compliant_vlan_configs(self, dv_pg_vlan_configs: List, desired_values: dict) -> List: + """Get list of dv port groups with vlan configurations overlapping with reserved vlan id. + + :param dv_pg_vlan_configs: List of non-uplink, non-nsx port groups. + :type dv_pg_vlan_configs: List + :param desired_values: Dict containing reserved VLAN ID to be excluded from port group configurations. + :type desired_values: Dict + :return: List of non-compliant vlan configs + :rtype: List + """ + non_compliant_dv_port_group_configs = [] + reserved_vlan_to_exclude = desired_values.get(DESIRED_KEY) + for dv_pg_vlan_config in dv_pg_vlan_configs: + vlan_config = dv_pg_vlan_config[VLAN_KEY] + # vlan trunk spec will be in list format Ex:["1-200", "205", "300-350"] + if isinstance(vlan_config, List): + for vlan_range in vlan_config: + # Trunk vlan can have ranges like "1-100" + if isinstance(vlan_range, str) and "-" in vlan_range: + ranges = vlan_range.split("-") + start = int(ranges[0]) + end = int(ranges[1]) + numeric_range = vim.NumericRange(start=start, end=end) + if self.__is_reserved_vlan_in_range(numeric_range, reserved_vlan_to_exclude): + non_compliant_dv_port_group_configs.append(dv_pg_vlan_config) + break + # Trunk ranges might also have single numeric values like "200" + else: + if int(vlan_range) == reserved_vlan_to_exclude: + non_compliant_dv_port_group_configs.append(dv_pg_vlan_config) + break + elif isinstance(vlan_config, int): + if reserved_vlan_to_exclude == vlan_config: + non_compliant_dv_port_group_configs.append(dv_pg_vlan_config) + return non_compliant_dv_port_group_configs + + def check_compliance(self, context: VcenterContext, desired_values: dict) -> Dict: + """Check compliance of all dv port groups against reserved vlan id to be excluded from configuration. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Dict containing reserved VLAN ID to be excluded from port group configurations. + :type desired_values: dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + all_dv_pg_vlan_configs, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_port_groups = self.__get_non_compliant_vlan_configs(all_dv_pg_vlan_configs, desired_values) + logger.info(f"Non compliant port groups {non_compliant_port_groups}") + + if non_compliant_port_groups: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_port_groups, + consts.DESIRED: desired_values, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/vcenter/dv_pg_vlan_trunking_authorized.py b/config_modules_vmware/controllers/vcenter/dv_pg_vlan_trunking_authorized.py new file mode 100644 index 0000000..07e7a5c --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/dv_pg_vlan_trunking_authorized.py @@ -0,0 +1,281 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_client import VcVmomiClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +# constants +SWITCH_NAME_KEY = "switch_name" +PORT_GROUP_NAME_KEY = "port_group_name" +VLAN_TYPE_TRUNKING = "VLAN trunking" +VLAN_RANGES = "vlan_ranges" +VLAN_INFO = "vlan_info" +VLAN_TYPE = "vlan_type" + + +class DVPortGroupVlanTrunkingConfig(BaseController): + """DV Port group Vlan trunking config get and set methods. + + | Config Id - 1227 + | Config Title - The vCenter Server must not configure VLAN Trunking unless Virtual Guest + Tagging (VGT) is required and authorized. + + """ + + metadata = ControllerMetadata( + name="dvpg_vlan_trunking_authorized_check", # controller name + path_in_schema="compliance_config.vcenter.dvpg_vlan_trunking_authorized_check", # path in the schema to this + # controller's definition. + configuration_id="1227", # configuration id as defined in compliance kit. + title="The vCenter Server must not configure VLAN Trunking unless Virtual Guest Tagging (VGT) is required and authorized.", # controller title + # as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """Get DV Port group Native Vlan exclusion config for all applicable port groups. + + | Sample get call output + + .. code-block:: json + + [ + { + switch_name: "SDDC-Dswitch-Private", + port_group_name: "SDDC-DPortGroup-VSAN", + vlan_info: { + vlan_type: "VLAN trunking", + vlan_ranges:[ + { start: 0, end: 90}, + { start: 120, end: 200} + ] + } + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of list of port group and their vlan configs and a list of error messages. + :rtype: tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + try: + result = self._get_all_dv_port_vlan_trunking_configs(vc_vmomi_client) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple: + """Set vlan config for DV port groups to remediate trunking vlan in the configuration. + + | Sample desired state + + .. code-block:: json + + [ + { + switch_name: "SDDC-Dswitch-Private", + port_group_name: "SDDC-DPortGroup-VSAN", + vlan_info: { + vlan_type: "VLAN trunking", + vlan_ranges:[ + { start: 0, end: 90}, + { start: 120, end: 200} + ] + } + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values containing vlan trunking configs. + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + logger.info(consts.REMEDIATION_SKIPPED_MESSAGE) + status = RemediateStatus.SKIPPED + return status, errors + + def _is_vlan_trunking_ranges_compliant(self, vlan_ranges, desired_vlan_ranges) -> bool: + for vlan_range in vlan_ranges: + vlan_in_desired_range = False + for desired_vlan_range in desired_vlan_ranges: + if ( + vlan_range.get("start") >= desired_vlan_range["start"] + and vlan_range.get("end") <= desired_vlan_range["end"] + ): + vlan_in_desired_range = True + if not vlan_in_desired_range: + return False + return True + + def _get_desired_value_map(self, desired_values): + desired_config_map = {} + for desired_value in desired_values: + switch_name = desired_value.get(SWITCH_NAME_KEY) + port_group_name = desired_value.get(PORT_GROUP_NAME_KEY) + key = (switch_name, port_group_name) + desired_config_map[key] = { + VLAN_INFO: desired_value.get(VLAN_INFO), + } + return desired_config_map + + def _get_vlan_ranges(self, vlan_range_configs): + vlan_ranges = [] + for vlan_range_config in vlan_range_configs: + vlan_range = {} + vlan_range["start"] = vlan_range_config.start + vlan_range["end"] = vlan_range_config.end + vlan_ranges.append(vlan_range) + return vlan_ranges + + def _get_dv_pg_vlan_config(self, pg_vlan_config, desired_config_map) -> Dict: + """compare pg vlan config with desired config + + :param pg_vlan_config: Dict containing port group vlan trunking config. + :type pg_vlan_config: Dict + :param desired_config_map: Dict containing VLAN ID a specific port group. + :type desired_config_map: Dict + :return: List of non-compliant vlan trunking configs + :rtype: List + """ + switch_name = pg_vlan_config[SWITCH_NAME_KEY] + port_group_name = pg_vlan_config[PORT_GROUP_NAME_KEY] + key = (switch_name, port_group_name) + # check if path name in desired config. + vlan_config = pg_vlan_config[VLAN_INFO][VLAN_RANGES] + desired_vlan_config = desired_config_map[key].get(VLAN_INFO) if key in desired_config_map else None + + # check if vlan config matches what in desired config + if desired_vlan_config and desired_vlan_config.get(VLAN_TYPE) == VLAN_TYPE_TRUNKING: + if self._is_vlan_trunking_ranges_compliant(vlan_config, desired_vlan_config.get(VLAN_RANGES)): + return None + + return pg_vlan_config + + def _get_non_compliant_dv_port_vlan_configs(self, pg_vlan_trunking_configs, desired_values) -> List: + """Get all non compliant vlan trunking configurations. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of tuple of non compliant configurations + :rtype: List + """ + non_compliant_dv_pg_configs = [] + desired_config_map = self._get_desired_value_map(desired_values) + + for pg_vlan_trunking_config in pg_vlan_trunking_configs: + non_compliant_dv_pg_config = self._get_dv_pg_vlan_config(pg_vlan_trunking_config, desired_config_map) + if non_compliant_dv_pg_config is not None: + non_compliant_dv_pg_configs.append(non_compliant_dv_pg_config) + + return non_compliant_dv_pg_configs + + def _get_vlan_config_for_non_uplink_dv_port_groups(self, vc_vmomi_client: VcVmomiClient) -> List[Tuple]: + """Helper function to retrieve vlan configurations for non-uplink dv port groups. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of tuple with non-uplink dv_pg_refs and their vlan configurations. + :rtype: List + """ + vlan_config_non_uplink_dv_port_group_refs = [] + all_dv_port_group_refs = vc_vmomi_client.get_objects_by_vimtype(vim.DistributedVirtualPortgroup) + + for dv_pg in all_dv_port_group_refs: + is_uplink_port_group = hasattr(dv_pg.config, "uplink") and getattr(dv_pg.config, "uplink") + # skip all uplink port groups + if not is_uplink_port_group: + vlan_config = getattr(getattr(dv_pg.config, "defaultPortConfig", None), "vlan", None) + vlan_config_non_uplink_dv_port_group_refs.append((dv_pg, vlan_config)) + return vlan_config_non_uplink_dv_port_group_refs + + def _get_all_dv_port_vlan_trunking_configs(self, vc_vmomi_client: VcVmomiClient) -> List: + """Get all non-uplink DV Port groups and their vlan configurations. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of tuple of + :rtype: List + """ + non_uplink_dv_port_groups = self._get_vlan_config_for_non_uplink_dv_port_groups(vc_vmomi_client) + logger.debug(f"Retrieved Non-uplink port group refs {non_uplink_dv_port_groups}") + + dv_pg_vlan_configs = [] + for dv_pg_ref, vlan_config in non_uplink_dv_port_groups: + port_group_vlan_config = {} + # check vlan type + is_vlan_trunk_type = isinstance(vlan_config, vim.dvs.VmwareDistributedVirtualSwitch.TrunkVlanSpec) + if is_vlan_trunk_type: + port_group_vlan_config = {} + has_switch_name_config = hasattr(dv_pg_ref.config, "distributedVirtualSwitch") and hasattr( + dv_pg_ref.config.distributedVirtualSwitch, "name" + ) + port_group_vlan_config[SWITCH_NAME_KEY] = ( + dv_pg_ref.config.distributedVirtualSwitch.name if has_switch_name_config else "" + ) + port_group_vlan_config[PORT_GROUP_NAME_KEY] = dv_pg_ref.name + port_group_vlan_config[VLAN_INFO] = {} + port_group_vlan_config[VLAN_INFO][VLAN_TYPE] = VLAN_TYPE_TRUNKING + port_group_vlan_config[VLAN_INFO][VLAN_RANGES] = self._get_vlan_ranges(vlan_config.vlanId) + dv_pg_vlan_configs.append(port_group_vlan_config) + logger.debug(f"Retrieved vlan trunking configs for non-uplink port groups {dv_pg_vlan_configs}") + return dv_pg_vlan_configs + + def check_compliance(self, context: VcenterContext, desired_values: dict) -> Dict: + """Check compliance for all dv port groups if vlan trunking is in configuration. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Dict containing VLAN trunking configs to be excluded checked. + :type desired_values: dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + all_dv_pg_vlan_trunking_configs, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_vlan_trunking_configs = self._get_non_compliant_dv_port_vlan_configs( + all_dv_pg_vlan_trunking_configs, desired_values + ) + logger.info(f"Non compliant port groups {non_compliant_vlan_trunking_configs}") + + if non_compliant_vlan_trunking_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_vlan_trunking_configs, + consts.DESIRED: desired_values, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/vcenter/dvs_health_check_config.py b/config_modules_vmware/controllers/vcenter/dvs_health_check_config.py new file mode 100644 index 0000000..5f4cc90 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/dvs_health_check_config.py @@ -0,0 +1,304 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_client import VcVmomiClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +# Constants +DESIRED_KEY = "health_check_enabled" +SWITCH_NAME = "switch_name" +GLOBAL = "__GLOBAL__" +OVERRIDES = "__OVERRIDES__" + + +class DVSHealthCheckConfig(BaseController): + """Manage DVS health check config with get and set methods. + + | Config Id - 1200 + | Config Title - The vCenter Server must disable the distributed virtual switch health check. + + """ + + metadata = ControllerMetadata( + name="dvs_health_check", # controller name + path_in_schema="compliance_config.vcenter.dvs_health_check", + # path in the schema to this controller's definition. + configuration_id="1200", # configuration id as defined in compliance kit. + title="The vCenter Server must disable the distributed virtual switch health check.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """ + Get DVS health check status for all virtual switches. + + | Sample get call output + + .. code-block:: json + + [ + { + "switch_name": "SwitchB", + "health_check_enabled": false + }, + { + "switch_name": "SwitchC", + "health_check_enabled": true + }, + { + "switch_name": "SwitchA", + "health_check_enabled": false + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of list of DV switch health check status and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + try: + result = self.__get_all_dv_switch_health_check_status(vc_vmomi_client) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Enable/Disable health check for DV switches. + + | Recommended value for DV switch health check: false | disabled + | Sample desired state + + .. code-block:: json + + { + "__GLOBAL__": { + "health_check_enabled": false + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch-A", + "health_check_enabled": true + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the enabling or disabling DVS health check config. + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + self.__set_health_check_config_for_all_dv_switches(vc_vmomi_client, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def __get_all_dv_switch_health_check_status(self, vc_vmomi_client: VcVmomiClient) -> List[Dict]: + """ + Get health check status for all DV Switches. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of health check status for all DV switches. + :rtype: List + """ + dv_switch_health_check_configs = [] + all_dv_switches = vc_vmomi_client.get_objects_by_vimtype(vim.DistributedVirtualSwitch) + + for dvs in all_dv_switches: + health_check_status = {SWITCH_NAME: dvs.name, DESIRED_KEY: False} + # check if VlanMtuHealthCheckConfig or TeamingHealthCheckConfig is enabled + if any([getattr(health_cfg, "enable", False) for health_cfg in dvs.config.healthCheckConfig]): + health_check_status[DESIRED_KEY] = True + dv_switch_health_check_configs.append(health_check_status) + return dv_switch_health_check_configs + + def __set_health_check_config_for_all_dv_switches( + self, vc_vmomi_client: VcVmomiClient, desired_values: Dict + ) -> None: + """ + Enable or disable health check config for all DV switches. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :param desired_values: Desired values for DVS health check config. + :type desired_values: Dict + :return: + :rtype: None + """ + desired_global_health_check_value = desired_values.get(GLOBAL, {}).get(DESIRED_KEY) + overrides = desired_values.get(OVERRIDES, []) + all_dv_switch_refs = vc_vmomi_client.get_objects_by_vimtype(vim.DistributedVirtualSwitch) + + for dvs_ref in all_dv_switch_refs: + # Check if there are overrides for the current DVS + override_health_check_value = next( + (switch.get(DESIRED_KEY) for switch in overrides if switch[SWITCH_NAME] == dvs_ref.name), + None, + ) + + # Set vlan mtu health check + vlan_mtu_health_check_config = vim.dvs.VmwareDistributedVirtualSwitch.VlanMtuHealthCheckConfig() + vlan_mtu_health_check_config.enable = ( + override_health_check_value + if override_health_check_value is not None + else desired_global_health_check_value + ) + # Set teaming health check + teaming_health_check_config = vim.dvs.VmwareDistributedVirtualSwitch.TeamingHealthCheckConfig() + teaming_health_check_config.enable = ( + override_health_check_value + if override_health_check_value is not None + else desired_global_health_check_value + ) + + health_check_config = [vlan_mtu_health_check_config, teaming_health_check_config] + dvs_health_config_task = dvs_ref.UpdateDVSHealthCheckConfig_Task(health_check_config) + vc_vmomi_client.wait_for_task(dvs_health_config_task) + + def __get_non_compliant_configs(self, switch_configs: List, desired_values: Dict) -> List: + """ + Get all non-compliant items for the given desired state spec. + + :return: + :meta private: + """ + non_compliant_configs = [] + # convert to dictionary for easy access + configs_by_switch_name = {config.get(SWITCH_NAME): config for config in switch_configs} + + global_desired_value = desired_values.get(GLOBAL, {}).get(DESIRED_KEY) + overrides = desired_values.get(OVERRIDES, []) + + # Check global non-compliance + non_compliant_global = [config for config in switch_configs if config.get(DESIRED_KEY) != global_desired_value] + if non_compliant_global: + non_compliant_configs.extend(non_compliant_global) + + # Remove non-compliant override config if exists from global + for override in overrides: + override_switch_name = override.get(SWITCH_NAME) + for config in non_compliant_global: + if config.get(SWITCH_NAME) == override_switch_name: + non_compliant_configs.remove(config) + + # Check overrides for non-compliance + for switch_override in overrides: + switch_name = switch_override.get(SWITCH_NAME) + desired_value = switch_override.get(DESIRED_KEY) + + # Find the configuration for the current switch + config = configs_by_switch_name.get(switch_name) + if config and config.get(DESIRED_KEY) != desired_value: + non_compliant_configs.append(config) + return non_compliant_configs + + def check_compliance(self, context: VcenterContext, desired_values: Dict) -> Dict: + """ + Check compliance of health check configs for all DV switches. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for DV switch health check config. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + dv_switch_health_check_configs, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_configs = self.__get_non_compliant_configs(dv_switch_health_check_configs, desired_values) + + if non_compliant_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_configs, + consts.DESIRED: desired_values, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result + + def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: + """ + Remediate configuration drifts by applying desired values. + + | Sample desired state for remediation + + .. code-block:: json + + { + "__GLOBAL__": { + "health_check_enabled": false + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch-A", + "health_check_enabled": true + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for DV switch health check config. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Running remediation") + result = self.check_compliance(context, desired_values) + + if result[consts.STATUS] == ComplianceStatus.COMPLIANT: + return {consts.STATUS: RemediateStatus.SUCCESS} + elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: + non_compliant_items = result[consts.CURRENT] + else: + errors = result[consts.ERRORS] + return {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + + status, errors = self.set(context=context, desired_values=desired_values) + + if not errors: + result = {consts.STATUS: status, consts.OLD: non_compliant_items, consts.NEW: desired_values} + else: + result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + return result diff --git a/config_modules_vmware/controllers/vcenter/dvs_network_io_control_policy.py b/config_modules_vmware/controllers/vcenter/dvs_network_io_control_policy.py new file mode 100644 index 0000000..e6d1533 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/dvs_network_io_control_policy.py @@ -0,0 +1,305 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from pyVmomi import vim # pylint: disable=E0401 + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_client import VcVmomiClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +# Constants +DESIRED_KEY = "network_io_control_status" +SWITCH_NAME = "switch_name" +GLOBAL = "__GLOBAL__" +OVERRIDES = "__OVERRIDES__" + + +class DVSNetworkIOControlPolicy(BaseController): + """Manage DV Switch Network I/O control policy with get and set methods. + + | Config Id - 409 + | Config Title - The vCenter Server must manage excessive bandwidth and Denial of Service (DoS) attacks by enabling + Network I/O Control (NIOC). + + """ + + metadata = ControllerMetadata( + name="dvs_network_io_control", # controller name + path_in_schema="compliance_config.vcenter.dvs_network_io_control", # path in the schema to this controller's definition. + configuration_id="409", # configuration id as defined in compliance kit. + title="The vCenter Server must manage excessive bandwidth and Denial of Service (DoS) attacks by enabling Network I/O Control (NIOC).", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """ + Get DVS Network I/O control policy for all DV switches. + + | Sample get output + + .. code-block:: json + + [ + { + "switch_name": "SwitchB", + "network_io_control_status": false + }, + { + "switch_name": "SwitchC", + "network_io_control_status": true + }, + { + "switch_name": "SwitchA", + "network_io_control_status": false + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of list of DV switch network I/O control policy and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + try: + result = self.__get_all_dv_switch_network_io_control_policy(vc_vmomi_client) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Set Network I/O control policy for all DV switches. + + | Sample desired state + + .. code-block:: json + + { + "__GLOBAL__": { + "network_io_control_status": false + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch-A", + "network_io_control_status": false + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for enabling/disabling Network I/O control policy. + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + self.__set_network_io_control_policy_for_all_dv_switches(vc_vmomi_client, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def __get_all_dv_switch_network_io_control_policy(self, vc_vmomi_client: VcVmomiClient) -> List[Dict]: + """ + Get Network I/O control policy for all DV Switches. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: List of Network I/O control policy for all DV switches. + :rtype: List + :return: + """ + dv_switch_network_io_control_configs = [] + all_dv_switches = vc_vmomi_client.get_objects_by_vimtype(vim.DistributedVirtualSwitch) + + for dvs in all_dv_switches: + network_io_control_status = { + SWITCH_NAME: dvs.name, + DESIRED_KEY: dvs.config.networkResourceManagementEnabled, + } + dv_switch_network_io_control_configs.append(network_io_control_status) + return dv_switch_network_io_control_configs + + def __set_network_io_control_policy_for_all_dv_switches( + self, vc_vmomi_client: VcVmomiClient, desired_values: Dict + ) -> None: + """ + Enable or disable Network I/O control policy for all dv switches. + + | Recommended value for network I/O control: true | enabled + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :param desired_values: Desired values for Network I/O control policy. + :type desired_values: Dict + :return: + :rtype: None + """ + desired_global_network_io_control_value = desired_values.get(GLOBAL, {}).get(DESIRED_KEY) + overrides = desired_values.get(OVERRIDES, []) + # desired_network_io_control_value = desired_values.get(DESIRED_KEY) + all_switch_refs = vc_vmomi_client.get_objects_by_vimtype(vim.DistributedVirtualSwitch) + + for dvs_ref in all_switch_refs: + # Check if there are overrides for the current DVS + override_health_check_value = next( + (switch.get(DESIRED_KEY) for switch in overrides if switch[SWITCH_NAME] == dvs_ref.name), + None, + ) + current_network_io_control_value = dvs_ref.config.networkResourceManagementEnabled + desired_network_io_control_value = ( + override_health_check_value + if override_health_check_value is not None + else desired_global_network_io_control_value + ) + + if current_network_io_control_value == desired_network_io_control_value: + logger.info( + f"DV switch {dvs_ref.name} already has desired network I/O control config," f" skipping remediation" + ) + else: + logger.info( + f"Setting network I/O control config {desired_network_io_control_value} on DV " + f"switch {dvs_ref.name}" + ) + dvs_ref.EnableNetworkResourceManagement(desired_network_io_control_value) + + def __get_non_compliant_configs(self, switch_configs: List, desired_values: Dict) -> List: + """ + Get all non-compliant items for the given desired state spec. + + :return: + :meta private: + """ + non_compliant_items = [] + # convert to dictionary for easy access + configs_by_switch_name = {config.get(SWITCH_NAME): config for config in switch_configs} + + global_desired_value = desired_values.get(GLOBAL, {}).get(DESIRED_KEY) + overrides = desired_values.get(OVERRIDES, []) + + # Check global non-compliance + non_compliant_global = [config for config in switch_configs if config.get(DESIRED_KEY) != global_desired_value] + if non_compliant_global: + non_compliant_items.extend(non_compliant_global) + + # Remove non-compliant override config if exists from global + for override in overrides: + override_switch_name = override.get(SWITCH_NAME) + for config in non_compliant_global: + if config.get(SWITCH_NAME) == override_switch_name: + non_compliant_items.remove(config) + + # Check overrides for non-compliance + for switch_override in overrides: + switch_name = switch_override.get(SWITCH_NAME) + desired_value = switch_override.get(DESIRED_KEY) + + # Find the configuration for the current switch + config = configs_by_switch_name.get(switch_name) + if config and config.get(DESIRED_KEY) != desired_value: + non_compliant_items.append(config) + return non_compliant_items + + def check_compliance(self, context: VcenterContext, desired_values: Dict) -> Dict: + """ + Check compliance of Network I/O control policy for all DV switches. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for network I/O control policy. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + dv_switch_network_io_control_configs, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + non_compliant_configs = self.__get_non_compliant_configs(dv_switch_network_io_control_configs, desired_values) + + if non_compliant_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_configs, + consts.DESIRED: desired_values, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result + + def remediate(self, context: VcenterContext, desired_values: Dict) -> Dict: + """ + Remediate configuration drifts by applying desired values. + + | Sample desired state for remediation. + + .. code-block:: json + + { + "__GLOBAL__": { + "network_io_control_status": false + }, + "__OVERRIDES__": [ + { + "switch_name": "Switch-A", + "network_io_control_status": false + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for Network I/O control for DV switches. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Running remediation") + result = self.check_compliance(context, desired_values) + + if result[consts.STATUS] == ComplianceStatus.COMPLIANT: + return {consts.STATUS: RemediateStatus.SUCCESS} + elif result[consts.STATUS] == ComplianceStatus.NON_COMPLIANT: + non_compliant_configs = result[consts.CURRENT] + else: + errors = result[consts.ERRORS] + return {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + + status, errors = self.set(context=context, desired_values=desired_values) + + if not errors: + result = {consts.STATUS: status, consts.OLD: non_compliant_configs, consts.NEW: desired_values} + else: + result = {consts.STATUS: RemediateStatus.FAILED, consts.ERRORS: errors} + return result diff --git a/config_modules_vmware/controllers/vcenter/h5_client_session_timeout_config.py b/config_modules_vmware/controllers/vcenter/h5_client_session_timeout_config.py new file mode 100644 index 0000000..fd29f6d --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/h5_client_session_timeout_config.py @@ -0,0 +1,180 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +import re +from typing import Any +from typing import List +from typing import Tuple +from typing import Union + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +CMD_TIMEOUT = 10 +GREP_CMD = "/usr/bin/grep" +SED_CMD = "/usr/bin/sed" +PROPERTY_FILE_PATH = "/etc/vmware/vsphere-ui/webclient.properties" +SESSION_TIMEOUT_GREP_PATTERN = r"^session\.timeout.*=" +SESSION_TIMEOUT_VALUE = r"session\.timeout = {}" +SED_REPLACE_PATTERN = r"/^session\.timeout/s/=[[:space:]]*[^[:space:]]*/=" +GREP_GET_PROPERTY_CMD = f"{GREP_CMD} -E {SESSION_TIMEOUT_GREP_PATTERN} {PROPERTY_FILE_PATH}" +SED_ADD_CMD = f"{SED_CMD} -i '$ a {SESSION_TIMEOUT_VALUE}' {PROPERTY_FILE_PATH}" + + +class H5ClientSessionTimeoutConfig(BaseController): + """Manage vCenter H5 client idle session timeout with get and set methods. + + | Config Id - 422 + | Config Title - The vCenter Server must terminate management sessions after certain period of inactivity. + + """ + + metadata = ControllerMetadata( + name="h5_client_session_timeout", # controller name + path_in_schema="compliance_config.vcenter.h5_client_session_timeout", # path in the schema to this controller's definition. + configuration_id="422", # configuration id as defined in compliance kit. + title="The vCenter Server must terminate management sessions after certain period of inactivity.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + functional_test_targets=["vcenter"], # location where functional tests are run. + ) + + def get(self, context: VcenterContext) -> Tuple[int, List[str]]: + """Get H5 client session timeout from vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of session timeout value as int and a list of error messages if any. + :rtype: tuple + """ + logger.info("Getting H5 client session timeout for audit.") + errors = [] + result = None + try: + prop_value = self.__get_session_timeout_value() + if prop_value is not None: + result = prop_value + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """Sets H5 client session timeout on vCenter. + + | STIG Recommended value: 10 Minutes; default: 120 minutes. + | Note: For session timeout setting to take effect a restart of the vsphere-ui service is required. + Please note that service restart is not included as part of this remediation procedure. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired value in minutes for the H5 client session timeout. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting H5 client session timeout") + errors = [] + status = RemediateStatus.SUCCESS + try: + _update_successful = self.__apply_vcsa_session_timeout_value(desired_value=desired_values) + if not _update_successful: + status = RemediateStatus.FAILED + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + @staticmethod + def __get_session_timeout_value() -> Union[int, None]: + """Check if the session.timeout property exists in prop file and retrieve value. + + :return: value of property if found in prop file else None + """ + output_str, _, ret_code = utils.run_shell_cmd( + command=GREP_GET_PROPERTY_CMD, + timeout=CMD_TIMEOUT, + raise_on_non_zero=False, + ) # nosec + if ret_code != 0: + logger.info(f"Error fetching the property {SESSION_TIMEOUT_GREP_PATTERN} from {PROPERTY_FILE_PATH}") + return None + + session_timeout = None + # Extract timeout value + match = re.search(r"session\.timeout\s*=\s*(\d+)", output_str.strip()) + + if match: + session_timeout = int(match.group(1)) + return session_timeout + + @staticmethod + def __apply_vcsa_session_timeout_value(desired_value: int) -> bool: + """Apply VCSA session timeout value for H5 client. + + :param: desired_value: Desired value for session timeout + :type desired_value: int + :return: True if property update is successful, False otherwise + """ + current_timeout_value = H5ClientSessionTimeoutConfig.__get_session_timeout_value() + + if current_timeout_value is not None: + logger.info(f"current timeout value {current_timeout_value}") + + if current_timeout_value == desired_value: + logger.info(f"Session timeout is already at desired value {current_timeout_value}") + return True + else: + # Update session.timeout + H5ClientSessionTimeoutConfig.__replace_property(desired_value) + else: + # We assume session.timeout property is missing in prop file. + H5ClientSessionTimeoutConfig.__add_new_property(desired_value) + + # check if property addition/update was successful + updated_timeout_value = H5ClientSessionTimeoutConfig.__get_session_timeout_value() + + if updated_timeout_value is not None and updated_timeout_value == desired_value: + logger.info(f"Successfully updated session timeout value to {updated_timeout_value}") + return True + else: + logger.info(f"Failed to updated session.timeout property in {PROPERTY_FILE_PATH}") + return False + + @staticmethod + def __replace_property(desired_value: int) -> None: + """Replace session timeout property using SED command. + + :param desired_value: Desired value for session timeout + :type desired_value: int + :return: None + """ + sed_replace_command = rf"{SED_CMD} -i '{SED_REPLACE_PATTERN} {desired_value}/'" f" {PROPERTY_FILE_PATH}" + utils.run_shell_cmd(command=sed_replace_command, timeout=CMD_TIMEOUT) # nosec + logger.info(f"Successfully replaced session timeout to {desired_value}") + + @staticmethod + def __add_new_property(desired_value: int) -> None: + """Add new property using SED command. + + :param desired_value: desired value for session timeout + :type desired_value: :class: `int` + :return: None + """ + add_sed_cmd = SED_ADD_CMD.format(desired_value) + utils.run_shell_cmd(command=add_sed_cmd, timeout=CMD_TIMEOUT) # nosec + logger.info(f"Successfully added session timeout property with value {desired_value}") diff --git a/config_modules_vmware/controllers/vcenter/ldap_identity_source_config.py b/config_modules_vmware/controllers/vcenter/ldap_identity_source_config.py new file mode 100644 index 0000000..82e2797 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/ldap_identity_source_config.py @@ -0,0 +1,111 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +import re +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +VC_SSO_CONFIG_CMD_GET_LDAP_IDENTITY_SOURCE = "/opt/vmware/bin/sso-config.sh -get_identity_sources" +LDAP_SOURCE_TYPE = "IDENTITY_STORE_TYPE_LDAP_WITH_AD_MAPPING" + + +class LdapIdentitySourceConfig(BaseController): + """ + Class for ldap identity source config with get and set methods. + + | Config Id - 1230 + | Config Title - The vCenter Server must use a limited privilege account when adding an + LDAP identity source. + """ + + metadata = ControllerMetadata( + name="ldap_identity_source_config", # controller name + path_in_schema="compliance_config.vcenter.ldap_identity_source_config", # path in the schema to this controller's definition. + configuration_id="1230", # configuration id as defined in compliance kit. + title="The vCenter Server must use a limited privilege account when adding an LDAP identity source.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + functional_test_targets=["vcenter"], # location where functional tests are run. + ) + + def __parse_ldap_identity_source(self, output): + ldap_accounts = [] + # extract ldap account name and type from command output + identity_source = r"IDENTITY SOURCE INFORMATION\s+([\s\S]*?)?(?=IDENTITY SOURCE INFORMATION|$)" + domain_type = r"DomainType\s+:\s+EXTERNAL_DOMAIN" + username_pattern = r"username\s+:\s+(.*?)\s+" + provider_type_pattern = r"providerType\s+:\s+(.*?)\s+" + # Search for all text marked with "IDENTITY SOURCE INFORMATION" + identity_sources = re.finditer(identity_source, output) + # look for external domain type + for identity_source in identity_sources: + if re.search(domain_type, identity_source.group()): + # Extract username after "username :" + username_match = re.search(username_pattern, identity_source.group()) + username = username_match.group(1) if username_match else None + provider_type_match = re.search(provider_type_pattern, identity_source.group()) + provider_type = provider_type_match.group(1) if provider_type_match else None + if provider_type == LDAP_SOURCE_TYPE: + ldap_accounts.append({"username": username}) + + # if no accounts found, append a empty one (user can put an empty account on + # desired state if no ldap binding account configured. + if not ldap_accounts: + ldap_accounts.append({"username": ""}) + + return ldap_accounts + + def get(self, context: VcenterContext) -> Tuple[Dict, List[Any]]: + """ + Get details of ldap identity source of vcenter server for audit. + + :param context: Product context instance. + :type context: VcenterContext + :return: Details of the ldap account name + :rtype: tuple + """ + logger.info("Getting ldap identity source details for audit.") + errors = [] + result = [] + try: + command_output, _, _ = utils.run_shell_cmd(VC_SSO_CONFIG_CMD_GET_LDAP_IDENTITY_SOURCE) + result = self.__parse_ldap_identity_source(command_output) + except Exception as e: + logger.exception(f"Unable to fetch identity source details {e}") + errors.append(str(e)) + + return result, errors + + def set(self, context: VcenterContext, desired_values) -> Dict: + """ + Set is not implemented as this control since modifying config would impact existing auth. + Refer to Jira : VCFSC-147 + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired value for the certificate authority + :type desired_values: String or list of strings + :return: Dict of status (RemediateStatus.SKIPPED) and errors if any + :rtype: tuple + """ + errors = ["Set is not implemented as modifying config would impact existing auth."] + status = RemediateStatus.SKIPPED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/logon_banner_config.py b/config_modules_vmware/controllers/vcenter/logon_banner_config.py new file mode 100644 index 0000000..1988ec7 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/logon_banner_config.py @@ -0,0 +1,185 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +import os +import re +import tempfile +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +LOGON_BANNER_FILE = "logon_banner.txt" +LOGON_BANNER_TITLE = "logon_banner_title" +LOGON_BANNER_CONTENT = "logon_banner_content" +LOGON_BANNER_CHECKBOX = "checkbox_enabled" +VC_SSO_CONFIG_CMD_GET_LOGON_BANNER = "/opt/vmware/bin/sso-config.sh -print_logon_banner" +VC_SSO_CONFIG_CMD_SET_LOGON_BANNER = ( + '/opt/vmware/bin/sso-config.sh -set_logon_banner -title "{title}" {file} -enable_checkbox {checkbox}' +) + + +class LogonBannerConfig(BaseController): + """ + Class for logon banner config with get and set methods. + + | Config Id - 1209 + | Config Title - Configure a logon message + + """ + + metadata = ControllerMetadata( + name="logon_banner_config", # controller name + path_in_schema="compliance_config.vcenter.logon_banner_config", # path in the schema to this controller's definition. + configuration_id="1209", # configuration id as defined in compliance kit. + title="Configure a logon message", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + functional_test_targets=["vcenter"], # location where functional tests are run. + ) + + def __parse_logon_banner(self, output): + # extract Logon Banner Title + title_match = re.search(r"Logon Banner Title:\s*(.*)", output) + logon_banner_title = title_match.group(1) if title_match else None + + # extract Logon Banner Content + # 1). if contens are between double ", use pattern 1 to extract the content and remove "; + # 2). if contents are not between double ", extract the content until we see Checkbox enabled. + pattern = r'Logon Banner Content:\s*"([^"]*)"|Logon Banner Content:(.*?)(?=Checkbox enabled :)' + content_match = re.search(pattern, output, re.DOTALL) + if content_match: + logon_banner_content = ( + content_match.group(1).strip() if content_match.group(1) else content_match.group(2).strip() + ) + else: + logon_banner_content = None + + # extract Checkbox enabled + checkbox_match = re.search(r"Checkbox enabled : (.*)", output) + checkbox_enabled = checkbox_match.group(1).strip() if checkbox_match else None + checkbox_bool_value = True if checkbox_enabled == "true" else False + + return { + LOGON_BANNER_TITLE: logon_banner_title, + LOGON_BANNER_CONTENT: logon_banner_content, + LOGON_BANNER_CHECKBOX: checkbox_bool_value, + } + + def get(self, context: VcenterContext) -> Tuple[Dict, List[Any]]: + """ + Function to get logon banner details of vcenter server for audit. + + :param context: Product context instance. + :type context: VcenterContext + :return: Details of the current logon banner + :rtype: tuple + """ + logger.info("Getting logon banner for audit.") + errors = [] + result = {} + vc_sso_config_cmd_get_logon_banner = VC_SSO_CONFIG_CMD_GET_LOGON_BANNER + try: + command_output, _, _ = utils.run_shell_cmd(vc_sso_config_cmd_get_logon_banner) + result = self.__parse_logon_banner(command_output) + except Exception as e: + logger.exception(f"Unable to fetch logon banner details {e}") + errors.append(str(e)) + + return result, errors + + def __format_content(self, desired_values): + desired_values[LOGON_BANNER_CONTENT] = desired_values[LOGON_BANNER_CONTENT].replace("\\n", "\n") + return desired_values + + def set(self, context: VcenterContext, desired_values) -> Tuple: + """ + Set to replace logon banner with desired config. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the vcenter logon banner + :type desired_config: Dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + errors = [] + title = desired_values.get(LOGON_BANNER_TITLE) + content = desired_values.get(LOGON_BANNER_CONTENT) + checkbox = desired_values.get(LOGON_BANNER_CHECKBOX) + checkbox_value = "Y" if checkbox else "N" + try: + with tempfile.TemporaryDirectory() as temp_dir: + logon_banner_file = os.path.join(temp_dir, LOGON_BANNER_FILE) + # open the file in the temporary directory + with open(logon_banner_file, "w", encoding="UTF-8") as f: + # put the content in logon banner file + f.write('"{}"\n'.format(content)) + + vc_sso_config_cmd_set_logon_banner = VC_SSO_CONFIG_CMD_SET_LOGON_BANNER.format( + title=title, file=logon_banner_file, checkbox=checkbox_value + ) + _, _, _ = utils.run_shell_cmd(vc_sso_config_cmd_set_logon_banner) + status = RemediateStatus.SUCCESS + except Exception as e: + logger.exception(f"Unable to fetch logon banner details {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + + return status, errors + + def check_compliance(self, context, desired_values) -> Dict: + """ + Check compliance of logon banner in vCenter server. Customer desired logon message need to + be provided as shown in the below sample format. + + | Sample desired_values spec + + .. code-block:: json + + { + "logon_banner_title": + "vCenter Server Managed by SDDC Manager", + "logon_banner_content": + "This vCenter Server is managed by SDDC Manager (sddc-manager.vrack.vsphere.local). + Making modifications directly in vCenter Server may break SDDC Manager workflows. + Please consult the product documentation before making changes through the vSphere Client.", + "checkbox_enabled": True + } + + :param context: Product context instance. + :param desired_values: Desired value for the logon banner. + :return: Dict of status and current/desired value or errors (for failure). + :rtype: dict + """ + logger.info("Checking compliance") + desired_values = self.__format_content(desired_values) + return super().check_compliance(context, desired_values) + + def remediate(self, context, desired_values) -> Dict: + """ + Replace logon banner with the one in desired value. + + :param context: Product context instance. + :param desired_values: Desired value for the logon banner. + :return: Dict of status (RemediateStatus.SKIPPED) and errors if any + """ + logger.info("Running remediation") + desired_values = self.__format_content(desired_values) + return super().remediate(context, desired_values) diff --git a/config_modules_vmware/controllers/vcenter/ntp_config.py b/config_modules_vmware/controllers/vcenter/ntp_config.py new file mode 100644 index 0000000..5ec1b59 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/ntp_config.py @@ -0,0 +1,225 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.vcenter import vc_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class NtpConfig(BaseController): + """Manage Ntp config with get and set methods. + + | Config Id - 1246 + | Config Title - The system must configure NTP time synchronization. + """ + + metadata = ControllerMetadata( + name="ntp", # controller name + path_in_schema="compliance_config.vcenter.ntp", # path in the schema to this controller's definition. + configuration_id="1246", # configuration id as defined in compliance kit. + title="The system must configure NTP time synchronization.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def _get_ntp_mode(self, context: VcenterContext) -> Tuple[Dict, List[Any]]: + """ + Get NTP mode. + Supported values - [DISABLED, NTP, HOST]. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of dict with key "mode" and list of error messages. + :rtype: tuple + """ + logger.info("Getting NTP mode.") + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + vc_consts.TIMESYNC_URL + + errors = [] + try: + ntp_mode = vc_rest_client.get_helper(url) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + ntp_mode = "" + return ntp_mode, errors + + def _get_ntp_servers(self, context: VcenterContext) -> Tuple[Dict, List[Any]]: + """ + Get ntp servers. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of dict with key "servers" and list of error messages. + :rtype: tuple + """ + logger.info("Getting NTP servers.") + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + vc_consts.NTP_URL + + errors = [] + try: + ntp_servers = vc_rest_client.get_helper(url) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + ntp_servers = [] + return ntp_servers, errors + + def _set_ntp_mode(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Set NTP mode. + Supported values - [DISABLED, NTP, HOST]. + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the NTP mode. Dict with key "mode". + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + logger.info("Setting NTP mode.") + vc_rest_client = context.vc_rest_client() + payload = {"mode": desired_values.get("mode")} + url = vc_rest_client.get_base_url() + vc_consts.TIMESYNC_URL + + errors = [] + status = RemediateStatus.SUCCESS + try: + vc_rest_client.put_helper(url, body=payload, raise_for_status=True) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def _set_ntp_servers(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Set ntp servers. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the NTP servers. Dict with key "servers". + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + logger.info("Setting NTP servers.") + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + vc_consts.NTP_URL + payload = {"servers": desired_values.get("servers")} + + errors = [] + status = RemediateStatus.SUCCESS + try: + vc_rest_client.put_helper(url, body=payload, raise_for_status=True) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def get(self, context: VcenterContext) -> Tuple[Dict, List[Any]]: + """ + Get NTP config from vCenter. + + | Sample get output + + .. code-block:: json + + { + "mode": "NTP", + "servers": ["time.vmware.com", "time.google.com"] + } + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of dict with key "servers", "mode" and list of error messages. + :rtype: tuple + """ + logger.info("Getting NTP config") + errors = [] + ntp_servers, servers_errors = self._get_ntp_servers(context) + errors.extend(servers_errors) + + ntp_mode, mode_errors = self._get_ntp_mode(context) + errors.extend(mode_errors) + return {"mode": ntp_mode, "servers": ntp_servers}, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Set NTP config in vCenter. + + | Sample desired state for NTP. + + .. code-block:: json + + { + "mode": "NTP", + "servers": ["time.vmware.com", "time.google.com"] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired value for the NTP config. Dict with keys "servers" and "mode". + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting NTP control config for audit.") + errors = [] + + server_status, err = self._set_ntp_servers(context, desired_values) + logger.info(f"server_status: '{server_status}'") + errors.extend(err) + + mode_status, err = self._set_ntp_mode(context, desired_values) + logger.info(f"mode_status: '{mode_status}'") + errors.extend(err) + + status = RemediateStatus.SUCCESS + if errors: + status = RemediateStatus.FAILED + return status, errors + + def check_compliance(self, context, desired_values: Dict) -> Dict: + """Check compliance of current NTP configuration in vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for NTP config. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + ntp_desired_value = {"servers": desired_values.get("servers", []), "mode": desired_values.get("mode")} + return super().check_compliance(context, desired_values=ntp_desired_value) + + def remediate(self, context, desired_values: Dict) -> Dict: + """Remediate configuration drifts for NTP config in vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for NTP config. + :type desired_values: dict + :return: Dict of status and old/new values(for success) or errors (for failure). + :rtype: dict + """ + ntp_desired_value = {"servers": desired_values.get("servers", []), "mode": desired_values.get("mode")} + return super().remediate(context, desired_values=ntp_desired_value) diff --git a/config_modules_vmware/controllers/vcenter/snmp_v3_config.py b/config_modules_vmware/controllers/vcenter/snmp_v3_config.py new file mode 100644 index 0000000..5831b35 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/snmp_v3_config.py @@ -0,0 +1,186 @@ +import logging +import re +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils + +logger = LoggerAdapter(logging.getLogger(__name__)) + +# Timeouts +CMD_TIMEOUT = 10 +# Commands +APPLIANCE_SHELL_CMD_PREFIX = "/bin/appliancesh --connect {}:'{}'@localhost -c " +SNMP_GET_CMD = "snmp.get" +ENABLE_SNMP_CMD = "snmp.enable" +DISABLE_SNMP_CMD = "snmp.disable" +SNMP_SET_CMD = "snmp.set --{} {}" +# Regex patterns +SNMP_FIELDS_TO_CHECK_PATTERN = r"(Enable|Authentication|Privacy):\s*(.+)" +# Keys +PRIVACY = "privacy" +AUTHENTICATION = "authentication" +ENABLE = "enable" + + +class SNMPv3SecurityPolicy(BaseController): + """Manage vCenter SNMP v3 security config with get and set methods. + + | Config Id - 1222 + | Config Title - The vCenter server must enforce SNMPv3 security features where SNMP is required. + """ + + metadata = ControllerMetadata( + name="snmp_v3", # controller name + path_in_schema="compliance_config.vcenter.snmp_v3", # path in the schema to this controller's definition. + configuration_id="1222", # configuration id as defined in compliance kit. + title="The vCenter server must enforce SNMPv3 security features where SNMP is required.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + functional_test_targets=["vcenter"], # location where functional tests are run. + ) + + def get(self, context: VcenterContext) -> Tuple[dict, List[Any]]: + """Get SNMP v3 security config. + + | Sample get call output: + + .. code-block:: json + + { + "enable": true, + "authentication": "SHA1", # none, SHA1, SHA256, SHA384, SHA512 + "privacy": "AES128" # none, AES128, AES192, AES256. + } + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of dict and a list of error messages if any. + :rtype: tuple + """ + logger.info("Getting SNMP v3 config from vCenter") + result = None + errors = [] + try: + result = self.__get_snmp_config(context) + if not result: + raise Exception("Unable to fetch SNMP config") + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """Sets SNMP v3 security config. Enables/disables configuration and sets privacy and authentication protocols. + + | If SNMP is enabled, recommendation is to configure Authentication as SHA1 and Privacy as AES128; + if SNMP is disabled, consider the system compliant. + + | Sample desired state for SNMP security config + + .. code-block:: json + + { + "enable": true, + "authentication": "SHA1", # none, SHA1, SHA256, SHA384, SHA512 + "privacy": "AES128" # none, AES128, AES192, AES256. + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the DNS config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + logger.info("Setting SNMP config") + errors = [] + status = RemediateStatus.SUCCESS + + try: + self.__apply_snmp_config(context, desired_values) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + + return status, errors + + @staticmethod + def __get_snmp_config(context: VcenterContext) -> Dict: + """Get SNMP security config. + + :param context: Product context instance. + :type context: VcenterContext + :return: SNMP security config as Dict + :rtype: Dict + """ + appliancesh_cmd_prefix = APPLIANCE_SHELL_CMD_PREFIX.format(context._username, context._password) + snmp_get_cmd = appliancesh_cmd_prefix + SNMP_GET_CMD + + out, _, _ = utils.run_shell_cmd(command=snmp_get_cmd, timeout=CMD_TIMEOUT) + + snmp_config = {} + for match in re.finditer(SNMP_FIELDS_TO_CHECK_PATTERN, out): + if match.groups(): + key, value = match.groups() + key = key.lower().strip() + if key == ENABLE: + value = value.strip().lower() == "true" + snmp_config[key] = value + + logger.info(f"SNMP config: {snmp_config}") + return snmp_config + + @staticmethod + def __apply_snmp_config(context: VcenterContext, desired_values: dict) -> None: + """Applies SNMP configuration based on desired values. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired state for SNMP config. + :type desired_values: dict + """ + enable_snmp_config = desired_values.get(ENABLE) + privacy_algorithm = desired_values.get(PRIVACY) + authentication_algorithm = desired_values.get(AUTHENTICATION) + + if enable_snmp_config: + SNMPv3SecurityPolicy.__set_snmp_config(context, ENABLE_SNMP_CMD) + + snmp_set_authentication_algorithm_cmd = SNMP_SET_CMD.format(AUTHENTICATION, authentication_algorithm) + SNMPv3SecurityPolicy.__set_snmp_config(context, snmp_set_authentication_algorithm_cmd) + + snmp_set_privacy_algorithm_cmd = SNMP_SET_CMD.format(PRIVACY, privacy_algorithm) + SNMPv3SecurityPolicy.__set_snmp_config(context, snmp_set_privacy_algorithm_cmd) + else: + SNMPv3SecurityPolicy.__set_snmp_config(context, DISABLE_SNMP_CMD) + + @staticmethod + def __set_snmp_config(context: VcenterContext, snmp_cmd: str): + """Logs into vCenter appliance shell and sets SNMP config property. + + :param context: Product context instance. + :type context: VcenterContext + :param snmp_cmd: SNMP command to execute + :type snmp_cmd: str + :return: None + """ + appliancesh_cmd_prefix = APPLIANCE_SHELL_CMD_PREFIX.format(context._username, context._password) + set_snmp_cmd = appliancesh_cmd_prefix + f'"{snmp_cmd}"' + utils.run_shell_cmd(command=set_snmp_cmd, timeout=CMD_TIMEOUT) diff --git a/config_modules_vmware/controllers/vcenter/sso_active_directory_authentication_policy.py b/config_modules_vmware/controllers/vcenter/sso_active_directory_authentication_policy.py new file mode 100644 index 0000000..0aae54d --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_active_directory_authentication_policy.py @@ -0,0 +1,86 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ACTIVE_DIRECTORY = "ActiveDirectory" +TYPE = "type" +EXTERNAL_DOMAINS_PYVMOMI_KEY = "externalDomains" + + +class SSOActiveDirectoryAuthPolicy(BaseController): + """Manage active directory authentication for VC with get and set methods. + + | Config Id - 1228 + | Config Title - The vCenter Server must implement Active Directory authentication. + + """ + + metadata = ControllerMetadata( + name="active_directory_authentication", # controller name + path_in_schema="compliance_config.vcenter.active_directory_authentication", + # path in the schema to this controller's definition. + configuration_id="1228", # configuration id as defined in compliance kit. + title="The vCenter Server must implement Active Directory authentication.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, + # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[bool, List[Any]]: + """Get active directory authentication config for VC. + + | If there is at least one external domain of type = 'ActiveDirectory', then we consider the system compliant. + + :param context: Product context instance. + :type context: VcenterContext + :return: A tuple indicating whether one or more external domains of Active Directory (AD) are configured, + along with a list of associated error messages. + :rtype: tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + all_domains = vc_vmomi_sso_client.get_all_domains() + logger.info(f"All domains in VC {all_domains}") + external_domains = getattr(all_domains, EXTERNAL_DOMAINS_PYVMOMI_KEY, []) + logger.info(f"External domains in VC {external_domains}") + result = any([getattr(domain, TYPE, None) == ACTIVE_DIRECTORY for domain in external_domains]) + except Exception as e: + logger.exception(f"An error occurred while retrieving external domains: {e}") + errors.append(str(e)) + result = None + return result, errors + + def set(self, context: VcenterContext, desired_values: bool) -> Tuple[str, List[Any]]: + """Set requires manual intervention, as seamlessly integrating AD with vCenter is not possible and + often requires a service or appliance restart. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Bool to enforce active directory based authentication in vCenter. + :type desired_values: bool + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_active_directory_ldaps_enabled_config.py b/config_modules_vmware/controllers/vcenter/sso_active_directory_ldaps_enabled_config.py new file mode 100644 index 0000000..7bb4d70 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_active_directory_ldaps_enabled_config.py @@ -0,0 +1,162 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +ACTIVE_DIRECTORY = "ActiveDirectory" +TYPE = "type" +PATTERN = "ldaps://" +EXTERNAL_DOMAINS_PYVMOMI_KEY = "externalDomains" + + +class SSOActiveDirectoryLdapsEnabledPolicy(BaseController): + """Manage active directory LDAPS enabled config for VC with get and set methods. + + | Config Id - 1229 + | Config Title - The vCenter Server must use LDAPS when adding an SSO identity source. + + """ + + metadata = ControllerMetadata( + name="active_directory_ldaps_enabled", # controller name + path_in_schema="compliance_config.vcenter.active_directory_ldaps_enabled", + # path in the schema to this controller's definition. + configuration_id="1229", # configuration id as defined in compliance kit. + title="The vCenter Server must use LDAPS when adding an SSO identity source.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, + # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List, List[Any]]: + """Get active directory authentication config for VC. + + | If any external domain is of type 'ActiveDirectory' and doesn't use LDAPS, then the system is deemed + non-compliant. + + :param context: Product context instance. + :type context: VcenterContext + :return: A tuple indicating that at least one Active Directory (AD) external domain is configured without LDAPS, + along with a list of associated error messages. + :rtype: tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + all_domains = vc_vmomi_sso_client.get_all_domains() + logger.info(f"All domains in VC {all_domains}") + external_domains = getattr(all_domains, "externalDomains", []) + logger.info(f"External domains in VC {external_domains}") + result = self.__get_non_compliant_domains(external_domains) + except Exception as e: + logger.exception(f"An error occurred while retrieving external domains: {e}") + errors.append(str(e)) + result = None + return result, errors + + def __get_non_compliant_domains(self, external_domains) -> List: + """Get list of all non-compliant AD domains not using LDAPS. + + :param external_domains: List of SSO ExternalDomain objects with AD details. + :return: + """ + non_compliant_external_domains = [] + for external_domain in external_domains: + ad_details = { + "domain_name": external_domain.name, + "domain_alias": external_domain.alias, + "user_base_dn": None, + "group_base_dn": None, + "primary_server_url": None, + "failover_server_url": None, + "use_ldaps": False, + } + + details = getattr(external_domain, "details") + if details: + ad_details["user_base_dn"] = getattr(details, "userBaseDn") + ad_details["group_base_dn"] = getattr(details, "groupBaseDn") + ad_details["primary_server_url"] = getattr(details, "primaryUrl") + ad_details["failover_server_url"] = getattr(details, "failoverUrl") + + is_primary_compliant = ad_details["primary_server_url"] and ad_details["primary_server_url"].startswith( + PATTERN + ) + # If failover is not set, we treat it as compliant. + is_failover_compliant = ad_details["failover_server_url"] is None or ad_details[ + "failover_server_url" + ].startswith(PATTERN) + + ad_details["use_ldaps"] = is_primary_compliant and is_failover_compliant + + if not ad_details["use_ldaps"]: + non_compliant_external_domains.append(ad_details) + + return non_compliant_external_domains + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """Set requires manual intervention, as seamlessly integrating AD with vCenter is not possible and + often requires a service or appliance restart. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Dict containing use_ldaps bool to enforce active directory based authentication in + vCenter. + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors + + def check_compliance(self, context: VcenterContext, desired_values: Dict) -> Dict: + """Check compliance of Active directories configurations in VC, if AD is configured but it does not use LDAPS + protocol then flag it as non-compliant. + + | The audit process flags an Active Directory source as non-compliant if it does not use LDAPS protocol. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Dict containing use_ldaps bool to enforce active directory based authentication in + vCenter. When set to true (the only allowed value), the audit process flags an Active directory as + non-compliant if it does not use LDAPS protocol. + :type desired_values: Dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: Dict + """ + logger.info("Checking compliance") + non_compliant_active_directory_configs, errors = self.get(context=context) + + if errors: + return {consts.STATUS: ComplianceStatus.FAILED, consts.ERRORS: errors} + + if non_compliant_active_directory_configs: + result = { + consts.STATUS: ComplianceStatus.NON_COMPLIANT, + consts.CURRENT: non_compliant_active_directory_configs, + consts.DESIRED: desired_values, + } + else: + result = {consts.STATUS: ComplianceStatus.COMPLIANT} + return result diff --git a/config_modules_vmware/controllers/vcenter/sso_auto_unlock_interval.py b/config_modules_vmware/controllers/vcenter/sso_auto_unlock_interval.py new file mode 100644 index 0000000..36548b3 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_auto_unlock_interval.py @@ -0,0 +1,82 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SSOAutoUnlockInterval(BaseController): + """Manage SSO Auto Unlock Interval Policy with get and set methods. + + | Config Id - 435 + | Config Title - The vCenter server passwords should meet max auto unlock interval policy. + + """ + + metadata = ControllerMetadata( + name="sso_auto_unlock_interval", # controller name + path_in_schema="compliance_config.vcenter.sso_auto_unlock_interval", # path in the schema to this controller's definition. + configuration_id="435", # configuration id as defined in compliance kit. + title="The vCenter server passwords should meet max auto unlock interval policy.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[int, List[Any]]: + """ + Get SSO auto unlock interval. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of an integer for the auto unlock interval in seconds and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + result = vc_vmomi_sso_client.get_auto_unlock_interval() + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = -1 + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """ + Set SSO auto unlock interval. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the SSO auto unlock interval in seconds. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + sso_auto_unlock_interval_sec = desired_values + if not isinstance(sso_auto_unlock_interval_sec, int) or sso_auto_unlock_interval_sec < 0: + raise ValueError("value must be a positive integer") + vc_vmomi_sso_client.set_auto_unlock_interval(interval=sso_auto_unlock_interval_sec) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_bash_shell_authorized_members_config.py b/config_modules_vmware/controllers/vcenter/sso_bash_shell_authorized_members_config.py new file mode 100644 index 0000000..76c43ca --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_bash_shell_authorized_members_config.py @@ -0,0 +1,165 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client import VcVmomiSSOClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +BASH_SHELL_ADMINISTRATOR_GROUP_KEY = "SystemConfiguration.BashShellAdministrators" +ID_KEY = "id" +DOMAIN_KEY = "domain" +NAME_KEY = "name" +MEMBER_TYPE_KEY = "member_type" +MEMBER_TYPE_USER = "USER" +MEMBER_TYPE_GROUP = "GROUP" + + +class SSOBashShellAuthorizedMembersConfig(BaseController): + """Manage authorized members in the SystemConfiguration.BashShellAdministrators group with get and set methods. + + | Config Id - 1216 + | Config Title - vCenter must limit membership to the SystemConfiguration.BashShellAdministrators SSO group. + + """ + + metadata = ControllerMetadata( + name="sso_bash_shell_authorized_members", # controller name + path_in_schema="compliance_config.vcenter.sso_bash_shell_authorized_members", + # path in the schema to this controller's definition. + configuration_id="1216", # configuration id as defined in compliance kit. + title="vCenter must limit membership to the SystemConfiguration.BashShellAdministrators SSO group.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, + # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List, List[Any]]: + """Get authorized members in the SystemConfiguration.BashShellAdministrators group. + + | We limit our traversal to the first level of groups because a group can have subgroups, which in turn can + contain groups with users. This approach is consistent with the behavior of the dir-cli command: + + .. code-block:: shell + + /usr/lib/vmware-vmafd/bin/dir-cli group list --name + + | Sample get output + + .. code-block:: json + + [ + { + "name": "user-1", + "domain": "vmware.com", + "member_type": "USER" + }, + { + "name": "user-2", + "domain": "vmware.com", + "member_type": "USER" + }, + { + "name": "devops", + "domain": "vsphere.local", + "member_type": "GROUP" + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of List of dictionaries containing user and groups belonging to the BashShellAdministrators + group and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + result = self.__get_all_members_of_bash_shell_administrator_group(vc_vmomi_sso_client) + except Exception as e: + logger.exception(f"An error occurred while retrieving members of bash shell admin group: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def __get_all_members_of_bash_shell_administrator_group(self, sso_client: VcVmomiSSOClient) -> List[Dict]: + """Retrieve all members in SystemConfiguration.BashShellAdministrators group. + + :param sso_client: VcVmomiSSOClient instance + :type sso_client: VcVmomiSSOClient + :return: List of dictionaries containing user and groups belonging to the BashShellAdministrators group. + :rtype: List[Dict] + """ + members_in_bash_shell_administrators_group = [] + + system_domain = sso_client.get_system_domain() + logger.info(f"Retrieved system domain - {system_domain}") + + bash_shell_admin_group = sso_client._get_group(BASH_SHELL_ADMINISTRATOR_GROUP_KEY, system_domain) + logger.info(f"Retrieved bash shell admin group - {bash_shell_admin_group}") + + if bash_shell_admin_group and hasattr(bash_shell_admin_group, ID_KEY): + group_id = bash_shell_admin_group.id + users_in_group = sso_client.find_users_in_group(group_id) + logger.debug(f"Users in group - {users_in_group}") + groups_in_group = sso_client.find_groups_in_group(group_id) + logger.debug(f"Groups in group - {groups_in_group}") + + for user in users_in_group: + if hasattr(user, ID_KEY): + user_principal_id = user.id + user_name = getattr(user_principal_id, NAME_KEY) + domain = getattr(user_principal_id, DOMAIN_KEY) + + if user_name and domain: + user = {NAME_KEY: user_name, DOMAIN_KEY: domain, MEMBER_TYPE_KEY: MEMBER_TYPE_USER} + members_in_bash_shell_administrators_group.append(user) + + for group in groups_in_group: + if hasattr(group, ID_KEY): + group_principal_id = group.id + group_name = getattr(group_principal_id, NAME_KEY) + domain = getattr(group_principal_id, DOMAIN_KEY) + + if group_name and domain: + group = {NAME_KEY: group_name, DOMAIN_KEY: domain, MEMBER_TYPE_KEY: MEMBER_TYPE_GROUP} + members_in_bash_shell_administrators_group.append(group) + + logger.info( + f"Retrieved all members in SystemConfiguration.BashShellAdministrators" + f" group {members_in_bash_shell_administrators_group}" + ) + return members_in_bash_shell_administrators_group + + def set(self, context: VcenterContext, desired_values: List) -> Tuple[str, List[Any]]: + """Remediation has not been implemented for this control. It's possible that a customer may legitimately add + a new user and forget to update the control accordingly. Remediating the control could lead to the removal + of these users, with potential unknown implications. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: List of objects containing users and groups details with name, domain and member_type. + :type desired_values: List + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_failed_login_attempt_interval.py b/config_modules_vmware/controllers/vcenter/sso_failed_login_attempt_interval.py new file mode 100644 index 0000000..04e1138 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_failed_login_attempt_interval.py @@ -0,0 +1,82 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SSOFailedLoginAttemptInterval(BaseController): + """Manage SSO Failed Login Attempt Interval Policy with get and set methods. + + | Config Id - 434 + | Config Title - The vCenter server should meet failed login attempts interval. + + """ + + metadata = ControllerMetadata( + name="sso_failed_login_attempts_interval", # controller name + path_in_schema="compliance_config.vcenter.sso_failed_login_attempts_interval", # path in the schema to this controller's definition. + configuration_id="434", # configuration id as defined in compliance kit. + title="The vCenter server should meet failed login attempts interval.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[int, List[Any]]: + """ + Get SSO failed login attempt interval. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of an integer for the failed login attempt interval in seconds and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + result = vc_vmomi_sso_client.get_interval_between_login_failures() + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = -1 + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """ + Set SSO failed login attempt interval. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the SSO failed login attempt interval in seconds. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + sso_failed_login_attempt_interval = desired_values + if not isinstance(sso_failed_login_attempt_interval, int) or sso_failed_login_attempt_interval < 0: + raise ValueError("value must be a positive integer") + vc_vmomi_sso_client.set_interval_between_login_failures(interval=sso_failed_login_attempt_interval) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_max_failed_login_attempts.py b/config_modules_vmware/controllers/vcenter/sso_max_failed_login_attempts.py new file mode 100644 index 0000000..cc2e1d6 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_max_failed_login_attempts.py @@ -0,0 +1,84 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SSOMaxFailedLoginAttempts(BaseController): + """Manage SSO Max Failed Login Attempts Policy with get and set methods. + + | Config Id - 436 + | Config Title - The vCenter server should meet max failed login attempts. + + """ + + metadata = ControllerMetadata( + name="sso_max_failed_login_attempts", # controller name + path_in_schema="compliance_config.vcenter.sso_max_failed_login_attempts", # path in the schema to this controller's definition. + configuration_id="436", # configuration id as defined in compliance kit. + title="The vCenter server should meet max failed login attempts.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List[Dict], List[Any]]: + """ + Get SSO max failed login attempts. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of an integer for the max failed login attempts and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + max_failed_login_attempts = vc_vmomi_sso_client.get_max_failed_login_attempts() + result = max_failed_login_attempts + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = -1 + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """ + Set SSO max failed login attempts. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the SSO max failed login attempts. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + sso_max_failed_login_attempts = desired_values + if not isinstance(sso_max_failed_login_attempts, int) or sso_max_failed_login_attempts < 0: + raise ValueError("value must be a positive integer") + vc_vmomi_sso_client.set_max_failed_login_attempts(attempts=sso_max_failed_login_attempts) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_password_max_lifetime_policy.py b/config_modules_vmware/controllers/vcenter/sso_password_max_lifetime_policy.py new file mode 100644 index 0000000..bd0368f --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_password_max_lifetime_policy.py @@ -0,0 +1,83 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SSOPasswordMaxLifetimePolicy(BaseController): + """Manage SSO Password Max Lifetime Policy with get and set methods. + + | Config Id - 421 + | Config Title - The vCenter server passwords should meet max password lifetime policy. + + """ + + metadata = ControllerMetadata( + name="sso_password_max_lifetime", # controller name + path_in_schema="compliance_config.vcenter.sso_password_max_lifetime", # path in the schema to this controller's definition. + configuration_id="421", # configuration id as defined in compliance kit. + title="The vCenter server passwords should meet max password lifetime policy.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[int, List[Any]]: + """ + Get SSO max password lifetime policy. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of an integer for the max password lifetime in days and a list of error messages. + :rtype: tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + result = vc_vmomi_sso_client.get_password_lifetime_days() + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = -1 + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """ + Set SSO max password lifetime policy. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the SSO max password lifetime in days. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + sso_password_max_lifetime_days = desired_values + if not isinstance(sso_password_max_lifetime_days, int) or sso_password_max_lifetime_days < 0: + raise ValueError("value must be a positive integer") + vc_vmomi_sso_client.set_password_lifetime_days(days=sso_password_max_lifetime_days) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_password_min_lowercase_character_policy.py b/config_modules_vmware/controllers/vcenter/sso_password_min_lowercase_character_policy.py new file mode 100644 index 0000000..39f7bf5 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_password_min_lowercase_character_policy.py @@ -0,0 +1,83 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SSOPasswordMinLowercaseCharacterPolicy(BaseController): + """Class for SSO Password min lowercase character Policy with get and set methods. + + | Config Id - 413 + | Config Title - The vCenter Server passwords must must meet minimum lowercase character policy. + + """ + + metadata = ControllerMetadata( + name="sso_password_min_lowercase_characters", # controller name + path_in_schema="compliance_config.vcenter.sso_password_min_lowercase_characters", # path in the schema to this controller's definition. + configuration_id="413", # configuration id as defined in compliance kit. + title="The vCenter Server passwords must must meet minimum lowercase character policy.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[int, List[Any]]: + """ + Get SSO password min lowercase character policy. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of an integer for the min number of lower case characters and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + sso_password_min_lowercase_characters = vc_vmomi_sso_client.get_min_number_of_lower_characters() + result = sso_password_min_lowercase_characters + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = -1 + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """ + Set SSO password min lowercase character policy. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the min number of lower case characters. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + sso_password_min_lowercase_characters = desired_values + if not isinstance(sso_password_min_lowercase_characters, int) or sso_password_min_lowercase_characters < 0: + raise ValueError("value must be a positive integer") + vc_vmomi_sso_client.enforce_min_number_of_lower_characters(num=sso_password_min_lowercase_characters) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_password_min_numeric_character_policy.py b/config_modules_vmware/controllers/vcenter/sso_password_min_numeric_character_policy.py new file mode 100644 index 0000000..131b59b --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_password_min_numeric_character_policy.py @@ -0,0 +1,86 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SSOPasswordMinNumericCharacterPolicy(BaseController): + """Manage SSO Password min numeric character Policy with get and set methods. + + | Config Id - 433 + | Config Title - The vCenter Server passwords must meet minimum numeric character policy. + + """ + + metadata = ControllerMetadata( + name="sso_password_min_numeric_characters", # controller name + path_in_schema="compliance_config.vcenter.sso_password_min_numeric_characters", # path in the schema to this controller's definition. + configuration_id="433", # configuration id as defined in compliance kit. + title="The vCenter Server passwords must meet minimum numeric character policy.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[int, List[Any]]: + """ + Get SSO password min numeric character policy. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of an integer for the min number of numeric characters and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + sso_password_min_numeric_character_length = vc_vmomi_sso_client.get_min_number_of_numeric_characters() + result = sso_password_min_numeric_character_length + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = -1 + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """ + Set SSO password min numeric character policy. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the min number of numeric characters. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + sso_password_min_numeric_character_length = desired_values + if ( + not isinstance(sso_password_min_numeric_character_length, int) + or sso_password_min_numeric_character_length < 0 + ): + raise ValueError("value must be a positive integer") + vc_vmomi_sso_client.enforce_min_number_of_numeric_characters(num=sso_password_min_numeric_character_length) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_password_min_special_character_policy.py b/config_modules_vmware/controllers/vcenter/sso_password_min_special_character_policy.py new file mode 100644 index 0000000..433127a --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_password_min_special_character_policy.py @@ -0,0 +1,83 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SSOPasswordMinSpecialCharacterPolicy(BaseController): + """Manage SSO Password min special character Policy with get and set methods. + + | Config Id - 432 + | Config Title - The vCenter Server passwords must meet minimum special character policy. + + """ + + metadata = ControllerMetadata( + name="sso_password_min_special_characters", # controller name + path_in_schema="compliance_config.vcenter.sso_password_min_special_characters", # path in the schema to this controller's definition. + configuration_id="432", # configuration id as defined in compliance kit. + title="The vCenter Server passwords must meet minimum special character policy.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[int, List[Any]]: + """ + Get SSO password min special character policy. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of an integer for the min number of special characters and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + sso_password_min_special_characters = vc_vmomi_sso_client.get_minimum_number_of_special_characters() + result = sso_password_min_special_characters + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = -1 + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """ + Set SSO password min special character policy. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the min number of special characters. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + sso_password_min_special_characters = desired_values + if not isinstance(sso_password_min_special_characters, int) or sso_password_min_special_characters < 0: + raise ValueError("value must be a positive integer") + vc_vmomi_sso_client.enforce_minimum_number_of_special_characters(num=sso_password_min_special_characters) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_password_min_uppercase_character_policy.py b/config_modules_vmware/controllers/vcenter/sso_password_min_uppercase_character_policy.py new file mode 100644 index 0000000..4ac35f5 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_password_min_uppercase_character_policy.py @@ -0,0 +1,83 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SSOPasswordMinUppercaseCharacterPolicy(BaseController): + """Manage SSO Password min uppercase character Policy with get and set methods. + + | Config Id - 408 + | Config Title - The vCenter Server passwords must meet minimum uppercase character policy. + + """ + + metadata = ControllerMetadata( + name="sso_password_min_uppercase_characters", # controller name + path_in_schema="compliance_config.vcenter.sso_password_min_uppercase_characters", # path in the schema to this controller's definition. + configuration_id="408", # configuration id as defined in compliance kit. + title="The vCenter Server passwords must meet minimum uppercase character policy.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[int, List[Any]]: + """ + Get SSO password min uppercase character policy. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of an integer for the min number of upper case characters and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + sso_password_min_uppercase_characters = vc_vmomi_sso_client.get_min_number_of_upper_characters() + result = sso_password_min_uppercase_characters + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = -1 + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """ + Set SSO password min uppercase character policy. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the min number of upper case characters. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + sso_password_min_uppercase_characters = desired_values + if not isinstance(sso_password_min_uppercase_characters, int) or sso_password_min_uppercase_characters < 0: + raise ValueError("value must be a positive integer") + vc_vmomi_sso_client.enforce_min_number_of_upper_characters(num=sso_password_min_uppercase_characters) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_password_minimum_length_policy.py b/config_modules_vmware/controllers/vcenter/sso_password_minimum_length_policy.py new file mode 100644 index 0000000..9510319 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_password_minimum_length_policy.py @@ -0,0 +1,83 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SSOPasswordMinimumLengthPolicy(BaseController): + """Manage SSO Password min length Policy with get and set methods. + + | Config Id - 410 + | Config Title - The vCenter Server passwords must meet minimum password length policy. + + """ + + metadata = ControllerMetadata( + name="sso_password_minimum_length", # controller name + path_in_schema="compliance_config.vcenter.sso_password_minimum_length", # path in the schema to this controller's definition. + configuration_id="410", # configuration id as defined in compliance kit. + title="The vCenter Server passwords must meet minimum password length policy.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[int, List[Any]]: + """ + Get SSO password min length policy. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of an integer for the min password length and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + sso_password_minimum_length = vc_vmomi_sso_client.get_minimum_password_length() + result = sso_password_minimum_length + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = -1 + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """ + Set SSO password min length policy. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the min password length. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + sso_password_minimum_length = desired_values + if not isinstance(sso_password_minimum_length, int) or sso_password_minimum_length < 0: + raise ValueError("value must be a positive integer") + vc_vmomi_sso_client.enforce_minimum_password_length(length=sso_password_minimum_length) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_password_reuse_restriction_policy.py b/config_modules_vmware/controllers/vcenter/sso_password_reuse_restriction_policy.py new file mode 100644 index 0000000..2b56bdb --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_password_reuse_restriction_policy.py @@ -0,0 +1,82 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SSOPasswordReusePolicy(BaseController): + """Manage SSO Password reuse restriction Policy with get and set methods. + + | Config Id - 403 + | Config Title - The vCenter Server must prohibit password reuse. + + """ + + metadata = ControllerMetadata( + name="sso_password_reuse_restriction", # controller name + path_in_schema="compliance_config.vcenter.sso_password_reuse_restriction", # path in the schema to this controller's definition. + configuration_id="403", # configuration id as defined in compliance kit. + title="The vCenter Server must prohibit password reuse.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[int, List[Any]]: + """ + Get SSO password reuse restriction policy. + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of an integer for the number of previous passwords restricted and a list of error messages. + :rtype: tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + result = vc_vmomi_sso_client.get_password_reuse_restriction() + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = -1 + return result, errors + + def set(self, context: VcenterContext, desired_values: int) -> Tuple[str, List[Any]]: + """ + Set SSO password reuse restriction policy. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the number of previous passwords restricted. + :type desired_values: int + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + sso_prohibit_password_reuse_count = desired_values + if not isinstance(sso_prohibit_password_reuse_count, int) or sso_prohibit_password_reuse_count < 0: + raise ValueError("value must be a positive integer") + vc_vmomi_sso_client.set_password_reuse_restriction(restrict_count=sso_prohibit_password_reuse_count) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/sso_trusted_admins_authorized_members_config.py b/config_modules_vmware/controllers/vcenter/sso_trusted_admins_authorized_members_config.py new file mode 100644 index 0000000..5653952 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/sso_trusted_admins_authorized_members_config.py @@ -0,0 +1,162 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_sso_client import VcVmomiSSOClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +TRUSTED_ADMINS_GROUP_KEY = "TrustedAdmins" +ID_KEY = "id" +DOMAIN_KEY = "domain" +NAME_KEY = "name" +MEMBER_TYPE_KEY = "member_type" +MEMBER_TYPE_USER = "USER" +MEMBER_TYPE_GROUP = "GROUP" + + +class SSOTrustedAdminsAuthorizedMembersConfig(BaseController): + """Manage authorized members in the TrustedAdmins group with get and set methods. + + | Config Id - 1217 + | Config Title - vCenter must limit membership to the TrustedAdmins SSO group. + + """ + + metadata = ControllerMetadata( + name="sso_trusted_admin_authorized_members", # controller name + path_in_schema="compliance_config.vcenter.sso_trusted_admin_authorized_members", + # path in the schema to this controller's definition. + configuration_id="1217", # configuration id as defined in compliance kit. + title="vCenter must limit membership to the TrustedAdmins SSO group.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.REMEDIATION_SKIPPED, + # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[List, List[Any]]: + """Get authorized members (users and groups) in the TrustedAdmins group. + + | We limit our traversal to the first level of groups because a group can have subgroups, which in turn can + contain groups with users. This approach is consistent with the behavior of the dir-cli command: + + .. code-block:: shell + + /usr/lib/vmware-vmafd/bin/dir-cli group list --name + + | Sample get output + + .. code-block:: json + + [ + { + "name": "user-1", + "domain": "vmware.com", + "member_type": "USER" + }, + { + "name": "user-2", + "domain": "vmware.com", + "member_type": "USER" + }, + { + "name": "devops", + "domain": "vsphere.local", + "member_type": "GROUP" + } + ] + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of List of dictionaries containing user and groups belonging to the TrustedAdmins group and a + list of error messages. + :rtype: tuple + """ + vc_vmomi_sso_client = context.vc_vmomi_sso_client() + errors = [] + try: + result = self.__get_all_members_of_trusted_admins_group(vc_vmomi_sso_client) + except Exception as e: + logger.exception(f"An error occurred whole retrieving members from SSO TrustedAdmins group: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def __get_all_members_of_trusted_admins_group(self, sso_client: VcVmomiSSOClient) -> List[Dict]: + """Retrieve all members in TrustedAdmins group. + + :param sso_client: VcVmomiSSOClient instance + :type sso_client: VcVmomiSSOClient + :return: List of dictionaries containing user and groups belonging to the TrustedAdmins group. + :rtype: List[Dict] + """ + members_in_trusted_admins_group = [] + + system_domain = sso_client.get_system_domain() + logger.info(f"Retrieved system domain - {system_domain}") + + trusted_admins_group = sso_client._get_group(TRUSTED_ADMINS_GROUP_KEY, system_domain) + logger.info(f"Retrieved TrustedAdmins group - {trusted_admins_group}") + + if trusted_admins_group and hasattr(trusted_admins_group, ID_KEY): + group_id = trusted_admins_group.id + users_in_group = sso_client.find_users_in_group(group_id) + logger.debug(f"Users in group - {users_in_group}") + groups_in_group = sso_client.find_groups_in_group(group_id) + logger.debug(f"Groups in group - {groups_in_group}") + + for user in users_in_group: + if hasattr(user, ID_KEY): + user_principal_id = user.id + user_name = getattr(user_principal_id, NAME_KEY) + domain = getattr(user_principal_id, DOMAIN_KEY) + + if user_name and domain: + user = {NAME_KEY: user_name, DOMAIN_KEY: domain, MEMBER_TYPE_KEY: MEMBER_TYPE_USER} + members_in_trusted_admins_group.append(user) + + for group in groups_in_group: + if hasattr(group, ID_KEY): + group_principal_id = group.id + group_name = getattr(group_principal_id, NAME_KEY) + domain = getattr(group_principal_id, DOMAIN_KEY) + + if group_name and domain: + group = {NAME_KEY: group_name, DOMAIN_KEY: domain, MEMBER_TYPE_KEY: MEMBER_TYPE_GROUP} + members_in_trusted_admins_group.append(group) + + logger.info(f"Retrieved all members in TrustedAdmins group {members_in_trusted_admins_group}") + return members_in_trusted_admins_group + + def set(self, context: VcenterContext, desired_values: List) -> Tuple[str, List[Any]]: + """Remediation has not been implemented for this control. It's possible that a customer may legitimately add + a new user and forget to update the control accordingly. Remediating the control could lead to the removal of + these users, with potential unknown implications. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: List of objects containing users and groups details with name, domain and member_type. + :type desired_values: List + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + errors = [consts.REMEDIATION_SKIPPED_MESSAGE] + status = RemediateStatus.SKIPPED + return status, errors diff --git a/config_modules_vmware/controllers/vcenter/syslog_config.py b/config_modules_vmware/controllers/vcenter/syslog_config.py new file mode 100644 index 0000000..5cb985e --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/syslog_config.py @@ -0,0 +1,152 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.vcenter import vc_consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + + +class SyslogConfig(BaseController): + """Manage Syslog config with get and set methods. + + | Config Id - 1218 + | Config Title - The vCenter Server must be configured to send logs to a central log server. + + """ + + metadata = ControllerMetadata( + name="syslog", # controller name + path_in_schema="compliance_config.vcenter.syslog", # path in the schema to this controller's definition. + configuration_id="1218", # configuration id as defined in compliance kit. + title="The vCenter Server must be configured to send logs to a central log server.", + # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[Dict, List[Any]]: + """ + Get Syslog config from vCenter. + + | sample get call output + + .. code-block:: json + + { + "servers": [ + { + "hostname": "8.8.4.4", + "port": 90, + "protocol": "TLS" + }, + { + "hostname": "8.8.1.8", + "port": 90, + "protocol": "TLS" + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of dict with key "servers" and list of error messages. + :rtype: tuple + """ + logger.info("Getting Syslog config.") + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + vc_consts.SYSLOG_URL + errors = [] + try: + syslog_config = vc_rest_client.get_helper(url) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + syslog_config = [] + return {"servers": syslog_config}, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Set Syslog config for the audit control. + + | Sample desired state for syslog config + + .. code-block:: json + + { + "servers": [ + { + "hostname": "10.0.0.250", + "port": 514, + "protocol": "TLS" + }, + { + "hostname": "10.0.0.251", + "port": 514, + "protocol": "TLS" + } + ] + } + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the Syslog config. + :type desired_values: dict + :return: Tuple of "status" and list of error messages. + :rtype: tuple + """ + logger.info("Setting Syslog control config for remediation.") + vc_rest_client = context.vc_rest_client() + url = vc_rest_client.get_base_url() + vc_consts.SYSLOG_URL + payload = {"cfg_list": desired_values.get("servers")} + + errors = [] + status = RemediateStatus.SUCCESS + try: + vc_rest_client.put_helper(url, body=payload, raise_for_status=True) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def check_compliance(self, context, desired_values: Dict) -> Dict: + """Check compliance of current syslog configuration in vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the syslog config. + :type desired_values: dict + :return: Dict of status and current/desired value(for non_compliant) or errors (for failure). + :rtype: dict + """ + syslog_desired_value = {"servers": desired_values.get("servers", [])} + return super().check_compliance(context, desired_values=syslog_desired_value) + + def remediate(self, context: BaseContext, desired_values: Dict) -> Dict: + """Remediate syslog configuration drifts in vCenter. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the syslog config. + :type desired_values: Any + :return: Dict of status and old/new values(for success) or errors (for failure). + :rtype: dict + """ + syslog_desired_value = {"servers": desired_values.get("servers", [])} + return super().remediate(context, desired_values=syslog_desired_value) diff --git a/config_modules_vmware/controllers/vcenter/task_and_event_retention_policy.py b/config_modules_vmware/controllers/vcenter/task_and_event_retention_policy.py new file mode 100644 index 0000000..b05ec4f --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/task_and_event_retention_policy.py @@ -0,0 +1,191 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.vcenter.vc_vmomi_client import VcVmomiClient +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus + +logger = LoggerAdapter(logging.getLogger(__name__)) + +# VPX Config keys +VPX_TASK_MAX_AGE_KEY = "task.maxAge" +VPX_TASK_CLEANUP_ENABLED_KEY = "task.maxAgeEnabled" +VPX_EVENT_MAX_AGE_KEY = "event.maxAge" +VPX_EVENT_CLEANUP_ENABLED_KEY = "event.maxAgeEnabled" + +# Desired state keys +DESIRED_TASK_MAX_AGE_KEY = "task_max_age" +DESIRED_TASK_CLEANUP_ENABLED_KEY = "task_cleanup_enabled" +DESIRED_EVENT_MAX_AGE_KEY = "event_max_age" +DESIRED_EVENT_CLEANUP_ENABLED_KEY = "event_cleanup_enabled" + + +class TaskAndEventRetentionPolicy(BaseController): + """Manage Task and Event retention policy with get and set methods. + + | Config Id - 1226 + | Config Title - vCenter task and event retention must be set to a defined number of days. + + """ + + metadata = ControllerMetadata( + name="task_and_event_retention", # controller name + path_in_schema="compliance_config.vcenter.task_and_event_retention", # path in the schema to this controller's definition. + configuration_id="1226", # configuration id as defined in compliance kit. + title="vCenter task and event retention must be set to a defined number of days.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=None, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + ) + + def get(self, context: VcenterContext) -> Tuple[Dict, List[Any]]: + """ + Get Task and event retention policy for vCenter. + + | Sample get output + + .. code-block:: json + + { + "task_cleanup_enabled": true, + "task_max_age": 50, + "event_cleanup_enabled": true, + "event_max_age": 50 + } + + :param context: Product context instance. + :type context: VcenterContext + :return: Tuple of dict with task and event retention policy and a list of error messages. + :rtype: Tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + try: + result = self.__get_task_and_event_retention_policy(vc_vmomi_client) + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + result = [] + return result, errors + + def set(self, context: VcenterContext, desired_values: Dict) -> Tuple[str, List[Any]]: + """ + Set Task and Event retention policy. + + | Recommended value task and event retention: >=30 days + + | Sample desired state + + .. code-block:: json + + { + "task_cleanup_enabled": true, + "task_max_age": 30, + "event_cleanup_enabled": true, + "event_max_age": 30 + } + + | Note: Increasing the events retention to more than 30 days will result in a significant increase of vCenter + database size and could shut down the vCenter Server. + | Please ensure that you enlarge the vCenter database accordingly. + | Applied changes will take effect only after restarting vCenter Server manually. + + :param context: Product context instance. + :type context: VcenterContext + :param desired_values: Desired values for the Task and event retention policies. + :type desired_values: Dict + :return: Tuple of "status" and list of error messages. + :rtype: Tuple + """ + vc_vmomi_client = context.vc_vmomi_client() + errors = [] + status = RemediateStatus.SUCCESS + try: + _update_successful = self.__set_task_and_event_retention_policy(vc_vmomi_client, desired_values) + if not _update_successful: + status = RemediateStatus.FAILED + except Exception as e: + logger.exception(f"An error occurred: {e}") + errors.append(str(e)) + status = RemediateStatus.FAILED + return status, errors + + def __get_task_and_event_retention_policy(self, vc_vmomi_client: VcVmomiClient) -> Dict: + """ + Get task and event retention policy. + + | Sample Task and Event retention policy output + + .. code-block:: json + + { + "task_cleanup_enabled": true, + "task_max_age": 30, + "event_cleanup_enabled": true, + "event_max_age": 30 + } + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :return: Dictionary containing task and event retention policy information. + :rtype: Dict + """ + task_cleanup_enabled = vc_vmomi_client.get_vpxd_option_value(VPX_TASK_CLEANUP_ENABLED_KEY) + task_max_age = vc_vmomi_client.get_vpxd_option_value(VPX_TASK_MAX_AGE_KEY) + + event_cleanup_enabled = vc_vmomi_client.get_vpxd_option_value(VPX_EVENT_CLEANUP_ENABLED_KEY) + event_max_age = vc_vmomi_client.get_vpxd_option_value(VPX_EVENT_MAX_AGE_KEY) + + return { + "task_cleanup_enabled": task_cleanup_enabled, + "task_max_age": task_max_age, + "event_cleanup_enabled": event_cleanup_enabled, + "event_max_age": event_max_age, + } + + def __set_task_and_event_retention_policy(self, vc_vmomi_client: VcVmomiClient, desired_values: Dict) -> bool: + """ + Set Task and Event retention policy. + + :param vc_vmomi_client: VC vmomi client instance. + :type vc_vmomi_client: VcVmomiClient + :param desired_values: Dictionary containing task and event retention policy information. + :type desired_values: Dict + :return: Returns bool denoting success/failure + :rtype: bool + """ + # Set Task config + task_cleanup_enabled_success = vc_vmomi_client.set_vpxd_option_value( + VPX_TASK_CLEANUP_ENABLED_KEY, desired_values.get(DESIRED_TASK_CLEANUP_ENABLED_KEY) + ) + task_max_age_success = vc_vmomi_client.set_vpxd_option_value( + VPX_TASK_MAX_AGE_KEY, desired_values.get(DESIRED_TASK_MAX_AGE_KEY) + ) + + # Set Event config + event_cleanup_enabled_success = vc_vmomi_client.set_vpxd_option_value( + VPX_EVENT_CLEANUP_ENABLED_KEY, desired_values.get(DESIRED_EVENT_CLEANUP_ENABLED_KEY) + ) + event_max_age_success = vc_vmomi_client.set_vpxd_option_value( + VPX_EVENT_MAX_AGE_KEY, desired_values.get(DESIRED_EVENT_MAX_AGE_KEY) + ) + + return ( + task_cleanup_enabled_success + and task_max_age_success + and event_cleanup_enabled_success + and event_max_age_success + ) diff --git a/config_modules_vmware/controllers/vcenter/tls_version_config.py b/config_modules_vmware/controllers/vcenter/tls_version_config.py new file mode 100644 index 0000000..6953b56 --- /dev/null +++ b/config_modules_vmware/controllers/vcenter/tls_version_config.py @@ -0,0 +1,262 @@ +# Copyright 2024 Broadcom. All Rights Reserved. +import logging +import re +from typing import Any +from typing import Dict +from typing import List +from typing import Tuple + +from config_modules_vmware.controllers.base_controller import BaseController +from config_modules_vmware.framework.auth.contexts.base_context import BaseContext +from config_modules_vmware.framework.auth.contexts.vc_context import VcenterContext +from config_modules_vmware.framework.clients.common import consts +from config_modules_vmware.framework.logging.logger_adapter import LoggerAdapter +from config_modules_vmware.framework.models.controller_models.metadata import ControllerMetadata +from config_modules_vmware.framework.models.output_models.compliance_response import ComplianceStatus +from config_modules_vmware.framework.models.output_models.remediate_response import RemediateStatus +from config_modules_vmware.framework.utils import utils +from config_modules_vmware.framework.utils.comparator import Comparator +from config_modules_vmware.framework.utils.comparator import ComparatorOptionForList + +logger = LoggerAdapter(logging.getLogger(__name__)) + +RECONFIGURE_VC_TLS_SCRIPT_PATH = "/usr/lib/vmware-TlsReconfigurator/VcTlsReconfigurator/reconfigureVc" +SCAN_COMMAND = "scan" +UPDATE_COMMAND = "update" +REGEX_PATTERN = r"\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|" +NOT_RUNNING = "NOT RUNNING" + + +class TlsVersion(BaseController): + """Class to implement get and set methods for configuring and enabling specified TLS versions. + For 4411 it supports multiple TLS versions - TLSv1.0, TLSv1.1, TLSv1.2 + For 5x onwards only supported version is TLSv1.2 + + | Config Id - 1204 + | Config Title - The vCenter Server must enable TLS 1.2 exclusively. + + """ + + metadata = ControllerMetadata( + name="tls_version", # controller name + path_in_schema="compliance_config.vcenter.tls_version", + # path in the schema to this controller's definition. + configuration_id="1204", # configuration id as defined in compliance kit. + title="The vCenter Server must enable TLS 1.2 exclusively.", # controller title as defined in compliance kit. + tags=[], # controller tags for future querying and filtering + version="1.0.0", # version of the controller implementation. + since="", # version when the controller was first introduced in the compliance kit. + products=[BaseContext.ProductEnum.VCENTER], # product from enum in BaseContext. + components=[], # subcomponent within the product if applicable. + status=ControllerMetadata.ControllerStatus.ENABLED, # used to enable/disable a controller + impact=ControllerMetadata.RemediationImpact.RESTART_REQUIRED, # from enum in ControllerMetadata.RemediationImpact. + scope="", # any information or limitations about how the controller operates. i.e. runs as a CLI on VCSA. + functional_test_targets=["vcenter"], # location where functional tests are run. + ) + + def _get_environment_variables(self) -> Dict[str, str]: + """Helper method to return all environment variables needed.""" + environment = { + "VMWARE_LOG_DIR": "/var/log", + "VMWARE_PYTHON_PATH": "/usr/lib/vmware/site-packages", + "VMWARE_DATA_DIR": "/storage", + "VMWARE_CFG_DIR": "/etc/vmware", + "VMWARE_RUNTIME_DATA_DIR": "/var", + "PATH": "/usr/sbin/", + } + return environment + + def _generate_remediate_commands(self, services_to_tls_versions: Dict = None) -> List[str]: + """ + Return list of commands to be executed for remediation. + :param services_to_tls_versions: Non-compliant services to tls versions data. + :type: dict + :return: List of remediation commands. + :rtype: list + """ + + if not services_to_tls_versions: + return [] + + tls_to_services = {} + for service, tls_versions in services_to_tls_versions.items(): + if NOT_RUNNING not in tls_versions: + tls_key = tuple(sorted(tls_versions)) + if tls_key not in tls_to_services: + tls_to_services[tls_key] = [service] + else: + tls_to_services[tls_key].append(service) + + remediation_commands = [] + + # If all services require same TLS versions, use