From f1f0609697eeeb8cb1f75b91141f64e26872276a Mon Sep 17 00:00:00 2001 From: Sergio Garcia <38561120+sergargar@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:38:05 -0400 Subject: [PATCH] chore(release): point v4.4 to master (#5250) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Mario Rodriguez Lopez <101330800+MarioRgzLpz@users.noreply.github.com> Co-authored-by: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Co-authored-by: Prowler Bot Co-authored-by: Pedro Martín Co-authored-by: Hugo Pereira Brito <101209179+HugoPBrito@users.noreply.github.com> Co-authored-by: LefterisXefteris <136985982+LefterisXefteris@users.noreply.github.com> Co-authored-by: Lefteris Gilmaz Co-authored-by: Rubén De la Torre Vico Co-authored-by: Amogh Bantwal <100332169+abant07@users.noreply.github.com> Co-authored-by: Harshit Raj Singh Co-authored-by: Pepe Fagoaga Co-authored-by: Jude Bae(Bae cheongho) Co-authored-by: MZC01-JUDE Co-authored-by: johannes-engler-mw <132657752+johannes-engler-mw@users.noreply.github.com> --- .../workflows/build-lint-push-containers.yml | 4 +- .github/workflows/find-secrets.yml | 2 +- README.md | 6 +- dashboard/common_methods.py | 229 + dashboard/compliance/kisa_isms_p_2023_aws.py | 25 + .../compliance/kisa_isms_p_2023_korean_aws.py | 25 + docs/developer-guide/checks.md | 2 +- docs/developer-guide/introduction.md | 6 +- docs/index.md | 109 +- docs/tutorials/configuration_file.md | 72 +- poetry.lock | 2233 +++++---- prowler/__main__.py | 120 +- .../compliance/aws/kisa_isms_p_2023_aws.json | 4335 +++++++++++++++++ .../aws/kisa_isms_p_2023_korean_aws.json | 4335 +++++++++++++++++ prowler/config/config.yaml | 3 + prowler/lib/check/check.py | 179 +- prowler/lib/check/compliance_models.py | 14 + prowler/lib/outputs/asff/asff.py | 6 +- prowler/lib/outputs/compliance/compliance.py | 10 + .../compliance/kisa_ismsp}/__init__.py | 0 .../compliance/kisa_ismsp/kisa_ismsp.py | 89 + .../compliance/kisa_ismsp/kisa_ismsp_aws.py | 93 + .../outputs/compliance/kisa_ismsp/models.py | 31 + prowler/lib/outputs/finding.py | 20 +- prowler/lib/outputs/outputs.py | 61 +- prowler/lib/outputs/slack/slack.py | 26 + prowler/lib/scan/scan.py | 23 +- prowler/providers/aws/aws_provider.py | 15 +- .../providers/aws/aws_regions_by_service.json | 62 +- .../policy_condition_parser.py | 157 - ...i_client_certificate_enabled.metadata.json | 2 +- ...eway_restapi_logging_enabled.metadata.json | 2 +- .../apigateway_restapi_public.py | 2 +- ...way_restapi_waf_acl_attached.metadata.json | 2 +- ...ult_internet_access_disabled.metadata.json | 2 +- ...eet_maximum_session_duration.metadata.json | 2 +- ...t_session_disconnect_timeout.metadata.json | 2 +- ...sion_idle_disconnect_timeout.metadata.json | 2 +- .../athena_workgroup_encryption.metadata.json | 2 +- ...kgroup_enforce_configuration.metadata.json | 2 +- ...ets_ec2_launch_configuration.metadata.json | 6 +- ...utoscaling_group_multiple_az.metadata.json | 2 +- .../awslambda_function_inside_vpc.py | 16 +- ...i_operations_cloudtrail_logging_enabled.py | 3 +- ...lambda_function_not_publicly_accessible.py | 37 +- .../__init__.py | 0 ...lambda_function_vpc_multi_az.metadata.json | 30 + .../awslambda_function_vpc_multi_az.py | 48 + .../services/awslambda/awslambda_service.py | 2 + .../backup_plans_exist/backup_plans_exist.py | 2 + .../backup_reportplans_exist.metadata.json | 2 +- .../aws/services/backup/backup_service.py | 66 +- .../backup_vaults_encrypted.py | 1 + .../backup_vaults_exist.py | 2 + .../__init__.py | 0 ...tions_custom_ssl_certificate.metadata.json | 32 + ...nt_distributions_custom_ssl_certificate.py | 25 + .../__init__.py | 0 ...ibutions_default_root_object.metadata.json | 30 + ...front_distributions_default_root_object.py | 26 + .../__init__.py | 0 ...tributions_https_sni_enabled.metadata.json | 32 + ...udfront_distributions_https_sni_enabled.py | 30 + .../__init__.py | 0 ...e_origin_failover_configured.metadata.json | 36 + ...ons_multiple_origin_failover_configured.py | 25 + .../__init__.py | 0 ...ons_origin_traffic_encrypted.metadata.json | 30 + ..._distributions_origin_traffic_encrypted.py | 36 + .../__init__.py | 0 ...ons_s3_origin_access_control.metadata.json | 32 + ..._distributions_s3_origin_access_control.py | 35 + .../__init__.py | 0 ...3_origin_non_existent_bucket.metadata.json | 34 + ...ributions_s3_origin_non_existent_bucket.py | 32 + .../services/cloudfront/cloudfront_service.py | 56 +- ...l_s3_dataevents_read_enabled.metadata.json | 2 +- ..._s3_dataevents_write_enabled.metadata.json | 2 +- ...cloudtrail_threat_detection_enumeration.py | 29 +- ...l_threat_detection_privilege_escalation.py | 35 +- ...etwork_acls_alarm_configured.metadata.json | 2 +- ...rk_gateways_alarm_configured.metadata.json | 2 +- ...oute_tables_alarm_configured.metadata.json | 2 +- ...ges_to_vpcs_alarm_configured.metadata.json | 2 +- ...oss_account_sharing_disabled.metadata.json | 2 +- ...group_kms_encryption_enabled.metadata.json | 2 +- ...log_group_no_secrets_in_logs.metadata.json | 2 +- ...onfiguration_changes_enabled.metadata.json | 2 +- ...onfiguration_changes_enabled.metadata.json | 2 +- ...lter_authentication_failures.metadata.json | 2 +- ...er_aws_organizations_changes.metadata.json | 2 +- ...cheduled_deletion_of_kms_cmk.metadata.json | 2 +- ...for_s3_bucket_policy_changes.metadata.json | 2 +- ...metric_filter_policy_changes.metadata.json | 2 +- ...log_metric_filter_root_usage.metadata.json | 2 +- ...ilter_security_group_changes.metadata.json | 2 +- ...c_filter_sign_in_without_mfa.metadata.json | 2 +- ...ilter_unauthorized_api_calls.metadata.json | 2 +- ...debuild_project_no_secrets_in_variables.py | 1 + .../codebuild_project_older_90_days.py | 1 + ...ource_repo_url_no_sensitive_credentials.py | 1 + ...build_project_user_controlled_buildspec.py | 1 + .../services/codebuild/codebuild_service.py | 2 + ...y_pool_guest_access_disabled.metadata.json | 2 +- ...ctory_log_forwarding_enabled.metadata.json | 2 +- ...ectory_monitor_notifications.metadata.json | 2 +- ...ce_directory_snapshots_limit.metadata.json | 2 +- ..._ldap_certificate_expiration.metadata.json | 2 +- ...ius_server_security_protocol.metadata.json | 2 +- ...supported_mfa_radius_enabled.metadata.json | 2 +- .../dms/dms_endpoint_ssl_enabled/__init__.py | 0 .../dms_endpoint_ssl_enabled.metadata.json | 32 + .../dms_endpoint_ssl_enabled.py | 24 + ..._instance_minor_version_upgrade_enabled.py | 1 + .../dms_instance_multi_az_enabled.py | 1 + .../dms_instance_no_public_access.py | 1 + .../providers/aws/services/dms/dms_service.py | 49 +- ...luster_cloudwatch_log_export.metadata.json | 2 +- ..._cluster_deletion_protection.metadata.json | 2 +- ...ntdb_cluster_public_snapshot.metadata.json | 2 +- ...db_cluster_storage_encrypted.metadata.json | 2 +- ...r_cluster_encryption_enabled.metadata.json | 2 +- ..._accelerator_cluster_encryption_enabled.py | 6 +- .../__init__.py | 0 ...n_transit_encryption_enabled.metadata.json | 34 + ...r_cluster_in_transit_encryption_enabled.py | 22 + .../aws/services/dynamodb/dynamodb_service.py | 41 +- ...b_table_cross_account_access.metadata.json | 2 +- .../dynamodb_table_cross_account_access.py | 42 +- .../__init__.py | 0 ..._deletion_protection_enabled.metadata.json | 32 + ...amodb_table_deletion_protection_enabled.py | 25 + .../__init__.py | 0 ...ble_protected_by_backup_plan.metadata.json | 34 + ...dynamodb_table_protected_by_backup_plan.py | 32 + ...s_kms_cmk_encryption_enabled.metadata.json | 2 +- ...amodb_tables_kms_cmk_encryption_enabled.py | 4 +- ...dynamodb_tables_pitr_enabled.metadata.json | 2 +- .../dynamodb_tables_pitr_enabled.py | 4 +- .../ec2_ebs_public_snapshot.metadata.json | 2 +- ..._account_block_public_access.metadata.json | 2 +- .../ec2_ebs_snapshots_encrypted.metadata.json | 2 +- .../__init__.py | 0 ...ume_protected_by_backup_plan.metadata.json | 34 + ...ec2_ebs_volume_protected_by_backup_plan.py | 31 + .../ec2_instance_paravirtual_type/__init__.py | 0 ...c2_instance_paravirtual_type.metadata.json | 30 + .../ec2_instance_paravirtual_type.py | 26 + ...emcached_exposed_to_internet.metadata.json | 2 +- .../ec2_instance_uses_single_eni/__init__.py | 0 ...ec2_instance_uses_single_eni.metadata.json | 34 + .../ec2_instance_uses_single_eni.py | 54 + .../__init__.py | 0 ...launch_template_no_public_ip.metadata.json | 30 + .../ec2_launch_template_no_public_ip.py | 52 + .../ec2_launch_template_no_secrets.py | 2 +- .../ec2_networkacl_allow_ingress_any_port.py | 4 +- ...c2_networkacl_allow_ingress_tcp_port_22.py | 4 +- ..._networkacl_allow_ingress_tcp_port_3389.py | 4 +- .../ec2/ec2_networkacl_unused/__init__.py | 0 .../ec2_networkacl_unused.metadata.json | 32 + .../ec2_networkacl_unused.py | 25 + ...ss_from_internet_to_any_port.metadata.json | 4 +- ...allow_ingress_from_internet_to_any_port.py | 100 +- .../providers/aws/services/ec2/ec2_service.py | 68 +- ..._scan_images_on_push_enabled.metadata.json | 2 +- ...cr_repositories_not_publicly_accessible.py | 17 +- .../__init__.py | 0 ...epositories_tag_immutability.metadata.json | 32 + .../ecr_repositories_tag_immutability.py | 27 + .../providers/aws/services/ecr/ecr_service.py | 4 + .../providers/aws/services/ecs/ecs_service.py | 188 +- .../__init__.py | 0 ..._service_no_assign_public_ip.metadata.json | 34 + .../ecs_service_no_assign_public_ip.py | 24 + .../__init__.py | 0 ...s_containers_readonly_access.metadata.json | 34 + ..._definitions_containers_readonly_access.py | 27 + .../__init__.py | 0 ...ns_host_namespace_not_shared.metadata.json | 34 + ...k_definitions_host_namespace_not_shared.py | 20 + .../__init__.py | 0 ...s_host_networking_mode_users.metadata.json | 34 + ..._definitions_host_networking_mode_users.py | 30 + .../__init__.py | 0 ..._definitions_logging_enabled.metadata.json | 34 + .../ecs_task_definitions_logging_enabled.py | 26 + ...task_definitions_no_environment_secrets.py | 51 +- .../__init__.py | 0 ...ons_no_privileged_containers.metadata.json | 34 + ...sk_definitions_no_privileged_containers.py | 25 + ...s_encryption_at_rest_enabled.metadata.json | 2 +- .../efs_have_backup_enabled.metadata.json | 2 +- .../efs_not_publicly_accessible.metadata.json | 2 +- .../efs_not_publicly_accessible.py | 19 +- prowler/providers/aws/services/efs/lib/lib.py | 45 - ...e_cluster_uses_public_subnet.metadata.json | 2 +- ..._auto_minor_version_upgrades.metadata.json | 2 +- .../__init__.py | 0 ...r_automatic_failover_enabled.metadata.json | 30 + ...edis_cluster_automatic_failover_enabled.py | 25 + ...redis_cluster_backup_enabled.metadata.json | 2 +- ...n_transit_encryption_enabled.metadata.json | 2 +- ...dis_cluster_multi_az_enabled.metadata.json | 6 +- ...ster_rest_encryption_enabled.metadata.json | 2 +- .../__init__.py | 0 ...plication_group_auth_enabled.metadata.json | 32 + ...he_redis_replication_group_auth_enabled.py | 33 + .../elasticache/elasticache_service.py | 25 +- .../__init__.py | 0 ..._connection_draining_enabled.metadata.json | 32 + .../elb_connection_draining_enabled.py | 25 + ..._zone_load_balancing_enabled.metadata.json | 2 +- .../providers/aws/services/elb/elb_service.py | 4 + .../elbv2_deletion_protection.metadata.json | 2 +- ...elbv2_desync_mitigation_mode.metadata.json | 2 +- .../elbv2_insecure_ssl_ciphers.metadata.json | 2 +- .../elbv2_internet_facing.metadata.json | 2 +- .../elbv2_is_in_multiple_az.metadata.json | 2 +- .../elbv2_listeners_underneath.metadata.json | 2 +- .../elbv2_logging_enabled.metadata.json | 2 +- .../elbv2_ssl_listeners.metadata.json | 2 +- .../elbv2_waf_acl_attached.metadata.json | 2 +- ...account_public_block_enabled.metadata.json | 2 +- ...er_master_nodes_no_public_ip.metadata.json | 2 +- ...r_cluster_publicly_accesible.metadata.json | 2 +- .../eventbridge/eventbridge_service.py | 2 +- ..._vaults_policy_public_access.metadata.json | 2 +- .../globalaccelerator_service.py | 18 +- ...passwords_encryption_enabled.metadata.json | 2 +- ..._metadata_encryption_enabled.metadata.json | 2 +- ...base_connections_ssl_enabled.metadata.json | 2 +- .../glue_database_connections_ssl_enabled.py | 1 + ...atch_logs_encryption_enabled.metadata.json | 2 +- ...ints_cloudwatch_logs_encryption_enabled.py | 1 + ..._bookmark_encryption_enabled.metadata.json | 2 +- ...dpoints_job_bookmark_encryption_enabled.py | 1 + ...points_s3_encryption_enabled.metadata.json | 2 +- ...lopment_endpoints_s3_encryption_enabled.py | 1 + ...amazon_s3_encryption_enabled.metadata.json | 2 +- ...e_etl_jobs_amazon_s3_encryption_enabled.py | 1 + ...atch_logs_encryption_enabled.metadata.json | 2 +- ...jobs_cloudwatch_logs_encryption_enabled.py | 1 + ..._bookmark_encryption_enabled.metadata.json | 2 +- ...tl_jobs_job_bookmark_encryption_enabled.py | 1 + .../aws/services/glue/glue_service.py | 19 +- .../guardduty_centrally_managed.metadata.json | 2 +- .../__init__.py | 0 ...dduty_rds_protection_enabled.metadata.json | 32 + .../guardduty_rds_protection_enabled.py | 25 + .../__init__.py | 0 ...rdduty_s3_protection_enabled.metadata.json | 32 + .../guardduty_s3_protection_enabled.py | 25 + .../services/guardduty/guardduty_service.py | 16 +- .../iam_administrator_access_with_mfa.py | 2 +- ...hed_policy_no_administrative_privileges.py | 27 +- ...hed_policy_no_administrative_privileges.py | 27 +- ...hed_policy_no_administrative_privileges.py | 27 +- ..._allows_privilege_escalation.metadata.json | 2 +- ...line_policy_allows_privilege_escalation.py | 16 +- ...no_administrative_privileges.metadata.json | 4 +- ...ine_policy_no_administrative_privileges.py | 39 +- ...ine_policy_no_full_access_to_cloudtrail.py | 10 +- ...iam_inline_policy_no_full_access_to_kms.py | 10 +- ...d_server_certificates_stored.metadata.json | 2 +- .../iam_no_root_access_key.metadata.json | 2 +- ...words_within_90_days_or_less.metadata.json | 2 +- ...am_password_policy_lowercase.metadata.json | 2 +- ...ord_policy_minimum_length_14.metadata.json | 2 +- .../iam_password_policy_number.metadata.json | 2 +- ...iam_password_policy_reuse_24.metadata.json | 2 +- .../iam_password_policy_symbol.metadata.json | 2 +- ...am_password_policy_uppercase.metadata.json | 2 +- ...ached_only_to_group_or_roles.metadata.json | 2 +- ...ross_service_confused_deputy_prevention.py | 29 +- ...am_rotate_access_key_90_days.metadata.json | 2 +- .../iam_securityaudit_role_created.py | 4 +- .../providers/aws/services/iam/iam_service.py | 8 +- .../iam_support_role_created.py | 4 +- ..._no_setup_initial_access_key.metadata.json | 2 +- .../providers/aws/services/iam/lib/policy.py | 394 +- .../services/iam/lib/privilege_escalation.py | 82 +- .../inspector2_is_enabled.metadata.json | 18 +- .../inspector2_is_enabled.py | 55 +- .../services/inspector2/inspector2_service.py | 11 + ..._encryption_at_rest_uses_cmk.metadata.json | 2 +- ..._enhanced_monitoring_enabled.metadata.json | 2 +- ...n_transit_encryption_enabled.metadata.json | 2 +- .../kafka_cluster_is_public.metadata.json | 2 +- ...l_tls_authentication_enabled.metadata.json | 2 +- ...unrestricted_access_disabled.metadata.json | 2 +- ..._cluster_uses_latest_version.metadata.json | 2 +- .../kms_key_not_publicly_accessible.py | 33 +- .../lightsail_database_public.metadata.json | 2 +- ...instance_automated_snapshots.metadata.json | 2 +- .../lightsail_instance_public.metadata.json | 2 +- .../services/lightsail/lightsail_service.py | 2 +- .../lightsail_static_ip_unused.metadata.json | 2 +- .../macie_is_enabled.metadata.json | 2 +- .../__init__.py | 0 ...uster_copy_tags_to_snapshots.metadata.json | 34 + .../neptune_cluster_copy_tags_to_snapshots.py | 22 + .../__init__.py | 0 ..._integration_cloudwatch_logs.metadata.json | 32 + ...une_cluster_integration_cloudwatch_logs.py | 24 + ...tune_cluster_public_snapshot.metadata.json | 2 +- .../__init__.py | 0 ...e_cluster_snapshot_encrypted.metadata.json | 34 + .../neptune_cluster_snapshot_encrypted.py | 26 + ...ne_cluster_storage_encrypted.metadata.json | 2 +- ...e_cluster_uses_public_subnet.metadata.json | 2 +- .../aws/services/neptune/neptune_service.py | 5 +- ...firewall_deletion_protection.metadata.json | 2 +- .../networkfirewall_deletion_protection.py | 4 +- .../networkfirewall_in_all_vpc.metadata.json | 2 +- .../networkfirewall_in_all_vpc.py | 2 +- .../__init__.py | 0 ...policy_rule_group_associated.metadata.json | 32 + ...rkfirewall_policy_rule_group_associated.py | 25 + .../networkfirewall_service.py | 73 +- .../services/opensearch/opensearch_service.py | 4 + ...mains_access_control_enabled.metadata.json | 30 + ..._service_domains_access_control_enabled.py | 24 + ...omains_audit_logging_enabled.metadata.json | 2 +- ...s_cloudwatch_logging_enabled.metadata.json | 2 +- ...s_encryption_at_rest_enabled.metadata.json | 2 +- ...ttps_communications_enforced.metadata.json | 2 +- ...ternal_user_database_enabled.metadata.json | 2 +- ...e_to_node_encryption_enabled.metadata.json | 2 +- ...ains_not_publicly_accessible.metadata.json | 2 +- ...est_service_software_version.metadata.json | 2 +- ...to_authentication_for_kibana.metadata.json | 2 +- .../__init__.py | 0 ...s_opt_out_ai_services_policy.metadata.json | 30 + ...rganizations_opt_out_ai_services_policy.py | 37 + .../organizations_scp_check_deny_regions.py | 37 +- .../organizations/organizations_service.py | 31 +- ...ions_tags_policies_enabled_and_attached.py | 23 +- ...ds_cluster_backtrack_enabled.metadata.json | 2 +- ..._critical_event_subscription.metadata.json | 2 +- ...rds_cluster_critical_event_subscription.py | 4 + .../__init__.py | 0 ..._integration_cloudwatch_logs.metadata.json | 30 + ...rds_cluster_integration_cloudwatch_logs.py | 25 + .../rds_cluster_non_default_port/__init__.py | 0 ...rds_cluster_non_default_port.metadata.json | 32 + .../rds_cluster_non_default_port.py | 38 + ...ds_instance_critical_event_subscription.py | 8 + ...nce_event_subscription_parameter_groups.py | 2 + ...ance_event_subscription_security_groups.py | 4 + .../rds/rds_instance_inside_vpc/__init__.py | 0 .../rds_instance_inside_vpc.metadata.json | 32 + .../rds_instance_inside_vpc.py | 25 + ...ds_instance_integration_cloudwatch_logs.py | 2 +- .../rds_instance_non_default_port/__init__.py | 0 ...ds_instance_non_default_port.metadata.json | 32 + .../rds_instance_non_default_port.py | 38 + .../rds_instance_protected_by_backup_plan.py | 18 +- .../providers/aws/services/rds/rds_service.py | 62 +- ...ngling_ip_subdomain_takeover.metadata.json | 2 +- ...s_privacy_protection_enabled.metadata.json | 2 +- ...domains_transferlock_enabled.metadata.json | 2 +- ...t_level_public_access_blocks.metadata.json | 2 +- .../__init__.py | 0 ..._bucket_cross_account_access.metadata.json | 32 + .../s3_bucket_cross_account_access.py | 31 + .../s3_bucket_policy_public_write_access.py | 44 +- .../s3_bucket_public_access.py | 44 +- .../providers/aws/services/s3/s3_service.py | 21 + .../__init__.py | 0 ...onfig_prod_variant_instances.metadata.json | 34 + ..._endpoint_config_prod_variant_instances.py | 27 + ...ls_network_isolation_enabled.metadata.json | 2 +- ...dels_vpc_settings_configured.metadata.json | 2 +- .../services/sagemaker/sagemaker_service.py | 216 +- ...container_encryption_enabled.metadata.json | 2 +- ...bs_network_isolation_enabled.metadata.json | 2 +- ...nd_output_encryption_enabled.metadata.json | 2 +- ...jobs_vpc_settings_configured.metadata.json | 2 +- .../securityhub_enabled.metadata.json | 2 +- .../securityhub_enabled.py | 1 + .../securityhub/securityhub_service.py | 18 +- ...on_in_classic_load_balancers.metadata.json | 2 +- ...ction_in_global_accelerators.metadata.json | 2 +- ...anced_protection_in_global_accelerators.py | 1 + ...ternet_facing_load_balancers.metadata.json | 2 +- ...tion_in_route53_hosted_zones.metadata.json | 2 +- ...ion_not_using_http_endpoints.metadata.json | 2 +- .../sns_topics_not_publicly_accessible.py | 4 +- .../sqs_queues_not_publicly_accessible.py | 4 +- .../ssm_documents_set_as_public.py | 21 +- ...m_managed_compliant_patching.metadata.json | 2 +- .../ssm_managed_compliant_patching.py | 6 + ...fileshare_encryption_enabled.metadata.json | 2 +- ...connections_trust_boundaries.metadata.json | 2 +- ...c_endpoint_connections_trust_boundaries.py | 4 +- .../vpc_endpoint_for_ec2_enabled/__init__.py | 0 ...vpc_endpoint_for_ec2_enabled.metadata.json | 30 + .../vpc_endpoint_for_ec2_enabled.py | 27 + .../providers/aws/services/vpc/vpc_service.py | 76 +- .../vpc_vpn_connection_tunnels_up/__init__.py | 0 ...pc_vpn_connection_tunnels_up.metadata.json | 34 + .../vpc_vpn_connection_tunnels_up.py | 31 + .../aws/services/wafv2/wafv2_service.py | 19 +- .../wafv2_webacl_logging_enabled.py | 1 + ...es_volume_encryption_enabled.metadata.json | 2 +- ...2private_1public_subnets_nat.metadata.json | 2 +- prowler/providers/azure/azure_provider.py | 32 +- .../services/containerregistry/__init__.py | 0 .../__init__.py | 0 ...registry_admin_user_disabled.metadata.json | 30 + .../containerregistry_admin_user_disabled.py | 27 + .../containerregistry_client.py | 6 + .../containerregistry_service.py | 88 + prowler/providers/common/provider.py | 37 +- prowler/providers/gcp/gcp_provider.py | 37 +- prowler/providers/gcp/lib/service/service.py | 1 + .../apikeys_api_restrictions_configured.py | 4 +- .../gcp/services/compute/compute_service.py | 29 +- .../providers/gcp/services/dns/dns_service.py | 5 +- ...anization_essential_contacts_configured.py | 2 +- .../providers/gcp/services/iam/iam_service.py | 8 +- .../kms_key_rotation_enabled.py | 11 +- .../kubernetes/kubernetes_provider.py | 15 +- pyproject.toml | 47 +- tests/config/config_test.py | 3 + tests/config/fixtures/config.yaml | 3 + tests/config/fixtures/config_old.yaml | 1 + tests/lib/check/check_test.py | 146 +- tests/lib/outputs/asff/asff_test.py | 14 +- tests/lib/outputs/compliance/fixtures.py | 71 + .../kisa_ismsp/kisa_ismsp_aws_test.py | 122 + tests/lib/outputs/finding_test.py | 12 +- .../output_options/output_options_test.py | 185 + tests/lib/outputs/outputs_test.py | 134 +- tests/lib/outputs/slack/slack_test.py | 114 +- tests/lib/scan/scan_test.py | 55 +- tests/providers/aws/aws_provider_test.py | 97 +- .../policy_condition_parser_test.py | 1439 ------ .../accessanalyzer_enabled_fixer_test.py | 16 +- .../apigateway_endpoint_public_test.py | 2 +- ...a_function_no_secrets_in_variables_test.py | 3 + ...a_function_not_publicly_accessible_test.py | 314 +- .../awslambda_function_vpc_multi_az_test.py | 548 +++ .../awslambda/awslambda_service_test.py | 1 + .../backup_plans_exist_test.py | 3 + .../services/backup/backup_service_test.py | 144 +- .../backup_vaults_encrypted_test.py | 4 + .../backup_vaults_exist_test.py | 3 + ...stributions_custom_ssl_certificate_test.py | 99 + ..._distributions_default_root_object_test.py | 113 + ...ons_field_level_encryption_enabled_test.py | 6 +- ...ributions_geo_restrictions_enabled_test.py | 8 +- ...dfront_distributions_https_enabled_test.py | 9 +- ...nt_distributions_https_sni_enabled_test.py | 130 + ...ront_distributions_logging_enabled_test.py | 12 +- ...ultiple_origin_failover_configured_test.py | 113 + ...ributions_origin_traffic_encrypted_test.py | 227 + ...ributions_s3_origin_access_control_test.py | 155 + ...ions_s3_origin_non_existent_bucket_test.py | 149 + ...ons_using_deprecated_ssl_protocols_test.py | 12 +- ...cloudfront_distributions_using_waf_test.py | 6 +- .../cloudfront/cloudfront_service_test.py | 129 +- ...trail_threat_detection_enumeration_test.py | 6 +- ...eat_detection_privilege_escalation_test.py | 6 +- ...ld_project_no_secrets_in_variables_test.py | 16 + .../codebuild_project_older_90_days_test.py | 3 + ..._repo_url_no_sensitive_credentials_test.py | 4 + ..._project_user_controlled_buildspec_test.py | 4 + .../codebuild/codebuild_service_test.py | 14 +- .../dms_endpoint_ssl_enabled_test.py | 185 + ...ance_minor_version_upgrade_enabled_test.py | 8 + .../dms_instance_multi_az_test.py | 8 + .../dms_no_public_access_test.py | 30 +- .../aws/services/dms/dms_service_test.py | 50 + ...ster_in_transit_encryption_enabled_test.py | 123 + .../dynamodb/dynamodb_service_test.py | 32 +- ...ynamodb_table_cross_account_access_test.py | 68 +- ..._table_deletion_protection_enabled_test.py | 133 + ...odb_table_protected_by_backup_plan_test.py | 329 ++ ...bs_volume_protected_by_backup_plan_test.py | 323 ++ .../ec2_instance_paravirtual_type_test.py | 140 + .../ec2_instance_uses_single_eni_test.py | 314 ++ .../ec2_launch_template_no_public_ip_test.py | 363 ++ .../ec2_launch_template_no_secrets_test.py | 41 +- .../ec2_networkacl_unused_test.py | 126 + ..._ingress_from_internet_to_any_port_test.py | 31 +- .../aws/services/ec2/ec2_service_test.py | 20 +- ...positories_not_publicly_accessible_test.py | 47 +- .../ecr_repositories_tag_immutability_test.py | 163 + .../aws/services/ecr/ecr_service_test.py | 3 + .../ecs_service_no_assign_public_ip_test.py | 87 + .../aws/services/ecs/ecs_service_test.py | 239 +- ...nitions_containers_readonly_access_test.py | 149 + ...initions_host_namespace_not_shared_test.py | 163 + ...nitions_host_networking_mode_users_test.py | 180 + ...s_task_definitions_logging_enabled_test.py | 166 + ...definitions_no_environment_secrets_test.py | 69 +- ...finitions_no_privileged_containers_test.py | 108 + .../aws/services/efs/lib/lib_test.py | 102 - ...luster_auto_minor_version_upgrades_test.py | 51 +- ...cluster_automatic_failover_enabled_test.py | 147 + ...cache_redis_cluster_backup_enabled_test.py | 21 +- ...ster_in_transit_encryption_enabled_test.py | 12 + ...che_redis_cluster_multi_az_enabled_test.py | 12 + ...is_cluster_rest_encryption_enabled_test.py | 12 + ...dis_replication_group_auth_enabled_test.py | 227 + .../elasticache/elasticache_service_test.py | 15 +- .../elb_connection_draining_enabled_test.py | 145 + .../aws/services/elb/elb_service_test.py | 2 + .../globalaccelerator_service_test.py | 17 + ...e_database_connections_ssl_enabled_test.py | 4 + ...cloudwatch_logs_encryption_enabled_test.py | 6 + ...ts_job_bookmark_encryption_enabled_test.py | 6 + ...nt_endpoints_s3_encryption_enabled_test.py | 6 + ..._jobs_amazon_s3_encryption_enabled_test.py | 8 + ...cloudwatch_logs_encryption_enabled_test.py | 6 + ...bs_job_bookmark_encryption_enabled_test.py | 6 + .../aws/services/glue/glue_service_test.py | 14 + .../guardduty_is_enabled_fixer_test.py | 4 + .../guardduty_rds_protection_enabled_test.py | 139 + .../guardduty_s3_protection_enabled_test.py | 139 + .../guardduty/guardduty_service_test.py | 7 +- ...policy_allows_privilege_escalation_test.py | 89 +- ...olicy_no_administrative_privileges_test.py | 24 +- ...olicy_no_full_access_to_cloudtrail_test.py | 20 +- ...nline_policy_no_full_access_to_kms_test.py | 20 +- ...policy_allows_privilege_escalation_test.py | 53 + .../aws/services/iam/lib/policy_test.py | 1639 ++++++- .../iam/lib/privilege_escalation_test.py | 76 +- .../inspector2_active_findings_exist_test.py | 16 + .../inspector2_is_enabled_test.py | 787 ++- .../inspector2/inspector2_service_test.py | 9 + ...une_cluster_copy_tags_to_snapshots_test.py | 121 + ...luster_integration_cloudwatch_logs_test.py | 205 + ...neptune_cluster_snapshot_encrypted_test.py | 107 + .../services/neptune/neptune_service_test.py | 4 +- ...etworkfirewall_deletion_protection_test.py | 19 +- .../networkfirewall_in_all_vpc_test.py | 22 +- ...ewall_policy_rule_group_associated_test.py | 244 + .../networkfirewall_service_test.py | 29 +- ...ice_domains_access_control_enabled_test.py | 94 + ...zations_opt_out_ai_services_policy_test.py | 216 + ...ganizations_scp_check_deny_regions_test.py | 37 + .../organizations_service_test.py | 23 +- ...tags_policies_enabled_and_attached_test.py | 47 +- ...luster_critical_event_subscription_test.py | 4 + ...luster_integration_cloudwatch_logs_test.py | 175 + .../rds_cluster_non_default_port_test.py | 227 + ...stance_critical_event_subscription_test.py | 8 + ...vent_subscription_parameter_groups_test.py | 3 + ...event_subscription_security_groups_test.py | 16 +- .../rds_instance_inside_vpc_test.py | 169 + ...stance_integration_cloudwatch_logs_test.py | 2 +- .../rds_instance_non_default_port_test.py | 243 + ..._instance_protected_by_backup_plan_test.py | 396 +- .../aws/services/rds/rds_service_test.py | 39 +- ...3_access_point_public_access_block_test.py | 109 + .../s3_bucket_cross_account_access_test.py | 245 + .../aws/services/s3/s3_service_test.py | 27 + ...oint_config_prod_variant_instances_test.py | 148 + ...r_models_network_isolation_enabled_test.py | 30 +- ...ker_models_vpc_settings_configured_test.py | 30 +- ...tebook_instance_encryption_enabled_test.py | 30 +- ...book_instance_root_access_disabled_test.py | 30 +- ...k_instance_vpc_settings_configured_test.py | 30 +- ..._direct_internet_access_configured_test.py | 30 +- .../sagemaker/sagemaker_service_test.py | 59 + ..._intercontainer_encryption_enabled_test.py | 30 +- ...ing_jobs_network_isolation_enabled_test.py | 30 +- ...lume_and_output_encryption_enabled_test.py | 30 +- ...ining_jobs_vpc_settings_configured_test.py | 30 +- .../securityhub_enabled_test.py | 16 +- .../securityhub/securityhub_service_test.py | 11 + ..._protection_in_global_accelerators_test.py | 8 + ...sns_topics_not_publicly_accessible_test.py | 4 +- .../ssm_documents_set_as_public_test.py | 113 +- .../ssm_managed_compliant_patching_test.py | 46 +- .../vpc_endpoint_for_ec2_enabled_test.py | 156 + .../aws/services/vpc/vpc_service_test.py | 105 +- .../vpc_vpn_connection_tunnels_up_test.py | 160 + .../aws/services/wafv2/wafv2_service_test.py | 26 +- .../wafv2_webacl_logging_enabled_test.py | 4 + tests/providers/aws/utils.py | 7 - tests/providers/azure/azure_provider_test.py | 91 +- ...tainerregistry_admin_user_disabled_test.py | 134 + .../containerregistry_service_test.py | 87 + tests/providers/gcp/gcp_fixtures.py | 3 +- tests/providers/gcp/gcp_provider_test.py | 103 +- ...pikeys_api_restrictions_configured_test.py | 4 +- ...tion_essential_contacts_configured_test.py | 2 + .../kms_key_rotation_enabled_test.py | 58 + .../kubernetes/kubernetes_provider_test.py | 72 - 593 files changed, 29462 insertions(+), 5471 deletions(-) create mode 100644 dashboard/compliance/kisa_isms_p_2023_aws.py create mode 100644 dashboard/compliance/kisa_isms_p_2023_korean_aws.py create mode 100644 prowler/compliance/aws/kisa_isms_p_2023_aws.json create mode 100644 prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json rename prowler/{providers/aws/lib/policy_condition_parser => lib/outputs/compliance/kisa_ismsp}/__init__.py (100%) create mode 100644 prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py create mode 100644 prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py create mode 100644 prowler/lib/outputs/compliance/kisa_ismsp/models.py delete mode 100644 prowler/providers/aws/lib/policy_condition_parser/policy_condition_parser.py rename prowler/providers/aws/services/{efs/lib => awslambda/awslambda_function_vpc_multi_az}/__init__.py (100%) create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.metadata.json create mode 100644 prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/__init__.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate.metadata.json create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/__init__.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.metadata.json create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/__init__.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled.metadata.json create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/__init__.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured.metadata.json create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/__init__.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.metadata.json create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/__init__.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control.metadata.json create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/__init__.py create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.metadata.json create mode 100644 prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.py create mode 100644 prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/__init__.py create mode 100644 prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/dms_endpoint_ssl_enabled.metadata.json create mode 100644 prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/dms_endpoint_ssl_enabled.py create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/__init__.py create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled.metadata.json create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled.py create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/__init__.py create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.metadata.json create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.py create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/__init__.py create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan.metadata.json create mode 100644 prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan.py create mode 100644 prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/__init__.py create mode 100644 prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan.metadata.json create mode 100644 prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan.py create mode 100644 prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/__init__.py create mode 100644 prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type.metadata.json create mode 100644 prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type.py create mode 100644 prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/__init__.py create mode 100644 prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni.metadata.json create mode 100644 prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni.py create mode 100644 prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/__init__.py create mode 100644 prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip.metadata.json create mode 100644 prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip.py create mode 100644 prowler/providers/aws/services/ec2/ec2_networkacl_unused/__init__.py create mode 100644 prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json create mode 100644 prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.py create mode 100644 prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/__init__.py create mode 100644 prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability.metadata.json create mode 100644 prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability.py create mode 100644 prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/__init__.py create mode 100644 prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip.metadata.json create mode 100644 prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip.py create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/__init__.py create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.metadata.json create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.py create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/__init__.py create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared.metadata.json create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared.py create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/__init__.py create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users.metadata.json create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users.py create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/__init__.py create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled.metadata.json create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled.py create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/__init__.py create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers.metadata.json create mode 100644 prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers.py delete mode 100644 prowler/providers/aws/services/efs/lib/lib.py create mode 100644 prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/__init__.py create mode 100644 prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled.metadata.json create mode 100644 prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled.py create mode 100644 prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/__init__.py create mode 100644 prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled.metadata.json create mode 100644 prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled.py create mode 100644 prowler/providers/aws/services/elb/elb_connection_draining_enabled/__init__.py create mode 100644 prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.metadata.json create mode 100644 prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.py create mode 100644 prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/__init__.py create mode 100644 prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled.metadata.json create mode 100644 prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled.py create mode 100644 prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/__init__.py create mode 100644 prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled.metadata.json create mode 100644 prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled.py create mode 100644 prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/__init__.py create mode 100644 prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots.metadata.json create mode 100644 prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots.py create mode 100644 prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/__init__.py create mode 100644 prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs.metadata.json create mode 100644 prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs.py create mode 100644 prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/__init__.py create mode 100644 prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted.metadata.json create mode 100644 prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted.py create mode 100644 prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/__init__.py create mode 100644 prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated.metadata.json create mode 100644 prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated.py create mode 100644 prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.metadata.json create mode 100644 prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.py create mode 100644 prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/__init__.py create mode 100644 prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy.metadata.json create mode 100644 prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy.py create mode 100644 prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/__init__.py create mode 100644 prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.metadata.json create mode 100644 prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.py create mode 100644 prowler/providers/aws/services/rds/rds_cluster_non_default_port/__init__.py create mode 100644 prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.metadata.json create mode 100644 prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.py create mode 100644 prowler/providers/aws/services/rds/rds_instance_inside_vpc/__init__.py create mode 100644 prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.metadata.json create mode 100644 prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.py create mode 100644 prowler/providers/aws/services/rds/rds_instance_non_default_port/__init__.py create mode 100644 prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.metadata.json create mode 100644 prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.py create mode 100644 prowler/providers/aws/services/s3/s3_bucket_cross_account_access/__init__.py create mode 100644 prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json create mode 100644 prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py create mode 100644 prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/__init__.py create mode 100644 prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances.metadata.json create mode 100644 prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances.py create mode 100644 prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/__init__.py create mode 100644 prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.metadata.json create mode 100644 prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.py create mode 100644 prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/__init__.py create mode 100644 prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.metadata.json create mode 100644 prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.py create mode 100644 prowler/providers/azure/services/containerregistry/__init__.py create mode 100644 prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/__init__.py create mode 100644 prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.metadata.json create mode 100644 prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.py create mode 100644 prowler/providers/azure/services/containerregistry/containerregistry_client.py create mode 100644 prowler/providers/azure/services/containerregistry/containerregistry_service.py create mode 100644 tests/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws_test.py create mode 100644 tests/lib/outputs/output_options/output_options_test.py delete mode 100644 tests/providers/aws/lib/policy_condition_parser/policy_condition_parser_test.py create mode 100644 tests/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az_test.py create mode 100644 tests/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate_test.py create mode 100644 tests/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object_test.py create mode 100644 tests/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled_test.py create mode 100644 tests/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured_test.py create mode 100644 tests/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted_test.py create mode 100644 tests/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control_test.py create mode 100644 tests/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket_test.py create mode 100644 tests/providers/aws/services/dms/dms_endpoint_ssl_enabled_test/dms_endpoint_ssl_enabled_test.py create mode 100644 tests/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled_test.py create mode 100644 tests/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled_test.py create mode 100644 tests/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan_test.py create mode 100644 tests/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan_test.py create mode 100644 tests/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type_test.py create mode 100644 tests/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni_test.py create mode 100644 tests/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip_test.py create mode 100644 tests/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused_test.py create mode 100644 tests/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability_test.py create mode 100644 tests/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip_test.py create mode 100644 tests/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access_test.py create mode 100644 tests/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared_test.py create mode 100644 tests/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users_test.py create mode 100644 tests/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled_test.py create mode 100644 tests/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers_test.py delete mode 100644 tests/providers/aws/services/efs/lib/lib_test.py create mode 100644 tests/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled_test.py create mode 100644 tests/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled_test.py create mode 100644 tests/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled_test.py create mode 100644 tests/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled_test.py create mode 100644 tests/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled_test.py create mode 100644 tests/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots_test.py create mode 100644 tests/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs_test.py create mode 100644 tests/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted_test.py create mode 100644 tests/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated_test.py create mode 100644 tests/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled_test.py create mode 100644 tests/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy_test.py create mode 100644 tests/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs_test.py create mode 100644 tests/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port_test.py create mode 100644 tests/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc_test.py create mode 100644 tests/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port_test.py create mode 100644 tests/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access_test.py create mode 100644 tests/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances_test.py create mode 100644 tests/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled_test.py create mode 100644 tests/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up_test.py create mode 100644 tests/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled_test.py create mode 100644 tests/providers/azure/services/containerregistry/containerregistry_service_test.py diff --git a/.github/workflows/build-lint-push-containers.yml b/.github/workflows/build-lint-push-containers.yml index eda088f140..6961617f68 100644 --- a/.github/workflows/build-lint-push-containers.yml +++ b/.github/workflows/build-lint-push-containers.yml @@ -153,7 +153,7 @@ jobs: run: | curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches \ -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \ + -H "Authorization: Bearer ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}" \ -H "X-GitHub-Api-Version: 2022-11-28" \ --data '{"event_type":"dispatch","client_payload":{"version":"v3-latest", "tag": "${{ env.LATEST_COMMIT_HASH }}"}}' @@ -162,6 +162,6 @@ jobs: run: | curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches \ -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.ACCESS_TOKEN }}" \ + -H "Authorization: Bearer ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}" \ -H "X-GitHub-Api-Version: 2022-11-28" \ --data '{"event_type":"dispatch","client_payload":{"version":"release", "tag":"${{ needs.container-build-push.outputs.prowler_version }}"}}' diff --git a/.github/workflows/find-secrets.yml b/.github/workflows/find-secrets.yml index 6a8b068036..607e87dbca 100644 --- a/.github/workflows/find-secrets.yml +++ b/.github/workflows/find-secrets.yml @@ -11,7 +11,7 @@ jobs: with: fetch-depth: 0 - name: TruffleHog OSS - uses: trufflesecurity/trufflehog@v3.81.10 + uses: trufflesecurity/trufflehog@v3.82.6 with: path: ./ base: ${{ github.event.repository.default_branch }} diff --git a/README.md b/README.md index bbd4715f02..dc8650675c 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,9 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe | Provider | Checks | Services | [Compliance Frameworks](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/compliance/) | [Categories](https://docs.prowler.com/projects/prowler-open-source/en/latest/tutorials/misc/#categories) | |---|---|---|---|---| -| AWS | 415 | 67 -> `prowler aws --list-services` | 28 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-categories` | -| GCP | 77 | 13 -> `prowler gcp --list-services` | 1 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`| -| Azure | 135 | 16 -> `prowler azure --list-services` | 2 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` | +| AWS | 457 | 67 -> `prowler aws --list-services` | 30 -> `prowler aws --list-compliance` | 9 -> `prowler aws --list-categories` | +| GCP | 77 | 13 -> `prowler gcp --list-services` | 2 -> `prowler gcp --list-compliance` | 2 -> `prowler gcp --list-categories`| +| Azure | 136 | 17 -> `prowler azure --list-services` | 3 -> `prowler azure --list-compliance` | 2 -> `prowler azure --list-categories` | | Kubernetes | 83 | 7 -> `prowler kubernetes --list-services` | 1 -> `prowler kubernetes --list-compliance` | 7 -> `prowler kubernetes --list-categories` | # 💻 Installation diff --git a/dashboard/common_methods.py b/dashboard/common_methods.py index 89ce89ebe4..23ec36845e 100644 --- a/dashboard/common_methods.py +++ b/dashboard/common_methods.py @@ -2223,3 +2223,232 @@ def get_section_containers_ens(data, section_1, section_2, section_3, section_4) section_containers.append(section_container) return html.Div(section_containers, className="compliance-data-layout") + + +# This function extracts and compares up to two numeric values, ensuring correct sorting for version-like strings. +def extract_numeric_values(value): + numbers = re.findall(r"\d+", str(value)) + if len(numbers) >= 2: + return int(numbers[0]), int(numbers[1]) + elif len(numbers) == 1: + return int(numbers[0]), 0 + return 0, 0 + + +def get_section_containers_kisa_ismsp(data, section_1, section_2): + data["STATUS"] = data["STATUS"].apply(map_status_to_icon) + data[section_1] = data[section_1].astype(str) + data[section_2] = data[section_2].astype(str) + data.sort_values( + by=section_1, + key=lambda x: x.map(extract_numeric_values), + ascending=True, + inplace=True, + ) + + findings_counts_section = ( + data.groupby([section_2, "STATUS"]).size().unstack(fill_value=0) + ) + findings_counts_name = ( + data.groupby([section_1, "STATUS"]).size().unstack(fill_value=0) + ) + + section_containers = [] + + for name in data[section_1].unique(): + success_name = ( + findings_counts_name.loc[name, pass_emoji] + if pass_emoji in findings_counts_name.columns + else 0 + ) + failed_name = ( + findings_counts_name.loc[name, fail_emoji] + if fail_emoji in findings_counts_name.columns + else 0 + ) + + fig_name = go.Figure( + data=[ + go.Bar( + name="Failed", + x=[failed_name], + y=[""], + orientation="h", + marker=dict(color="#e77676"), + width=[0.8], + ), + go.Bar( + name="Success", + x=[success_name], + y=[""], + orientation="h", + marker=dict(color="#45cc6e"), + width=[0.8], + ), + ] + ) + + fig_name.update_layout( + barmode="stack", + margin=dict(l=10, r=10, t=10, b=10), + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + showlegend=False, + width=350, + height=30, + xaxis=dict(showticklabels=False, showgrid=False, zeroline=False), + yaxis=dict(showticklabels=False, showgrid=False, zeroline=False), + annotations=[ + dict( + x=success_name + failed_name, + y=0, + xref="x", + yref="y", + text=str(success_name), + showarrow=False, + font=dict(color="#45cc6e", size=14), + xanchor="left", + yanchor="middle", + ), + dict( + x=0, + y=0, + xref="x", + yref="y", + text=str(failed_name), + showarrow=False, + font=dict(color="#e77676", size=14), + xanchor="right", + yanchor="middle", + ), + ], + ) + + graph_name = dcc.Graph( + figure=fig_name, config={"staticPlot": True}, className="info-bar" + ) + + graph_div = html.Div(graph_name, className="graph-section") + + direct_internal_items = [] + + for section in data[data[section_1] == name][section_2].unique(): + specific_data = data[ + (data[section_1] == name) & (data[section_2] == section) + ] + success_section = ( + findings_counts_section.loc[section, pass_emoji] + if pass_emoji in findings_counts_section.columns + else 0 + ) + failed_section = ( + findings_counts_section.loc[section, fail_emoji] + if fail_emoji in findings_counts_section.columns + else 0 + ) + + data_table = dash_table.DataTable( + data=specific_data.to_dict("records"), + columns=[ + {"name": i, "id": i} + for i in ["CHECKID", "STATUS", "REGION", "ACCOUNTID", "RESOURCEID"] + ], + style_table={"overflowX": "auto"}, + style_as_list_view=True, + style_cell={"textAlign": "left", "padding": "5px"}, + ) + + fig_section = go.Figure( + data=[ + go.Bar( + name="Failed", + x=[failed_section], + y=[""], + orientation="h", + marker=dict(color="#e77676"), + ), + go.Bar( + name="Success", + x=[success_section], + y=[""], + orientation="h", + marker=dict(color="#45cc6e"), + ), + ] + ) + + fig_section.update_layout( + barmode="stack", + margin=dict(l=10, r=10, t=10, b=10), + paper_bgcolor="rgba(0,0,0,0)", + plot_bgcolor="rgba(0,0,0,0)", + showlegend=False, + width=350, + height=30, + xaxis=dict(showticklabels=False, showgrid=False, zeroline=False), + yaxis=dict(showticklabels=False, showgrid=False, zeroline=False), + annotations=[ + dict( + x=success_section + failed_section, + y=0, + xref="x", + yref="y", + text=str(success_section), + showarrow=False, + font=dict(color="#45cc6e", size=14), + xanchor="left", + yanchor="middle", + ), + dict( + x=0, + y=0, + xref="x", + yref="y", + text=str(failed_section), + showarrow=False, + font=dict(color="#e77676", size=14), + xanchor="right", + yanchor="middle", + ), + ], + ) + + graph_section = dcc.Graph( + figure=fig_section, + config={"staticPlot": True}, + className="info-bar-child", + ) + + graph_div_section = html.Div(graph_section, className="graph-section-req") + + internal_accordion_item = dbc.AccordionItem( + title=section, + children=[html.Div([data_table], className="inner-accordion-content")], + ) + + internal_section_container = html.Div( + [ + graph_div_section, + dbc.Accordion( + [internal_accordion_item], start_collapsed=True, flush=True + ), + ], + className="accordion-inner--child", + ) + + direct_internal_items.append(internal_section_container) + + accordion_item = dbc.AccordionItem( + title=f"{name}", children=direct_internal_items + ) + section_container = html.Div( + [ + graph_div, + dbc.Accordion([accordion_item], start_collapsed=True, flush=True), + ], + className="accordion-inner", + ) + + section_containers.append(section_container) + + return html.Div(section_containers, className="compliance-data-layout") diff --git a/dashboard/compliance/kisa_isms_p_2023_aws.py b/dashboard/compliance/kisa_isms_p_2023_aws.py new file mode 100644 index 0000000000..1d079d46af --- /dev/null +++ b/dashboard/compliance/kisa_isms_p_2023_aws.py @@ -0,0 +1,25 @@ +import warnings + +from dashboard.common_methods import get_section_containers_kisa_ismsp + +warnings.filterwarnings("ignore") + + +def get_table(data): + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_ATTRIBUTES_SUBDOMAIN", + "REQUIREMENTS_ATTRIBUTES_SECTION", + # "REQUIREMENTS_DESCRIPTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_kisa_ismsp( + aux, "REQUIREMENTS_ATTRIBUTES_SUBDOMAIN", "REQUIREMENTS_ATTRIBUTES_SECTION" + ) diff --git a/dashboard/compliance/kisa_isms_p_2023_korean_aws.py b/dashboard/compliance/kisa_isms_p_2023_korean_aws.py new file mode 100644 index 0000000000..1d079d46af --- /dev/null +++ b/dashboard/compliance/kisa_isms_p_2023_korean_aws.py @@ -0,0 +1,25 @@ +import warnings + +from dashboard.common_methods import get_section_containers_kisa_ismsp + +warnings.filterwarnings("ignore") + + +def get_table(data): + aux = data[ + [ + "REQUIREMENTS_ID", + "REQUIREMENTS_ATTRIBUTES_SUBDOMAIN", + "REQUIREMENTS_ATTRIBUTES_SECTION", + # "REQUIREMENTS_DESCRIPTION", + "CHECKID", + "STATUS", + "REGION", + "ACCOUNTID", + "RESOURCEID", + ] + ].copy() + + return get_section_containers_kisa_ismsp( + aux, "REQUIREMENTS_ATTRIBUTES_SUBDOMAIN", "REQUIREMENTS_ATTRIBUTES_SECTION" + ) diff --git a/docs/developer-guide/checks.md b/docs/developer-guide/checks.md index e85cb1694e..3acbb14dfe 100644 --- a/docs/developer-guide/checks.md +++ b/docs/developer-guide/checks.md @@ -272,7 +272,7 @@ Each Prowler check has metadata associated which is stored at the same level of # Severity holds the check's severity, always in lowercase (critical, high, medium, low or informational) "Severity": "critical", # ResourceType only for AWS, holds the type from here - # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html + # https://docs.aws.amazon.com/securityhub/latest/userguide/asff-resources.html "ResourceType": "Other", # Description holds the title of the check, for now is the same as CheckTitle "Description": "Ensure there are no EC2 AMIs set as Public.", diff --git a/docs/developer-guide/introduction.md b/docs/developer-guide/introduction.md index 48d3efa812..df2f921880 100644 --- a/docs/developer-guide/introduction.md +++ b/docs/developer-guide/introduction.md @@ -14,10 +14,8 @@ Once that is satisfied go ahead and clone your forked repo: git clone https://github.com//prowler cd prowler ``` -For isolation and avoid conflicts with other environments, we recommend usage of `poetry`: -``` -pip install poetry -``` +For isolation and to avoid conflicts with other environments, we recommend using `poetry`, a Python dependency management tool. You can install it by following the instructions [here](https://python-poetry.org/docs/#installation). + Then install all dependencies including the ones for developers: ``` poetry install --with dev diff --git a/docs/index.md b/docs/index.md index ea485e2b11..2b06f269bf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -19,14 +19,40 @@ It contains hundreds of controls covering CIS, NIST 800, NIST CSF, CISA, RBI, Fe ## Quick Start ### Installation -Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), thus can be installed using pip with `Python >= 3.9`: +Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), thus can be installed as Python package with `Python >= 3.9`: -=== "Generic" +=== "pipx" + + [pipx](https://pipx.pypa.io/stable/) is a tool to install Python applications in isolated environments. It is recommended to use `pipx` for a global installation. _Requirements_: * `Python >= 3.9` - * `Python pip >= 3.9` + * `pipx` installed: [pipx installation](https://pipx.pypa.io/stable/installation/). + * AWS, GCP, Azure and/or Kubernetes credentials + + _Commands_: + + ``` bash + pipx install prowler + prowler -v + ``` + + To upgrade Prowler to the latest version, run: + + ``` bash + pipx upgrade prowler + ``` + +=== "pip" + + ???+ warning + This method is not recommended because it will modify the environment which you choose to install. Consider using [pipx](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) for a global installation. + + _Requirements_: + + * `Python >= 3.9` + * `Python pip >= 21.0.0` * AWS, GCP, Azure and/or Kubernetes credentials _Commands_: @@ -36,13 +62,19 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), prowler -v ``` + To upgrade Prowler to the latest version, run: + + ``` bash + pip install --upgrade prowler + ``` + === "Docker" _Requirements_: * Have `docker` installed: https://docs.docker.com/get-docker/. - * AWS, GCP, Azure and/or Kubernetes credentials * In the command below, change `-v` to your local directory path in order to access the reports. + * AWS, GCP, Azure and/or Kubernetes credentials _Commands_: @@ -54,41 +86,21 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), --env AWS_SESSION_TOKEN toniblyx/prowler:latest ``` -=== "Ubuntu" - - _Requirements for Ubuntu 20.04.3 LTS_: - - * AWS, GCP, Azure and/or Kubernetes credentials - * Install python 3.9 with: `sudo apt-get install python3.9` - * Remove python 3.8 to avoid conflicts if you can: `sudo apt-get remove python3.8` - * Make sure you have the python3 distutils package installed: `sudo apt-get install python3-distutils` - * To make sure you use pip for 3.9 get the get-pip script with: `curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py` - * Execute it with the proper python version: `sudo python3.9 get-pip.py` - * Now you should have pip for 3.9 ready: `pip3.9 --version` - - _Commands_: - - ``` - pip3.9 install prowler - export PATH=$PATH:/home/$HOME/.local/bin/ - prowler -v - ``` - === "GitHub" _Requirements for Developers_: + * `git` + * `poetry` installed: [poetry installation](https://python-poetry.org/docs/#installation). * AWS, GCP, Azure and/or Kubernetes credentials - * `git`, `Python >= 3.9`, `pip` and `poetry` installed (`pip install poetry`) _Commands_: ``` git clone https://github.com/prowler-cloud/prowler cd prowler - poetry shell poetry install - python prowler.py -v + poetry run python prowler.py -v ``` ???+ note If you want to clone Prowler from Windows, use `git config core.longpaths true` to allow long file paths. @@ -97,15 +109,33 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), _Requirements_: + * `Python >= 3.9` * AWS, GCP, Azure and/or Kubernetes credentials - * Latest Amazon Linux 2 should come with Python 3.9 already installed however it may need pip. Install Python pip 3.9 with: `sudo yum install -y python3-pip`. - * Make sure setuptools for python is already installed with: `pip3 install setuptools` _Commands_: ``` - pip3.9 install prowler - export PATH=$PATH:/home/$HOME/.local/bin/ + python3 -m pip install --user pipx + python3 -m pipx ensurepath + pipx install prowler + prowler -v + ``` + +=== "Ubuntu" + + _Requirements_: + + * `Ubuntu 23.04` or above, if you are using an older version of Ubuntu check [pipx installation](https://docs.prowler.com/projects/prowler-open-source/en/latest/#__tabbed_1_1) and ensure you have `Python >= 3.9`. + * `Python >= 3.9` + * AWS, GCP, Azure and/or Kubernetes credentials + + _Commands_: + + ``` bash + sudo apt update + sudo apt install pipx + pipx ensurepath + pipx install prowler prowler -v ``` @@ -125,7 +155,7 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), === "AWS CloudShell" - After the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [2](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html), there is no longer a need to manually compile Python 3.9 as it's already included in AL2023. Prowler can thus be easily installed following the Generic method of installation via pip. Follow the steps below to successfully execute Prowler v4 in AWS CloudShell: + After the migration of AWS CloudShell from Amazon Linux 2 to Amazon Linux 2023 [[1]](https://aws.amazon.com/about-aws/whats-new/2023/12/aws-cloudshell-migrated-al2023/) [[2]](https://docs.aws.amazon.com/cloudshell/latest/userguide/cloudshell-AL2023-migration.html), there is no longer a need to manually compile Python 3.9 as it's already included in AL2023. Prowler can thus be easily installed following the Generic method of installation via pip. Follow the steps below to successfully execute Prowler v4 in AWS CloudShell: _Requirements_: @@ -133,11 +163,13 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), _Commands_: - ``` + ```bash sudo bash adduser prowler su prowler - pip install prowler + python3 -m pip install --user pipx + python3 -m pipx ensurepath + pipx install prowler cd /tmp prowler aws ``` @@ -153,9 +185,12 @@ Prowler is available as a project in [PyPI](https://pypi.org/project/prowler/), _Commands_: - ``` - pip install prowler - prowler -v + ```bash + python3 -m pip install --user pipx + python3 -m pipx ensurepath + pipx install prowler + cd /tmp + prowler azure --az-cli-auth ``` ## Prowler container versions diff --git a/docs/tutorials/configuration_file.md b/docs/tutorials/configuration_file.md index 744ddf688d..63fea6dd95 100644 --- a/docs/tutorials/configuration_file.md +++ b/docs/tutorials/configuration_file.md @@ -13,52 +13,53 @@ The following list includes all the AWS checks with configurable variables that | Check Name | Value | Type | |---------------------------------------------------------------|--------------------------------------------------|-----------------| -| `iam_user_accesskey_unused` | `max_unused_access_keys_days` | Integer | -| `iam_user_console_access_unused` | `max_console_access_days` | Integer | -| `ec2_elastic_ip_shodan` | `shodan_api_key` | String | -| `ec2_securitygroup_with_many_ingress_egress_rules` | `max_security_group_rules` | Integer | -| `ec2_instance_older_than_specific_days` | `max_ec2_instance_age_in_days` | Integer | -| `ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports`| `ec2_sg_high_risk_ports` | List of Integer | -| `vpc_endpoint_connections_trust_boundaries` | `trusted_account_ids` | List of Strings | -| `vpc_endpoint_services_allowed_principals_trust_boundaries` | `trusted_account_ids` | List of Strings | -| `cloudwatch_log_group_retention_policy_specific_days_enabled` | `log_group_retention_days` | Integer | -| `appstream_fleet_session_idle_disconnect_timeout` | `max_idle_disconnect_timeout_in_seconds` | Integer | -| `appstream_fleet_session_disconnect_timeout` | `max_disconnect_timeout_in_seconds` | Integer | -| `appstream_fleet_maximum_session_duration` | `max_session_duration_seconds` | Integer | -| `awslambda_function_using_supported_runtimes` | `obsolete_lambda_runtimes` | Integer | -| `organizations_scp_check_deny_regions` | `organizations_enabled_regions` | List of Strings | -| `organizations_delegated_administrators` | `organizations_trusted_delegated_administrators` | List of Strings | -| `ecr_repositories_scan_vulnerabilities_in_latest_image` | `ecr_repository_vulnerability_minimum_severity` | String | -| `trustedadvisor_premium_support_plan_subscribed` | `verify_premium_support_plans` | Boolean | -| `config_recorder_all_regions_enabled` | `mute_non_default_regions` | Boolean | -| `drs_job_exist` | `mute_non_default_regions` | Boolean | -| `guardduty_is_enabled` | `mute_non_default_regions` | Boolean | -| `securityhub_enabled` | `mute_non_default_regions` | Boolean | -| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_entropy` | Integer | -| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_minutes` | Integer | -| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_actions` | List of Strings | -| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_entropy` | Integer | -| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_minutes` | Integer | -| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_actions` | List of Strings | -| `codebuild_project_no_secrets_in_variables` | `excluded_sensitive_environment_variables` | List of Strings | -| `rds_instance_backup_enabled` | `check_rds_instance_replicas` | Boolean | -| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_interface_types` | List of Strings | -| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_instance_owners` | List of Strings | | `acm_certificates_expiration_check` | `days_to_expire_threshold` | Integer | -| `eks_control_plane_logging_all_types_enabled` | `eks_required_log_types` | List of Strings | -| `eks_cluster_uses_a_supported_version` | `eks_cluster_oldest_version_supported` | String | -| `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer | -| `elb_is_in_multiple_az` | `elb_min_azs` | Integer | +| `appstream_fleet_maximum_session_duration` | `max_session_duration_seconds` | Integer | +| `appstream_fleet_session_disconnect_timeout` | `max_disconnect_timeout_in_seconds` | Integer | +| `appstream_fleet_session_idle_disconnect_timeout` | `max_idle_disconnect_timeout_in_seconds` | Integer | | `autoscaling_find_secrets_ec2_launch_configuration` | `secrets_ignore_patterns` | List of Strings | | `awslambda_function_no_secrets_in_code` | `secrets_ignore_patterns` | List of Strings | | `awslambda_function_no_secrets_in_variables` | `secrets_ignore_patterns` | List of Strings | +| `awslambda_function_using_supported_runtimes` | `obsolete_lambda_runtimes` | Integer | +| `awslambda_function_vpc_is_in_multi_azs` | `lambda_min_azs` | Integer | | `cloudformation_stack_outputs_find_secrets` | `secrets_ignore_patterns` | List of Strings | +| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_actions` | List of Strings | +| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_entropy` | Integer | +| `cloudtrail_threat_detection_enumeration` | `threat_detection_enumeration_minutes` | Integer | +| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_actions` | List of Strings | +| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_entropy` | Integer | +| `cloudtrail_threat_detection_privilege_escalation` | `threat_detection_privilege_escalation_minutes` | Integer | | `cloudwatch_log_group_no_secrets_in_logs` | `secrets_ignore_patterns` | List of Strings | +| `cloudwatch_log_group_retention_policy_specific_days_enabled` | `log_group_retention_days` | Integer | +| `codebuild_project_no_secrets_in_variables` | `excluded_sensitive_environment_variables` | List of Strings | | `codebuild_project_no_secrets_in_variables` | `secrets_ignore_patterns` | List of Strings | +| `config_recorder_all_regions_enabled` | `mute_non_default_regions` | Boolean | +| `drs_job_exist` | `mute_non_default_regions` | Boolean | +| `ec2_elastic_ip_shodan` | `shodan_api_key` | String | +| `ec2_instance_older_than_specific_days` | `max_ec2_instance_age_in_days` | Integer | | `ec2_instance_secrets_user_data` | `secrets_ignore_patterns` | List of Strings | | `ec2_launch_template_no_secrets` | `secrets_ignore_patterns` | List of Strings | +| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_instance_owners` | List of Strings | +| `ec2_securitygroup_allow_ingress_from_internet_to_any_port` | `ec2_allowed_interface_types` | List of Strings | +| `ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports`| `ec2_sg_high_risk_ports` | List of Integer | +| `ec2_securitygroup_with_many_ingress_egress_rules` | `max_security_group_rules` | Integer | | `ecs_task_definitions_no_environment_secrets` | `secrets_ignore_patterns` | List of Strings | +| `ecr_repositories_scan_vulnerabilities_in_latest_image` | `ecr_repository_vulnerability_minimum_severity` | String | +| `eks_cluster_uses_a_supported_version` | `eks_cluster_oldest_version_supported` | String | +| `eks_control_plane_logging_all_types_enabled` | `eks_required_log_types` | List of Strings | +| `elb_is_in_multiple_az` | `elb_min_azs` | Integer | +| `elbv2_is_in_multiple_az` | `elbv2_min_azs` | Integer | +| `guardduty_is_enabled` | `mute_non_default_regions` | Boolean | +| `iam_user_accesskey_unused` | `max_unused_access_keys_days` | Integer | +| `iam_user_console_access_unused` | `max_console_access_days` | Integer | +| `organizations_delegated_administrators` | `organizations_trusted_delegated_administrators` | List of Strings | +| `organizations_scp_check_deny_regions` | `organizations_enabled_regions` | List of Strings | +| `rds_instance_backup_enabled` | `check_rds_instance_replicas` | Boolean | +| `securityhub_enabled` | `mute_non_default_regions` | Boolean | | `ssm_document_secrets` | `secrets_ignore_patterns` | List of Strings | +| `trustedadvisor_premium_support_plan_subscribed` | `verify_premium_support_plans` | Boolean | +| `vpc_endpoint_connections_trust_boundaries` | `trusted_account_ids` | List of Strings | +| `vpc_endpoint_services_allowed_principals_trust_boundaries` | `trusted_account_ids` | List of Strings | ## Azure @@ -157,6 +158,7 @@ aws: ] # AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries) + # AWS SSM Configuration (aws.ssm_documents_set_as_public) # Single account environment: No action required. The AWS account number will be automatically added by the checks. # Multi account environment: Any additional trusted account number should be added as a space separated list, e.g. # trusted_account_ids : ["123456789012", "098765432109", "678901234567"] diff --git a/poetry.lock b/poetry.lock index d20ad4e034..129f84b265 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,98 +13,113 @@ files = [ [[package]] name = "aiohappyeyeballs" -version = "2.3.5" +version = "2.4.2" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "aiohappyeyeballs-2.3.5-py3-none-any.whl", hash = "sha256:4d6dea59215537dbc746e93e779caea8178c866856a721c9c660d7a5a7b8be03"}, - {file = "aiohappyeyeballs-2.3.5.tar.gz", hash = "sha256:6fa48b9f1317254f122a07a131a86b71ca6946ca989ce6326fff54a99a920105"}, + {file = "aiohappyeyeballs-2.4.2-py3-none-any.whl", hash = "sha256:8522691d9a154ba1145b157d6d5c15e5c692527ce6a53c5e5f9876977f6dab2f"}, + {file = "aiohappyeyeballs-2.4.2.tar.gz", hash = "sha256:4ca893e6c5c1f5bf3888b04cb5a3bee24995398efef6e0b9f747b5e89d84fd74"}, ] [[package]] name = "aiohttp" -version = "3.10.2" +version = "3.10.8" description = "Async http client/server framework (asyncio)" optional = false python-versions = ">=3.8" files = [ - {file = "aiohttp-3.10.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:95213b3d79c7e387144e9cb7b9d2809092d6ff2c044cb59033aedc612f38fb6d"}, - {file = "aiohttp-3.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1aa005f060aff7124cfadaa2493f00a4e28ed41b232add5869e129a2e395935a"}, - {file = "aiohttp-3.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eabe6bf4c199687592f5de4ccd383945f485779c7ffb62a9b9f1f8a3f9756df8"}, - {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96e010736fc16d21125c7e2dc5c350cd43c528b85085c04bf73a77be328fe944"}, - {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99f81f9c1529fd8e03be4a7bd7df32d14b4f856e90ef6e9cbad3415dbfa9166c"}, - {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d611d1a01c25277bcdea06879afbc11472e33ce842322496b211319aa95441bb"}, - {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00191d38156e09e8c81ef3d75c0d70d4f209b8381e71622165f22ef7da6f101"}, - {file = "aiohttp-3.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74c091a5ded6cb81785de2d7a8ab703731f26de910dbe0f3934eabef4ae417cc"}, - {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:18186a80ec5a701816adbf1d779926e1069392cf18504528d6e52e14b5920525"}, - {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5a7ceb2a0d2280f23a02c64cd0afdc922079bb950400c3dd13a1ab2988428aac"}, - {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8bd7be6ff6c162a60cb8fce65ee879a684fbb63d5466aba3fa5b9288eb04aefa"}, - {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fae962b62944eaebff4f4fddcf1a69de919e7b967136a318533d82d93c3c6bd1"}, - {file = "aiohttp-3.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a0fde16d284efcacbe15fb0c1013f0967b6c3e379649239d783868230bf1db42"}, - {file = "aiohttp-3.10.2-cp310-cp310-win32.whl", hash = "sha256:f81cd85a0e76ec7b8e2b6636fe02952d35befda4196b8c88f3cec5b4fb512839"}, - {file = "aiohttp-3.10.2-cp310-cp310-win_amd64.whl", hash = "sha256:54ba10eb5a3481c28282eb6afb5f709aedf53cf9c3a31875ffbdc9fc719ffd67"}, - {file = "aiohttp-3.10.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:87fab7f948e407444c2f57088286e00e2ed0003ceaf3d8f8cc0f60544ba61d91"}, - {file = "aiohttp-3.10.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec6ad66ed660d46503243cbec7b2b3d8ddfa020f984209b3b8ef7d98ce69c3f2"}, - {file = "aiohttp-3.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a4be88807283bd96ae7b8e401abde4ca0bab597ba73b5e9a2d98f36d451e9aac"}, - {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01c98041f90927c2cbd72c22a164bb816fa3010a047d264969cf82e1d4bcf8d1"}, - {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54e36c67e1a9273ecafab18d6693da0fb5ac48fd48417e4548ac24a918c20998"}, - {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7de3ddb6f424af54535424082a1b5d1ae8caf8256ebd445be68c31c662354720"}, - {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dd9c7db94b4692b827ce51dcee597d61a0e4f4661162424faf65106775b40e7"}, - {file = "aiohttp-3.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e57e21e1167705f8482ca29cc5d02702208d8bf4aff58f766d94bcd6ead838cd"}, - {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a1a50e59b720060c29e2951fd9f13c01e1ea9492e5a527b92cfe04dd64453c16"}, - {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:686c87782481fda5ee6ba572d912a5c26d9f98cc5c243ebd03f95222af3f1b0f"}, - {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:dafb4abb257c0ed56dc36f4e928a7341b34b1379bd87e5a15ce5d883c2c90574"}, - {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:494a6f77560e02bd7d1ab579fdf8192390567fc96a603f21370f6e63690b7f3d"}, - {file = "aiohttp-3.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6fe8503b1b917508cc68bf44dae28823ac05e9f091021e0c41f806ebbb23f92f"}, - {file = "aiohttp-3.10.2-cp311-cp311-win32.whl", hash = "sha256:4ddb43d06ce786221c0dfd3c91b4892c318eaa36b903f7c4278e7e2fa0dd5102"}, - {file = "aiohttp-3.10.2-cp311-cp311-win_amd64.whl", hash = "sha256:ca2f5abcb0a9a47e56bac173c01e9f6c6e7f27534d91451c5f22e6a35a5a2093"}, - {file = "aiohttp-3.10.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:14eb6b17f6246959fb0b035d4f4ae52caa870c4edfb6170aad14c0de5bfbf478"}, - {file = "aiohttp-3.10.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:465e445ec348d4e4bd349edd8b22db75f025da9d7b6dc1369c48e7935b85581e"}, - {file = "aiohttp-3.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:341f8ece0276a828d95b70cd265d20e257f5132b46bf77d759d7f4e0443f2906"}, - {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01fbb87b5426381cd9418b3ddcf4fc107e296fa2d3446c18ce6c76642f340a3"}, - {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c474af073e1a6763e1c5522bbb2d85ff8318197e4c6c919b8d7886e16213345"}, - {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d9076810a5621236e29b2204e67a68e1fe317c8727ee4c9abbfbb1083b442c38"}, - {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e8f515d6859e673940e08de3922b9c4a2249653b0ac181169313bd6e4b1978ac"}, - {file = "aiohttp-3.10.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:655e583afc639bef06f3b2446972c1726007a21003cd0ef57116a123e44601bc"}, - {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8da9449a575133828cc99985536552ea2dcd690e848f9d41b48d8853a149a959"}, - {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19073d57d0feb1865d12361e2a1f5a49cb764bf81a4024a3b608ab521568093a"}, - {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8e98e1845805f184d91fda6f9ab93d7c7b0dddf1c07e0255924bfdb151a8d05"}, - {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:377220a5efde6f9497c5b74649b8c261d3cce8a84cb661be2ed8099a2196400a"}, - {file = "aiohttp-3.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92f7f4a4dc9cdb5980973a74d43cdbb16286dacf8d1896b6c3023b8ba8436f8e"}, - {file = "aiohttp-3.10.2-cp312-cp312-win32.whl", hash = "sha256:9bb2834a6f11d65374ce97d366d6311a9155ef92c4f0cee543b2155d06dc921f"}, - {file = "aiohttp-3.10.2-cp312-cp312-win_amd64.whl", hash = "sha256:518dc3cb37365255708283d1c1c54485bbacccd84f0a0fb87ed8917ba45eda5b"}, - {file = "aiohttp-3.10.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:7f98e70bbbf693086efe4b86d381efad8edac040b8ad02821453083d15ec315f"}, - {file = "aiohttp-3.10.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f6f0b252a009e98fe84028a4ec48396a948e7a65b8be06ccfc6ef68cf1f614d"}, - {file = "aiohttp-3.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9360e3ffc7b23565600e729e8c639c3c50d5520e05fdf94aa2bd859eef12c407"}, - {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3988044d1635c7821dd44f0edfbe47e9875427464e59d548aece447f8c22800a"}, - {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30a9d59da1543a6f1478c3436fd49ec59be3868bca561a33778b4391005e499d"}, - {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9f49bdb94809ac56e09a310a62f33e5f22973d6fd351aac72a39cd551e98194"}, - {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddfd2dca3f11c365d6857a07e7d12985afc59798458a2fdb2ffa4a0332a3fd43"}, - {file = "aiohttp-3.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:685c1508ec97b2cd3e120bfe309a4ff8e852e8a7460f1ef1de00c2c0ed01e33c"}, - {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:49904f38667c44c041a0b44c474b3ae36948d16a0398a8f8cd84e2bb3c42a069"}, - {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:352f3a4e5f11f3241a49b6a48bc5b935fabc35d1165fa0d87f3ca99c1fcca98b"}, - {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:fc61f39b534c5d5903490478a0dd349df397d2284a939aa3cbaa2fb7a19b8397"}, - {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:ad2274e707be37420d0b6c3d26a8115295fe9d8e6e530fa6a42487a8ca3ad052"}, - {file = "aiohttp-3.10.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c836bf3c7512100219fe1123743fd8dd9a2b50dd7cfb0c3bb10d041309acab4b"}, - {file = "aiohttp-3.10.2-cp38-cp38-win32.whl", hash = "sha256:53e8898adda402be03ff164b0878abe2d884e3ea03a4701e6ad55399d84b92dc"}, - {file = "aiohttp-3.10.2-cp38-cp38-win_amd64.whl", hash = "sha256:7cc8f65f5b22304693de05a245b6736b14cb5bc9c8a03da6e2ae9ef15f8b458f"}, - {file = "aiohttp-3.10.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9dfc906d656e14004c5bc672399c1cccc10db38df2b62a13fb2b6e165a81c316"}, - {file = "aiohttp-3.10.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:91b10208b222ddf655c3a3d5b727879d7163db12b634492df41a9182a76edaae"}, - {file = "aiohttp-3.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9fd16b5e1a7bdd14668cd6bde60a2a29b49147a535c74f50d8177d11b38433a7"}, - {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2bfdda4971bd79201f59adbad24ec2728875237e1c83bba5221284dbbf57bda"}, - {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69d73f869cf29e8a373127fc378014e2b17bcfbe8d89134bc6fb06a2f67f3cb3"}, - {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df59f8486507c421c0620a2c3dce81fbf1d54018dc20ff4fecdb2c106d6e6abc"}, - {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0df930015db36b460aa9badbf35eccbc383f00d52d4b6f3de2ccb57d064a6ade"}, - {file = "aiohttp-3.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:562b1153ab7f766ee6b8b357ec777a302770ad017cf18505d34f1c088fccc448"}, - {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d984db6d855de58e0fde1ef908d48fe9a634cadb3cf715962722b4da1c40619d"}, - {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:14dc3fcb0d877911d775d511eb617a486a8c48afca0a887276e63db04d3ee920"}, - {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b52a27a5c97275e254704e1049f4b96a81e67d6205f52fa37a4777d55b0e98ef"}, - {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:cd33d9de8cfd006a0d0fe85f49b4183c57e91d18ffb7e9004ce855e81928f704"}, - {file = "aiohttp-3.10.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1238fc979160bc03a92fff9ad021375ff1c8799c6aacb0d8ea1b357ea40932bb"}, - {file = "aiohttp-3.10.2-cp39-cp39-win32.whl", hash = "sha256:e2f43d238eae4f0b04f58d4c0df4615697d4ca3e9f9b1963d49555a94f0f5a04"}, - {file = "aiohttp-3.10.2-cp39-cp39-win_amd64.whl", hash = "sha256:947847f07a8f81d7b39b2d0202fd73e61962ebe17ac2d8566f260679e467da7b"}, - {file = "aiohttp-3.10.2.tar.gz", hash = "sha256:4d1f694b5d6e459352e5e925a42e05bac66655bfde44d81c59992463d2897014"}, + {file = "aiohttp-3.10.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a1ba7bc139592339ddeb62c06486d0fa0f4ca61216e14137a40d626c81faf10c"}, + {file = "aiohttp-3.10.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:85e4d7bd05d18e4b348441e7584c681eff646e3bf38f68b2626807f3add21aa2"}, + {file = "aiohttp-3.10.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69de056022e7abf69cb9fec795515973cc3eeaff51e3ea8d72a77aa933a91c52"}, + {file = "aiohttp-3.10.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee3587506898d4a404b33bd19689286ccf226c3d44d7a73670c8498cd688e42c"}, + {file = "aiohttp-3.10.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe285a697c851734285369614443451462ce78aac2b77db23567507484b1dc6f"}, + {file = "aiohttp-3.10.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10c7932337285a6bfa3a5fe1fd4da90b66ebfd9d0cbd1544402e1202eb9a8c3e"}, + {file = "aiohttp-3.10.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd9716ef0224fe0d0336997eb242f40619f9f8c5c57e66b525a1ebf9f1d8cebe"}, + {file = "aiohttp-3.10.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ceacea31f8a55cdba02bc72c93eb2e1b77160e91f8abd605969c168502fd71eb"}, + {file = "aiohttp-3.10.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9721554bfa9e15f6e462da304374c2f1baede3cb06008c36c47fa37ea32f1dc4"}, + {file = "aiohttp-3.10.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:22cdeb684d8552490dd2697a5138c4ecb46f844892df437aaf94f7eea99af879"}, + {file = "aiohttp-3.10.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e56bb7e31c4bc79956b866163170bc89fd619e0581ce813330d4ea46921a4881"}, + {file = "aiohttp-3.10.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3a95d2686bc4794d66bd8de654e41b5339fab542b2bca9238aa63ed5f4f2ce82"}, + {file = "aiohttp-3.10.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d82404a0e7b10e0d7f022cf44031b78af8a4f99bd01561ac68f7c24772fed021"}, + {file = "aiohttp-3.10.8-cp310-cp310-win32.whl", hash = "sha256:4e10b04542d27e21538e670156e88766543692a0a883f243ba8fad9ddea82e53"}, + {file = "aiohttp-3.10.8-cp310-cp310-win_amd64.whl", hash = "sha256:680dbcff5adc7f696ccf8bf671d38366a1f620b5616a1d333d0cb33956065395"}, + {file = "aiohttp-3.10.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:33a68011a38020ed4ff41ae0dbf4a96a202562ecf2024bdd8f65385f1d07f6ef"}, + {file = "aiohttp-3.10.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6c7efa6616a95e3bd73b8a69691012d2ef1f95f9ea0189e42f338fae080c2fc6"}, + {file = "aiohttp-3.10.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb9b9764cfb4459acf01c02d2a59d3e5066b06a846a364fd1749aa168efa2be"}, + {file = "aiohttp-3.10.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c7f270f4ca92760f98a42c45a58674fff488e23b144ec80b1cc6fa2effed377"}, + {file = "aiohttp-3.10.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6984dda9d79064361ab58d03f6c1e793ea845c6cfa89ffe1a7b9bb400dfd56bd"}, + {file = "aiohttp-3.10.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f6d47e392c27206701565c8df4cac6ebed28fdf6dcaea5b1eea7a4631d8e6db"}, + {file = "aiohttp-3.10.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a72f89aea712c619b2ca32c6f4335c77125ede27530ad9705f4f349357833695"}, + {file = "aiohttp-3.10.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36074b26f3263879ba8e4dbd33db2b79874a3392f403a70b772701363148b9f"}, + {file = "aiohttp-3.10.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e32148b4a745e70a255a1d44b5664de1f2e24fcefb98a75b60c83b9e260ddb5b"}, + {file = "aiohttp-3.10.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5aa1a073514cf59c81ad49a4ed9b5d72b2433638cd53160fd2f3a9cfa94718db"}, + {file = "aiohttp-3.10.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d3a79200a9d5e621c4623081ddb25380b713c8cf5233cd11c1aabad990bb9381"}, + {file = "aiohttp-3.10.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e45fdfcb2d5bcad83373e4808825b7512953146d147488114575780640665027"}, + {file = "aiohttp-3.10.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f78e2a78432c537ae876a93013b7bc0027ba5b93ad7b3463624c4b6906489332"}, + {file = "aiohttp-3.10.8-cp311-cp311-win32.whl", hash = "sha256:f8179855a4e4f3b931cb1764ec87673d3fbdcca2af496c8d30567d7b034a13db"}, + {file = "aiohttp-3.10.8-cp311-cp311-win_amd64.whl", hash = "sha256:ef9b484604af05ca745b6108ca1aaa22ae1919037ae4f93aaf9a37ba42e0b835"}, + {file = "aiohttp-3.10.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ab2d6523575fc98896c80f49ac99e849c0b0e69cc80bf864eed6af2ae728a52b"}, + {file = "aiohttp-3.10.8-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f5d5d5401744dda50b943d8764508d0e60cc2d3305ac1e6420935861a9d544bc"}, + {file = "aiohttp-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de23085cf90911600ace512e909114385026b16324fa203cc74c81f21fd3276a"}, + {file = "aiohttp-3.10.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4618f0d2bf523043866a9ff8458900d8eb0a6d4018f251dae98e5f1fb699f3a8"}, + {file = "aiohttp-3.10.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21c1925541ca84f7b5e0df361c0a813a7d6a56d3b0030ebd4b220b8d232015f9"}, + {file = "aiohttp-3.10.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:497a7d20caea8855c5429db3cdb829385467217d7feb86952a6107e033e031b9"}, + {file = "aiohttp-3.10.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c887019dbcb4af58a091a45ccf376fffe800b5531b45c1efccda4bedf87747ea"}, + {file = "aiohttp-3.10.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40d2d719c3c36a7a65ed26400e2b45b2d9ed7edf498f4df38b2ae130f25a0d01"}, + {file = "aiohttp-3.10.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:57359785f27394a8bcab0da6dcd46706d087dfebf59a8d0ad2e64a4bc2f6f94f"}, + {file = "aiohttp-3.10.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a961ee6f2cdd1a2be4735333ab284691180d40bad48f97bb598841bfcbfb94ec"}, + {file = "aiohttp-3.10.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:fe3d79d6af839ffa46fdc5d2cf34295390894471e9875050eafa584cb781508d"}, + {file = "aiohttp-3.10.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a281cba03bdaa341c70b7551b2256a88d45eead149f48b75a96d41128c240b3"}, + {file = "aiohttp-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6769d71bfb1ed60321363a9bc05e94dcf05e38295ef41d46ac08919e5b00d19"}, + {file = "aiohttp-3.10.8-cp312-cp312-win32.whl", hash = "sha256:a3081246bab4d419697ee45e555cef5cd1def7ac193dff6f50be761d2e44f194"}, + {file = "aiohttp-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:ab1546fc8e00676febc81c548a876c7bde32f881b8334b77f84719ab2c7d28dc"}, + {file = "aiohttp-3.10.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b1a012677b8e0a39e181e218de47d6741c5922202e3b0b65e412e2ce47c39337"}, + {file = "aiohttp-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2df786c96c57cd6b87156ba4c5f166af7b88f3fc05f9d592252fdc83d8615a3c"}, + {file = "aiohttp-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8885ca09d3a9317219c0831276bfe26984b17b2c37b7bf70dd478d17092a4772"}, + {file = "aiohttp-3.10.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dbf252ac19860e0ab56cd480d2805498f47c5a2d04f5995d8d8a6effd04b48c"}, + {file = "aiohttp-3.10.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2036479b6b94afaaca7d07b8a68dc0e67b0caf5f6293bb6a5a1825f5923000"}, + {file = "aiohttp-3.10.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:365783e1b7c40b59ed4ce2b5a7491bae48f41cd2c30d52647a5b1ee8604c68ad"}, + {file = "aiohttp-3.10.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:270e653b5a4b557476a1ed40e6b6ce82f331aab669620d7c95c658ef976c9c5e"}, + {file = "aiohttp-3.10.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8960fabc20bfe4fafb941067cda8e23c8c17c98c121aa31c7bf0cdab11b07842"}, + {file = "aiohttp-3.10.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f21e8f2abed9a44afc3d15bba22e0dfc71e5fa859bea916e42354c16102b036f"}, + {file = "aiohttp-3.10.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fecd55e7418fabd297fd836e65cbd6371aa4035a264998a091bbf13f94d9c44d"}, + {file = "aiohttp-3.10.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:badb51d851358cd7535b647bb67af4854b64f3c85f0d089c737f75504d5910ec"}, + {file = "aiohttp-3.10.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e860985f30f3a015979e63e7ba1a391526cdac1b22b7b332579df7867848e255"}, + {file = "aiohttp-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:71462f8eeca477cbc0c9700a9464e3f75f59068aed5e9d4a521a103692da72dc"}, + {file = "aiohttp-3.10.8-cp313-cp313-win32.whl", hash = "sha256:177126e971782769b34933e94fddd1089cef0fe6b82fee8a885e539f5b0f0c6a"}, + {file = "aiohttp-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:98a4eb60e27033dee9593814ca320ee8c199489fbc6b2699d0f710584db7feb7"}, + {file = "aiohttp-3.10.8-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ffef3d763e4c8fc97e740da5b4d0f080b78630a3914f4e772a122bbfa608c1db"}, + {file = "aiohttp-3.10.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:597128cb7bc5f068181b49a732961f46cb89f85686206289d6ccb5e27cb5fbe2"}, + {file = "aiohttp-3.10.8-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f23a6c1d09de5de89a33c9e9b229106cb70dcfdd55e81a3a3580eaadaa32bc92"}, + {file = "aiohttp-3.10.8-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da57af0c54a302b7c655fa1ccd5b1817a53739afa39924ef1816e7b7c8a07ccb"}, + {file = "aiohttp-3.10.8-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e7a6af57091056a79a35104d6ec29d98ec7f1fb7270ad9c6fff871b678d1ff8"}, + {file = "aiohttp-3.10.8-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:32710d6b3b6c09c60c794d84ca887a3a2890131c0b02b3cefdcc6709a2260a7c"}, + {file = "aiohttp-3.10.8-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b91f4f62ad39a8a42d511d66269b46cb2fb7dea9564c21ab6c56a642d28bff5"}, + {file = "aiohttp-3.10.8-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:471a8c47344b9cc309558b3fcc469bd2c12b49322b4b31eb386c4a2b2d44e44a"}, + {file = "aiohttp-3.10.8-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc0e7f91705445d79beafba9bb3057dd50830e40fe5417017a76a214af54e122"}, + {file = "aiohttp-3.10.8-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:85431c9131a9a0f65260dc7a65c800ca5eae78c4c9931618f18c8e0933a0e0c1"}, + {file = "aiohttp-3.10.8-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:b91557ee0893da52794b25660d4f57bb519bcad8b7df301acd3898f7197c5d81"}, + {file = "aiohttp-3.10.8-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:4954e6b06dd0be97e1a5751fc606be1f9edbdc553c5d9b57d72406a8fbd17f9d"}, + {file = "aiohttp-3.10.8-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a087c84b4992160ffef7afd98ef24177c8bd4ad61c53607145a8377457385100"}, + {file = "aiohttp-3.10.8-cp38-cp38-win32.whl", hash = "sha256:e1f0f7b27171b2956a27bd8f899751d0866ddabdd05cbddf3520f945130a908c"}, + {file = "aiohttp-3.10.8-cp38-cp38-win_amd64.whl", hash = "sha256:c4916070e12ae140110aa598031876c1bf8676a36a750716ea0aa5bd694aa2e7"}, + {file = "aiohttp-3.10.8-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5284997e3d88d0dfb874c43e51ae8f4a6f4ca5b90dcf22995035187253d430db"}, + {file = "aiohttp-3.10.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9443d9ebc5167ce1fbb552faf2d666fb22ef5716a8750be67efd140a7733738c"}, + {file = "aiohttp-3.10.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b667e2a03407d79a76c618dc30cedebd48f082d85880d0c9c4ec2faa3e10f43e"}, + {file = "aiohttp-3.10.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98fae99d5c2146f254b7806001498e6f9ffb0e330de55a35e72feb7cb2fa399b"}, + {file = "aiohttp-3.10.8-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8296edd99d0dd9d0eb8b9e25b3b3506eef55c1854e9cc230f0b3f885f680410b"}, + {file = "aiohttp-3.10.8-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ce46dfb49cfbf9e92818be4b761d4042230b1f0e05ffec0aad15b3eb162b905"}, + {file = "aiohttp-3.10.8-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c38cfd355fd86c39b2d54651bd6ed7d63d4fe3b5553f364bae3306e2445f847"}, + {file = "aiohttp-3.10.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:713dff3f87ceec3bde4f3f484861464e722cf7533f9fa6b824ec82bb5a9010a7"}, + {file = "aiohttp-3.10.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:21a72f4a9c69a8567a0aca12042f12bba25d3139fd5dd8eeb9931f4d9e8599cd"}, + {file = "aiohttp-3.10.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6d1ad868624f6cea77341ef2877ad4e71f7116834a6cd7ec36ec5c32f94ee6ae"}, + {file = "aiohttp-3.10.8-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a78ba86d5a08207d1d1ad10b97aed6ea48b374b3f6831d02d0b06545ac0f181e"}, + {file = "aiohttp-3.10.8-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:aff048793d05e1ce05b62e49dccf81fe52719a13f4861530706619506224992b"}, + {file = "aiohttp-3.10.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d088ca05381fd409793571d8e34eca06daf41c8c50a05aeed358d2d340c7af81"}, + {file = "aiohttp-3.10.8-cp39-cp39-win32.whl", hash = "sha256:ee97c4e54f457c366e1f76fbbf3e8effee9de57dae671084a161c00f481106ce"}, + {file = "aiohttp-3.10.8-cp39-cp39-win_amd64.whl", hash = "sha256:d95ae4420669c871667aad92ba8cce6251d61d79c1a38504621094143f94a8b4"}, + {file = "aiohttp-3.10.8.tar.gz", hash = "sha256:21f8225f7dc187018e8433c9326be01477fb2810721e048b33ac49091b19fb4a"}, ] [package.dependencies] @@ -114,7 +129,7 @@ async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} attrs = ">=17.3.0" frozenlist = ">=1.1.1" multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" +yarl = ">=1.12.0,<2.0" [package.extras] speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] @@ -150,24 +165,24 @@ grapheme = "0.6.0" [[package]] name = "antlr4-python3-runtime" -version = "4.13.1" -description = "ANTLR 4.13.1 runtime for Python 3" +version = "4.13.2" +description = "ANTLR 4.13.2 runtime for Python 3" optional = false python-versions = "*" files = [ - {file = "antlr4-python3-runtime-4.13.1.tar.gz", hash = "sha256:3cd282f5ea7cfb841537fe01f143350fdb1c0b1ce7981443a2fa8513fddb6d1a"}, - {file = "antlr4_python3_runtime-4.13.1-py3-none-any.whl", hash = "sha256:78ec57aad12c97ac039ca27403ad61cb98aaec8a3f9bb8144f889aa0fa28b943"}, + {file = "antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8"}, + {file = "antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916"}, ] [[package]] name = "anyio" -version = "4.4.0" +version = "4.6.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, + {file = "anyio-4.6.0-py3-none-any.whl", hash = "sha256:c7d2e9d63e31599eeb636c8c5c03a7e108d73b345f064f1c19fdc87b79036a9a"}, + {file = "anyio-4.6.0.tar.gz", hash = "sha256:137b4559cbb034c477165047febb6ff83f390fc3b20bf181c1fc0a728cb8beeb"}, ] [package.dependencies] @@ -177,19 +192,19 @@ sniffio = ">=1.1" typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] [[package]] name = "astroid" -version = "3.2.4" +version = "3.3.4" description = "An abstract syntax tree for Python with inference support." optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" files = [ - {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, - {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, + {file = "astroid-3.3.4-py3-none-any.whl", hash = "sha256:5eba185467253501b62a9f113c263524b4f5d55e1b30456370eed4cdbd6438fd"}, + {file = "astroid-3.3.4.tar.gz", hash = "sha256:e73d0b62dd680a7c07cb2cd0ce3c22570b044dd01bd994bc3a2dd16c6cbba162"}, ] [package.dependencies] @@ -208,32 +223,32 @@ files = [ [[package]] name = "attrs" -version = "23.2.0" +version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "authlib" -version = "1.3.1" +version = "1.3.2" description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false python-versions = ">=3.8" files = [ - {file = "Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377"}, - {file = "authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917"}, + {file = "Authlib-1.3.2-py2.py3-none-any.whl", hash = "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc"}, + {file = "authlib-1.3.2.tar.gz", hash = "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2"}, ] [package.dependencies] @@ -241,13 +256,13 @@ cryptography = "*" [[package]] name = "aws-sam-translator" -version = "1.87.0" +version = "1.89.0" description = "AWS SAM Translator is a library that transform SAM templates into AWS CloudFormation templates" optional = false python-versions = "!=4.0,<=4.0,>=3.8" files = [ - {file = "aws-sam-translator-1.87.0.tar.gz", hash = "sha256:80f4fb6d53774634b6ea84af5fdfa9ad94a46945bc4ad4ef11c8008540dfa7f8"}, - {file = "aws_sam_translator-1.87.0-py3-none-any.whl", hash = "sha256:40cef8980d656107406dafe30aef34b67be33929d2b9492e93f8e9ce07ece1c1"}, + {file = "aws_sam_translator-1.89.0-py3-none-any.whl", hash = "sha256:843be1b5ca7634f700ad0c844a7e0dc42858f35da502e91691473eadd1731ded"}, + {file = "aws_sam_translator-1.89.0.tar.gz", hash = "sha256:fff1005d0b1f3cb511d0ac7e85f54af06afc9d9e433df013a2338d7a0168d174"}, ] [package.dependencies] @@ -257,17 +272,17 @@ pydantic = ">=1.8,<3" typing-extensions = ">=4.4" [package.extras] -dev = ["black (==23.10.1)", "boto3 (>=1.23,<2)", "boto3-stubs[appconfig,serverlessrepo] (>=1.19.5,<2.dev0)", "coverage (>=5.3,<8)", "dateparser (>=1.1,<2.0)", "mypy (>=1.3.0,<1.4.0)", "parameterized (>=0.7,<1.0)", "pytest (>=6.2,<8)", "pytest-cov (>=2.10,<5)", "pytest-env (>=0.6,<1)", "pytest-rerunfailures (>=9.1,<12)", "pytest-xdist (>=2.5,<4)", "pyyaml (>=6.0,<7.0)", "requests (>=2.28,<3.0)", "ruamel.yaml (==0.17.21)", "ruff (>=0.1.0,<0.2.0)", "tenacity (>=8.0,<9.0)", "types-PyYAML (>=6.0,<7.0)", "types-jsonschema (>=3.2,<4.0)"] +dev = ["black (==24.3.0)", "boto3 (>=1.23,<2)", "boto3-stubs[appconfig,serverlessrepo] (>=1.19.5,<2.dev0)", "coverage (>=5.3,<8)", "dateparser (>=1.1,<2.0)", "mypy (>=1.3.0,<1.4.0)", "parameterized (>=0.7,<1.0)", "pytest (>=6.2,<8)", "pytest-cov (>=2.10,<5)", "pytest-env (>=0.6,<1)", "pytest-rerunfailures (>=9.1,<12)", "pytest-xdist (>=2.5,<4)", "pyyaml (>=6.0,<7.0)", "requests (>=2.28,<3.0)", "ruamel.yaml (==0.17.21)", "ruff (>=0.1.0,<0.2.0)", "tenacity (>=8.0,<9.0)", "types-PyYAML (>=6.0,<7.0)", "types-jsonschema (>=3.2,<4.0)"] [[package]] name = "aws-xray-sdk" -version = "2.13.0" +version = "2.14.0" description = "The AWS X-Ray SDK for Python (the SDK) enables Python developers to record and emit information from within their applications to the AWS X-Ray service." optional = false python-versions = ">=3.7" files = [ - {file = "aws-xray-sdk-2.13.0.tar.gz", hash = "sha256:816186126917bc35ae4e6e2f304702a43d494ecef34a39e6330f5018bdecc9f5"}, - {file = "aws_xray_sdk-2.13.0-py2.py3-none-any.whl", hash = "sha256:d18604a8688b4bed03ce4a858cc9acd72b71400e085bf7512fc31cf657ca85f9"}, + {file = "aws_xray_sdk-2.14.0-py2.py3-none-any.whl", hash = "sha256:cfbe6feea3d26613a2a869d14c9246a844285c97087ad8f296f901633554ad94"}, + {file = "aws_xray_sdk-2.14.0.tar.gz", hash = "sha256:aab843c331af9ab9ba5cefb3a303832a19db186140894a523edafc024cc0493c"}, ] [package.dependencies] @@ -298,13 +313,13 @@ files = [ [[package]] name = "azure-core" -version = "1.30.1" +version = "1.31.0" description = "Microsoft Azure Core Library for Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "azure-core-1.30.1.tar.gz", hash = "sha256:26273a254131f84269e8ea4464f3560c731f29c0c1f69ac99010845f239c1a8f"}, - {file = "azure_core-1.30.1-py3-none-any.whl", hash = "sha256:7c5ee397e48f281ec4dd773d67a0a47a0962ed6fa833036057f9ea067f688e74"}, + {file = "azure_core-1.31.0-py3-none-any.whl", hash = "sha256:22954de3777e0250029360ef31d80448ef1be13b80a459bff80ba7073379e2cd"}, + {file = "azure_core-1.31.0.tar.gz", hash = "sha256:656a0dd61e1869b1506b7c6a3b31d62f15984b1a573d6326f6aa2f3e4123284b"}, ] [package.dependencies] @@ -317,20 +332,20 @@ aio = ["aiohttp (>=3.0)"] [[package]] name = "azure-identity" -version = "1.17.1" +version = "1.18.0" description = "Microsoft Azure Identity Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure-identity-1.17.1.tar.gz", hash = "sha256:32ecc67cc73f4bd0595e4f64b1ca65cd05186f4fe6f98ed2ae9f1aa32646efea"}, - {file = "azure_identity-1.17.1-py3-none-any.whl", hash = "sha256:db8d59c183b680e763722bfe8ebc45930e6c57df510620985939f7f3191e0382"}, + {file = "azure_identity-1.18.0-py3-none-any.whl", hash = "sha256:bccf6106245b49ff41d0c4cd7b72851c5a2ba3a32cef7589da246f5727f26f02"}, + {file = "azure_identity-1.18.0.tar.gz", hash = "sha256:f567579a65d8932fa913c76eddf3305101a15e5727a5e4aa5df649a0f553d4c3"}, ] [package.dependencies] -azure-core = ">=1.23.0" +azure-core = ">=1.31.0" cryptography = ">=2.5" -msal = ">=1.24.0" -msal-extensions = ">=0.3.0" +msal = ">=1.30.0" +msal-extensions = ">=1.2.0" typing-extensions = ">=4.0.0" [[package]] @@ -399,15 +414,31 @@ azure-mgmt-core = ">=1.3.2" isodate = ">=0.6.1" typing-extensions = ">=4.6.0" +[[package]] +name = "azure-mgmt-containerregistry" +version = "10.3.0" +description = "Microsoft Azure Container Registry Client Library for Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "azure-mgmt-containerregistry-10.3.0.tar.gz", hash = "sha256:ae21651855dfb19c42d91d6b3a965c6c611e23f8bc4bf7138835e652d2f918e3"}, + {file = "azure_mgmt_containerregistry-10.3.0-py3-none-any.whl", hash = "sha256:851e1c57f9bc4a3589c6b21fb627c11fd6cbb57a0388b7dfccd530ba3160805f"}, +] + +[package.dependencies] +azure-common = ">=1.1,<2.0" +azure-mgmt-core = ">=1.3.2,<2.0.0" +isodate = ">=0.6.1,<1.0.0" + [[package]] name = "azure-mgmt-containerservice" -version = "31.0.0" +version = "32.0.0" description = "Microsoft Azure Container Service Management Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure-mgmt-containerservice-31.0.0.tar.gz", hash = "sha256:134358d7f88c4d29b4009f91d7619861e1fad5dbea5e147402dd61ad96b5624a"}, - {file = "azure_mgmt_containerservice-31.0.0-py3-none-any.whl", hash = "sha256:75c6dbbaf27ed6c8251e34b86fefcfb001fe096c69df9ce93c163de71a2b5275"}, + {file = "azure_mgmt_containerservice-32.0.0-py3-none-any.whl", hash = "sha256:66ff0a2651122207db0d5492e86550561f9d81509379abe7d401ed4805a1c66b"}, + {file = "azure_mgmt_containerservice-32.0.0.tar.gz", hash = "sha256:ccb587479d8a93ec78f7162590adc82e4fba4de76e4adc3419f33ad225efb292"}, ] [package.dependencies] @@ -432,19 +463,20 @@ azure-core = ">=1.26.2,<2.0.0" [[package]] name = "azure-mgmt-cosmosdb" -version = "9.5.1" +version = "9.6.0" description = "Microsoft Azure Cosmos DB Management Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure-mgmt-cosmosdb-9.5.1.tar.gz", hash = "sha256:4e55d3973f11cf02cf7a8055aee86615233d7cbe3bb227a07c176bac8bc7ef02"}, - {file = "azure_mgmt_cosmosdb-9.5.1-py3-none-any.whl", hash = "sha256:860cad5583d63936e5f9477f7878a256c4dee039169cdea554da851f0762f0c7"}, + {file = "azure_mgmt_cosmosdb-9.6.0-py3-none-any.whl", hash = "sha256:02b4108867de58e0b89a206ee7b7588b439e1f6fef2377ce1979b803a0d02d5a"}, + {file = "azure_mgmt_cosmosdb-9.6.0.tar.gz", hash = "sha256:667c7d8a8f542b0e7972e63274af536ad985187e24a6cc2e3c8eef35560881fc"}, ] [package.dependencies] azure-common = ">=1.1" azure-mgmt-core = ">=1.3.2" isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" [[package]] name = "azure-mgmt-keyvault" @@ -481,19 +513,20 @@ isodate = ">=0.6.1,<1.0.0" [[package]] name = "azure-mgmt-network" -version = "26.0.0" +version = "27.0.0" description = "Microsoft Azure Network Management Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure-mgmt-network-26.0.0.tar.gz", hash = "sha256:4de676184195053fdb106a6ea1042a894e70c731a6d3c8a633d52f5229f4ee1b"}, - {file = "azure_mgmt_network-26.0.0-py3-none-any.whl", hash = "sha256:15d6a77d7429bdcc76f8482158845d42f9d9434529e1ad2b6778269cc14627e9"}, + {file = "azure_mgmt_network-27.0.0-py3-none-any.whl", hash = "sha256:67295fda351fdae79d14591487009e911346adea9578bb1b04636a15e3472598"}, + {file = "azure_mgmt_network-27.0.0.tar.gz", hash = "sha256:5c1c61d8bb13ad40f788a26fd7569c1d9d60eb2e4cb19c2a1b5d9c02ae862316"}, ] [package.dependencies] azure-common = ">=1.1" azure-mgmt-core = ">=1.3.2" isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" [[package]] name = "azure-mgmt-rdbms" @@ -610,33 +643,33 @@ typing-extensions = ">=4.6.0" [[package]] name = "azure-storage-blob" -version = "12.22.0" +version = "12.23.1" description = "Microsoft Azure Blob Storage Client Library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "azure-storage-blob-12.22.0.tar.gz", hash = "sha256:b3804bb4fe8ab1c32771fa464053da772a682c2737b19da438a3f4e5e3b3736e"}, - {file = "azure_storage_blob-12.22.0-py3-none-any.whl", hash = "sha256:bb7d2d824ce3f11f14a27ee7d9281289f7e072ac8311c52e3652672455b7d5e8"}, + {file = "azure_storage_blob-12.23.1-py3-none-any.whl", hash = "sha256:1c2238aa841d1545f42714a5017c010366137a44a0605da2d45f770174bfc6b4"}, + {file = "azure_storage_blob-12.23.1.tar.gz", hash = "sha256:a587e54d4e39d2a27bd75109db164ffa2058fe194061e5446c5a89bca918272f"}, ] [package.dependencies] -azure-core = ">=1.28.0" +azure-core = ">=1.30.0" cryptography = ">=2.1.4" isodate = ">=0.6.1" typing-extensions = ">=4.6.0" [package.extras] -aio = ["azure-core[aio] (>=1.28.0)"] +aio = ["azure-core[aio] (>=1.30.0)"] [[package]] name = "babel" -version = "2.14.0" +version = "2.16.0" description = "Internationalization utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, - {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] [package.extras] @@ -644,13 +677,13 @@ dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] [[package]] name = "bandit" -version = "1.7.9" +version = "1.7.10" description = "Security oriented static analyser for python code." optional = false python-versions = ">=3.8" files = [ - {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, - {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, + {file = "bandit-1.7.10-py3-none-any.whl", hash = "sha256:665721d7bebbb4485a339c55161ac0eedde27d51e638000d91c8c2d68343ad02"}, + {file = "bandit-1.7.10.tar.gz", hash = "sha256:59ed5caf5d92b6ada4bf65bc6437feea4a9da1093384445fed4d472acc6cff7b"}, ] [package.dependencies] @@ -714,28 +747,28 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "blinker" -version = "1.7.0" +version = "1.8.2" description = "Fast, simple object-to-object and broadcast signaling" optional = false python-versions = ">=3.8" files = [ - {file = "blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9"}, - {file = "blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"}, + {file = "blinker-1.8.2-py3-none-any.whl", hash = "sha256:1779309f71bf239144b9399d06ae925637cf6634cf6bd131104184531bf67c01"}, + {file = "blinker-1.8.2.tar.gz", hash = "sha256:8f77b09d3bf7c795e969e9486f39c2c5e9c39d4ee07424be2bc594ece9642d83"}, ] [[package]] name = "boto3" -version = "1.35.15" +version = "1.35.28" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.35.15-py3-none-any.whl", hash = "sha256:c8e3567a843bee89b826b71916748088386ccd48014f0f55f56288215a686048"}, - {file = "boto3-1.35.15.tar.gz", hash = "sha256:5e286daa84f945e4c57b133fc96bd2abbcf4066e5d4e63c591ce5e949913d6fd"}, + {file = "boto3-1.35.28-py3-none-any.whl", hash = "sha256:dc088b86a14f17d3cd2e96915c6ccfd31bce640dfe9180df579ed311bc6bf0fc"}, + {file = "boto3-1.35.28.tar.gz", hash = "sha256:8960fc458b9ba3c8a9890a607c31cee375db821f39aefaec9ff638248e81644a"}, ] [package.dependencies] -botocore = ">=1.35.15,<1.36.0" +botocore = ">=1.35.28,<1.36.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -744,13 +777,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.35.15" +version = "1.35.29" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.35.15-py3-none-any.whl", hash = "sha256:a01a7668941c9276ca7697fb0b09a47ab1f13e585c5ca6551270528f4e086de8"}, - {file = "botocore-1.35.15.tar.gz", hash = "sha256:bbfe4f6cd417d1d097900db062f1017e5185775acdeccd0eff1ec0f499633242"}, + {file = "botocore-1.35.29-py3-none-any.whl", hash = "sha256:f8e3ae0d84214eff3fb69cb4dc51cea6c43d3bde82027a94d00c52b941d6c3d5"}, + {file = "botocore-1.35.29.tar.gz", hash = "sha256:4ed28ab03675bb008a290c452c5ddd7aaa5d4e3fa1912aadbdf93057ee84362b"}, ] [package.dependencies] @@ -762,89 +795,104 @@ urllib3 = [ ] [package.extras] -crt = ["awscrt (==0.21.2)"] +crt = ["awscrt (==0.21.5)"] [[package]] name = "cachetools" -version = "5.3.3" +version = "5.5.0" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" files = [ - {file = "cachetools-5.3.3-py3-none-any.whl", hash = "sha256:0abad1021d3f8325b2fc1d2e9c8b9c9d57b04c3932657a72465447332c24d945"}, - {file = "cachetools-5.3.3.tar.gz", hash = "sha256:ba29e2dfa0b8b556606f097407ed1aa62080ee108ab0dc5ec9d6a723a007d105"}, + {file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"}, + {file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"}, ] [[package]] name = "certifi" -version = "2024.7.4" +version = "2024.8.30" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, + {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, + {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, ] [[package]] name = "cffi" -version = "1.16.0" +version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, ] [package.dependencies] @@ -852,26 +900,29 @@ pycparser = "*" [[package]] name = "cfn-lint" -version = "0.86.4" +version = "1.10.3" description = "Checks CloudFormation templates for practices and behaviour that could potentially be improved" optional = false -python-versions = "!=4.0,<=4.0,>=3.8" +python-versions = ">=3.8" files = [ - {file = "cfn_lint-0.86.4-py3-none-any.whl", hash = "sha256:2e779afba0acd9878a4e2dc07a93679411495512241a4da9b9d52aca5254334f"}, - {file = "cfn_lint-0.86.4.tar.gz", hash = "sha256:9ee31451a18457f2b27cd064f5d99adbf79afd1402aaf4b614514d13d8bc0174"}, + {file = "cfn_lint-1.10.3-py3-none-any.whl", hash = "sha256:03c0193c38b805fed025e66a6d5bdf41cba0390f6ed590bf5702a8c38d08e542"}, + {file = "cfn_lint-1.10.3.tar.gz", hash = "sha256:afb62356719cfadc6c8561773ee479a6f4f335cdeea6c36c3ecb241f26d586a9"}, ] [package.dependencies] -aws-sam-translator = ">=1.87.0" -jschema-to-python = ">=1.2.3,<1.3.0" +aws-sam-translator = ">=1.89.0" jsonpatch = "*" -jsonschema = ">=3.0,<5" -junit-xml = ">=1.9,<2.0" networkx = ">=2.4,<4" pyyaml = ">5.4" -regex = ">=2021.7.1" -sarif-om = ">=1.0.4,<1.1.0" +regex = "*" sympy = ">=1.0.0" +typing-extensions = "*" + +[package.extras] +full = ["jschema-to-python (>=1.2.3,<1.3.0)", "junit-xml (>=1.9,<2.0)", "pydot", "sarif-om (>=1.0.4,<1.1.0)"] +graph = ["pydot"] +junit = ["junit-xml (>=1.9,<2.0)"] +sarif = ["jschema-to-python (>=1.2.3,<1.3.0)", "sarif-om (>=1.0.4,<1.1.0)"] [[package]] name = "charset-normalizer" @@ -1152,13 +1203,13 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "dash" -version = "2.18.0" +version = "2.18.1" description = "A Python framework for building reactive web-apps. Developed by Plotly." optional = false python-versions = ">=3.8" files = [ - {file = "dash-2.18.0-py3-none-any.whl", hash = "sha256:2d37b479be20bef1a184d35be4a654e4da131da6538d563fc813be3e28b90cbc"}, - {file = "dash-2.18.0.tar.gz", hash = "sha256:9be6ea7562d40bb70211df8cbfb23611cc2ca9ae63cfca9af8f96679989adc40"}, + {file = "dash-2.18.1-py3-none-any.whl", hash = "sha256:07c4513bb5f79a4b936847a0b49afc21dbd4b001ff77ea78d4d836043e211a07"}, + {file = "dash-2.18.1.tar.gz", hash = "sha256:ffdf89690d734f6851ef1cb344222826ffb11ad2214ab9172668bf8aadd75d12"}, ] [package.dependencies] @@ -1271,13 +1322,13 @@ word-list = ["pyahocorasick"] [[package]] name = "dill" -version = "0.3.8" +version = "0.3.9" description = "serialize all of Python" optional = false python-versions = ">=3.8" files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, ] [package.extras] @@ -1347,6 +1398,16 @@ conda = ["pyyaml"] pipenv = ["pipenv"] poetry = ["poetry"] +[[package]] +name = "durationpy" +version = "0.7" +description = "Module for converting between datetime.timedelta and Go's Duration strings." +optional = false +python-versions = "*" +files = [ + {file = "durationpy-0.7.tar.gz", hash = "sha256:8447c43df4f1a0b434e70c15a38d77f5c9bd17284bfc1ff1d430f233d5083732"}, +] + [[package]] name = "email-validator" version = "2.1.1" @@ -1364,13 +1425,13 @@ idna = ">=2.0.0" [[package]] name = "exceptiongroup" -version = "1.2.1" +version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, - {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] @@ -1596,20 +1657,20 @@ test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", [[package]] name = "google-api-core" -version = "2.18.0" +version = "2.20.0" description = "Google API client core library" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-core-2.18.0.tar.gz", hash = "sha256:62d97417bfc674d6cef251e5c4d639a9655e00c45528c4364fbfebb478ce72a9"}, - {file = "google_api_core-2.18.0-py3-none-any.whl", hash = "sha256:5a63aa102e0049abe85b5b88cb9409234c1f70afcda21ce1e40b285b9629c1d6"}, + {file = "google_api_core-2.20.0-py3-none-any.whl", hash = "sha256:ef0591ef03c30bb83f79b3d0575c3f31219001fc9c5cf37024d08310aeffed8a"}, + {file = "google_api_core-2.20.0.tar.gz", hash = "sha256:f74dff1889ba291a4b76c5079df0711810e2d9da81abfdc99957bc961c1eb28f"}, ] [package.dependencies] google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" proto-plus = ">=1.22.3,<2.0.0dev" -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" +protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" [package.extras] @@ -1619,13 +1680,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.144.0" +version = "2.147.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google_api_python_client-2.144.0-py2.py3-none-any.whl", hash = "sha256:f9c333ac4454a012adca90c297f9a22611a8953f3aae5481f90b3a56b9bdd413"}, - {file = "google_api_python_client-2.144.0.tar.gz", hash = "sha256:fe00851b257157bca600e1692ed8a54762c4a5c7d9eb7f6d4822059424b0d0a9"}, + {file = "google_api_python_client-2.147.0-py2.py3-none-any.whl", hash = "sha256:c6ecfa193c695baa41e84562d8f8f244fcd164419eca3fc9fd7565646668f9b2"}, + {file = "google_api_python_client-2.147.0.tar.gz", hash = "sha256:e864c2cf61d34c00f05278b8bdb72b93b6fa34f0de9ead51d20435f3b65f91be"}, ] [package.dependencies] @@ -1637,13 +1698,13 @@ uritemplate = ">=3.0.1,<5" [[package]] name = "google-auth" -version = "2.30.0" +version = "2.35.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google-auth-2.30.0.tar.gz", hash = "sha256:ab630a1320f6720909ad76a7dbdb6841cdf5c66b328d690027e4867bdfb16688"}, - {file = "google_auth-2.30.0-py2.py3-none-any.whl", hash = "sha256:8df7da660f62757388b8a7f249df13549b3373f24388cb5d2f1dd91cc18180b5"}, + {file = "google_auth-2.35.0-py2.py3-none-any.whl", hash = "sha256:25df55f327ef021de8be50bad0dfd4a916ad0de96da86cd05661c9297723ad3f"}, + {file = "google_auth-2.35.0.tar.gz", hash = "sha256:f4c64ed4e01e8e8b646ef34c018f8bf3338df0c8e37d8b3bba40e7f574a3278a"}, ] [package.dependencies] @@ -1653,7 +1714,7 @@ rsa = ">=3.1.4,<5" [package.extras] aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] -enterprise-cert = ["cryptography (==36.0.2)", "pyopenssl (==22.0.0)"] +enterprise-cert = ["cryptography", "pyopenssl"] pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] requests = ["requests (>=2.20.0,<3.0.0.dev0)"] @@ -1675,17 +1736,17 @@ httplib2 = ">=0.19.0" [[package]] name = "googleapis-common-protos" -version = "1.63.0" +version = "1.65.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" files = [ - {file = "googleapis-common-protos-1.63.0.tar.gz", hash = "sha256:17ad01b11d5f1d0171c06d3ba5c04c54474e883b66b949722b4938ee2694ef4e"}, - {file = "googleapis_common_protos-1.63.0-py2.py3-none-any.whl", hash = "sha256:ae45f75702f7c08b541f750854a678bd8f534a1a6bace6afe975f1d0a82d6632"}, + {file = "googleapis_common_protos-1.65.0-py2.py3-none-any.whl", hash = "sha256:2972e6c496f435b92590fd54045060867f3fe9be2c82ab148fc8885035479a63"}, + {file = "googleapis_common_protos-1.65.0.tar.gz", hash = "sha256:334a29d07cddc3aa01dee4988f9afd9b2916ee2ff49d6b757155dc0d197852c0"}, ] [package.dependencies] -protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<5.0.0.dev0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" [package.extras] grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] @@ -1705,13 +1766,13 @@ test = ["pytest", "sphinx", "sphinx-autobuild", "twine", "wheel"] [[package]] name = "graphql-core" -version = "3.2.3" +version = "3.2.4" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." optional = false -python-versions = ">=3.6,<4" +python-versions = "<4,>=3.6" files = [ - {file = "graphql-core-3.2.3.tar.gz", hash = "sha256:06d2aad0ac723e35b1cb47885d3e5c45e956a53bc1b209a9fc5369007fe46676"}, - {file = "graphql_core-3.2.3-py3-none-any.whl", hash = "sha256:5766780452bd5ec8ba133f8bf287dc92713e3868ddd83aee4faab9fc3e303dc3"}, + {file = "graphql-core-3.2.4.tar.gz", hash = "sha256:acbe2e800980d0e39b4685dd058c2f4042660b89ebca38af83020fd872ff1264"}, + {file = "graphql_core-3.2.4-py3-none-any.whl", hash = "sha256:1604f2042edc5f3114f49cac9d77e25863be51b23a54a61a23245cf32f6476f0"}, ] [[package]] @@ -1788,13 +1849,13 @@ pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0 [[package]] name = "httpx" -version = "0.27.0" +version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, ] [package.dependencies] @@ -1810,6 +1871,7 @@ brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "hyperframe" @@ -1824,33 +1886,36 @@ files = [ [[package]] name = "idna" -version = "3.7" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "importlib-metadata" -version = "7.0.0" +version = "8.4.0" description = "Read metadata from Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "importlib_metadata-7.0.0-py3-none-any.whl", hash = "sha256:d97503976bb81f40a193d41ee6570868479c69d5068651eb039c40d850c59d67"}, - {file = "importlib_metadata-7.0.0.tar.gz", hash = "sha256:7fc841f8b8332803464e5dc1c63a2e59121f46ca186c0e2e182e80bf8c1319f7"}, + {file = "importlib_metadata-8.4.0-py3-none-any.whl", hash = "sha256:66f342cc6ac9818fc6ff340576acd24d65ba0b3efabb2b4ac08b598965a4a2f1"}, + {file = "importlib_metadata-8.4.0.tar.gz", hash = "sha256:9a547d3bc3608b025f93d403fdd1aae741c24fbb8314df4b155675742ce303c5"}, ] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" @@ -1932,13 +1997,13 @@ files = [ [[package]] name = "joserfc" -version = "0.9.0" +version = "1.0.0" description = "The ultimate Python library for JOSE RFCs, including JWS, JWE, JWK, JWA, JWT" optional = false python-versions = ">=3.8" files = [ - {file = "joserfc-0.9.0-py3-none-any.whl", hash = "sha256:4026bdbe2c196cd40574e916fa1e28874d99649412edaab0e373dec3077153fb"}, - {file = "joserfc-0.9.0.tar.gz", hash = "sha256:eebca7f587b1761ce43a98ffd5327f2b600b9aa5bb0a77b947687f503ad43bc0"}, + {file = "joserfc-1.0.0-py3-none-any.whl", hash = "sha256:1de2c3ac203db8fceb2e84c1e78ba357030b195c21af046a1411711927654a09"}, + {file = "joserfc-1.0.0.tar.gz", hash = "sha256:298a9820c76576f8ca63375d1851cc092f3f225508305c7a36c4632cec38f7bc"}, ] [package.dependencies] @@ -1948,31 +2013,21 @@ cryptography = "*" drafts = ["pycryptodome"] [[package]] -name = "jschema-to-python" -version = "1.2.3" -description = "Generate source code for Python classes from a JSON schema." +name = "jsondiff" +version = "2.2.1" +description = "Diff JSON and JSON-like structures in Python" optional = false -python-versions = ">= 2.7" +python-versions = ">=3.8" files = [ - {file = "jschema_to_python-1.2.3-py3-none-any.whl", hash = "sha256:8a703ca7604d42d74b2815eecf99a33359a8dccbb80806cce386d5e2dd992b05"}, - {file = "jschema_to_python-1.2.3.tar.gz", hash = "sha256:76ff14fe5d304708ccad1284e4b11f96a658949a31ee7faed9e0995279549b91"}, + {file = "jsondiff-2.2.1-py3-none-any.whl", hash = "sha256:b1f0f7e2421881848b1d556d541ac01a91680cfcc14f51a9b62cdf4da0e56722"}, + {file = "jsondiff-2.2.1.tar.gz", hash = "sha256:658d162c8a86ba86de26303cd86a7b37e1b2c1ec98b569a60e2ca6180545f7fe"}, ] [package.dependencies] -attrs = "*" -jsonpickle = "*" -pbr = "*" +pyyaml = "*" -[[package]] -name = "jsondiff" -version = "2.0.0" -description = "Diff JSON and JSON-like structures in Python" -optional = false -python-versions = "*" -files = [ - {file = "jsondiff-2.0.0-py3-none-any.whl", hash = "sha256:689841d66273fc88fc79f7d33f4c074774f4f214b6466e3aff0e5adaf889d1e0"}, - {file = "jsondiff-2.0.0.tar.gz", hash = "sha256:2795844ef075ec8a2b8d385c4d59f5ea48b08e7180fce3cb2787be0db00b1fb4"}, -] +[package.extras] +dev = ["build", "hypothesis", "pytest", "setuptools-scm"] [[package]] name = "jsonpatch" @@ -2003,30 +2058,14 @@ files = [ ply = "*" [[package]] -name = "jsonpickle" -version = "3.0.4" -description = "Serialize any Python object to JSON" +name = "jsonpointer" +version = "3.0.0" +description = "Identify specific nodes in a JSON document (RFC 6901)" optional = false python-versions = ">=3.7" files = [ - {file = "jsonpickle-3.0.4-py3-none-any.whl", hash = "sha256:04ae7567a14269579e3af66b76bda284587458d7e8a204951ca8f71a3309952e"}, - {file = "jsonpickle-3.0.4.tar.gz", hash = "sha256:a1b14c8d6221cd8f394f2a97e735ea1d7edc927fbd135b26f2f8700657c8c62b"}, -] - -[package.extras] -docs = ["furo", "rst.linker (>=1.9)", "sphinx"] -packaging = ["build", "twine"] -testing = ["bson", "ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-benchmark", "pytest-benchmark[histogram]", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-ruff (>=0.2.1)", "scikit-learn", "scipy", "scipy (>=1.9.3)", "simplejson", "sqlalchemy", "ujson"] - -[[package]] -name = "jsonpointer" -version = "2.4" -description = "Identify specific nodes in a JSON document (RFC 6901)" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" -files = [ - {file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"}, - {file = "jsonpointer-2.4.tar.gz", hash = "sha256:585cee82b70211fa9e6043b7bb89db6e1aa49524340dde8ad6b63206ea689d88"}, + {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, + {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, ] [[package]] @@ -2052,19 +2091,19 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "jsonschema-path" -version = "0.3.2" +version = "0.3.3" description = "JSONSchema Spec with object-oriented paths" optional = false -python-versions = ">=3.8.0,<4.0.0" +python-versions = "<4.0.0,>=3.8.0" files = [ - {file = "jsonschema_path-0.3.2-py3-none-any.whl", hash = "sha256:271aedfefcd161a0f467bdf23e1d9183691a61eaabf4b761046a914e369336c7"}, - {file = "jsonschema_path-0.3.2.tar.gz", hash = "sha256:4d0dababf341e36e9b91a5fb2a3e3fd300b0150e7fe88df4e55cc8253c5a3989"}, + {file = "jsonschema_path-0.3.3-py3-none-any.whl", hash = "sha256:203aff257f8038cd3c67be614fe6b2001043408cb1b4e36576bc4921e09d83c4"}, + {file = "jsonschema_path-0.3.3.tar.gz", hash = "sha256:f02e5481a4288ec062f8e68c808569e427d905bedfecb7f2e4c69ef77957c382"}, ] [package.dependencies] pathable = ">=0.4.1,<0.5.0" PyYAML = ">=5.1" -referencing = ">=0.28.0,<0.32.0" +referencing = ">=0.28.0,<0.36.0" requests = ">=2.31.0,<3.0.0" [[package]] @@ -2081,33 +2120,20 @@ files = [ [package.dependencies] referencing = ">=0.31.0" -[[package]] -name = "junit-xml" -version = "1.9" -description = "Creates JUnit XML test result documents that can be read by tools such as Jenkins" -optional = false -python-versions = "*" -files = [ - {file = "junit-xml-1.9.tar.gz", hash = "sha256:de16a051990d4e25a3982b2dd9e89d671067548718866416faec14d9de56db9f"}, - {file = "junit_xml-1.9-py2.py3-none-any.whl", hash = "sha256:ec5ca1a55aefdd76d28fcc0b135251d156c7106fa979686a4b48d62b761b4732"}, -] - -[package.dependencies] -six = "*" - [[package]] name = "kubernetes" -version = "30.1.0" +version = "31.0.0" description = "Kubernetes python client" optional = false python-versions = ">=3.6" files = [ - {file = "kubernetes-30.1.0-py2.py3-none-any.whl", hash = "sha256:e212e8b7579031dd2e512168b617373bc1e03888d41ac4e04039240a292d478d"}, - {file = "kubernetes-30.1.0.tar.gz", hash = "sha256:41e4c77af9f28e7a6c314e3bd06a8c6229ddd787cad684e0ab9f69b498e98ebc"}, + {file = "kubernetes-31.0.0-py2.py3-none-any.whl", hash = "sha256:bf141e2d380c8520eada8b351f4e319ffee9636328c137aa432bc486ca1200e1"}, + {file = "kubernetes-31.0.0.tar.gz", hash = "sha256:28945de906c8c259c1ebe62703b56a03b714049372196f854105afe4e6d014c0"}, ] [package.dependencies] certifi = ">=14.05.14" +durationpy = ">=0.7" google-auth = ">=1.0.1" oauthlib = ">=3.2.2" python-dateutil = ">=2.5.3" @@ -2169,13 +2195,13 @@ files = [ [[package]] name = "markdown" -version = "3.6" +version = "3.7" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"}, - {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"}, + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, ] [package.dependencies] @@ -2280,13 +2306,13 @@ files = [ [[package]] name = "marshmallow" -version = "3.21.1" +version = "3.22.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.8" files = [ - {file = "marshmallow-3.21.1-py3-none-any.whl", hash = "sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633"}, - {file = "marshmallow-3.21.1.tar.gz", hash = "sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3"}, + {file = "marshmallow-3.22.0-py3-none-any.whl", hash = "sha256:71a2dce49ef901c3f97ed296ae5051135fd3febd2bf43afe0ae9a82143a494d9"}, + {file = "marshmallow-3.22.0.tar.gz", hash = "sha256:4972f529104a220bb8637d595aa4c9762afbe7f7a77d82dc58c1615d70c5823e"}, ] [package.dependencies] @@ -2294,7 +2320,7 @@ packaging = ">=17.0" [package.extras] dev = ["marshmallow[tests]", "pre-commit (>=3.5,<4.0)", "tox"] -docs = ["alabaster (==0.7.16)", "autodocsumm (==0.2.12)", "sphinx (==7.2.6)", "sphinx-issues (==4.0.0)", "sphinx-version-warning (==1.1.2)"] +docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.13)", "sphinx (==8.0.2)", "sphinx-issues (==4.1.0)", "sphinx-version-warning (==1.1.2)"] tests = ["pytest", "pytz", "simplejson"] [[package]] @@ -2348,13 +2374,13 @@ std-uritemplate = ">=0.0.38" [[package]] name = "microsoft-kiota-authentication-azure" -version = "1.0.0" +version = "1.1.0" description = "Authentication provider for Kiota using Azure Identity" optional = false python-versions = "*" files = [ - {file = "microsoft_kiota_authentication_azure-1.0.0-py2.py3-none-any.whl", hash = "sha256:289fe002951ae661415a6d3fa7c422c096b739165acb32d786316988120a1b27"}, - {file = "microsoft_kiota_authentication_azure-1.0.0.tar.gz", hash = "sha256:752304f8d94b884cfec12583dd763ec0478805c7f80b29344e78c6d55a97bd01"}, + {file = "microsoft_kiota_authentication_azure-1.1.0-py2.py3-none-any.whl", hash = "sha256:22ecbf7e89120aca525cb3f89576617e6c6601b20b06ac720a8e66175698cf84"}, + {file = "microsoft_kiota_authentication_azure-1.1.0.tar.gz", hash = "sha256:8940084a3c1c25d1cde1d3b193ef7164dc6323e3b8200e2f695cbc937797bac6"}, ] [package.dependencies] @@ -2366,13 +2392,13 @@ opentelemetry-sdk = ">=1.20.0" [[package]] name = "microsoft-kiota-http" -version = "1.3.1" +version = "1.3.3" description = "Kiota http request adapter implementation for httpx library" optional = false python-versions = "*" files = [ - {file = "microsoft_kiota_http-1.3.1-py2.py3-none-any.whl", hash = "sha256:d62972c6ed4c785f9808a15479a7421abb38a9519b39e6933e5d05555b9fb427"}, - {file = "microsoft_kiota_http-1.3.1.tar.gz", hash = "sha256:09d85310379f88af0a0967925d1fcbe82f2520a9fe6fa1fd50e79af813bc451d"}, + {file = "microsoft_kiota_http-1.3.3-py2.py3-none-any.whl", hash = "sha256:21109a34140bf42e18855b7cf983939b891ae30739f21a9ce045c3a715f325fd"}, + {file = "microsoft_kiota_http-1.3.3.tar.gz", hash = "sha256:0b40f37c6c158c2e5b2dffa963a7fc342d368c1a64b8cca08631ba19d0ff94a9"}, ] [package.dependencies] @@ -2383,13 +2409,13 @@ opentelemetry-sdk = ">=1.20.0" [[package]] name = "microsoft-kiota-serialization-form" -version = "0.1.0" +version = "0.1.1" description = "Implementation of Kiota Serialization Interfaces for URI-Form encoded serialization" optional = false python-versions = "*" files = [ - {file = "microsoft_kiota_serialization_form-0.1.0-py2.py3-none-any.whl", hash = "sha256:5bc76fb2fc67d7c1f878f876d252ea814e4fc38df505099b9b86de52d974380a"}, - {file = "microsoft_kiota_serialization_form-0.1.0.tar.gz", hash = "sha256:663ece0cb1a41fe9ddfc9195aa3f15f219e14d2a1ee51e98c53ad8d795b2785d"}, + {file = "microsoft_kiota_serialization_form-0.1.1-py2.py3-none-any.whl", hash = "sha256:7a2da956edd3329ff74ec1a34eded910fd9cda57ce8c71f3797e359adc9993f7"}, + {file = "microsoft_kiota_serialization_form-0.1.1.tar.gz", hash = "sha256:f72dd50081250d1e49111682eccf620d3c2e335195d50c096b35ec5a5ef59a54"}, ] [package.dependencies] @@ -2398,13 +2424,13 @@ pendulum = ">=3.0.0" [[package]] name = "microsoft-kiota-serialization-json" -version = "1.3.0" +version = "1.3.2" description = "Implementation of Kiota Serialization interfaces for JSON" optional = false python-versions = "*" files = [ - {file = "microsoft_kiota_serialization_json-1.3.0-py2.py3-none-any.whl", hash = "sha256:fbf82835d8b77ef21b496aa711a512fe4494fa94dfe88f7fd014dffe33778e20"}, - {file = "microsoft_kiota_serialization_json-1.3.0.tar.gz", hash = "sha256:235b680e6eb646479ffb7b59d2a6f0216c4f7e1c2ff1219fd4d59e898fa6b124"}, + {file = "microsoft_kiota_serialization_json-1.3.2-py2.py3-none-any.whl", hash = "sha256:def0021ee30f0acb25e65994720414567da8303d2e769d8cc4351a5c7eedd4a0"}, + {file = "microsoft_kiota_serialization_json-1.3.2.tar.gz", hash = "sha256:627b93f48f6887c3ffba23c7c85445dd6e82ba693b0b7f1540e9de29f3801014"}, ] [package.dependencies] @@ -2442,44 +2468,61 @@ python-dateutil = ">=2.8.2" [[package]] name = "mkdocs" -version = "1.5.3" +version = "1.6.1" description = "Project documentation with Markdown." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, - {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, ] [package.dependencies] click = ">=7.0" colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" -importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} jinja2 = ">=2.11.1" -markdown = ">=3.2.1" +markdown = ">=3.3.6" markupsafe = ">=2.0.1" mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" packaging = ">=20.5" pathspec = ">=0.11.1" -platformdirs = ">=2.2.0" pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] -min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" [[package]] name = "mkdocs-git-revision-date-localized-plugin" -version = "1.2.8" +version = "1.2.9" description = "Mkdocs plugin that enables displaying the localized date of the last git modification of a markdown file." optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_git_revision_date_localized_plugin-1.2.8-py3-none-any.whl", hash = "sha256:c7ec3b1481ca23134269e84927bd8a5dc1aa359c0e515b832dbd5d25019b5748"}, - {file = "mkdocs_git_revision_date_localized_plugin-1.2.8.tar.gz", hash = "sha256:6e09c308bb27bcf36b211d17b74152ecc2837cdfc351237f70cffc723ef0fd99"}, + {file = "mkdocs_git_revision_date_localized_plugin-1.2.9-py3-none-any.whl", hash = "sha256:dea5c8067c23df30275702a1708885500fadf0abfb595b60e698bffc79c7a423"}, + {file = "mkdocs_git_revision_date_localized_plugin-1.2.9.tar.gz", hash = "sha256:df9a50873fba3a42ce9123885f8c53d589e90ef6c2443fe3280ef1e8d33c8f65"}, ] [package.dependencies] @@ -2495,13 +2538,13 @@ dev = ["click", "codecov", "mkdocs-gen-files", "mkdocs-git-authors-plugin", "mkd [[package]] name = "mkdocs-material" -version = "9.5.18" +version = "9.5.38" description = "Documentation that simply works" optional = false python-versions = ">=3.8" files = [ - {file = "mkdocs_material-9.5.18-py3-none-any.whl", hash = "sha256:1e0e27fc9fe239f9064318acf548771a4629d5fd5dfd45444fd80a953fe21eb4"}, - {file = "mkdocs_material-9.5.18.tar.gz", hash = "sha256:a43f470947053fa2405c33995f282d24992c752a50114f23f30da9d8d0c57e62"}, + {file = "mkdocs_material-9.5.38-py3-none-any.whl", hash = "sha256:d4779051d52ba9f1e7e344b34de95449c7c366c212b388e4a2db9a3db043c228"}, + {file = "mkdocs_material-9.5.38.tar.gz", hash = "sha256:1843c5171ad6b489550aeaf7358e5b7128cc03ddcf0fb4d91d19aa1e691a63b8"}, ] [package.dependencies] @@ -2509,7 +2552,7 @@ babel = ">=2.10,<3.0" colorama = ">=0.4,<1.0" jinja2 = ">=3.0,<4.0" markdown = ">=3.2,<4.0" -mkdocs = ">=1.5.3,<1.6.0" +mkdocs = ">=1.6,<2.0" mkdocs-material-extensions = ">=1.3,<2.0" paginate = ">=0.5,<1.0" pygments = ">=2.16,<3.0" @@ -2551,13 +2594,13 @@ test = ["pytest", "pytest-cov"] [[package]] name = "moto" -version = "5.0.14" +version = "5.0.15" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "moto-5.0.14-py2.py3-none-any.whl", hash = "sha256:c738ffe85d3844ef37b865951736c4faf2e0f3e4f05db87bdad97a6c01b88174"}, - {file = "moto-5.0.14.tar.gz", hash = "sha256:0f849243269fd03372426c302b18cb605302da32620d7f0266be6a40735b2acd"}, + {file = "moto-5.0.15-py2.py3-none-any.whl", hash = "sha256:fa1e92ffb55dbfb9fa92a2115a88c32481b75aa3fbd24075d1f29af2f9becffa"}, + {file = "moto-5.0.15.tar.gz", hash = "sha256:57aa8c2af417cc64a0ddfe63e5bcd1ada90f5079b73cdd1f74c4e9fb30a1a7e6"}, ] [package.dependencies] @@ -2627,51 +2670,47 @@ tests = ["pytest (>=4.6)"] [[package]] name = "msal" -version = "1.28.0" +version = "1.31.0" description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." optional = false python-versions = ">=3.7" files = [ - {file = "msal-1.28.0-py3-none-any.whl", hash = "sha256:3064f80221a21cd535ad8c3fafbb3a3582cd9c7e9af0bb789ae14f726a0ca99b"}, - {file = "msal-1.28.0.tar.gz", hash = "sha256:80bbabe34567cb734efd2ec1869b2d98195c927455369d8077b3c542088c5c9d"}, + {file = "msal-1.31.0-py3-none-any.whl", hash = "sha256:96bc37cff82ebe4b160d5fc0f1196f6ca8b50e274ecd0ec5bf69c438514086e7"}, + {file = "msal-1.31.0.tar.gz", hash = "sha256:2c4f189cf9cc8f00c80045f66d39b7c0f3ed45873fd3d1f2af9f22db2e12ff4b"}, ] [package.dependencies] -cryptography = ">=0.6,<45" +cryptography = ">=2.5,<46" PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} requests = ">=2.0.0,<3" [package.extras] -broker = ["pymsalruntime (>=0.13.2,<0.15)"] +broker = ["pymsalruntime (>=0.14,<0.18)", "pymsalruntime (>=0.17,<0.18)"] [[package]] name = "msal-extensions" -version = "1.1.0" +version = "1.2.0" description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." optional = false python-versions = ">=3.7" files = [ - {file = "msal-extensions-1.1.0.tar.gz", hash = "sha256:6ab357867062db7b253d0bd2df6d411c7891a0ee7308d54d1e4317c1d1c54252"}, - {file = "msal_extensions-1.1.0-py3-none-any.whl", hash = "sha256:01be9711b4c0b1a151450068eeb2c4f0997df3bba085ac299de3a66f585e382f"}, + {file = "msal_extensions-1.2.0-py3-none-any.whl", hash = "sha256:cf5ba83a2113fa6dc011a254a72f1c223c88d7dfad74cc30617c4679a417704d"}, + {file = "msal_extensions-1.2.0.tar.gz", hash = "sha256:6f41b320bfd2933d631a215c91ca0dd3e67d84bd1a2f50ce917d5874ec646bef"}, ] [package.dependencies] -msal = ">=0.4.1,<2.0.0" -packaging = "*" -portalocker = [ - {version = ">=1.0,<3", markers = "platform_system != \"Windows\""}, - {version = ">=1.6,<3", markers = "platform_system == \"Windows\""}, -] +msal = ">=1.29,<2" +portalocker = ">=1.4,<3" [[package]] name = "msgraph-core" -version = "1.0.0" +version = "1.1.4" description = "Core component of the Microsoft Graph Python SDK" optional = false python-versions = ">=3.8" files = [ - {file = "msgraph-core-1.0.0.tar.gz", hash = "sha256:f26bcbbb3cd149dd7f1613159e0c2ed862888d61bfd20ef0b08b9408eb670c9d"}, - {file = "msgraph_core-1.0.0-py3-none-any.whl", hash = "sha256:f3de5149e246833b4b03605590d0b4eacf58d9c5a10fd951c37e53f0a345afd5"}, + {file = "msgraph_core-1.1.4-py3-none-any.whl", hash = "sha256:6ae70abec758b3d3e6aec3a59cd946f71f9042c7cc1e84f5028fe52f512ad256"}, + {file = "msgraph_core-1.1.4.tar.gz", hash = "sha256:ce8e1df0450600949a64a4a64e8c4a67e20de8d29203f1fd63a69e369035d0b0"}, ] [package.dependencies] @@ -2685,13 +2724,13 @@ dev = ["bumpver", "isort", "mypy", "pylint", "pytest", "yapf"] [[package]] name = "msgraph-sdk" -version = "1.6.0" +version = "1.8.0" description = "The Microsoft Graph Python SDK" optional = false python-versions = ">=3.8" files = [ - {file = "msgraph_sdk-1.6.0-py3-none-any.whl", hash = "sha256:00dc0037a0a15a00bf04a9fd1340bb14b70dd0bbd5bc881bfde4ec9c0e2b9dbc"}, - {file = "msgraph_sdk-1.6.0.tar.gz", hash = "sha256:6724b186564b5a208e5069adff4a491a5401f36ed6928edcc4b6702b82b44c94"}, + {file = "msgraph_sdk-1.8.0-py3-none-any.whl", hash = "sha256:22a8e4a63f989865228f66a54501bef8105909c7156fe0a079ca9b5296339cc2"}, + {file = "msgraph_sdk-1.8.0.tar.gz", hash = "sha256:1ac84bd47ea288a84f46f6c6d0c89d164ee3453b917615632652344538098314"}, ] [package.dependencies] @@ -2731,114 +2770,122 @@ async = ["aiodns", "aiohttp (>=3.0)"] [[package]] name = "multidict" -version = "6.0.5" +version = "6.1.0" description = "multidict implementation" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1"}, + {file = "multidict-6.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429"}, + {file = "multidict-6.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160"}, + {file = "multidict-6.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7"}, + {file = "multidict-6.1.0-cp310-cp310-win32.whl", hash = "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0"}, + {file = "multidict-6.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156"}, + {file = "multidict-6.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351"}, + {file = "multidict-6.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3"}, + {file = "multidict-6.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753"}, + {file = "multidict-6.1.0-cp311-cp311-win32.whl", hash = "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80"}, + {file = "multidict-6.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436"}, + {file = "multidict-6.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925"}, + {file = "multidict-6.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6"}, + {file = "multidict-6.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3"}, + {file = "multidict-6.1.0-cp312-cp312-win32.whl", hash = "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133"}, + {file = "multidict-6.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f"}, + {file = "multidict-6.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44"}, + {file = "multidict-6.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4"}, + {file = "multidict-6.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6"}, + {file = "multidict-6.1.0-cp313-cp313-win32.whl", hash = "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81"}, + {file = "multidict-6.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a"}, + {file = "multidict-6.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d"}, + {file = "multidict-6.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492"}, + {file = "multidict-6.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd"}, + {file = "multidict-6.1.0-cp38-cp38-win32.whl", hash = "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167"}, + {file = "multidict-6.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1"}, + {file = "multidict-6.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255"}, + {file = "multidict-6.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972"}, + {file = "multidict-6.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43"}, + {file = "multidict-6.1.0-cp39-cp39-win32.whl", hash = "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada"}, + {file = "multidict-6.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a"}, + {file = "multidict-6.1.0-py3-none-any.whl", hash = "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506"}, + {file = "multidict-6.1.0.tar.gz", hash = "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} + [[package]] name = "multipart" -version = "0.2.4" -description = "Parser for multipart/form-data." +version = "1.0.0" +description = "Parser for multipart/form-data" optional = false -python-versions = "*" +python-versions = ">=3.5" files = [ - {file = "multipart-0.2.4-py3-none-any.whl", hash = "sha256:5aec990820b8a9e94f9c164fbeb58cf118cfbde2854865b67a9a730edd1fb9d1"}, - {file = "multipart-0.2.4.tar.gz", hash = "sha256:06ba205360bc7096fefe618e4f1e9b2cdb890b4f2157053a81f386912a2522cb"}, + {file = "multipart-1.0.0-py3-none-any.whl", hash = "sha256:85824b3d48b63fe0b6f438feb2b39f9753512e889426fb339e96b6095d4239c8"}, + {file = "multipart-1.0.0.tar.gz", hash = "sha256:6ac937fe07cd4e11cf4ca199f3d8f668e6a37e0f477c5ee032673d45be7f7957"}, ] +[package.extras] +dev = ["build", "pytest", "pytest-cov", "twine"] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -2984,103 +3031,125 @@ openapi-schema-validator = ">=0.6.0,<0.7.0" [[package]] name = "opentelemetry-api" -version = "1.24.0" +version = "1.27.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_api-1.24.0-py3-none-any.whl", hash = "sha256:0f2c363d98d10d1ce93330015ca7fd3a65f60be64e05e30f557c61de52c80ca2"}, - {file = "opentelemetry_api-1.24.0.tar.gz", hash = "sha256:42719f10ce7b5a9a73b10a4baf620574fb8ad495a9cbe5c18d76b75d8689c67e"}, + {file = "opentelemetry_api-1.27.0-py3-none-any.whl", hash = "sha256:953d5871815e7c30c81b56d910c707588000fff7a3ca1c73e6531911d53065e7"}, + {file = "opentelemetry_api-1.27.0.tar.gz", hash = "sha256:ed673583eaa5f81b5ce5e86ef7cdaf622f88ef65f0b9aab40b843dcae5bef342"}, ] [package.dependencies] deprecated = ">=1.2.6" -importlib-metadata = ">=6.0,<=7.0" +importlib-metadata = ">=6.0,<=8.4.0" [[package]] name = "opentelemetry-sdk" -version = "1.24.0" +version = "1.27.0" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_sdk-1.24.0-py3-none-any.whl", hash = "sha256:fa731e24efe832e98bcd90902085b359dcfef7d9c9c00eb5b9a18587dae3eb59"}, - {file = "opentelemetry_sdk-1.24.0.tar.gz", hash = "sha256:75bc0563affffa827700e0f4f4a68e1e257db0df13372344aebc6f8a64cde2e5"}, + {file = "opentelemetry_sdk-1.27.0-py3-none-any.whl", hash = "sha256:365f5e32f920faf0fd9e14fdfd92c086e317eaa5f860edba9cdc17a380d9197d"}, + {file = "opentelemetry_sdk-1.27.0.tar.gz", hash = "sha256:d525017dea0ccce9ba4e0245100ec46ecdc043f2d7b8315d56b19aff0904fa6f"}, ] [package.dependencies] -opentelemetry-api = "1.24.0" -opentelemetry-semantic-conventions = "0.45b0" +opentelemetry-api = "1.27.0" +opentelemetry-semantic-conventions = "0.48b0" typing-extensions = ">=3.7.4" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.45b0" +version = "0.48b0" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.8" files = [ - {file = "opentelemetry_semantic_conventions-0.45b0-py3-none-any.whl", hash = "sha256:a4a6fb9a7bacd9167c082aa4681009e9acdbfa28ffb2387af50c2fef3d30c864"}, - {file = "opentelemetry_semantic_conventions-0.45b0.tar.gz", hash = "sha256:7c84215a44ac846bc4b8e32d5e78935c5c43482e491812a0bb8aaf87e4d92118"}, + {file = "opentelemetry_semantic_conventions-0.48b0-py3-none-any.whl", hash = "sha256:a0de9f45c413a8669788a38569c7e0a11ce6ce97861a628cca785deecdc32a1f"}, + {file = "opentelemetry_semantic_conventions-0.48b0.tar.gz", hash = "sha256:12d74983783b6878162208be57c9effcb89dc88691c64992d70bb89dc00daa1a"}, ] +[package.dependencies] +deprecated = ">=1.2.6" +opentelemetry-api = "1.27.0" + [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] name = "paginate" -version = "0.5.6" +version = "0.5.7" description = "Divides large result sets into pages for easier browsing" optional = false python-versions = "*" files = [ - {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, ] +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + [[package]] name = "pandas" -version = "2.2.2" +version = "2.2.3" description = "Powerful data structures for data analysis, time series, and statistics" optional = false python-versions = ">=3.9" files = [ - {file = "pandas-2.2.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:90c6fca2acf139569e74e8781709dccb6fe25940488755716d1d354d6bc58bce"}, - {file = "pandas-2.2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7adfc142dac335d8c1e0dcbd37eb8617eac386596eb9e1a1b77791cf2498238"}, - {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4abfe0be0d7221be4f12552995e58723c7422c80a659da13ca382697de830c08"}, - {file = "pandas-2.2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8635c16bf3d99040fdf3ca3db669a7250ddf49c55dc4aa8fe0ae0fa8d6dcc1f0"}, - {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:40ae1dffb3967a52203105a077415a86044a2bea011b5f321c6aa64b379a3f51"}, - {file = "pandas-2.2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e5a0b00e1e56a842f922e7fae8ae4077aee4af0acb5ae3622bd4b4c30aedf99"}, - {file = "pandas-2.2.2-cp310-cp310-win_amd64.whl", hash = "sha256:ddf818e4e6c7c6f4f7c8a12709696d193976b591cc7dc50588d3d1a6b5dc8772"}, - {file = "pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288"}, - {file = "pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151"}, - {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b"}, - {file = "pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee"}, - {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db"}, - {file = "pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1"}, - {file = "pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef"}, - {file = "pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce"}, - {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad"}, - {file = "pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad"}, - {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76"}, - {file = "pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32"}, - {file = "pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23"}, - {file = "pandas-2.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0ca6377b8fca51815f382bd0b697a0814c8bda55115678cbc94c30aacbb6eff2"}, - {file = "pandas-2.2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9057e6aa78a584bc93a13f0a9bf7e753a5e9770a30b4d758b8d5f2a62a9433cd"}, - {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:001910ad31abc7bf06f49dcc903755d2f7f3a9186c0c040b827e522e9cef0863"}, - {file = "pandas-2.2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66b479b0bd07204e37583c191535505410daa8df638fd8e75ae1b383851fe921"}, - {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a77e9d1c386196879aa5eb712e77461aaee433e54c68cf253053a73b7e49c33a"}, - {file = "pandas-2.2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92fd6b027924a7e178ac202cfbe25e53368db90d56872d20ffae94b96c7acc57"}, - {file = "pandas-2.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:640cef9aa381b60e296db324337a554aeeb883ead99dc8f6c18e81a93942f5f4"}, - {file = "pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"}, + {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"}, + {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"}, + {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"}, + {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"}, + {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"}, + {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"}, + {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"}, + {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"}, + {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"}, + {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"}, + {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"}, + {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"}, + {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"}, + {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"}, + {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"}, + {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"}, + {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"}, + {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"}, + {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"}, + {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"}, + {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"}, + {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"}, + {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"}, + {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"}, ] [package.dependencies] @@ -3142,13 +3211,13 @@ files = [ [[package]] name = "pbr" -version = "6.0.0" +version = "6.1.0" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" files = [ - {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, - {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, + {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, + {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, ] [[package]] @@ -3252,29 +3321,29 @@ test = ["time-machine (>=2.6.0)"] [[package]] name = "platformdirs" -version = "4.2.1" +version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] [[package]] name = "plotly" -version = "5.21.0" +version = "5.24.1" description = "An open-source, interactive data visualization library for Python" optional = false python-versions = ">=3.8" files = [ - {file = "plotly-5.21.0-py3-none-any.whl", hash = "sha256:a33f41fd5922e45b2b253f795b200d14452eb625790bb72d0a72cf1328a6abbf"}, - {file = "plotly-5.21.0.tar.gz", hash = "sha256:69243f8c165d4be26c0df1c6f0b7b258e2dfeefe032763404ad7e7fb7d7c2073"}, + {file = "plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089"}, + {file = "plotly-5.24.1.tar.gz", hash = "sha256:dbc8ac8339d248a4bcc36e08a5659bacfe1b079390b8953533f4eb22169b4bae"}, ] [package.dependencies] @@ -3309,13 +3378,13 @@ files = [ [[package]] name = "portalocker" -version = "2.8.2" +version = "2.10.1" description = "Wraps the portalocker recipe for easy usage" optional = false python-versions = ">=3.8" files = [ - {file = "portalocker-2.8.2-py3-none-any.whl", hash = "sha256:cfb86acc09b9aa7c3b43594e19be1345b9d16af3feb08bf92f23d4dce513a28e"}, - {file = "portalocker-2.8.2.tar.gz", hash = "sha256:2b035aa7828e46c58e9b31390ee1f169b98e1066ab10b9a6a861fe7e25ee4f33"}, + {file = "portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf"}, + {file = "portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f"}, ] [package.dependencies] @@ -3328,39 +3397,39 @@ tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "p [[package]] name = "proto-plus" -version = "1.23.0" +version = "1.24.0" description = "Beautiful, Pythonic protocol buffers." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "proto-plus-1.23.0.tar.gz", hash = "sha256:89075171ef11988b3fa157f5dbd8b9cf09d65fffee97e29ce403cd8defba19d2"}, - {file = "proto_plus-1.23.0-py3-none-any.whl", hash = "sha256:a829c79e619e1cf632de091013a4173deed13a55f326ef84f05af6f50ff4c82c"}, + {file = "proto-plus-1.24.0.tar.gz", hash = "sha256:30b72a5ecafe4406b0d339db35b56c4059064e69227b8c3bda7462397f966445"}, + {file = "proto_plus-1.24.0-py3-none-any.whl", hash = "sha256:402576830425e5f6ce4c2a6702400ac79897dab0b4343821aa5188b0fab81a12"}, ] [package.dependencies] -protobuf = ">=3.19.0,<5.0.0dev" +protobuf = ">=3.19.0,<6.0.0dev" [package.extras] -testing = ["google-api-core[grpc] (>=1.31.5)"] +testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "4.25.3" +version = "5.28.2" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-4.25.3-cp310-abi3-win32.whl", hash = "sha256:d4198877797a83cbfe9bffa3803602bbe1625dc30d8a097365dbc762e5790faa"}, - {file = "protobuf-4.25.3-cp310-abi3-win_amd64.whl", hash = "sha256:209ba4cc916bab46f64e56b85b090607a676f66b473e6b762e6f1d9d591eb2e8"}, - {file = "protobuf-4.25.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:f1279ab38ecbfae7e456a108c5c0681e4956d5b1090027c1de0f934dfdb4b35c"}, - {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:e7cb0ae90dd83727f0c0718634ed56837bfeeee29a5f82a7514c03ee1364c019"}, - {file = "protobuf-4.25.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:7c8daa26095f82482307bc717364e7c13f4f1c99659be82890dcfc215194554d"}, - {file = "protobuf-4.25.3-cp38-cp38-win32.whl", hash = "sha256:f4f118245c4a087776e0a8408be33cf09f6c547442c00395fbfb116fac2f8ac2"}, - {file = "protobuf-4.25.3-cp38-cp38-win_amd64.whl", hash = "sha256:c053062984e61144385022e53678fbded7aea14ebb3e0305ae3592fb219ccfa4"}, - {file = "protobuf-4.25.3-cp39-cp39-win32.whl", hash = "sha256:19b270aeaa0099f16d3ca02628546b8baefe2955bbe23224aaf856134eccf1e4"}, - {file = "protobuf-4.25.3-cp39-cp39-win_amd64.whl", hash = "sha256:e3c97a1555fd6388f857770ff8b9703083de6bf1f9274a002a332d65fbb56c8c"}, - {file = "protobuf-4.25.3-py3-none-any.whl", hash = "sha256:f0700d54bcf45424477e46a9f0944155b46fb0639d69728739c0e47bab83f2b9"}, - {file = "protobuf-4.25.3.tar.gz", hash = "sha256:25b5d0b42fd000320bd7830b349e3b696435f3b329810427a6bcce6a5492cc5c"}, + {file = "protobuf-5.28.2-cp310-abi3-win32.whl", hash = "sha256:eeea10f3dc0ac7e6b4933d32db20662902b4ab81bf28df12218aa389e9c2102d"}, + {file = "protobuf-5.28.2-cp310-abi3-win_amd64.whl", hash = "sha256:2c69461a7fcc8e24be697624c09a839976d82ae75062b11a0972e41fd2cd9132"}, + {file = "protobuf-5.28.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8b9403fc70764b08d2f593ce44f1d2920c5077bf7d311fefec999f8c40f78b7"}, + {file = "protobuf-5.28.2-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:35cfcb15f213449af7ff6198d6eb5f739c37d7e4f1c09b5d0641babf2cc0c68f"}, + {file = "protobuf-5.28.2-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:5e8a95246d581eef20471b5d5ba010d55f66740942b95ba9b872d918c459452f"}, + {file = "protobuf-5.28.2-cp38-cp38-win32.whl", hash = "sha256:87317e9bcda04a32f2ee82089a204d3a2f0d3c8aeed16568c7daf4756e4f1fe0"}, + {file = "protobuf-5.28.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0ea0123dac3399a2eeb1a1443d82b7afc9ff40241433296769f7da42d142ec3"}, + {file = "protobuf-5.28.2-cp39-cp39-win32.whl", hash = "sha256:ca53faf29896c526863366a52a8f4d88e69cd04ec9571ed6082fa117fac3ab36"}, + {file = "protobuf-5.28.2-cp39-cp39-win_amd64.whl", hash = "sha256:8ddc60bf374785fb7cb12510b267f59067fa10087325b8e1855b898a0d81d276"}, + {file = "protobuf-5.28.2-py3-none-any.whl", hash = "sha256:52235802093bd8a2811abbe8bf0ab9c5f54cca0a751fdd3f6ac2a21438bffece"}, + {file = "protobuf-5.28.2.tar.gz", hash = "sha256:59379674ff119717404f7454647913787034f03fe7049cbef1d74a97bb4593f0"}, ] [[package]] @@ -3423,24 +3492,24 @@ dev = ["black (==22.6.0)", "flake8", "mypy", "pytest"] [[package]] name = "pyasn1" -version = "0.6.0" +version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" files = [ - {file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"}, - {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, + {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, + {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, ] [[package]] name = "pyasn1-modules" -version = "0.4.0" +version = "0.4.1" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" files = [ - {file = "pyasn1_modules-0.4.0-py3-none-any.whl", hash = "sha256:be04f15b66c206eed667e0bb5ab27e2b1855ea54a842e5037738099e8ca4ae0b"}, - {file = "pyasn1_modules-0.4.0.tar.gz", hash = "sha256:831dbcea1b177b28c9baddf4c6d1013c24c3accd14a1873fffaa6a2e905f17b6"}, + {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, + {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, ] [package.dependencies] @@ -3448,13 +3517,13 @@ pyasn1 = ">=0.4.6,<0.7.0" [[package]] name = "pycodestyle" -version = "2.12.0" +version = "2.12.1" description = "Python style guide checker" optional = false python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, - {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, + {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, + {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, ] [[package]] @@ -3533,28 +3602,27 @@ files = [ [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyjwt" -version = "2.8.0" +version = "2.9.0" description = "JSON Web Token implementation in Python" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, - {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, + {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"}, + {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"}, ] [package.dependencies] @@ -3562,23 +3630,23 @@ cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryp [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] [[package]] name = "pylint" -version = "3.2.7" +version = "3.3.1" description = "python code static checker" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" files = [ - {file = "pylint-3.2.7-py3-none-any.whl", hash = "sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b"}, - {file = "pylint-3.2.7.tar.gz", hash = "sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e"}, + {file = "pylint-3.3.1-py3-none-any.whl", hash = "sha256:2f846a466dd023513240bc140ad2dd73bfc080a5d85a710afdb728c420a5a2b9"}, + {file = "pylint-3.3.1.tar.gz", hash = "sha256:9f3dcc87b1203e612b78d91a896407787e708b3f189b5fa0b307712d49ff0c6e"}, ] [package.dependencies] -astroid = ">=3.2.4,<=3.3.0-dev0" +astroid = ">=3.3.4,<=3.4.0-dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, @@ -3598,13 +3666,13 @@ testutils = ["gitpython (>3)"] [[package]] name = "pymdown-extensions" -version = "10.8" +version = "10.11.1" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.8" files = [ - {file = "pymdown_extensions-10.8-py3-none-any.whl", hash = "sha256:3539003ff0d5e219ba979d2dc961d18fcad5ac259e66c764482e8347b4c0503c"}, - {file = "pymdown_extensions-10.8.tar.gz", hash = "sha256:91ca336caf414e1e5e0626feca86e145de9f85a3921a7bcbd32890b51738c428"}, + {file = "pymdown_extensions-10.11.1-py3-none-any.whl", hash = "sha256:a2b28f5786e041f19cb5bb30a1c2c853668a7099da8e3dd822a5ad05f2e855e3"}, + {file = "pymdown_extensions-10.11.1.tar.gz", hash = "sha256:a8836e955851542fa2625d04d59fdf97125ca001377478ed5618e04f9183a59a"}, ] [package.dependencies] @@ -3616,13 +3684,13 @@ extra = ["pygments (>=2.12)"] [[package]] name = "pyparsing" -version = "3.1.2" +version = "3.1.4" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, - {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, + {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, + {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, ] [package.extras] @@ -3670,21 +3738,21 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-env" -version = "1.1.4" +version = "1.1.5" description = "pytest plugin that allows you to add environment variables." optional = false python-versions = ">=3.8" files = [ - {file = "pytest_env-1.1.4-py3-none-any.whl", hash = "sha256:a4212056d4d440febef311a98fdca56c31256d58fb453d103cba4e8a532b721d"}, - {file = "pytest_env-1.1.4.tar.gz", hash = "sha256:86653658da8f11c6844975db955746c458a9c09f1e64957603161e2ff93f5133"}, + {file = "pytest_env-1.1.5-py3-none-any.whl", hash = "sha256:ce90cf8772878515c24b31cd97c7fa1f4481cd68d588419fd45f10ecaee6bc30"}, + {file = "pytest_env-1.1.5.tar.gz", hash = "sha256:91209840aa0e43385073ac464a554ad2947cc2fd663a9debf88d03b01e0cc1cf"}, ] [package.dependencies] -pytest = ">=8.3.2" +pytest = ">=8.3.3" tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [package.extras] -test = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "pytest-mock (>=3.14)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "pytest-mock (>=3.14)"] [[package]] name = "pytest-randomly" @@ -3737,13 +3805,13 @@ six = ">=1.5" [[package]] name = "pytz" -version = "2024.1" +version = "2024.2" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, + {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"}, + {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"}, ] [[package]] @@ -3771,62 +3839,64 @@ files = [ [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] @@ -3845,13 +3915,13 @@ pyyaml = "*" [[package]] name = "referencing" -version = "0.31.1" +version = "0.35.1" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" files = [ - {file = "referencing-0.31.1-py3-none-any.whl", hash = "sha256:c19c4d006f1757e3dd75c4f784d38f8698d87b649c54f9ace14e5e8c9667c01d"}, - {file = "referencing-0.31.1.tar.gz", hash = "sha256:81a1471c68c9d5e3831c30ad1dd9815c45b558e596653db751a2bfdd17b3b9ec"}, + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, ] [package.dependencies] @@ -3860,104 +3930,105 @@ rpds-py = ">=0.7.0" [[package]] name = "regex" -version = "2024.4.16" +version = "2024.9.11" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "regex-2024.4.16-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb83cc090eac63c006871fd24db5e30a1f282faa46328572661c0a24a2323a08"}, - {file = "regex-2024.4.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c91e1763696c0eb66340c4df98623c2d4e77d0746b8f8f2bee2c6883fd1fe18"}, - {file = "regex-2024.4.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10188fe732dec829c7acca7422cdd1bf57d853c7199d5a9e96bb4d40db239c73"}, - {file = "regex-2024.4.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:956b58d692f235cfbf5b4f3abd6d99bf102f161ccfe20d2fd0904f51c72c4c66"}, - {file = "regex-2024.4.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a70b51f55fd954d1f194271695821dd62054d949efd6368d8be64edd37f55c86"}, - {file = "regex-2024.4.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c02fcd2bf45162280613d2e4a1ca3ac558ff921ae4e308ecb307650d3a6ee51"}, - {file = "regex-2024.4.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ed75ea6892a56896d78f11006161eea52c45a14994794bcfa1654430984b22"}, - {file = "regex-2024.4.16-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd727ad276bb91928879f3aa6396c9a1d34e5e180dce40578421a691eeb77f47"}, - {file = "regex-2024.4.16-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7cbc5d9e8a1781e7be17da67b92580d6ce4dcef5819c1b1b89f49d9678cc278c"}, - {file = "regex-2024.4.16-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:78fddb22b9ef810b63ef341c9fcf6455232d97cfe03938cbc29e2672c436670e"}, - {file = "regex-2024.4.16-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:445ca8d3c5a01309633a0c9db57150312a181146315693273e35d936472df912"}, - {file = "regex-2024.4.16-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:95399831a206211d6bc40224af1c635cb8790ddd5c7493e0bd03b85711076a53"}, - {file = "regex-2024.4.16-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7731728b6568fc286d86745f27f07266de49603a6fdc4d19c87e8c247be452af"}, - {file = "regex-2024.4.16-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4facc913e10bdba42ec0aee76d029aedda628161a7ce4116b16680a0413f658a"}, - {file = "regex-2024.4.16-cp310-cp310-win32.whl", hash = "sha256:911742856ce98d879acbea33fcc03c1d8dc1106234c5e7d068932c945db209c0"}, - {file = "regex-2024.4.16-cp310-cp310-win_amd64.whl", hash = "sha256:e0a2df336d1135a0b3a67f3bbf78a75f69562c1199ed9935372b82215cddd6e2"}, - {file = "regex-2024.4.16-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1210365faba7c2150451eb78ec5687871c796b0f1fa701bfd2a4a25420482d26"}, - {file = "regex-2024.4.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ab40412f8cd6f615bfedea40c8bf0407d41bf83b96f6fc9ff34976d6b7037fd"}, - {file = "regex-2024.4.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd80d1280d473500d8086d104962a82d77bfbf2b118053824b7be28cd5a79ea5"}, - {file = "regex-2024.4.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb966fdd9217e53abf824f437a5a2d643a38d4fd5fd0ca711b9da683d452969"}, - {file = "regex-2024.4.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20b7a68444f536365af42a75ccecb7ab41a896a04acf58432db9e206f4e525d6"}, - {file = "regex-2024.4.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b74586dd0b039c62416034f811d7ee62810174bb70dffcca6439f5236249eb09"}, - {file = "regex-2024.4.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c8290b44d8b0af4e77048646c10c6e3aa583c1ca67f3b5ffb6e06cf0c6f0f89"}, - {file = "regex-2024.4.16-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2d80a6749724b37853ece57988b39c4e79d2b5fe2869a86e8aeae3bbeef9eb0"}, - {file = "regex-2024.4.16-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3a1018e97aeb24e4f939afcd88211ace472ba566efc5bdf53fd8fd7f41fa7170"}, - {file = "regex-2024.4.16-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8d015604ee6204e76569d2f44e5a210728fa917115bef0d102f4107e622b08d5"}, - {file = "regex-2024.4.16-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:3d5ac5234fb5053850d79dd8eb1015cb0d7d9ed951fa37aa9e6249a19aa4f336"}, - {file = "regex-2024.4.16-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:0a38d151e2cdd66d16dab550c22f9521ba79761423b87c01dae0a6e9add79c0d"}, - {file = "regex-2024.4.16-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:159dc4e59a159cb8e4e8f8961eb1fa5d58f93cb1acd1701d8aff38d45e1a84a6"}, - {file = "regex-2024.4.16-cp311-cp311-win32.whl", hash = "sha256:ba2336d6548dee3117520545cfe44dc28a250aa091f8281d28804aa8d707d93d"}, - {file = "regex-2024.4.16-cp311-cp311-win_amd64.whl", hash = "sha256:8f83b6fd3dc3ba94d2b22717f9c8b8512354fd95221ac661784df2769ea9bba9"}, - {file = "regex-2024.4.16-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:80b696e8972b81edf0af2a259e1b2a4a661f818fae22e5fa4fa1a995fb4a40fd"}, - {file = "regex-2024.4.16-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d61ae114d2a2311f61d90c2ef1358518e8f05eafda76eaf9c772a077e0b465ec"}, - {file = "regex-2024.4.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ba6745440b9a27336443b0c285d705ce73adb9ec90e2f2004c64d95ab5a7598"}, - {file = "regex-2024.4.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295004b2dd37b0835ea5c14a33e00e8cfa3c4add4d587b77287825f3418d310"}, - {file = "regex-2024.4.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4aba818dcc7263852aabb172ec27b71d2abca02a593b95fa79351b2774eb1d2b"}, - {file = "regex-2024.4.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0800631e565c47520aaa04ae38b96abc5196fe8b4aa9bd864445bd2b5848a7a"}, - {file = "regex-2024.4.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08dea89f859c3df48a440dbdcd7b7155bc675f2fa2ec8c521d02dc69e877db70"}, - {file = "regex-2024.4.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eeaa0b5328b785abc344acc6241cffde50dc394a0644a968add75fcefe15b9d4"}, - {file = "regex-2024.4.16-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4e819a806420bc010489f4e741b3036071aba209f2e0989d4750b08b12a9343f"}, - {file = "regex-2024.4.16-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:c2d0e7cbb6341e830adcbfa2479fdeebbfbb328f11edd6b5675674e7a1e37730"}, - {file = "regex-2024.4.16-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:91797b98f5e34b6a49f54be33f72e2fb658018ae532be2f79f7c63b4ae225145"}, - {file = "regex-2024.4.16-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:d2da13568eff02b30fd54fccd1e042a70fe920d816616fda4bf54ec705668d81"}, - {file = "regex-2024.4.16-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:370c68dc5570b394cbaadff50e64d705f64debed30573e5c313c360689b6aadc"}, - {file = "regex-2024.4.16-cp312-cp312-win32.whl", hash = "sha256:904c883cf10a975b02ab3478bce652f0f5346a2c28d0a8521d97bb23c323cc8b"}, - {file = "regex-2024.4.16-cp312-cp312-win_amd64.whl", hash = "sha256:785c071c982dce54d44ea0b79cd6dfafddeccdd98cfa5f7b86ef69b381b457d9"}, - {file = "regex-2024.4.16-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e2f142b45c6fed48166faeb4303b4b58c9fcd827da63f4cf0a123c3480ae11fb"}, - {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87ab229332ceb127a165612d839ab87795972102cb9830e5f12b8c9a5c1b508"}, - {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81500ed5af2090b4a9157a59dbc89873a25c33db1bb9a8cf123837dcc9765047"}, - {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b340cccad138ecb363324aa26893963dcabb02bb25e440ebdf42e30963f1a4e0"}, - {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c72608e70f053643437bd2be0608f7f1c46d4022e4104d76826f0839199347a"}, - {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a01fe2305e6232ef3e8f40bfc0f0f3a04def9aab514910fa4203bafbc0bb4682"}, - {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:03576e3a423d19dda13e55598f0fd507b5d660d42c51b02df4e0d97824fdcae3"}, - {file = "regex-2024.4.16-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:549c3584993772e25f02d0656ac48abdda73169fe347263948cf2b1cead622f3"}, - {file = "regex-2024.4.16-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:34422d5a69a60b7e9a07a690094e824b66f5ddc662a5fc600d65b7c174a05f04"}, - {file = "regex-2024.4.16-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:5f580c651a72b75c39e311343fe6875d6f58cf51c471a97f15a938d9fe4e0d37"}, - {file = "regex-2024.4.16-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3399dd8a7495bbb2bacd59b84840eef9057826c664472e86c91d675d007137f5"}, - {file = "regex-2024.4.16-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d1f86f3f4e2388aa3310b50694ac44daefbd1681def26b4519bd050a398dc5a"}, - {file = "regex-2024.4.16-cp37-cp37m-win32.whl", hash = "sha256:dd5acc0a7d38fdc7a3a6fd3ad14c880819008ecb3379626e56b163165162cc46"}, - {file = "regex-2024.4.16-cp37-cp37m-win_amd64.whl", hash = "sha256:ba8122e3bb94ecda29a8de4cf889f600171424ea586847aa92c334772d200331"}, - {file = "regex-2024.4.16-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:743deffdf3b3481da32e8a96887e2aa945ec6685af1cfe2bcc292638c9ba2f48"}, - {file = "regex-2024.4.16-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7571f19f4a3fd00af9341c7801d1ad1967fc9c3f5e62402683047e7166b9f2b4"}, - {file = "regex-2024.4.16-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:df79012ebf6f4efb8d307b1328226aef24ca446b3ff8d0e30202d7ebcb977a8c"}, - {file = "regex-2024.4.16-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e757d475953269fbf4b441207bb7dbdd1c43180711b6208e129b637792ac0b93"}, - {file = "regex-2024.4.16-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4313ab9bf6a81206c8ac28fdfcddc0435299dc88cad12cc6305fd0e78b81f9e4"}, - {file = "regex-2024.4.16-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d83c2bc678453646f1a18f8db1e927a2d3f4935031b9ad8a76e56760461105dd"}, - {file = "regex-2024.4.16-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9df1bfef97db938469ef0a7354b2d591a2d438bc497b2c489471bec0e6baf7c4"}, - {file = "regex-2024.4.16-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62120ed0de69b3649cc68e2965376048793f466c5a6c4370fb27c16c1beac22d"}, - {file = "regex-2024.4.16-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c2ef6f7990b6e8758fe48ad08f7e2f66c8f11dc66e24093304b87cae9037bb4a"}, - {file = "regex-2024.4.16-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8fc6976a3395fe4d1fbeb984adaa8ec652a1e12f36b56ec8c236e5117b585427"}, - {file = "regex-2024.4.16-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:03e68f44340528111067cecf12721c3df4811c67268b897fbe695c95f860ac42"}, - {file = "regex-2024.4.16-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ec7e0043b91115f427998febaa2beb82c82df708168b35ece3accb610b91fac1"}, - {file = "regex-2024.4.16-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c21fc21a4c7480479d12fd8e679b699f744f76bb05f53a1d14182b31f55aac76"}, - {file = "regex-2024.4.16-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:12f6a3f2f58bb7344751919a1876ee1b976fe08b9ffccb4bbea66f26af6017b9"}, - {file = "regex-2024.4.16-cp38-cp38-win32.whl", hash = "sha256:479595a4fbe9ed8f8f72c59717e8cf222da2e4c07b6ae5b65411e6302af9708e"}, - {file = "regex-2024.4.16-cp38-cp38-win_amd64.whl", hash = "sha256:0534b034fba6101611968fae8e856c1698da97ce2efb5c2b895fc8b9e23a5834"}, - {file = "regex-2024.4.16-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7ccdd1c4a3472a7533b0a7aa9ee34c9a2bef859ba86deec07aff2ad7e0c3b94"}, - {file = "regex-2024.4.16-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f2f017c5be19984fbbf55f8af6caba25e62c71293213f044da3ada7091a4455"}, - {file = "regex-2024.4.16-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:803b8905b52de78b173d3c1e83df0efb929621e7b7c5766c0843704d5332682f"}, - {file = "regex-2024.4.16-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:684008ec44ad275832a5a152f6e764bbe1914bea10968017b6feaecdad5736e0"}, - {file = "regex-2024.4.16-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65436dce9fdc0aeeb0a0effe0839cb3d6a05f45aa45a4d9f9c60989beca78b9c"}, - {file = "regex-2024.4.16-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea355eb43b11764cf799dda62c658c4d2fdb16af41f59bb1ccfec517b60bcb07"}, - {file = "regex-2024.4.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c1165f3809ce7774f05cb74e5408cd3aa93ee8573ae959a97a53db3ca3180d"}, - {file = "regex-2024.4.16-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cccc79a9be9b64c881f18305a7c715ba199e471a3973faeb7ba84172abb3f317"}, - {file = "regex-2024.4.16-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00169caa125f35d1bca6045d65a662af0202704489fada95346cfa092ec23f39"}, - {file = "regex-2024.4.16-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6cc38067209354e16c5609b66285af17a2863a47585bcf75285cab33d4c3b8df"}, - {file = "regex-2024.4.16-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:23cff1b267038501b179ccbbd74a821ac4a7192a1852d1d558e562b507d46013"}, - {file = "regex-2024.4.16-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d320b3bf82a39f248769fc7f188e00f93526cc0fe739cfa197868633d44701"}, - {file = "regex-2024.4.16-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:89ec7f2c08937421bbbb8b48c54096fa4f88347946d4747021ad85f1b3021b3c"}, - {file = "regex-2024.4.16-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4918fd5f8b43aa7ec031e0fef1ee02deb80b6afd49c85f0790be1dc4ce34cb50"}, - {file = "regex-2024.4.16-cp39-cp39-win32.whl", hash = "sha256:684e52023aec43bdf0250e843e1fdd6febbe831bd9d52da72333fa201aaa2335"}, - {file = "regex-2024.4.16-cp39-cp39-win_amd64.whl", hash = "sha256:e697e1c0238133589e00c244a8b676bc2cfc3ab4961318d902040d099fec7483"}, - {file = "regex-2024.4.16.tar.gz", hash = "sha256:fa454d26f2e87ad661c4f0c5a5fe4cf6aab1e307d1b94f16ffdfcb089ba685c0"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1494fa8725c285a81d01dc8c06b55287a1ee5e0e382d8413adc0a9197aac6408"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e12c481ad92d129c78f13a2a3662317e46ee7ef96c94fd332e1c29131875b7d"}, + {file = "regex-2024.9.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:16e13a7929791ac1216afde26f712802e3df7bf0360b32e4914dca3ab8baeea5"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46989629904bad940bbec2106528140a218b4a36bb3042d8406980be1941429c"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a906ed5e47a0ce5f04b2c981af1c9acf9e8696066900bf03b9d7879a6f679fc8"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9a091b0550b3b0207784a7d6d0f1a00d1d1c8a11699c1a4d93db3fbefc3ad35"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ddcd9a179c0a6fa8add279a4444015acddcd7f232a49071ae57fa6e278f1f71"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b41e1adc61fa347662b09398e31ad446afadff932a24807d3ceb955ed865cc8"}, + {file = "regex-2024.9.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ced479f601cd2f8ca1fd7b23925a7e0ad512a56d6e9476f79b8f381d9d37090a"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:635a1d96665f84b292e401c3d62775851aedc31d4f8784117b3c68c4fcd4118d"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c0256beda696edcf7d97ef16b2a33a8e5a875affd6fa6567b54f7c577b30a137"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3ce4f1185db3fbde8ed8aa223fc9620f276c58de8b0d4f8cc86fd1360829edb6"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:09d77559e80dcc9d24570da3745ab859a9cf91953062e4ab126ba9d5993688ca"}, + {file = "regex-2024.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a22ccefd4db3f12b526eccb129390942fe874a3a9fdbdd24cf55773a1faab1a"}, + {file = "regex-2024.9.11-cp310-cp310-win32.whl", hash = "sha256:f745ec09bc1b0bd15cfc73df6fa4f726dcc26bb16c23a03f9e3367d357eeedd0"}, + {file = "regex-2024.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:01c2acb51f8a7d6494c8c5eafe3d8e06d76563d8a8a4643b37e9b2dd8a2ff623"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2cce2449e5927a0bf084d346da6cd5eb016b2beca10d0013ab50e3c226ffc0df"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b37fa423beefa44919e009745ccbf353d8c981516e807995b2bd11c2c77d268"}, + {file = "regex-2024.9.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:64ce2799bd75039b480cc0360907c4fb2f50022f030bf9e7a8705b636e408fad"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4cc92bb6db56ab0c1cbd17294e14f5e9224f0cc6521167ef388332604e92679"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d05ac6fa06959c4172eccd99a222e1fbf17b5670c4d596cb1e5cde99600674c4"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040562757795eeea356394a7fb13076ad4f99d3c62ab0f8bdfb21f99a1f85664"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6113c008a7780792efc80f9dfe10ba0cd043cbf8dc9a76ef757850f51b4edc50"}, + {file = "regex-2024.9.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e5fb5f77c8745a60105403a774fe2c1759b71d3e7b4ca237a5e67ad066c7199"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54d9ff35d4515debf14bc27f1e3b38bfc453eff3220f5bce159642fa762fe5d4"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df5cbb1fbc74a8305b6065d4ade43b993be03dbe0f8b30032cced0d7740994bd"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7fb89ee5d106e4a7a51bce305ac4efb981536301895f7bdcf93ec92ae0d91c7f"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a738b937d512b30bf75995c0159c0ddf9eec0775c9d72ac0202076c72f24aa96"}, + {file = "regex-2024.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e28f9faeb14b6f23ac55bfbbfd3643f5c7c18ede093977f1df249f73fd22c7b1"}, + {file = "regex-2024.9.11-cp311-cp311-win32.whl", hash = "sha256:18e707ce6c92d7282dfce370cd205098384b8ee21544e7cb29b8aab955b66fa9"}, + {file = "regex-2024.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:313ea15e5ff2a8cbbad96ccef6be638393041b0a7863183c2d31e0c6116688cf"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b0d0a6c64fcc4ef9c69bd5b3b3626cc3776520a1637d8abaa62b9edc147a58f7"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:49b0e06786ea663f933f3710a51e9385ce0cba0ea56b67107fd841a55d56a231"}, + {file = "regex-2024.9.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5b513b6997a0b2f10e4fd3a1313568e373926e8c252bd76c960f96fd039cd28d"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee439691d8c23e76f9802c42a95cfeebf9d47cf4ffd06f18489122dbb0a7ad64"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8f877c89719d759e52783f7fe6e1c67121076b87b40542966c02de5503ace42"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23b30c62d0f16827f2ae9f2bb87619bc4fba2044911e2e6c2eb1af0161cdb766"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85ab7824093d8f10d44330fe1e6493f756f252d145323dd17ab6b48733ff6c0a"}, + {file = "regex-2024.9.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8dee5b4810a89447151999428fe096977346cf2f29f4d5e29609d2e19e0199c9"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98eeee2f2e63edae2181c886d7911ce502e1292794f4c5ee71e60e23e8d26b5d"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:57fdd2e0b2694ce6fc2e5ccf189789c3e2962916fb38779d3e3521ff8fe7a822"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d552c78411f60b1fdaafd117a1fca2f02e562e309223b9d44b7de8be451ec5e0"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a0b2b80321c2ed3fcf0385ec9e51a12253c50f146fddb2abbb10f033fe3d049a"}, + {file = "regex-2024.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:18406efb2f5a0e57e3a5881cd9354c1512d3bb4f5c45d96d110a66114d84d23a"}, + {file = "regex-2024.9.11-cp312-cp312-win32.whl", hash = "sha256:e464b467f1588e2c42d26814231edecbcfe77f5ac414d92cbf4e7b55b2c2a776"}, + {file = "regex-2024.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:9e8719792ca63c6b8340380352c24dcb8cd7ec49dae36e963742a275dfae6009"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c157bb447303070f256e084668b702073db99bbb61d44f85d811025fcf38f784"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4db21ece84dfeefc5d8a3863f101995de646c6cb0536952c321a2650aa202c36"}, + {file = "regex-2024.9.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:220e92a30b426daf23bb67a7962900ed4613589bab80382be09b48896d211e92"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb1ae19e64c14c7ec1995f40bd932448713d3c73509e82d8cd7744dc00e29e86"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f47cd43a5bfa48f86925fe26fbdd0a488ff15b62468abb5d2a1e092a4fb10e85"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9d4a76b96f398697fe01117093613166e6aa8195d63f1b4ec3f21ab637632963"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ea51dcc0835eea2ea31d66456210a4e01a076d820e9039b04ae8d17ac11dee6"}, + {file = "regex-2024.9.11-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7aaa315101c6567a9a45d2839322c51c8d6e81f67683d529512f5bcfb99c802"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c57d08ad67aba97af57a7263c2d9006d5c404d721c5f7542f077f109ec2a4a29"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8404bf61298bb6f8224bb9176c1424548ee1181130818fcd2cbffddc768bed8"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dd4490a33eb909ef5078ab20f5f000087afa2a4daa27b4c072ccb3cb3050ad84"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:eee9130eaad130649fd73e5cd92f60e55708952260ede70da64de420cdcad554"}, + {file = "regex-2024.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a2644a93da36c784e546de579ec1806bfd2763ef47babc1b03d765fe560c9f8"}, + {file = "regex-2024.9.11-cp313-cp313-win32.whl", hash = "sha256:e997fd30430c57138adc06bba4c7c2968fb13d101e57dd5bb9355bf8ce3fa7e8"}, + {file = "regex-2024.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:042c55879cfeb21a8adacc84ea347721d3d83a159da6acdf1116859e2427c43f"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:35f4a6f96aa6cb3f2f7247027b07b15a374f0d5b912c0001418d1d55024d5cb4"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:55b96e7ce3a69a8449a66984c268062fbaa0d8ae437b285428e12797baefce7e"}, + {file = "regex-2024.9.11-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb130fccd1a37ed894824b8c046321540263013da72745d755f2d35114b81a60"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:323c1f04be6b2968944d730e5c2091c8c89767903ecaa135203eec4565ed2b2b"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be1c8ed48c4c4065ecb19d882a0ce1afe0745dfad8ce48c49586b90a55f02366"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5b029322e6e7b94fff16cd120ab35a253236a5f99a79fb04fda7ae71ca20ae8"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6fff13ef6b5f29221d6904aa816c34701462956aa72a77f1f151a8ec4f56aeb"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:587d4af3979376652010e400accc30404e6c16b7df574048ab1f581af82065e4"}, + {file = "regex-2024.9.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:079400a8269544b955ffa9e31f186f01d96829110a3bf79dc338e9910f794fca"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f9268774428ec173654985ce55fc6caf4c6d11ade0f6f914d48ef4719eb05ebb"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:23f9985c8784e544d53fc2930fc1ac1a7319f5d5332d228437acc9f418f2f168"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2941333154baff9838e88aa71c1d84f4438189ecc6021a12c7573728b5838e"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e93f1c331ca8e86fe877a48ad64e77882c0c4da0097f2212873a69bbfea95d0c"}, + {file = "regex-2024.9.11-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:846bc79ee753acf93aef4184c040d709940c9d001029ceb7b7a52747b80ed2dd"}, + {file = "regex-2024.9.11-cp38-cp38-win32.whl", hash = "sha256:c94bb0a9f1db10a1d16c00880bdebd5f9faf267273b8f5bd1878126e0fbde771"}, + {file = "regex-2024.9.11-cp38-cp38-win_amd64.whl", hash = "sha256:2b08fce89fbd45664d3df6ad93e554b6c16933ffa9d55cb7e01182baaf971508"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:07f45f287469039ffc2c53caf6803cd506eb5f5f637f1d4acb37a738f71dd066"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4838e24ee015101d9f901988001038f7f0d90dc0c3b115541a1365fb439add62"}, + {file = "regex-2024.9.11-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6edd623bae6a737f10ce853ea076f56f507fd7726bee96a41ee3d68d347e4d16"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c69ada171c2d0e97a4b5aa78fbb835e0ffbb6b13fc5da968c09811346564f0d3"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02087ea0a03b4af1ed6ebab2c54d7118127fee8d71b26398e8e4b05b78963199"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69dee6a020693d12a3cf892aba4808fe168d2a4cef368eb9bf74f5398bfd4ee8"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:297f54910247508e6e5cae669f2bc308985c60540a4edd1c77203ef19bfa63ca"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecea58b43a67b1b79805f1a0255730edaf5191ecef84dbc4cc85eb30bc8b63b9"}, + {file = "regex-2024.9.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:eab4bb380f15e189d1313195b062a6aa908f5bd687a0ceccd47c8211e9cf0d4a"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0cbff728659ce4bbf4c30b2a1be040faafaa9eca6ecde40aaff86f7889f4ab39"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:54c4a097b8bc5bb0dfc83ae498061d53ad7b5762e00f4adaa23bee22b012e6ba"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:73d6d2f64f4d894c96626a75578b0bf7d9e56dcda8c3d037a2118fdfe9b1c664"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:e53b5fbab5d675aec9f0c501274c467c0f9a5d23696cfc94247e1fb56501ed89"}, + {file = "regex-2024.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ffbcf9221e04502fc35e54d1ce9567541979c3fdfb93d2c554f0ca583a19b35"}, + {file = "regex-2024.9.11-cp39-cp39-win32.whl", hash = "sha256:e4c22e1ac1f1ec1e09f72e6c44d8f2244173db7eb9629cc3a346a8d7ccc31142"}, + {file = "regex-2024.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:faa3c142464efec496967359ca99696c896c591c56c53506bac1ad465f66e919"}, + {file = "regex-2024.9.11.tar.gz", hash = "sha256:6c188c307e8433bcb63dc1915022deb553b4203a70722fc542c363bf120a01fd"}, ] [[package]] @@ -3983,13 +4054,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-file" -version = "2.0.0" +version = "2.1.0" description = "File transport adapter for Requests" optional = false python-versions = "*" files = [ - {file = "requests-file-2.0.0.tar.gz", hash = "sha256:20c5931629c558fda566cacc10cfe2cd502433e628f568c34c80d96a0cc95972"}, - {file = "requests_file-2.0.0-py2.py3-none-any.whl", hash = "sha256:3e493d390adb44aa102ebea827a48717336d5268968c370eaf19abaf5cae13bf"}, + {file = "requests_file-2.1.0-py2.py3-none-any.whl", hash = "sha256:cf270de5a4c5874e84599fc5778303d496c10ae5e870bfa378818f35d21bda5c"}, + {file = "requests_file-2.1.0.tar.gz", hash = "sha256:0f549a3f3b0699415ac04d167e9cb39bccfb730cb832b4d20be3d9867356e658"}, ] [package.dependencies] @@ -4015,13 +4086,13 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "responses" -version = "0.25.0" +version = "0.25.3" description = "A utility library for mocking out the `requests` Python library." optional = false python-versions = ">=3.8" files = [ - {file = "responses-0.25.0-py3-none-any.whl", hash = "sha256:2f0b9c2b6437db4b528619a77e5d565e4ec2a9532162ac1a131a83529db7be1a"}, - {file = "responses-0.25.0.tar.gz", hash = "sha256:01ae6a02b4f34e39bffceb0fc6786b67a25eae919c6368d05eabc8d9576c2a66"}, + {file = "responses-0.25.3-py3-none-any.whl", hash = "sha256:521efcbc82081ab8daa588e08f7e8a64ce79b91c39f6e62199b19159bea7dbcb"}, + {file = "responses-0.25.3.tar.gz", hash = "sha256:617b9247abd9ae28313d57a75880422d55ec63c29d33d629697590a034358dba"}, ] [package.dependencies] @@ -4062,13 +4133,13 @@ six = "*" [[package]] name = "rich" -version = "13.7.1" +version = "13.8.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, + {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, ] [package.dependencies] @@ -4080,110 +4151,114 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.18.0" +version = "0.20.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.18.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e"}, - {file = "rpds_py-0.18.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1"}, - {file = "rpds_py-0.18.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e"}, - {file = "rpds_py-0.18.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88"}, - {file = "rpds_py-0.18.0-cp310-none-win32.whl", hash = "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337"}, - {file = "rpds_py-0.18.0-cp310-none-win_amd64.whl", hash = "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66"}, - {file = "rpds_py-0.18.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4"}, - {file = "rpds_py-0.18.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5"}, - {file = "rpds_py-0.18.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b"}, - {file = "rpds_py-0.18.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836"}, - {file = "rpds_py-0.18.0-cp311-none-win32.whl", hash = "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1"}, - {file = "rpds_py-0.18.0-cp311-none-win_amd64.whl", hash = "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa"}, - {file = "rpds_py-0.18.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0"}, - {file = "rpds_py-0.18.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3"}, - {file = "rpds_py-0.18.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f"}, - {file = "rpds_py-0.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7"}, - {file = "rpds_py-0.18.0-cp312-none-win32.whl", hash = "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98"}, - {file = "rpds_py-0.18.0-cp312-none-win_amd64.whl", hash = "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec"}, - {file = "rpds_py-0.18.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e"}, - {file = "rpds_py-0.18.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d"}, - {file = "rpds_py-0.18.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c"}, - {file = "rpds_py-0.18.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594"}, - {file = "rpds_py-0.18.0-cp38-none-win32.whl", hash = "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e"}, - {file = "rpds_py-0.18.0-cp38-none-win_amd64.whl", hash = "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1"}, - {file = "rpds_py-0.18.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33"}, - {file = "rpds_py-0.18.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9"}, - {file = "rpds_py-0.18.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024"}, - {file = "rpds_py-0.18.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20"}, - {file = "rpds_py-0.18.0-cp39-none-win32.whl", hash = "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7"}, - {file = "rpds_py-0.18.0-cp39-none-win_amd64.whl", hash = "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984"}, - {file = "rpds_py-0.18.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da"}, - {file = "rpds_py-0.18.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432"}, - {file = "rpds_py-0.18.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f"}, - {file = "rpds_py-0.18.0.tar.gz", hash = "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, + {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, + {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, + {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, + {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, + {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, + {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, + {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, + {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, + {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, + {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, + {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, + {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, + {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, ] [[package]] @@ -4227,24 +4302,24 @@ python-versions = ">=3.6" files = [ {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b42169467c42b692c19cf539c38d4602069d8c1505e97b86387fcf7afb766e1d"}, {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:07238db9cbdf8fc1e9de2489a4f68474e70dffcb32232db7c08fa61ca0c7c462"}, + {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d92f81886165cb14d7b067ef37e142256f1c6a90a65cd156b063a43da1708cfd"}, {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:fff3573c2db359f091e1589c3d7c5fc2f86f5bdb6f24252c2d8e539d4e45f412"}, - {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:aa2267c6a303eb483de8d02db2871afb5c5fc15618d894300b88958f729ad74f"}, {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:840f0c7f194986a63d2c2465ca63af8ccbbc90ab1c6001b1978f05119b5e7334"}, {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:024cfe1fc7c7f4e1aff4a81e718109e13409767e4f871443cbff3dba3578203d"}, {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win32.whl", hash = "sha256:c69212f63169ec1cfc9bb44723bf2917cbbd8f6191a00ef3410f5a7fe300722d"}, {file = "ruamel.yaml.clib-0.2.8-cp310-cp310-win_amd64.whl", hash = "sha256:cabddb8d8ead485e255fe80429f833172b4cadf99274db39abc080e068cbcc31"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bef08cd86169d9eafb3ccb0a39edb11d8e25f3dae2b28f5c52fd997521133069"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b16420e621d26fdfa949a8b4b47ade8810c56002f5389970db4ddda51dbff248"}, + {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:b5edda50e5e9e15e54a6a8a0070302b00c518a9d32accc2346ad6c984aacd279"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:25c515e350e5b739842fc3228d662413ef28f295791af5e5110b543cf0b57d9b"}, - {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:1707814f0d9791df063f8c19bb51b0d1278b8e9a2353abbb676c2f685dee6afe"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:46d378daaac94f454b3a0e3d8d78cafd78a026b1d71443f4966c696b48a6d899"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09b055c05697b38ecacb7ac50bdab2240bfca1a0c4872b0fd309bb07dc9aa3a9"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win32.whl", hash = "sha256:53a300ed9cea38cf5a2a9b069058137c2ca1ce658a874b79baceb8f892f915a7"}, {file = "ruamel.yaml.clib-0.2.8-cp311-cp311-win_amd64.whl", hash = "sha256:c2a72e9109ea74e511e29032f3b670835f8a59bbdc9ce692c5b4ed91ccf1eedb"}, {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ebc06178e8821efc9692ea7544aa5644217358490145629914d8020042c24aa1"}, {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:edaef1c1200c4b4cb914583150dcaa3bc30e592e907c01117c08b13a07255ec2"}, + {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:7048c338b6c86627afb27faecf418768acb6331fc24cfa56c93e8c9780f815fa"}, {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d176b57452ab5b7028ac47e7b3cf644bcfdc8cacfecf7e71759f7f51a59e5c92"}, - {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:1dc67314e7e1086c9fdf2680b7b6c2be1c0d8e3a8279f2e993ca2a7545fecf62"}, {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3213ece08ea033eb159ac52ae052a4899b56ecc124bb80020d9bbceeb50258e9"}, {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aab7fd643f71d7946f2ee58cc88c9b7bfc97debd71dcc93e03e2d174628e7e2d"}, {file = "ruamel.yaml.clib-0.2.8-cp312-cp312-win32.whl", hash = "sha256:5c365d91c88390c8d0a8545df0b5857172824b1c604e867161e6b3d59a827eaa"}, @@ -4252,7 +4327,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.8-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a5aa27bad2bb83670b71683aae140a1f52b0857a2deff56ad3f6c13a017a26ed"}, {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c58ecd827313af6864893e7af0a3bb85fd529f862b6adbefe14643947cfe2942"}, {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-macosx_12_0_arm64.whl", hash = "sha256:f481f16baec5290e45aebdc2a5168ebc6d35189ae6fea7a58787613a25f6e875"}, - {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:77159f5d5b5c14f7c34073862a6b7d34944075d9f93e681638f6d753606c6ce6"}, + {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:3fcc54cb0c8b811ff66082de1680b4b14cf8a81dce0d4fbf665c2265a81e07a1"}, {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:7f67a1ee819dc4562d444bbafb135832b0b909f81cc90f7aa00260968c9ca1b3"}, {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4ecbf9c3e19f9562c7fdd462e8d18dd902a47ca046a2e64dba80699f0b6c09b7"}, {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:87ea5ff66d8064301a154b3933ae406b0863402a799b16e4a1d24d9fbbcbe0d3"}, @@ -4260,7 +4335,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.8-cp37-cp37m-win_amd64.whl", hash = "sha256:3f215c5daf6a9d7bbed4a0a4f760f3113b10e82ff4c5c44bec20a68c8014f675"}, {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1b617618914cb00bf5c34d4357c37aa15183fa229b24767259657746c9077615"}, {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:a6a9ffd280b71ad062eae53ac1659ad86a17f59a0fdc7699fd9be40525153337"}, - {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:305889baa4043a09e5b76f8e2a51d4ffba44259f6b4c72dec8ca56207d9c6fe1"}, + {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:665f58bfd29b167039f714c6998178d27ccd83984084c286110ef26b230f259f"}, {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:700e4ebb569e59e16a976857c8798aee258dceac7c7d6b50cab63e080058df91"}, {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e2b4c44b60eadec492926a7270abb100ef9f72798e18743939bdbf037aab8c28"}, {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e79e5db08739731b0ce4850bed599235d601701d5694c36570a99a0c5ca41a9d"}, @@ -4268,7 +4343,7 @@ files = [ {file = "ruamel.yaml.clib-0.2.8-cp38-cp38-win_amd64.whl", hash = "sha256:56f4252222c067b4ce51ae12cbac231bce32aee1d33fbfc9d17e5b8d6966c312"}, {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03d1162b6d1df1caa3a4bd27aa51ce17c9afc2046c31b0ad60a0a96ec22f8001"}, {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba64af9fa9cebe325a62fa398760f5c7206b215201b0ec825005f1b18b9bccf"}, - {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:a1a45e0bb052edf6a1d3a93baef85319733a888363938e1fc9924cb00c8df24c"}, + {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:9eb5dee2772b0f704ca2e45b1713e4e5198c18f515b52743576d196348f374d3"}, {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:da09ad1c359a728e112d60116f626cc9f29730ff3e0e7db72b9a2dbc2e4beed5"}, {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:184565012b60405d93838167f425713180b949e9d8dd0bbc7b49f074407c5a8b"}, {file = "ruamel.yaml.clib-0.2.8-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a75879bacf2c987c003368cf14bed0ffe99e8e85acfa6c0bfffc21a090f16880"}, @@ -4279,13 +4354,13 @@ files = [ [[package]] name = "s3transfer" -version = "0.10.1" +version = "0.10.2" description = "An Amazon S3 Transfer Manager" optional = false -python-versions = ">= 3.8" +python-versions = ">=3.8" files = [ - {file = "s3transfer-0.10.1-py3-none-any.whl", hash = "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d"}, - {file = "s3transfer-0.10.1.tar.gz", hash = "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19"}, + {file = "s3transfer-0.10.2-py3-none-any.whl", hash = "sha256:eca1c20de70a39daee580aef4986996620f365c4e0fda6a86100231d62f1bf69"}, + {file = "s3transfer-0.10.2.tar.gz", hash = "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6"}, ] [package.dependencies] @@ -4296,13 +4371,13 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "safety" -version = "3.2.7" +version = "3.2.8" description = "Checks installed dependencies for known vulnerabilities and licenses." optional = false python-versions = ">=3.7" files = [ - {file = "safety-3.2.7-py3-none-any.whl", hash = "sha256:7f111b689901f2e65f7dd524e4c84158c368bcbbdbb38cbbb06ba1249250da11"}, - {file = "safety-3.2.7.tar.gz", hash = "sha256:cd6093b82d84e85815f037334f1b83ed89d31f767b7b324bfed6228c434f2667"}, + {file = "safety-3.2.8-py3-none-any.whl", hash = "sha256:af6b8c0bb5305b27e00a24b91d3c691d980d43c9becc919d16e5264a78a0437e"}, + {file = "safety-3.2.8.tar.gz", hash = "sha256:4385ea2b321fda163910d1a96ecaaca89d518e28ea5a88c1bb3f53171b45bf94"}, ] [package.dependencies] @@ -4331,13 +4406,13 @@ spdx = ["spdx-tools (>=0.8.2)"] [[package]] name = "safety-schemas" -version = "0.0.4" +version = "0.0.5" description = "Schemas for Safety tools" optional = false python-versions = ">=3.7" files = [ - {file = "safety_schemas-0.0.4-py3-none-any.whl", hash = "sha256:b8b93e447bbffe62e4bd4364877f8ac0dc9688056911b2618d6f48773f9c9011"}, - {file = "safety_schemas-0.0.4.tar.gz", hash = "sha256:5ec6a8e2a80620a829a9d236165cce9d9e864b0345345d1fc983397eb5d2ac65"}, + {file = "safety_schemas-0.0.5-py3-none-any.whl", hash = "sha256:6ac9eb71e60f0d4e944597c01dd48d6d8cd3d467c94da4aba3702a05a3a6ab4f"}, + {file = "safety_schemas-0.0.5.tar.gz", hash = "sha256:0de5fc9a53d4423644a8ce9a17a2e474714aa27e57f3506146e95a41710ff104"}, ] [package.dependencies] @@ -4347,21 +4422,6 @@ pydantic = "*" ruamel-yaml = ">=0.17.21" typing-extensions = ">=4.7.1" -[[package]] -name = "sarif-om" -version = "1.0.4" -description = "Classes implementing the SARIF 2.1.0 object model." -optional = false -python-versions = ">= 2.7" -files = [ - {file = "sarif_om-1.0.4-py3-none-any.whl", hash = "sha256:539ef47a662329b1c8502388ad92457425e95dc0aaaf995fe46f4984c4771911"}, - {file = "sarif_om-1.0.4.tar.gz", hash = "sha256:cd5f416b3083e00d402a92e449a7ff67af46f11241073eea0461802a3b5aef98"}, -] - -[package.dependencies] -attrs = "*" -pbr = "*" - [[package]] name = "schema" version = "0.7.7" @@ -4375,18 +4435,23 @@ files = [ [[package]] name = "setuptools" -version = "70.0.0" +version = "75.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, - {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, + {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, + {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] [[package]] name = "shellingham" @@ -4430,13 +4495,13 @@ files = [ [[package]] name = "slack-sdk" -version = "3.32.0" +version = "3.33.1" description = "The Slack API Platform SDK for Python" optional = false python-versions = ">=3.6" files = [ - {file = "slack_sdk-3.32.0-py2.py3-none-any.whl", hash = "sha256:f35e85f2847e6c25cf7c2d1df206ca0ad75556263fb592457bf03cca68ef64bb"}, - {file = "slack_sdk-3.32.0.tar.gz", hash = "sha256:af8fc4ef1d1cbcecd28d01acf6955a3bb5b13d56f0a43a1b1c7e3b212cc5ec5b"}, + {file = "slack_sdk-3.33.1-py2.py3-none-any.whl", hash = "sha256:ef93beec3ce9c8f64da02fd487598a05ec4bc9c92ceed58f122dbe632691cbe2"}, + {file = "slack_sdk-3.33.1.tar.gz", hash = "sha256:e328bb661d95db5f66b993b1d64288ac7c72201a745b4c7cf8848dafb7b74e40"}, ] [package.extras] @@ -4466,42 +4531,45 @@ files = [ [[package]] name = "std-uritemplate" -version = "0.0.57" +version = "2.0.0" description = "std-uritemplate implementation for Python" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "std_uritemplate-0.0.57-py3-none-any.whl", hash = "sha256:66691cb6ff1d1b3612741053d6f5573ec7eb1c1a33ffb5ca49557e8aa2372aa8"}, - {file = "std_uritemplate-0.0.57.tar.gz", hash = "sha256:f4adc717aec138562e652b95da74fc6815a942231d971314856b81f434c1b94c"}, + {file = "std_uritemplate-2.0.0-py3-none-any.whl", hash = "sha256:b69a80b707b93095075066005155eebd9172ec1ac7601992e2f9e4ec380f5a7b"}, + {file = "std_uritemplate-2.0.0.tar.gz", hash = "sha256:be88c8a4a89fee6768237f594c537d9347d96d7e68b8ad12200a3f11a0f6398f"}, ] [[package]] name = "stevedore" -version = "5.2.0" +version = "5.3.0" description = "Manage dynamic plugins for Python applications" optional = false python-versions = ">=3.8" files = [ - {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"}, - {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"}, + {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, + {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, ] [package.dependencies] -pbr = ">=2.0.0,<2.1.0 || >2.1.0" +pbr = ">=2.0.0" [[package]] name = "sympy" -version = "1.12" +version = "1.13.3" description = "Computer algebra system (CAS) in Python" optional = false python-versions = ">=3.8" files = [ - {file = "sympy-1.12-py3-none-any.whl", hash = "sha256:c3588cd4295d0c0f603d0f2ae780587e64e2efeedb3521e46b9bb1d08d184fa5"}, - {file = "sympy-1.12.tar.gz", hash = "sha256:ebf595c8dac3e0fdc4152c51878b498396ec7f30e7a914d6071e674d49420fb8"}, + {file = "sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73"}, + {file = "sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9"}, ] [package.dependencies] -mpmath = ">=0.19" +mpmath = ">=1.1.0,<1.4" + +[package.extras] +dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] [[package]] name = "tabulate" @@ -4519,17 +4587,18 @@ widechars = ["wcwidth"] [[package]] name = "tenacity" -version = "8.2.3" +version = "9.0.0" description = "Retry code until it succeeds" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tenacity-8.2.3-py3-none-any.whl", hash = "sha256:ce510e327a630c9e1beaf17d42e6ffacc88185044ad85cf74c0a8887c6a0f88c"}, - {file = "tenacity-8.2.3.tar.gz", hash = "sha256:5398ef0d78e63f40007c1fb4c0bff96e1911394d2fa8d194f77619c05ff6cc8a"}, + {file = "tenacity-9.0.0-py3-none-any.whl", hash = "sha256:93de0c98785b27fcf659856aa9f54bfbd399e29969b0621bc7f762bd441b4539"}, + {file = "tenacity-9.0.0.tar.gz", hash = "sha256:807f37ca97d62aa361264d497b0e31e92b8027044942bfa756160d908320d73b"}, ] [package.extras] -doc = ["reno", "sphinx", "tornado (>=4.5)"] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] [[package]] name = "tldextract" @@ -4565,24 +4634,24 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.4" +version = "0.13.2" description = "Style preserving TOML library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, - {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] name = "typer" -version = "0.12.3" +version = "0.12.5" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, - {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, + {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, + {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, ] [package.dependencies] @@ -4593,24 +4662,24 @@ typing-extensions = ">=3.7.4.3" [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] name = "tzdata" -version = "2024.1" +version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" files = [ - {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, - {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] [[package]] @@ -4643,13 +4712,13 @@ files = [ [[package]] name = "urllib3" -version = "1.26.19" +version = "1.26.20" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, - {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, + {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, ] [package.extras] @@ -4657,15 +4726,32 @@ brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotl secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "vulture" -version = "2.11" +version = "2.12" description = "Find dead code" optional = false python-versions = ">=3.8" files = [ - {file = "vulture-2.11-py2.py3-none-any.whl", hash = "sha256:12d745f7710ffbf6aeb8279ba9068a24d4e52e8ed333b8b044035c9d6b823aba"}, - {file = "vulture-2.11.tar.gz", hash = "sha256:f0fbb60bce6511aad87ee0736c502456737490a82d919a44e6d92262cb35f1c2"}, + {file = "vulture-2.12-py2.py3-none-any.whl", hash = "sha256:68ee4c4ce0128bb504dc7c2df4244c97ef5a2e29af42f27a976a6e30906e993a"}, + {file = "vulture-2.12.tar.gz", hash = "sha256:c35e98e992eb84b01cdadbfeb0aca2d44363e7dfe6c19416f65001ae69360ccc"}, ] [package.dependencies] @@ -4673,40 +4759,41 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [[package]] name = "watchdog" -version = "4.0.0" +version = "5.0.3" description = "Filesystem events monitoring" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, - {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, - {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, - {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, - {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, - {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, - {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, - {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, - {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, - {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, - {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, - {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, - {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, - {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, + {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:85527b882f3facda0579bce9d743ff7f10c3e1e0db0a0d0e28170a7d0e5ce2ea"}, + {file = "watchdog-5.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53adf73dcdc0ef04f7735066b4a57a4cd3e49ef135daae41d77395f0b5b692cb"}, + {file = "watchdog-5.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e25adddab85f674acac303cf1f5835951345a56c5f7f582987d266679979c75b"}, + {file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f01f4a3565a387080dc49bdd1fefe4ecc77f894991b88ef927edbfa45eb10818"}, + {file = "watchdog-5.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91b522adc25614cdeaf91f7897800b82c13b4b8ac68a42ca959f992f6990c490"}, + {file = "watchdog-5.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d52db5beb5e476e6853da2e2d24dbbbed6797b449c8bf7ea118a4ee0d2c9040e"}, + {file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:94d11b07c64f63f49876e0ab8042ae034674c8653bfcdaa8c4b32e71cfff87e8"}, + {file = "watchdog-5.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:349c9488e1d85d0a58e8cb14222d2c51cbc801ce11ac3936ab4c3af986536926"}, + {file = "watchdog-5.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:53a3f10b62c2d569e260f96e8d966463dec1a50fa4f1b22aec69e3f91025060e"}, + {file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:950f531ec6e03696a2414b6308f5c6ff9dab7821a768c9d5788b1314e9a46ca7"}, + {file = "watchdog-5.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ae6deb336cba5d71476caa029ceb6e88047fc1dc74b62b7c4012639c0b563906"}, + {file = "watchdog-5.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1021223c08ba8d2d38d71ec1704496471ffd7be42cfb26b87cd5059323a389a1"}, + {file = "watchdog-5.0.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:752fb40efc7cc8d88ebc332b8f4bcbe2b5cc7e881bccfeb8e25054c00c994ee3"}, + {file = "watchdog-5.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a2e8f3f955d68471fa37b0e3add18500790d129cc7efe89971b8a4cc6fdeb0b2"}, + {file = "watchdog-5.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b8ca4d854adcf480bdfd80f46fdd6fb49f91dd020ae11c89b3a79e19454ec627"}, + {file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:90a67d7857adb1d985aca232cc9905dd5bc4803ed85cfcdcfcf707e52049eda7"}, + {file = "watchdog-5.0.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:720ef9d3a4f9ca575a780af283c8fd3a0674b307651c1976714745090da5a9e8"}, + {file = "watchdog-5.0.3-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:223160bb359281bb8e31c8f1068bf71a6b16a8ad3d9524ca6f523ac666bb6a1e"}, + {file = "watchdog-5.0.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:560135542c91eaa74247a2e8430cf83c4342b29e8ad4f520ae14f0c8a19cfb5b"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dd021efa85970bd4824acacbb922066159d0f9e546389a4743d56919b6758b91"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_armv7l.whl", hash = "sha256:78864cc8f23dbee55be34cc1494632a7ba30263951b5b2e8fc8286b95845f82c"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_i686.whl", hash = "sha256:1e9679245e3ea6498494b3028b90c7b25dbb2abe65c7d07423ecfc2d6218ff7c"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64.whl", hash = "sha256:9413384f26b5d050b6978e6fcd0c1e7f0539be7a4f1a885061473c5deaa57221"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:294b7a598974b8e2c6123d19ef15de9abcd282b0fbbdbc4d23dfa812959a9e05"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_s390x.whl", hash = "sha256:26dd201857d702bdf9d78c273cafcab5871dd29343748524695cecffa44a8d97"}, + {file = "watchdog-5.0.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0f9332243355643d567697c3e3fa07330a1d1abf981611654a1f2bf2175612b7"}, + {file = "watchdog-5.0.3-py3-none-win32.whl", hash = "sha256:c66f80ee5b602a9c7ab66e3c9f36026590a0902db3aea414d59a2f55188c1f49"}, + {file = "watchdog-5.0.3-py3-none-win_amd64.whl", hash = "sha256:f00b4cf737f568be9665563347a910f8bdc76f88c2970121c86243c8cfdf90e9"}, + {file = "watchdog-5.0.3-py3-none-win_ia64.whl", hash = "sha256:49f4d36cb315c25ea0d946e018c01bb028048023b9e103d3d3943f58e109dd45"}, + {file = "watchdog-5.0.3.tar.gz", hash = "sha256:108f42a7f0345042a854d4d0ad0834b741d421330d5f575b81cb27b883500176"}, ] [package.extras] @@ -4730,13 +4817,13 @@ test = ["websockets"] [[package]] name = "werkzeug" -version = "3.0.3" +version = "3.0.4" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.8" files = [ - {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, - {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, + {file = "werkzeug-3.0.4-py3-none-any.whl", hash = "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c"}, + {file = "werkzeug-3.0.4.tar.gz", hash = "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306"}, ] [package.dependencies] @@ -4848,101 +4935,103 @@ files = [ [[package]] name = "yarl" -version = "1.9.4" +version = "1.13.1" description = "Yet another URL library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, + {file = "yarl-1.13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:82e692fb325013a18a5b73a4fed5a1edaa7c58144dc67ad9ef3d604eccd451ad"}, + {file = "yarl-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df4e82e68f43a07735ae70a2d84c0353e58e20add20ec0af611f32cd5ba43fb4"}, + {file = "yarl-1.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec9dd328016d8d25702a24ee274932aebf6be9787ed1c28d021945d264235b3c"}, + {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5820bd4178e6a639b3ef1db8b18500a82ceab6d8b89309e121a6859f56585b05"}, + {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86c438ce920e089c8c2388c7dcc8ab30dfe13c09b8af3d306bcabb46a053d6f7"}, + {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3de86547c820e4f4da4606d1c8ab5765dd633189791f15247706a2eeabc783ae"}, + {file = "yarl-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca53632007c69ddcdefe1e8cbc3920dd88825e618153795b57e6ebcc92e752a"}, + {file = "yarl-1.13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4ee1d240b84e2f213565f0ec08caef27a0e657d4c42859809155cf3a29d1735"}, + {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c49f3e379177f4477f929097f7ed4b0622a586b0aa40c07ac8c0f8e40659a1ac"}, + {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5c5e32fef09ce101fe14acd0f498232b5710effe13abac14cd95de9c274e689e"}, + {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ab9524e45ee809a083338a749af3b53cc7efec458c3ad084361c1dbf7aaf82a2"}, + {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b1481c048fe787f65e34cb06f7d6824376d5d99f1231eae4778bbe5c3831076d"}, + {file = "yarl-1.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:31497aefd68036d8e31bfbacef915826ca2e741dbb97a8d6c7eac66deda3b606"}, + {file = "yarl-1.13.1-cp310-cp310-win32.whl", hash = "sha256:1fa56f34b2236f5192cb5fceba7bbb09620e5337e0b6dfe2ea0ddbd19dd5b154"}, + {file = "yarl-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:1bbb418f46c7f7355084833051701b2301092e4611d9e392360c3ba2e3e69f88"}, + {file = "yarl-1.13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:216a6785f296169ed52cd7dcdc2612f82c20f8c9634bf7446327f50398732a51"}, + {file = "yarl-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40c6e73c03a6befb85b72da213638b8aaa80fe4136ec8691560cf98b11b8ae6e"}, + {file = "yarl-1.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2430cf996113abe5aee387d39ee19529327205cda975d2b82c0e7e96e5fdabdc"}, + {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fb4134cc6e005b99fa29dbc86f1ea0a298440ab6b07c6b3ee09232a3b48f495"}, + {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309c104ecf67626c033845b860d31594a41343766a46fa58c3309c538a1e22b2"}, + {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f90575e9fe3aae2c1e686393a9689c724cd00045275407f71771ae5d690ccf38"}, + {file = "yarl-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d2e1626be8712333a9f71270366f4a132f476ffbe83b689dd6dc0d114796c74"}, + {file = "yarl-1.13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b66c87da3c6da8f8e8b648878903ca54589038a0b1e08dde2c86d9cd92d4ac9"}, + {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cf1ad338620249f8dd6d4b6a91a69d1f265387df3697ad5dc996305cf6c26fb2"}, + {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9915300fe5a0aa663c01363db37e4ae8e7c15996ebe2c6cce995e7033ff6457f"}, + {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:703b0f584fcf157ef87816a3c0ff868e8c9f3c370009a8b23b56255885528f10"}, + {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1d8e3ca29f643dd121f264a7c89f329f0fcb2e4461833f02de6e39fef80f89da"}, + {file = "yarl-1.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7055bbade838d68af73aea13f8c86588e4bcc00c2235b4b6d6edb0dbd174e246"}, + {file = "yarl-1.13.1-cp311-cp311-win32.whl", hash = "sha256:a3442c31c11088e462d44a644a454d48110f0588de830921fd201060ff19612a"}, + {file = "yarl-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:81bad32c8f8b5897c909bf3468bf601f1b855d12f53b6af0271963ee67fff0d2"}, + {file = "yarl-1.13.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f452cc1436151387d3d50533523291d5f77c6bc7913c116eb985304abdbd9ec9"}, + {file = "yarl-1.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9cec42a20eae8bebf81e9ce23fb0d0c729fc54cf00643eb251ce7c0215ad49fe"}, + {file = "yarl-1.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d959fe96e5c2712c1876d69af0507d98f0b0e8d81bee14cfb3f6737470205419"}, + {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c837ab90c455f3ea8e68bee143472ee87828bff19ba19776e16ff961425b57"}, + {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94a993f976cdcb2dc1b855d8b89b792893220db8862d1a619efa7451817c836b"}, + {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2442a415a5f4c55ced0fade7b72123210d579f7d950e0b5527fc598866e62c"}, + {file = "yarl-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3fdbf0418489525231723cdb6c79e7738b3cbacbaed2b750cb033e4ea208f220"}, + {file = "yarl-1.13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b7f6e699304717fdc265a7e1922561b02a93ceffdaefdc877acaf9b9f3080b8"}, + {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bcd5bf4132e6a8d3eb54b8d56885f3d3a38ecd7ecae8426ecf7d9673b270de43"}, + {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2a93a4557f7fc74a38ca5a404abb443a242217b91cd0c4840b1ebedaad8919d4"}, + {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:22b739f99c7e4787922903f27a892744189482125cc7b95b747f04dd5c83aa9f"}, + {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2db874dd1d22d4c2c657807562411ffdfabec38ce4c5ce48b4c654be552759dc"}, + {file = "yarl-1.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4feaaa4742517eaceafcbe74595ed335a494c84634d33961214b278126ec1485"}, + {file = "yarl-1.13.1-cp312-cp312-win32.whl", hash = "sha256:bbf9c2a589be7414ac4a534d54e4517d03f1cbb142c0041191b729c2fa23f320"}, + {file = "yarl-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:d07b52c8c450f9366c34aa205754355e933922c79135125541daae6cbf31c799"}, + {file = "yarl-1.13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:95c6737f28069153c399d875317f226bbdea939fd48a6349a3b03da6829fb550"}, + {file = "yarl-1.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cd66152561632ed4b2a9192e7f8e5a1d41e28f58120b4761622e0355f0fe034c"}, + {file = "yarl-1.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6a2acde25be0cf9be23a8f6cbd31734536a264723fca860af3ae5e89d771cd71"}, + {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9a18595e6a2ee0826bf7dfdee823b6ab55c9b70e8f80f8b77c37e694288f5de1"}, + {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a31d21089894942f7d9a8df166b495101b7258ff11ae0abec58e32daf8088813"}, + {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45f209fb4bbfe8630e3d2e2052535ca5b53d4ce2d2026bed4d0637b0416830da"}, + {file = "yarl-1.13.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f722f30366474a99745533cc4015b1781ee54b08de73260b2bbe13316079851"}, + {file = "yarl-1.13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3bf60444269345d712838bb11cc4eadaf51ff1a364ae39ce87a5ca8ad3bb2c8"}, + {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:942c80a832a79c3707cca46bd12ab8aa58fddb34b1626d42b05aa8f0bcefc206"}, + {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:44b07e1690f010c3c01d353b5790ec73b2f59b4eae5b0000593199766b3f7a5c"}, + {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:396e59b8de7e4d59ff5507fb4322d2329865b909f29a7ed7ca37e63ade7f835c"}, + {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3bb83a0f12701c0b91112a11148b5217617982e1e466069d0555be9b372f2734"}, + {file = "yarl-1.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c92b89bffc660f1274779cb6fbb290ec1f90d6dfe14492523a0667f10170de26"}, + {file = "yarl-1.13.1-cp313-cp313-win32.whl", hash = "sha256:269c201bbc01d2cbba5b86997a1e0f73ba5e2f471cfa6e226bcaa7fd664b598d"}, + {file = "yarl-1.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:1d0828e17fa701b557c6eaed5edbd9098eb62d8838344486248489ff233998b8"}, + {file = "yarl-1.13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8be8cdfe20787e6a5fcbd010f8066227e2bb9058331a4eccddec6c0db2bb85b2"}, + {file = "yarl-1.13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08d7148ff11cb8e886d86dadbfd2e466a76d5dd38c7ea8ebd9b0e07946e76e4b"}, + {file = "yarl-1.13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4afdf84610ca44dcffe8b6c22c68f309aff96be55f5ea2fa31c0c225d6b83e23"}, + {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0d12fe78dcf60efa205e9a63f395b5d343e801cf31e5e1dda0d2c1fb618073d"}, + {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298c1eecfd3257aa16c0cb0bdffb54411e3e831351cd69e6b0739be16b1bdaa8"}, + {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c14c16831b565707149c742d87a6203eb5597f4329278446d5c0ae7a1a43928e"}, + {file = "yarl-1.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a9bacedbb99685a75ad033fd4de37129449e69808e50e08034034c0bf063f99"}, + {file = "yarl-1.13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:658e8449b84b92a4373f99305de042b6bd0d19bf2080c093881e0516557474a5"}, + {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:373f16f38721c680316a6a00ae21cc178e3a8ef43c0227f88356a24c5193abd6"}, + {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:45d23c4668d4925688e2ea251b53f36a498e9ea860913ce43b52d9605d3d8177"}, + {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:f7917697bcaa3bc3e83db91aa3a0e448bf5cde43c84b7fc1ae2427d2417c0224"}, + {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:5989a38ba1281e43e4663931a53fbf356f78a0325251fd6af09dd03b1d676a09"}, + {file = "yarl-1.13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:11b3ca8b42a024513adce810385fcabdd682772411d95bbbda3b9ed1a4257644"}, + {file = "yarl-1.13.1-cp38-cp38-win32.whl", hash = "sha256:dcaef817e13eafa547cdfdc5284fe77970b891f731266545aae08d6cce52161e"}, + {file = "yarl-1.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:7addd26594e588503bdef03908fc207206adac5bd90b6d4bc3e3cf33a829f57d"}, + {file = "yarl-1.13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a0ae6637b173d0c40b9c1462e12a7a2000a71a3258fa88756a34c7d38926911c"}, + {file = "yarl-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:576365c9f7469e1f6124d67b001639b77113cfd05e85ce0310f5f318fd02fe85"}, + {file = "yarl-1.13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:78f271722423b2d4851cf1f4fa1a1c4833a128d020062721ba35e1a87154a049"}, + {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d74f3c335cfe9c21ea78988e67f18eb9822f5d31f88b41aec3a1ec5ecd32da5"}, + {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1891d69a6ba16e89473909665cd355d783a8a31bc84720902c5911dbb6373465"}, + {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb382fd7b4377363cc9f13ba7c819c3c78ed97c36a82f16f3f92f108c787cbbf"}, + {file = "yarl-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c8854b9f80693d20cec797d8e48a848c2fb273eb6f2587b57763ccba3f3bd4b"}, + {file = "yarl-1.13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbf2c3f04ff50f16404ce70f822cdc59760e5e2d7965905f0e700270feb2bbfc"}, + {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fb9f59f3848edf186a76446eb8bcf4c900fe147cb756fbbd730ef43b2e67c6a7"}, + {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ef9b85fa1bc91c4db24407e7c4da93a5822a73dd4513d67b454ca7064e8dc6a3"}, + {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:098b870c18f1341786f290b4d699504e18f1cd050ed179af8123fd8232513424"}, + {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:8c723c91c94a3bc8033dd2696a0f53e5d5f8496186013167bddc3fb5d9df46a3"}, + {file = "yarl-1.13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:44a4c40a6f84e4d5955b63462a0e2a988f8982fba245cf885ce3be7618f6aa7d"}, + {file = "yarl-1.13.1-cp39-cp39-win32.whl", hash = "sha256:84bbcdcf393139f0abc9f642bf03f00cac31010f3034faa03224a9ef0bb74323"}, + {file = "yarl-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:fc2931ac9ce9c61c9968989ec831d3a5e6fcaaff9474e7cfa8de80b7aff5a093"}, + {file = "yarl-1.13.1-py3-none-any.whl", hash = "sha256:6a5185ad722ab4dd52d5fb1f30dcc73282eb1ed494906a92d1a228d3f89607b0"}, + {file = "yarl-1.13.1.tar.gz", hash = "sha256:ec8cfe2295f3e5e44c51f57272afbd69414ae629ec7c6b27f5a410efc78b70a0"}, ] [package.dependencies] @@ -4951,20 +5040,24 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.20.0" +version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"}, - {file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"}, + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "219b2bca0e73319d0fde3ed71990402cd744097377f4b5590cfcb3cdadb8dce4" +content-hash = "dc828306f5e9f20d48ce7ad55ea41ec08e2e84f07e3757366670ac78a29e7896" diff --git a/prowler/__main__.py b/prowler/__main__.py index f153a8a4bb..7a229112f3 100644 --- a/prowler/__main__.py +++ b/prowler/__main__.py @@ -54,6 +54,7 @@ from prowler.lib.outputs.compliance.compliance import display_compliance_table from prowler.lib.outputs.compliance.ens.ens_aws import AWSENS from prowler.lib.outputs.compliance.generic.generic import GenericCompliance from prowler.lib.outputs.compliance.iso27001.iso27001_aws import AWSISO27001 +from prowler.lib.outputs.compliance.kisa_ismsp.kisa_ismsp_aws import AWSKISAISMSP from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_aws import AWSMitreAttack from prowler.lib.outputs.compliance.mitre_attack.mitre_attack_azure import ( AzureMitreAttack, @@ -68,8 +69,12 @@ from prowler.lib.outputs.slack.slack import Slack from prowler.lib.outputs.summary_table import display_summary_table from prowler.providers.aws.lib.s3.s3 import S3 from prowler.providers.aws.lib.security_hub.security_hub import SecurityHub +from prowler.providers.aws.models import AWSOutputOptions +from prowler.providers.azure.models import AzureOutputOptions from prowler.providers.common.provider import Provider from prowler.providers.common.quick_inventory import run_provider_quick_inventory +from prowler.providers.gcp.models import GCPOutputOptions +from prowler.providers.kubernetes.models import KubernetesOutputOptions def prowler(): @@ -190,7 +195,7 @@ def prowler(): sys.exit() # Provider to scan - Provider.set_global_provider(args) + Provider.init_global_provider(args) global_provider = Provider.get_global_provider() # Print Provider Credentials @@ -235,7 +240,22 @@ def prowler(): global_provider.mutelist = args.mutelist_file # Setup Output Options - global_provider.output_options = (args, bulk_checks_metadata) + if provider == "aws": + output_options = AWSOutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) + elif provider == "azure": + output_options = AzureOutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) + elif provider == "gcp": + output_options = GCPOutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) + elif provider == "kubernetes": + output_options = KubernetesOutputOptions( + args, bulk_checks_metadata, global_provider.identity + ) # Run the quick inventory for the provider if available if hasattr(args, "quick_inventory") and args.quick_inventory: @@ -251,6 +271,7 @@ def prowler(): global_provider, custom_checks_metadata, args.config_file, + output_options, ) else: logger.error( @@ -258,7 +279,7 @@ def prowler(): ) # Prowler Fixer - if global_provider.output_options.fixer: + if output_options.fixer: print(f"{Style.BRIGHT}\nRunning Prowler Fixer, please wait...{Style.RESET_ALL}") # Check if there are any FAIL findings if any("FAIL" in finding.status for finding in findings): @@ -304,7 +325,8 @@ def prowler(): # TODO: this part is needed since the checks generates a Check_Report_XXX and the output uses Finding # This will be refactored for the outputs generate directly the Finding finding_outputs = [ - Finding.generate_output(global_provider, finding) for finding in findings + Finding.generate_output(global_provider, finding, output_options) + for finding in findings ] generated_outputs = {"regular": [], "compliance": []} @@ -312,8 +334,8 @@ def prowler(): if args.output_formats: for mode in args.output_formats: filename = ( - f"{global_provider.output_options.output_directory}/" - f"{global_provider.output_options.output_filename}" + f"{output_options.output_directory}/" + f"{output_options.output_filename}" ) if mode == "csv": csv_output = CSV( @@ -355,16 +377,16 @@ def prowler(): ) # Compliance Frameworks - input_compliance_frameworks = set( - global_provider.output_options.output_modes - ).intersection(get_available_compliance_frameworks(provider)) + input_compliance_frameworks = set(output_options.output_modes).intersection( + get_available_compliance_frameworks(provider) + ) if provider == "aws": for compliance_name in input_compliance_frameworks: if compliance_name.startswith("cis_"): # Generate CIS Finding Object filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) cis = AWSCIS( findings=finding_outputs, @@ -377,8 +399,8 @@ def prowler(): elif compliance_name == "mitre_attack_aws": # Generate MITRE ATT&CK Finding Object filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) mitre_attack = AWSMitreAttack( findings=finding_outputs, @@ -391,8 +413,8 @@ def prowler(): elif compliance_name.startswith("ens_"): # Generate ENS Finding Object filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) ens = AWSENS( findings=finding_outputs, @@ -405,8 +427,8 @@ def prowler(): elif compliance_name.startswith("aws_well_architected_framework"): # Generate AWS Well-Architected Finding Object filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) aws_well_architected = AWSWellArchitected( findings=finding_outputs, @@ -419,8 +441,8 @@ def prowler(): elif compliance_name.startswith("iso27001_"): # Generate ISO27001 Finding Object filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) iso27001 = AWSISO27001( findings=finding_outputs, @@ -430,10 +452,24 @@ def prowler(): ) generated_outputs["compliance"].append(iso27001) iso27001.batch_write_data_to_file() + elif compliance_name.startswith("kisa"): + # Generate KISA-ISMS-P Finding Object + filename = ( + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" + ) + kisa_ismsp = AWSKISAISMSP( + findings=finding_outputs, + compliance=bulk_compliance_frameworks[compliance_name], + create_file_descriptor=True, + file_path=filename, + ) + generated_outputs["compliance"].append(kisa_ismsp) + kisa_ismsp.batch_write_data_to_file() else: filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) generic_compliance = GenericCompliance( findings=finding_outputs, @@ -449,8 +485,8 @@ def prowler(): if compliance_name.startswith("cis_"): # Generate CIS Finding Object filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) cis = AzureCIS( findings=finding_outputs, @@ -463,8 +499,8 @@ def prowler(): elif compliance_name == "mitre_attack_azure": # Generate MITRE ATT&CK Finding Object filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) mitre_attack = AzureMitreAttack( findings=finding_outputs, @@ -476,8 +512,8 @@ def prowler(): mitre_attack.batch_write_data_to_file() else: filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) generic_compliance = GenericCompliance( findings=finding_outputs, @@ -493,8 +529,8 @@ def prowler(): if compliance_name.startswith("cis_"): # Generate CIS Finding Object filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) cis = GCPCIS( findings=finding_outputs, @@ -507,8 +543,8 @@ def prowler(): elif compliance_name == "mitre_attack_gcp": # Generate MITRE ATT&CK Finding Object filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) mitre_attack = GCPMitreAttack( findings=finding_outputs, @@ -520,8 +556,8 @@ def prowler(): mitre_attack.batch_write_data_to_file() else: filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) generic_compliance = GenericCompliance( findings=finding_outputs, @@ -537,8 +573,8 @@ def prowler(): if compliance_name.startswith("cis_"): # Generate CIS Finding Object filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) cis = KubernetesCIS( findings=finding_outputs, @@ -550,8 +586,8 @@ def prowler(): cis.batch_write_data_to_file() else: filename = ( - f"{global_provider.output_options.output_directory}/compliance/" - f"{global_provider.output_options.output_filename}_{compliance_name}.csv" + f"{output_options.output_directory}/compliance/" + f"{output_options.output_filename}_{compliance_name}.csv" ) generic_compliance = GenericCompliance( findings=finding_outputs, @@ -594,7 +630,7 @@ def prowler(): aws_partition=global_provider.identity.partition, aws_session=global_provider.session.current_session, findings=asff_output.data, - send_only_fails=global_provider.output_options.send_sh_only_fails, + send_only_fails=output_options.send_sh_only_fails, aws_security_hub_available_regions=security_hub_regions, ) # Send the findings to Security Hub @@ -620,7 +656,7 @@ def prowler(): display_summary_table( findings, global_provider, - global_provider.output_options, + output_options, ) # Only display compliance table if there are findings (not all MANUAL) and it is a default execution if ( @@ -639,13 +675,13 @@ def prowler(): findings, bulk_checks_metadata, compliance, - global_provider.output_options.output_filename, - global_provider.output_options.output_directory, + output_options.output_filename, + output_options.output_directory, compliance_overview, ) if compliance_overview: print( - f"\nDetailed compliance results are in {Fore.YELLOW}{global_provider.output_options.output_directory}/compliance/{Style.RESET_ALL}\n" + f"\nDetailed compliance results are in {Fore.YELLOW}{output_options.output_directory}/compliance/{Style.RESET_ALL}\n" ) # If custom checks were passed, remove the modules diff --git a/prowler/compliance/aws/kisa_isms_p_2023_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_aws.json new file mode 100644 index 0000000000..c64c4195e6 --- /dev/null +++ b/prowler/compliance/aws/kisa_isms_p_2023_aws.json @@ -0,0 +1,4335 @@ +{ + "Framework": "KISA-ISMS-P", + "Version": "2023", + "Provider": "AWS", + "Description": "The ISMS-P certification, established by KISA (Korea Internet & Security Agency), is a system where an independent certification body evaluates whether a company or organization's information security and privacy protection measures comply with certification standards, and grants certification. This helps organizations improve public trust in their services and respond effectively to increasingly complex cyber threats. The ISMS-P framework also provides comprehensive guidelines for systematically establishing, implementing, and managing information security and privacy protection.", + "Requirements": [ + { + "Id": "1.1.1", + "Name": "Executive Participation", + "Description": "The CEO must establish and operate a reporting and decision-making system to ensure executive participation in the establishment and operation of the information protection and personal information protection management system.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.1. Management System", + "Section": "1.1.1 Executive Participation", + "AuditChecklist": [ + "Is there documentation outlining the responsibilities and roles of executives to ensure their participation in the establishment and operation of the information protection and personal information protection management system?", + "Is there a reporting, review, and approval process in place to ensure that executives actively participate in decision-making regarding information protection and personal information protection activities?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Information protection and personal information protection reporting system (e.g., communication plan)", + "Minutes of the Information Protection and Personal Information Protection Committee", + "Information protection and personal information protection policies/guidelines (including executive approval records)", + "Information protection plans and internal management plans (including executive approval records)", + "Information protection and personal information protection organization chart" + ], + "NonComplianceCases": [ + "Case 1: Although it is stated in the information protection and personal information protection policy to report the status of information protection and personal information protection to the executives on a quarterly basis, no such reports have been made for an extended period.", + "Case 2: In performing major information protection activities (e.g., risk assessment, determining risk acceptance levels, reviewing information protection measures and implementation plans, reviewing the results of information protection measures, security audits, etc.), executives or those authorized by the executives did not participate in decision-making or there was no evidence of their involvement." + ] + } + ] + }, + { + "Id": "1.1.2", + "Name": "Designation of Chief Officers", + "Description": "The CEO must appoint a Chief Information Security Officer (CISO) responsible for information protection and a Chief Privacy Officer (CPO) responsible for personal information protection, both at an executive level with authority to allocate resources such as budget and personnel.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.1. Management System", + "Section": "1.1.2 Designation of Chief Officers", + "AuditChecklist": [ + "Has the CEO officially designated a chief officer responsible for overseeing information protection and personal information protection?", + "Are the CISO and CPO appointed at an executive level with authority to allocate resources such as budget and personnel, and do they meet the qualifications required by relevant laws?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures), Article 31 (Designation of a Personal Information Protection Officer)", + "Information and Communications Network Act, Article 45-3 (Designation of a Chief Information Security Officer, etc.)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Documents related to the appointment of the CISO and CPO (e.g., personnel orders, personnel records)", + "Information protection and personal information protection organization chart", + "Information protection and personal information protection policies/guidelines", + "Job descriptions (roles and responsibilities of the CISO and CPO)", + "Records of CISO reports", + "Internal management plans (regarding the appointment of the CPO)" + ], + "NonComplianceCases": [ + "Case 1: Failure to appoint and report a CISO as required under the Information and Communications Network Act, even though the organization is obligated to do so.", + "Case 2: Appointing a person without substantial authority and status as the CPO, making it difficult to believe that they are responsible for overseeing personal information processing.", + "Case 3: Although the organization chart specifies the CISO and CPO, the formal appointment process, such as issuing personnel orders, was not followed.", + "Case 4: Although the entity is subject to ISMS certification and had over 5 trillion won in assets at the end of the previous year, the CISO also holds the position of CIO, in violation of the ISMS requirements." + ] + } + ] + }, + { + "Id": "1.1.3", + "Name": "Organization Structure", + "Description": "The CEO must establish and operate a working group to effectively implement information protection and personal information protection, a committee that can review and approve key matters related to information protection and personal information protection across the organization, and a consultative body consisting of department-level information protection and personal information protection officers for enterprise-wide protection activities.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.1. Management System", + "Section": "1.1.3 Organization Structure", + "AuditChecklist": [ + "Has the organization established and operated a working group with expertise to support the work of the CISO and CPO and systematically implement the organization's information protection and personal information protection activities?", + "Has the organization established and operated a committee that can review, approve, and make decisions on important information protection and personal information protection matters across the organization?", + "Has the organization established and operated a working group composed of information protection and personal information protection officers and department-level personnel for enterprise-wide information protection and personal information protection activities?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Information protection and personal information protection committee regulations/minutes", + "Information protection and personal information protection working group regulations/minutes", + "Information protection and personal information protection organization chart", + "Internal management plan", + "Job descriptions" + ], + "NonComplianceCases": [ + "Case 1: The Information Protection and Personal Information Protection Committee was established, but it consists only of department heads without the inclusion of executives, making it difficult to make decisions on the organization's key information and personal information protection matters.", + "Case 2: Although a working group for information protection and personal information protection was established, including heads of departments that handle important information and personal data, it has not been active for an extended period.", + "Case 3: Although the Information Protection and Personal Information Protection Committee was convened, major matters such as the annual information protection and personal information protection plan, training plan, budget, and personnel were not reviewed or decided upon.", + "Case 4: Although an Information Protection Committee was established for deliberation and decision-making on information protection and personal information protection matters, only the operations and IT security departments participated, without the involvement of departments responsible for personal information protection, leaving personal information protection matters undecided." + ] + } + ] + }, + { + "Id": "1.1.4", + "Name": "Scope Setting", + "Description": "The organization must set the scope of the management system by considering its core services and the current state of personal information processing, and document the related services, personal information processing tasks, organizations, assets, and physical locations.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.1. Management System", + "Section": "1.1.4 Scope Setting", + "AuditChecklist": [ + "Has the organization set the scope of the management system to include key assets that may affect core services and personal information processing?", + "If there are exceptions within the defined scope, are clear reasons documented, and are consultations with relevant stakeholders and approvals from responsible parties recorded and managed?", + "Is the organization maintaining documentation that includes the major services, operational status, and systems, allowing for clear verification of the scope of the information protection and personal information protection management system?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Scope definition document for information protection and personal information protection management system", + "List of information assets and personal information", + "Document list", + "Service flowchart", + "Personal information flowchart", + "Organization-wide organizational chart", + "System and network configuration diagram" + ], + "NonComplianceCases": [ + "Case 1: The development and test systems, external staff, PCs, and test devices related to the development work for information systems and personal information processing systems were omitted from the management system's scope.", + "Case 2: Key organizations (personnel) in departments and business units that play critical roles in decision-making for services or businesses within the scope of the information protection and personal information protection management system were not included in the certification scope.", + "Case 3: The development and test systems, developer PCs, test devices, and development organizations related to the development work for information systems and personal information processing systems were omitted from the management system's scope." + ] + } + ] + }, + { + "Id": "1.1.5", + "Name": "Policy Establishment", + "Description": "The organization must establish and document information protection and personal information protection policies and implementation documents, clearly stating the organization's information protection and personal information protection guidelines and direction. These policies and implementation documents must be approved by the executive management and communicated in an understandable form to employees and relevant parties.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.1. Management System", + "Section": "1.1.5 Policy Establishment", + "AuditChecklist": [ + "Has the organization established a top-level information protection and personal information protection policy that serves as the foundation for all information protection and personal information protection activities?", + "Has the organization established detailed guidelines, procedures, and manuals specifying the methods, processes, and frequencies required to implement the information protection and personal information protection policies?", + "Are the information protection and personal information protection policies and implementation documents approved by the CEO or by someone delegated by the CEO when newly established or revised?", + "Are the latest versions of the information protection and personal information protection policies and implementation documents provided to relevant employees in an easily understandable format?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Information protection and personal information protection policies/guidelines/procedures (including records of new/revised versions)", + "Meeting minutes of stakeholder reviews of newly established/revised information protection and personal information protection policies/guidelines/procedures", + "Internal management plans for personal information", + "Notifications of new/revised information protection and personal information protection policies/guidelines (via groupware, intranet, etc.)", + "Minutes of the Information Protection and Personal Information Protection Committee" + ], + "NonComplianceCases": [ + "Case 1: Although internal regulations stipulate that revisions to the information protection and personal information protection policies must be approved by the Information Protection and Personal Information Protection Committee, recent revisions were made solely based on the approval of the CISO and CPO without presenting the revisions to the committee.", + "Case 2: The information protection and personal information protection policies and guidelines were recently revised, but these changes were not communicated to relevant departments and employees, leading some departments to continue operating based on outdated guidelines.", + "Case 3: The information protection and personal information protection policies and guidelines are managed solely by the security department and are not made available for employees to access through bulletin boards or documents." + ] + } + ] + }, + { + "Id": "1.1.6", + "Name": "Resource Allocation", + "Description": "The CEO must allocate the necessary resources, including budget and personnel with expertise in the fields of information protection and personal information protection, for the effective implementation and continuous operation of the management system.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.1. Management System", + "Section": "1.1.6 Resource Allocation", + "AuditChecklist": [ + "Has the organization secured personnel with expertise in the fields of information protection and personal information protection?", + "Has the organization evaluated and allocated the necessary resources, including budget and personnel, for the effective implementation and continuous operation of the information protection and personal information protection management system?", + "Has the organization established and implemented an annual detailed action plan for information protection and personal information protection, and conducted audits, analyses, and evaluations of the results?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Annual action plan for information protection and personal information protection activities (including budget and personnel plans)", + "Reports on the results of information protection and personal information protection activities", + "Records of investments in information protection and personal information protection", + "Information protection and personal information protection organization chart" + ], + "NonComplianceCases": [ + "Case 1: The organization assembled an information protection and personal information protection team, but the team consisted only of personnel without expertise in information protection or IT, resulting in inadequate security staffing.", + "Case 2: The CEO failed to allocate sufficient resources, such as budget and security solutions, for implementing the technical and managerial safeguards required for personal information processing systems.", + "Case 3: After obtaining certification, the organization significantly reduced personnel and budget support, reassigned existing staff to other departments, and repurposed part of the budget for other uses." + ] + } + ] + }, + { + "Id": "1.2.1", + "Name": "Identification of Information Assets", + "Description": "Organizations must establish classification criteria for information assets according to the characteristics of their operations, identify and classify all information assets within the scope of the management system, assess their importance, and maintain an up-to-date list.", + "Checks": [ + "organizations_tags_policies_enabled_and_attached", + "organizations_account_part_of_organizations", + "macie_is_enabled", + "config_recorder_all_regions_enabled", + "resourceexplorer2_indexes_found" + ], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.2. Risk Management", + "Section": "1.2.1 Identification of Information Assets", + "AuditChecklist": [ + "Has the organization established classification criteria for information assets and identified all assets within the scope of the information protection and personal information protection management system, maintaining them in a list?", + "For the identified information assets, does the organization determine their importance by considering legal requirements and their impact on operations, and assign security levels?", + "Does the organization regularly review the status of information assets to keep the list up-to-date?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Information asset and personal information asset classification criteria", + "Information asset and personal information asset list (from asset management system screen)", + "Information asset and personal information security levels", + "Asset audit details", + "Risk analysis report (including asset identification)" + ], + "NonComplianceCases": [ + "Case 1: The list of assets within the scope of the information protection and personal information protection management system omits internal information leakage control systems, such as print security, document encryption, and USB media control, which are used to manage PCs handling important information and personal information.", + "Case 2: Personal information provided by third parties within the scope of the information protection and personal information protection management system has not been identified as an asset.", + "Case 3: The asset classification criteria in the internal guidelines and the classification criteria in the asset management register are inconsistent.", + "Case 4: Although on-premises assets have been identified, assets related to externally entrusted IT services (web hosting, server hosting, cloud, etc.) have been omitted (only for assets within the certification scope).", + "Case 5: The backup server storing unique identification information and other personal data has been classified with a low confidentiality rating, raising concerns about the reasonableness and reliability of the importance assessment." + ] + } + ] + }, + { + "Id": "1.2.2", + "Name": "Status and Flow Analysis", + "Description": "Organizations must analyze the status of information services and personal information processing across all areas of the management system, document the procedures and workflows, and review them regularly to maintain their accuracy.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.2. Risk Management", + "Section": "1.2.2 Status and Flow Analysis", + "AuditChecklist": [ + "Has the organization identified and documented the status and workflows of information services across all areas of the management system?", + "Has the organization identified and documented the status of personal information processing within the scope of the management system, and mapped out personal information flows in flowcharts?", + "Does the organization regularly review procedures and workflows in response to changes in services, operations, and information assets, and keep the flowcharts and related documents up-to-date?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Information service status table", + "Information service workflow charts and process maps", + "Personal information processing status table (for ISMS-P certification)", + "Personal information flowcharts (for ISMS-P certification)" + ], + "NonComplianceCases": [ + "Case 1: There are no documents outlining the workflows and procedures for major services within the scope of the management system.", + "Case 2: The personal information flowchart contains significant discrepancies from the actual personal information flow, or important personal information flows are missing.", + "Case 3: After the initial creation of the personal information flowchart, it has not been updated to reflect changes in the personal information flow." + ] + } + ] + }, + { + "Id": "1.2.3", + "Name": "Risk Assessment", + "Description": "Organizations must collect threat information by analyzing internal and external environments, select a risk assessment method suitable for the organization, conduct a risk assessment at least once a year across all areas of the management system, and manage acceptable risks with the approval of the executives.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.2. Risk Management", + "Section": "1.2.3 Risk Assessment", + "AuditChecklist": [ + "Has the organization defined methods for identifying and assessing risks that could arise from various aspects, depending on the characteristics of the organization or service?", + "Does the organization annually develop a risk management plan that specifies the personnel, timeline, targets, methods, and budget for risk management activities?", + "Does the organization conduct regular or ad-hoc risk assessments at least once a year according to the risk management plan?", + "Has the organization established an acceptable target risk level and identified risks that exceed that level?", + "Are the results of risk identification and assessment reported to the executives?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Risk management guidelines", + "Risk management manuals/guides", + "Risk management plan", + "Risk assessment report", + "Minutes of the Information Protection and Personal Information Protection Committee", + "Minutes of the Information Protection and Personal Information Protection Working Group", + "Information asset and personal information asset list", + "Information service and personal information flowcharts" + ], + "NonComplianceCases": [ + "Case 1: The risk management plan specifies the risk assessment period and the targets and methods for risk management, but lacks details on the personnel and budget required for execution.", + "Case 2: While a risk assessment was conducted in the previous year, no risk assessment was conducted this year due to a lack of changes in assets.", + "Case 3: The organization conducted risk identification and assessment according to the risk management plan, but failed to assess the risks of important information assets within the scope, or failed to assess compliance with legal requirements related to information protection.", + "Case 4: The organization identified and assessed risks and set an acceptable target risk level according to the risk management plan, but did not report and seek approval from the executives (e.g., the Chief Information Security Officer).", + "Case 5: The method defined in the internal guidelines for risk assessment differs from the method actually used.", + "Case 6: The organization failed to identify and assess risks in the administrative and physical areas related to the information protection management system, and used only the results of technical vulnerability assessments as the risk assessment outcome.", + "Case 7: The organization set the acceptable target risk level (DoA) unreasonably high, designating risks that required action as acceptable risks, even though these risks were significant and required immediate or short-term action." + ] + } + ] + }, + { + "Id": "1.2.4", + "Name": "Selection of Protective Measures", + "Description": "Based on the results of the risk assessment, appropriate protective measures must be selected to address the identified risks, and an implementation plan including the priority, schedule, responsible department/personnel, and budget for the protective measures must be established and approved by management.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.2. Risk Management", + "Section": "1.2.4 Selection of Protective Measures", + "AuditChecklist": [ + "Has the organization developed risk treatment strategies (e.g., risk reduction, avoidance, transfer, acceptance) and selected protective measures to address the identified risks?", + "Has the organization established and reported to management an implementation plan that includes priority, schedule, responsible department/personnel, and budget for the protective measures?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Information protection and personal information protection implementation plans/risk management plans", + "Information protection and personal information protection measures", + "Information protection and personal information protection master plan", + "Records of management reports and approvals for the information protection and personal information protection implementation plan" + ], + "NonComplianceCases": [ + "Case 1: Although an implementation plan for the information protection and personal information protection measures was established, it was not reported to the CISO and CPO.", + "Case 2: Some risk mitigation actions that were required were missing from the implementation plan.", + "Case 3: Mandatory legal requirements and risks with high security vulnerabilities were accepted without additional protective measures, instead of being addressed by a risk treatment plan.", + "Case 4: The rationale and validity for risk acceptance were insufficient, and some risks that could have been addressed immediately or in the short term due to urgency or ease of implementation were classified under long-term plans without specific justification." + ] + } + ] + }, + { + "Id": "1.3.1", + "Name": "Implementation of Protective Measures", + "Description": "The selected protective measures must be effectively implemented according to the implementation plan, and management must verify the accuracy and effectiveness of the implementation results.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.3. Operation of the Management System", + "Section": "1.3.1 Implementation of Protective Measures", + "AuditChecklist": [ + "Are the protective measures effectively implemented according to the implementation plan, and are the implementation results reported to management to verify their accuracy and effectiveness?", + "Has the organization created and documented detailed operating statements recording the implementation and operation status of protective measures according to the certification standards of the management system?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Information protection and personal information protection implementation plans/risk management plans", + "Information protection and personal information protection measures", + "Information protection and personal information protection implementation progress reports (including reports to management)", + "Information protection and personal information protection implementation completion reports (including reports to management)", + "Information protection and personal information protection operating statements" + ], + "NonComplianceCases": [ + "Case 1: The results of the completion of the information protection and personal information protection measures were not reported to the CISO and CPO.", + "Case 2: The risk action implementation result report indicated 'completed,' but related risks still existed, or the accuracy and effectiveness of the implementation results were not verified.", + "Case 3: Risks classified as medium- to long-term in the previous year's information protection measures implementation plan were not implemented in the current year, or the results were not reviewed and verified by management.", + "Case 4: The actual operating status described in the operating statements did not match reality, and related documents, approvals, and meeting minutes mentioned in the operating statements did not exist.", + "Case 5: Although the implementation results were reported to the CISO and CPO, some incomplete items were not followed up with reasons and corrective actions." + ] + } + ] + }, + { + "Id": "1.3.2", + "Name": "Sharing of Protective Measures", + "Description": "The departments and personnel responsible for the actual operation or implementation of the protective measures must be identified, and the related information must be shared and provided through training to ensure continuous operation.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.3. Operation of the Management System", + "Section": "1.3.2 Sharing of Protective Measures", + "AuditChecklist": [ + "Has the organization clearly identified the departments and personnel responsible for the operation or implementation of the protective measures?", + "Has the organization shared or provided training to the departments and personnel responsible for the operation or implementation of the protective measures?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "List of operating or implementing departments for each protective measure", + "Evidence of internal sharing of information protection and personal information protection plans (e.g., notices, training materials, shared documents)" + ], + "NonComplianceCases": [ + "Case 1: Although protective measures were developed and implemented, the relevant information was not sufficiently shared or provided through training, so the departments or personnel responsible for the actual operation or implementation were unaware of the details." + ] + } + ] + }, + { + "Id": "1.3.3", + "Name": "Operation Status Management", + "Description": "According to the management system established by the organization, operational activities that must be performed continuously or periodically must be recorded and managed in a way that allows identification and tracking, and management must regularly review the effectiveness of operational activities and manage them accordingly.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.3. Operation of the Management System", + "Section": "1.3.3 Operation Status Management", + "AuditChecklist": [ + "Are information protection and personal information protection activities that need to be performed periodically or continuously for the operation of the management system documented and managed?", + "Does management periodically review the effectiveness of the operation of the management system and manage it accordingly?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 31 (Designation of a Personal Information Protection Officer)", + "Information and Communications Network Act, Article 45-3 (Designation of a Chief Information Security Officer, etc.)" + ], + "AuditEvidence": [ + "Annual plan for information protection and personal information protection", + "Operation status report for information protection and personal information protection", + "Results of inspections on the implementation of information protection and personal information protection activities" + ], + "NonComplianceCases": [ + "Case 1: Failure to document activities that are required to be performed periodically or continuously as part of the operation of the information protection and personal information protection management system.", + "Case 2: Although documentation of the operational status of the information protection and personal information protection management system has been completed, periodic reviews have not been conducted, resulting in the omission of some required monthly and quarterly activities, and some activities have not been verified for implementation." + ] + } + ] + }, + { + "Id": "1.4.1", + "Name": "Review of Legal Requirements Compliance", + "Description": "The organization must regularly identify and reflect legal requirements related to information protection and personal information protection and continuously review whether compliance is being maintained.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.4. Inspection and Improvement of the Management System", + "Section": "1.4.1 Review of Legal Requirements Compliance", + "AuditChecklist": [ + "Is the organization regularly identifying and maintaining up-to-date legal requirements related to information protection and personal information protection?", + "Is the organization conducting regular reviews of compliance with legal requirements at least once a year?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Records of legal compliance reviews", + "Records of reviews and revisions of information protection and personal information protection policies and guidelines", + "Comparison tables of revised policies and guidelines", + "Internal sharing documents of legal revisions", + "Proof of personal information liability insurance or equivalent guarantees (e.g., cyber insurance contracts)", + "Information protection disclosure records" + ], + "NonComplianceCases": [ + "Case 1: Although the Information and Communications Network Act and Personal Information Protection Act were recently revised, the organization did not review the impact of the changes on the organization, and as a result, the policy documents, implementation documents, and legal compliance checklists were not updated, leading to inconsistencies between the documents and the law.", + "Case 2: Although legal requirements that the organization must comply with were amended, the organization failed to conduct legal compliance reviews for an extended period.", + "Case 3: Inadequate legal compliance reviews resulted in numerous violations of the Personal Information Protection Act and other regulations.", + "Case 4: The organization was subject to the Personal Information Liability Compensation Guarantee system under the Personal Information Protection Act but failed to recognize this, resulting in non-compliance with insurance or reserve requirements. In cases where insurance was obtained, the organization failed to meet the minimum coverage requirements based on the number of users and revenue.", + "Case 5: Although the organization was required by law to disclose information protection status, it failed to do so within the legally mandated timeframe.", + "Case 6: The organization used a mobile app to receive personal location information from a location-based service provider, but failed to report its location-based service business.", + "Case 7: A foreign personal information controller without a domestic address or business office, whose personal information of domestic subjects stored and managed in the previous three months averaged over one million persons per day, failed to appoint a domestic representative in writing as required." + ] + } + ] + }, + { + "Id": "1.4.2", + "Name": "Management System Audit", + "Description": "The organization must audit its management system at least once a year with a team of personnel who possess independence and expertise, to ensure the system is operating effectively in accordance with internal policies and legal requirements, and report any identified issues to management.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.4. Inspection and Improvement of the Management System", + "Section": "1.4.2 Management System Audit", + "AuditChecklist": [ + "Has the organization established a management system audit plan that includes the criteria, scope, frequency, and qualifications for audit personnel to audit the management system's effectiveness in accordance with legal requirements and established policies?", + "Has the organization conducted audits at least once a year with personnel who have independence, objectivity, and expertise, and reported any identified issues to management?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Management system audit plan (internal audit plan, internal inspection plan)", + "Management system audit report", + "Minutes of the Information Protection and Personal Information Protection Committee" + ], + "NonComplianceCases": [ + "Case 1: The audit team included personnel from the IT department, which was also the subject of the audit, compromising the independence of the audit.", + "Case 2: Although a management system audit was conducted this year, the audit scope was limited to certain areas, failing to cover the full scope of the information protection and personal information protection management system.", + "Case 3: The management system audit team was composed solely of internal staff and external consultants who participated in the development of the management system, compromising the independence of the audit." + ] + } + ] + }, + { + "Id": "1.4.3", + "Name": "Management System Improvement", + "Description": "The root causes of the issues identified during legal compliance reviews and management system inspections must be analyzed, and preventive measures must be established and implemented. The management must confirm the accuracy and effectiveness of the improvement results.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. Establishment and Operation of the Management System", + "Subdomain": "1.4. Management System Inspection and Improvement", + "Section": "1.4.3 Management System Improvement", + "AuditChecklist": [ + "Are the root causes of the issues identified during legal compliance reviews and management system inspections analyzed, and are preventive and improvement measures established and implemented?", + "Are there criteria and procedures in place to verify the accuracy and effectiveness of preventive and improvement results?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Management system inspection result reports", + "Management system inspection action plans and implementation result reports", + "Preventive measures", + "Effectiveness measurement indicators and results (including reports to management)" + ], + "NonComplianceCases": [ + "Case 1: The same issues in the operation of the information protection and personal information protection management system, identified during internal inspections, are repeated each time.", + "Case 2: Although internal regulations require the analysis of root causes and the establishment of preventive measures for issues identified during internal inspections, recent internal inspections failed to include root cause analysis and preventive measures.", + "Case 3: Preventive measures for the issues in the management system were established, and key performance indicators (KPIs) were developed for periodic measurement, but the results were not reported to management for a long period.", + "Case 4: Action plans were not established or the completion of actions was not confirmed for issues identified during management system inspections." + ] + } + ] + }, + { + "Id": "2.1.1", + "Name": "Policy Maintenance", + "Description": "Information protection and personal information protection policies and implementation documents must be periodically reviewed and, if necessary, revised in response to changes in laws and regulations, policies of higher organizations and related agencies, and changes in the internal and external environment. These changes must be documented and tracked.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Control Measures Requirements", + "Subdomain": "2.1. Policies, Organization, and Asset Management", + "Section": "2.1.1 Policy Maintenance", + "AuditChecklist": [ + "Has the organization established and implemented a procedure for regularly reviewing the validity of information protection and personal information protection policies and implementation documents?", + "When there are significant changes in the internal and external environment, are the impacts on information protection and personal information protection policies and implementation documents reviewed and revised as necessary?", + "Are stakeholders consulted when revising information protection and personal information protection policies and implementation documents?", + "Is there a system in place to track the revision history of information protection and personal information protection policies and implementation documents?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Information protection and personal information protection policies and implementation documents (e.g., guidelines, procedures, manuals)", + "Results of regular and ad hoc validity reviews of policies and guidelines", + "Meeting minutes and circulation records with relevant departments regarding policies and guidelines", + "Revision history of policies and guidelines" + ], + "NonComplianceCases": [ + "Case 1: There is inconsistency between password setting rules in guidelines and procedures.", + "Case 2: Information protection activities (e.g., training, encryption, backup) have different targets, frequencies, levels, and methods described in internal regulations, guidelines, and procedures, leading to inconsistency.", + "Case 3: A new database access control solution was introduced to effectively record and manage access and operation logs for the database, but internal security guidelines such as those for security systems and database security management have not been updated to reflect these new controls.", + "Case 4: Although the personal information protection policy was revised, the policy implementation date was not specified, and information such as the author, creation date, and approval date were missing from the relevant policy.", + "Case 5: Although significant changes occurred in laws and regulations related to personal information protection, these changes were not reviewed or reflected in the personal information protection policy and implementation documents." + ] + } + ] + }, + { + "Id": "2.1.2", + "Name": "Organization Maintenance", + "Description": "Roles and responsibilities related to information protection and personal information protection must be assigned to all members of the organization, and systems must be established for evaluating these activities and for communication between members and departments.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Control Measures Requirements", + "Subdomain": "2.1. Policies, Organization, and Asset Management", + "Section": "2.1.2 Organization Maintenance", + "AuditChecklist": [ + "Are the roles and responsibilities of those responsible for and involved in information protection and personal information protection clearly defined?", + "Has the organization established a system for evaluating the activities of those responsible for and involved in information protection and personal information protection?", + "Has the organization established and implemented systems and procedures for communication between the information protection and personal information protection organization and its members?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures), Article 31 (Designation of a Personal Information Protection Officer)", + "Information and Communications Network Act, Article 45-3 (Designation of a Chief Information Security Officer, etc.)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Information protection and personal information protection organization chart", + "Job descriptions for the information protection and personal information protection organization", + "Assignment tables for information protection and personal information protection roles", + "Information protection and personal information protection policies/guidelines and internal management plans", + "Information protection and personal information protection communication management plans", + "Records of communication activities (e.g., monthly/weekly reports, internal notices)", + "Communication channels (e.g., information protection portal, bulletin boards)" + ], + "NonComplianceCases": [ + "Case 1: Although the roles and responsibilities of the CISO, CPO, and related personnel are defined in internal guidelines and job descriptions, they do not align with the actual operating status.", + "Case 2: There are no goals, criteria, or performance indicators in place for the periodic evaluation of the activities of the CISO and related personnel.", + "Case 3: Although internal guidelines require departments to set KPIs related to information protection for the information protection officers in each department to be reflected in performance evaluations, no information protection-related KPIs were set for any of the departmental information protection officers.", + "Case 4: Although the CISO and CPO are designated, the roles and responsibilities required by law are not specifically defined in internal guidelines or job descriptions." + ] + } + ] + }, + { + "Id": "2.1.3", + "Name": "Management of Information Assets", + "Description": "The procedures and protection measures for handling information assets according to their purpose and importance must be established and implemented, and the responsibilities for each asset must be clearly defined and managed.", + "Checks": [ + "organizations_tags_policies_enabled_and_attached", + "organizations_account_part_of_organizations", + "macie_is_enabled", + "config_recorder_all_regions_enabled", + "resourceexplorer2_indexes_found", + "account_maintain_current_contact_details", + "account_maintain_different_contact_details_to_security_billing_and_operations", + "account_security_questions_are_registered_in_the_aws_account", + "account_security_contact_information_is_registered" + ], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.1. Policy, Organization, Asset Management", + "Section": "2.1.3 Management of Information Assets", + "AuditChecklist": [ + "Are handling procedures (creation, introduction, storage, use, disposal) and protection measures defined and implemented according to the security classification of information assets?", + "Have responsible persons and managers been designated for identified information assets?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "List of information assets (designation of responsible persons and managers)", + "Handling procedures for information assets (documents, information systems, etc.)", + "Information asset management system screen", + "Security classification indicators for information assets" + ], + "NonComplianceCases": [ + "Case 1: Although internal guidelines require security classification to be indicated on documents, this has not been followed.", + "Case 2: Responsible persons and managers for information assets have not been identified, or the asset list has not been updated, leading to changes in responsible personnel due to resignations, transfers, etc., not being reflected.", + "Case 3: Although security classifications were assigned to identified information assets after evaluating their importance, handling procedures based on the security classification were not defined." + ] + } + ] + }, + { + "Id": "2.2.1", + "Name": "Designation and Management of Key Personnel", + "Description": "Criteria and management plans for key duties, such as handling personal information and important information or accessing key systems, must be established, and the number of key personnel must be minimized and their list kept up-to-date.", + "Checks": [ + "organizations_delegated_administrators", + "account_security_contact_information_is_registered", + "iam_support_role_created" + ], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.2. Personnel Security", + "Section": "2.2.1 Designation and Management of Key Personnel", + "AuditChecklist": [ + "Are the criteria for key duties, such as handling personal information and important information or accessing key systems, clearly defined?", + "Are employees and external personnel performing key duties designated as key personnel, and is the list kept up-to-date?", + "Are personnel handling personal information designated as personal information handlers, and is the list kept up-to-date?", + "Is the designation of key personnel and personal information handlers minimized based on business needs, and are management plans established and implemented?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 28 (Supervision of Personal Information Handlers), Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Criteria for key duties", + "List of key personnel", + "List of personal information handlers", + "Account and authority management ledger for key information systems and personal information processing systems", + "Management status of key personnel (e.g., training results, security pledges)" + ], + "NonComplianceCases": [ + "Case 1: Although a list of key personnel (e.g., personal information handlers, secret information managers) has been created, some employees who handle large volumes of personal information (e.g., DBAs, DLP managers) were omitted.", + "Case 2: Although the list of key personnel and personal information handlers is being managed, it has not been updated, including resigned employees and newly hired personnel.", + "Case 3: Personal information handler privileges were granted collectively to entire departments, leading to personnel without the need to handle personal information being excessively designated as personal information handlers.", + "Case 4: Although internal guidelines require approval from the security team and the signing of security pledges when granting key personnel privileges, many key personnel were registered without following this process." + ] + } + ] + }, + { + "Id": "2.2.2", + "Name": "Separation of Duties", + "Description": "Criteria for the separation of duties must be established and applied to prevent potential harm from the misuse or abuse of authority. If separation of duties is unavoidable, supplementary measures must be established and implemented.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.2. Personnel Security", + "Section": "2.2.2 Separation of Duties", + "AuditChecklist": [ + "Are criteria for the separation of duties established and applied to prevent potential harm from the misuse or abuse of authority?", + "If separation of duties is difficult, have supplementary controls such as mutual review between personnel, regular monitoring and approval of changes by senior management, and ensuring accountability been established?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Guidelines on the separation of duties (e.g., personnel security guidelines)", + "Job descriptions (e.g., system operation/management, development/operation)", + "Status of supplementary controls when duties are not separated" + ], + "NonComplianceCases": [ + "Case 1: Although the organization has sufficient size and personnel to enable separation of duties, the established internal separation of duties criteria were not followed due to operational convenience.", + "Case 2: Although the organization received approval from senior management to combine development and operation duties due to the organization's characteristics, supplementary control measures such as mutual review between personnel, regular monitoring and review of changes by senior management, and ensuring accountability were not established." + ] + } + ] + }, + { + "Id": "2.2.3", + "Name": "Security Pledge", + "Description": "Employees, temporary staff, or external personnel handling information assets or granted access must sign a security and confidentiality agreement in accordance with internal policies.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.2. Human Security", + "Section": "2.2.3 Security Pledge", + "AuditChecklist": [ + "When hiring new personnel, is there a signed security and personal information protection agreement that specifies their responsibilities?", + "If temporary or external personnel are granted access to information assets, is there a signed agreement outlining their responsibilities for information protection and confidentiality?", + "Upon the resignation of an employee, is a separate confidentiality agreement obtained?", + "Are security, personal information protection, and confidentiality agreements stored safely and managed in a way that they can be easily retrieved when necessary?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Security and personal information protection pledge (for employees and external personnel)", + "Confidentiality agreement (for resigned employees)" + ], + "NonComplianceCases": [ + "Case 1: While it is stipulated that new hires must sign a security pledge, some recently hired employees have not completed the pledge.", + "Case 2: Although employees sign a security pledge, external personnel with direct access to information systems have not signed such an agreement.", + "Case 3: Submitted security and personal information protection pledges are poorly managed, with documents left accessible on desks where unauthorized personnel can access them.", + "Case 4: Although personal information handlers have signed security pledges, the content only covers confidentiality and does not include specific responsibilities related to personal information protection." + ] + } + ] + }, + { + "Id": "2.2.4", + "Name": "Awareness and Training", + "Description": "Organizations must establish and operate an annual awareness and training plan to ensure that employees and related external personnel understand the organization's management system and policies and acquire the necessary job-specific expertise. The effectiveness of this plan must be evaluated and reflected in future plans.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.2. Human Security", + "Section": "2.2.4 Awareness and Training", + "AuditChecklist": [ + "Is an annual training plan approved by management, detailing the timing, duration, target audience, content, and method of information protection and personal information protection training?", + "Are all employees and external personnel within the scope of the management system provided with regular training at least once per year, and are additional training sessions provided when there are significant changes in relevant laws and regulations?", + "Is information protection and personal information protection training provided to new hires and external personnel before they begin their duties?", + "Are IT, information protection, and personal information protection staff receiving specialized training to enhance their job-specific expertise?", + "Are training records maintained, and is the effectiveness of the training evaluated and reflected in future training plans?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 26 (Limitation on the Processing of Personal Information by Outsourcing), Article 28 (Supervision of Personal Information Handlers), Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans)" + ], + "AuditEvidence": [ + "Information protection and personal information protection training plan", + "Training result report", + "General and job-specific training materials", + "List of training attendees" + ], + "NonComplianceCases": [ + "Case 1: Although an annual information protection and personal information protection training plan was established and implemented last year, no such plan was established for the current year without a valid reason.", + "Case 2: The annual information protection and personal information protection training plan includes the frequency and target audience but lacks details such as schedule, content, and method.", + "Case 3: Although the annual training plan includes general personal information awareness training for all employees, it does not include job-specific training for those responsible for personal information protection, such as the personal information protection officer.", + "Case 4: Upon reviewing the training plan and result reports, it was found that certain external contractors (e.g., cleaning staff and security guards who have access to critical facilities within the certification scope) were not included in the training.", + "Case 5: Although information protection and personal information protection training was conducted, some records (e.g., training materials, attendance lists, evaluation surveys, result reports) were not retained.", + "Case 6: There is no system in place to identify employees who did not complete the required training or to provide make-up sessions for them (e.g., additional training, online courses)." + ] + } + ] + }, + { + "Id": "2.2.5", + "Name": "Management of Resignation and Job Changes", + "Description": "Procedures must be established and managed for the return of assets, the revocation or adjustment of accounts and access rights, and confirmation of results when there is a resignation, job change, or leave of absence, involving departments such as HR, information protection, personal information protection, and IT.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.2. Human Security", + "Section": "2.2.5 Management of Resignation and Job Changes", + "AuditChecklist": [ + "Are personnel changes (e.g., resignation, job changes, department transfers, leave of absence) shared among HR, information protection, personal information protection, and IT system operations departments?", + "Are procedures in place and implemented to promptly return information assets, revoke or adjust access rights, and confirm results when an employee (including temporary staff and external contractors) resigns or changes roles?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 5 (Management of Access Rights)" + ], + "AuditEvidence": [ + "Procedures for resignation and job changes", + "Asset (account) return management ledger upon resignation", + "Security checklists and inspection records for resigned employees" + ], + "NonComplianceCases": [ + "Case 1: Accounts and access rights for personnel no longer handling personal information due to job changes remain active in the personal information processing system.", + "Case 2: No records of asset returns or access rights revocation procedures were found for recently resigned key personnel and personal information handlers.", + "Case 3: While asset returns are properly managed for resigned employees, the security check and resignation confirmation forms required by HR regulations are not being completed.", + "Case 4: Although access rights to personal information processing systems were revoked promptly upon the resignation of personal information handlers, access rights to systems like physical access control and VPN were not revoked in a timely manner." + ] + } + ] + }, + { + "Id": "2.2.6", + "Name": "Actions in Case of Security Violations", + "Description": "In the event that employees or relevant external parties violate laws, regulations, or internal policies, the organization must establish and implement procedures to take appropriate actions.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.2. Human Security", + "Section": "2.2.6 Actions in Case of Security Violations", + "AuditChecklist": [ + "Has the organization established disciplinary measures for employees and relevant external parties in case of violations of information protection and personal information protection responsibilities and obligations under laws, regulations, and internal policies?", + "When violations of information protection and personal information protection are detected, are actions taken in accordance with internal procedures?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "HR regulations (disciplinary measures for violations of information protection and personal information protection regulations)", + "Records of disciplinary actions for violations of information protection and personal information protection guidelines", + "Incident case studies (company-wide notices, training materials)" + ], + "NonComplianceCases": [ + "Case 1: No disciplinary measures or procedures are included in internal regulations for handling violations of information protection and personal information protection regulations.", + "Case 2: Although warning messages are sent to those who violate policies detected by security systems (e.g., DLP, database access control system, internal information leakage control system), follow-up actions such as explanations, additional investigations, or disciplinary actions are not carried out in accordance with internal regulations." + ] + } + ] + }, + { + "Id": "2.3.1", + "Name": "Management of External Parties", + "Description": "When outsourcing part of the work (e.g., handling personal information, information protection, operating or developing information systems) or using external facilities or services (e.g., data centers, cloud services, application services), the organization must identify the current status, understand the legal requirements and risks arising from external organizations or services, and establish appropriate protective measures.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.3. External Security", + "Section": "2.3.1 Management of External Parties", + "AuditChecklist": [ + "Has the organization identified the status of outsourcing and the use of external facilities and services within the scope of the management system?", + "Has the organization identified the legal requirements and risks associated with outsourcing and the use of external facilities and services, and established appropriate protective measures?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 26 (Restrictions on the Processing of Personal Information by Outsourcing)", + "Information and Communications Network Act, Article 50-3 (Entrustment of the Transmission of Commercial Information for Profit)" + ], + "AuditEvidence": [ + "List of outsourced services and external facilities/services", + "Outsourcing contracts", + "Risk analysis reports and protective measures", + "Outsourcing security management guidelines, checklists, etc." + ], + "NonComplianceCases": [ + "Case 1: Although the organization manages a list of outsourced services and external facilities/services as required by internal regulations, the list is outdated and does not reflect changes made to vendors several months ago.", + "Case 2: The organization has migrated some personal information processing systems to external cloud services within the scope of the management system, but no identification or risk assessment has been performed." + ] + } + ] + }, + { + "Id": "2.3.2", + "Name": "Security in Contracts with External Parties", + "Description": "When using external services or outsourcing work to external parties, the organization must identify the information protection and personal information protection requirements and specify them in contracts or agreements.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.3. External Security", + "Section": "2.3.2 Security in Contracts with External Parties", + "AuditChecklist": [ + "When selecting external services or outsourcing vendors related to the handling of important information and personal information, does the organization follow procedures to consider the vendors' capabilities in information protection and personal information protection?", + "Has the organization identified the information protection and personal information protection requirements associated with the use of external services and outsourcing, and specified them in contracts or agreements?", + "When outsourcing the development of information systems and personal information processing systems, has the organization specified the information protection and personal information protection requirements that must be followed during development in the contract?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 26 (Restrictions on the Processing of Personal Information by Outsourcing)" + ], + "AuditEvidence": [ + "Outsourcing contracts", + "Information protection and personal information protection agreements (agreements, annexes)", + "Internal guidelines on outsourcing", + "RFPs (Requests for Proposals), evaluation forms related to the selection of outsourcing vendors" + ], + "NonComplianceCases": [ + "Case 1: No outsourcing contract exists for external vendors performing IT operations, development, or personal information processing tasks.", + "Case 2: The outsourcing contract with an external vendor handling personal information does not include some items required by the Personal Information Protection Act (e.g., management and supervision provisions).", + "Case 3: Although infrastructure operation and part of personal information processing tasks are outsourced to external vendors, the contract does not specify security requirements related to the nature of the outsourced work, but only includes general provisions on confidentiality and liability for damages." + ] + } + ] + }, + { + "Id": "2.3.3", + "Name": "External Party Security Implementation Management", + "Description": "Security measures specified in contracts, agreements, and internal policies must be regularly inspected or audited to ensure external parties comply with information protection and personal information protection requirements.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.3. External Party Security", + "Section": "2.3.3 External Party Security Implementation Management", + "AuditChecklist": [ + "Are periodic inspections or audits conducted to ensure external parties comply with information protection and personal information protection requirements specified in contracts, agreements, and internal policies?", + "When issues are identified during inspections or audits of external parties, are improvement plans established and implemented?", + "If a contractor entrusted with personal information processing re-outsources related tasks to a third party, does the contractor obtain the principal's consent?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 26 (Restrictions on the Outsourcing of Personal Information Processing)", + "Information and Communications Network Act, Article 50-3 (Outsourcing of the Transmission of Commercial Information for Profit)" + ], + "AuditEvidence": [ + "Security inspection results for external parties and contractors", + "Training details for external parties and contractors (training outcomes, attendee list, training materials, etc.)", + "Personal information outsourcing contract", + "Evidence of consent for re-outsourcing of personal information processing tasks" + ], + "NonComplianceCases": [ + "Case 1: Failure to regularly conduct security inspections of external contractors who perform IT development and operations tasks on-site.", + "Case 2: Sending a notification to contractors entrusted with personal information processing to conduct security training, but failing to verify whether the training has been conducted.", + "Case 3: Allowing contractors to perform their own security inspections and report the results, without a verification process to ensure the inspections were properly conducted, thus undermining the reliability of the inspection results.", + "Case 4: Allowing contractors to re-outsource personal information processing tasks to a third party without the principal's consent.", + "Case 5: Failure to supervise contractors entrusted with transmitting commercial information for profit." + ] + } + ] + }, + { + "Id": "2.3.4", + "Name": "Security for External Party Contract Changes and Expiry", + "Description": "When an external party contract expires, the task is completed, or there is a personnel change, security measures such as returning provided information assets, deleting information system access accounts, destroying sensitive information, and obtaining confidentiality agreements for acquired information must be implemented.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.3. External Party Security", + "Section": "2.3.4 Security for External Party Contract Changes and Expiry", + "AuditChecklist": [ + "Has the organization established and implemented security measures to ensure the return of information assets, deletion of information system access accounts, and the acquisition of confidentiality agreements in accordance with official procedures when an external party contract expires, a task is completed, or there is a personnel change?", + "When an external party contract expires, has the organization established and implemented procedures to confirm whether the external party holds any sensitive or personal information related to the outsourced task, and to retrieve or destroy such information?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 26 (Restrictions on the Outsourcing of Personal Information Processing)", + "Information and Communications Network Act, Article 50-3 (Outsourcing of the Transmission of Commercial Information for Profit)" + ], + "AuditEvidence": [ + "Information protection and personal information protection agreements", + "Confidentiality agreements", + "Information and personal information destruction agreements", + "Internal policies and guidelines related to the termination of external party contracts" + ], + "NonComplianceCases": [ + "Case 1: Failure to delete accounts and permissions for external parties after their contract has expired, allowing access to certain information systems.", + "Case 2: During an outsourcing project, failure to take appropriate measures for some contractors who were replaced or whose contracts expired, including failing to obtain security agreements as required by internal regulations.", + "Case 3: After terminating a contract with a contractor entrusted with personal information processing, failure to verify whether the contractor destroyed any personal information they held." + ] + } + ] + }, + { + "Id": "2.4.1", + "Name": "Designation of Protected Zones", + "Description": "To protect personal and sensitive information, documents, storage media, key facilities, and systems from physical and environmental threats, physical protection zones such as controlled areas, restricted areas, and reception areas must be designated, and protection measures for each zone must be established and implemented.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.4. Physical Security", + "Section": "2.4.1 Designation of Protected Zones", + "AuditChecklist": [ + "Has the organization established criteria for designating physical protection zones such as controlled areas, restricted areas, and reception areas to protect personal and sensitive information, documents, storage media, key facilities, and systems from physical and environmental threats?", + "Has the organization designated physical protection zones in accordance with the criteria and established and implemented protection measures for each zone?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 10 (Physical Safety Measures)" + ], + "AuditEvidence": [ + "Physical security guidelines (criteria for designating protected zones)", + "List of designated protected zones", + "Protected zone signage", + "List of protection measures for each zone" + ], + "NonComplianceCases": [ + "Case 1: Although internal physical security guidelines state that areas where personal information is stored and processed must be designated as controlled zones, certain document storage rooms containing membership application forms were omitted from the list of controlled zones.", + "Case 2: Internal physical security guidelines require that controlled zones be marked with specific signs, but some controlled zones do not have the required signage." + ] + } + ] + }, + { + "Id": "2.4.2", + "Name": "Access Control", + "Description": "Access to protected areas must be restricted to authorized personnel only, and entry and access logs should be reviewed periodically to ensure accountability.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protective Measures Requirements", + "Subdomain": "2.4. Physical Security", + "Section": "2.4.2 Access Control", + "AuditChecklist": [ + "Is access to protected areas controlled so that only authorized personnel are allowed to enter according to access procedures?", + "Are entry records for internal and external personnel for each protected area retained for a certain period, and are entry records and access permissions reviewed periodically?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 10 (Physical Safety Measures)" + ], + "AuditEvidence": [ + "Access logbook and entry logs", + "Access registration application form and approval records", + "Entry record review report", + "Access control system management screen (status of registered personnel, etc.)" + ], + "NonComplianceCases": [ + "Case 1: Although control areas are defined, protective measures are established, and employees with access are managed, the entry records are not reviewed periodically, resulting in many inactive personnel (due to retirement, transfer, etc.) having long periods of no entry.", + "Case 2: Although access control devices are installed in controlled areas such as data centers and document storage rooms, they are left open for extended periods without valid reasons or approval.", + "Case 3: Some external partner employees are excessively granted all-area access cards for unrestricted entry." + ] + } + ] + }, + { + "Id": "2.4.3", + "Name": "Information System Protection", + "Description": "Information systems should be arranged considering their importance and characteristics to reduce environmental threats, harmful factors, and unauthorized access, and communication and power cables should be protected from damage.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protective Measures Requirements", + "Subdomain": "2.4. Physical Security", + "Section": "2.4.3 Information System Protection", + "AuditChecklist": [ + "Are information systems placed in separated locations based on their importance, usage, and characteristics?", + "Are there measures in place to easily verify the actual physical location of the information systems?", + "Are power and communication cables protected from physical damage and electrical interference from external sources?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Data processing facility diagram", + "Information system layout", + "Asset list" + ], + "NonComplianceCases": [ + "Case 1: The system layout is not updated to reflect the latest changes, making it difficult to quickly identify the information system that has experienced a failure.", + "Case 2: Many cables are tangled and not properly organized on the server room floor or in racks, increasing the risk of failure due to electrical interference, damage, leakage, or negligence." + ] + } + ] + }, + { + "Id": "2.4.4", + "Name": "Operation of Protective Facilities", + "Description": "Based on the importance and characteristics of the information systems located in protected areas, protective facilities such as temperature and humidity control, fire detection, firefighting equipment, leak detection, UPS, emergency generators, and dual power lines should be established and operated according to operational procedures.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protective Measures Requirements", + "Subdomain": "2.4. Physical Security", + "Section": "2.4.4 Operation of Protective Facilities", + "AuditChecklist": [ + "Are necessary facilities established and operational procedures set up based on the importance and characteristics of each protected area to prevent disasters such as fire, flood, and power failure caused by human error or natural disasters?", + "If operating outsourced integrated data centers (IDC), are physical security requirements included in the contract, and is the operational status periodically reviewed?" + ], + "RelatedRegulations": [ + "Information and Communications Network Act, Article 46 (Protection of Integrated Data Centers)", + "Guidelines for the Protection of Integrated Data Centers", + "Fire Facility Installation and Management Act, Article 12 (Management of Fire Protection Facilities in Specific Fire Protection Objects), Article 16 (Management of Evacuation Facilities, Fire Zones, and Fire Protection Facilities)" + ], + "AuditEvidence": [ + "Physical security guidelines (related to protective facilities)", + "Data center facility status and inspection checklist", + "IDC outsourcing contract, SLA, etc." + ], + "NonComplianceCases": [ + "Case 1: In some protected areas, such as the main office data center, the required protective facilities specified in internal guidelines are not installed.", + "Case 2: Although protective facilities such as UPS and fire suppression systems are in place in the data center, operational and inspection standards for the related facilities are not established.", + "Case 3: Although temperature and humidity control devices were installed in the data center according to operational guidelines, insufficient capacity means that the standard temperature and humidity levels are not maintained, increasing the risk of failure." + ] + } + ] + }, + { + "Id": "2.4.5", + "Name": "Operations in Secure Zones", + "Description": "Procedures to prevent unauthorized actions and abuse of privileges within secure zones must be established and implemented, and the records of operations should be periodically reviewed.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.4. Physical Security", + "Section": "2.4.5 Operations in Secure Zones", + "AuditChecklist": [ + "When operations within secure zones, such as the introduction and maintenance of information systems, are required, are formal procedures for application and execution of such operations established and implemented?", + "Are the records of operations within secure zones periodically reviewed to confirm that the operations were carried out in accordance with the control procedures?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Operation request forms, operation logs", + "Entry logs for controlled areas", + "Records of review of entry and operation logs for controlled areas" + ], + "NonComplianceCases": [ + "Case 1: The entry log of the data center shows the presence of external maintenance personnel, but there is no record of an operation request or approval for work within the secure zone (i.e., entry and work in the secure zone were carried out without an operation request as required by internal regulations).", + "Case 2: Although internal regulations state that the records of operations within secure zones must be reviewed at least once per quarter, the review of such records has not been conducted for a long period without a valid reason." + ] + } + ] + }, + { + "Id": "2.4.6", + "Name": "Device Control for Inbound and Outbound", + "Description": "Procedures to control the inbound and outbound movement of information systems, mobile devices, storage media, etc., within secure zones must be established, implemented, and periodically reviewed.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.4. Physical Security", + "Section": "2.4.6 Device Control for Inbound and Outbound", + "AuditChecklist": [ + "Are control procedures established and implemented to prevent security incidents such as information leakage and malware infection when information systems, mobile devices, storage media, etc., are brought into or taken out of secure zones?", + "Are records maintained and managed in accordance with the inbound and outbound control procedures, and is the compliance with the procedures periodically checked by reviewing the history of inbound and outbound activities?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 10 (Physical Safety Measures)" + ], + "AuditEvidence": [ + "Inbound and outbound application forms for secure zones", + "Inbound and outbound management logs", + "Results of the review of inbound and outbound records" + ], + "NonComplianceCases": [ + "Case 1: Although control procedures for the inbound and outbound of mobile computing devices are established, there is no control over the movement of such devices within the controlled area, allowing both internal and external personnel with access to the controlled area to use mobile computing devices without restriction.", + "Case 2: Although internal guidelines state that inbound and outbound details of IT equipment must be recorded in the operation plan and signed by the person responsible for management, many signatures of responsible managers are missing from the records." + ] + } + ] + }, + { + "Id": "2.4.7", + "Name": "Work Environment Security", + "Description": "Protection measures such as clean desk policies and regular inspections must be established and implemented to prevent unauthorized exposure or leakage of personal and sensitive information through shared office equipment (e.g., document storage, shared PCs, multifunction printers, file servers) and individual work environments (e.g., work PCs, desks).", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.4. Physical Security", + "Section": "2.4.7 Work Environment Security", + "AuditChecklist": [ + "Are protection measures established and implemented for shared facilities and office equipment such as document storage, shared PCs, multifunction printers, file servers, etc.?", + "Are protection measures established and implemented to prevent the exposure or leakage of personal and sensitive information through individual work environments such as work PCs, desks, drawers, etc.?", + "Are appropriate protection measures in place to ensure the safe handling of printed or copied materials containing personal information, such as paper documents?", + "Is compliance with information protection requirements in both individual and shared work environments periodically reviewed?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 10 (Physical Safety Measures), Article 12 (Safety Measures for Printing and Copying)" + ], + "AuditEvidence": [ + "Security inspection reports for offices and shared spaces", + "Security inspection checklists for offices and shared spaces", + "Actions taken for non-compliance (e.g., training, rewards and penalties)", + "Current status of protection measures for printed and copied materials" + ], + "NonComplianceCases": [ + "Case 1: Although the internal management plan for personal information specifies that regular security inspections (e.g., clean desk policies) must be conducted, no such inspections have been carried out.", + "Case 2: Documents containing personal information, such as membership application forms, are stored in an office cabinet without a lock.", + "Case 3: Employee computers do not have screen savers or passwords set, and important documents have been left on vacationing employees' desks for an extended period.", + "Case 4: No protection measures are in place for shared PCs installed in shared office spaces such as meeting rooms, resulting in personal information files being stored unencrypted, or security updates not applied, or antivirus software not installed, leaving the systems vulnerable." + ] + } + ] + }, + { + "Id": "2.5.1", + "Name": "User Account Management", + "Description": "To control unauthorized access to information systems, personal information, and critical information, organizations must establish and implement procedures for user registration, termination, and granting, changing, or revoking access rights, ensuring that access rights are granted only to the minimum necessary for work purposes. Additionally, when registering or granting user rights, it must be made clear to users that they are responsible for the security of their accounts.", + "Checks": [ + "organizations_scp_check_deny_regions", + "cognito_user_pool_self_registration_disabled", + "cloudtrail_threat_detection_privilege_escalation", + "iam_user_administrator_access_policy", + "iam_customer_unattached_policy_no_administrative_privileges", + "iam_inline_policy_allows_privilege_escalation", + "iam_inline_policy_no_administrative_privileges", + "iam_aws_attached_policy_no_administrative_privileges", + "iam_policy_allows_privilege_escalation", + "iam_policy_no_full_access_to_kms", + "iam_role_cross_service_confused_deputy_prevention", + "iam_inline_policy_no_full_access_to_cloudtrail", + "iam_role_cross_account_readonlyaccess_policy", + "iam_inline_policy_no_full_access_to_kms", + "iam_user_accesskey_unused", + "iam_securityaudit_role_created", + "iam_no_custom_policy_permissive_role_assumption", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_group_administrator_access_policy", + "iam_user_console_access_unused", + "iam_policy_attached_only_to_group_or_roles", + "iam_policy_no_full_access_to_cloudtrail", + "iam_role_administratoraccess_policy" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.5. Authentication and Access Management", + "Section": "2.5.1 User Account Management", + "AuditChecklist": [ + "Has the organization established and implemented formal procedures for registering, changing, and deleting user accounts and access rights to information systems, personal information, and critical information?", + "When creating and registering user accounts and access rights to information systems, personal information, and critical information, is access limited to the minimum necessary for each job based on the job-specific access classification system?", + "When granting users accounts and access rights, are they made fully aware that they are responsible for the security of those accounts?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 5 (Management of Access Rights)" + ], + "AuditEvidence": [ + "User account and access request forms", + "User account and access management log or screen", + "Access classification table for information systems and personal information processing systems", + "Lists of users, administrators, and personal information handlers for each information system and personal information processing system" + ], + "NonComplianceCases": [ + "Case 1: User registration, termination, and approval procedures for accounts and permissions for users and personal information handlers were processed through verbal requests, email, etc., without proper approval and handling records.", + "Case 2: A personal information handler shared their account with an unauthorized individual for backup purposes during vacations, business trips, or other absences without going through official procedures.", + "Case 3: Users of information systems or personal information processing systems were granted excessive permissions, allowing access to unnecessary information or personal data." + ] + } + ] + }, + { + "Id": "2.5.2", + "Name": "User Identification", + "Description": "User accounts must be assigned unique identifiers that distinguish each user individually, and the use of easily guessable identifiers must be restricted. If the same identifier is shared by multiple users, the reason and justification must be reviewed, supplementary measures such as approval from a responsible party must be established, and accountability must be ensured.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.5. Authentication and Access Management", + "Section": "2.5.2 User Identification", + "AuditChecklist": [ + "Are unique identifiers assigned to users and personal information handlers in information systems and personal information processing systems, and is the use of easily guessable identifiers restricted?", + "If the same identifier is shared by multiple users for unavoidable reasons, has the justification been reviewed and have supplementary measures such as approval from the responsible party been established?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 5 (Management of Access Rights)" + ], + "AuditEvidence": [ + "Login screen for information systems and personal information processing systems", + "Lists of administrators, users, and personal information handlers for information systems and personal information processing systems", + "Records of approvals for exceptions" + ], + "NonComplianceCases": [ + "Case 1: The account status of information systems (servers, networks, firewalls, DBMS, etc.) shows that default administrator accounts provided by the manufacturer are still in use, despite being technically modifiable.", + "Case 2: Developers are sharing personal information processing system accounts for common use without any justification or approval from responsible parties.", + "Case 3: External personnel maintaining information systems are using operational accounts like personal accounts without going through the required approval procedures." + ] + } + ] + }, + { + "Id": "2.5.3", + "Name": "User Authentication", + "Description": "User access to information systems, personal information, and critical information must be secured through safe authentication procedures and, if necessary, enhanced authentication methods. In addition, access control measures such as limiting login attempts and issuing warnings for illegal login attempts must be established and implemented.", + "Checks": [ + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", + "cognito_identity_pool_guest_access_disabled", + "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", + "cognito_user_pool_advanced_security_enabled", + "cognito_user_pool_client_token_revocation_enabled", + "cognito_user_pool_client_prevent_user_existence_errors", + "cognito_user_pool_mfa_enabled", + "cognito_user_pool_self_registration_disabled", + "opensearch_service_domains_internal_user_database_enabled", + "opensearch_service_domains_use_cognito_authentication_for_kibana", + "directoryservice_supported_mfa_radius_enabled", + "iam_rotate_access_key_90_days", + "iam_root_hardware_mfa_enabled", + "iam_user_no_setup_initial_access_key", + "iam_root_mfa_enabled", + "iam_user_with_temporary_credentials", + "iam_user_hardware_mfa_enabled", + "iam_check_saml_providers_sts", + "iam_user_two_active_access_key", + "iam_user_accesskey_unused", + "iam_user_mfa_enabled_console_access", + "iam_administrator_access_with_mfa", + "apigatewayv2_api_authorizers_enabled", + "kafka_cluster_unrestricted_access_disabled", + "rds_cluster_iam_authentication_enabled", + "rds_instance_iam_authentication_enabled", + "apigateway_restapi_authorizers_enabled" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.5. Authentication and Authorization Management", + "Section": "2.5.3 User Authentication", + "AuditChecklist": [ + "Is access to information systems and personal information processing systems controlled through secure user authentication procedures, login attempt limitations, and warnings for illegal login attempts?", + "When accessing personal information processing systems from outside via a communication network, are secure authentication methods or secure access measures applied in accordance with legal requirements?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 5 (Management of Access Rights), Article 6 (Access Control)" + ], + "AuditEvidence": [ + "Login screen for information systems and personal information processing systems", + "Login attempt limitation setting screen", + "Login failure message screen", + "Procedures for external access (e.g., external access request forms, list of external accessors)" + ], + "NonComplianceCases": [ + "Case 1: When a personal information handler accesses a personal information processing system through the public external internet, secure authentication methods are not applied, and authentication is done only through ID and password.", + "Case 2: In the login process for information systems and personal information processing systems, detailed messages are displayed about whether the ID exists or the password is incorrect, and there is no limit on login failure attempts." + ] + } + ] + }, + { + "Id": "2.5.4", + "Name": "Password Management", + "Description": "Procedures for managing passwords used by users of information systems, as well as customers and members, must be established and implemented, taking into account legal requirements and external threats.", + "Checks": [ + "cognito_user_pool_temporary_password_expiration", + "cognito_user_pool_password_policy_uppercase", + "cognito_user_pool_password_policy_number", + "cognito_user_pool_password_policy_lowercase", + "cognito_user_pool_password_policy_minimum_length_14", + "cognito_user_pool_password_policy_symbol", + "iam_password_policy_number", + "iam_password_policy_minimum_length_14", + "iam_password_policy_expires_passwords_within_90_days_or_less", + "iam_password_policy_symbol", + "iam_password_policy_reuse_24", + "iam_password_policy_lowercase", + "iam_password_policy_uppercase" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.5. Authentication and Authorization Management", + "Section": "2.5.4 Password Management", + "AuditChecklist": [ + "Are procedures for managing and creating secure user passwords for information systems established and implemented?", + "Are password creation rules established and enforced to ensure that users can use secure passwords?", + "Are authentication methods for personal information handlers and users securely applied and managed?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 5 (Management of Access Rights)" + ], + "AuditEvidence": [ + "Password setting screens for web pages, information systems, and personal information processing systems", + "Password management policies and procedures" + ], + "NonComplianceCases": [ + "Case 1: Although password creation rules are set in policies and guidelines related to information protection and personal information protection, some information systems and personal information processing systems use passwords that differ from internal guidelines.", + "Case 2: Internal regulations state that when passwords are reset, temporary passwords must be assigned and forced to be changed, but in practice, temporary passwords are being used without change.", + "Case 3: Although internal regulations require users and personal information handlers to change their passwords periodically, passwords are being used without change." + ] + } + ] + }, + { + "Id": "2.5.5", + "Name": "Management of Special Accounts and Privileges", + "Description": "Accounts and privileges used for special purposes, such as managing information systems, personal information, and important information, should be granted minimally, separately identified, and controlled.", + "Checks": [ + "organizations_delegated_administrators", + "cloudwatch_log_metric_filter_root_usage", + "iam_root_hardware_mfa_enabled", + "iam_root_mfa_enabled", + "iam_support_role_created", + "iam_avoid_root_usage", + "iam_no_root_access_key", + "rds_instance_default_admin", + "rds_cluster_default_admin", + "sagemaker_notebook_instance_root_access_disabled", + "ec2_instance_profile_attached" + ], + "Attributes": [ + { + "Domain": "2. Protection Measures Requirements", + "Subdomain": "2.5. Authentication and Privilege Management", + "Section": "2.5.5 Management of Special Accounts and Privileges", + "AuditChecklist": [ + "Is there a formal privilege request and approval process established and implemented to ensure that special privileges, such as administrative privileges, are only granted to a minimal number of people?", + "Is there a control procedure established and implemented to identify and manage accounts and privileges granted for special purposes in a separate list?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 5 (Management of Access Rights)" + ], + "AuditEvidence": [ + "Guidelines related to special privileges", + "Records of special privilege requests and approvals", + "List of special privilege holders", + "Records of special privilege reviews" + ], + "NonComplianceCases": [ + "Case 1: The approval history for granting administrator and special privileges in the information system and personal information processing system is not documented or does not match the special privileges list.", + "Case 2: Internal regulations require that personal information administrators and special privilege holders be documented and managed in a list, but the list is not maintained or some special privileges, such as security system administrators, are not identified or managed.", + "Case 3: A maintenance special account for visiting maintenance once a quarter remains active at all times without a time limit on usage.", + "Case 4: Regular reviews of administrator and special privilege usage are not conducted, and some individuals retain special privileges even after their roles have changed." + ] + } + ] + }, + { + "Id": "2.5.6", + "Name": "Review of Access Rights", + "Description": "The registration, use, and deletion of user accounts accessing information systems, personal information, and important information, as well as the history of granting, changing, and deleting access rights, should be recorded and periodically reviewed to ensure their appropriateness.", + "Checks": [ + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_cross_account_sharing_disabled", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_policy_changes", + "accessanalyzer_enabled_without_findings", + "accessanalyzer_enabled", + "cloudtrail_insights_exist", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_cloudwatch_logging_enabled" + ], + "Attributes": [ + { + "Domain": "2. Protection Measures Requirements", + "Subdomain": "2.5. Authentication and Privilege Management", + "Section": "2.5.6 Review of Access Rights", + "AuditChecklist": [ + "Are the histories of account and access right creation, registration, granting, use, modification, and deletion for information systems, personal information, and important information being recorded?", + "Are standards, review subjects, review methods, and periodic review schedules established to regularly review the appropriateness of user accounts and access rights to information systems, personal information, and important information?", + "When issues such as excessive access rights, failure to follow access right granting procedures, or misuse of access rights are identified in the review results, are appropriate response procedures established and implemented?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 5 (Management of Access Rights)" + ], + "AuditEvidence": [ + "Access rights review standards and procedures", + "History of access rights reviews", + "Access rights review reports and follow-up action records" + ], + "NonComplianceCases": [ + "Case 1: The methods, review periods, reporting structure, and misuse criteria related to access rights reviews are not clearly defined in the relevant guidelines, leading to irregular performance of access rights reviews.", + "Case 2: Although internal policies and guidelines require locking (deactivating) or deleting long-unused accounts, some accounts that have not been accessed for more than six months remain active (indicating that the access rights review was not thoroughly conducted, failing to identify these accounts).", + "Case 3: During the access rights review, cases of excessive privileges or suspected misuse were identified, but no detailed investigation, internal reporting, or follow-up actions were taken." + ] + } + ] + }, + { + "Id": "2.6.1", + "Name": "Network Access", + "Description": "In order to control unauthorized access to the network, management procedures such as IP management and device authentication must be established and implemented. Network segmentation (DMZ, server farm, DB zone, development zone, etc.) and access controls must be applied according to the business purpose and importance.", + "Checks": [ + "ssm_documents_set_as_public", + "s3_access_point_public_access_block", + "s3_bucket_public_write_acl", + "s3_bucket_acl_prohibited", + "s3_account_level_public_access_blocks", + "s3_bucket_level_public_access_block", + "emr_cluster_account_public_block_enabled", + "emr_cluster_master_nodes_no_public_ip", + "emr_cluster_publicly_accesible", + "documentdb_cluster_public_snapshot", + "sns_topics_not_publicly_accessible", + "cognito_identity_pool_guest_access_disabled", + "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", + "cloudfront_distributions_geo_restrictions_enabled", + "opensearch_service_domains_not_publicly_accessible", + "glacier_vaults_policy_public_access", + "lightsail_instance_public", + "lightsail_database_public", + "neptune_cluster_uses_public_subnet", + "neptune_cluster_public_snapshot", + "ecr_repositories_not_publicly_accessible", + "workspaces_vpc_2private_1public_subnets_nat", + "elb_internet_facing", + "sqs_queues_not_publicly_accessible", + "redshift_cluster_public_access", + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "awslambda_function_url_public", + "awslambda_function_url_cors_policy", + "awslambda_function_not_publicly_accessible", + "iam_user_administrator_access_policy", + "iam_group_administrator_access_policy", + "iam_user_mfa_enabled_console_access", + "networkfirewall_in_all_vpc", + "eks_cluster_network_policy_enabled", + "eks_control_plane_endpoint_access_restricted", + "eks_cluster_private_nodes_enabled", + "eks_endpoints_not_publicly_accessible", + "kafka_cluster_is_public", + "kafka_cluster_unrestricted_access_disabled", + "vpc_peering_routing_tables_with_least_privilege", + "vpc_subnet_no_public_ip_by_default", + "vpc_endpoint_services_allowed_principals_trust_boundaries", + "vpc_endpoint_connections_trust_boundaries", + "vpc_subnet_separate_private_public", + "dms_instance_no_public_access", + "elbv2_internet_facing", + "elbv2_listeners_underneath", + "elasticache_cluster_uses_public_subnet", + "appstream_fleet_maximum_session_duration", + "appstream_fleet_default_internet_access_disabled", + "appstream_fleet_session_idle_disconnect_timeout", + "appstream_fleet_session_disconnect_timeout", + "rds_snapshots_public_access", + "rds_instance_no_public_access", + "rds_instance_event_subscription_security_groups", + "rds_instance_iam_authentication_enabled", + "apigateway_restapi_public", + "apigateway_restapi_authorizers_enabled", + "apigateway_restapi_public_with_authorizer", + "sagemaker_training_jobs_network_isolation_enabled", + "sagemaker_models_vpc_settings_configured", + "sagemaker_training_jobs_vpc_settings_configured", + "sagemaker_models_network_isolation_enabled", + "sagemaker_notebook_instance_without_direct_internet_access_configured", + "sagemaker_notebook_instance_vpc_settings_configured", + "ec2_instance_port_ldap_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_instance_port_kerberos_exposed_to_internet", + "ec2_securitygroup_not_used", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", + "ec2_securitygroup_default_restrict_traffic", + "ec2_instance_port_cifs_exposed_to_internet", + "ec2_instance_port_kafka_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", + "ec2_instance_port_mysql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432", + "ec2_elastic_ip_shodan", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_instance_port_cassandra_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", + "ec2_instance_port_memcached_exposed_to_internet", + "ec2_networkacl_allow_ingress_any_port", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", + "ec2_ebs_public_snapshot", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_transitgateway_auto_accept_vpc_attachments", + "ec2_instance_port_postgresql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", + "ec2_securitygroup_with_many_ingress_egress_rules", + "ec2_ami_public", + "ec2_instance_port_oracle_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", + "ec2_instance_imdsv2_enabled", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", + "ec2_instance_port_redis_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", + "ec2_instance_port_ftp_exposed_to_internet", + "ec2_instance_public_ip", + "ec2_networkacl_allow_ingress_tcp_port_22", + "ec2_instance_port_mongodb_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_instance_port_telnet_exposed_to_internet" + ], + "Attributes": [ + { + "Domain": "2. Control Measures", + "Subdomain": "2.6. Access Control", + "Section": "2.6.1 Network Access", + "AuditChecklist": [ + "Has the organization identified all access paths to its network and ensured that internal networks are controlled so that only authorized users can access them according to the access control policy?", + "Has the organization physically or logically segmented the network based on services, user groups, information asset importance, and legal requirements, and applied access control between different network segments?", + "Has the organization established IP address allocation standards for each network segment, and applied measures such as assigning private IPs to systems like database servers that do not require external connections?", + "Has the organization implemented protective measures for communication paths when connecting networks between physically separated locations, such as IDCs, branches, and agents?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 6 (Access Control)" + ], + "AuditEvidence": [ + "Network diagram", + "IP management ledger", + "Information asset list", + "Firewall rules" + ], + "NonComplianceCases": [ + "Case 1: The network configuration and interviews revealed that data transmission and reception between external sites and the servers located in the IDC are being processed through the general internet line, rather than using VPN or dedicated lines as specified in internal regulations.", + "Case 2: The IP addresses of some important servers, such as database servers located in the internal network, were set to public IPs instead of private IPs as per internal regulations, and network access blocking was not applied.", + "Case 3: Although a server farm was established, access from the internal network to the server farm was excessively allowed due to insufficient network access control settings.", + "Case 4: The network provided to external parties (e.g., external developers, visitors) was not separated from the internal business network without appropriate controls.", + "Case 5: Contrary to internal regulations, the organization's network could be accessed and used simply by connecting a network cable without applying protective measures such as MAC address authentication and mandatory security software installation." + ] + } + ] + }, + { + "Id": "2.6.2", + "Name": "Access to Information Systems", + "Description": "The users, access restriction methods, and secure access means for accessing information systems such as servers and network systems must be defined and controlled.", + "Checks": [ + "lightsail_instance_public", + "lightsail_static_ip_unused", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_elastic_ip_unassigned", + "ec2_securitygroup_default_restrict_traffic", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_elastic_ip_shodan", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_instance_internet_facing_with_instance_profile", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", + "ec2_instance_managed_by_ssm", + "ec2_networkacl_allow_ingress_any_port", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", + "ec2_securitygroup_with_many_ingress_egress_rules", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_instance_port_ftp_exposed_to_internet", + "ec2_instance_public_ip", + "ec2_networkacl_allow_ingress_tcp_port_22", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_instance_port_telnet_exposed_to_internet" + ], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.6. Access Control", + "Section": "2.6.2 Access to Information Systems", + "AuditChecklist": [ + "Have users, access locations, and access means allowed to access operating systems (OS) of information systems such as servers, network systems, and security systems been defined and controlled?", + "Is the system automatically disconnected when there is no work processed after accessing the information system for a certain period?", + "Are services unrelated to the purpose of using the information system removed?", + "Are information systems that provide key services operated on independent servers?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 6 (Access Control)" + ], + "AuditEvidence": [ + "List of operating system accounts of information systems", + "Server security settings", + "Server access control policy (e.g., SecureOS management screen)", + "Server and network configuration diagram", + "Information asset list" + ], + "NonComplianceCases": [ + "Case 1: When a server administrator accesses a Windows server located in the IDC from the office using terminal services, session timeout settings are not configured, allowing the session to remain open for a long period without any activity.", + "Case 2: Due to improper restrictions on server-to-server access, a user authorized to access a particular server can access other unauthorized servers via that server.", + "Case 3: Unsafe access protocols (e.g., telnet, ftp) are being used without valid reasons or compensatory measures, and unnecessary services and ports are open.", + "Case 4: Although the access control policy requires all access to servers to go through a server access control system, bypass routes exist that allow access to servers without going through the system." + ] + } + ] + }, + { + "Id": "2.6.3", + "Name": "Access to Applications", + "Description": "Access rights to applications must be restricted according to the user's tasks and the importance of the accessed information, and criteria should be established to minimize exposure of unnecessary or sensitive information.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Requirements", + "Subdomain": "2.6. Access Control", + "Section": "2.6.3 Access to Applications", + "AuditChecklist": [ + "Are access rights to applications granted differentially based on the user's tasks to control access to sensitive information?", + "Are sessions automatically disconnected after a certain period of inactivity, and is the number of simultaneous sessions per user restricted?", + "Is access to administrator-exclusive applications (e.g., admin web pages, admin consoles) restricted to unauthorized users?", + "Are criteria established and applied to ensure consistency in protection measures for limiting the display of personal and sensitive information?", + "Are applications implemented and operated to minimize unnecessary exposure (e.g., viewing, screen display, printing, downloading) of personal and sensitive information?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 5 (Management of Access Rights), Article 6 (Access Control), Article 12 (Safety Measures for Printing and Copying)" + ], + "AuditEvidence": [ + "Application access rights classification system", + "Application account and rights management screen", + "Application user and administrator screens (e.g., personal information viewing, etc.)", + "Application session time and concurrent session restriction settings", + "Application administrator access log monitoring details", + "Information asset list", + "Personal information processing system's personal information viewing and search screens", + "Personal information masking standards", + "Personal information masking application screen" + ], + "NonComplianceCases": [ + "Case 1: There is a flaw in the authorization control function of certain personal information processing screens in the application, allowing users without permission to view personal information.", + "Case 2: The administrator page of the application is open to the public internet without secure authentication methods applied.", + "Case 3: Session timeouts or concurrent logins for the same user account are not restricted without valid reasons.", + "Case 4: When personal information is downloaded through the application, the file contains excessive unnecessary information such as resident registration numbers.", + "Case 5: The application excessively allows 'like' searches, allowing all users to retrieve all customer information by searching only for a surname, even beyond their work scope.", + "Case 6: Due to the lack of criteria for limiting the display of personal information or failure to adhere to them, different masking standards are applied to the same personal information items on different screens of the personal information processing system.", + "Case 7: Although personal information is masked on the screen of the personal information processing system, unmasked personal information is exposed by viewing the web browser source." + ] + } + ] + }, + { + "Id": "2.6.4", + "Name": "Database Access", + "Description": "Identify the information stored and managed in the database, such as the table list, and establish and implement access control policies according to the importance of the information and the type of applications and users.", + "Checks": [ + "documentdb_cluster_public_snapshot", + "opensearch_service_domains_internal_user_database_enabled", + "opensearch_service_domains_use_cognito_authentication_for_kibana", + "opensearch_service_domains_https_communications_enforced", + "opensearch_service_domains_not_publicly_accessible", + "accessanalyzer_enabled_without_findings", + "accessanalyzer_enabled", + "lightsail_database_public", + "neptune_cluster_iam_authentication_enabled", + "neptune_cluster_uses_public_subnet", + "neptune_cluster_public_snapshot", + "dynamodb_table_cross_account_access", + "redshift_cluster_public_access", + "vpc_subnet_separate_private_public", + "dms_instance_no_public_access", + "rds_cluster_iam_authentication_enabled", + "rds_snapshots_public_access", + "rds_instance_no_public_access", + "rds_instance_iam_authentication_enabled", + "rds_instance_transport_encrypted", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", + "ec2_instance_port_mysql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_instance_port_cassandra_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_instance_port_oracle_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", + "ec2_instance_port_redis_exposed_to_internet", + "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_instance_port_mongodb_exposed_to_internet" + ], + "Attributes": [ + { + "Domain": "2. Protection Measures Requirements", + "Subdomain": "2.6. Access Control", + "Section": "2.6.4 Database Access", + "AuditChecklist": [ + "Are you identifying the information stored and managed in the database, such as the table list?", + "Are you clearly identifying the applications, information systems (servers), and users that need access to information in the database and controlling access according to the access control policy?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 5 (Management of Access Rights), Article 6 (Access Control)" + ], + "AuditEvidence": [ + "Database status (e.g., tables, columns)", + "List of database user accounts and permissions", + "Database access control policy (e.g., database access control system management screen)", + "Network diagram (e.g., database zone)", + "Information asset list" + ], + "NonComplianceCases": [ + "Case 1: A database that stores and processes a large amount of personal information is operated on the same physical server as a web application accessible via the Internet, without separating them.", + "Case 2: Developers and operators share accounts used by the application to access the production database.", + "Case 3: Although internal regulations require database access rights to be restricted by object, access rights to the database are granted uniformly to administrators, even those who do not need access to personal information tables.", + "Case 4: A database access control solution has been implemented, but access to the database is not properly restricted by IP address, allowing users to bypass the access control solution.", + "Case 5: The table status of a database storing personal information has not been identified, resulting in the unnecessary retention of personal information in temporary tables that have not been deleted." + ] + } + ] + }, + { + "Id": "2.6.5", + "Name": "Wireless Network Access", + "Description": "When using a wireless network, wireless network protection measures such as user authentication, encryption of transmitted and received data, and AP control must be applied. In addition, protection measures must be established and implemented to prevent unauthorized wireless network access, such as AD Hoc connections and the use of unauthorized APs.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measures Requirements", + "Subdomain": "2.6. Access Control", + "Section": "2.6.5 Wireless Network Access", + "AuditChecklist": [ + "When using a wireless network for business purposes, are you establishing and implementing protection measures such as authentication and encryption of transmitted and received data to ensure the security of the wireless AP and network segment?", + "Have you established and implemented procedures for applying for and terminating access to ensure that only authorized employees can use the wireless network?", + "Have you established and implemented protection measures against unauthorized wireless networks, such as detecting and blocking AD Hoc connections and unauthorized wireless APs within the organization?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 6 (Access Control)" + ], + "AuditEvidence": [ + "Network diagram", + "AP security settings history", + "Inspection records of unauthorized wireless networks", + "Wireless network usage application and approval records" + ], + "NonComplianceCases": [ + "Case 1: The wireless network segments for external users and internal users are the same, allowing external users to access the internal network without separate control via the wireless network.", + "Case 2: Although the encryption function for information transmission and reception was enabled when configuring the wireless AP, it was set in an insecure manner.", + "Case 3: A wireless AP connected to the internal network for business purposes has security settings that are insufficient, such as exposure of the administrator password (using the default password) and lack of access control." + ] + } + ] + }, + { + "Id": "2.6.6", + "Name": "Remote Access Control", + "Description": "Managing information systems and handling personal information outside of protected areas is, in principle, prohibited. However, if remote access is allowed for unavoidable reasons such as telecommuting, incident response, or remote collaboration, protective measures must be established and implemented, including approval from responsible personnel, designation of access devices, setting access scope and duration, enhanced authentication, encrypted communication, and securing access devices (e.g., antivirus, patches).", + "Checks": [ + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cognito_identity_pool_guest_access_disabled", + "cognito_user_pool_mfa_enabled", + "cognito_user_pool_self_registration_disabled", + "workspaces_vpc_2private_1public_subnets_nat", + "workspaces_volume_encryption_enabled", + "iam_user_console_access_unused", + "iam_user_mfa_enabled_console_access", + "networkfirewall_in_all_vpc", + "vpc_subnet_no_public_ip_by_default", + "vpc_flow_logs_enabled", + "vpc_subnet_separate_private_public", + "appstream_fleet_maximum_session_duration", + "appstream_fleet_default_internet_access_disabled", + "appstream_fleet_session_idle_disconnect_timeout", + "appstream_fleet_session_disconnect_timeout", + "ec2_client_vpn_endpoint_connection_logging_enabled", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_networkacl_allow_ingress_any_port", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_networkacl_allow_ingress_tcp_port_22" + ], + "Attributes": [ + { + "Domain": "2. Protective Measures Requirements", + "Subdomain": "2.6. Access Control", + "Section": "2.6.6 Remote Access Control", + "AuditChecklist": [ + "Is remote operation of information systems through external networks such as the internet prohibited in principle, and are compensatory measures in place if allowed for unavoidable reasons such as incident response?", + "Is access through remote operation of information systems allowed only for specific devices when done through internal networks?", + "Are protective measures established and implemented to prevent security incidents such as data breaches and hacking during remote work, such as telecommuting, remote collaboration, and smart work?", + "Are the devices used for remote access to personal information processing systems for management, operation, development, and security purposes designated as management terminals, and are safety measures such as prohibiting unauthorized operations and use for purposes other than those intended being applied?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information Article 6 (Access Control)" + ], + "AuditEvidence": [ + "Remote access application form (e.g., VPN)", + "VPN account list", + "VPN access control policy settings", + "IP management log", + "Remote access control settings (server settings, security system settings, etc.)", + "Designation and management of management terminals", + "Network diagram" + ], + "NonComplianceCases": [ + "Case 1: Although internal regulations state that remote access to the system is prohibited in principle and, when allowed, access is restricted through IP-based access control, remote desktop connections and SSH access to the system are not limited by IP addresses, allowing access from any PC.", + "Case 2: A VPN has been established for remote management, but it is always available without usage approval or access period restrictions.", + "Case 3: Work-related mobile apps have been installed on personal smart devices for external workers, but appropriate protective measures (e.g., antivirus, encryption, wiping in case of loss or theft) to prevent personal information leaks are not being applied.", + "Case 4: VPN access for external users is not limited by network segments and information systems, allowing excessive access to the entire internal network and information systems for authenticated remote users." + ] + } + ] + }, + { + "Id": "2.6.7", + "Name": "Internet Access Control", + "Description": "To prevent information leaks, malware infections, and intrusions into the internal network through the internet, policies must be established and implemented to restrict internet access or services (e.g., P2P, web hard drives, messengers) on key information systems, devices handling sensitive duties, and terminals processing personal information.", + "Checks": [ + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_changes_to_network_acls_alarm_configured", + "workspaces_vpc_2private_1public_subnets_nat", + "workspaces_volume_encryption_enabled", + "networkfirewall_in_all_vpc", + "vpc_subnet_no_public_ip_by_default", + "vpc_flow_logs_enabled", + "vpc_subnet_separate_private_public", + "appstream_fleet_maximum_session_duration", + "appstream_fleet_default_internet_access_disabled", + "appstream_fleet_session_idle_disconnect_timeout", + "appstream_fleet_session_disconnect_timeout", + "route53_dangling_ip_subdomain_takeover", + "ec2_elastic_ip_unassigned", + "ec2_elastic_ip_shodan", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_instance_public_ip" + ], + "Attributes": [ + { + "Domain": "2. Protective Measures Requirements", + "Subdomain": "2.6. Access Control", + "Section": "2.6.7 Internet Access Control", + "AuditChecklist": [ + "Is there an established and implemented policy to control internet access for work PCs used for key duties and personal information handling terminals?", + "Is unnecessary external internet access from key information systems (e.g., database servers) being controlled?", + "Are internet access restrictions being applied in a secure manner for individuals who are required by law to have their internet access restricted?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information Article 6 (Access Control)" + ], + "AuditEvidence": [ + "Policy for blocking non-work-related sites (e.g., P2P) (management screen of non-work-related site blocking system)", + "Internet access monitoring history", + "List of individuals subject to internet access restriction measures", + "Procedures and records for data transfer between networks (e.g., application and approval records)", + "Network diagram" + ], + "NonComplianceCases": [ + "Case 1: Internet access restriction measures were applied according to the Personal Information Protection Act, but the restriction was not applied to some individuals with the authority to set access rights for personal information processing systems.", + "Case 2: Although internet access restriction measures were applied as required under the Personal Information Protection Act, it was possible to bypass the restriction by accessing the system through another server, allowing the download and deletion of personal information.", + "Case 3: Some servers located in the DMZ and internal network were unnecessarily able to access the internet directly.", + "Case 4: Although a physical network separation system was applied between internet PCs and internal work PCs, and a data transfer system was established, there was no approval process for data transfers, and there was no periodic review of the data transfer records.", + "Case 5: Internal regulations require that individuals handling personal information obtain approval from a responsible person before accessing P2P or web hard drive sites, and access is only permitted for a specific period, but there are numerous cases of exceptions being made without going through the approval process." + ] + } + ] + }, + { + "Id": "2.7.1", + "Name": "Application of Encryption Policy", + "Description": "To protect personal and important information, encryption policies that reflect legal requirements, such as encryption targets, encryption strength, and encryption usage policies, must be established. Encryption must be applied during the storage, transmission, and transfer of personal and important information.", + "Checks": [ + "codebuild_project_no_secrets_in_variables", + "ssm_document_secrets", + "cloudwatch_log_group_kms_encryption_enabled", + "cloudwatch_log_group_no_secrets_in_logs", + "glue_etl_jobs_amazon_s3_encryption_enabled", + "glue_development_endpoints_s3_encryption_enabled", + "glue_data_catalogs_metadata_encryption_enabled", + "glue_data_catalogs_connection_passwords_encryption_enabled", + "glue_development_endpoints_cloudwatch_logs_encryption_enabled", + "glue_database_connections_ssl_enabled", + "glue_etl_jobs_job_bookmark_encryption_enabled", + "glue_development_endpoints_job_bookmark_encryption_enabled", + "glue_etl_jobs_cloudwatch_logs_encryption_enabled", + "s3_bucket_kms_encryption", + "s3_bucket_default_encryption", + "s3_bucket_secure_transport_policy", + "documentdb_cluster_storage_encrypted", + "sns_subscription_not_using_http_endpoints", + "sns_topics_kms_encryption_at_rest_enabled", + "cloudfront_distributions_https_enabled", + "cloudfront_distributions_field_level_encryption_enabled", + "cloudfront_distributions_using_deprecated_ssl_protocols", + "opensearch_service_domains_encryption_at_rest_enabled", + "opensearch_service_domains_https_communications_enforced", + "opensearch_service_domains_node_to_node_encryption_enabled", + "neptune_cluster_storage_encrypted", + "dynamodb_accelerator_cluster_encryption_enabled", + "dynamodb_tables_kms_cmk_encryption_enabled", + "storagegateway_fileshare_encryption_enabled", + "ecs_task_definitions_no_environment_secrets", + "directoryservice_radius_server_security_protocol", + "workspaces_volume_encryption_enabled", + "elb_insecure_ssl_ciphers", + "elb_ssl_listeners", + "sqs_queues_server_side_encryption_enabled", + "cloudtrail_kms_encryption_enabled", + "awslambda_function_no_secrets_in_code", + "awslambda_function_no_secrets_in_variables", + "eks_cluster_kms_cmk_encryption_in_secrets_enabled", + "kafka_cluster_mutual_tls_authentication_enabled", + "kafka_cluster_encryption_at_rest_uses_cmk", + "kafka_cluster_in_transit_encryption_enabled", + "autoscaling_find_secrets_ec2_launch_configuration", + "elbv2_insecure_ssl_ciphers", + "elbv2_ssl_listeners", + "backup_vaults_exist", + "backup_vaults_encrypted", + "cloudformation_stack_outputs_find_secrets", + "elasticache_redis_cluster_in_transit_encryption_enabled", + "elasticache_redis_cluster_rest_encryption_enabled", + "rds_instance_storage_encrypted", + "rds_snapshots_encrypted", + "rds_instance_transport_encrypted", + "apigateway_restapi_client_certificate_enabled", + "efs_encryption_at_rest_enabled", + "sagemaker_training_jobs_volume_and_output_encryption_enabled", + "sagemaker_training_jobs_intercontainer_encryption_enabled", + "sagemaker_notebook_instance_encryption_enabled", + "acm_certificates_with_secure_key_algorithms", + "athena_workgroup_enforce_configuration", + "athena_workgroup_encryption", + "ec2_instance_secrets_user_data", + "ec2_ebs_default_encryption", + "ec2_ebs_snapshots_encrypted", + "ec2_launch_template_no_secrets", + "ec2_ebs_volume_encryption" + ], + "Attributes": [ + { + "Domain": "2. Protection Measures Requirements", + "Subdomain": "2.7. Application of Encryption", + "Section": "2.7.1 Application of Encryption Policy", + "AuditChecklist": [ + "Has an encryption policy been established that includes encryption targets, encryption strength, and encryption usage in consideration of legal requirements for the protection of personal and important information?", + "Is encryption applied to personal and important information during storage, transmission, and transfer according to the encryption policy?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 24-2 (Restrictions on Processing of Resident Registration Numbers), Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 7 (Encryption of Personal Information)" + ], + "AuditEvidence": [ + "Encryption control policy (targets, methods, algorithms, etc.)", + "Encryption application status (during storage and transmission)", + "Risk analysis results (if encryption is not applied to unique identifiers other than resident registration numbers in the internal network)", + "Encryption solution management screen" + ], + "NonComplianceCases": [ + "Case 1: Internal policies and guidelines do not properly specify encryption targets, encryption strength, encryption methods during storage and transmission, or the roles and responsibilities of those responsible for encryption, considering legal requirements.", + "Case 2: The company applied incorrect regulations during the creation of its encryption policy, leading to non-compliance with legal encryption requirements (e.g., storing user account numbers without encryption).", + "Case 3: Although one-way encryption was applied to the passwords of both personal information handlers and data subjects, an insecure MD5 algorithm was used.", + "Case 4: Although a security server was applied to an internet shopping mall in accordance with relevant laws and internal regulations, encryption was missing in some sections where users' personal information is transmitted (e.g., viewing or modifying member information, password retrieval, password changes).", + "Case 5: Passwords for accessing information systems, authentication keys, and other values were stored in plaintext in system configuration files and source code." + ] + } + ] + }, + { + "Id": "2.7.2", + "Name": "Cryptographic Key Management", + "Description": "Establish and implement management procedures for the secure generation, use, storage, distribution, and destruction of cryptographic keys, and prepare recovery methods if necessary.", + "Checks": [ + "secretsmanager_automatic_rotation_enabled", + "kms_cmk_are_used", + "kms_key_not_publicly_accessible", + "kms_cmk_rotation_enabled", + "kms_cmk_not_deleted_unintentionally", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "directoryservice_ldap_certificate_expiration", + "rds_instance_certificate_expiration", + "acm_certificates_transparency_logs_enabled" + ], + "Attributes": [ + { + "Domain": "2. Security Control Requirements", + "Subdomain": "2.7. Application of Encryption", + "Section": "2.7.2 Cryptographic Key Management", + "AuditChecklist": [ + "Are procedures for the generation, use, storage, distribution, modification, recovery, and destruction of cryptographic keys established and implemented?", + "Are cryptographic keys securely stored in a separate location to ensure they can be recovered if necessary, and is access to the use of cryptographic keys minimized?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 7 (Encryption of Personal Information)" + ], + "AuditEvidence": [ + "Cryptographic Key Management Policy", + "Cryptographic Key Management Log and System Screens" + ], + "NonComplianceCases": [ + "Case 1: If encryption policies do not specify procedures and methods for managing cryptographic keys, leading to varying levels and methods of cryptographic key management among personnel, resulting in vulnerabilities.", + "Case 2: Internal regulations require the generation of encryption keys under the approval of a responsible person when encrypting important information, and to maintain a key management log, but some keys are either missing or outdated in the log.", + "Case 3: The encryption key applied in the development system is the same as the one applied in the production system, making it easy to decrypt actual data through the development system." + ] + } + ] + }, + { + "Id": "2.8.1", + "Name": "Definition of Security Requirements", + "Description": "When introducing, developing, or modifying information systems, security requirements such as legal requirements related to information protection and personal information protection, the latest security vulnerabilities, and secure coding methods must be defined and applied.", + "Checks": [ + "codebuild_project_no_secrets_in_variables", + "codebuild_project_user_controlled_buildspec", + "codebuild_project_source_repo_url_no_sensitive_credentials", + "macie_is_enabled", + "inspector2_is_enabled", + "config_recorder_all_regions_enabled", + "fms_policy_compliant", + "wafv2_webacl_logging_enabled", + "accessanalyzer_enabled", + "wellarchitected_workload_no_high_or_medium_risks", + "cloudtrail_cloudwatch_logging_enabled", + "networkfirewall_in_all_vpc", + "vpc_flow_logs_enabled", + "guardduty_is_enabled", + "guardduty_centrally_managed", + "securityhub_enabled" + ], + "Attributes": [ + { + "Domain": "2. Security Control Requirements", + "Subdomain": "2.8. Security for Information System Introduction and Development", + "Section": "2.8.1 Definition of Security Requirements", + "AuditChecklist": [ + "When introducing, developing, or modifying an information system, are procedures for reviewing the validity of information protection and personal information protection aspects and for acquisition established and implemented?", + "When introducing, developing, or modifying an information system, are security requirements, including legal requirements and the latest vulnerabilities, clearly defined and reflected from the design stage?", + "Are coding standards for secure implementation of the information system established and applied?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Information System Acquisition Standards and Procedures", + "RFP (Request for Proposal) and Purchase Contracts for Information System Introduction", + "Development Outputs (Project Execution Plans, Requirements Definition, Screen Design, Security Architecture Design, Test Plans, etc.)", + "Secure Coding Standards" + ], + "NonComplianceCases": [ + "Case 1: Lack of established security verification standards and procedures prior to acquiring an information system.", + "Case 2: Internal regulations require the review of the security impact and the operating environment when introducing a new system, but recent acquisitions of some information systems lacked detailed standards and plans, and therefore, no security review was conducted during the acquisition.", + "Case 3: Internal development guidelines do not define key security requirements related to development (authentication and encryption, security logging, etc.).", + "Case 4: In the 'Development Standards Definition Document', user passwords are to be encrypted using insecure algorithms (MD5, SHA1), resulting in failure to comply with relevant legal requirements." + ] + } + ] + }, + { + "Id": "2.8.2", + "Name": "Review and Testing of Security Requirements", + "Description": "To verify that an information system has been introduced or implemented according to predefined security requirements, review standards and procedures must be established and implemented to check compliance with legal requirements, the latest security vulnerabilities, secure coding implementation, and personal information impact assessment, and corrective measures must be taken for any identified issues.", + "Checks": [ + "codebuild_project_no_secrets_in_variables", + "codebuild_project_user_controlled_buildspec", + "codebuild_project_source_repo_url_no_sensitive_credentials", + "macie_is_enabled", + "inspector2_is_enabled", + "inspector2_active_findings_exist", + "config_recorder_all_regions_enabled", + "fms_policy_compliant", + "wafv2_webacl_logging_enabled", + "accessanalyzer_enabled_without_findings", + "accessanalyzer_enabled", + "wellarchitected_workload_no_high_or_medium_risks", + "cloudtrail_cloudwatch_logging_enabled", + "networkfirewall_in_all_vpc", + "vpc_flow_logs_enabled", + "guardduty_no_high_severity_findings", + "guardduty_is_enabled", + "guardduty_centrally_managed", + "securityhub_enabled" + ], + "Attributes": [ + { + "Domain": "2. Security Control Requirements", + "Subdomain": "2.8. Security for Information System Introduction and Development", + "Section": "2.8.2 Review and Testing of Security Requirements", + "AuditChecklist": [ + "When introducing, developing, or modifying an information system, are tests conducted to verify whether the security requirements defined during the analysis and design stages have been effectively applied?", + "Are vulnerability assessments conducted to confirm that the information system has been securely developed according to secure coding standards?", + "Are procedures established and implemented to ensure that issues identified during testing and vulnerability assessments are promptly addressed through corrective action plans and follow-up checks?", + "For public institutions, are impact assessments conducted during the analysis and design stages when developing or modifying personal information processing systems, as required by relevant laws, and are the results reflected during development and modification?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 33 (Personal Information Impact Assessment)", + "Notification on Personal Information Impact Assessment" + ], + "AuditEvidence": [ + "Information System Acceptance Test Results", + "Requirements Traceability Matrix", + "Test Plans, Test Results", + "Vulnerability Assessment Results", + "Personal Information Impact Assessment Report", + "Confirmation of Implementation of Corrective Actions for Personal Information Impact Assessment" + ], + "NonComplianceCases": [ + "Case 1: Failure to test security requirements defined in internal guidelines and documents after implementing an information system.", + "Case 2: In the application program test scenario and technical vulnerability checklist, important validation items such as input validation checks were omitted.", + "Case 3: Failure to assess whether known technical vulnerabilities exist during implementation or testing, or failure to address identified vulnerabilities without valid reasons or approval.", + "Case 4: A public institution failed to conduct an impact assessment when developing a personal information file or personal information processing system subject to an impact assessment requirement, such as processing unique identifiers of more than 50,000 data subjects.", + "Case 5: A public institution failed to submit the impact assessment report to the Personal Information Protection Commission within two months after receiving the report from the impact assessment agency.", + "Case 6: Internal guidelines require reviewing the security and impact on the operating environment when introducing a new system (e.g., vulnerability assessments), but recent acquisitions of some information systems lacked security reviews during the acceptance process." + ] + } + ] + }, + { + "Id": "2.8.3", + "Name": "Separation of Test and Production Environments", + "Description": "Development and test systems must, in principle, be separated from production systems to reduce the risk of unauthorized access and changes to the production system.", + "Checks": [ + "codebuild_project_user_controlled_buildspec" + ], + "Attributes": [ + { + "Domain": "2. Security Requirements for Protection Measures", + "Subdomain": "2.8. Security for Information System Introduction and Development", + "Section": "2.8.3 Separation of Test and Production Environments", + "AuditChecklist": [ + "Are development and test systems separated from the production system?", + "If separation of development and production environments is difficult due to unavoidable reasons, have security measures such as mutual review, monitoring by supervisors, approval for changes, and ensuring accountability been implemented?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Network diagrams (including test environment configuration)", + "Current application of access control between the production environment and the development/test environment" + ], + "NonComplianceCases": [ + "Case 1: Source code changes are being made directly in the production environment without a separate development environment or proper approval.", + "Case 2: Although it is unavoidable to operate the development and production systems without separation, records of mutual review or monitoring are missing.", + "Case 3: Although a separate development system is in place, access from the development environment to the production environment is not controlled, allowing developers unnecessary access to the production system through the development system." + ] + } + ] + }, + { + "Id": "2.8.4", + "Name": "Test Data Security", + "Description": "In order to prevent the leakage of operational data during system testing, procedures for the creation, use, management, disposal, and technical protection measures of test data must be established and implemented.", + "Checks": [ + "codebuild_project_no_secrets_in_variables" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.8. Security in Information System Introduction and Development", + "Section": "2.8.4 Test Data Security", + "AuditChecklist": [ + "Is the use of actual operational data restricted during the development and testing of information systems?", + "If it is inevitable to use operational data in a test environment, are control procedures such as approval by the responsible person, monitoring of access and leakage, and deletion of data after testing established and implemented?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Test data status", + "Test data generation rules", + "If operational data was used in a test environment, the approval history" + ], + "NonComplianceCases": [ + "Case 1: There are no specific standards and procedures established for generating test data for use on the development server.", + "Case 2: Operational data is being used as test data without proper processing and without approval from the responsible person for a valid reason.", + "Case 3: Although operational data was approved in advance for use as test data for unavoidable reasons, the same level of access control as the operational database is not applied to the test database.", + "Case 4: After using operational data for testing purposes, the data was not deleted from the test database even though the testing was completed." + ] + } + ] + }, + { + "Id": "2.8.5", + "Name": "Source Program Management", + "Description": "Source programs must be managed so that only authorized users can access them, and it is a principle that they should not be stored in the operational environment.", + "Checks": [ + "ecr_repositories_not_publicly_accessible", + "codeartifact_packages_external_public_publishing_disabled" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.8. Security in Information System Introduction and Development", + "Section": "2.8.5 Source Program Management", + "AuditChecklist": [ + "Have procedures been established and implemented to control access to source programs by unauthorized persons?", + "Is the source program stored safely in a non-operational environment for emergencies such as system failures?", + "Is the history of changes to the source program being managed?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Status of configuration management systems such as SVN (e.g., list of authorized personnel)", + "History of changes to the source program" + ], + "NonComplianceCases": [ + "Case 1: There is no separate backup or configuration management system for source programs, and previous versions of the source code are stored on the operational server or developer's PC without approval or history management.", + "Case 2: A configuration management system has been established, but access control, access and change history for the system or the source code stored in the system are not properly managed.", + "Case 3: The internal regulations require version control of source programs through a configuration management system, but the latest version of the source program is only stored on the developer's PC, and no separate backup is performed." + ] + } + ] + }, + { + "Id": "2.8.6", + "Name": "Transition to Operational Environment", + "Description": "When transitioning newly introduced, developed, or modified systems to the operational environment, the process must be controlled, and the executable code must be run according to test and user acceptance procedures.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.8. Security in Information System Introduction and Development", + "Section": "2.8.6 Transition to Operational Environment", + "AuditChecklist": [ + "Have control procedures been established and implemented to safely transition newly introduced, developed, or modified systems to the operational environment?", + "Are contingency plans in place to address issues that may arise during the transition to the operational environment?", + "Are only the files necessary for service execution installed in the operational environment?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Transition procedures", + "Transition records (requests, approvals, tests, transitions, etc.)" + ], + "NonComplianceCases": [ + "Case 1: There are no procedures in place to review and approve the transition of developed or modified source programs to the operational environment.", + "Case 2: Unnecessary files (source code, distribution modules, backups, development-related documents, manuals, etc.) exist in the operational server.", + "Case 3: The internal guidelines require the preparation of change request and result documents for safe transition and recovery during transitions to the operational environment, but such documents are not available.", + "Case 4: The internal guidelines require internal review and approval before distributing mobile apps to the app market, but developers are bypassing these procedures and distributing the apps directly." + ] + } + ] + }, + { + "Id": "2.9.1", + "Name": "Change Management", + "Description": "Procedures must be established and implemented to manage all changes to assets related to information systems, and the impact on system performance and security must be analyzed before changes are made.", + "Checks": [ + "codebuild_project_older_90_days", + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_log_metric_filter_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", + "config_recorder_all_regions_enabled", + "cloudtrail_cloudwatch_logging_enabled" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.9. System and Service Operations Management", + "Section": "2.9.1 Change Management", + "AuditChecklist": [ + "Have procedures been established and implemented for changes to assets related to information systems (hardware, operating systems, commercial software packages, etc.)?", + "Are the performance and security impacts analyzed before making changes to information system-related assets?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Change management procedures", + "Change management records (requests, approvals, change details, etc.)", + "Impact analysis results of changes" + ], + "NonComplianceCases": [ + "Case 1: A recent change to the DMZ section for redundancy was made, but there is no evidence of performing and approving the security risk and performance evaluation that may occur after the change.", + "Case 2: A recent network change was made, but the review and notification were not sufficiently carried out, so the changes were not properly reflected in the network diagram or some access control systems (e.g., firewalls, database access control systems) ACLs.", + "Case 3: Although a change management system was established to analyze and discuss the impact on performance and security when information systems are introduced or changed, changes can still be made outside the system, and related changes are not properly reviewed." + ] + } + ] + }, + { + "Id": "2.9.2", + "Name": "Performance and Fault Management", + "Description": "To ensure the availability of information systems, performance and capacity requirements must be defined, and the status must be continuously monitored. Procedures for detecting, recording, analyzing, recovering, and reporting in response to faults must be established and managed effectively.", + "Checks": [ + "kms_cmk_not_deleted_unintentionally", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "s3_bucket_no_mfa_delete", + "s3_bucket_cross_region_replication", + "documentdb_cluster_cloudwatch_log_export", + "documentdb_cluster_deletion_protection", + "config_recorder_all_regions_enabled", + "cognito_user_pool_deletion_protection_enabled", + "neptune_cluster_multi_az", + "neptune_cluster_deletion_protection", + "directoryservice_directory_monitor_notifications", + "directoryservice_ldap_certificate_expiration", + "elb_is_in_multiple_az", + "elb_cross_zone_load_balancing_enabled", + "cloudtrail_bucket_requires_mfa_delete", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_multi_region_enabled", + "iam_no_expired_server_certificates_stored", + "networkfirewall_deletion_protection", + "kafka_cluster_enhanced_monitoring_enabled", + "vpc_subnet_different_az", + "trustedadvisor_premium_support_plan_subscribed", + "trustedadvisor_errors_and_warnings", + "dms_instance_multi_az_enabled", + "autoscaling_group_multiple_az", + "elbv2_is_in_multiple_az", + "elbv2_deletion_protection", + "cloudformation_stacks_termination_protection_enabled", + "elasticache_redis_cluster_multi_az_enabled", + "rds_instance_multi_az", + "rds_cluster_backtrack_enabled", + "rds_instance_enhanced_monitoring_enabled", + "rds_instance_deletion_protection", + "rds_instance_certificate_expiration", + "rds_cluster_deletion_protection", + "rds_cluster_multi_az", + "acm_certificates_expiration_check", + "route53_domains_transferlock_enabled", + "ec2_instance_detailed_monitoring_enabled" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.9. System and Service Operations Management", + "Section": "2.9.2 Performance and Fault Management", + "AuditChecklist": [ + "Have procedures been established and implemented to continuously monitor performance and capacity to ensure the availability of information systems?", + "Are response procedures in place and implemented to address cases where the performance and capacity requirements (thresholds) of the information system are exceeded?", + "Have procedures been established and implemented to immediately recognize and respond to information system faults?", + "Are procedures in place to record and manage actions taken in response to faults through fault response reports?", + "For serious faults, are measures being taken to prevent recurrence through cause analysis?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Procedures for performance and capacity monitoring", + "Evidence of performance and capacity monitoring (e.g., internal reporting results)", + "Fault response procedures", + "Fault response report" + ], + "NonComplianceCases": [ + "Case 1: Failure to define requirements (e.g., thresholds) for managing performance and capacity for each target, or the absence of records in regular inspection reports, making it difficult to assess the current status.", + "Case 2: Performance or capacity standards were exceeded, but no related reviews or follow-up measures were taken or implemented.", + "Case 3: Fault response procedures for IT equipment have been established, but internal and external environmental changes such as network configuration or vendor changes are not adequately reflected.", + "Case 4: Inconsistencies exist between fault handling procedures and fault type-specific response methods, or there is a lack of rationale for estimating response times, making swift, accurate, and systematic responses difficult." + ] + } + ] + }, + { + "Id": "2.9.3", + "Name": "Backup and Recovery Management", + "Description": "To maintain the availability and data integrity of the information system, procedures must be established and implemented regarding the backup targets, frequency, methods, storage locations, retention periods, and disaster recovery. Additionally, management must ensure timely recovery in case of incidents.", + "Checks": [ + "cloudwatch_log_group_retention_policy_specific_days_enabled", + "s3_access_point_public_access_block", + "s3_bucket_lifecycle_enabled", + "s3_bucket_object_versioning", + "s3_bucket_kms_encryption", + "s3_bucket_public_write_acl", + "s3_bucket_policy_public_write_access", + "s3_bucket_public_access", + "s3_bucket_public_list_acl", + "s3_bucket_cross_region_replication", + "s3_bucket_default_encryption", + "documentdb_cluster_backup_enabled", + "documentdb_cluster_public_snapshot", + "lightsail_instance_automated_snapshots", + "neptune_cluster_backup_enabled", + "neptune_cluster_public_snapshot", + "ecr_repositories_lifecycle_policy_enabled", + "dynamodb_tables_pitr_enabled", + "directoryservice_directory_snapshots_limit", + "redshift_cluster_automated_snapshot", + "dlm_ebs_snapshot_lifecycle_policy_exists", + "backup_reportplans_exist", + "backup_vaults_exist", + "backup_vaults_encrypted", + "backup_plans_exist", + "elasticache_redis_cluster_backup_enabled", + "rds_cluster_copy_tags_to_snapshots", + "rds_instance_copy_tags_to_snapshots", + "rds_cluster_backtrack_enabled", + "rds_snapshots_public_access", + "rds_instance_backup_enabled", + "efs_have_backup_enabled", + "ec2_ebs_snapshot_account_block_public_access", + "ec2_ebs_public_snapshot", + "ec2_ebs_volume_snapshots_exists", + "ec2_ebs_snapshots_encrypted", + "ec2_ami_public" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.9. System and Service Operation Management", + "Section": "2.9.3 Backup and Recovery Management", + "AuditChecklist": [ + "Have backup and recovery procedures been established and implemented, including targets, frequency, methods, and procedures?", + "Is regular recovery testing conducted to verify the completeness and accuracy of the backed-up information and the adequacy of the recovery procedures?", + "For backup media containing critical information, is the media stored in physically separate locations to address disaster recovery?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 11 (Safety Measures for Disaster Recovery)" + ], + "AuditEvidence": [ + "Backup and recovery procedures", + "Recovery test results", + "Disaster recovery backup status" + ], + "NonComplianceCases": [ + "Case 1: Backup and recovery procedures, including targets, frequency, methods, and procedures, have not been established.", + "Case 2: Although a backup policy is in place, information required to be stored for a long period (6 months, 3 years, 5 years, etc.) according to legal requirements is not being stored according to the backup policy.", + "Case 3: Some systems (e.g., security system policies and logs) that are required to be separately backed up according to higher-level or internal guidelines are not being backed up.", + "Case 4: Although higher-level or internal guidelines stipulate that recovery tests for backup media should be conducted periodically, recovery tests have not been performed for an extended period." + ] + } + ] + }, + { + "Id": "2.9.4", + "Name": "Log and Access Record Management", + "Description": "The organization must define the types of logs, retention periods, and retention methods for user access records, system logs, and privilege grant records for information systems such as servers, applications, security systems, and network systems, and must securely retain and manage them to prevent tampering, theft, or loss.", + "Checks": [ + "eventbridge_bus_exposed", + "eventbridge_bus_cross_account_access", + "eventbridge_schema_registry_cross_account_access", + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_group_kms_encryption_enabled", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_cross_account_sharing_disabled", + "cloudwatch_log_group_retention_policy_specific_days_enabled", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_log_group_no_secrets_in_logs", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", + "glue_development_endpoints_cloudwatch_logs_encryption_enabled", + "glue_etl_jobs_cloudwatch_logs_encryption_enabled", + "s3_access_point_public_access_block", + "s3_bucket_server_access_logging_enabled", + "s3_bucket_kms_encryption", + "s3_bucket_public_write_acl", + "s3_bucket_policy_public_write_access", + "s3_bucket_public_access", + "s3_bucket_public_list_acl", + "s3_bucket_default_encryption", + "s3_account_level_public_access_blocks", + "s3_bucket_level_public_access_block", + "documentdb_cluster_cloudwatch_log_export", + "macie_is_enabled", + "inspector2_is_enabled", + "config_recorder_all_regions_enabled", + "cloudfront_distributions_logging_enabled", + "wafv2_webacl_logging_enabled", + "opensearch_service_domains_audit_logging_enabled", + "opensearch_service_domains_cloudwatch_logging_enabled", + "directoryservice_directory_log_forwarding_enabled", + "directoryservice_directory_monitor_notifications", + "elb_logging_enabled", + "redshift_cluster_audit_logging", + "cloudtrail_insights_exist", + "cloudtrail_logs_s3_bucket_access_logging_enabled", + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_s3_dataevents_read_enabled", + "cloudtrail_log_file_validation_enabled", + "cloudtrail_s3_dataevents_write_enabled", + "cloudtrail_cloudwatch_logging_enabled", + "cloudtrail_multi_region_enabled", + "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", + "iam_inline_policy_no_full_access_to_cloudtrail", + "iam_securityaudit_role_created", + "eks_control_plane_logging_all_types_enabled", + "apigatewayv2_api_access_logging_enabled", + "kafka_cluster_enhanced_monitoring_enabled", + "vpc_flow_logs_enabled", + "guardduty_is_enabled", + "guardduty_centrally_managed", + "trustedadvisor_premium_support_plan_subscribed", + "elbv2_logging_enabled", + "securityhub_enabled", + "rds_instance_enhanced_monitoring_enabled", + "rds_snapshots_public_access", + "rds_snapshots_encrypted", + "rds_instance_integration_cloudwatch_logs", + "apigateway_restapi_logging_enabled", + "efs_not_publicly_accessible", + "acm_certificates_transparency_logs_enabled", + "route53_public_hosted_zones_cloudwatch_logging_enabled", + "ec2_instance_detailed_monitoring_enabled", + "ec2_ebs_snapshot_account_block_public_access", + "ec2_instance_managed_by_ssm", + "ec2_ebs_public_snapshot", + "ec2_ebs_snapshots_encrypted", + "ec2_client_vpn_endpoint_connection_logging_enabled" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.9. System and Service Operation Management", + "Section": "2.9.4 Log and Access Record Management", + "AuditChecklist": [ + "Has the organization established log management procedures for information systems such as servers, applications, security systems, and network systems, and is it generating and storing the necessary logs accordingly?", + "Are log records of information systems securely stored to prevent tampering, theft, or loss, and is access to the log records minimized?", + "Are access records for personal information processing systems securely stored for a specified period in accordance with legal requirements, including all necessary items?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 8 (Retention and Inspection of Access Records)" + ], + "AuditEvidence": [ + "Log management procedures", + "Log record details", + "Access control records for log storage devices", + "Access records of personal information" + ], + "NonComplianceCases": [ + "Case 1: The detailed criteria and procedures for log recording, retention periods, review frequency, and responsible personnel have not been established.", + "Case 2: The maximum size for critical logs such as security event logs, application, and service logs (for Windows Server 2008 or later) is not sufficiently configured, resulting in logs not being recorded and retained for the period specified by internal standards.", + "Case 3: The log records of important Linux/UNIX servers are not separately backed up or adequately protected, allowing users to arbitrarily delete command execution histories and access logs.", + "Case 4: Upon reviewing access records for the personal information processing system, while the account, access time, and IP address of the user were logged, details about the data subject information handled and the tasks performed (e.g., view, modify, delete, download) were not recorded.", + "Case 5: The capacity of the log server is insufficient, leaving only two months of access records for the personal information processing system.", + "Case 6: A personal information processing system handling personal information of 100,000 data subjects is only retaining access logs for one year." + ] + } + ] + }, + { + "Id": "2.9.5", + "Name": "Log and Access Record Inspection", + "Description": "To ensure normal use of the information system and prevent misuse (unauthorized access, excessive queries, etc.) by users, log review criteria for access and usage must be established and inspected periodically, and post-event actions must be taken promptly if issues arise.", + "Checks": [ + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_group_kms_encryption_enabled", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_cross_account_sharing_disabled", + "cloudwatch_log_group_retention_policy_specific_days_enabled", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_log_group_no_secrets_in_logs", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", + "inspector2_active_findings_exist", + "cognito_user_pool_client_prevent_user_existence_errors", + "accessanalyzer_enabled_without_findings", + "cloudtrail_insights_exist", + "cloudtrail_threat_detection_enumeration", + "cloudtrail_threat_detection_privilege_escalation", + "guardduty_no_high_severity_findings", + "trustedadvisor_errors_and_warnings" + ], + "Attributes": [ + { + "Domain": "2. Protection Control Requirements", + "Subdomain": "2.9. System and Service Operation Management", + "Section": "2.9.5 Log and Access Record Inspection", + "AuditChecklist": [ + "Are there established log review and monitoring procedures, including the frequency, targets, and methods for detecting errors, misuse (unauthorized access, excessive queries, etc.), fraud, and other anomalies in the information system?", + "Are the results of log reviews and monitoring reported to the responsible person, and are responses taken following procedures when anomalies are detected?", + "Are access records of the personal information processing system regularly inspected according to the periods specified in relevant laws?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 8 (Retention and Inspection of Access Records)" + ], + "AuditEvidence": [ + "Log review and monitoring procedures", + "Log review and monitoring results (review details, reports, etc.)", + "Access record inspection details for personal information", + "Criteria and results for verifying reasons for personal information downloads", + "Evidence of responses to detected anomalies" + ], + "NonComplianceCases": [ + "Case 1: Monitoring and alert policies (criteria) for abnormal access (e.g., early morning access on holidays, access via bypass routes) or abnormal behaviors (e.g., large-scale data queries or continuous small data queries) on information systems processing important information have not been established.", + "Case 2: Although periodic inspection/monitoring criteria for access and usage are established in internal guidelines or systems, there is no record of actual review of abnormal access or behavior.", + "Case 3: The personal information processor sets the inspection frequency of access records for personal information processing systems to once per quarter.", + "Case 4: The internal management plan for the personal information processor sets criteria for verifying reasons when more than 1,000 items of personal information are downloaded, but the reasons are not verified when such downloads occur." + ] + } + ] + }, + { + "Id": "2.9.6", + "Name": "Time Synchronization", + "Description": "To ensure the accuracy of logs and access records and provide reliable log analysis, the system time must be synchronized with a standard time and regularly maintained.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Control Requirements", + "Subdomain": "2.9. System and Service Operation Management", + "Section": "2.9.6 Time Synchronization", + "AuditChecklist": [ + "Is the system time synchronized with the standard time?", + "Is regular inspection conducted to ensure that time synchronization is functioning properly?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Time synchronization settings", + "Evidence of time synchronization for key systems" + ], + "NonComplianceCases": [ + "Case 1: The time of some critical systems (e.g., security systems, CCTV, etc.) is not synchronized with the standard time, and regular inspections for synchronization are not being conducted.", + "Case 2: Although internal NTP servers are configured for time synchronization, some systems are not synchronized, and there has been no cause analysis or response." + ] + } + ] + }, + { + "Id": "2.9.7", + "Name": "Reuse and Disposal of Information Assets", + "Description": "To prevent the recovery or regeneration of personal and critical information during the reuse and disposal process, secure reuse and disposal procedures for information assets must be established and implemented.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Control Requirements", + "Subdomain": "2.9. System and Service Operation Management", + "Section": "2.9.7 Reuse and Disposal of Information Assets", + "AuditChecklist": [ + "Are secure reuse and disposal procedures for information assets established and implemented?", + "When reusing or disposing of information assets and storage media, are personal and critical information processed to be irrecoverable?", + "If information assets and storage media are disposed of internally, are the disposal records maintained in a management ledger along with evidence of disposal?", + "If disposal is outsourced to an external company, are disposal procedures specified in the contract and is the complete disposal confirmed?", + "Are measures in place to protect data on storage media during maintenance, repairs, or replacements of systems and PCs?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 21 (Destruction of Personal Information)", + "Standards for Ensuring the Safety of Personal Information, Article 13 (Destruction of Personal Information)" + ], + "AuditEvidence": [ + "Procedures for the disposal and reuse of information assets", + "Storage media management ledger", + "Evidence of the disposal of information assets and storage media", + "Disposal-related outsourcing contracts for information assets and storage media" + ], + "NonComplianceCases": [ + "Case 1: Although the policy and procedure require the complete deletion of data using a data deletion program when reusing PCs used by personal information handlers, in practice, PCs are reused without complete deletion or are only formatted before reuse, indicating that procedures are not being followed.", + "Case 2: Although storage media are disposed of through an external company, the contract lacks details on secure disposal procedures and protective measures, and there is no verification or supervision of the disposal process or evidence of compliance.", + "Case 3: Instead of recording the serial numbers of disposed HDDs, the system names are recorded, or the disposal ledger is not maintained, making it impossible to verify the disposal history and traceability.", + "Case 4: Discarded hard disks are left unsecured in an area without locks, and the data has not been fully deleted." + ] + } + ] + }, + { + "Id": "2.10.1", + "Name": "Security System Operation", + "Description": "For each type of security system, an administrator must be designated, and operational procedures such as updating to the latest policies, modifying rule sets, and monitoring events must be established and implemented. The status of policy application for each security system must be managed.", + "Checks": [ + "organizations_delegated_administrators", + "shield_advanced_protection_in_classic_load_balancers", + "shield_advanced_protection_in_route53_hosted_zones", + "shield_advanced_protection_in_cloudfront_distributions", + "shield_advanced_protection_in_global_accelerators", + "shield_advanced_protection_in_associated_elastic_ips", + "shield_advanced_protection_in_internet_facing_load_balancers", + "ssm_managed_compliant_patching", + "secretsmanager_automatic_rotation_enabled", + "kms_cmk_are_used", + "kms_key_not_publicly_accessible", + "kms_cmk_rotation_enabled", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "macie_is_enabled", + "inspector2_is_enabled", + "inspector2_active_findings_exist", + "config_recorder_all_regions_enabled", + "cognito_user_pool_waf_acl_attached", + "fms_policy_compliant", + "ssmincidents_enabled_with_plans", + "cloudfront_distributions_using_waf", + "wafv2_webacl_logging_enabled", + "guardduty_no_high_severity_findings", + "guardduty_is_enabled", + "guardduty_centrally_managed", + "trustedadvisor_errors_and_warnings", + "elbv2_waf_acl_attached", + "securityhub_enabled", + "apigateway_restapi_waf_acl_attached", + "ec2_instance_port_ldap_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_instance_port_kerberos_exposed_to_internet", + "ec2_securitygroup_from_launch_wizard", + "ec2_securitygroup_not_used", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", + "ec2_securitygroup_default_restrict_traffic", + "ec2_instance_port_cifs_exposed_to_internet", + "ec2_instance_port_kafka_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", + "ec2_instance_port_mysql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_instance_internet_facing_with_instance_profile", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_instance_port_cassandra_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", + "ec2_instance_port_memcached_exposed_to_internet", + "ec2_networkacl_allow_ingress_any_port", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_instance_port_postgresql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", + "ec2_securitygroup_with_many_ingress_egress_rules", + "ec2_instance_port_oracle_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", + "ec2_instance_port_redis_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", + "ec2_instance_port_ftp_exposed_to_internet", + "ec2_networkacl_allow_ingress_tcp_port_22", + "ec2_instance_port_mongodb_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_instance_port_telnet_exposed_to_internet" + ], + "Attributes": [ + { + "Domain": "2. Security Control Requirements", + "Subdomain": "2.10. System and Service Security Management", + "Section": "2.10.1 Security System Operation", + "AuditChecklist": [ + "Has the organization established and implemented operational procedures for the security systems in use?", + "Is access to the security system administrators limited to a minimum, and is unauthorized access strictly controlled?", + "Has the organization established and implemented formal procedures for registering, modifying, and deleting policies for each security system?", + "Are exception policies for the security system managed according to procedures, and are users of exception policies managed with the minimum privileges?", + "Is the validity of the policies set on the security system periodically reviewed?", + "Has the organization installed and operated security systems that perform functions specified by law to prevent illegal access and data leakage in personal information processing systems?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 6 (Access Control)" + ], + "AuditEvidence": [ + "Security system configuration", + "Network configuration", + "Security system operational procedures", + "Firewall policies", + "Firewall policy setup/modification request forms", + "Exception list for the security system", + "Management screens for each security system (firewall, IPS, server access control, DLP, DRM, etc.)", + "Security system policy review history" + ], + "NonComplianceCases": [ + "Case 1: Regular reviews of the security policies for the intrusion prevention system were not conducted, resulting in unnecessary or excessively permissive policies.", + "Case 2: There are no procedures or criteria for applying, modifying, or deleting security policies, or such procedures exist but are not followed.", + "Case 3: The assignment and supervision of administrators for the security system were not properly implemented.", + "Case 4: Although internal guidelines stipulate that the information security officer must record and maintain the history of security policy changes for the security system, the policy management ledger was not periodically maintained, or the policies recorded in the ledger did not match those actually applied in the operating system." + ] + } + ] + }, + { + "Id": "2.10.2", + "Name": "Cloud Security", + "Description": "When using cloud services, protection measures must be established and implemented for administrator access and security settings to prevent unauthorized access and configuration errors that could lead to the leakage or exposure of critical information and personal data, depending on the service type (SaaS, PaaS, IaaS, etc.).", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protective Measure Requirements", + "Subdomain": "2.10. System and Service Security Management", + "Section": "2.10.2 Cloud Security", + "AuditChecklist": [ + "Is the responsibility and role for information protection and personal information protection clearly defined with the cloud service provider, and is it reflected in contracts (such as SLA)?", + "Are security risks based on the service type evaluated when using cloud services, and are security control policies established and implemented, including security configurations and setting standards, security setting changes and approval procedures, secure connection methods, and authority systems to prevent unauthorized access and configuration errors?", + "Are administrator privileges for cloud services granted minimally according to roles, and are enhanced protection measures such as strengthened authentication, encryption, access control, and audit logs applied to prevent unauthorized access and abuse of privileges?", + "Is the monitoring of cloud service security setting changes and operation status conducted, and is the appropriateness of these settings reviewed regularly?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Cloud service-related contracts and SLA", + "Cloud service risk analysis results", + "Cloud service security control policies", + "Cloud service administrator privilege assignment status", + "Cloud service architecture diagram", + "Cloud service security setting status", + "Cloud service security setting appropriateness review history" + ], + "NonComplianceCases": [ + "Case 1: The cloud service contract does not include responsibilities and roles related to security.", + "Case 2: Employees without a business need have been excessively granted permissions to change the security settings of the cloud service.", + "Case 3: Internal guidelines require security officer approval when changing access control rules in the private network of the cloud, but many access control rules were registered or changed without following the approval procedure.", + "Case 4: Due to security setting errors in the cloud service, internal log files were exposed to the internet." + ] + } + ] + }, + { + "Id": "2.10.3", + "Name": "Public Server Security", + "Description": "For servers exposed to external networks, protective measures must be established and implemented, including separating them from internal networks, conducting vulnerability assessments, access control, authentication, and establishing procedures for information collection, storage, and disclosure.", + "Checks": [ + "ssm_document_secrets", + "ssm_managed_compliant_patching", + "inspector2_active_findings_exist", + "cloudfront_distributions_https_enabled", + "cloudfront_distributions_using_deprecated_ssl_protocols", + "cloudfront_distributions_using_waf", + "lightsail_instance_public", + "lightsail_database_public", + "lightsail_static_ip_unused", + "elb_insecure_ssl_ciphers", + "elb_ssl_listeners", + "awslambda_function_no_secrets_in_code", + "awslambda_function_no_secrets_in_variables", + "awslambda_function_url_public", + "awslambda_function_url_cors_policy", + "awslambda_function_inside_vpc", + "awslambda_function_not_publicly_accessible", + "networkfirewall_in_all_vpc", + "apigatewayv2_api_authorizers_enabled", + "kafka_cluster_mutual_tls_authentication_enabled", + "vpc_subnet_separate_private_public", + "autoscaling_find_secrets_ec2_launch_configuration", + "elbv2_waf_acl_attached", + "elbv2_desync_mitigation_mode", + "elbv2_insecure_ssl_ciphers", + "elbv2_ssl_listeners", + "apigateway_restapi_public", + "apigateway_restapi_waf_acl_attached", + "apigateway_restapi_client_certificate_enabled", + "apigateway_restapi_authorizers_enabled", + "apigateway_restapi_public_with_authorizer", + "acm_certificates_expiration_check", + "route53_domains_privacy_protection_enabled", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_instance_port_kerberos_exposed_to_internet", + "ec2_instance_secrets_user_data", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", + "ec2_securitygroup_default_restrict_traffic", + "ec2_instance_port_cifs_exposed_to_internet", + "ec2_instance_port_kafka_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", + "ec2_instance_port_mysql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_instance_internet_facing_with_instance_profile", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_instance_port_cassandra_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", + "ec2_instance_port_memcached_exposed_to_internet", + "ec2_networkacl_allow_ingress_any_port", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_instance_port_postgresql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", + "ec2_securitygroup_with_many_ingress_egress_rules", + "ec2_instance_port_oracle_exposed_to_internet", + "ec2_launch_template_no_secrets", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", + "ec2_instance_imdsv2_enabled", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", + "ec2_instance_port_redis_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", + "ec2_instance_port_ftp_exposed_to_internet", + "ec2_networkacl_allow_ingress_tcp_port_22", + "ec2_instance_port_mongodb_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_instance_port_telnet_exposed_to_internet" + ], + "Attributes": [ + { + "Domain": "2. Protective Measure Requirements", + "Subdomain": "2.10. System and Service Security Management", + "Section": "2.10.3 Public Server Security", + "AuditChecklist": [ + "Are protective measures established and implemented for the operation of public servers?", + "Are public servers installed in a DMZ separated from internal networks and protected by security systems such as firewalls?", + "When posting or storing personal or sensitive information on public servers, are approval and posting procedures, including obtaining approval from the responsible person, established and followed?", + "Does the organization regularly check whether sensitive information is being exposed through websites and web servers, and if exposure is detected, are measures taken immediately to block it?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Network diagram", + "Website information disclosure procedures and history (e.g., requests, approvals, posting history)", + "Inspection history of personal and sensitive information exposure" + ], + "NonComplianceCases": [ + "Case 1: Due to vulnerabilities in publicly exposed websites, unauthorized individuals were able to access others' personal information through Google search.", + "Case 2: Although internal regulations require approval procedures before posting personal information on websites, there were multiple cases where personal information was posted without following these procedures.", + "Case 3: In web applications such as bulletin boards, it was possible to arbitrarily modify or delete posts made by others, or view password-protected posts." + ] + } + ] + }, + { + "Id": "2.10.4", + "Name": "Security for Electronic Transactions and FinTech", + "Description": "When providing electronic transaction and FinTech services, protection measures such as authentication and encryption must be established to prevent data leakage, data tampering, or fraud. The security of external systems, such as payment systems, must be checked when integrated.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.10. System and Service Security Management", + "Section": "2.10.4 Security for Electronic Transactions and FinTech", + "AuditChecklist": [ + "Are protection measures established and implemented to ensure the safety and reliability of transactions when providing electronic transaction and FinTech services?", + "Are protection measures established and implemented to protect transmitted information when integrating with external systems, such as payment systems, and is the security of the integration checked?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Protection measures for electronic transaction and FinTech services", + "Security review results for payment system integration" + ], + "NonComplianceCases": [ + "Case 1: While a contract was made with a payment service provider and integration was established, all payment-related information was transmitted in plain text through a specific URL without appropriate authentication or access restrictions.", + "Case 2: Although the external payment system was connected via a dedicated network, internal business systems were not properly controlled by firewalls or other security measures.", + "Case 3: Although internal guidelines required a security review by the information protection team before integrating external FinTech services, the review was skipped due to scheduling reasons when integrating a new FinTech service." + ] + } + ] + }, + { + "Id": "2.10.5", + "Name": "Secure Information Transmission", + "Description": "When transmitting personal or critical information to other organizations, a secure transmission policy must be established, and agreements must be made between organizations regarding management responsibilities, transmission methods, and technical protection measures for personal and critical information.", + "Checks": [ + "glue_database_connections_ssl_enabled", + "s3_bucket_secure_transport_policy", + "sns_subscription_not_using_http_endpoints", + "cloudfront_distributions_https_enabled", + "cloudfront_distributions_using_deprecated_ssl_protocols", + "opensearch_service_domains_https_communications_enforced", + "opensearch_service_domains_node_to_node_encryption_enabled", + "directoryservice_radius_server_security_protocol", + "elb_insecure_ssl_ciphers", + "elb_ssl_listeners", + "kafka_cluster_mutual_tls_authentication_enabled", + "kafka_cluster_in_transit_encryption_enabled", + "elbv2_insecure_ssl_ciphers", + "elbv2_ssl_listeners", + "elasticache_redis_cluster_in_transit_encryption_enabled", + "rds_instance_transport_encrypted", + "apigateway_restapi_client_certificate_enabled" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.10. System and Service Security Management", + "Section": "2.10.5 Secure Information Transmission", + "AuditChecklist": [ + "Has a secure transmission policy been established when transmitting personal and critical information to external organizations?", + "When exchanging personal and critical information between organizations for business purposes, are agreements and protection measures for secure transmission established and implemented?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Information transmission agreement or contract", + "Technical standards for information transmission", + "System diagrams and interface definitions related to information transmission" + ], + "NonComplianceCases": [ + "Case 1: Although a dedicated network or VPN is applied when integrating with external organizations, there is inadequate management of the timing, method, responsible person, transmitted information, and legal basis for each integration.", + "Case 2: There is a lack of implementation of security reviews, security standards, and action plans for using weak encryption algorithms (e.g., DES, 3DES) or decrypting during intermediate transmission stages." + ] + } + ] + }, + { + "Id": "2.10.6", + "Name": "Security for Business Devices", + "Description": "When connecting devices such as PCs and mobile devices to the network for business purposes, access control measures such as device authentication, approval, access scope, and security settings must be established and periodically checked.", + "Checks": [ + "workspaces_vpc_2private_1public_subnets_nat", + "workspaces_volume_encryption_enabled", + "appstream_fleet_maximum_session_duration", + "appstream_fleet_default_internet_access_disabled", + "appstream_fleet_session_idle_disconnect_timeout", + "appstream_fleet_session_disconnect_timeout" + ], + "Attributes": [ + { + "Domain": "2. Protection Measure Requirements", + "Subdomain": "2.10. System and Service Security Management", + "Section": "2.10.6 Security for Business Devices", + "AuditChecklist": [ + "Are security control policies, such as device authentication, approval, access scope, and security settings, established and implemented for devices used for business purposes, such as PCs, laptops, virtual PCs, and tablets?", + "Are policies established and implemented to prevent the leakage of personal and critical information through business devices by prohibiting the use of file-sharing programs, limiting shared settings, and controlling wireless network usage?", + "Are security measures applied to prevent the leakage of personal and critical information in case of loss or theft of business mobile devices?", + "Is the appropriateness of access control measures for business devices periodically reviewed?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 6 (Access Control)" + ], + "AuditEvidence": [ + "Security control guidelines and procedures for business devices", + "Registration status of business devices", + "Security settings for business devices", + "Authentication and approval history for business devices", + "Security check status for business devices" + ], + "NonComplianceCases": [ + "Case 1: Although laptops and tablet PCs are used for business purposes, there are no policies established for device approval, usage scope, approval procedures, or authentication methods.", + "Case 2: The security management guidelines for mobile devices prohibit the use of mobile devices for business purposes unless specifically approved, but unapproved mobile devices are still being used to access internal information systems.", + "Case 3: Personal and critical information is handled on mobile devices, but security measures such as password protection are not applied to prevent leaks due to loss or theft.", + "Case 4: Although internal regulations prohibit the use of shared folders on business devices, periodic checks are not conducted, resulting in excessive use of shared folders on many business devices." + ] + } + ] + }, + { + "Id": "2.10.7", + "Name": "Management of Removable Media", + "Description": "Procedures must be established and implemented to prevent the leakage of personal or important information or infection by malware through removable media. Removable media containing personal or important information must be stored in a secure location.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Security Control Requirements", + "Subdomain": "2.10. System and Service Security Management", + "Section": "2.10.7 Management of Removable Media", + "AuditChecklist": [ + "Are policies and procedures established and implemented for handling (use), storage, disposal, and reuse of removable media such as external hard drives, USB memory, and CDs?", + "Is the status of ownership, use, and management of removable media periodically checked?", + "Is the use of removable media restricted in controlled areas, such as key information systems or important restricted areas?", + "Are measures in place to prevent the infection of malware and the leakage of important information through removable media?", + "Are removable media containing personal or important information stored in a secure location with locking mechanisms?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 10 (Physical Safety Measures)" + ], + "AuditEvidence": [ + "Policy on blocking removable media (USB, CD, etc.)", + "Removable media management log", + "Inspection records of removable media status" + ], + "NonComplianceCases": [ + "Case 1: Although there is a policy restricting the use of removable media in controlled areas like server rooms, several cases were found where removable media was used without following the exception approval process, and periodic inspections of the removable media management status were not conducted, resulting in inadequate updates to the management log.", + "Case 2: Removable media containing personal information was not stored in a secure location with locking mechanisms and was left unattended in office drawers.", + "Case 3: Although a solution to control removable media was implemented, some users were granted write access without appropriate approval procedures.", + "Case 4: Some common PCs and IT equipment in the server room allowed writing to standard USB memory devices, but controls such as media import and usage restrictions, usage history records, and reviews were not applied." + ] + } + ] + }, + { + "Id": "2.10.8", + "Name": "Patch Management", + "Description": "To prevent security incidents due to vulnerabilities in software, operating systems, or security systems, the latest patches must be applied. However, if the application of the latest patches is difficult due to service impact considerations, alternative measures must be implemented.", + "Checks": [ + "ssm_managed_compliant_patching", + "cloudfront_distributions_using_deprecated_ssl_protocols", + "opensearch_service_domains_updated_to_the_latest_service_software_version", + "redshift_cluster_automatic_upgrades", + "awslambda_function_using_supported_runtimes", + "eks_cluster_uses_a_supported_version", + "kafka_cluster_uses_latest_version", + "dms_instance_minor_version_upgrade_enabled", + "elasticache_redis_cluster_auto_minor_version_upgrades", + "rds_instance_deprecated_engine_version", + "rds_cluster_minor_version_upgrade_enabled", + "rds_instance_minor_version_upgrade_enabled", + "ec2_instance_account_imdsv2_enabled", + "ec2_instance_older_than_specific_days" + ], + "Attributes": [ + { + "Domain": "2. Security Control Requirements", + "Subdomain": "2.10. System and Service Security Management", + "Section": "2.10.8 Patch Management", + "AuditChecklist": [ + "Are patch management policies and procedures for operating systems (OS) and software established and implemented according to the characteristics and importance of each asset, such as servers, network systems, security systems, and PCs?", + "Are the patch status of installed OS and software on key servers, network systems, and security systems periodically managed?", + "If applying the latest patches to address vulnerabilities is difficult due to service impact, are alternative measures implemented?", + "Is the application of patches via public internet access restricted for key servers, network systems, and security systems?", + "When using a patch management system, are sufficient protection measures, such as access control, established?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 9 (Prevention of Malware, etc.)" + ], + "AuditEvidence": [ + "Patch management policies and procedures", + "Patch status of each system", + "Impact analysis results related to patch application" + ], + "NonComplianceCases": [ + "Case 1: In some systems, OS patches were not applied for a long period without valid reasons or approval from the responsible personnel.", + "Case 2: Some systems were using OS versions that were no longer supported (EOS), but no response plans or alternative measures were in place.", + "Case 3: Although the latest patches were applied to commercial software and OS, there were no procedures or personnel assigned to confirm and apply the latest patches for open-source programs (e.g., OpenSSL, OpenSSH, Apache), resulting in the lack of application of the latest security patches." + ] + } + ] + }, + { + "Id": "2.10.9", + "Name": "Malware Control", + "Description": "To protect personal and important information, information systems, and business terminals from malware such as viruses, worms, Trojans, and ransomware, prevention, detection, and response measures must be established and implemented.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Security Control Requirements", + "Subdomain": "2.10. System and Service Security Management", + "Section": "2.10.9 Malware Control", + "AuditChecklist": [ + "Are protection measures established and implemented to protect information systems and business terminals from malware such as viruses, worms, Trojans, and ransomware?", + "Are prevention and detection activities for the latest malware continuously performed using security programs such as antivirus software?", + "Are security programs such as antivirus software kept up to date, and are emergency security updates performed when necessary?", + "Are procedures for response, such as minimizing the spread of malware and mitigating damage, established and implemented when malware infections are discovered?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 9 (Prevention of Malware, etc.)" + ], + "AuditEvidence": [ + "Guidelines, procedures, and manuals for malware response", + "Antivirus program installation status", + "Antivirus program configuration screens", + "Malware response history (e.g., response reports)" + ], + "NonComplianceCases": [ + "Case 1: Some PCs and servers do not have antivirus software installed, or the antivirus engine has not been updated to the latest version for a long time.", + "Case 2: Although users can change the antivirus program settings (e.g., real-time scanning, scheduled scanning, update settings) at their discretion, no additional protection measures were established to address this.", + "Case 3: Insufficient protection measures, such as access control, were in place for the central antivirus management system, leading to the possibility of security incidents through the central management system, or no integrity verification of the antivirus pattern was performed, making it possible for malware to spread through malicious users.", + "Case 4: Although multiple malware infections were confirmed on some internal network PCs and servers, there was no confirmation of the infection status, infection routes, cause analysis, or resulting actions." + ] + } + ] + }, + { + "Id": "2.11.1", + "Name": "Establishment of Incident Prevention and Response System", + "Description": "To prevent incidents such as security breaches and personal information leaks, and to respond quickly and effectively in the event of an incident, the organization must establish procedures for detecting, responding to, analyzing, and sharing internal and external intrusion attempts. In addition, the organization must establish a cooperative system with relevant external institutions and experts.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protective Measures Requirements", + "Subdomain": "2.11. Incident Prevention and Response", + "Section": "2.11.1 Establishment of Incident Prevention and Response System", + "AuditChecklist": [ + "Has the organization established procedures and systems to prevent security breaches and personal information leaks and to respond quickly and effectively when incidents occur?", + "If the organization is operating an incident response system through an external institution, such as a security monitoring service, are the details of the incident response procedures reflected in the contract?", + "Has the organization established a cooperative system with external experts, specialized companies, or institutions for monitoring, responding to, and handling security incidents?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 34 (Notification and Reporting of Personal Information Leaks, etc.)", + "Information and Communications Network Act, Article 48-3 (Reporting of Security Incidents), Article 48-4 (Analysis of Causes of Security Incidents, etc.)" + ], + "AuditEvidence": [ + "Incident response guidelines/procedures/manual", + "Incident response organization chart and emergency contact list", + "Security monitoring service contract (SLA, etc.)" + ], + "NonComplianceCases": [ + "Case 1: Failure to clearly define the incident response organization and procedures for responding to security breaches.", + "Case 2: Although internal guidelines and procedures specify incident response steps for different phases (before, during, after detection, recovery, reporting, etc.), some or all of the response and recovery procedures for specific incident types and severity levels are not established.", + "Case 3: Failure to keep the incident response organization chart and emergency contact list up to date, or the roles and responsibilities of each team member are not clearly defined.", + "Case 4: Errors or outdated information in the contact details for external agencies responsible for incident reporting, notification, and cooperation, or failure to keep some agency details current.", + "Case 5: When outsourcing incident detection and response to an external security monitoring company or related institution, failure to clearly define the roles and responsibilities for both parties in the contract or SLA.", + "Case 6: Although incident response procedures are in place, they do not meet the legal requirements for reporting and notifying personal data breaches, such as criteria and timing." + ] + } + ] + }, + { + "Id": "2.11.2", + "Name": "Vulnerability Inspection and Remediation", + "Description": "Regular vulnerability inspections must be conducted to verify whether information systems have exposed vulnerabilities, and any identified vulnerabilities must be promptly addressed. In addition, the organization must continuously monitor for new security vulnerabilities, assess their impact on the information systems, and take necessary actions.", + "Checks": [ + "codebuild_project_no_secrets_in_variables", + "codebuild_project_source_repo_url_no_sensitive_credentials", + "ssm_document_secrets", + "cloudwatch_log_group_no_secrets_in_logs", + "inspector2_is_enabled", + "inspector2_active_findings_exist", + "ecr_registry_scan_images_on_push_enabled", + "ecr_repositories_scan_images_on_push_enabled", + "ecr_repositories_scan_vulnerabilities_in_latest_image", + "ecs_task_definitions_no_environment_secrets", + "awslambda_function_no_secrets_in_code", + "awslambda_function_no_secrets_in_variables", + "autoscaling_find_secrets_ec2_launch_configuration", + "guardduty_no_high_severity_findings", + "guardduty_is_enabled", + "guardduty_centrally_managed", + "trustedadvisor_premium_support_plan_subscribed", + "trustedadvisor_errors_and_warnings", + "securityhub_enabled", + "cloudformation_stack_outputs_find_secrets", + "ec2_instance_secrets_user_data", + "ec2_launch_template_no_secrets", + "ec2_instance_imdsv2_enabled" + ], + "Attributes": [ + { + "Domain": "2. Protective Measures Requirements", + "Subdomain": "2.11. Incident Prevention and Response", + "Section": "2.11.2 Vulnerability Inspection and Remediation", + "AuditChecklist": [ + "Has the organization established and implemented procedures for conducting regular vulnerability inspections of information systems?", + "Are actions taken to address identified vulnerabilities, and are the results reported to the responsible authorities?", + "Does the organization continuously monitor for new security vulnerabilities and assess their impact on the information systems, taking appropriate actions?", + "Is a record of vulnerability inspections maintained, and are protective measures implemented to address recurring vulnerabilities identified in previous years?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 4 (Establishment, Implementation, and Inspection of Internal Management Plans), Article 6 (Access Control)" + ], + "AuditEvidence": [ + "Vulnerability inspection plan", + "Vulnerability inspection report (for web, mobile apps, servers, network systems, security systems, DBMS, etc.)", + "Vulnerability inspection records", + "Vulnerability remediation plan", + "Vulnerability remediation completion report", + "Penetration testing plan/results report" + ], + "NonComplianceCases": [ + "Case 1: Although internal regulations require annual technical vulnerability inspections for major systems, some major systems were excluded from the inspection.", + "Case 2: Failure to implement corrective actions for identified vulnerabilities, or failure to provide justification and approval records for vulnerabilities that cannot be addressed promptly." + ] + } + ] + }, + { + "Id": "2.11.3", + "Name": "Abnormal Behavior Analysis and Monitoring", + "Description": "To quickly detect and respond to intrusion attempts, personal information leakage attempts, and fraudulent activities from internal or external sources, the organization must collect and analyze network and data flows. Post-monitoring and inspection actions must be timely.", + "Checks": [ + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_group_kms_encryption_enabled", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_cross_account_sharing_disabled", + "cloudwatch_log_group_retention_policy_specific_days_enabled", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_log_group_no_secrets_in_logs", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", + "cognito_user_pool_client_prevent_user_existence_errors", + "fms_policy_compliant", + "cloudtrail_insights_exist", + "cloudtrail_threat_detection_enumeration", + "networkfirewall_in_all_vpc", + "vpc_flow_logs_enabled", + "guardduty_no_high_severity_findings", + "trustedadvisor_errors_and_warnings", + "securityhub_enabled" + ], + "Attributes": [ + { + "Domain": "2. Protection Measures Requirements", + "Subdomain": "2.11. Incident Prevention and Response", + "Section": "2.11.3 Abnormal Behavior Analysis and Monitoring", + "AuditChecklist": [ + "Is the organization collecting, analyzing, and monitoring network traffic, data flows, and event logs from major information systems, applications, networks, and security systems to detect abnormal behaviors such as intrusion attempts, personal information leakage attempts, or fraudulent activities?", + "Has the organization defined criteria and thresholds to determine abnormal behaviors, and is follow-up action, such as the determination and investigation of abnormal activities, taken in a timely manner?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 6 (Access Control)" + ], + "AuditEvidence": [ + "Status of abnormal behavior analysis and monitoring", + "Evidence of responses taken when abnormal behaviors were detected" + ], + "NonComplianceCases": [ + "Case 1: Failure to establish a real-time or regular monitoring system and procedures to detect intrusion attempts on servers, networks, databases, and security systems from external sources.", + "Case 2: Although the organization has outsourced monitoring tasks to an external security monitoring agency, there is no record of reviewing the reports provided by the agency, and the organization does not have its own monitoring system for systems excluded from the outsourced service.", + "Case 3: Although abnormal traffic exceeding internally defined thresholds has been continuously detected, no response measures have been taken." + ] + } + ] + }, + { + "Id": "2.11.4", + "Name": "Incident Response Training and Improvement", + "Description": "The organization must conduct at least one simulation training per year based on scenarios to ensure that employees and stakeholders are familiar with the procedures for responding to security incidents and personal information leakage incidents. The response system must be improved based on the training results.", + "Checks": [ + "ssmincidents_enabled_with_plans" + ], + "Attributes": [ + { + "Domain": "2. Protection Measures Requirements", + "Subdomain": "2.11. Incident Prevention and Response", + "Section": "2.11.4 Incident Response Training and Improvement", + "AuditChecklist": [ + "Has the organization established a simulation training plan for responding to security incidents and personal information leakage incidents, and are such training exercises conducted at least once a year?", + "Is the organization reflecting the results of security incident and personal information leakage incident training to improve its response system?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "Simulation training plan for responding to security and personal information leakage incidents", + "Simulation training result reports for responding to security and personal information leakage incidents", + "Incident response procedures" + ], + "NonComplianceCases": [ + "Case 1: Failure to conduct simulation training or provide related training plans and result reports.", + "Case 2: Although an annual simulation training plan for security incidents was established, it was not conducted within the planned period without valid reason or approval.", + "Case 3: Simulation training was conducted, but it was not performed according to the procedures and forms defined in the internal guidelines." + ] + } + ] + }, + { + "Id": "2.11.5", + "Name": "Incident Response and Recovery", + "Description": "When signs of or actual incidents of security breaches or personal information leakage are detected, the organization must comply with legal notification and reporting obligations, respond and recover promptly according to established procedures, and analyze the incident to establish preventive measures to reflect in the response system.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. Protection Measures Requirements", + "Subdomain": "2.11. Incident Prevention and Response", + "Section": "2.11.5 Incident Response and Recovery", + "AuditChecklist": [ + "When signs of or actual incidents of security breaches or personal information leakage are detected, is the organization responding and reporting promptly according to the defined incident response procedures?", + "Is the organization notifying data subjects and reporting to relevant authorities as required by law in case of a personal information breach?", + "After the incident is resolved, is the organization analyzing the cause, reporting the results, and sharing them with relevant departments and personnel?", + "Is the organization utilizing the information obtained from incident analysis to establish preventive measures to prevent similar incidents from recurring, and if necessary, modifying its incident response procedures?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 34 (Notification and Reporting of Personal Information Leakage)", + "Information and Communications Network Act, Article 48-3 (Reporting of Security Incidents), Article 48-4 (Analysis of Causes of Security Incidents)" + ], + "AuditEvidence": [ + "Incident response procedures", + "Incident response reports", + "Incident management logs", + "Personal information leakage reports", + "Emergency contact list" + ], + "NonComplianceCases": [ + "Case 1: Although internal incident response guidelines require that security incidents be reported to the internal information protection committee and relevant departments, the department in charge responded to the incident independently without reporting to the information protection committee or relevant departments.", + "Case 2: Although a service outage suspected to be caused by a DDoS attack occurred recently, the organization did not analyze the cause or establish preventive measures.", + "Case 3: Although a personal information leakage incident occurred due to external hacking, notification and reporting were not made within 72 hours, citing the small number of affected personal information records as the reason.", + "Case 4: Although personal information of more than 1,000 individuals was leaked due to an employee's mistake on the company website, the affected data subjects were not notified." + ] + } + ] + }, + { + "Id": "2.12.1", + "Name": "Safety Measures for Disaster Preparedness", + "Description": "Identify types of disasters that could threaten the operational continuity of the organization's core services and systems, such as natural disasters, communication or power failures, and hacking. Analyze the expected scale of damage and impact for each type, define the recovery time objective (RTO) and recovery point objective (RPO), and establish a disaster recovery system including recovery strategies, emergency recovery teams, emergency contact networks, and recovery procedures.", + "Checks": [ + "kms_cmk_not_deleted_unintentionally", + "s3_bucket_lifecycle_enabled", + "s3_bucket_object_versioning", + "s3_bucket_cross_region_replication", + "s3_bucket_object_lock", + "documentdb_cluster_backup_enabled", + "documentdb_cluster_cloudwatch_log_export", + "lightsail_instance_automated_snapshots", + "neptune_cluster_multi_az", + "neptune_cluster_deletion_protection", + "dynamodb_tables_pitr_enabled", + "elb_is_in_multiple_az", + "elb_cross_zone_load_balancing_enabled", + "redshift_cluster_automated_snapshot", + "drs_job_exist", + "dlm_ebs_snapshot_lifecycle_policy_exists", + "vpc_subnet_different_az", + "vpc_different_regions", + "dms_instance_multi_az_enabled", + "autoscaling_group_multiple_az", + "elbv2_is_in_multiple_az", + "elbv2_deletion_protection", + "backup_reportplans_exist", + "backup_plans_exist", + "elasticache_redis_cluster_backup_enabled", + "elasticache_redis_cluster_multi_az_enabled", + "rds_instance_multi_az", + "rds_instance_deletion_protection", + "rds_cluster_deletion_protection", + "rds_cluster_multi_az", + "rds_instance_backup_enabled", + "efs_have_backup_enabled", + "ec2_ebs_volume_snapshots_exists" + ], + "Attributes": [ + { + "Domain": "2. Protective Measure Requirements", + "Subdomain": "2.12. Disaster Recovery", + "Section": "2.12.1 Safety Measures for Disaster Preparedness", + "AuditChecklist": [ + "Has the organization identified IT disaster types that could threaten the continuity of core services (businesses) and analyzed the expected scale of damage and impact on operations to identify core IT services (businesses) and systems?", + "Has the organization defined recovery time objectives (RTO) and recovery point objectives (RPO) based on the importance and characteristics of core IT services and systems?", + "Has the organization established and implemented disaster recovery plans, including recovery strategies, emergency recovery teams, emergency contact networks, and recovery procedures, to ensure the continuity of core services and systems during disasters?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 29 (Obligation to Take Safety Measures)", + "Standards for Ensuring the Safety of Personal Information, Article 11 (Safety Measures for Disaster Preparedness)" + ], + "AuditEvidence": [ + "IT disaster recovery guidelines/procedures", + "IT disaster recovery plans (including RTO and RPO definitions)", + "Emergency contact list", + "Crisis response manual for personal information processing systems" + ], + "NonComplianceCases": [ + "Case 1: The IT disaster recovery procedures lack critical details such as the definition of IT disaster recovery teams and roles, emergency contact systems, and recovery procedures and methods.", + "Case 2: Although a backup center has been established to ensure the continuity of information systems and minimize damage during emergencies, the relevant policies do not include disaster recovery procedures using the backup center, making disaster recovery tests and actual recovery efforts ineffective.", + "Case 3: Recovery time objectives for some critical systems related to service operations have not been defined, and appropriate recovery strategies are not in place.", + "Case 4: The disaster recovery guidelines do not define the recovery priorities, RTO, or RPO for IT services or systems.", + "Case 5: Unrealistic recovery objectives have been set, either too high or too low, and the RPO and backup policies (e.g., targets, frequency) are not appropriately linked, making it difficult to ensure the effectiveness of recovery." + ] + } + ] + }, + { + "Id": "2.12.2", + "Name": "Disaster Recovery Testing and Improvement", + "Description": "Regularly test the adequacy of the disaster recovery strategies and plans, and supplement the recovery strategies and plans based on test results, changes in the information system environment, and legal requirements.", + "Checks": [ + "kms_cmk_not_deleted_unintentionally", + "s3_bucket_lifecycle_enabled", + "s3_bucket_object_versioning", + "s3_bucket_cross_region_replication", + "s3_bucket_object_lock", + "documentdb_cluster_backup_enabled", + "documentdb_cluster_cloudwatch_log_export", + "lightsail_instance_automated_snapshots", + "neptune_cluster_multi_az", + "neptune_cluster_deletion_protection", + "dynamodb_tables_pitr_enabled", + "elb_is_in_multiple_az", + "elb_cross_zone_load_balancing_enabled", + "redshift_cluster_automated_snapshot", + "drs_job_exist", + "dlm_ebs_snapshot_lifecycle_policy_exists", + "vpc_subnet_different_az", + "vpc_different_regions", + "dms_instance_multi_az_enabled", + "autoscaling_group_multiple_az", + "elbv2_is_in_multiple_az", + "elbv2_deletion_protection", + "backup_reportplans_exist", + "backup_plans_exist", + "elasticache_redis_cluster_backup_enabled", + "elasticache_redis_cluster_multi_az_enabled", + "rds_instance_multi_az", + "rds_instance_deletion_protection", + "rds_cluster_deletion_protection", + "rds_cluster_multi_az", + "rds_instance_backup_enabled", + "efs_have_backup_enabled", + "ec2_ebs_volume_snapshots_exists" + ], + "Attributes": [ + { + "Domain": "2. Protective Measure Requirements", + "Subdomain": "2.12. Disaster Recovery", + "Section": "2.12.2 Disaster Recovery Testing and Improvement", + "AuditChecklist": [ + "Has the organization established and implemented disaster recovery test plans to evaluate the effectiveness of the established IT disaster recovery system?", + "Are the disaster recovery strategies and plans regularly reviewed and supplemented to reflect test results, changes in the information system environment, and legal requirements?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "IT disaster recovery procedures", + "IT disaster recovery test plans", + "IT disaster recovery test results" + ], + "NonComplianceCases": [ + "Case 1: Disaster recovery drills were not planned or conducted, and the related plans and result reports are not available.", + "Case 2: Although a disaster recovery drill plan was established, it was not conducted as planned or approved, and the related result reports are missing.", + "Case 3: Disaster recovery drills were conducted, but they did not follow the procedures and forms outlined in the internal guidelines, making it difficult to evaluate the adequacy and effectiveness of the disaster recovery procedures." + ] + } + ] + }, + { + "Id": "3.1.1", + "Name": "Collection and Use of Personal Information", + "Description": "Personal information must be collected and used lawfully and fairly. When collecting personal information based on the consent of the data subject, such consent must be obtained through legal means. Additionally, when collecting personal information from children under the age of 14, consent must be obtained from their legal representative, and it must be verified that such consent was given by the legal representative.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.1. Protection Measures during Personal Information Collection", + "Section": "3.1.1 Collection and Use of Personal Information", + "AuditChecklist": [ + "When collecting personal information, is it collected in accordance with lawful requirements such as obtaining the data subject’s consent, complying with legal obligations, or concluding and fulfilling contracts?", + "When obtaining consent from the data subject for the collection of personal information, are the method and timing of obtaining consent appropriate?", + "When obtaining consent from the data subject for the collection of personal information, are the relevant details clearly communicated, and are significant points required by law highlighted in a way that is easy to understand?", + "When collecting, using, or providing personal information from children under the age of 14, are necessary details notified to their legal representatives, and is consent obtained?", + "When obtaining the consent of a legal representative, is only the minimum necessary personal information collected, and are procedures and methods in place to verify the qualifications of the legal representative?", + "When notifying children under the age of 14 about matters related to the processing of their personal information, are the notifications presented in a format and language that is clear and easy to understand?", + "Are records of consent obtained from data subjects and legal representatives being retained?", + "For personal information that can be processed without the consent of the data subject, are the relevant items and legal basis for processing disclosed in the privacy policy or communicated to the data subject separately from the personal information processed with consent?", + "When personal information is used for additional purposes without the consent of the data subject, are criteria established and implemented to assess the relevance to the original purpose, predictability, impact on the data subject, and safety measures? If additional usage continues to occur, are these criteria disclosed in the privacy policy and regularly reviewed?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 15 (Collection and Use of Personal Information), Article 22 (Methods for Obtaining Consent), Article 22-2 (Protection of Personal Information of Children)", + "Notice on the Processing of Personal Information" + ], + "AuditEvidence": [ + "Online personal information collection forms (e.g., website sign-up screens, mobile app registration screens, event participation forms)", + "Offline personal information collection forms (e.g., membership application forms)", + "Records of personal information collection consent (e.g., member databases)", + "Records of legal guardian consent", + "Privacy policy" + ], + "NonComplianceCases": [ + "Case 1: A personal information processor subject to the Personal Information Protection Act failed to include the 'right to refuse consent and the consequences of refusal' in the notifications when obtaining consent to collect personal information.", + "Case 2: During the process of obtaining consent for the collection of personal information, the items of personal information to be collected were not specified in detail, and were instead described in general terms like 'etc.'", + "Case 3: On a shopping mall website, personal information necessary for membership registration was collected alongside payment and delivery information required for future purchases, even though such information was not necessary at the time of registration.", + "Case 4: Personal information (e.g., name, email, phone number) was collected through Q&A boards without obtaining the data subject's consent.", + "Case 5: Personal information of children under the age of 14 was collected without obtaining the consent of their legal guardians.", + "Case 6: Although the service was not intended for children under 14, some members were under 14 because the website did not check birthdates during registration, allowing them to register without legal guardian consent.", + "Case 7: The procedure for verifying the authenticity of the legal representative was insufficient, allowing individuals who were not legal guardians to provide consent.", + "Case 8: Personal information (e.g., name, phone number) of legal guardians was collected for the purpose of obtaining their consent to collect personal information from children under the age of 14, but the consent of the legal guardian was not confirmed for an extended period, and the information was retained without being destroyed.", + "Case 9: Personal information of children under 14 was collected based on the consent of their legal guardians, but records of this consent were not maintained, making it impossible to verify the details related to legal guardian consent (e.g., legal guardian’s name, time of consent)." + ] + } + ] + }, + { + "Id": "3.1.2", + "Name": "Restrictions on the Collection of Personal Information", + "Description": "When collecting personal information, only the minimum amount of personal information necessary for the intended purpose may be collected, and the data subject must not be denied the provision of goods or services for refusing to consent to optional matters.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.1. Protection Measures during Personal Information Collection", + "Section": "3.1.2 Restrictions on the Collection of Personal Information", + "AuditChecklist": [ + "When collecting personal information, is only the minimum amount of information necessary for the intended purpose being collected?", + "When collecting personal information based on the data subject’s consent, is the data subject clearly informed that they can refuse to consent to the collection of additional personal information beyond the minimum required?", + "Is the data subject not denied goods or services for refusing to consent to the collection of additional personal information beyond the minimum necessary for the intended purpose?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 16 (Restrictions on the Collection of Personal Information), Article 22 (Methods for Obtaining Consent)" + ], + "AuditEvidence": [ + "Online personal information collection forms (e.g., website sign-up screens, event participation forms)", + "Offline personal information collection forms (e.g., membership application forms)", + "Privacy policy" + ], + "NonComplianceCases": [ + "Case 1: Although personal information was being collected based on the fulfillment of a contract, excessive personal information not essential to the fulfillment of the contract was being collected.", + "Case 2: During the process of obtaining consent from the data subject for optional information, the data subject was not explicitly informed that they could refuse to provide such information.", + "Case 3: Although the sign-up form distinguished between required and optional information, the data subject was not clearly informed that they could complete registration without providing optional information (e.g., there was no indication on the personal information entry form of which fields were required and which were optional).", + "Case 4: On the website registration page, the data subject was unable to proceed or complete registration if they refused to provide optional information or consent to optional matters.", + "Case 5: During the hiring process, excessive personal information unrelated to the job position (e.g., family details) was collected." + ] + } + ] + }, + { + "Id": "3.1.3", + "Name": "Restrictions on the Processing of Resident Registration Numbers", + "Description": "Resident registration numbers may not be collected, used, or processed unless there is a legal basis for doing so. Even when the processing of resident registration numbers is permitted, alternative methods must be provided, such as through an internet website.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.1. Protection Measures during Personal Information Collection", + "Section": "3.1.3 Restrictions on the Processing of Resident Registration Numbers", + "AuditChecklist": [ + "Are resident registration numbers only processed when there is a clear legal basis?", + "Is the legal provision that forms the basis for the collection of resident registration numbers clearly identified?", + "When processing resident registration numbers under a legal basis, does the organization provide a method for data subjects to register without using their resident registration number during the membership registration process on an internet website?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 24-2 (Restrictions on the Processing of Resident Registration Numbers)", + "Information and Communications Network Act, Article 23-2 (Restrictions on the Use of Resident Registration Numbers)" + ], + "AuditEvidence": [ + "Personal information collection forms (e.g., website sign-up screens, event participation forms, membership application forms)", + "Online personal information collection forms (alternative registration methods for identity verification)", + "Evidence of legal grounds for processing resident registration numbers", + "Privacy policy" + ], + "NonComplianceCases": [ + "Case 1: Resident registration numbers were collected for simple membership management purposes, such as identity verification, during website sign-up based on the data subject's consent.", + "Case 2: Resident registration numbers were collected based on provisions in enforcement rules or local ordinances.", + "Case 3: The last 6 digits of the resident registration number were collected for identity verification, such as during password recovery, without any legal basis.", + "Case 4: Resident registration numbers were collected from job applicants during the hiring process without a legal basis.", + "Case 5: Resident registration numbers were collected during customer service inquiries at a call center for identity verification purposes.", + "Case 6: Even when there was a legal basis for the collection of resident registration numbers, alternative registration methods were not provided during the membership registration process on the website, and resident registration numbers were required for identity verification and membership registration." + ] + } + ] + }, + { + "Id": "3.1.4", + "Name": "Restriction on Processing of Sensitive and Unique Identifying Information", + "Description": "In order to process sensitive information and unique identifying information (excluding resident registration numbers), separate consent from the data subject must be obtained unless the processing is specifically required or permitted by law.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Personal Information Processing Requirements", + "Subdomain": "3.1. Protection Measures for Personal Information Collection", + "Section": "3.1.4 Restriction on Processing of Sensitive and Unique Identifying Information", + "AuditChecklist": [ + "Is sensitive information processed only with the separate consent of the data subject or when legally required?", + "Is unique identifying information (excluding resident registration numbers) processed only with the separate consent of the data subject or when there is a specific legal basis?", + "If there is a risk of invasion of privacy due to the disclosure of sensitive information during the provision of goods or services, is the data subject clearly informed of the possibility of such disclosure and how to opt for non-disclosure before the provision of goods or services?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 23 (Restrictions on Processing of Sensitive Information), Article 24 (Restrictions on Processing of Unique Identifying Information)" + ], + "AuditEvidence": [ + "Online personal information collection forms (e.g., membership sign-up pages, event participation forms)", + "Offline personal information collection forms (e.g., membership application forms)", + "Privacy policy" + ], + "NonComplianceCases": [ + "Case 1: Collecting sensitive information such as disability status for discounts or benefits for disabled individuals, and obtaining blanket consent for all personal information items.", + "Case 2: Collecting foreign registration numbers only from foreigners during membership registration, and obtaining blanket consent for all personal information items.", + "Case 3: When obtaining separate consent for the collection of sensitive or unique identifying information, failing to inform or incorrectly informing the data subject about the four key points that must be disclosed (e.g., the right to refuse consent and the consequences of refusal)." + ] + } + ] + }, + { + "Id": "3.1.5", + "Name": "Indirect Collection of Personal Information", + "Description": "When collecting personal information from sources other than the data subject or when receiving personal information from a third party, only the minimum amount of personal information necessary for the task should be collected or received. If there is a legal basis or at the request of the data subject, the source of the personal information, the purpose of processing, and the right to request a suspension of processing must be disclosed.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Personal Information Processing Requirements", + "Subdomain": "3.1. Protection Measures for Personal Information Collection", + "Section": "3.1.5 Indirect Collection of Personal Information", + "AuditChecklist": [ + "When receiving personal information from a third party, is it clearly stated in the contract that the responsibility for obtaining consent for the collection of personal information lies with the party providing the information?", + "When collecting personal information from public media or places, is the collection limited to the scope recognized as having the data subject's consent, based on common societal standards?", + "Even for personal information collected or generated through automated collection devices during the process of providing services, is the principle of minimum collection applied?", + "When personal information is collected from a source other than the data subject and the data subject requests it, is the required information immediately provided to the data subject?", + "When personal information collected from a source other than the data subject meets legal requirements in terms of type or scale, is the required information provided to the data subject?", + "Is there a record of informing the data subject about the source of personal information, and is this record maintained until the personal information is destroyed?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 16 (Restrictions on the Collection of Personal Information), Article 19 (Restrictions on Use and Provision of Personal Information Provided by a Third Party), Article 20 (Notification of the Source, Purpose, etc. of Indirectly Collected Personal Information)" + ], + "AuditEvidence": [ + "Contracts related to the provision of personal information (agreements with providers)", + "Records of notifications to data subjects about the source of personal information", + "Privacy policy" + ], + "NonComplianceCases": [ + "Case 1: In the case of collecting personal information published on websites or social media, there is no procedure for handling requests from data subjects about the source of the information.", + "Case 2: Personal information provided by another business entity was received based on consent for the provision of personal information under Article 17(1)(1) of the Personal Information Protection Act, but the data subjects were not notified within three months (note: this applies to cases where the recipient handles the personal information of more than 50,000 data subjects, sensitive information, or unique identifying information, or processes personal information of over 1 million data subjects).", + "Case 3: The data subject was informed about the source of the personal information as required by law, but some mandatory notification items were omitted, such as the purpose of processing or the right to withdraw consent.", + "Case 4: The data subject was informed about the source of the personal information, but the record of this notification was not maintained until the personal information was destroyed, in violation of legal obligations." + ] + } + ] + }, + { + "Id": "3.1.6", + "Name": "Installation and Operation of Video Information Processing Devices", + "Description": "When installing and operating fixed video information processing devices in public places or operating mobile video information processing devices in public places for business purposes, legal requirements must be followed according to the purpose and location of the installation, and appropriate protection measures must be established and implemented.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Personal Information Processing Requirements", + "Subdomain": "3.1. Protection Measures for Personal Information Collection", + "Section": "3.1.6 Installation and Operation of Video Information Processing Devices", + "AuditChecklist": [ + "When installing and operating fixed video information processing devices in public places, is it reviewed whether the installation meets legal requirements?", + "If public institutions install and operate fixed video information processing devices in public places, are public hearings or explanation sessions held to gather opinions from relevant experts and stakeholders, as required by law?", + "When installing and operating fixed video information processing devices, are necessary measures taken, such as installing signs to ensure the data subject can easily recognize the presence of the devices?", + "When operating mobile video information processing devices in public places for business purposes, is it reviewed whether the operation meets legal requirements?", + "When operating mobile video information processing devices in public places for business purposes, is the fact that the video is being recorded indicated and informed to the public through lights, sounds, or signs?", + "Is there an operation and management policy in place for the safe management of video information processing devices and the video information they record, and is it being implemented?", + "Is the retention period for video information set, and is the information destroyed without delay after the retention period expires?", + "When outsourcing the operation of video information processing devices, are the related procedures and requirements reflected in the contract?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 25 (Restrictions on the Installation and Operation of Fixed Video Information Processing Devices), Article 25-2 (Restrictions on the Operation of Mobile Video Information Processing Devices)" + ], + "AuditEvidence": [ + "Status of video information processing device operation", + "Signs for video information processing devices", + "Video information processing device operation and management policies", + "Management screens for video information processing devices (e.g., account/permission details, video retention periods)", + "Contracts with operators of video information processing devices and inspection records" + ], + "NonComplianceCases": [ + "Case 1: The wording on the signs for video information processing devices is incomplete, or there is no established and implemented policy for the operation and management of video information processing devices.", + "Case 2: Although there is a policy for the operation and management of video information processing devices, the policy is not followed, such as failing to comply with the retention period or failing to implement access control and logging as described in the policy.", + "Case 3: The operation of video information processing devices is outsourced, but the legal requirements, such as inspection of the video information management status and provisions regarding liability for damages, are not reflected in the contract.", + "Case 4: The operation of video information processing devices is outsourced, but the signs for the devices do not include the name and contact information of the contractor." + ] + } + ] + }, + { + "Id": "3.1.7", + "Name": "Collection and Use of Personal Information for Marketing Purposes", + "Description": "When collecting and using personal information for marketing purposes, such as promoting goods or services, sales recommendations, or sending advertising information, the purpose must be clearly communicated to the data subject, and their consent must be obtained.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.1. Protection Measures When Collecting Personal Information", + "Section": "3.1.7 Collection and Use of Personal Information for Marketing Purposes", + "AuditChecklist": [ + "When obtaining consent from data subjects to process personal information for the purpose of promoting or recommending goods or services, is the data subject clearly informed, and is separate consent obtained?", + "When sending advertising information for profit using electronic transmission media, is the recipient's explicit prior consent obtained, and is the consent reconfirmed every two years?", + "When a recipient indicates refusal or withdraws prior consent for receiving advertising information for profit, is the transmission of such advertising information stopped?", + "When sending advertising information for profit, is the sender's name, method for opting out, etc., clearly stated, and are such messages not sent during nighttime hours?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 22 (Method of Obtaining Consent)", + "Information and Communications Network Act, Article 50 (Restrictions on Transmission of Advertising Information)" + ], + "AuditEvidence": [ + "Online personal information collection forms (e.g., website membership sign-up, mobile app sign-up, event participation)", + "Offline personal information collection forms (e.g., membership application forms)", + "Marketing consent records", + "Records of consent for receiving advertising information and confirmation of consent", + "Administrator screen for advertising information transmission systems (e.g., email, SMS, app push notifications)", + "Advertising information transmission content", + "Personal information processing policy" + ], + "NonComplianceCases": [ + "Case 1: When collecting personal information for 'promotion and marketing' purposes, the purpose is vaguely explained (e.g., 'providing additional services', 'providing partner services') or blanket consent is obtained without distinguishing between different purposes.", + "Case 2: Even after a user has expressed refusal to receive advertising push notifications via a mobile app, such notifications are sent due to a program error.", + "Case 3: The option to receive advertising information via text messages or email is pre-selected by default on the online sign-up page.", + "Case 4: The recipient's consent for receiving advertising information is not reconfirmed every two years.", + "Case 5: When sending advertising information for profit via email, the subject line does not begin with '(Advertisement)'." + ] + } + ] + }, + { + "Id": "3.2.1", + "Name": "Management of Personal Information Status", + "Description": "The items, volume, purpose and method of processing, and retention period of collected and retained personal information must be regularly managed. In the case of public institutions, such information must be registered with the head of the relevant agency as stipulated by law.", + "Checks": [ + "s3_bucket_lifecycle_enabled", + "macie_is_enabled" + ], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.2. Protection Measures When Retaining and Using Personal Information", + "Section": "3.2.1 Management of Personal Information Status", + "AuditChecklist": [ + "Is the status of collected and retained personal information, including the items, volume, purpose and method of processing, and retention period, regularly managed?", + "When a public institution operates or modifies personal information files, are the relevant matters registered with the head of the relevant agency as required by law?", + "Does the public institution disclose the status of personal information files in the personal information processing policy?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 32 (Registration and Disclosure of Personal Information Files)" + ], + "AuditEvidence": [ + "Personal information status table", + "Personal information flowchart", + "Registration status of personal information files", + "Personal information file management ledger", + "Personal information processing policy-related personal information files", + "Personal information files related to investigations under the Punishment of Tax Offenses Act and the Customs Act", + "Personal information files for one-time operations deemed to have a low need for continuous management as determined by Presidential Decree", + "Personal information files for simple tasks such as attending meetings, sending documents or materials, and financial settlements, which have a low need for continuous management", + "Personal information files processed temporarily for public health or public safety emergencies", + "Other personal information files collected for one-time tasks that are not stored or recorded", + "Personal information files classified as confidential under other laws", + "Personal information files collected or requested for analysis related to national security", + "Personal video information files processed via video information processing devices", + "Personal information files retained by financial institutions for handling financial transactions under the Real Name Financial Transactions and Guarantee of Secrecy Act" + ], + "NonComplianceCases": [ + "Case 1: Although personal information files are managed through the website's personal information file registration menu, some personal information files related to website services are missing from the personal information processing policy.", + "Case 2: Although two months have passed since a new personal information file was created, it has not been registered with the Personal Information Protection Commission.", + "Case 3: The content of personal information files registered and disclosed with the Personal Information Protection Commission (e.g., items of personal information collected) does not match the actual status of personal information files being processed.", + "Case 4: A public institution has not registered personal information files with the Personal Information Protection Commission, even though the files do not qualify for exceptions such as employee personal information files or personal information files collected under the Statistics Act." + ] + } + ] + }, + { + "Id": "3.2.2", + "Name": "Personal Information Quality Assurance", + "Description": "Collected personal information must be managed to ensure its accuracy, completeness, and up-to-dateness within the scope necessary for the processing purpose, and procedures must be provided to data subjects to manage their information.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.2. Protection Measures for Retention and Use of Personal Information", + "Section": "3.2.2 Personal Information Quality Assurance", + "AuditChecklist": [ + "Are procedures and methods in place to maintain personal information in an accurate and up-to-date state?", + "Is there a method provided for data subjects to ensure the accuracy, completeness, and up-to-dateness of their personal information?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 3 (Principles of Personal Information Protection)" + ], + "AuditEvidence": [ + "Form for data subjects to modify/update their personal information (online, offline)", + "Procedures to maintain the up-to-date status of personal information" + ], + "NonComplianceCases": [ + "Case 1: Although an identity verification process is implemented when changing member information through the website, the identity verification process is insufficient when changing member information via customer service, making unauthorized changes possible.", + "Case 2: While an online method is provided for online members to change their personal information, no such method is provided for offline members." + ] + } + ] + }, + { + "Id": "3.2.3", + "Name": "Protection of User Device Access", + "Description": "When accessing information stored on the user's mobile device or functions installed on the mobile device, it is necessary to notify the user clearly and obtain their consent.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.2. Protection Measures for Retention and Use of Personal Information", + "Section": "3.2.3 Protection of User Device Access", + "AuditChecklist": [ + "When accessing information stored on the user's mobile device or functions installed on the device, are users clearly informed and their consent obtained?", + "Is it ensured that services are not denied if users do not consent to access rights that are not essential for the service?", + "Are methods provided for users to consent to or withdraw access rights on their mobile devices?" + ], + "RelatedRegulations": [ + "Information and Communications Network Act, Article 22-2 (Consent for Access Rights)" + ], + "AuditEvidence": [ + "App access rights consent screen", + "App access rights settings" + ], + "NonComplianceCases": [ + "Case 1: A smartphone app requests excessive access to personal information areas such as contacts, photos, and messages, even though such access is unnecessary for the service.", + "Case 2: A service provider's smartphone app accesses information stored on the smartphone and installed functions without notifying the user and obtaining their consent.", + "Case 3: Consent is obtained for app access rights by informing users that optional permissions are required as essential permissions.", + "Case 4: A smartphone app supports Android versions below 6.0, where individual consent for access rights is not possible, making it impossible for users to reject optional access rights." + ] + } + ] + }, + { + "Id": "3.2.4", + "Name": "Use and Provision of Personal Information Beyond Purpose", + "Description": "Personal information must only be used or provided within the scope notified and consented to by the data subject at the time of collection or as permitted by law. If personal information is to be used or provided beyond this scope, additional consent must be obtained from the data subject or the legality must be verified, and appropriate protective measures must be established and implemented.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.2. Protection Measures for Retention and Use of Personal Information", + "Section": "3.2.4 Use and Provision of Personal Information Beyond Purpose", + "AuditChecklist": [ + "Is personal information used or provided only within the scope of the purpose consented to by the data subject at the time of collection or as permitted by law?", + "When receiving personal information from a personal information processor, is the information used or provided only within the scope of the purpose for which it was provided?", + "If personal information is used or provided beyond the scope of the purpose of collection or the purpose for which it was received from a personal information processor, is additional consent obtained from the data subject or limited to cases with a legal basis?", + "When providing personal information to a third party for purposes beyond the original purpose, is the recipient required to take necessary actions to restrict the use of personal information and ensure safety?", + "When public institutions use or provide personal information beyond the original purpose, are the legal basis, purpose, and scope published in the official gazette or on the internet?", + "When public institutions use or provide personal information beyond the original purpose, is there a record of such use or provision and are procedures in place for managing it?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 18 (Restriction on the Use and Provision of Personal Information Beyond the Original Purpose), Article 19 (Restriction on Use and Provision by Recipients of Personal Information)" + ], + "AuditEvidence": [ + "Records of personal information use and provision beyond the original purpose (including related evidence such as requests)", + "Log of personal information use and provision beyond the original purpose (for public institutions)", + "Records of publication in the official gazette or on the website (for public institutions)", + "Guidelines for handling information provision requests", + "Official documents requesting information provision and records of personal information provision" + ], + "NonComplianceCases": [ + "Case 1: Personal information collected for product delivery is used for telemarketing of other company products without prior consent.", + "Case 2: Personal information collected for customer satisfaction surveys or sweepstakes entries is used for advertising other promotional events without consent.", + "Case 3: A public institution provides personal information to another institution for purposes outside the scope of the original purpose based on legal grounds but does not publish the information in the official gazette or on the internet.", + "Case 4: A public institution provides personal information to a police department for criminal investigation purposes but fails to record the details in the log of personal information use and provision beyond the original purpose." + ] + } + ] + }, + { + "Id": "3.2.5", + "Name": "Processing of Pseudonymized Information", + "Description": "When processing pseudonymized information, legal requirements such as purpose limitation, combination restrictions, safety measures, and prohibition obligations must be met, and procedures must be established and implemented to ensure an appropriate level of pseudonymization.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.2. Protection Measures for Retention and Use of Personal Information", + "Section": "3.2.5 Processing of Pseudonymized Information", + "AuditChecklist": [ + "When processing pseudonymized information, are procedures established for purpose limitation, pseudonymization methods and standards, adequacy review, prohibition of re-identification, and actions in case of re-identification?", + "When using or providing pseudonymized personal information, is the information pseudonymized to a level where individuals cannot be identified without using or combining additional information?", + "When combining pseudonymized information with that of other personal information processors, is the combination conducted through a specialized agency or data professional organization?", + "When processing pseudonymized information, are technical, administrative, and physical measures taken to ensure safety, such as deleting or separately storing additional information and keeping records?", + "Is the processing period for pseudonymized information set to an appropriate period considering the processing purpose, and is the information destroyed without delay when that period expires?", + "When anonymizing personal information, is the information anonymized to a level where individuals cannot be identified even with the use of additional information, considering the time, cost, and technology available?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 2 (Definitions), Article 28-2 (Processing of Pseudonymized Information), Article 28-3 (Restrictions on Combining Pseudonymized Information), Article 28-4 (Obligations for the Safe Processing of Pseudonymized Information), Article 28-5 (Prohibition of Re-identification in Processing Pseudonymized Information), Article 28-7 (Scope of Application), Article 58-2 (Exemptions)" + ], + "AuditEvidence": [ + "Procedures and results of the adequacy review of pseudonymization/anonymization", + "Records of pseudonymized information processing", + "Privacy policy (regarding the use and provision of pseudonymized information)" + ], + "NonComplianceCases": [ + "Case 1: When processing pseudonymized information for statistical purposes or scientific research without obtaining consent from data subjects, records of the pseudonymization process were not kept, or the privacy policy was not updated to include relevant information.", + "Case 2: Additional information was not stored separately from pseudonymized information in the same database, or access rights to both sets of information were not appropriately segregated.", + "Case 3: Although pseudonymized personal information was used, the pseudonymization process was not sufficient, making it possible to identify individuals by combining the information with other data without using additional information.", + "Case 4: Personal information was anonymized for generating test data or for public release, but due to outliers or other factors, it was still possible to identify individuals, indicating that the anonymization process was not sufficient." + ] + } + ] + }, + { + "Id": "3.3.1", + "Name": "Provision of Personal Information to Third Parties", + "Description": "When providing personal information to third parties, there must be a legal basis or consent from the data subject, and protection measures must be established and implemented to securely protect personal information during the process of providing access to third parties.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.3. Protective Measures When Providing Personal Information", + "Section": "3.3.1 Provision of Personal Information to Third Parties", + "AuditChecklist": [ + "When providing personal information to third parties, are legal requirements such as consent from the data subject or compliance with legal obligations clearly identified and followed?", + "When obtaining consent from the data subject for the provision of personal information to third parties, are the related matters clearly communicated, and is consent legally obtained by distinguishing it from other consents?", + "When obtaining consent from the data subject for the provision of personal information to third parties, are important matters clearly indicated and made easily understandable as required by law?", + "When providing personal information to third parties, is the information limited to the minimum necessary for the intended purpose?", + "When providing personal information to third parties, is it done through secure procedures and methods, and is the provision recorded and stored?", + "When allowing third parties to access personal information, is control implemented in accordance with protection procedures to securely protect the personal information?", + "When providing additional personal information without the data subject's consent, are criteria for determining the relevance to the original purpose of collection, predictability, potential harm, and safety measures established and followed? If such provisions continue, are these criteria disclosed in the privacy policy and periodically reviewed?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 17 (Provision of Personal Information), Article 22 (Methods of Obtaining Consent)", + "Notification on the Methods of Processing Personal Information" + ], + "AuditEvidence": [ + "Forms related to the provision of personal information to third parties online (e.g., membership registration page, consent for third-party provision on websites)", + "Forms related to the provision of personal information to third parties offline (e.g., membership application forms, consent forms for third-party provision)", + "Records of third-party provisions", + "Privacy policy" + ], + "NonComplianceCases": [ + "Case 1: When obtaining consent from the data subject for the provision of personal information to third parties, some necessary information (e.g., the right to refuse consent, the items provided) was omitted.", + "Case 2: In the process of providing personal information to third parties, personal information from data subjects who did not consent was provided due to improper verification of consent.", + "Case 3: When obtaining consent for the provision of personal information, the recipient was not specifically identified and was vaguely referred to as ʻ~ etc.ʼ in the consent.", + "Case 4: Although third-party provision consent was optional during the membership registration process, if the data subject did not agree to third-party provision, the registration process could not be completed.", + "Case 5: An excessive amount of personal information was provided beyond what was necessary for the recipient's purpose of use." + ] + } + ] + }, + { + "Id": "3.3.2", + "Name": "Outsourcing of Personal Information Processing", + "Description": "When outsourcing personal information processing tasks to third parties, the details of the outsourced tasks and the trustee must be disclosed. Additionally, if the task involves promoting or selling goods or services, the details of the outsourced task and the trustee must be notified to the data subject.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.3. Protective Measures When Providing Personal Information", + "Section": "3.3.2 Outsourcing of Personal Information Processing", + "AuditChecklist": [ + "When outsourcing personal information processing tasks (including sub-outsourcing) to third parties, are the details of the outsourced tasks and the trustees regularly updated and disclosed on the website?", + "When outsourcing tasks related to promoting or selling goods or services, is the data subject notified of the details of the outsourced tasks and the trustees through methods such as written notice, email, or text messages?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 26 (Restrictions on the Processing of Personal Information by Outsourcing)" + ], + "AuditEvidence": [ + "Privacy policy (disclosing details related to the outsourcing of personal information processing)", + "Personal information collection forms", + "Contracts for outsourcing personal information processing", + "Records of notifications to data subjects regarding outsourced tasks related to promoting or selling goods or services" + ], + "NonComplianceCases": [ + "Case 1: Although the details of the outsourcing of personal information processing tasks were disclosed on the website's privacy policy, some trustees and the details of the outsourced tasks were missing.", + "Case 2: When outsourcing tasks related to promoting or selling goods or services, the details of the outsourced tasks and trustees were not notified to the data subject through written methods, and instead, the information was disclosed only in the privacy policy.", + "Case 3: After terminating a contract with an existing trustee for personal information processing, the new trustee was not promptly reflected in the privacy policy.", + "Case 4: Although the trustee sub-outsourced the personal information processing tasks to a third party, the sub-outsourcing details were not disclosed on the website." + ] + } + ] + }, + { + "Id": "3.3.3", + "Name": "Transfer of Personal Information Due to Business Transfers", + "Description": "When transferring or receiving personal information due to business transfers or mergers, appropriate protection measures such as notifying the data subjects must be established and implemented.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.3. Protective Measures When Providing Personal Information", + "Section": "3.3.3 Transfer of Personal Information Due to Business Transfers", + "AuditChecklist": [ + "When transferring personal information to another party due to the transfer or merger of all or part of the business, are the necessary matters communicated to the data subjects in advance?", + "When receiving personal information, does the recipient notify the data subjects without delay regarding the fact that personal information has been received and other necessary matters, if legally required?", + "Does the recipient of the personal information use the information only for its original purpose at the time of transfer, or provide it to third parties in compliance with the original purpose?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 27 (Restrictions on the Transfer of Personal Information Due to Business Transfers)" + ], + "AuditEvidence": [ + "Records of notifications to data subjects regarding the transfer of personal information (during business transfers)", + "Privacy policy" + ], + "NonComplianceCases": [ + "Case 1: When receiving personal information through business acquisition, the data subjects were not notified of the transfer of personal information, even though the data provider failed to notify them of the transfer.", + "Case 2: When receiving personal information through business acquisition or merger, no procedures or methods were provided to allow data subjects to opt-out of the transfer, nor were such options communicated to the data subjects." + ] + } + ] + }, + { + "Id": "3.3.4", + "Name": "Transfer of Personal Information Abroad", + "Description": "When transferring personal information abroad, appropriate protective measures such as obtaining consent and disclosing relevant details about the transfer must be established and implemented.", + "Checks": [ + "s3_bucket_cross_region_replication" + ], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.3. Protection Measures When Providing Personal Information", + "Section": "3.3.4 Transfer of Personal Information Abroad", + "AuditChecklist": [ + "When transferring personal information abroad, has the data subject been fully informed of all notification requirements and obtained separate consent, or complied with certification or recognition, as required by law?", + "When informing the data subject about the outsourcing or storage of personal information abroad for the purpose of contract execution, are all necessary details included and communicated appropriately?", + "Has a contract for the transfer of personal information abroad been established, including compliance with personal information protection laws?", + "Are necessary measures being taken to protect personal information when transferring it abroad?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Articles 28-8 (Transfer of Personal Information Abroad), 28-9 (Order to Suspend Transfer of Personal Information Abroad), 28-10 (Reciprocity), 28-11 (Applicable Provisions)", + "Regulations on the Operation of Personal Information Transfer Abroad" + ], + "AuditEvidence": [ + "Consent form for personal information transfer abroad", + "Contract related to personal information transfer abroad", + "Privacy policy", + "Notification or disclosure records regarding outsourcing or storage of personal information abroad" + ], + "NonComplianceCases": [ + "Case 1: Personal information was provided to a foreign company during processing, but separate consent for the transfer of personal information abroad was not obtained, even though the conditions for consent exemption (such as certification or recognition by the recipient country) were not met.", + "Case 2: While using foreign cloud services (foreign regions) for outsourcing and storing personal information, the relevant details, such as the destination country and transfer method, were not disclosed in the privacy policy or communicated to the data subject.", + "Case 3: While obtaining consent for the transfer of personal information abroad, only the name of the recipient (company name) was disclosed, and the destination country was not properly notified." + ] + } + ] + }, + { + "Id": "3.4.1", + "Name": "Destruction of Personal Information", + "Description": "The organization must establish an internal policy regarding retention periods and destruction of personal information. When the retention period has expired or the purpose of processing has been achieved, personal information must be destroyed without delay using methods that ensure safety and completeness.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.4. Protection Measures When Destroying Personal Information", + "Section": "3.4.1 Destruction of Personal Information", + "AuditChecklist": [ + "Has an internal policy been established regarding the retention period and destruction of personal information?", + "Is personal information being destroyed without delay when the processing purpose is achieved or the retention period has expired?", + "Is personal information destroyed using safe methods that prevent recovery or reconstruction?", + "Are records kept of the destruction of personal information and managed properly?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 21 (Destruction of Personal Information)", + "Standards for Ensuring the Safety of Personal Information, Article 13 (Destruction of Personal Information)" + ], + "AuditEvidence": [ + "Regulations regarding the retention period and destruction of personal information", + "Destruction results (e.g., from member databases)", + "Personal information destruction management records" + ], + "NonComplianceCases": [ + "Case 1: When a member withdraws or the purpose of retention is achieved, personal information was destroyed from the member database, but not from associated systems (CRM, DW) where duplicate personal information was stored.", + "Case 2: Personal information collected during a specific event was not destroyed or no destruction policy was established, even after the event ended.", + "Case 3: Personal information collected through a call center (such as call logs, recordings) is retained for three years under the Electronic Commerce Act, but the information was not destroyed even after three years had passed.", + "Case 4: Due to technical limitations, such as using blockchain, it was not possible to completely destroy personal information, so it was anonymized instead. However, the anonymization process was not done properly, allowing partial re-identification of personal information." + ] + } + ] + }, + { + "Id": "3.4.2", + "Name": "Measures When Retaining Personal Information After Purpose Is Achieved", + "Description": "If personal information is retained beyond the retention period or after the purpose of processing has been achieved, as permitted by relevant laws, it must be limited to the minimum necessary items and stored separately from other personal information.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.4. Protection Measures When Destroying Personal Information", + "Section": "3.4.2 Measures When Retaining Personal Information After Purpose Is Achieved", + "AuditChecklist": [ + "When personal information is retained beyond the retention period or after the processing purpose has been achieved, in accordance with relevant laws, is it limited to the minimum necessary period and only the minimum necessary information?", + "When personal information is retained beyond the retention period or after the processing purpose has been achieved, is it stored separately from other personal information?", + "Is personal information that is stored separately processed only within the scope allowed by law?", + "Is access to separately stored personal information limited to the minimum number of personnel?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 21 (Destruction of Personal Information)" + ], + "AuditEvidence": [ + "Regulations regarding the retention period and destruction of personal information", + "Current status of separated databases (table structure, etc.)", + "Access permissions for separated databases" + ], + "NonComplianceCases": [ + "Case 1: Information from withdrawn members was not destroyed but kept for a certain period under the Electronic Commerce Act, with only the flag value changed, and stored in the same table as other member information.", + "Case 2: Records related to consumer complaints and disputes were kept for five years instead of the required three years, due to misinterpretation of legal requirements.", + "Case 3: Although a separate database was set up, access permissions were not appropriately configured, allowing personnel who did not require access to view the separated database.", + "Case 4: Information from withdrawn members was stored separately in accordance with the Electronic Commerce Act, but excessive optional information was also stored, even though there was no legal obligation to do so." + ] + } + ] + }, + { + "Id": "3.5.1", + "Name": "Disclosure of Privacy Policy", + "Description": "A privacy policy must be established to include all necessary information, such as the purpose of personal information processing, in a way that is easy for data subjects to understand. The policy must be disclosed through appropriate methods so that data subjects can easily access it at any time, and it must be continuously updated.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.5. Protection of Data Subject's Rights", + "Section": "3.5.1 Disclosure of Privacy Policy", + "AuditChecklist": [ + "Is the privacy policy written in clear and easy-to-understand language, covering all the contents required by law?", + "Is the privacy policy continuously updated and made easily accessible to data subjects via the internet or other means?", + "When the privacy policy is updated, are the reasons for the changes and the contents of the changes promptly notified, and can the data subjects easily recognize the changes at any time?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 30 (Establishment and Disclosure of Privacy Policy), Article 30-2 (Evaluation and Improvement Recommendations for Privacy Policy)" + ], + "AuditEvidence": [ + "Privacy policy", + "Records of privacy policy amendments (e.g., board notices)" + ], + "NonComplianceCases": [ + "Case 1: The privacy policy discloses information about the collection and provision of personal information, but the actual details differ from what is being collected and provided.", + "Case 2: Changes such as the replacement of the privacy officer or changes in subcontractors have occurred, but these changes have not been reflected in the privacy policy.", + "Case 3: The privacy policy is disclosed, but it is labeled 'Privacy Protection Policy' instead of 'Privacy Policy,' and its visibility is not enhanced with larger font sizes or color to make it easy for data subjects to find.", + "Case 4: Several amendments have been made to the privacy policy, but older versions of the policy are not made available for review.", + "Case 5: Although personal information is retained in compliance with laws such as the Electronic Commerce Act and the Commercial Act, the legal grounds for retention and the retained personal information items are not disclosed in the privacy policy." + ] + } + ] + }, + { + "Id": "3.5.2", + "Name": "Guaranteeing Data Subject's Rights", + "Description": "Procedures must be established and implemented to ensure that data subjects can easily exercise their rights, such as requesting access, rectification, deletion, suspension of processing, objection, or withdrawal of consent, through simpler processes than those used for collecting their information. When a request is received, it must be processed without delay, and records must be kept. Measures such as deletion requests and temporary actions must be taken to prevent the distribution of information that infringes on the rights of others, such as invasion of privacy or defamation.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.5. Protection of Data Subject's Rights", + "Section": "3.5.2 Guaranteeing Data Subject's Rights", + "AuditChecklist": [ + "Are procedures in place to ensure that data subjects or their representatives can exercise their rights (hereinafter referred to as 'Requests for Access, etc.') to access, rectify, delete, or suspend the processing of their personal information in a way that is not more difficult than the process used for collecting it?", + "When data subjects or their representatives submit Requests for Access, etc., are the necessary measures taken within the required time frame?", + "When data subjects withdraw their consent to the collection, use, or provision of their personal information, are the collected personal information and associated data promptly deleted or otherwise handled appropriately?", + "Are appropriate procedures in place to allow data subjects to object to the actions taken regarding their Requests for Access, etc., and are they informed of these procedures?", + "Are records kept of data subjects' Requests for Access, etc., and the resulting actions?", + "When the rights of others, such as privacy or honor, are violated on information networks, does the organization have procedures for the affected individuals to request the deletion of the information from service providers, and are these procedures being implemented?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 34-2 (Deletion or Blocking of Exposed Personal Information), Article 35 (Access to Personal Information), Article 35-2 (Right to Data Portability), Article 36 (Rectification or Deletion of Personal Information), Article 37 (Suspension of Processing, etc.), Article 37-2 (Right of Data Subjects to Contest Automated Decisions), Article 38 (Methods and Procedures for Exercising Rights)", + "Information and Communications Network Act, Article 44 (Protection of Rights in Information Networks), Article 44-2 (Request for Deletion of Information, etc.), Article 44-3 (Temporary Measures)" + ], + "AuditEvidence": [ + "Privacy policy", + "Procedures and forms for handling Requests for Access, etc.", + "Records of actions taken in response to Requests for Access, etc.", + "Procedures for member withdrawal and consent withdrawal" + ], + "NonComplianceCases": [ + "Case 1: The method for requesting access, rectification, deletion, or suspension of personal information is not disclosed in a way that data subjects can easily find.", + "Case 2: There has been no response to access requests for personal information within 10 days, without any valid reason.", + "Case 3: Records of actions taken in response to personal information access requests are not maintained.", + "Case 4: Access notifications are being sent without verifying whether the requester is the data subject or their legitimate representative.", + "Case 5: There has been a failure to respond to rectification or deletion requests within 10 days.", + "Case 6: It was easy to sign up online as a member, but to withdraw membership, additional documents such as ID must be submitted, or in-person visits are required." + ] + } + ] + }, + { + "Id": "3.5.3", + "Name": "Notification to Data Subjects", + "Description": "The organization must identify matters that must be notified to data subjects, such as the use and provision of personal information, and periodically inform the data subjects of these matters.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. Requirements for Each Stage of Personal Information Processing", + "Subdomain": "3.5. Protection of Data Subject's Rights", + "Section": "3.5.3 Notification to Data Subjects", + "AuditChecklist": [ + "If the organization is legally obligated to do so, does it periodically notify data subjects of the use and provision of their personal information, or provide them with access to an information system where they can review such details?", + "Do the notification items regarding the use and provision of personal information include all legally required elements?" + ], + "RelatedRegulations": [ + "Personal Information Protection Act, Article 20-2 (Notification of Use and Provision of Personal Information)" + ], + "AuditEvidence": [ + "Records of notifications regarding the use and provision of personal information", + "Forms and wording used for notifications regarding the use and provision of personal information" + ], + "NonComplianceCases": [ + "Case 1: Although the organization is required to notify data subjects of the use and provision of their personal information, no notifications have been sent during the year despite being obligated due to handling personal information of more than 1 million people on a daily average for the past three months at the end of the previous year.", + "Case 2: Instead of directly notifying individual data subjects, notifications about the use and provision of personal information were made through simple pop-ups or general announcements on the website." + ] + } + ] + } + ] +} diff --git a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json new file mode 100644 index 0000000000..5e4897a0fa --- /dev/null +++ b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json @@ -0,0 +1,4335 @@ +{ + "Framework": "KISA-ISMS-P", + "Version": "2023-korean", + "Provider": "AWS", + "Description": "ISMS-P 인증은 한국인터넷진흥원(KISA)이 제정한 정보보호 및 개인정보보호 관리체계를 기반으로, 독립적인 심사기관이 기업이나 조직의 보안 및 개인정보 보호 활동이 인증 기준을 충족하는지 평가한 후 인증을 부여하는 제도입니다. 이를 통해 기업과 기관은 제공하는 서비스에 대한 대중의 신뢰를 높이고, 점점 복잡해지는 사이버 위협에 효과적으로 대응할 수 있습니다. 또한, ISMS-P는 정보보호와 개인정보 보호를 체계적으로 수립하고 운영할 수 있는 포괄적인 지침을 제공합니다.", + "Requirements": [ + { + "Id": "1.1.1", + "Name": "경영진의 참여", + "Description": "최고경영자는 정보보호 및 개인정보보호 관리체계의 수립과 운영활동 전반에 경영진의 참여가 이루어질 수 있도록 보고 및 의사결정 체계를 수립하여 운영하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.1. 관리체계", + "Section": "1.1.1 경영진의 참여", + "AuditChecklist": [ + "정보보호 및 개인정보보호 관리체계의 수립 및 운영활동 전반에 경영진의 참여가 이루어질 수 있도록 보고 및 의사결정 등의 책임과 역할을 문서화하고 있는가?", + "경영진이 정보보호 및 개인정보보호 활동에 관한 의사결정에 적극적으로 참여할 수 있는 보고, 검토 및 승인 절차를 수립·이행하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보보호 및 개인정보보호 보고 체계(의사소통계획 등)", + "정보보호 및 개인정보보호 위원회 회의록", + "정보보호 및 개인정보보호 정책·지침(경영진 승인내역 포함)", + "정보보호계획 및 내부 관리계획(경영진 승인내역 포함)", + "정보보호 및 개인정보보호 조직도" + ], + "NonComplianceCases": [ + "사례 1 : 정보보호 및 개인정보보호 정책서에 분기별로 정보보호 및 개인정보보호 현황을 경영진에게 보고하도록 명시하였으나, 장기간 관련 보고를 수행하지 않은 경우", + "사례 2 : 중요 정보보호 활동(위험평가, 위험수용수준 결정, 정보보호대책 및 이행계획 검토, 정보보호대책 이행결과 검토, 보안감사 등)을 수행하면서 관련 활동관련 보고, 승인 등 의사결정에 경영진 또는 경영진의 권한을 위임받은 자가 참여하지 않았거나 관련 증거자료가 확인되지 않은 경우" + ] + } + ] + }, + { + "Id": "1.1.2", + "Name": "최고책임자의 지정", + "Description": "최고경영자는 정보보호 업무를 총괄하는 정보보호 최고책임자와 개인정보보호 업무를 총괄하는 개인정보보호 책임자를 예산·인력 등 자원을 할당할 수 있는 임원급으로 지정하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.1. 관리체계", + "Section": "1.1.2 최고책임자의 지정", + "AuditChecklist": [ + "최고경영자는 정보보호 및 개인정보보호 처리에 관한 업무를 총괄하여 책임질 최고책임자를 공식적으로 지정하고 있는가?", + "정보보호 최고책임자 및 개인정보 보호책임자는 예산, 인력 등 자원을 할당할 수 있는 임원급으로 지정하고 있으며, 관련 법령에 따른 자격요건을 충족하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무), 제31조(개인정보 보호책임자의 지정)", + "정보통신망법 제45조의3(정보보호 최고책임자의 지정 등)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "정보보호 최고책임자 및 개인정보 보호책임자 임명관련 자료(인사명령, 인사카드 등)", + "정보보호 및 개인정보보호 조직도", + "정보보호 및 개인정보보호 정책·지침", + "직무기술서(정보보호 최고책임자 및 개인정보 보호책임자의 역할 및 책임에 관한 사항)", + "정보보호 최고책임자 신고 내역", + "내부 관리계획(개인정보 보호책임자 지정에 관한 사항)" + ], + "NonComplianceCases": [ + "사례 1 : 정보통신망법에 따른 정보보호 최고책임자 지정 및 신고 의무 대상자임에도 불구하고 정보보호 최고책임자를 지정 및 신고하지 않은 경우", + "사례 2 : 개인정보 보호와 관련된 실질적인 권한 및 지위를 보유하고 있지 않은 인원을 개인정보 보호 책임자로 지정하고 있어, 개인정보 처리에 관한 업무를 총괄해서 책임질 수 있다고 보기 어려운 경우", + "사례 3 : 조직도상에 정보보호 최고책임자 및 개인정보 보호책임자를 명시하고 있으나, 인사발령 등의 공식적인 지정절차를 거치지 않은 경우", + "사례 4 : ISMS 인증 의무대상자이면서 전년도 말 기준 자산총액이 5천억 원을 초과한 정보통신서비스 제공자이지만 정보보호 최고책임자가 CIO를 겸직하고 있는 경우" + ] + } + ] + }, + { + "Id": "1.1.3", + "Name": "조직 구성", + "Description": "최고경영자는 정보보호와 개인정보보호의 효과적 구현을 위한 실무조직, 조직 전반의 정보보호와 개인정보보호 관련 주요 사항을 검토 및 의결할 수 있는 위원회, 전사적 보호활동을 위한 부서별 정보보호와 개인정보보호 담당자로 구성된 협의체를 구성하여 운영하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.1. 관리체계", + "Section": "1.1.3 조직 구성", + "AuditChecklist": [ + "정보보호 최고책임자 및 개인정보 보호책임자의 업무를 지원하고 조직의 정보보호 및 개인정보보호 활동을 체계적으로 이행하기 위하여 전문성을 갖춘 실무조직을 구성하여 운영하고 있는가?", + "조직 전반에 걸친 중요한 정보보호 및 개인정보보호 관련사항에 대하여 검토, 승인 및 의사결정을 할 수 있는 위원회를 구성하여 운영하고 있는가?", + "전사적 정보보호 및 개인정보보호 활동을 위하여 정보보호 및 개인정보보호 관련 담당자 및 부서별 담당자로 구성된 실무 협의체를 구성하여 운영하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "정보보호 및 개인정보보호 위원회 규정·회의록", + "정보보호 및 개인정보보호 실무 협의체 규정·회의록", + "정보보호 및 개인정보보호 조직도", + "내부 관리계획", + "직무기술서" + ], + "NonComplianceCases": [ + "사례 1 : 정보보호 및 개인정보보호 위원회를 구성하였으나, 임원 등 경영진이 포함되어 있지 않고 실무 부서의 장으로 구성되어 있어 조직의 중요 정보 및 개인정보 보호에 관한 사항을 결정할 수 없는 경우", + "사례 2 : 내부 지침에 따라 중요 정보처리 부서 및 개인정보처리 부서의 장(팀장급)으로 구성된 정보보호 및 개인정보보호 실무 협의체를 구성하였으나, 장기간 운영 실적이 없는 경우", + "사례 3 : 정보보호 및 개인정보보호 위원회를 개최하였으나, 연간 정보보호 및 개인정보보호 계획 및 교육 계획, 예산 및 인력 등 정보보호 및 개인정보보호에 관한 주요 사항이 검토 및 의사결정이 되지 않은 경우", + "사례 4 : 정보보호 및 개인정보보호 관련 심의·의결을 위해 정보보호위원회를 구성하여 운영하고 있으나, 운영 및 IT보안 관련 조직만 참여하고 개인정보보호 관련 조직은 참여하지 않고 있어 개인정보보호에 관한 사항을 결정할 수 없는 경우" + ] + } + ] + }, + { + "Id": "1.1.4", + "Name": "범위 설정", + "Description": "조직의 핵심 서비스와 개인정보 처리 현황 등을 고려하여 관리체계 범위를 설정하고, 관련된 서비스를 비롯하여 개인정보 처리 업무와 조직, 자산, 물리적 위치 등을 문서화하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.1. 관리체계", + "Section": "1.1.4 범위 설정", + "AuditChecklist": [ + "조직의 핵심 서비스 및 개인정보 처리에 영향을 줄 수 있는 핵심자산을 포함하도록 관리체계 범위를 설정하고 있는가?", + "정의된 범위 내에서 예외사항이 있을 경우 명확한 사유 및 관련자 협의·책임자 승인 등 관련 근거를 기록·관리하고 있는가?", + "정보보호 및 개인정보보호 관리체계 범위를 명확히 확인할 수 있도록 관련된 내용(주요 서비스 및 업무 현황, 정보시스템 목록, 문서목록 등)이 포함된 문서를 작성하여 관리하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보보호 및 개인정보보호 관리체계 범위 정의서", + "정보자산 및 개인정보 목록", + "문서 목록", + "서비스 흐름도", + "개인정보 흐름도", + "전사 조직도", + "시스템 및 네트워크 구성도" + ], + "NonComplianceCases": [ + "사례 1 : 정보시스템 및 개인정보처리시스템 개발업무에 관련한 개발 및 시험 시스템, 외주업체직원, PC, 테스트용 단말기 등이 관리체계 범위에서 누락된 경우", + "사례 2 : 정보보호 및 개인정보보호 관리체계 범위로 설정된 서비스 또는 사업에 대하여 중요 의사결정자 역할을 수행하고 있는 임직원, 사업부서 등의 핵심 조직(인력)을 인증범위에 포함하지 않은 경우", + "사례 3 : 정보시스템 및 개인정보처리시스템 개발업무에 관련한 개발 및 시험 시스템, 개발자 PC, 테스트용 단말기, 개발조직 등이 관리체계 범위에서 누락된 경우" + ] + } + ] + }, + { + "Id": "1.1.5", + "Name": "정책 수립", + "Description": "정보보호와 개인정보보호 정책 및 시행문서를 수립·작성하며, 이때 조직의 정보보호와 개인정보보호 방침 및 방향을 명확하게 제시하여야 한다. 또한 정책과 시행문서는 경영진의 승인을 받고, 임직원 및 관련자에게 이해하기 쉬운 형태로 전달하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.1. 관리체계", + "Section": "1.1.5 정책 수립", + "AuditChecklist": [ + "조직이 수행하는 모든 정보보호 및 개인정보보호 활동의 근거를 포함하는 최상위 수준의 정보보호 및 개인정보보호 정책을 수립하고 있는가?", + "정보보호 및 개인정보보호 정책의 시행을 위하여 필요한 세부적인 방법, 절차, 주기 등을 규정한 지침, 절차, 매뉴얼 등을 수립하고 있는가?", + "정보보호 및 개인정보보호 정책·시행문서의 제·개정 시 최고경영자 또는 최고경영자로부터 권한을 위임받은 자의 승인을 받고 있는가?", + "정보보호 및 개인정보보호 정책·시행문서의 최신본을 관련 임직원에게 이해하기 쉬운 형태로 제공하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "정보보호 및 개인정보보호 정책·지침·절차서(제·개정 내역 포함)", + "정보보호 및 개인정보보호 정책·지침절차서 제·개정 시 이해관계자 검토 회의록", + "개인정보 내부 관리계획", + "정보보호 및 개인정보보호 정책·지침 제·개정 공지내역(그룹웨어, 사내게시판 등)", + "정보보호 및 개인정보보호 위원회 회의록" + ], + "NonComplianceCases": [ + "사례 1 : 내부 규정에 따르면 정보보호 및 개인정보보호 정책서 제·개정 시에는 정보보호 및 개인정보보호 위원회의 의결을 거치도록 하고 있으나, 최근 정책서 개정 시 위원회에 안건으로 상정하지 않고 정보보호 최고책임자 및 개인정보 보호책임자의 승인을 근거로만 개정한 경우", + "사례 2 : 정보보호 및 개인정보보호 정책 및 지침서가 최근에 개정되었으나, 해당 사항이 관련 부서 및 임직원에게 공유·전달되지 않아 일부 부서에서는 구버전의 지침서를 기준으로 업무를 수행하고 있는 경우", + "사례 3 : 정보보호 및 개인정보보호 정책 및 지침서를 보안부서에서만 관리하고 있고, 임직원이 열람할 수 있도록 게시판, 문서 등의 방법으로 제공하지 않는 경우" + ] + } + ] + }, + { + "Id": "1.1.6", + "Name": "자원 할당", + "Description": "최고경영자는 정보보호와 개인정보보호 분야별 전문성을 갖춘 인력을 확보하고, 관리체계의 효과적 구현과 지속적 운영을 위한 예산 및 자원을 할당하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.1. 관리체계", + "Section": "1.1.6 자원 할당", + "AuditChecklist": [ + "정보보호 및 개인정보보호 분야별 전문성을 갖춘 인력을 확보하고 있는가?", + "정보보호 및 개인정보보호 관리체계의 효과적 구현과 지속적 운영을 위하여 필요한 자원을 평가하여 필요한 예산과 인력을 지원하고 있는가?", + "연도별 정보보호 및 개인정보보호 업무 세부추진 계획을 수립·시행하고, 그 추진결과에 대한 심사분석·평가를 실시하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보보호 및 개인정보보호 활동 연간 추진계획서(예산 및 인력운영계획)", + "정보보호 및 개인정보보호 활동 결과 보고서", + "정보보호 및 개인정보보호 투자 내역", + "정보보호 및 개인정보보호 조직도" + ], + "NonComplianceCases": [ + "사례 1 : 정보보호 및 개인정보보호 조직을 구성하는데, 분야별 전문성을 갖춘 인력이 아닌 정보보호 관련 또는 IT 관련 전문성이 없는 인원으로만 보안인력을 구성한 경우", + "사례 2 : 개인정보처리시스템의 기술적·관리적 보호조치의 요건을 갖추기 위한 최소한의 보안 솔루션 도입, 안전조치 적용 등을 위한 비용을 최고경영자가 지원하지 않고 있는 경우", + "사례 3 : 인증을 취득한 이후에 인력과 예산 지원을 대폭 줄이고 기존 인력을 다른 부서로 배치하거나 일부 예산을 다른 용도로 사용하는 경우" + ] + } + ] + }, + { + "Id": "1.2.1", + "Name": "정보자산 식별", + "Description": "조직의 업무특성에 따라 정보자산 분류기준을 수립하여 관리체계 범위 내 모든 정보자산을 식별·분류하고, 중요도를 산정한 후 그 목록을 최신으로 관리하여야 한다.", + "Checks": [ + "organizations_tags_policies_enabled_and_attached", + "organizations_account_part_of_organizations", + "macie_is_enabled", + "config_recorder_all_regions_enabled", + "resourceexplorer2_indexes_found" + ], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.2. 위험 관리", + "Section": "1.2.1 정보자산 식별", + "AuditChecklist": [ + "정보자산의 분류기준을 수립하고 정보보호 및 개인정보보호 관리체계 범위 내의 모든 자산을 식별하여 목록으로 관리하고 있는가?", + "식별된 정보자산에 대하여 법적 요구사항 및 업무에 미치는 영향 등을 고려하여 중요도를 결정하고 보안등급을 부여하고 있는가?", + "정기적으로 정보자산 현황을 조사하여 정보자산목록을 최신으로 유지하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보자산 및 개인정보 자산분류 기준", + "정보자산 및 개인정보 자산목록(자산관리시스템 화면)", + "정보자산 및 개인정보 보안등급", + "자산실사 내역", + "위험분석 보고서(자산식별 내역)" + ], + "NonComplianceCases": [ + "사례 1 : 정보보호 및 개인정보보호 관리체계 범위 내의 자산 목록에서 중요정보 취급자 및 개인정보 취급자 PC를 통제하는 데 사용되는 출력물 보안, 문서암호화, USB매체제어 등의 내부정보 유출통제 시스템이 누락된 경우", + "사례 2 : 정보보호 및 개인정보보호 관리체계 범위 내에서 제3자로부터 제공받은 개인정보가 있으나, 해당 개인정보에 대한 자산 식별이 이루어지지 않은 경우", + "사례 3 : 내부 지침에 명시된 정보자산 및 개인정보 보안등급 분류 기준과 자산관리 대장의 분류 기준이 일치하지 않은 경우", + "사례 4 : 온프레미스 자산에 대해서는 식별이 이루어졌으나, 외부에 위탁한 IT 서비스(웹호스팅, 서버호스팅, 클라우드 등)에 대한 자산 식별이 누락된 경우(단, 인증범위 내)", + "사례 5 : 고유식별정보 등 개인정보를 저장하고 있는 백업서버의 기밀성 등급을 (하)로 산정하는 등 정보자산 중요도 평가의 합리성 및 신뢰성이 미흡한 경우" + ] + } + ] + }, + { + "Id": "1.2.2", + "Name": "현황 및 흐름분석", + "Description": "관리체계 전 영역에 대한 정보서비스 및 개인정보 처리 현황을 분석하고 업무 절차와 흐름을 파악하여 문서화하며, 이를 주기적으로 검토하여 최신성을 유지하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.2. 위험 관리", + "Section": "1.2.2 현황 및 흐름분석", + "AuditChecklist": [ + "관리체계 전 영역에 대한 정보서비스 현황을 식별하고 업무 절차와 흐름을 파악하여 문서화하고 있는가?", + "관리체계 범위 내 개인정보 처리 현황을 식별하고 개인정보의 흐름을 파악하여 개인정보 흐름도 등으로 문서화하고 있는가?", + "서비스 및 업무, 정보자산 등의 변화에 따른 업무절차 및 개인정보 흐름을 주기적으로 검토하여 흐름도 등 관련 문서의 최신성을 유지하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보서비스 현황표", + "정보서비스 업무흐름표·업무흐름도", + "개인정보 처리 현황표(ISMS-P 인증인 경우)", + "개인정보 흐름표·흐름도(ISMS-P 인증인 경우)" + ], + "NonComplianceCases": [ + "사례 1 : 관리체계 범위 내 주요 서비스의 업무 절차·흐름 및 현황에 문서화가 이루어지지 않은 경우", + "사례 2 : 개인정보 흐름도를 작성하였으나, 실제 개인정보의 흐름과 상이한 부분이 다수 존재하거나 중요한 개인정보 흐름이 누락되어 있는 경우", + "사례 3 : 최초 개인정보 흐름도 작성 이후에 현행화가 이루어지지 않아 변화된 개인정보 흐름이 흐름도에 반영되지 않고 있는 경우" + ] + } + ] + }, + { + "Id": "1.2.3", + "Name": "위험 평가", + "Description": "조직의 대내외 환경분석을 통하여 유형별 위협정보를 수집하고 조직에 적합한 위험 평가 방법을 선정하여 관리체계 전 영역에 대하여 연 1회 이상 위험을 평가하며, 수용할 수 있는 위험은 경영진의 승인을 받아 관리하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.2. 위험 관리", + "Section": "1.2.3 위험 평가", + "AuditChecklist": [ + "조직 또는 서비스의 특성에 따라 다양한 측면에서 발생할 수 있는 위험을 식별하고 평가할 수 있는 방법을 정의하고 있는가?", + "위험관리 방법 및 절차(수행인력, 기간, 대상, 방법, 예산 등)를 구체화한 위험관리계획을 매년 수립하고 있는가?", + "위험관리계획에 따라 연 1회 이상 정기적으로 또는 필요한 시점에 위험평가를 수행하고 있는가?", + "조직에서 수용 가능한 목표 위험수준을 정하고, 그 수준을 초과하는 위험을 식별하고 있는가?", + "위험식별 및 평가 결과를 경영진에게 보고하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "위험관리 지침", + "위험관리 매뉴얼·가이드", + "위험관리 계획서", + "위험평가 결과보고서", + "정보보호 및 개인정보보호 위원회 회의록", + "정보보호 및 개인정보보호 실무 협의회 회의록", + "정보자산 및 개인정보자산 목록", + "정보서비스 및 개인정보 흐름표·흐름도" + ], + "NonComplianceCases": [ + "사례 1 : 수립된 위험관리계획서에 위험평가 기간 및 위험관리 대상과 방법이 정의되어 있으나, 위험관리 수행 인력과 소요 예산 등 구체적인 실행계획이 누락되어 있는 경우", + "사례 2 : 전년도에는 위험평가를 수행하였으나, 금년도에는 자산 변경이 없었다는 사유로 위험 평가를 수행하지 않은 경우", + "사례 3 : 위험관리 계획에 따라 위험 식별 및 평가를 수행하고 있으나, 범위 내 중요 정보자산에 대한 위험 식별 및 평가를 수행하지 않았거나, 정보보호 관련 법적 요구 사항 준수 여부에 따른 위험을 식별 및 평가하지 않은 경우", + "사례 4 : 위험관리 계획에 따라 위험 식별 및 평가를 수행하고 수용 가능한 목표 위험수준을 설정하였으나, 관련 사항을 경영진(정보보호 최고책임자 등)에 보고하여 승인받지 않은 경우", + "사례 5 : 내부 지침에 정의한 위험 평가 방법과 실제 수행한 위험 평가 방법이 상이할 경우", + "사례 6 : 정보보호 관리체계와 관련된 관리적·물리적 영역의 위험 식별 및 평가를 수행하지 않고, 단순히 기술적 취약점진단 결과를 위험 평가 결과로 갈음하고 있는 경우", + "사례 7 : 수용 가능한 목표 위험수준(DoA)을 타당한 사유 없이 과도하게 높이는 것으로 결정함에 따라, 실질적으로 대응이 필요한 주요 위험들이 조치가 불필요한 위험(수용 가능한 위험)으로 지정된 경우" + ] + } + ] + }, + { + "Id": "1.2.4", + "Name": "보호대책 선정", + "Description": "위험 평가 결과에 따라 식별된 위험을 처리하기 위하여 조직에 적합한 보호대책을 선정하고, 보호대책의 우선순위와 일정·담당자·예산 등을 포함한 이행계획을 수립하여 경영진의 승인을 받아야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.2. 위험 관리", + "Section": "1.2.4 보호대책 선정", + "AuditChecklist": [ + "식별된 위험에 대한 처리 전략(감소, 회피, 전가, 수용 등)을 수립하고 위험처리를 위한 보호대책을 선정하고 있는가?", + "보호대책의 우선순위를 고려하여 일정, 담당부서 및 담당자, 예산 등의 항목을 포함한 보호대책 이행계획을 수립하고 경영진에 보고하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보보호 및 개인정보보호 이행계획서·위험관리계획서", + "정보보호 및 개인정보보호 대책서", + "정보보호 및 개인정보보호 마스터플랜", + "정보보호 및 개인정보보호 이행계획 경영진 보고 및 승인 내역" + ], + "NonComplianceCases": [ + "사례 1 : 정보보호 및 개인정보보호 대책에 대한 이행계획은 수립하였으나, 정보보호 최고책임자 및 개인정보 보호책임자에게 보고가 이루어지지 않은 경우", + "사례 2 : 위험감소가 요구되는 일부 위험의 조치 이행계획이 누락되어 있는 경우", + "사례 3 : 법에 따라 의무적으로 이행하여야 할 사항, 보안 취약성이 높은 위험 등을 별도의 보호조치 계획 없이 위험수용으로 결정하여 조치하지 않은 경우", + "사례 4 : 위험수용에 대한 근거와 타당성이 미흡하고, 시급성 및 구현 용이성 등의 측면에서 즉시 또는 단기 조치가 가능한 위험요인에 대해서도 특별한 사유 없이 장기 조치계획으로 분류한 경우" + ] + } + ] + }, + { + "Id": "1.3.1", + "Name": "보호대책 구현", + "Description": "선정한 보호대책은 이행계획에 따라 효과적으로 구현하고, 경영진은 이행결과의 정확성과 효과성 여부를 확인하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.3. 관리체계 운영", + "Section": "1.3.1 보호대책 구현", + "AuditChecklist": [ + "이행계획에 따라 보호대책을 효과적으로 구현하고 이행결과의 정확성 및 효과성 여부를 경영진이 확인할 수 있도록 보고하고 있는가?", + "관리체계 인증기준별로 보호대책 구현 및 운영 현황을 기록한 운영명세서를 구체적으로 작성하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보보호 및 개인정보보호 이행계획서·위험관리계획서", + "정보보호 및 개인정보보호 대책서", + "정보보호 및 개인정보보호 이행계획 경과보고서(경영진 보고 포함)", + "정보보호 및 개인정보보호 이행 완료 보고서(경영진 보고 포함)", + "정보보호 및 개인정보보호 운영명세서" + ], + "NonComplianceCases": [ + "사례 1 : 정보보호 및 개인정보보호 대책에 대한 이행완료 결과를 정보보호 최고책임자 및 개인정보 보호책임자에게 보고하지 않은 경우", + "사례 2 : 위험조치 이행결과보고서는 ʻ조치 완료ʼ로 명시되어 있으나, 관련된 위험이 여전히 존재하거나 이행결과의 정확성 및 효과성이 확인되지 않은 경우", + "사례 3 : 전년도 정보보호대책 이행계획에 따라 중·장기로 분류된 위험들이 해당연도에 구현이 되고 있지 않거나 이행결과를 경영진이 검토 및 확인하고 있지 않은 경우", + "사례 4 : 운영명세서에 작성된 운영 현황이 실제와 일치하지 않고, 운명명세서에 기록되어 있는 관련 문서, 결재 내용, 회의록 등이 존재하지 않는 경우", + "사례 5 : 이행계획 시행에 대한 결과를 정보보호 최고책임자 및 개인정보 보호책임자에게 보고하였으나, 일부 미이행된 건에 대한 사유 보고 및 후속 조치가 이루어지지 않은 경우" + ] + } + ] + }, + { + "Id": "1.3.2", + "Name": "보호대책 공유", + "Description": "보호대책의 실제 운영 또는 시행할 부서 및 담당자를 파악하여 관련 내용을 공유하고 교육하여 지속적으로 운영되도록 하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.3. 관리체계 운영", + "Section": "1.3.2 보호대책 공유", + "AuditChecklist": [ + "구현된 보호대책을 운영 또는 시행할 부서 및 담당자를 명확하게 파악하고 있는가?", + "구현된 보호대책을 운영 또는 시행할 부서 및 담당자에게 관련 내용을 공유 또는 교육하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보보호 및 개인정보보호 대책별 운영부서 또는 시행부서 현황", + "정보보호 및 개인정보 관리계획 내부공유 증거자료(공지 내역, 교육자료, 공유 자료 등)" + ], + "NonComplianceCases": [ + "사례 1 : 정보보호대책을 마련하여 구현하고 있으나, 관련 내용을 충분히 공유·교육하지 않아 실제 운영 또는 수행 부서 및 담당자가 해당 내용을 인지하지 못하고 있는 경우" + ] + } + ] + }, + { + "Id": "1.3.3", + "Name": "운영현황 관리", + "Description": "조직이 수립한 관리체계에 따라 상시적 또는 주기적으로 수행하여야 하는 운영활동 및 수행 내역은 식별 및 추적이 가능하도록 기록하여 관리하고, 경영진은 주기적으로 운영활동의 효과성을 확인하여 관리하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.3. 관리체계 운영", + "Section": "1.3.3 운영현황 관리", + "AuditChecklist": [ + "관리체계 운영을 위하여 주기적 또는 상시적으로 수행하여야 하는 정보보호 및 개인정보보호 활동을 문서화하여 관리하고 있는가?", + "경영진은 주기적으로 관리체계 운영활동의 효과성을 확인하고 이를 관리하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제31조(개인정보 보호책임자의 지정)", + "정보통신망법 제45조의3(정보보호 최고책임자의 지정 등)" + ], + "AuditEvidence": [ + "정보보호 및 개인정보보호 연간계획서", + "정보보호 및 개인정보보호 운영현황표", + "정보보호 및 개인정보보호 활동 수행 여부 점검 결과" + ], + "NonComplianceCases": [ + "사례 1 : 정보보호 및 개인정보보호 관리체계 운영현황 중 주기적 또는 상시적인 활동이 요구되는 활동 현황을 문서화하지 않은 경우", + "사례 2 : 정보보호 및 개인정보보호 관리체계 운영현황에 대한 문서화는 이루어졌으나, 해당 운영현황에 대한 주기적인 검토가 이루어지지 않아 월별 및 분기별 활동이 요구되는 일부 정보보호 및 개인정보보호 활동이 누락되었고 일부는 이행 여부를 확인할 수 없는 경우" + ] + } + ] + }, + { + "Id": "1.4.1", + "Name": "법적 요구사항 준수 검토", + "Description": "조직이 준수하여야 할 정보보호 및 개인정보보호 관련 법적 요구사항을 주기적으로 파악하여 규정에 반영하고, 준수 여부를 지속적으로 검토하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.4. 관리체계 점검 및 개선", + "Section": "1.4.1 법적 요구사항 준수 검토", + "AuditChecklist": [ + "조직이 준수하여야 하는 정보보호 및 개인정보보호 관련 법적 요구사항을 파악하여 최신성을 유지하고 있는가?", + "법적 요구사항의 준수 여부를 연 1회 이상 정기적으로 검토하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "법적 준거성 검토 내역", + "정보보호 및 개인정보보호 정책·지침 검토 및 개정이력", + "정책·지침 신구대조표", + "법 개정사항 내부공유 자료", + "개인정보 손해배상 책임보장 입증 자료(사이버보험 약정서 등)", + "정보보호 공시 내역" + ], + "NonComplianceCases": [ + "사례 1 : 정보통신망법 및 개인정보 보호법이 최근 개정되었으나 개정사항이 조직에 미치는 영향을 검토하지 않았으며, 정책서·시행문서 및 법적준거성 체크리스트 등에도 해당 내용을 반영하지 않아 정책서·시행문서 및 법적준거성 체크리스트 등의 내용이 법령 내용과 일치하지 않은 경우", + "사례 2 : 조직에서 준수하여야 할 법률이 개정되었으나, 해당 법률 준거성 검토를 장기간 수행하지 않은 경우", + "사례 3 : 법적 준거성 준수 여부에 대한 검토가 적절히 이루어지지 않아 개인정보 보호법 등 법규 위반 사항이 다수 발견된 경우", + "사례 4 : 개인정보 보호법에 따라 개인정보 손해배상책임 보장제도 적용 대상이 되었으나, 이를 인지하지 못하여 보험 가입이나 준비금 적립을 하지 않은 경우 또는 보험 가입을 하였으나 이용자 수 및 매출액에 따른 최저가입금액 기준을 준수하지 못한 경우", + "사례 5 : 정보보호 공시 의무대상 사업자이지만 법에 정한 시점 내에 정보보호 공시가 시행되지 않은 경우", + "사례 6 : 모바일앱을 통해 위치정보사업자로부터 이용자의 개인위치정보를 전송받아 서비스에 이용하고 있으나, 위치기반서비스사업 신고를 하지 않은 경우", + "사례 7 : 국내에 주소 또는 영업소가 없는 개인정보처리자로서 전년도 말 기준 직전 3개월 간 그 개인정보가 저장·관리되고 있는 국내 정보주체의 수가 일일평균 100만명 이상인 자에 해당되어 국내대리인 지정의무에 해당됨에도 불구하고, 국내대리인을 문서로 지정하지 않은 경우" + ] + } + ] + }, + { + "Id": "1.4.2", + "Name": "관리체계 점검", + "Description": "관리체계가 내부 정책 및 법적 요구사항에 따라 효과적으로 운영되고 있는지 독립성과 전문성이 확보된 인력을 구성하여 연 1회 이상 점검하고, 발견된 문제점을 경영진에게 보고하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.4. 관리체계 점검 및 개선", + "Section": "1.4.2 관리체계 점검", + "AuditChecklist": [ + "법적 요구사항 및 수립된 정책에 따라 정보보호 및 개인정보보호 관리체계가 효과적으로 운영되는지를 점검하기 위한 관리체계 점검기준, 범위, 주기, 점검인력 자격요건 등을 포함한 관리체계 점검 계획을 수립하고 있는가?", + "관리체계 점검 계획에 따라 독립성, 객관성 및 전문성이 확보된 인력을 구성하여 연 1회 이상 점검을 수행하고 발견된 문제점을 경영진에게 보고하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "관리체계 점검 계획서(내부점검 계획서, 내부감사 계획서)", + "관리체계 점검 결과보고서", + "정보보호 및 개인정보보호 위원회 회의록" + ], + "NonComplianceCases": [ + "사례 1 : 관리체계 점검 인력에 점검 대상으로 식별된 전산팀 직원이 포함되어 전산팀 관리 영역에 대한 점검에 관여하고 있어, 점검의 독립성이 훼손된 경우", + "사례 2 : 금년도 관리체계 점검을 실시하였으나, 점검범위가 일부 영역에 국한되어 있어 정보보호 및 개인정보보호 관리체계 범위를 충족하지 못한 경우", + "사례 3 : 관리체계 점검팀이 위험평가 또는 취약점 점검 등 관리체계 구축 과정에 참여한 내부 직원 및 외부 컨설턴트로만 구성되어, 점검의 독립성이 확보되었다고 볼 수 없는 경우" + ] + } + ] + }, + { + "Id": "1.4.3", + "Name": "관리체계 개선", + "Description": "법적 요구사항 준수검토 및 관리체계 점검을 통하여 식별된 관리체계상의 문제점에 대한 원인을 분석하고 재발방지 대책을 수립·이행하여야 하며, 경영진은 개선 결과의 정확성과 효과성 여부를 확인하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "1. 관리체계 수립 및 운영", + "Subdomain": "1.4. 관리체계 점검 및 개선", + "Section": "1.4.3 관리체계 개선", + "AuditChecklist": [ + "법적 요구사항 준수검토 및 관리체계 점검을 통하여 식별된 관리체계상의 문제점에 대한 근본 원인을 분석하여 재발방지 및 개선 대책을 수립·이행하고 있는가?", + "재발방지 및 개선 결과의 정확성 및 효과성 여부를 확인하기 위한 기준과 절차를 마련하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "관리체계 점검 결과보고서", + "관리체계 점검 조치계획서·이행조치결과서", + "재발방지 대책", + "효과성 측정 지표 및 측정 결과(경영진 보고 포함)" + ], + "NonComplianceCases": [ + "사례 1 : 내부점검을 통하여 발견된 정보보호 및 개인정보보호 관리체계 운영상 문제점이 매번 동일하게 반복되어 발생되는 경우", + "사례 2 : 내부 규정에는 내부점검 시 발견된 문제점에 대해서는 근본원인에 대한 분석 및 재발방지 대책을 수립하도록 되어 있으나, 최근에 수행된 내부점검에서는 발견된 문제점에 대하여 근본원인 분석 및 재발방지 대책이 수립되지 않은 경우", + "사례 3 : 관리체계상 문제점에 대한 재발방지 대책을 수립하고 핵심성과지표를 마련하여 주기적으로 측정하고 있으나, 그 결과에 대하여 경영진 보고가 장기간 이루어지지 않은 경우", + "사례 4 : 관리체계 점검 시 발견된 문제점에 대하여 조치계획을 수립하지 않았거나 조치 완료 여부를 확인하지 않은 경우" + ] + } + ] + }, + { + "Id": "2.1.1", + "Name": "정책의 유지관리", + "Description": "정보보호 및 개인정보보호 관련 정책과 시행문서는 법령 및 규제, 상위 조직 및 관련 기관 정책과의 연계성, 조직의 대내외 환경변화 등에 따라 주기적으로 검토하여 필요한 경우 제·개정하고 그 내역을 이력관리하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.1. 정책, 조직, 자산 관리", + "Section": "2.1.1 정책의 유지관리", + "AuditChecklist": [ + "정보보호 및 개인정보보호 관련 정책 및 시행문서에 대한 정기적인 타당성 검토 절차를 수립·이행하고 있는가?", + "조직의 대내외 환경에 중대한 변화 발생 시 정보보호 및 개인정보보호 관련 정책 및 시행문서에 미치는 영향을 검토하고 필요시 제·개정하고 있는가?", + "정보보호 및 개인정보보호 관련 정책 및 시행문서의 제·개정 시 이해 관계자의 검토를 받고 있는가?", + "정보보호 및 개인정보보호 관련 정책 및 시행문서의 제·개정 내역에 대하여 이력관리를 하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "정보보호 및 개인정보보호 정책 및 시행문서(지침, 절차, 가이드, 매뉴얼 등)", + "정책·지침 정기·비정기 타당성 검토 결과", + "정책·지침 관련 부서와의 검토 회의록, 회람내용", + "정책·지침 제·개정 이력" + ], + "NonComplianceCases": [ + "사례 1 : 지침서와 절차서 간 패스워드 설정 규칙에 일관성이 없는 경우", + "사례 2 : 정보보호 활동(정보보호 교육, 암호화, 백업 등)의 대상, 주기, 수준, 방법 등이 관련 내부 규정, 지침, 절차에 서로 다르게 명시되어 일관성이 없는 경우", + "사례 3 : 데이터베이스에 대한 접근 및 작업이력을 효과적으로 기록 및 관리하기 위하여 데이터베이스 접근통제 솔루션을 신규로 도입하여 운영하고 있으나, 보안시스템 보안 관리지침 및 데이터베이스 보안 관리지침 등 내부 보안지침에 접근통제, 작업이력, 로깅, 검토 등에 관한 사항이 반영되어 있지 않은 경우", + "사례 4 : 개인정보보호 정책이 개정되었으나 정책 시행 기준일이 명시되어 있지 않으며, 관련 정책의 작성일, 작성자 및 승인자 등이 누락되어 있는 경우", + "사례 5 : 개인정보 보호 관련 법령, 고시 등에 중대한 변경사항이 발생하였으나, 이러한 변경이 개인정보보호 정책 및 시행문서에 미치는 영향을 검토하지 않았거나 변경사항을 반영하여 개정하지 않은 경우" + ] + } + ] + }, + { + "Id": "2.1.2", + "Name": "조직의 유지관리", + "Description": "조직의 각 구성원에게 정보보호와 개인정보보호 관련 역할 및 책임을 할당하고, 그 활동을 평가할 수 있는 체계와 조직 및 조직의 구성원 간 상호 의사소통할 수 있는 체계를 수립하여 운영하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.1. 정책, 조직, 자산 관리", + "Section": "2.1.2 조직의 유지관리", + "AuditChecklist": [ + "정보보호 및 개인정보보호 관련 책임자와 담당자의 역할 및 책임을 명확히 정의하고 있는가?", + "정보보호 및 개인정보보호 관련 책임자와 담당자의 활동을 평가할 수 있는 체계를 수립하고 있는가?", + "정보보호 및 개인정보보호 관련 조직 및 조직의 구성원 간 상호 의사소통할 수 있는 체계 및 절차를 수립·이행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무), 제31조(개인정보 보호책임자의 지정)", + "정보통신망법 제45조의3(정보보호 최고책임자의 지정 등)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "정보보호 및 개인정보보호 조직도", + "정보보호 및 개인정보보호 조직 직무기술서", + "정보보호 및 개인정보보호 업무 분장표", + "정보보호 및 개인정보보호 정책·지침, 내부 관리계획", + "정보보호 및 개인정보보호 의사소통 관리계획", + "의사소통 수행 이력(월간보고, 주간보고, 내부공지 등)", + "의사소통 채널(정보보호포털, 게시판 등)" + ], + "NonComplianceCases": [ + "사례 1 : 내부 지침 및 직무기술서에 정보보호 최고책임자, 개인정보 보호책임자 및 관련 담당자의 역할과 책임을 정의하고 있으나, 실제 운영현황과 일치하지 않는 경우", + "사례 2 : 정보보호 최고책임자 및 관련 담당자의 활동을 주기적으로 평가할 수 있는 목표, 기준, 지표 등의 체계가 마련되어 있지 않은 경우", + "사례 3 : 내부 지침에는 부서별 정보보호 담당자는 정보보호와 관련된 KPI를 설정하여 인사평가 시 반영하도록 되어 있으나, 부서별 정보보호 담당자의 KPI에 정보보호와 관련된 사항이 전혀 반영되어 있지 않은 경우", + "사례 4 : 정보보호 최고책임자 및 개인정보 보호책임자가 지정되어 있으나, 관련 법령에서 요구하는 역할 및 책임이 내부 지침이나 직무기술서 등에 구체적으로 명시되어 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.1.3", + "Name": "정보자산 관리", + "Description": "정보자산의 용도와 중요도에 따른 취급 절차 및 보호대책을 수립·이행하고, 자산별 책임소재를 명확히 정의하여 관리하여야 한다.", + "Checks": [ + "organizations_tags_policies_enabled_and_attached", + "organizations_account_part_of_organizations", + "macie_is_enabled", + "config_recorder_all_regions_enabled", + "resourceexplorer2_indexes_found", + "account_maintain_current_contact_details", + "account_maintain_different_contact_details_to_security_billing_and_operations", + "account_security_questions_are_registered_in_the_aws_account", + "account_security_contact_information_is_registered" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.1. 정책, 조직, 자산 관리", + "Section": "2.1.3 정보자산 관리", + "AuditChecklist": [ + "정보자산의 보안등급에 따른 취급절차(생성·도입, 저장, 이용, 파기) 및 보호대책을 정의하고 이행하고 있는가?", + "식별된 정보자산에 대하여 책임자 및 관리자를 지정하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보자산 목록(책임자, 담당자 지정)", + "정보자산 취급 절차(문서, 정보시스템 등)", + "정보자산 관리 시스템 화면", + "정보자산 보안등급 표시 내역" + ], + "NonComplianceCases": [ + "사례 1 : 내부 지침에 따라 문서에 보안등급을 표기하도록 되어 있으나, 이를 표시하지 않은 경우", + "사례 2 : 정보자산별 담당자 및 책임자를 식별하지 않았거나, 자산목록 현행화가 미흡하여 퇴직, 전보 등 인사이동이 발생하여 주요 정보자산의 담당자 및 책임자가 변경되었음에도 이를 식별하지 않은 경우", + "사례 3 : 식별된 정보자산에 대한 중요도 평가를 실시하여 보안등급을 부여하고 정보 자산목록에 기록하고 있으나, 보안등급에 따른 취급절차를 정의하지 않은 경우" + ] + } + ] + }, + { + "Id": "2.2.1", + "Name": "주요 직무자 지정 및 관리", + "Description": "개인정보 및 중요정보의 취급이나 주요 시스템 접근 등 주요 직무의 기준과 관리방안을 수립하고, 주요 직무자를 최소한으로 지정하여 그 목록을 최신으로 관리하여야 한다.", + "Checks": [ + "organizations_delegated_administrators", + "account_security_contact_information_is_registered", + "iam_support_role_created" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.2. 인적 보안", + "Section": "2.2.1 주요 직무자 지정 및 관리", + "AuditChecklist": [ + "개인정보 및 중요정보의 취급, 주요 시스템 접근 등 주요 직무의 기준을 명확히 정의하고 있는가?", + "주요 직무를 수행하는 임직원 및 외부자를 주요 직무자로 지정하고 그 목록을 최신으로 관리하고 있는가?", + "업무상 개인정보를 취급하는 자를 개인정보취급자로 지정하고 목록을 최신으로 관리하고 있는가?", + "업무 필요성에 따라 주요 직무자 및 개인정보취급자 지정을 최소화하는 등 관리방안을 수립·이행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제28조(개인정보취급자에 대한 감독), 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "주요 직무 기준", + "주요직무자 목록", + "개인정보취급자 목록", + "중요 정보시스템 및 개인정보처리시스템 계정 및 권한 관리 대장", + "주요 직무자에 대한 관리 현황(교육 결과, 보안서약서 등)" + ], + "NonComplianceCases": [ + "사례 1 : 주요 직무자 명단(개인정보취급자 명단, 비밀정보관리자 명단 등)을 작성하고 있으나, 대량의 개인정보 등 중요정보를 취급하는 일부 임직원(DBA, DLP 관리자 등)을 명단에 누락한 경우", + "사례 2 : 주요 직무자 및 개인정보취급자 목록을 관리하고 있으나, 퇴사한 임직원이 포함되어 있고 최근 신규 입사한 인력이 포함되어 있지 않는 등 현행화 관리가 되어 있지 않은 경우", + "사례 3 : 부서 단위로 개인정보취급자 권한을 일괄 부여하고 있어 실제 개인정보를 취급할 필요가 없는 인원까지 과다하게 개인정보취급자로 지정된 경우", + "사례 4 : 내부 지침에는 주요 직무자 권한 부여 시에는 보안팀의 승인을 받고 주요 직무에 따른 보안서약서를 작성하도록 하고 있으나, 보안팀 승인 및 보안서약서 작성 없이 등록된 주요 직무자가 다수 존재하는 경우" + ] + } + ] + }, + { + "Id": "2.2.2", + "Name": "직무 분리", + "Description": "권한 오·남용 등으로 인한 잠재적인 피해 예방을 위하여 직무 분리 기준을 수립하고 적용하여야 한다. 다만 불가피하게 직무 분리가 어려운 경우 별도의 보완대책을 마련하여 이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.2. 인적 보안", + "Section": "2.2.2 직무 분리", + "AuditChecklist": [ + "권한 오·남용 등으로 인한 잠재적인 피해 예방을 위하여 직무 분리 기준을 수립하여 적용하고 있는가?", + "직무 분리가 어려운 경우 직무자 간 상호 검토, 상위관리자 정기 모니터링 및 변경사항 승인, 책임추적성 확보 방안 등의 보완통제를 마련하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "직무 분리 관련 지침(인적 보안 지침 등)", + "직무기술서(시스템 운영·관리, 개발·운영 등)", + "직무 미분리 시 보완통제 현황" + ], + "NonComplianceCases": [ + "사례 1 : 조직의 규모와 인원이 담당자별 직무 분리가 충분히 가능한 조직임에도 업무 편의성만을 사유로 내부 규정으로 정한 직무 분리 기준을 준수하고 있지 않은 경우", + "사례 2 : 조직의 특성상 경영진의 승인을 받은 후 개발과 운영 직무를 병행하고 있으나, 직무자 간 상호 검토, 상위관리자의 주기적인 직무수행 모니터링 및 변경 사항 검토·승인, 직무자의 책임추적성 확보 등의 보완통제 절차가 마련되어 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.2.3", + "Name": "보안 서약", + "Description": "정보자산을 취급하거나 접근권한이 부여된 임직원·임시직원·외부자 등이 내부 정책 및", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.2. 인적 보안", + "Section": "2.2.3 보안 서약", + "AuditChecklist": [ + "신규 인력 채용 시 정보보호 및 개인정보보호 책임이 명시된 정보보호 및 개인정보보호 서약서를 받고 있는가?", + "임시직원, 외주용역직원 등 외부자에게 정보자산에 대한 접근권한을 부여할 경우 정보보호 및 개인정보보호에 대한 책임, 비밀유지 의무 등이 명시된 서약서를 받고 있는가?", + "임직원 퇴직 시 별도의 비밀유지에 관련한 서약서를 받고 있는가?", + "정보보호, 개인정보보호 및 비밀유지 서약서는 안전하게 보관하고 필요시 쉽게 찾아볼 수 있도록 관리하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보보호 및 개인정보보호 서약서(임직원, 외부인력)", + "비밀유지서약서(퇴직자)" + ], + "NonComplianceCases": [ + "사례 1 : 신규 입사자에 대해서는 입사 절차상에 보안서약서를 받도록 규정하고 있으나, 최근에 입사한 일부 직원의 보안서약서 작성이 누락된 경우", + "사례 2 : 임직원에 대해서는 보안서약서를 받고 있으나, 정보처리시스템에 직접 접속이 가능한 외주 인력에 대해서는 보안서약서를 받지 않은 경우", + "사례 3 : 제출된 정보보호 및 개인정보보호 서약서를 모아 놓은 문서철이 비인가자가 접근 가능한 상태로 사무실 책상에 방치되어 있는 등 관리가 미흡한 경우", + "사례 4 : 개인정보취급자에 대하여 보안서약서만 받고 있으나, 보안서약서 내에 비밀유지에 대한 내용만 있고 개인정보보호에 관한 책임 및 내용이 포함되어 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.2.4", + "Name": "인식제고 및 교육훈련", + "Description": "임직원 및 관련 외부자가 조직의 관리체계와 정책을 이해하고 직무별 전문성을 확보할 수 있도록 연간 인식제고 활동 및 교육훈련 계획을 수립·운영하고, 그 결과에 따른 효과성을 평가하여 다음 계획에 반영하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.2. 인적 보안", + "Section": "2.2.4 인식제고 및 교육훈련", + "AuditChecklist": [ + "정보보호 및 개인정보보호 교육의 시기, 기간, 대상, 내용, 방법 등의 내용이 포함된 연간 교육 계획을 수립하고 경영진의 승인을 받고 있는가?", + "관리체계 범위 내 모든 임직원과 외부자를 대상으로 연간 교육 계획에 따라 연 1회 이상 정기적으로 교육을 수행하고, 관련 법규 및 규정의 중대한 변경 시 이에 대한 추가교육을 수행하고 있는가?", + "임직원 채용 및 외부자 신규 계약 시 업무 시작 전에 정보보호 및 개인정보보호 교육을 시행하고 있는가?", + "IT 및 정보보호, 개인정보보호 조직 내 임직원은 정보보호 및 개인정보보호와 관련하여 직무별 전문성 제고를 위한 별도의 교육을 받고 있는가?", + "교육시행에 대한 기록을 남기고 교육 효과와 적정성을 평가하여 다음 교육 계획에 반영하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제26조(업무위탁에 따른 개인정보의 처리 제한), 제28조(개인정보 취급자에 대한 감독), 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검)" + ], + "AuditEvidence": [ + "정보보호 및 개인정보보호 교육 계획서", + "교육 결과보고서", + "공통, 직무별 교육자료", + "교육참석자 목록" + ], + "NonComplianceCases": [ + "사례 1 : 전년도에는 연간 정보보호 및 개인정보보호 교육 계획을 수립하여 이행하였으나, 당해 연도에 타당한 사유 없이 연간 정보보호 및 개인정보보호 교육 계획을 수립하지 않은 경우", + "사례 2 : 연간 정보보호 및 개인정보보호 교육 계획에 교육 주기와 대상은 명시하고 있으나, 시행 일정, 내용 및 방법 등의 내용이 포함되어 있지 않은 경우", + "사례 3 : 연간 정보보호 및 개인정보보호 교육 계획에 전 직원을 대상으로 하는 개인정보보호 인식 교육은 일정시간 계획되어 있으나, 개인정보 보호책임자 및 개인정보담당자 등 직무별로 필요한 개인정보보호 관련 교육 계획이 포함되어 있지 않은 경우", + "사례 4 : 정보보호 및 개인정보보호 교육 계획서 및 결과 보고서를 확인한 결과, 인증범위 내의 정보자산 및 설비에 접근하는 외주용역업체 직원(전산실 출입 청소원, 경비원, 외주개발자 등)을 교육 대상에서 누락한 경우", + "사례 5 : 당해 연도 정보보호 및 개인정보보호 교육을 실시하였으나, 교육시행 및 평가에 관한 기록(교육 자료, 출석부, 평가 설문지, 결과보고서 등) 일부를 남기지 않고 있는 경우", + "사례 6 : 정보보호 및 개인정보보호 교육 미이수자를 파악하지 않고 있거나, 해당 미이수자에 대한 추가교육 방법(전달교육, 추가교육, 온라인교육 등)을 수립·이행하고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.2.5", + "Name": "퇴직 및 직무변경 관리", + "Description": "퇴직 및 직무변경 시 인사·정보보호·개인정보보호·IT 등 관련 부서별 이행하여야 할 자산반납, 계정 및 접근권한 회수·조정, 결과확인 등의 절차를 수립·관리하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.2. 인적 보안", + "Section": "2.2.5 퇴직 및 직무변경 관리", + "AuditChecklist": [ + "퇴직, 직무변경, 부서이동, 휴직 등으로 인한 인사변경 내용이 인사부서, 정보보호 및 개인정보보호 부서, 정보시스템 및 개인정보처리시스템 운영부서 간 공유되고 있는가?", + "조직 내 인력(임직원, 임시직원, 외주용역직원 등)의 퇴직 또는 직무변경 시 지체 없는 정보자산 반납, 접근권한 회수·조정, 결과 확인 등의 절차를 수립·이행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제5조(접근 권한의 관리)" + ], + "AuditEvidence": [ + "퇴직 및 직무변경 절차서", + "퇴직 시 자산(계정) 반납관리대장", + "퇴직자 보안점검 체크리스트 및 점검 내역" + ], + "NonComplianceCases": [ + "사례 1 : 직무 변동에 따라 개인정보취급자에서 제외된 인력의 계정과 권한이 개인정보처리시스템에 그대로 남아 있는 경우", + "사례 2 : 최근에 퇴직한 주요직무자 및 개인정보취급자에 대하여 자산반납, 권한 회수 등의 퇴직절차 이행 기록이 확인되지 않은 경우", + "사례 3 : 임직원 퇴직 시 자산반납 관리는 잘 이행하고 있으나, 인사규정에서 정한 퇴직자 보안점검 및 퇴직확인서를 작성하지 않은 경우", + "사례 4 : 개인정보취급자 퇴직 시 개인정보처리시스템의 접근 권한은 지체 없이 회수되었지만, 출입통제 시스템 및 VPN 등 일부 시스템의 접근 권한이 회수되지 않은 경우" + ] + } + ] + }, + { + "Id": "2.2.6", + "Name": "보안 위반 시 조치", + "Description": "임직원 및 관련 외부자가 법령, 규제 및 내부정책을 위반한 경우 이에 따른 조치 절차를 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.2. 인적 보안", + "Section": "2.2.6 보안 위반 시 조치", + "AuditChecklist": [ + "임직원 및 관련 외부자가 법령과 규제 및 내부정책에 따른 정보보호 및 개인정보보호 책임과 의무를 위반한 경우에 대한 처벌 규정을 수립하고 있는가?", + "정보보호 및 개인정보 보호 위반 사항이 적발된 경우 내부 절차에 따른 조치를 수행하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "인사 규정(정보보호 및 개인정보보호 관련 규정 위반에 따른 처벌규정)", + "정보보호 및 개인정보보호 지침 위반자 징계 내역", + "사고 사례(전사 공지, 교육 내용)" + ], + "NonComplianceCases": [ + "사례 1 : 정보보호 및 개인정보보호 규정 위반자에 대한 처리 기준 및 절차가 내부 규정에 전혀 포함되어 있지 않은 경우", + "사례 2 : 보안시스템(DLP, 데이터베이스 접근제어시스템, 내부정보유출통제시스템 등)을 통하여 정책 위반이 탐지된 관련자에게 경고 메시지를 전달하고 있으나, 이에 대한 소명 및 추가 조사, 징계 처분 등 내부 규정에 따른 후속 조치가 이행되고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.3.1", + "Name": "외부자 현황 관리", + "Description": "업무의 일부(개인정보취급, 정보보호, 정보시스템 운영 또는 개발 등)를 외부에 위탁하거나 외부의 시설 또는 서비스(집적정보통신시설, 클라우드 서비스, 애플리케이션 서비스 등)를 이용하는 경우 그 현황을 식별하고 법적 요구사항 및 외부 조직·서비스로부터 발생되는 위험을 파악하여 적절한 보호대책을 마련하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.3. 외부자 보안", + "Section": "2.3.1 외부자 현황 관리", + "AuditChecklist": [ + "관리체계 범위 내에서 발생하고 있는 업무 위탁 및 외부 시설·서비스의 이용 현황을 식별하고 있는가?", + "업무 위탁 및 외부 시설·서비스의 이용에 따른 법적 요구사항과 위험을 파악하고 적절한 보호대책을 마련하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제26조(업무위탁에 따른 개인정보의 처리 제한)", + "정보통신망법 제50조의3(영리목적의 광고성 정보 전송의 위탁 등)" + ], + "AuditEvidence": [ + "외부 위탁 및 외부 시설·서비스 현황", + "외부 위탁 계약서", + "위험분석 보고서 및 보호대책", + "위탁 보안관리 지침, 체크리스트 등" + ], + "NonComplianceCases": [ + "사례 1 : 내부 규정에 따라 외부 위탁 및 외부 시설·서비스 현황을 목록으로 관리하고 있으나, 몇 개월 전에 변경된 위탁업체가 목록에 반영되어 있지 않은 등 현행화 관리가 미흡한 경우", + "사례 2 : 관리체계 범위 내 일부 개인정보처리시스템을 외부 클라우드 서비스로 이전하였으나, 이에 대한 식별 및 위험평가가 수행되지 않은 경우" + ] + } + ] + }, + { + "Id": "2.3.2", + "Name": "외부자 계약 시 보안", + "Description": "외부 서비스를 이용하거나 외부자에게 업무를 위탁하는 경우 이에 따른 정보보호 및 개인정보보호 요구사항을 식별하고, 관련 내용을 계약서 또는 협정서 등에 명시하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.3. 외부자 보안", + "Section": "2.3.2 외부자 계약 시 보안", + "AuditChecklist": [ + "중요정보 및 개인정보 처리와 관련된 외부 서비스 및 위탁 업체를 선정하는 경우 정보보호 및 개인정보 보호 역량을 고려하도록 절차를 마련하고 있는가?", + "외부 서비스 이용 및 업무 위탁에 따른 정보보호 및 개인정보보호 요구사항을 식별하고 이를 계약서 또는 협정서에 명시하고 있는가?", + "정보시스템 및 개인정보처리시스템 개발을 위탁하는 경우 개발 시 준수하여야 할 정보보호 및 개인정보보호 요구사항을 계약서에 명시하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제26조(업무위탁에 따른 개인정보의 처리 제한)" + ], + "AuditEvidence": [ + "위탁 계약서", + "정보보호 및 개인정보보호 협약서(약정서, 부속합의서)", + "위탁 관련 내부 지침", + "위탁업체 선정 관련 RFP(제안요청서), 평가표" + ], + "NonComplianceCases": [ + "사례 1 : IT 운영, 개발 및 개인정보 처리업무를 위탁하는 외주용역업체에 대한 위탁계약서가 존재하지 않는 경우", + "사례 2 : 개인정보 처리업무를 위탁하는 외부업체와의 위탁계약서상에 개인정보 보호법 등 법령에서 요구하는 일부 항목(관리·감독에 관한 사항 등)이 포함되어 있지 않은 경우", + "사례 3 : 인프라 운영과 개인정보 처리업무 일부를 외부업체에 위탁하고 있으나, 계약서 등에는 위탁업무의 특성에 따른 보안 요구사항을 식별·반영하지 않고 비밀유지 및 손해배상에 관한 일반 사항만 규정하고 있는 경우" + ] + } + ] + }, + { + "Id": "2.3.3", + "Name": "외부자 보안 이행 관리", + "Description": "계약서, 협정서, 내부정책에 명시된 정보보호 및 개인정보보호 요구사항에 따라 외부자의 보호대책 이행 여부를 주기적인 점검 또는 감사 등 관리·감독하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.3. 외부자 보안", + "Section": "2.3.3 외부자 보안 이행 관리", + "AuditChecklist": [ + "외부자가 계약서, 협정서, 내부정책에 명시된 정보보호 및 개인정보보호 요구사항을 준수하고 있는지 주기적으로 점검 또는 감사를 수행하고 있는가?", + "외부자에 대한 점검 또는 감사 시 발견된 문제점에 대하여 개선계획을 수립·이행하고 있는가?", + "개인정보 처리업무를 위탁받은 수탁자가 관련 업무를 제3자에게 재위탁하는 경우 위탁자의 동의를 받도록 하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제26조(업무위탁에 따른 개인정보의 처리 제한)", + "정보통신망법 제50조의3(영리목적의 광고성 정보 전송의 위탁 등)" + ], + "AuditEvidence": [ + "외부자 및 수탁자 보안점검 결과", + "외부자 및 수탁자 교육 내역(교육 결과, 참석자 명단, 교육교재 등)", + "개인정보 위탁 계약서", + "개인정보 처리업무 재위탁 시 위탁자 동의 증거자료" + ], + "NonComplianceCases": [ + "사례 1 : 회사 내에 상주하여 IT 개발 및 운영 업무를 수행하는 외주업체에 대해서는 정기적으로 보안점검을 수행하고 있지 않은 경우", + "사례 2 : 개인정보 수탁자에 대하여 보안교육을 실시하라는 공문을 발송하고 있으나, 교육 수행 여부를 확인하고 있지 않은 경우", + "사례 3 : 수탁자가 자체적으로 보안점검을 수행한 후 그 결과를 통지하도록 하고 있으나, 수탁자가 보안 점검을 충실히 수행하고 있는지 여부에 대하여 확인하는 절차가 존재하지 않아 보안점검 결과의 신뢰성이 매우 떨어지는 경우", + "사례 4 : 개인정보 처리업무 수탁자 중 일부가 위탁자의 동의 없이 해당 업무를 제3자에게 재위탁한 경우", + "사례 5 : 영리 목적의 광고성 정보전송 업무를 타인에게 위탁하면서 수탁자에 대한 관리·감독을 수행하지 않고 있는 경우" + ] + } + ] + }, + { + "Id": "2.3.4", + "Name": "외부자 계약 변경 및 만료 시 보안", + "Description": "외부자 계약만료, 업무종료, 담당자 변경 시에는 제공한 정보자산 반납, 정보시스템 접근계정 삭제, 중요정보 파기, 업무 수행 중 취득정보의 비밀유지 확약서 징구 등의 보호대책을 이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.3. 외부자 보안", + "Section": "2.3.4 외부자 계약 변경 및 만료 시 보안", + "AuditChecklist": [ + "외부자 계약만료, 업무 종료, 담당자 변경 시 공식적인 절차에 따른 정보자산 반납, 정보시스템 접근계정 삭제, 비밀유지 확약서 징구 등이 이루어질 수 있도록 보안대책을 수립·이행하고 있는가?", + "외부자 계약 만료 시 위탁 업무와 관련하여 외부자가 중요정보 및 개인정보를 보유하고 있는지 확인하고 이를 회수·파기할 수 있도록 절차를 수립·이행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제26조(업무위탁에 따른 개인정보의 처리 제한)", + "정보통신망법 제50조의3(영리목적의 광고성 정보 전송의 위탁 등)" + ], + "AuditEvidence": [ + "정보보호 및 개인정보보호 서약서", + "비밀유지 확약서", + "정보 및 개인정보 파기 확약서", + "외부자 계약 종료와 관련된 내부 정책, 지침" + ], + "NonComplianceCases": [ + "사례 1 : 일부 정보시스템에서 계약 만료된 외부자의 계정 및 권한이 삭제되지 않고 존재하는 경우", + "사례 2 : 외주용역사업 수행과정에서 일부 용역업체 담당자가 교체되거나 계약 만료로 퇴직하였으나, 관련 인력들에 대한 퇴사 시 보안서약서 등 내부 규정에 따른 조치가 이행되지 않은 경우", + "사례 3 : 개인정보 처리 위탁한 업체와 계약 종료 이후 보유하고 있는 개인정보를 파기하였는지 여부를 확인·점검하지 않은 경우" + ] + } + ] + }, + { + "Id": "2.4.1", + "Name": "보호구역 지정", + "Description": "물리적·환경적 위협으로부터 개인정보 및 중요정보, 문서, 저장매체, 주요 설비 및 시스템 등을 보호하기 위하여 통제구역·제한구역·접견구역 등 물리적 보호구역을 지정하고 구역별 보호대책을 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.4. 물리 보안", + "Section": "2.4.1 보호구역 지정", + "AuditChecklist": [ + "물리적·환경적 위협으로부터 개인정보 및 중요정보, 문서, 저장매체, 주요 설비 및 시스템 등을 보호하기 위하여 통제구역, 제한구역, 접견구역 등 물리적 보호구역 지정기준을 마련하고 있는가?", + "물리적 보호구역 지정기준에 따라 보호구역을 지정하고 구역별 보호대책을 수립·이행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제10조(물리적 안전조치)" + ], + "AuditEvidence": [ + "물리적 보안 지침(보호구역 지정 기준)", + "보호구역 지정 현황", + "보호구역 표시", + "보호구역별 보호대책 현황" + ], + "NonComplianceCases": [ + "사례 1 : 내부 물리보안 지침에는 개인정보 보관시설 및 시스템 구역을 통제구역으로 지정한다고 명시되어 있으나, 멤버십 가입신청 서류가 보관되어 있는 문서고 등 일부 대상 구역이 통제구역에서 누락된 경우", + "사례 2 : 내부 물리보안 지침에 통제구역에 대해서는 지정된 양식의 통제구역 표지판을 설치하도록 명시하고 있으나, 일부 통제구역에 표시판을 설치하지 않은 경우" + ] + } + ] + }, + { + "Id": "2.4.2", + "Name": "출입통제", + "Description": "보호구역은 인가된 사람만이 출입하도록 통제하고 책임추적성을 확보할 수 있도록 출입 및 접근 이력을 주기적으로 검토하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.4. 물리 보안", + "Section": "2.4.2 출입통제", + "AuditChecklist": [ + "보호구역은 출입절차에 따라 출입이 허가된 자만 출입하도록 통제하고 있는가?", + "각 보호구역에 대한 내·외부자 출입기록을 일정기간 보존하고 출입기록 및 출입권한을 주기적으로 검토하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제10조(물리적 안전조치)" + ], + "AuditEvidence": [ + "출입 관리대장 및 출입로그", + "출입 등록 신청서 및 승인 내역", + "출입기록 검토서", + "출입통제시스템 관리화면(출입자 등록 현황 등)" + ], + "NonComplianceCases": [ + "사례 1 : 통제구역을 정의하여 보호대책을 수립하고 출입 가능한 임직원을 관리하고 있으나, 출입기록을 주기적으로 검토하지 않아 퇴직, 전배 등에 따른 장기 미출입자가 다수 존재하고 있는 경우", + "사례 2 : 전산실, 문서고 등 통제구역에 출입통제 장치가 설치되어 있으나, 타당한 사유 또는 승인 없이 장시간 개방 상태로 유지하고 있는 경우", + "사례 3 : 일부 외부 협력업체 직원에게 과도하게 전 구역을 상시 출입할 수 있는 출입카드를 부여하고 있는 경우" + ] + } + ] + }, + { + "Id": "2.4.3", + "Name": "정보시스템 보호", + "Description": "정보시스템은 환경적 위협과 유해요소, 비인가 접근 가능성을 감소시킬 수 있도록 중요도와 특성을 고려하여 배치하고, 통신 및 전력 케이블이 손상을 입지 않도록 보호하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.4. 물리 보안", + "Section": "2.4.3 정보시스템 보호", + "AuditChecklist": [ + "정보시스템의 중요도, 용도, 특성 등을 고려하여 배치 장소를 분리하고 있는가?", + "정보시스템의 실제 물리적 위치를 손쉽게 확인할 수 있는 방안을 마련하고 있는가?", + "전력 및 통신케이블을 외부로부터의 물리적 손상 및 전기적 영향으로부터 안전하게 보호하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보처리시설 도면", + "정보시스템 배치도", + "자산목록" + ], + "NonComplianceCases": [ + "사례 1 : 시스템 배치도가 최신 변경사항을 반영하여 업데이트되지 않아 장애가 발생된 정보시스템을 신속하게 확인할 수 없는 경우", + "사례 2 : 서버실 바닥 또는 랙에 많은 케이블이 정리되지 않고 뒤엉켜 있어 전기적으로 간섭, 손상, 누수, 부주의 등에 의한 장애 발생이 우려되는 경우" + ] + } + ] + }, + { + "Id": "2.4.4", + "Name": "보호설비 운영", + "Description": "보호구역에 위치한 정보시스템의 중요도 및 특성에 따라 온·습도 조절, 화재감지, 소화설비, 누수감지, UPS, 비상발전기, 이중전원선 등의 보호설비를 갖추고 운영절차를 수립·운영하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.4. 물리 보안", + "Section": "2.4.4 보호설비 운영", + "AuditChecklist": [ + "각 보호구역의 중요도 및 특성에 따라 화재, 수해, 전력 이상 등 인재 및 자연재해 등에 대비하여 필요한 설비를 갖추고 운영절차를 수립하여 운영하고 있는가?", + "외부 집적정보통신시설(IDC)에 위탁 운영하는 경우 물리적 보호에 필요한 요구사항을 계약서에 반영하고 운영상태를 주기적으로 검토하고 있는가?" + ], + "RelatedRegulations": [ + "정보통신망법 제46조(집적된 정보통신시설의 보호)", + "집적정보 통신시설 보호지침", + "소방시설 설치 및 관리에 관한 법률(소방시설법) 제12조(특정소방대상물에 설치하는 소방시설의 관리 등), 제16조(피난시설, 방화구역 및 방화시설의 관리)" + ], + "AuditEvidence": [ + "물리적 보안 지침(보호설비 관련)", + "전산실 설비 현황 및 점검표", + "IDC 위탁운영 계약서, SLA 등" + ], + "NonComplianceCases": [ + "사례 1 : 본사 전산실 등 일부 보호구역에 내부 지침에 정한 보호설비를 갖추고 있지 않은 경우", + "사례 2 : 전산실 내에 UPS, 소화설비 등의 보호설비는 갖추고 있으나, 관련 설비에 대한 운영 및 점검 기준을 수립하고 있지 않은 경우", + "사례 3 : 운영지침에 따라 전산실 내에 온·습도 조절기를 설치하였으나, 용량 부족으로 인하여 표준 온·습도를 유지하지 못하여 장애발생 가능성이 높은 경우" + ] + } + ] + }, + { + "Id": "2.4.5", + "Name": "보호구역 내 작업", + "Description": "보호구역 내에서의 비인가행위 및 권한 오·남용 등을 방지하기 위한 작업 절차를 수립 및이행하고, 작업 기록을 주기적으로 검토하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.4. 물리 보안", + "Section": "2.4.5 보호구역 내 작업", + "AuditChecklist": [ + "정보시스템 도입, 유지보수 등으로 보호구역 내 작업이 필요한 경우에 대한 공식적인 작업신청 및 수행 절차를 수립·이행하고 있는가?", + "보호구역 내 작업이 통제 절차에 따라 적절히 수행되었는지 여부를 확인하기 위하여 작업 기록을 주기적으로 검토하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "작업 신청서, 작업 일지", + "통제구역 출입 대장", + "통제구역에 대한 출입기록 및 작업 기록 검토 내역" + ], + "NonComplianceCases": [ + "사례 1 : 전산실 출입로그에는 외부 유지보수 업체 직원의 출입기록이 남아 있으나, 이에 대한 보호구역 작업 신청 및 승인 내역이 존재하지 않은 경우(내부 규정에 따른 보호구역 작업 신청 없이 보호구역 출입 및 작업이 이루어지고 있는 경우)", + "사례 2 : 내부 규정에는 보호구역 내 작업 기록에 대하여 분기별 1회 이상 점검하도록 되어 있으나, 특별한 사유 없이 장기간 동안 보호구역 내 작업 기록에 대한 점검이 이루어지고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.4.6", + "Name": "반출입 기기 통제", + "Description": "보호구역 내 정보시스템, 모바일 기기, 저장매체 등에 대한 반출입 통제절차를 수립 및이행하고 주기적으로 검토하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.4. 물리 보안", + "Section": "2.4.6 반출입 기기 통제", + "AuditChecklist": [ + "정보시스템, 모바일 기기, 저장매체 등을 보호구역에 반입하거나 반출하는 경우 정보유출, 악성코드 감염 등 보안사고 예방을 위한 통제 절차를 수립·이행하고 있는가?", + "반출입 통제절차에 따른 기록을 유지·관리하고, 절차 준수 여부를 확인할 수 있도록 반출입 이력을 주기적으로 점검하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제10조(물리적 안전조치)" + ], + "AuditEvidence": [ + "보호구역 내 반출입 신청서", + "반출입 관리대장", + "반출입 이력 검토 결과" + ], + "NonComplianceCases": [ + "사례 1 : 이동컴퓨팅기기 반출입에 대한 통제 절차를 수립하고 있으나, 통제구역 내 이동컴퓨팅기기 반입에 대한 통제를 하고 있지 않아 출입이 허용된 내·외부인이 이동컴퓨팅기기를 제약 없이 사용하고 있는 경우", + "사례 2 : 내부 지침에 따라 전산장비 반출입이 있는 경우 작업계획서에 반출입 내용을 기재하고 관리 책임자의 서명을 받도록 되어 있으나, 작업계획서의 반출입 기록에 관리책임자의 서명이 다수 누락되어 있는 경우" + ] + } + ] + }, + { + "Id": "2.4.7", + "Name": "업무환경 보안", + "Description": "공용으로 사용하는 사무용 기기(문서고, 공용 PC, 복합기, 파일서버 등) 및 개인 업무환경(업무용 PC, 책상 등)을 통하여 개인정보 및 중요정보가 비인가자에게 노출 또는 유출되지 않도록 클린데스크, 정기점검 등 업무환경 보호대책을 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.4. 물리 보안", + "Section": "2.4.7 업무환경 보안", + "AuditChecklist": [ + "문서고, 공용 PC, 복합기, 파일서버 등 공용으로 사용하는 시설 및 사무용 기기에 대한 보호대책을 수립·이행하고 있는가?", + "업무용 PC, 책상, 서랍 등 개인업무 환경을 통한 개인정보 및 중요정보의 유·노출을 방지하기 위한 보호대책을 수립·이행하고 있는가?", + "개인정보가 포함된 종이 인쇄물 등 개인정보의 출력·복사물을 안전하게 관리하기 위해 필요한 보호조치를 하고 있는가?", + "개인 및 공용업무 환경에서의 정보보호 준수 여부를 주기적으로 검토하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제10조(물리적 안전조치), 제12조(출력·복사시 안전조치)" + ], + "AuditEvidence": [ + "사무실 및 공용공간 보안점검 보고서", + "사무실 및 공용공간 보안점검표", + "미준수자에 대한 조치 사항(교육, 상벌 등)", + "출력·복사물 보호조치 현황" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보 내부 관리계획서 내 개인정보보호를 위한 생활보안 점검(클린데스크 운영 등)을 정기적으로 수행하도록 명시하고 있으나, 이를 이행하지 않은 경우", + "사례 2 : 멤버십 가입신청서 등 개인정보가 포함된 서류를 잠금장치가 없는 사무실 문서함에 보관한 경우", + "사례 3 : 직원들의 컴퓨터 화면보호기 및 패스워드가 설정되어 있지 않고, 휴가자 책상 위에 중요문서가 장기간 방치되어 있는 경우", + "사례 4 : 회의실 등 공용 사무 공간에 설치된 공용PC에 대한 보호대책이 수립되어 있지 않아 개인정보가 포함된 파일이 암호화되지 않은 채로 저장되어 있거나, 보안 업데이트 미적용, 백신 미설치 등 취약한 상태로 유지하고 있는 경우" + ] + } + ] + }, + { + "Id": "2.5.1", + "Name": "사용자 계정 관리", + "Description": "정보시스템과 개인정보 및 중요정보에 대한 비인가 접근을 통제하고 업무 목적에 따른 접근권한을 최소한으로 부여할 수 있도록 사용자 등록·해지 및 접근권한 부여·변경·말소 절차를 수립·이행하고, 사용자 등록 및 권한부여 시 사용자에게 보안책임이 있음을 규정화하고 인식시켜야 한다.", + "Checks": [ + "organizations_scp_check_deny_regions", + "cognito_user_pool_self_registration_disabled", + "cloudtrail_threat_detection_privilege_escalation", + "iam_user_administrator_access_policy", + "iam_customer_unattached_policy_no_administrative_privileges", + "iam_inline_policy_allows_privilege_escalation", + "iam_inline_policy_no_administrative_privileges", + "iam_aws_attached_policy_no_administrative_privileges", + "iam_policy_allows_privilege_escalation", + "iam_policy_no_full_access_to_kms", + "iam_role_cross_service_confused_deputy_prevention", + "iam_inline_policy_no_full_access_to_cloudtrail", + "iam_role_cross_account_readonlyaccess_policy", + "iam_inline_policy_no_full_access_to_kms", + "iam_user_accesskey_unused", + "iam_securityaudit_role_created", + "iam_no_custom_policy_permissive_role_assumption", + "iam_customer_attached_policy_no_administrative_privileges", + "iam_group_administrator_access_policy", + "iam_user_console_access_unused", + "iam_policy_attached_only_to_group_or_roles", + "iam_policy_no_full_access_to_cloudtrail", + "iam_role_administratoraccess_policy" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.5. 인증 및 권한관리", + "Section": "2.5.1 사용자 계정 관리", + "AuditChecklist": [ + "정보시스템과 개인정보 및 중요정보에 접근할 수 있는 사용자 계정 및 접근권한의 등록·변경·삭제에 관한 공식적인 절차를 수립·이행하고 있는가?", + "정보시스템과 개인정보 및 중요정보에 접근할 수 있는 사용자 계정 및 접근권한 생성 및 등록·변경 시 직무별 접근권한 분류 체계에 따라 업무상 필요한 최소한의 권한만을 부여하고 있는가?", + "사용자에게 계정 및 접근권한을 부여하는 경우 해당 계정에 대한 보안책임이 본인에게 있음을 명확히 인식시키고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제5조(접근 권한의 관리)" + ], + "AuditEvidence": [ + "사용자 계정 및 권한 신청서", + "사용자 계정 및 권한 관리대장 또는 화면", + "정보시스템 및 개인정보처리시스템별 접근권한 분류표", + "정보시스템 및 개인정보처리시스템별 사용자, 관리자, 개인정보취급자 목록" + ], + "NonComplianceCases": [ + "사례 1 : 사용자 및 개인정보취급자에 대한 계정·권한에 대한 사용자 등록, 해지 및 승인절차 없이 구두 요청, 이메일 등으로 처리하여 이에 대한 승인 및 처리 이력이 확인되지 않는 경우", + "사례 2 : 개인정보취급자가 휴가, 출장, 공가 등에 따른 업무 백업을 사유로 공식적인 절차를 거치지 않고 개인정보취급자로 지정되지 않은 인원에게 개인정보취급자 계정을 알려주는 경우", + "사례 3 : 정보시스템 또는 개인정보처리시스템 사용자에게 필요 이상의 과도한 권한을 부여하여 업무상 불필요한 정보 또는 개인정보에 접근이 가능한 경우" + ] + } + ] + }, + { + "Id": "2.5.2", + "Name": "사용자 식별", + "Description": "사용자 계정은 사용자별로 유일하게 구분할 수 있도록 식별자를 할당하고 추측 가능한 식별자 사용을 제한하여야 하며, 동일한 식별자를 공유하여 사용하는 경우 그 사유와 타당성을 검토하여 책임자의 승인 및 책임추적성 확보 등 보완대책을 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.5. 인증 및 권한관리", + "Section": "2.5.2 사용자 식별", + "AuditChecklist": [ + "정보시스템 및 개인정보처리시스템에서 사용자 및 개인정보취급자를 유일하게 구분할 수 있는 식별자를 할당하고 추측 가능한 식별자의 사용을 제한하고 있는가?", + "불가피한 사유로 동일한 식별자를 공유하여 사용하는 경우 그 사유와 타당성을 검토하고 보완대책을 마련하여 책임자의 승인을 받고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제5조(접근 권한의 관리)" + ], + "AuditEvidence": [ + "정보시스템 및 개인정보처리시스템 로그인 화면", + "정보시스템 및 개인정보처리시스템 관리자, 사용자, 개인정보취급자 계정 목록", + "예외 처리에 대한 승인 내역" + ], + "NonComplianceCases": [ + "사례 1 : 정보시스템(서버, 네트워크, 침입차단시스템, DBMS 등)의 계정 현황을 확인한 결과, 제조사에서 제공하는 기본 관리자 계정을 기술적으로 변경 가능함에도 불구하고 변경하지 않고 사용하고 있는 경우", + "사례 2 : 개발자가 개인정보처리시스템 계정을 공용으로 사용하고 있으나, 타당성 검토 또는 책임자의 승인 등이 없이 사용하고 있는 경우", + "사례 3 : 외부직원이 유지보수하고 있는 정보시스템의 운영계정을 별도의 승인 절차 없이 개인 계정처럼 사용하고 있는 경우" + ] + } + ] + }, + { + "Id": "2.5.3", + "Name": "사용자 인증", + "Description": "정보시스템과 개인정보 및 중요정보에 대한 사용자의 접근은 안전한 인증절차와 필요에 따라 강화된 인증방식을 적용하여야 한다. 또한 로그인 횟수 제한, 불법 로그인 시도 경고 등 비인가자 접근 통제방안을 수립·이행하여야 한다.", + "Checks": [ + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", + "cognito_identity_pool_guest_access_disabled", + "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", + "cognito_user_pool_advanced_security_enabled", + "cognito_user_pool_client_token_revocation_enabled", + "cognito_user_pool_client_prevent_user_existence_errors", + "cognito_user_pool_mfa_enabled", + "cognito_user_pool_self_registration_disabled", + "opensearch_service_domains_internal_user_database_enabled", + "opensearch_service_domains_use_cognito_authentication_for_kibana", + "directoryservice_supported_mfa_radius_enabled", + "iam_rotate_access_key_90_days", + "iam_root_hardware_mfa_enabled", + "iam_user_no_setup_initial_access_key", + "iam_root_mfa_enabled", + "iam_user_with_temporary_credentials", + "iam_user_hardware_mfa_enabled", + "iam_check_saml_providers_sts", + "iam_user_two_active_access_key", + "iam_user_accesskey_unused", + "iam_user_mfa_enabled_console_access", + "iam_administrator_access_with_mfa", + "apigatewayv2_api_authorizers_enabled", + "kafka_cluster_unrestricted_access_disabled", + "rds_cluster_iam_authentication_enabled", + "rds_instance_iam_authentication_enabled", + "apigateway_restapi_authorizers_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.5. 인증 및 권한관리", + "Section": "2.5.3 사용자 인증", + "AuditChecklist": [ + "정보시스템 및 개인정보처리시스템에 대한 접근은 사용자 인증, 로그인 횟수 제한, 불법 로그인 시도 경고 등 안전한 사용자 인증 절차에 따라 통제하고 있는가?", + "정보통신망을 통하여 외부에서 개인정보처리시스템에 접속하려는 경우에는 법적 요구사항에 따라 안전한 인증수단 또는 안전한 접속수단을 적용하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제5조(접근 권한의 관리), 제6조(접근통제)" + ], + "AuditEvidence": [ + "정보시스템 및 개인정보처리시스템 로그인 화면", + "로그인 횟수 제한 설정 화면", + "로그인 실패 메시지 화면", + "외부 접속 시 절차(외부접속 신청서, 외부접속자 현황 등)" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보취급자가 공개된 외부 인터넷망을 통하여 이용자의 개인정보를 처리하는 개인정보처리 시스템에 접근 시 안전한 인증수단을 적용하지 않고 ID·비밀번호 방식으로만 인증하고 있는 경우", + "사례 2 : 정보시스템 및 개인정보처리시스템 로그인 실패 시 해당 ID가 존재하지 않거나 비밀번호가 틀림을 자세히 표시해 주고 있으며, 로그인 실패횟수에 대한 제한이 없는 경우" + ] + } + ] + }, + { + "Id": "2.5.4", + "Name": "비밀번호 관리", + "Description": "법적 요구사항, 외부 위협요인 등을 고려하여 정보시스템 사용자 및 고객, 회원 등 정보주체(이용자)가 사용하는 비밀번호 관리절차를 수립·이행하여야 한다.", + "Checks": [ + "cognito_user_pool_temporary_password_expiration", + "cognito_user_pool_password_policy_uppercase", + "cognito_user_pool_password_policy_number", + "cognito_user_pool_password_policy_lowercase", + "cognito_user_pool_password_policy_minimum_length_14", + "cognito_user_pool_password_policy_symbol", + "iam_password_policy_number", + "iam_password_policy_minimum_length_14", + "iam_password_policy_expires_passwords_within_90_days_or_less", + "iam_password_policy_symbol", + "iam_password_policy_reuse_24", + "iam_password_policy_lowercase", + "iam_password_policy_uppercase" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.5. 인증 및 권한관리", + "Section": "2.5.4 비밀번호 관리", + "AuditChecklist": [ + "정보시스템에 대한 안전한 사용자 비밀번호 관리절차 및 작성규칙을 수립·이행하고 있는가?", + "정보주체(이용자)가 안전한 비밀번호를 이용할 수 있도록 비밀번호 작성규칙을 수립 및 이행하고 있는가?", + "개인정보취급자 또는 정보주체의 인증수단을 안전하게 적용하고 관리하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제5조(접근 권한의 관리)" + ], + "AuditEvidence": [ + "웹페이지, 정보시스템 및 개인정보처리시스템 비밀번호 설정 화면", + "비밀번호 관리 정책 및 절차" + ], + "NonComplianceCases": [ + "사례 1 : 정보보호 및 개인정보보호 관련 정책, 지침 등에서 비밀번호 생성규칙의 기준을 정하고 있으나, 일부 정보시스템 및 개인정보처리시스템에서 내부 지침과 상이한 비밀번호를 사용하고 있는 경우", + "사례 2 : 비밀번호 관련 내부 규정에는 비밀번호를 초기화 시 임시 비밀번호를 부여받고 강제적으로 변경하도록 되어 있으나, 실제로는 임시 비밀번호를 그대로 사용하고 있는 경우", + "사례 3 : 비밀번호 관련 내부 규정에는 사용자 및 개인정보취급자의 비밀번호 변경주기를 정하고 이행하도록 하고 있음에도 불구하고 변경하지 않고 그대로 사용하고 있는 경우" + ] + } + ] + }, + { + "Id": "2.5.5", + "Name": "특수 계정 및 권한 관리", + "Description": "정보시스템 관리, 개인정보 및 중요정보 관리 등 특수 목적을 위하여 사용하는 계정 및 권한은 최소한으로 부여하고 별도로 식별하여 통제하여야 한다.", + "Checks": [ + "organizations_delegated_administrators", + "cloudwatch_log_metric_filter_root_usage", + "iam_root_hardware_mfa_enabled", + "iam_root_mfa_enabled", + "iam_support_role_created", + "iam_avoid_root_usage", + "iam_no_root_access_key", + "rds_instance_default_admin", + "rds_cluster_default_admin", + "sagemaker_notebook_instance_root_access_disabled", + "ec2_instance_profile_attached" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.5. 인증 및 권한관리", + "Section": "2.5.5 특수 계정 및 권한 관리", + "AuditChecklist": [ + "관리자 권한 등 특수권한은 최소한의 인원에게만 부여될 수 있도록 공식적인 권한 신청 및 승인 절차를 수립·이행하고 있는가?", + "특수 목적을 위하여 부여한 계정 및 권한을 식별하고 별도 목록으로 관리하는 등 통제절차를 수립·이행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제5조(접근 권한의 관리)" + ], + "AuditEvidence": [ + "특수권한 관련 지침", + "특수권한 신청·승인 내역", + "특수권한자 목록", + "특수권한 검토 내용" + ], + "NonComplianceCases": [ + "사례 1 : 정보시스템 및 개인정보처리시스템의 관리자 및 특수권한 부여 등의 승인 이력이 시스템이나 문서상으로 확인이 되지 않거나, 승인 이력과 특수권한 내역이 서로 일치되지 않는 경우", + "사례 2 : 내부 규정에는 개인정보 관리자 및 특수권한 보유자를 목록으로 작성·관리하도록 되어 있으나 이를 작성·관리하고 있지 않거나, 보안시스템 관리자 등 일부 특수권한이 식별·관리되지 않는 경우", + "사례 3 : 정보시스템 및 개인정보처리시스템의 유지보수를 위하여 분기 1회에 방문하는 유지보수용 특수 계정이 사용기간 제한없이 상시로 활성화되어 있는 경우", + "사례 4 : 관리자 및 특수권한의 사용 여부를 정기적으로 검토하지 않아 일부 특수권한자의 업무가 변경되었음에도 불구하고 기존 관리자 및 특수권한을 계속 보유하고 있는 경우" + ] + } + ] + }, + { + "Id": "2.5.6", + "Name": "접근권한 검토", + "Description": "정보시스템과 개인정보 및 중요정보에 접근하는 사용자 계정의 등록·이용·삭제 및 접근권한의 부여·변경·삭제 이력을 남기고 주기적으로 검토하여 적정성 여부를 점검하여야 한다.", + "Checks": [ + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_cross_account_sharing_disabled", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_policy_changes", + "accessanalyzer_enabled_without_findings", + "accessanalyzer_enabled", + "cloudtrail_insights_exist", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_cloudwatch_logging_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.5. 인증 및 권한관리", + "Section": "2.5.6 접근권한 검토", + "AuditChecklist": [ + "정보시스템과 개인정보 및 중요정보에 대한 사용자 계정 및 접근권한 생성·등록·부여 및 이용·변경·말소 등의 이력을 남기고 있는가?", + "정보시스템과 개인정보 및 중요정보에 대한 사용자 계정 및 접근권한의 적정성 검토 기준, 검토주체, 검토방법, 주기 등을 수립하여 정기적 검토를 이행하고 있는가?", + "접근권한 검토 결과 접근권한 과다 부여, 권한부여 절차 미준수, 권한 오·남용 등 문제점이 발견된 경우 그에 따른 조치절차를 수립·이행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제5조(접근 권한의 관리)" + ], + "AuditEvidence": [ + "접근권한 검토 기준 및 절차", + "접근권한 검토 이력", + "접근권한 검토 결과보고서 및 후속조치 내역" + ], + "NonComplianceCases": [ + "사례 1 : 접근권한 검토와 관련된 방법, 점검주기, 보고체계, 오·남용 기준 등이 관련 지침에 구체적으로 정의되어 있지 않아 접근권한 검토가 정기적으로 수행되지 않은 경우", + "사례 2 : 내부 정책, 지침 등에 장기 미사용자 계정에 대한 잠금(비활성화) 또는 삭제 조치하도록 되어 있으나, 6개월 이상 미접속한 사용자의 계정이 활성화되어 있는 경우(접근권한 검토가 충실히 수행되지 않아 해당 계정이 식별되지 않은 경우)", + "사례 3 : 접근권한 검토 시 접근권한의 과다 부여 및 오·남용 의심사례가 발견되었으나, 이에 대한 상세조사, 내부보고 등의 후속조치가 수행되지 않은 경우" + ] + } + ] + }, + { + "Id": "2.6.1", + "Name": "네트워크 접근", + "Description": "네트워크에 대한 비인가 접근을 통제하기 위하여 IP관리, 단말인증 등 관리절차를 수립 및이행하고, 업무목적 및 중요도에 따라 네트워크 분리(DMZ, 서버팜, DB존, 개발존 등)와 접근통제를 적용하여야 한다.", + "Checks": [ + "ssm_documents_set_as_public", + "s3_access_point_public_access_block", + "s3_bucket_public_write_acl", + "s3_bucket_acl_prohibited", + "s3_account_level_public_access_blocks", + "s3_bucket_level_public_access_block", + "emr_cluster_account_public_block_enabled", + "emr_cluster_master_nodes_no_public_ip", + "emr_cluster_publicly_accesible", + "documentdb_cluster_public_snapshot", + "sns_topics_not_publicly_accessible", + "cognito_identity_pool_guest_access_disabled", + "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", + "cloudfront_distributions_geo_restrictions_enabled", + "opensearch_service_domains_not_publicly_accessible", + "glacier_vaults_policy_public_access", + "lightsail_instance_public", + "lightsail_database_public", + "neptune_cluster_uses_public_subnet", + "neptune_cluster_public_snapshot", + "ecr_repositories_not_publicly_accessible", + "workspaces_vpc_2private_1public_subnets_nat", + "elb_internet_facing", + "sqs_queues_not_publicly_accessible", + "redshift_cluster_public_access", + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "awslambda_function_url_public", + "awslambda_function_url_cors_policy", + "awslambda_function_not_publicly_accessible", + "iam_user_administrator_access_policy", + "iam_group_administrator_access_policy", + "iam_user_mfa_enabled_console_access", + "networkfirewall_in_all_vpc", + "eks_cluster_network_policy_enabled", + "eks_control_plane_endpoint_access_restricted", + "eks_cluster_private_nodes_enabled", + "eks_endpoints_not_publicly_accessible", + "kafka_cluster_is_public", + "kafka_cluster_unrestricted_access_disabled", + "vpc_peering_routing_tables_with_least_privilege", + "vpc_subnet_no_public_ip_by_default", + "vpc_endpoint_services_allowed_principals_trust_boundaries", + "vpc_endpoint_connections_trust_boundaries", + "vpc_subnet_separate_private_public", + "dms_instance_no_public_access", + "elbv2_internet_facing", + "elbv2_listeners_underneath", + "elasticache_cluster_uses_public_subnet", + "appstream_fleet_maximum_session_duration", + "appstream_fleet_default_internet_access_disabled", + "appstream_fleet_session_idle_disconnect_timeout", + "appstream_fleet_session_disconnect_timeout", + "rds_snapshots_public_access", + "rds_instance_no_public_access", + "rds_instance_event_subscription_security_groups", + "rds_instance_iam_authentication_enabled", + "apigateway_restapi_public", + "apigateway_restapi_authorizers_enabled", + "apigateway_restapi_public_with_authorizer", + "sagemaker_training_jobs_network_isolation_enabled", + "sagemaker_models_vpc_settings_configured", + "sagemaker_training_jobs_vpc_settings_configured", + "sagemaker_models_network_isolation_enabled", + "sagemaker_notebook_instance_without_direct_internet_access_configured", + "sagemaker_notebook_instance_vpc_settings_configured", + "ec2_instance_port_ldap_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_instance_port_kerberos_exposed_to_internet", + "ec2_securitygroup_not_used", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", + "ec2_securitygroup_default_restrict_traffic", + "ec2_instance_port_cifs_exposed_to_internet", + "ec2_instance_port_kafka_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", + "ec2_instance_port_mysql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432", + "ec2_elastic_ip_shodan", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_instance_port_cassandra_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", + "ec2_instance_port_memcached_exposed_to_internet", + "ec2_networkacl_allow_ingress_any_port", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", + "ec2_ebs_public_snapshot", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_transitgateway_auto_accept_vpc_attachments", + "ec2_instance_port_postgresql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", + "ec2_securitygroup_with_many_ingress_egress_rules", + "ec2_ami_public", + "ec2_instance_port_oracle_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", + "ec2_instance_imdsv2_enabled", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", + "ec2_instance_port_redis_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", + "ec2_instance_port_ftp_exposed_to_internet", + "ec2_instance_public_ip", + "ec2_networkacl_allow_ingress_tcp_port_22", + "ec2_instance_port_mongodb_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_instance_port_telnet_exposed_to_internet" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.6. 접근통제", + "Section": "2.6.1 네트워크 접근", + "AuditChecklist": [ + "조직의 네트워크에 접근할 수 있는 모든 경로를 식별하고 접근통제 정책에 따라 내부 네트워크는 인가된 사용자만이 접근할 수 있도록 통제하고 있는가?", + "서비스, 사용자 그룹, 정보자산의 중요도, 법적 요구사항에 따라 네트워크 영역을 물리적 또는 논리적으로 분리하고 각 영역 간 접근통제를 적용하고 있는가?", + "네트워크 대역별 IP주소 부여 기준을 마련하고 데이터베이스 서버 등 외부 연결이 필요하지 않은 경우 사설 IP로 할당하는 등의 대책을 적용하고 있는가?", + "물리적으로 떨어진 IDC, 지사, 대리점 등과의 네트워크 연결 시 전송구간 보호대책을 마련하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제6조(접근통제)" + ], + "AuditEvidence": [ + "네트워크 구성도", + "IP 관리대장", + "정보자산 목록", + "방화벽룰" + ], + "NonComplianceCases": [ + "사례 1 : 네트워크 구성도와 인터뷰를 통하여 확인한 결과, 외부 지점에서 사용하는 정보시스템 및 개인정보 처리시스템과 IDC에 위치한 서버 간 연결 시 일반 인터넷 회선을 통하여 데이터 송수신을 처리하고 있어 내부 규정에 명시된 VPN이나 전용망 등을 이용한 통신이 이루어지고 있지 않은 경우", + "사례 2 : 내부망에 위치한 데이터베이스 서버 등 일부 중요 서버의 IP주소가 내부 규정과 달리 공인 IP로 설정되어 있고, 네트워크 접근 차단이 적용되어 있지 않은 경우", + "사례 3 : 서버팜이 구성되어 있으나, 네트워크 접근제어 설정 미흡으로 내부망에서 서버팜으로의 접근이 과도하게 허용되어 있는 경우", + "사례 4 : 외부자(외부 개발자, 방문자 등)에게 제공되는 네트워크를 별도의 통제 없이 내부 업무 네트워크와 분리하지 않은 경우", + "사례 5 : 내부 규정과는 달리 MAC주소 인증, 필수 보안 소프트웨어 설치 등의 보호대책을 적용하지 않은 상태로 네트워크 케이블 연결만으로 사내 네트워크에 접근 및 이용할 수 있는 경우" + ] + } + ] + }, + { + "Id": "2.6.2", + "Name": "정보시스템 접근", + "Description": "서버, 네트워크시스템 등 정보시스템에 접근을 허용하는 사용자, 접근제한 방식, 안전한 접근수단 등을 정의하여 통제하여야 한다.", + "Checks": [ + "lightsail_instance_public", + "lightsail_static_ip_unused", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_elastic_ip_unassigned", + "ec2_securitygroup_default_restrict_traffic", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_elastic_ip_shodan", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_instance_internet_facing_with_instance_profile", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", + "ec2_instance_managed_by_ssm", + "ec2_networkacl_allow_ingress_any_port", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", + "ec2_securitygroup_with_many_ingress_egress_rules", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_instance_port_ftp_exposed_to_internet", + "ec2_instance_public_ip", + "ec2_networkacl_allow_ingress_tcp_port_22", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_instance_port_telnet_exposed_to_internet" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.6. 접근통제", + "Section": "2.6.2 정보시스템 접근", + "AuditChecklist": [ + "서버, 네트워크시스템, 보안시스템 등 정보시스템별 운영체제(OS)에 접근이 허용되는 사용자, 접근 가능 위치, 접근 수단 등을 정의하여 통제하고 있는가?", + "정보시스템에 접속 후 일정시간 업무처리를 하지 않는 경우 자동으로 시스템 접속이 차단되도록 하고 있는가?", + "정보시스템의 사용목적과 관계 없는 서비스를 제거하고 있는가?", + "주요 서비스를 제공하는 정보시스템은 독립된 서버로 운영하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제6조(접근통제)" + ], + "AuditEvidence": [ + "정보시스템 운영체제 계정 목록", + "서버 보안 설정", + "서버접근제어 정책(SecureOS 관리화면 등)", + "서버 및 네트워크 구성도", + "정보자산 목록" + ], + "NonComplianceCases": [ + "사례 1 : 사무실에서 서버관리자가 IDC에 위치한 윈도우 서버에 접근 시 터미널 서비스를 이용하여 접근하고 있으나, 터미널 서비스에 대한 세션 타임아웃 설정이 되어 있지 않아 장시간 아무런 작업을 하지 않아도 해당 세션이 차단되지 않는 경우", + "사례 2 : 서버 간 접속이 적절히 제한되지 않아 특정 사용자가 본인에게 인가된 서버에 접속한 후 해당 서버를 경유하여 다른 인가받지 않은 서버에도 접속할 수 있는 경우", + "사례 3 : 타당한 사유 또는 보완 대책 없이 안전하지 않은 접속 프로토콜(telnet, ftp 등)을 사용하여 접근하고 있으며, 불필요한 서비스 및 포트를 오픈하고 있는 경우", + "사례 4 : 모든 서버로의 접근은 서버접근제어 시스템을 통하도록 접근통제 정책을 가져가고 있으나, 서버접근제어 시스템을 통하지 않고 서버에 접근할 수 있는 우회 경로가 존재하는 경우" + ] + } + ] + }, + { + "Id": "2.6.3", + "Name": "응용프로그램 접근", + "Description": "사용자별 업무 및 접근 정보의 중요도 등에 따라 응용프로그램 접근권한을 제한하고, 불필요한 정보 또는 중요정보 노출을 최소화할 수 있도록 기준을 수립하여 적용하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.6. 접근통제", + "Section": "2.6.3 응용프로그램 접근", + "AuditChecklist": [ + "중요정보 접근을 통제하기 위하여 사용자의 업무에 따라 응용프로그램 접근권한을 차등 부여하고 있는가?", + "일정시간 동안 입력이 없는 세션은 자동 차단하고, 동일 사용자의 동시 세션 수를 제한하고 있는가?", + "관리자 전용 응용프로그램(관리자 웹페이지, 관리콘솔 등)은 비인가자가 접근할 수 없도록 접근을 통제하고 있는가?", + "개인정보 및 중요정보의 표시제한 보호조치의 일관성을 확보할 수 있도록 관련 기준을 수립하여 적용하고 있는가?", + "개인정보 및 중요정보의 불필요한 노출(조회, 화면표시, 인쇄, 다운로드 등)을 최소화할 수 있도록 응용프로그램을 구현하여 운영하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제5조(접근권한의 관리), 제6조(접근통제), 제12조(출력·복사시 안전조치)" + ], + "AuditEvidence": [ + "응용프로그램 접근권한 분류 체계", + "응용프로그램 계정·권한 관리 화면", + "응용프로그램 사용자·관리자 화면(개인정보 조회 등)", + "응용프로그램 세션 타임 및 동시접속 허용 여부 내역", + "응용프로그램 관리자 접속로그 모니터링 내역", + "정보자산 목록", + "개인정보처리시스템의 개인정보 조회, 검색 화면", + "개인정보 마스킹 표준", + "개인정보 마스킹 적용 화면" + ], + "NonComplianceCases": [ + "사례 1 : 응용프로그램의 개인정보 처리화면 중 일부 화면의 권한 제어 기능에 오류가 존재하여 개인정보 열람 권한이 없는 사용자에게도 개인정보가 노출되고 있는 경우", + "사례 2 : 응용프로그램의 관리자 페이지가 외부인터넷에 오픈되어 있으면서 안전한 인증수단이 적용되어 있지 않은 경우", + "사례 3 : 응용프로그램에 대하여 타당한 사유 없이 세션 타임아웃 또는 동일 사용자 계정의 동시 접속을 제한하고 있지 않은 경우", + "사례 4 : 응용프로그램을 통하여 개인정보를 다운로드받는 경우 해당 파일 내에 주민등록번호 등 업무상 불필요한 정보가 과도하게 포함되어 있는 경우", + "사례 5 : 응용프로그램의 개인정보 조회화면에서 like 검색을 과도하게 허용하고 있어, 모든 사용자가 본인의 업무 범위를 초과하여 성씨만으로도 전체 고객 정보를 조회할 수 있는 경우", + "사례 6 : 개인정보 표시제한 조치 기준이 마련되어 있지 않거나 이를 준수하지 않는 등의 사유로 동일한 개인정보 항목에 대하여 개인정보처리시스템 화면별로 서로 다른 마스킹 기준이 적용된 경우", + "사례 7 : 개인정보처리시스템의 화면상에는 개인정보가 마스킹되어 표시되어 있으나, 웹브라우저 소스보기를 통하여 마스킹되지 않은 전체 개인정보가 노출되는 경우" + ] + } + ] + }, + { + "Id": "2.6.4", + "Name": "데이터베이스 접근", + "Description": "테이블 목록 등 데이터베이스 내에서 저장·관리되고 있는 정보를 식별하고, 정보의 중요도와 응용프로그램 및 사용자 유형 등에 따른 접근통제 정책을 수립·이행하여야 한다.", + "Checks": [ + "documentdb_cluster_public_snapshot", + "opensearch_service_domains_internal_user_database_enabled", + "opensearch_service_domains_use_cognito_authentication_for_kibana", + "opensearch_service_domains_https_communications_enforced", + "opensearch_service_domains_not_publicly_accessible", + "accessanalyzer_enabled_without_findings", + "accessanalyzer_enabled", + "lightsail_database_public", + "neptune_cluster_iam_authentication_enabled", + "neptune_cluster_uses_public_subnet", + "neptune_cluster_public_snapshot", + "dynamodb_table_cross_account_access", + "redshift_cluster_public_access", + "vpc_subnet_separate_private_public", + "dms_instance_no_public_access", + "rds_cluster_iam_authentication_enabled", + "rds_snapshots_public_access", + "rds_instance_no_public_access", + "rds_instance_iam_authentication_enabled", + "rds_instance_transport_encrypted", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", + "ec2_instance_port_mysql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_instance_port_cassandra_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_instance_port_oracle_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", + "ec2_instance_port_redis_exposed_to_internet", + "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_instance_port_mongodb_exposed_to_internet" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.6. 접근통제", + "Section": "2.6.4 데이터베이스 접근", + "AuditChecklist": [ + "데이터베이스의 테이블 목록 등 저장·관리되고 있는 정보를 식별하고 있는가?", + "데이터베이스 내 정보에 접근이 필요한 응용프로그램, 정보시스템(서버) 및 사용자를 명확히 식별하고 접근통제 정책에 따라 통제하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제5조(접근권한의 관리), 제6조(접근통제)" + ], + "AuditEvidence": [ + "데이터베이스 현황(테이블, 컬럼 등)", + "데이터베이스 접속자 계정·권한 목록", + "데이터베이스 접근제어 정책(데이터베이스 접근제어시스템 관리화면 등)", + "네트워크 구성도(데이터베이스존 등)", + "정보자산 목록" + ], + "NonComplianceCases": [ + "사례 1 : 대량의 개인정보를 보관·처리하고 있는 데이터베이스를 인터넷을 통하여 접근 가능한 웹 응용프로그램과 분리하지 않고 물리적으로 동일한 서버에서 운영하고 있는 경우", + "사례 2 : 개발자 및 운영자들이 응응 프로그램에서 사용하고 있는 계정을 공유하여 운영 데이터베이스에 접속하고 있는 경우", + "사례 3 : 내부 규정에는 데이터베이스의 접속권한을 오브젝트별로 제한하도록 되어 있으나, 데이터베이스 접근권한을 운영자에게 일괄 부여하고 있어 개인정보 테이블에 접근할 필요가 없는 운영자에게도 과도하게 접근 권한이 부여된 경우", + "사례 4 : 데이터베이스 접근제어 솔루션을 도입하여 운영하고 있으나, 데이터베이스 접속자에 대한 IP주소 등이 적절히 제한되어 있지 않아 데이터베이스 접근제어 솔루션을 우회하여 데이터베이스에 접속하고 있는 경우", + "사례 5 : 개인정보를 저장하고 있는 데이터베이스의 테이블 현황이 파악되지 않아, 임시로 생성된 테이블에 불필요한 개인정보가 파기되지 않고 대량으로 저장되어 있는 경우" + ] + } + ] + }, + { + "Id": "2.6.5", + "Name": "무선 네트워크 접근", + "Description": "무선 네트워크를 사용하는 경우 사용자 인증, 송수신 데이터 암호화, AP 통제 등 무선 네트워크 보호대책을 적용하여야 한다. 또한 AD Hoc 접속, 비인가 AP 사용 등 비인가 무선 네트워크 접속으로부터 보호대책을 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.6. 접근통제", + "Section": "2.6.5 무선 네트워크 접근", + "AuditChecklist": [ + "무선네트워크를 업무적으로 사용하는 경우 무선 AP 및 네트워크 구간 보안을 위하여 인증, 송수신 데이터 암호화 등 보호대책을 수립·이행하고 있는가?", + "인가된 임직원만이 무선네트워크를 사용할 수 있도록 사용 신청 및 해지 절차를 수립 및 이행하고 있는가?", + "AD Hoc 접속 및 조직 내 허가받지 않은 무선 AP 탐지·차단 등 비인가된 무선네트워크에 대한 보호대책을 수립·이행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제6조(접근통제)" + ], + "AuditEvidence": [ + "네트워크 구성도", + "AP 보안 설정 내역", + "비인가 무선 네트워크 점검 이력", + "무선네트워크 사용 신청·승인 이력" + ], + "NonComplianceCases": [ + "사례 1 : 외부인용 무선 네트워크와 내부 무선 네트워크 영역대가 동일하여 외부인도 무선네트워크를 통하여 별도의 통제 없이 내부 네트워크에 접근이 가능한 경우", + "사례 2 : 무선 AP 설정 시 정보 송수신 암호화 기능을 설정하였으나, 안전하지 않은 방식으로 설정한 경우", + "사례 3 : 업무 목적으로 내부망에 연결된 무선AP에 대하여 무선AP 관리자 비밀번호 노출(디폴트 비밀번호 사용), 접근제어 미적용 등 보안 설정이 미흡한 경우" + ] + } + ] + }, + { + "Id": "2.6.6", + "Name": "원격접근 통제", + "Description": "보호구역 이외 장소에서의 정보시스템 관리 및 개인정보 처리는 원칙적으로 금지하고, 재택근무·장애대응·원격협업 등 불가피한 사유로 원격접근을 허용하는 경우 책임자 승인, 접근 단말 지정, 접근 허용범위 및 기간 설정, 강화된 인증, 구간 암호화, 접속단말 보안(백신, 패치 등) 등 보호대책을 수립·이행하여야 한다.", + "Checks": [ + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cognito_identity_pool_guest_access_disabled", + "cognito_user_pool_mfa_enabled", + "cognito_user_pool_self_registration_disabled", + "workspaces_vpc_2private_1public_subnets_nat", + "workspaces_volume_encryption_enabled", + "iam_user_console_access_unused", + "iam_user_mfa_enabled_console_access", + "networkfirewall_in_all_vpc", + "vpc_subnet_no_public_ip_by_default", + "vpc_flow_logs_enabled", + "vpc_subnet_separate_private_public", + "appstream_fleet_maximum_session_duration", + "appstream_fleet_default_internet_access_disabled", + "appstream_fleet_session_idle_disconnect_timeout", + "appstream_fleet_session_disconnect_timeout", + "ec2_client_vpn_endpoint_connection_logging_enabled", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_networkacl_allow_ingress_any_port", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_networkacl_allow_ingress_tcp_port_22" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.6. 접근통제", + "Section": "2.6.6 원격접근 통제", + "AuditChecklist": [ + "인터넷과 같은 외부 네트워크를 통한 정보시스템 원격운영은 원칙적으로 금지하고 장애대응 등 부득이하게 허용하는 경우 보완대책을 마련하고 있는가?", + "내부 네트워크를 통하여 원격으로 정보시스템을 운영하는 경우 특정 단말에 한해서만 접근을 허용하고 있는가?", + "재택근무, 원격협업, 스마트워크 등과 같은 원격업무 수행 시 중요정보 유출, 해킹 등 침해사고 예방을 위한 보호대책을 수립·이행하고 있는가?", + "개인정보처리시스템의 관리, 운영, 개발, 보안 등을 목적으로 원격으로 개인정보처리 시스템에 접속하는 단말기는 관리용 단말기로 지정하고 임의조작 및 목적 외 사용 금지 등 안전조치를 적용하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제6조(접근통제)" + ], + "AuditEvidence": [ + "VPN 등 사외접속 신청서", + "VPN 계정 목록", + "VPN 접근제어 정책 설정 현황", + "IP 관리대장", + "원격 접근제어 설정(서버 설정, 보안시스템 설정 등)", + "관리용 단말기 지정 및 관리 현황", + "네트워크 구성도" + ], + "NonComplianceCases": [ + "사례 1 : 내부 규정에는 시스템에 대한 원격 접근은 원칙적으로 금지하고 불가피한 경우 IP 기반의 접근통제를 통하여 승인된 사용자만 접근할 수 있도록 명시하고 있으나, 시스템에 대한 원격 데스크톱 연결, SSH 접속이 IP주소 등으로 제한되어 있지 않아 모든 PC에서 원격 접속이 가능한 경우", + "사례 2 : 원격운영관리를 위하여 VPN을 구축하여 운영하고 있으나, VPN에 대한 사용 승인 또는 접속 기간 제한 없이 상시 허용하고 있는 경우", + "사례 3 : 외부 근무자를 위하여 개인 스마트 기기에 업무용 모바일 앱을 설치하여 운영하고 있으나, 악성코드, 분실·도난 등에 의한 개인정보 유출을 방지하기 위한 적절한 보호대책(백신, 초기화, 암호화 등)을 적용하고 있지 않은 경우", + "사례 4 : 외부 접속용 VPN에서 사용자별로 원격접근이 가능한 네트워크 구간 및 정보시스템을 제한하지 않아 원격접근 인증을 받은 사용자가 전체 내부망 및 정보시스템에 과도하게 접근이 가능한 경우" + ] + } + ] + }, + { + "Id": "2.6.7", + "Name": "인터넷 접속 통제", + "Description": "인터넷을 통한 정보 유출, 악성코드 감염, 내부망 침투 등을 예방하기 위하여 주요 정보시스템, 주요 직무 수행 및 개인정보 취급 단말기 등에 대한 인터넷 접속 또는 서비스(P2P, 웹하드, 메신저 등)를 제한하는 등 인터넷 접속 통제 정책을 수립·이행하여야 한다.", + "Checks": [ + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_changes_to_network_acls_alarm_configured", + "workspaces_vpc_2private_1public_subnets_nat", + "workspaces_volume_encryption_enabled", + "networkfirewall_in_all_vpc", + "vpc_subnet_no_public_ip_by_default", + "vpc_flow_logs_enabled", + "vpc_subnet_separate_private_public", + "appstream_fleet_maximum_session_duration", + "appstream_fleet_default_internet_access_disabled", + "appstream_fleet_session_idle_disconnect_timeout", + "appstream_fleet_session_disconnect_timeout", + "route53_dangling_ip_subdomain_takeover", + "ec2_elastic_ip_unassigned", + "ec2_elastic_ip_shodan", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_instance_public_ip" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.6. 접근통제", + "Section": "2.6.7 인터넷 접속 통제", + "AuditChecklist": [ + "주요 직무 수행 및 개인정보 취급 단말기 등 업무용 PC의 인터넷 접속에 대한 통제정책을 수립·이행하고 있는가?", + "주요 정보시스템(DB서버 등)에서 불필요한 외부 인터넷 접속을 통제하고 있는가?", + "관련 법령에 따라 인터넷망 차단 의무가 부과된 경우 대상자를 식별하여 안전한 방식으로 인터넷망 차단 조치를 적용하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제6조(접근통제)" + ], + "AuditEvidence": [ + "비업무사이트(P2P 등) 차단정책(비업무사이트 차단시스템 관리화면 등)", + "인터넷 접속내역 모니터링 이력", + "인터넷망 차단조치 대상자 목록", + "망간 자료 전송 절차 및 처리내역(신청·승인내역 등)", + "네트워크 구성도" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보 보호법에 따라 인터넷망 차단 조치를 적용하였으나, 개인정보처리시스템의 접근권한 설정 가능자 등 일부 의무대상자에 대하여 인터넷망 차단 조치 적용이 누락된 경우", + "사례 2 : 개인정보 보호법에 따른 인터넷망 차단 조치 의무대상으로서 인터넷망 차단 조치를 적용하였으나, 다른 서버를 경유한 우회접속이 가능하여 인터넷망 차단 조치가 적용되지 않은 환경에서 개인정보처리시스템에 접속하여 개인정보의 다운로드, 파기 등이 가능한 경우", + "사례 3 : DMZ 및 내부망에 위치한 일부 서버에서 불필요하게 인터넷으로의 직접 접속이 가능한 경우", + "사례 4 : 인터넷 PC와 내부 업무용 PC를 물리적 망분리 방식으로 인터넷망 차단 조치를 적용하고 망간 자료전송시스템을 구축·운영하고 있으나, 자료 전송에 대한 승인 절차가 부재하고 자료 전송 내역에 대한 주기적 검토가 이루어지고 있지 않은 경우", + "사례 5 : 내부 규정에는 개인정보취급자가 P2P 및 웹하드 사이트 접속 시 책임자 승인을 거쳐 특정 기간 동안만 허용하도록 되어 있으나, 승인절차를 거치지 않고 예외 접속이 허용된 사례가 다수 존재하는 경우" + ] + } + ] + }, + { + "Id": "2.7.1", + "Name": "암호정책 적용", + "Description": "개인정보 및 주요정보 보호를 위하여 법적 요구사항을 반영한 암호화 대상, 암호 강도, 암호 사용 정책을 수립하고 개인정보 및 주요정보의 저장·전송·전달 시 암호화를 적용하여야 한다.", + "Checks": [ + "codebuild_project_no_secrets_in_variables", + "ssm_document_secrets", + "cloudwatch_log_group_kms_encryption_enabled", + "cloudwatch_log_group_no_secrets_in_logs", + "glue_etl_jobs_amazon_s3_encryption_enabled", + "glue_development_endpoints_s3_encryption_enabled", + "glue_data_catalogs_metadata_encryption_enabled", + "glue_data_catalogs_connection_passwords_encryption_enabled", + "glue_development_endpoints_cloudwatch_logs_encryption_enabled", + "glue_database_connections_ssl_enabled", + "glue_etl_jobs_job_bookmark_encryption_enabled", + "glue_development_endpoints_job_bookmark_encryption_enabled", + "glue_etl_jobs_cloudwatch_logs_encryption_enabled", + "s3_bucket_kms_encryption", + "s3_bucket_default_encryption", + "s3_bucket_secure_transport_policy", + "documentdb_cluster_storage_encrypted", + "sns_subscription_not_using_http_endpoints", + "sns_topics_kms_encryption_at_rest_enabled", + "cloudfront_distributions_https_enabled", + "cloudfront_distributions_field_level_encryption_enabled", + "cloudfront_distributions_using_deprecated_ssl_protocols", + "opensearch_service_domains_encryption_at_rest_enabled", + "opensearch_service_domains_https_communications_enforced", + "opensearch_service_domains_node_to_node_encryption_enabled", + "neptune_cluster_storage_encrypted", + "dynamodb_accelerator_cluster_encryption_enabled", + "dynamodb_tables_kms_cmk_encryption_enabled", + "storagegateway_fileshare_encryption_enabled", + "ecs_task_definitions_no_environment_secrets", + "directoryservice_radius_server_security_protocol", + "workspaces_volume_encryption_enabled", + "elb_insecure_ssl_ciphers", + "elb_ssl_listeners", + "sqs_queues_server_side_encryption_enabled", + "cloudtrail_kms_encryption_enabled", + "awslambda_function_no_secrets_in_code", + "awslambda_function_no_secrets_in_variables", + "eks_cluster_kms_cmk_encryption_in_secrets_enabled", + "kafka_cluster_mutual_tls_authentication_enabled", + "kafka_cluster_encryption_at_rest_uses_cmk", + "kafka_cluster_in_transit_encryption_enabled", + "autoscaling_find_secrets_ec2_launch_configuration", + "elbv2_insecure_ssl_ciphers", + "elbv2_ssl_listeners", + "backup_vaults_exist", + "backup_vaults_encrypted", + "cloudformation_stack_outputs_find_secrets", + "elasticache_redis_cluster_in_transit_encryption_enabled", + "elasticache_redis_cluster_rest_encryption_enabled", + "rds_instance_storage_encrypted", + "rds_snapshots_encrypted", + "rds_instance_transport_encrypted", + "apigateway_restapi_client_certificate_enabled", + "efs_encryption_at_rest_enabled", + "sagemaker_training_jobs_volume_and_output_encryption_enabled", + "sagemaker_training_jobs_intercontainer_encryption_enabled", + "sagemaker_notebook_instance_encryption_enabled", + "acm_certificates_with_secure_key_algorithms", + "athena_workgroup_enforce_configuration", + "athena_workgroup_encryption", + "ec2_instance_secrets_user_data", + "ec2_ebs_default_encryption", + "ec2_ebs_snapshots_encrypted", + "ec2_launch_template_no_secrets", + "ec2_ebs_volume_encryption" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.7. 암호화 적용", + "Section": "2.7.1 암호정책 적용", + "AuditChecklist": [ + "개인정보 및 주요정보의 보호를 위하여 법적 요구사항을 반영한 암호화 대상, 암호강도, 암호사용 등이 포함된 암호정책을 수립하고 있는가?", + "암호정책에 따라 개인정보 및 주요정보의 저장, 전송, 전달 시 암호화를 수행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제24조의2(주민등록번호 처리의 제한), 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제7조(개인정보의 암호화)" + ], + "AuditEvidence": [ + "암호통제 정책(대상, 방식, 알고리즘 등)", + "암호화 적용현황(저장 및 전송 시)", + "위험도 분석 결과(내부망에서 주민등록번호 이외의 고유식별정보 암호화 미적용 시)", + "암호화 솔루션 관리 화면" + ], + "NonComplianceCases": [ + "사례 1 : 내부 정책·지침에 암호통제 관련 법적 요구사항을 고려한 암호화 대상, 암호 강도, 저장 및 전송 시 암호화 방법, 암호화 관련 담당자의 역할 및 책임 등에 관한 사항이 적절히 명시되지 않은 경우", + "사례 2 : 암호정책을 수립하면서 해당 기업이 적용받는 법규를 잘못 적용하여 암호화 관련 법적 요구사항을 준수하지 못하고 있는 경우(예를 들어, 이용자의 계좌번호를 저장하면서 암호화 미적용)", + "사례 3 : 개인정보취급자 및 정보주체의 비밀번호에 대하여 일방향 암호화를 적용하였으나, 안전하지 않은 MD5 알고리즘을 사용한 경우", + "사례 4 : 개인정보처리자가 관련 법규 및 내부 규정에 따라 인터넷 쇼핑몰에 대하여 보안서버를 적용하였으나, 회원정보 조회 및 변경, 비밀번호 찾기, 비밀번호 변경 등 이용자의 개인정보가 전송되는 일부 구간에 암호화 조치가 누락된 경우", + "사례 5 : 정보시스템 접속용 비밀번호, 인증키 값 등이 시스템 설정파일 및 소스코드 내에 평문으로 저장되어 있는 경우" + ] + } + ] + }, + { + "Id": "2.7.2", + "Name": "암호키 관리", + "Description": "암호키의 안전한 생성·이용·보관·배포·파기를 위한 관리 절차를 수립·이행하고, 필요 시 복구방안을 마련하여야 한다.", + "Checks": [ + "secretsmanager_automatic_rotation_enabled", + "kms_cmk_are_used", + "kms_key_not_publicly_accessible", + "kms_cmk_rotation_enabled", + "kms_cmk_not_deleted_unintentionally", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "directoryservice_ldap_certificate_expiration", + "rds_instance_certificate_expiration", + "acm_certificates_transparency_logs_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.7. 암호화 적용", + "Section": "2.7.2 암호키 관리", + "AuditChecklist": [ + "암호키 생성, 이용, 보관, 배포, 변경, 복구, 파기 등에 관한 절차를 수립·이행하고 있는가?", + "암호키는 필요시 복구가 가능하도록 별도의 안전한 장소에 보관하고 암호키 사용에 관한 접근권한을 최소화하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제7조(개인정보의 암호화)" + ], + "AuditEvidence": [ + "암호키 관리정책", + "암호키 관리대장 및 관리시스템 화면" + ], + "NonComplianceCases": [ + "사례 1 : 암호 정책 내에 암호키 관리와 관련된 절차, 방법 등이 명시되어 있지 않아 담당자별로 암호키 관리 수준 및 방법 상이 등 암호키 관리에 취약사항이 존재하는 경우", + "사례 2 : 내부 규정에 중요 정보를 암호화 할 경우 관련 책임자 승인 하에 암호화 키를 생성하고 암호키 관리대장을 작성하도록 정하고 있으나, 암호키 관리대장에 일부 암호키가 누락되어 있거나 현행화되어 있지 않은 경우", + "사례 3 : 개발시스템에 적용되어 있는 암호키와 운영시스템에 적용된 암호키가 동일하여, 암호화된 실데이터가 개발시스템을 통해 쉽게 복호화가 가능한 경우" + ] + } + ] + }, + { + "Id": "2.8.1", + "Name": "보안 요구사항 정의", + "Description": "정보시스템의 도입·개발·변경 시 정보보호 및 개인정보보호 관련 법적 요구사항, 최신 보안취약점, 안전한 코딩방법 등 보안 요구사항을 정의하고 적용하여야 한다.", + "Checks": [ + "codebuild_project_no_secrets_in_variables", + "codebuild_project_user_controlled_buildspec", + "codebuild_project_source_repo_url_no_sensitive_credentials", + "macie_is_enabled", + "inspector2_is_enabled", + "config_recorder_all_regions_enabled", + "fms_policy_compliant", + "wafv2_webacl_logging_enabled", + "accessanalyzer_enabled", + "wellarchitected_workload_no_high_or_medium_risks", + "cloudtrail_cloudwatch_logging_enabled", + "networkfirewall_in_all_vpc", + "vpc_flow_logs_enabled", + "guardduty_is_enabled", + "guardduty_centrally_managed", + "securityhub_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.8. 정보시스템 도입 및 개발 보안", + "Section": "2.8.1 보안 요구사항 정의", + "AuditChecklist": [ + "정보시스템을 신규로 도입·개발 또는 변경하는 경우 정보보호 및 개인정보보호 측면의 타당성 검토 및 인수 절차를 수립·이행하고 있는가?", + "정보시스템을 신규로 도입·개발 또는 변경하는 경우 법적 요구사항, 최신 취약점 등을 포함한 보안 요구사항을 명확히 정의하고 설계 단계에서부터 반영하고 있는가?", + "정보시스템의 안전한 구현을 위한 코딩 표준을 수립하여 적용하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보시스템 인수 기준 및 절차", + "정보시스템 도입 RFP(제안요청서) 및 구매계약서", + "개발 산출물(사업수행계획서, 요구사항정의서, 화면설계서, 보안아키텍처 설계서, 시험계획서 등)", + "시큐어 코딩 표준" + ], + "NonComplianceCases": [ + "사례 1 : 정보시스템 인수 전 보안성 검증 기준 및 절차가 마련되어 있지 않은 경우", + "사례 2 : 신규 시스템 도입 시 기존 운영환경에 대한 영향 및 보안성을 검토하도록 내부 규정을 마련하고 있으나, 최근 도입한 일부 정보시스템에 대하여 인수 시 보안요건에 대해 세부 기준 및 계획이 수립되지 않았으며, 이에 따라 인수 시 보안성검토가 수행되지 않은 경우", + "사례 3 : 개발 관련 내부 지침에 개발과 관련된 주요 보안 요구사항(인증 및 암호화, 보안로그 등)이 정의되어 있지 않은 경우", + "사례 4 : ʻ개발표준정의서ʼ에 사용자 패스워드를 안전하지 않은 암호화 알고리즘(MD5, SHA1)으로 사용하도록 되어 있어 관련 법적 요구사항을 적절히 반영하지 않는 경우" + ] + } + ] + }, + { + "Id": "2.8.2", + "Name": "보안 요구사항 검토 및 시험", + "Description": "사전 정의된 보안 요구사항에 따라 정보시스템이 도입 또는 구현되었는지를 검토하기 위하여 법적 요구사항 준수, 최신 보안취약점 점검, 안전한 코딩 구현, 개인정보 영향평가 등의 검토 기준과 절차를 수립·이행하고, 발견된 문제점에 대한 개선조치를 수행하여야 한다.", + "Checks": [ + "codebuild_project_no_secrets_in_variables", + "codebuild_project_user_controlled_buildspec", + "codebuild_project_source_repo_url_no_sensitive_credentials", + "macie_is_enabled", + "inspector2_is_enabled", + "inspector2_active_findings_exist", + "config_recorder_all_regions_enabled", + "fms_policy_compliant", + "wafv2_webacl_logging_enabled", + "accessanalyzer_enabled_without_findings", + "accessanalyzer_enabled", + "wellarchitected_workload_no_high_or_medium_risks", + "cloudtrail_cloudwatch_logging_enabled", + "networkfirewall_in_all_vpc", + "vpc_flow_logs_enabled", + "guardduty_no_high_severity_findings", + "guardduty_is_enabled", + "guardduty_centrally_managed", + "securityhub_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.8. 정보시스템 도입 및 개발 보안", + "Section": "2.8.2 보안 요구사항 검토 및 시험", + "AuditChecklist": [ + "정보시스템의 도입, 개발, 변경 시 분석 및 설계 단계에서 정의한 보안 요구사항이 효과적으로 적용되었는지를 확인하기 위한 시험을 수행하고 있는가?", + "정보시스템이 안전한 코딩 기준 등에 따라 안전하게 개발되었는지를 확인하기 위한 취약점 점검이 수행되고 있는가?", + "시험 및 취약점 점검 과정에서 발견된 문제점이 신속하게 개선될 수 있도록 개선계획 수립, 이행점검 등의 절차를 이행하고 있는가?", + "공공기관은 관련 법령에 따라 개인정보처리시스템 신규 개발 및 변경 시 분석·설계 단계에서 영향평가기관을 통하여 영향평가를 수행하고 그 결과를 개발 및 변경 시 반영하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제33조(개인정보 영향평가)", + "개인정보 영향평가에 관한 고시" + ], + "AuditEvidence": [ + "정보시스템 인수 시험 결과", + "요구사항 추적 매트릭스", + "시험 계획서, 시험 결과서", + "취약점 점검 결과서", + "개인정보 영향평가서", + "개인정보 영향평가 개선계획 이행점검 확인서" + ], + "NonComplianceCases": [ + "사례 1 : 정보시스템 구현 이후 개발 관련 내부 지침 및 문서에 정의된 보안 요구사항을 시험하지 않고 있는 경우", + "사례 2 : 응용프로그램 테스트 시나리오 및 기술적 취약점 점검항목에 입력값 유효성 체크 등의 중요 점검항목 일부가 누락된 경우", + "사례 3 : 구현 또는 시험 과정에서 알려진 기술적 취약점이 존재하는지 여부를 점검하지 않거나, 타당한 사유 또는 승인 없이 확인된 취약점에 대한 개선조치를 이행하지 않은 경우", + "사례 4 : 공공기관이 5만 명 이상 정보주체의 고유식별정보를 처리하는 등 영향평가 의무 대상 개인정보 파일 및 개인정보처리시스템을 신규로 구축하면서 영향평가를 실시하지 않은 경우", + "사례 5 : 공공기관이 영향평가를 수행한 후 영향평가기관으로부터 영향평가서를 받은 지 2개월이 지났음에도 불구하고 영향평가서를 개인정보 보호위원회에 제출하지 않은 경우", + "사례 6 : 신규 시스템 도입 시 기존 운영환경에 대한 영향 및 보안성을 검토(취약점 점검 등)하도록 내부 지침을 마련하고 있으나, 최근 도입한 일부 정보시스템에 대하여 인수 시 취약점 점검 등 보안성검토가 수행되지 않은 경우" + ] + } + ] + }, + { + "Id": "2.8.3", + "Name": "시험과 운영 환경 분리", + "Description": "개발 및 시험 시스템은 운영시스템에 대한 비인가 접근 및 변경의 위험을 감소시키기 위하여 원칙적으로 분리하여야 한다.", + "Checks": [ + "codebuild_project_user_controlled_buildspec" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.8. 정보시스템 도입 및 개발 보안", + "Section": "2.8.3 시험과 운영 환경 분리", + "AuditChecklist": [ + "정보시스템의 개발 및 시험 시스템을 운영시스템과 분리하고 있는가?", + "불가피한 사유로 개발과 운영환경의 분리가 어려운 경우 상호검토, 상급자 모니터링, 변경 승인, 책임추적성 확보 등의 보안대책을 마련하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "네트워크 구성도(시험환경 구성 포함)", + "운영 환경과 개발·시험 환경 간 접근통제 적용 현황" + ], + "NonComplianceCases": [ + "사례 1 : 타당한 사유 또는 승인 없이 별도의 개발환경을 구성하지 않고 운영환경에서 직접 소스코드 변경을 수행하고 있는 경우", + "사례 2 : 불가피하게 개발시스템과 운영시스템을 분리하지 않고 운영 중에 있으나, 이에 대한 상호 검토 내역, 모니터링 내역 등이 누락되어 있는 경우", + "사례 3 : 개발시스템이 별도로 구성되어 있으나, 개발환경으로부터 운영환경으로의 접근이 통제되지 않아 개발자들이 개발시스템을 경유하여 불필요하게 운영시스템 접근이 가능한 경우" + ] + } + ] + }, + { + "Id": "2.8.4", + "Name": "시험 데이터 보안", + "Description": "시스템 시험 과정에서 운영데이터의 유출을 예방하기 위하여 시험 데이터의 생성과 이용 및 관리, 파기, 기술적 보호조치에 관한 절차를 수립·이행하여야 한다.", + "Checks": [ + "codebuild_project_no_secrets_in_variables" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.8. 정보시스템 도입 및 개발 보안", + "Section": "2.8.4 시험 데이터 보안", + "AuditChecklist": [ + "정보시스템의 개발 및 시험 과정에서 실제 운영 데이터의 사용을 제한하고 있는가?", + "불가피하게 운영데이터를 시험 환경에서 사용할 경우 책임자 승인, 접근 및 유출 모니터링, 시험 후 데이터 삭제 등의 통제 절차를 수립·이행하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "시험데이터 현황", + "시험데이터 생성 규칙", + "운영데이터를 시험환경에 사용한 경우, 관련 승인 이력" + ], + "NonComplianceCases": [ + "사례 1 : 개발 서버에서 사용할 시험 데이터 생성에 대한 구체적 기준 및 절차가 수립되어 있지 않은 경우", + "사례 2 : 타당한 사유 및 책임자 승인 없이 실 운영데이터를 가공하지 않고 시험 데이터로 사용하고 있는 경우", + "사례 3 : 불가피한 사유로 사전 승인을 받아 실 운영데이터를 시험 용도로 사용하면서, 테스트 데이터베이스에 대하여 운영 데이터베이스와 동일한 수준의 접근통제를 적용하고 있지 않은 경우", + "사례 4 : 실 운영데이터를 테스트 용도로 사용한 후 테스트가 완료되었음에도 실 운영데이터를 테스트 데이터베이스에서 삭제하지 않은 경우" + ] + } + ] + }, + { + "Id": "2.8.5", + "Name": "소스 프로그램 관리", + "Description": "소스 프로그램은 인가된 사용자만이 접근할 수 있도록 관리하고, 운영환경에 보관하지 않는 것을 원칙으로 하여야 한다.", + "Checks": [ + "ecr_repositories_not_publicly_accessible", + "codeartifact_packages_external_public_publishing_disabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.8. 정보시스템 도입 및 개발 보안", + "Section": "2.8.5 소스 프로그램 관리", + "AuditChecklist": [ + "비인가자에 의한 소스 프로그램 접근을 통제하기 위한 절차를 수립·이행하고 있는가?", + "소스 프로그램은 장애 등 비상시를 대비하여 운영환경이 아닌 곳에 안전하게 보관하고 있는가?", + "소스 프로그램에 대한 변경이력을 관리하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "SVN 등 형상관리시스템 운영 현황(접근권한자 목록 등)", + "소스 프로그램 변경 이력" + ], + "NonComplianceCases": [ + "사례 1 : 별도의 소스 프로그램 백업 및 형상관리시스템이 구축되어 있지 않으며, 이전 버전의 소스 코드를 운영 서버 또는 개발자 PC에 승인 및 이력관리 없이 보관하고 있는 경우", + "사례 2 : 형상관리시스템을 구축하여 운영하고 있으나 형상관리시스템 또는 형상관리시스템에 저장된 소스코드에 대한 접근제한, 접근 및 변경이력이 적절히 관리되지 않고 있는 경우", + "사례 3 : 내부 규정에는 형상관리시스템을 통하여 소스 프로그램 버전관리를 하도록 되어 있으나, 최신 버전의 소스 프로그램은 개발자 PC에만 보관되어 있고 이에 대한 별도의 백업이 수행되고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.8.6", + "Name": "운영환경 이관", + "Description": "신규 도입·개발 또는 변경된 시스템을 운영환경으로 이관할 때는 통제된 절차를 따라야 하고, 실행코드는 시험 및 사용자 인수 절차에 따라 실행되어야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.8. 정보시스템 도입 및 개발 보안", + "Section": "2.8.6 운영환경 이관", + "AuditChecklist": [ + "신규 도입·개발 및 변경된 시스템을 운영환경으로 안전하게 이관하기 위한 통제 절차를 수립·이행하고 있는가?", + "운영환경으로 이관 시 발생할 수 있는 문제에 대한 대응 방안을 마련하고 있는가?", + "운영환경에는 서비스 실행에 필요한 파일만을 설치하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "이관 절차", + "이관 내역(신청·승인, 시험, 이관 등)" + ], + "NonComplianceCases": [ + "사례 1 : 개발·변경이 완료된 소스 프로그램을 운영환경으로 이관 시 검토·승인하는 절차가 마련되어 있지 않은 경우", + "사례 2 : 운영서버에 서비스 실행에 불필요한 파일(소스코드 또는 배포모듈, 백업본, 개발 관련 문서, 매뉴얼 등)이 존재하는 경우", + "사례 3 : 내부 지침에 운영환경 이관 시 안전한 이관·복구를 위하여 변경작업 요청서 및 결과서를 작성하도록 정하고 있으나, 관련 문서가 확인되지 않은 경우", + "사례 4 : 내부 지침에는 모바일 앱을 앱마켓에 배포하는 경우 내부 검토 및 승인을 받도록 하고 있으나, 개발자가 해당 절차를 거치지 않고 임의로 앱마켓에 배포하고 있는 경우" + ] + } + ] + }, + { + "Id": "2.9.1", + "Name": "변경관리", + "Description": "정보시스템 관련 자산의 모든 변경내역을 관리할 수 있도록 절차를 수립·이행하고, 변경 전 시스템의 성능 및 보안에 미치는 영향을 분석하여야 한다.", + "Checks": [ + "codebuild_project_older_90_days", + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_log_metric_filter_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", + "config_recorder_all_regions_enabled", + "cloudtrail_cloudwatch_logging_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.9. 시스템 및 서비스 운영관리", + "Section": "2.9.1 변경관리", + "AuditChecklist": [ + "정보시스템 관련 자산(하드웨어, 운영체제, 상용 소프트웨어 패키지 등) 변경에 관한 절차를 수립·이행하고 있는가?", + "정보시스템 관련 자산 변경을 수행하기 전 성능 및 보안에 미치는 영향을 분석하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "변경관리 절차", + "변경관리 수행 내역(신청·승인, 변경 내역 등)", + "변경에 따른 영향분석 결과" + ], + "NonComplianceCases": [ + "사례 1 : 최근 DMZ 구간 이중화에 따른 변경 작업을 수행하였으나, 변경 후 발생할 수 있는 보안위험성 및 성능 평가에 대한 수행·승인 증거자료가 확인되지 않은 경우", + "사례 2 : 최근 네트워크 변경 작업을 수행하였으나 관련 검토 및 공지가 충분히 이루어지지 않아 네트워크 구성도 및 일부 접근통제시스템(침입차단시스템, 데이터베이스 접근제어시스템 등)의 접근통제 리스트(ACL)에 적절히 반영되어 있지 않은 경우", + "사례 3 : 변경관리시스템을 구축하여 정보시스템 입고 또는 변경 시 성능 및 보안에 미치는 영향을 분석 및협의하고 관련 이력을 관리하도록 하고 있으나, 해당 시스템을 통하지 않고도 시스템 변경이 가능하며, 관련 변경사항이 적절히 검토되지 않는 경우" + ] + } + ] + }, + { + "Id": "2.9.2", + "Name": "성능 및 장애관리", + "Description": "정보시스템의 가용성 보장을 위하여 성능 및 용량 요구사항을 정의하고 현황을 지속적으로 모니터링하여야 하며, 장애 발생 시 효과적으로 대응하기 위한 탐지·기록·분석·복구·보고 등의 절차를 수립·관리하여야 한다.", + "Checks": [ + "kms_cmk_not_deleted_unintentionally", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "s3_bucket_no_mfa_delete", + "s3_bucket_cross_region_replication", + "documentdb_cluster_cloudwatch_log_export", + "documentdb_cluster_deletion_protection", + "config_recorder_all_regions_enabled", + "cognito_user_pool_deletion_protection_enabled", + "neptune_cluster_multi_az", + "neptune_cluster_deletion_protection", + "directoryservice_directory_monitor_notifications", + "directoryservice_ldap_certificate_expiration", + "elb_is_in_multiple_az", + "elb_cross_zone_load_balancing_enabled", + "cloudtrail_bucket_requires_mfa_delete", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_multi_region_enabled", + "iam_no_expired_server_certificates_stored", + "networkfirewall_deletion_protection", + "kafka_cluster_enhanced_monitoring_enabled", + "vpc_subnet_different_az", + "trustedadvisor_premium_support_plan_subscribed", + "trustedadvisor_errors_and_warnings", + "dms_instance_multi_az_enabled", + "autoscaling_group_multiple_az", + "elbv2_is_in_multiple_az", + "elbv2_deletion_protection", + "cloudformation_stacks_termination_protection_enabled", + "elasticache_redis_cluster_multi_az_enabled", + "rds_instance_multi_az", + "rds_cluster_backtrack_enabled", + "rds_instance_enhanced_monitoring_enabled", + "rds_instance_deletion_protection", + "rds_instance_certificate_expiration", + "rds_cluster_deletion_protection", + "rds_cluster_multi_az", + "acm_certificates_expiration_check", + "route53_domains_transferlock_enabled", + "ec2_instance_detailed_monitoring_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.9. 시스템 및 서비스 운영관리", + "Section": "2.9.2 성능 및 장애관리", + "AuditChecklist": [ + "정보시스템의 가용성 보장을 위하여 성능 및 용량을 지속적으로 모니터링할 수 있는 절차를 수립·이행하고 있는가?", + "정보시스템 성능 및 용량 요구사항(임계치)을 초과하는 경우에 대한 대응절차를 수립 및 이행하고 있는가?", + "정보시스템 장애를 즉시 인지하고 대응하기 위한 절차를 수립·이행하고 있는가?", + "장애 발생 시 절차에 따라 조치하고 장애조치보고서 등을 통하여 장애조치내역을 기록하여 관리하고 있는가?", + "심각도가 높은 장애의 경우 원인분석을 통한 재발방지 대책을 마련하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "성능 및 용량 모니터링 절차", + "성능 및 용량 모니터링 증거자료(내부보고 결과 등)", + "장애대응 절차", + "장애조치보고서" + ], + "NonComplianceCases": [ + "사례 1 : 성능 및 용량 관리를 위한 대상별 요구사항(임계치 등)을 정의하고 있지 않거나 정기 점검보고서 등에 기록하고 있지 않아 현황을 파악할 수 없는 경우", + "사례 2 : 성능 또는 용량 기준을 초과하였으나 관련 검토 및 후속조치방안 수립·이행이 이루어지고 있지 않은 경우", + "사례 3 : 전산장비 장애대응절차를 수립하고 있으나 네트워크 구성 및 외주업체 변경 등의 내·외부 환경변화가 적절히 반영되어 있지 않은 경우", + "사례 4 : 장애처리절차와 장애유형별 조치방법 간 일관성이 없거나 예상소요시간 산정에 대한 근거가 부족하여 신속·정확하고 체계적인 대응이 어려운 경우" + ] + } + ] + }, + { + "Id": "2.9.3", + "Name": "백업 및 복구관리", + "Description": "정보시스템의 가용성과 데이터 무결성을 유지하기 위하여 백업 대상, 주기, 방법, 보관장소, 보관기간, 소산 등의 절차를 수립·이행하여야 한다. 아울러 사고 발생 시 적시에 복구할 수 있도록 관리하여야 한다.", + "Checks": [ + "cloudwatch_log_group_retention_policy_specific_days_enabled", + "s3_access_point_public_access_block", + "s3_bucket_lifecycle_enabled", + "s3_bucket_object_versioning", + "s3_bucket_kms_encryption", + "s3_bucket_public_write_acl", + "s3_bucket_policy_public_write_access", + "s3_bucket_public_access", + "s3_bucket_public_list_acl", + "s3_bucket_cross_region_replication", + "s3_bucket_default_encryption", + "documentdb_cluster_backup_enabled", + "documentdb_cluster_public_snapshot", + "lightsail_instance_automated_snapshots", + "neptune_cluster_backup_enabled", + "neptune_cluster_public_snapshot", + "ecr_repositories_lifecycle_policy_enabled", + "dynamodb_tables_pitr_enabled", + "directoryservice_directory_snapshots_limit", + "redshift_cluster_automated_snapshot", + "dlm_ebs_snapshot_lifecycle_policy_exists", + "backup_reportplans_exist", + "backup_vaults_exist", + "backup_vaults_encrypted", + "backup_plans_exist", + "elasticache_redis_cluster_backup_enabled", + "rds_cluster_copy_tags_to_snapshots", + "rds_instance_copy_tags_to_snapshots", + "rds_cluster_backtrack_enabled", + "rds_snapshots_public_access", + "rds_instance_backup_enabled", + "efs_have_backup_enabled", + "ec2_ebs_snapshot_account_block_public_access", + "ec2_ebs_public_snapshot", + "ec2_ebs_volume_snapshots_exists", + "ec2_ebs_snapshots_encrypted", + "ec2_ami_public" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.9. 시스템 및 서비스 운영관리", + "Section": "2.9.3 백업 및 복구관리", + "AuditChecklist": [ + "백업 대상, 주기, 방법, 절차 등이 포함된 백업 및 복구절차를 수립·이행하고 있는가?", + "백업된 정보의 완전성과 정확성, 복구절차의 적절성을 확인하기 위하여 정기적으로 복구 테스트를 실시하고 있는가?", + "중요정보가 저장된 백업매체의 경우 재해·재난에 대처할 수 있도록 백업매체를 물리적으로 떨어진 장소에 소산하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치 의무)", + "개인정보의 안전성 확보조치 기준 제11조(재해·재난 대비 안전조치)" + ], + "AuditEvidence": [ + "백업 및 복구 절차", + "복구테스트 결과", + "소산백업 현황" + ], + "NonComplianceCases": [ + "사례 1 : 백업 대상, 주기, 방법, 절차 등이 포함된 백업 및 복구 절차가 수립되어 있지 않은 경우", + "사례 2 : 백업정책을 수립하고 있으나 법적 요구사항에 따라 장기간(6개월, 3년, 5년 등) 보관이 필요한 백업 대상 정보가 백업 정책에 따라 보관되고 있지 않은 경우", + "사례 3 : 상위 지침 또는 내부 지침에 따라 별도로 백업하여 관리하도록 명시된 일부 시스템(보안시스템 정책 및 로그 등)에 대한 백업이 이행되고 있지 않은 경우", + "사례 4 : 상위 지침 또는 내부 지침에는 주기적으로 백업매체에 대한 복구 테스트를 수행하도록 정하고 있으나 복구테스트를 장기간 실시하지 않은 경우" + ] + } + ] + }, + { + "Id": "2.9.4", + "Name": "로그 및 접속기록 관리", + "Description": "서버, 응용프로그램, 보안시스템, 네트워크시스템 등 정보시스템에 대한 사용자 접속기록, 시스템로그, 권한부여 내역 등의 로그유형, 보존기간, 보존방법 등을 정하고 위·변조, 도난, 분실되지 않도록 안전하게 보존·관리하여야 한다.", + "Checks": [ + "eventbridge_bus_exposed", + "eventbridge_bus_cross_account_access", + "eventbridge_schema_registry_cross_account_access", + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_group_kms_encryption_enabled", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_cross_account_sharing_disabled", + "cloudwatch_log_group_retention_policy_specific_days_enabled", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_log_group_no_secrets_in_logs", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", + "glue_development_endpoints_cloudwatch_logs_encryption_enabled", + "glue_etl_jobs_cloudwatch_logs_encryption_enabled", + "s3_access_point_public_access_block", + "s3_bucket_server_access_logging_enabled", + "s3_bucket_kms_encryption", + "s3_bucket_public_write_acl", + "s3_bucket_policy_public_write_access", + "s3_bucket_public_access", + "s3_bucket_public_list_acl", + "s3_bucket_default_encryption", + "s3_account_level_public_access_blocks", + "s3_bucket_level_public_access_block", + "documentdb_cluster_cloudwatch_log_export", + "macie_is_enabled", + "inspector2_is_enabled", + "config_recorder_all_regions_enabled", + "cloudfront_distributions_logging_enabled", + "wafv2_webacl_logging_enabled", + "opensearch_service_domains_audit_logging_enabled", + "opensearch_service_domains_cloudwatch_logging_enabled", + "directoryservice_directory_log_forwarding_enabled", + "directoryservice_directory_monitor_notifications", + "elb_logging_enabled", + "redshift_cluster_audit_logging", + "cloudtrail_insights_exist", + "cloudtrail_logs_s3_bucket_access_logging_enabled", + "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_s3_dataevents_read_enabled", + "cloudtrail_log_file_validation_enabled", + "cloudtrail_s3_dataevents_write_enabled", + "cloudtrail_cloudwatch_logging_enabled", + "cloudtrail_multi_region_enabled", + "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", + "iam_inline_policy_no_full_access_to_cloudtrail", + "iam_securityaudit_role_created", + "eks_control_plane_logging_all_types_enabled", + "apigatewayv2_api_access_logging_enabled", + "kafka_cluster_enhanced_monitoring_enabled", + "vpc_flow_logs_enabled", + "guardduty_is_enabled", + "guardduty_centrally_managed", + "trustedadvisor_premium_support_plan_subscribed", + "elbv2_logging_enabled", + "securityhub_enabled", + "rds_instance_enhanced_monitoring_enabled", + "rds_snapshots_public_access", + "rds_snapshots_encrypted", + "rds_instance_integration_cloudwatch_logs", + "apigateway_restapi_logging_enabled", + "efs_not_publicly_accessible", + "acm_certificates_transparency_logs_enabled", + "route53_public_hosted_zones_cloudwatch_logging_enabled", + "ec2_instance_detailed_monitoring_enabled", + "ec2_ebs_snapshot_account_block_public_access", + "ec2_instance_managed_by_ssm", + "ec2_ebs_public_snapshot", + "ec2_ebs_snapshots_encrypted", + "ec2_client_vpn_endpoint_connection_logging_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.9. 시스템 및 서비스 운영관리", + "Section": "2.9.4 로그 및 접속기록 관리", + "AuditChecklist": [ + "서버, 응용프로그램, 보안시스템, 네트워크시스템 등 정보시스템에 대한 로그관리 절차를 수립하고 이에 따라 필요한 로그를 생성하여 보관하고 있는가?", + "정보시스템의 로그기록은 위·변조 및 도난, 분실되지 않도록 안전하게 보관하고 로그기록에 대한 접근권한은 최소화하여 부여하고 있는가?", + "개인정보처리시스템에 대한 접속기록은 법적 요구사항을 준수할 수 있도록 필요한 항목을 모두 포함하여 일정기간 안전하게 보관하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제8조(접속기록의 보관 및 점검)" + ], + "AuditEvidence": [ + "로그관리 절차", + "로그기록 내역", + "로그 저장장치에 대한 접근통제 내역", + "개인정보 접속기록 내역" + ], + "NonComplianceCases": [ + "사례 1 : 로그 기록 대상, 방법, 보존기간, 검토 주기, 담당자 등에 대한 세부 기준 및 절차가 수립되어 있지 않은 경우", + "사례 2 : 보안 이벤트 로그, 응용프로그램 및 서비스 로그(윈도우 2008 서버 이상) 등 중요 로그에 대한 최대 크기를 충분하게 설정하지 않아 내부 기준에 정한 기간 동안 기록·보관되고 있지 않은 경우", + "사례 3 : 중요 Linux/UNIX 계열 서버에 대한 로그 기록을 별도로 백업하거나 적절히 보호하지 않아 사용자의 명령 실행 기록 및 접속 이력 등을 임의로 삭제할 수 있는 경우", + "사례 4 : 개인정보처리시스템에 접속한 기록을 확인한 결과 접속자의 계정, 접속 일시, 접속자 IP주소 정보는 남기고 있으나, 처리한 정보주체 정보 및 수행업무(조회, 변경, 삭제, 다운로드 등)와 관련된 정보를 남기고 있지 않은 경우", + "사례 5 : 로그 서버의 용량의 충분하지 않아서 개인정보처리시스템 접속기록이 2개월 밖에 남아 있지 않은 경우", + "사례 6 : 개인정보처리자가 정보주체 10만 명의 개인정보를 처리하는 개인정보처리시스템의 개인정보취급자 접속기록을 1년간만 보관하고 있는 경우" + ] + } + ] + }, + { + "Id": "2.9.5", + "Name": "로그 및 접속기록 점검", + "Description": "정보시스템의 정상적인 사용을 보장하고 사용자 오·남용(비인가접속, 과다조회 등)을 방지하기 위하여 접근 및 사용에 대한 로그 검토기준을 수립하여 주기적으로 점검하며, 문제 발생 시 사후조치를 적시에 수행하여야 한다.", + "Checks": [ + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_group_kms_encryption_enabled", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_cross_account_sharing_disabled", + "cloudwatch_log_group_retention_policy_specific_days_enabled", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_log_group_no_secrets_in_logs", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", + "inspector2_active_findings_exist", + "cognito_user_pool_client_prevent_user_existence_errors", + "accessanalyzer_enabled_without_findings", + "cloudtrail_insights_exist", + "cloudtrail_threat_detection_enumeration", + "cloudtrail_threat_detection_privilege_escalation", + "guardduty_no_high_severity_findings", + "trustedadvisor_errors_and_warnings" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.9. 시스템 및 서비스 운영관리", + "Section": "2.9.5 로그 및 접속기록 점검", + "AuditChecklist": [ + "정보시스템 관련 오류, 오·남용(비인가접속, 과다조회 등), 부정행위 등 이상징후를 인지할 수 있도록 로그 검토 주기, 대상, 방법 등을 포함한 로그 검토 및 모니터링 절차를 수립·이행하고 있는가?", + "로그 검토 및 모니터링 결과를 책임자에게 보고하고 이상징후 발견 시 절차에 따라 대응하고 있는가?", + "개인정보처리시스템의 접속기록은 관련 법령에서 정한 주기에 따라 정기적으로 점검하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제8조(접속기록의 보관 및 점검)" + ], + "AuditEvidence": [ + "로그 검토 및 모니터링 절차", + "로그 검토 및 모니터링 결과(검토 내역, 보고서 등)", + "개인정보 접속기록 점검 내역", + "개인정보 다운로드 시 사유 확인 기준 및 결과", + "이상징후 발견 시 대응 증거자료" + ], + "NonComplianceCases": [ + "사례 1 : 중요 정보를 처리하고 있는 정보시스템에 대한 이상접속(휴일 새벽 접속, 우회경로 접속 등) 또는 이상행위(대량 데이터 조회 또는 소량 데이터의 지속적·연속적 조회 등)에 대한 모니터링 및 경고·알림 정책(기준)이 수립되어 있지 않은 경우", + "사례 2 : 내부 지침 또는 시스템 등에 접근 및 사용에 대한 주기적인 점검·모니터링 기준을 마련하고 있으나 실제 이상접속 및 이상행위에 대한 검토 내역이 확인되지 않은 경우", + "사례 3 : 개인정보처리자가 개인정보처리시스템의 접속기록 점검 주기를 분기 1회로 정하고 있는 경우", + "사례 4 : 개인정보처리자의 내부 관리계획에는 1,000명 이상의 정보주체에 대한 개인정보를 다운로드한 경우에는 사유를 확인하도록 기준이 책정되어 있는 상태에서 1,000건 이상의 개인정보 다운로드가 발생하였으나 그 사유를 확인하지 않고 있는 경우" + ] + } + ] + }, + { + "Id": "2.9.6", + "Name": "시간 동기화", + "Description": "로그 및 접속기록의 정확성을 보장하고 신뢰성 있는 로그분석을 위하여 관련 정보시스템의 시각을 표준시각으로 동기화하고 주기적으로 관리하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.9. 시스템 및 서비스 운영관리", + "Section": "2.9.6 시간 동기화", + "AuditChecklist": [ + "정보시스템의 시간을 표준시간으로 동기화하고 있는가?", + "시간 동기화가 정상적으로 이루어지고 있는지 주기적으로 점검하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "시간 동기화 설정", + "주요 시스템 시간 동기화 증거자료" + ], + "NonComplianceCases": [ + "사례 1 : 일부 중요 시스템(보안시스템, CCTV 등)의 시각이 표준시와 동기화되어 있지 않으며, 관련 동기화 여부에 대한 주기적 점검이 이행되고 있지 않은 경우", + "사례 2 : 내부 NTP 서버와 시각을 동기화하도록 설정하고 있으나 일부 시스템의 시각이 동기화되지 않고 있고, 이에 대한 원인분석 및 대응이 이루어지고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.9.7", + "Name": "정보자산의 재사용 및 폐기", + "Description": "정보자산의 재사용과 폐기 과정에서 개인정보 및 중요정보가 복구·재생되지 않도록 안전한 재사용 및 폐기 절차를 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.9. 시스템 및 서비스 운영관리", + "Section": "2.9.7 정보자산의 재사용 및 폐기", + "AuditChecklist": [ + "정보자산의 안전한 재사용 및 폐기에 대한 절차를 수립·이행하고 있는가?", + "정보자산 및 저장매체를 재사용 및 폐기하는 경우 개인정보 및 중요정보를 복구되지 않는 방법으로 처리하고 있는가?", + "자체적으로 정보자산 및 저장매체를 폐기할 경우 관리대장을 통하여 폐기이력을 남기고 폐기확인 증적을 함께 보관하고 있는가?", + "외부업체를 통하여 정보자산 및 저장매체를 폐기할 경우 폐기 절차를 계약서에 명시하고 완전히 폐기하였는지 여부를 확인하고 있는가?", + "정보시스템, PC 등 유지보수, 수리 과정에서 저장매체 교체, 복구 등 발생 시 저장매체 내 정보를 보호하기 위한 대책을 마련하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제21조(개인정보의 파기)", + "개인정보의 안전성 확보조치 기준 제13조(개인정보의 파기)" + ], + "AuditEvidence": [ + "정보자산 폐기 및 재사용 절차", + "저장매체 관리대장", + "정보자산 및 저장매체 폐기 증거자료", + "정보자산 및 저장매체 폐기 관련 위탁계약서" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보취급자 PC를 재사용할 경우 데이터 삭제프로그램을 이용하여 완전삭제 하도록 정책 및 절차가 수립되어 있으나, 실제로는 완전삭제 조치 없이 재사용하거나 기본 포맷만 하고 재사용하고 있는 등 관련 절차가 이행되고 있지 않은 경우", + "사례 2 : 외부업체를 통하여 저장매체를 폐기하고 있으나, 계약 내용상 안전한 폐기 절차 및 보호대책에 대한 내용이 누락되어 있고 폐기 이행 증거자료 확인 및 실사 등의 관리·감독이 이루어지지 않은 경우", + "사례 3 : 폐기된 HDD의 일련번호가 아닌 시스템명을 기록하거나 폐기 대장을 작성하지 않아 폐기 이력 및 추적할 수 있는 증거자료를 확인할 수 없는 경우", + "사례 4 : 회수한 폐기 대상 하드디스크가 완전삭제 되지 않은 상태로 잠금장치가 되지 않은 장소에 방치되고 있는 경우" + ] + } + ] + }, + { + "Id": "2.10.1", + "Name": "보안시스템 운영", + "Description": "보안시스템 유형별로 관리자 지정, 최신 정책 업데이트, 룰셋 변경, 이벤트 모니터링 등의 운영절차를 수립·이행하고 보안시스템별 정책적용 현황을 관리하여야 한다.", + "Checks": [ + "organizations_delegated_administrators", + "shield_advanced_protection_in_classic_load_balancers", + "shield_advanced_protection_in_route53_hosted_zones", + "shield_advanced_protection_in_cloudfront_distributions", + "shield_advanced_protection_in_global_accelerators", + "shield_advanced_protection_in_associated_elastic_ips", + "shield_advanced_protection_in_internet_facing_load_balancers", + "ssm_managed_compliant_patching", + "secretsmanager_automatic_rotation_enabled", + "kms_cmk_are_used", + "kms_key_not_publicly_accessible", + "kms_cmk_rotation_enabled", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "macie_is_enabled", + "inspector2_is_enabled", + "inspector2_active_findings_exist", + "config_recorder_all_regions_enabled", + "cognito_user_pool_waf_acl_attached", + "fms_policy_compliant", + "ssmincidents_enabled_with_plans", + "cloudfront_distributions_using_waf", + "wafv2_webacl_logging_enabled", + "guardduty_no_high_severity_findings", + "guardduty_is_enabled", + "guardduty_centrally_managed", + "trustedadvisor_errors_and_warnings", + "elbv2_waf_acl_attached", + "securityhub_enabled", + "apigateway_restapi_waf_acl_attached", + "ec2_instance_port_ldap_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_instance_port_kerberos_exposed_to_internet", + "ec2_securitygroup_from_launch_wizard", + "ec2_securitygroup_not_used", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", + "ec2_securitygroup_default_restrict_traffic", + "ec2_instance_port_cifs_exposed_to_internet", + "ec2_instance_port_kafka_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", + "ec2_instance_port_mysql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_instance_internet_facing_with_instance_profile", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_instance_port_cassandra_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", + "ec2_instance_port_memcached_exposed_to_internet", + "ec2_networkacl_allow_ingress_any_port", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_instance_port_postgresql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", + "ec2_securitygroup_with_many_ingress_egress_rules", + "ec2_instance_port_oracle_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", + "ec2_instance_port_redis_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", + "ec2_instance_port_ftp_exposed_to_internet", + "ec2_networkacl_allow_ingress_tcp_port_22", + "ec2_instance_port_mongodb_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_instance_port_telnet_exposed_to_internet" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.10. 시스템 및 서비스 보안관리", + "Section": "2.10.1 보안시스템 운영", + "AuditChecklist": [ + "조직에서 운영하고 있는 보안시스템에 대한 운영절차를 수립·이행하고 있는가?", + "보안시스템 관리자 등 접근이 허용된 인원을 최소화하고 비인가자의 접근을 엄격하게 통제하고 있는가?", + "보안시스템별로 정책의 신규 등록, 변경, 삭제 등을 위한 공식적인 절차를 수립 및 이행하고 있는가?", + "보안시스템의 예외 정책 등록에 대하여 절차에 따라 관리하고 있으며, 예외 정책 사용자에 대하여 최소한의 권한으로 관리하고 있는가?", + "보안시스템에 설정된 정책의 타당성 여부를 주기적으로 검토하고 있는가?", + "개인정보처리시스템에 대한 불법적인 접근 및 개인정보 유출 방지를 위하여 관련 법령에서 정한 기능을 수행하는 보안시스템을 설치하여 운영하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제6조(접근통제)" + ], + "AuditEvidence": [ + "보안시스템 구성", + "네트워크 구성", + "보안시스템 운영절차", + "방화벽 정책", + "방화벽 정책 설정·변경 요청서", + "보안시스템 예외자 목록", + "보안시스템별 관리 화면(방화벽, IPS, 서버접근제어, DLP, DRM 등)", + "보안시스템 정책 검토 이력" + ], + "NonComplianceCases": [ + "사례 1 : 침입차단시스템 보안정책에 대한 정기 검토가 수행되지 않아 불필요하거나 과도하게 허용된 정책이 다수 존재하는 경우", + "사례 2 : 보안시스템 보안정책의 신청, 변경, 삭제, 주기적 검토에 대한 절차 및 기준이 없거나, 절차는 있으나 이를 준수하지 않은 경우", + "사례 3 : 보안시스템의 관리자 지정 및 권한 부여 현황에 대한 관리감독이 적절히 이행되고 있지 않은 경우", + "사례 4 : 내부 지침에는 정보보호담당자가 보안시스템의 보안정책 변경 이력을 기록·보관하도록 정하고 있으나, 정책관리대장을 주기적으로 작성하지 않고 있거나 정책관리대장에 기록된 보안정책과 실제 운영 중인 시스템의 보안정책이 상이한 경우" + ] + } + ] + }, + { + "Id": "2.10.2", + "Name": "클라우드 보안", + "Description": "클라우드 서비스 이용 시 서비스 유형(SaaS, PaaS, IaaS 등)에 따른 비인가 접근, 설정 오류 등에 따라 중요정보와 개인정보가 유·노출되지 않도록 관리자 접근 및 보안 설정 등에 대한 보호대책을 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.10. 시스템 및 서비스 보안관리", + "Section": "2.10.2 클라우드 보안", + "AuditChecklist": [ + "클라우드 서비스 제공자와 정보보호 및 개인정보보호에 대한 책임과 역할을 명확히 정의하고 이를 계약서(SLA 등)에 반영하고 있는가?", + "클라우드 서비스 이용 시 서비스 유형에 따른 보안위험을 평가하여 비인가 접근, 설정오류 등을 방지할 수 있도록 보안 구성 및 설정 기준, 보안설정 변경 및 승인 절차, 안전한 접속방법, 권한 체계 등 보안 통제 정책을 수립·이행하고 있는가?", + "클라우드 서비스 관리자 권한은 역할에 따라 최소화하여 부여하고 관리자 권한에 대한 비인가 접근, 권한 오·남용 등을 방지할 수 있도록 강화된 인증, 암호화, 접근통제, 감사기록 등 보호대책을 적용하고 있는가?", + "클라우드 서비스의 보안 설정 변경, 운영 현황 등을 모니터링하고 그 적절성을 정기적으로 검토하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "클라우드 서비스 관련 계약서 및 SLA", + "클라우드 서비스 위험분석 결과", + "클라우드 서비스 보안통제 정책", + "클라우드 서비스 관리자 권한 부여 현황", + "클라우드 서비스 구성도", + "클라우드 서비스 보안설정 현황", + "클라우드 서비스 보안설정 적정성 검토 이력" + ], + "NonComplianceCases": [ + "사례 1 : 클라우드 서비스 계약서 내에 보안에 대한 책임 및 역할 등에 대한 사항이 포함되어 있지 않은 경우", + "사례 2 : 클라우드 서비스의 보안설정을 변경할 수 있는 권한이 업무상 반드시 필요하지 않은 직원들에게 과도하게 부여되어 있는 경우", + "사례 3 : 내부 지침에는 클라우드 내 사설 네트워크의 접근통제 룰(Rule) 변경 시 보안책임자 승인을 받도록 하고 있으나, 승인절차를 거치지 않고 등록·변경된 접근제어 룰이 다수 발견된 경우", + "사례 4 : 클라우드 서비스의 보안설정 오류로 내부 로그 파일이 인터넷을 통하여 공개되어 있는 경우" + ] + } + ] + }, + { + "Id": "2.10.3", + "Name": "공개서버 보안", + "Description": "외부 네트워크에 공개되는 서버의 경우 내부 네트워크와 분리하고 취약점 점검, 접근통제, 인증, 정보 수집·저장·공개 절차 등 강화된 보호대책을 수립·이행하여야 한다.", + "Checks": [ + "ssm_document_secrets", + "ssm_managed_compliant_patching", + "inspector2_active_findings_exist", + "cloudfront_distributions_https_enabled", + "cloudfront_distributions_using_deprecated_ssl_protocols", + "cloudfront_distributions_using_waf", + "lightsail_instance_public", + "lightsail_database_public", + "lightsail_static_ip_unused", + "elb_insecure_ssl_ciphers", + "elb_ssl_listeners", + "awslambda_function_no_secrets_in_code", + "awslambda_function_no_secrets_in_variables", + "awslambda_function_url_public", + "awslambda_function_url_cors_policy", + "awslambda_function_inside_vpc", + "awslambda_function_not_publicly_accessible", + "networkfirewall_in_all_vpc", + "apigatewayv2_api_authorizers_enabled", + "kafka_cluster_mutual_tls_authentication_enabled", + "vpc_subnet_separate_private_public", + "autoscaling_find_secrets_ec2_launch_configuration", + "elbv2_waf_acl_attached", + "elbv2_desync_mitigation_mode", + "elbv2_insecure_ssl_ciphers", + "elbv2_ssl_listeners", + "apigateway_restapi_public", + "apigateway_restapi_waf_acl_attached", + "apigateway_restapi_client_certificate_enabled", + "apigateway_restapi_authorizers_enabled", + "apigateway_restapi_public_with_authorizer", + "acm_certificates_expiration_check", + "route53_domains_privacy_protection_enabled", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_instance_port_kerberos_exposed_to_internet", + "ec2_instance_secrets_user_data", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", + "ec2_securitygroup_default_restrict_traffic", + "ec2_instance_port_cifs_exposed_to_internet", + "ec2_instance_port_kafka_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", + "ec2_instance_port_mysql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_postgres_5432", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_instance_internet_facing_with_instance_profile", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_redis_6379", + "ec2_securitygroup_allow_wide_open_public_ipv4", + "ec2_instance_port_cassandra_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_sql_server_1433_1434", + "ec2_instance_port_memcached_exposed_to_internet", + "ec2_networkacl_allow_ingress_any_port", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", + "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_instance_port_postgresql_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", + "ec2_securitygroup_with_many_ingress_egress_rules", + "ec2_instance_port_oracle_exposed_to_internet", + "ec2_launch_template_no_secrets", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", + "ec2_instance_imdsv2_enabled", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", + "ec2_instance_port_redis_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", + "ec2_instance_port_ftp_exposed_to_internet", + "ec2_networkacl_allow_ingress_tcp_port_22", + "ec2_instance_port_mongodb_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_instance_port_telnet_exposed_to_internet" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.10. 시스템 및 서비스 보안관리", + "Section": "2.10.3 공개서버 보안", + "AuditChecklist": [ + "공개서버를 운영하는 경우 이에 대한 보호대책을 수립·이행하고 있는가?", + "공개서버는 내부 네트워크와 분리된 DMZ 영역에 설치하고 침입차단시스템 등 보안시스템을 통하여 보호하고 있는가?", + "공개서버에 개인정보 및 중요정보를 게시하거나 저장하여야 할 경우 책임자 승인 등 허가 및 게시절차를 수립·이행하고 있는가?", + "조직의 중요정보가 웹사이트 및 웹서버를 통하여 노출되고 있는지 여부를 주기적으로 확인하여 중요정보 노출을 인지한 경우 이를 즉시 차단하는 등의 조치를 취하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "네트워크 구성도", + "웹사이트 정보공개 절차 및 내역(신청·승인·게시 이력 등)", + "개인정보 및 중요정보 노출 여부 점검 이력" + ], + "NonComplianceCases": [ + "사례 1 : 인터넷에 공개된 웹사이트의 취약점으로 인하여 구글 검색을 통하여 열람 권한이 없는 타인의 개인정보에 접근할 수 있는 경우", + "사례 2 : 웹사이트에 개인정보를 게시하는 경우 승인 절차를 거치도록 내부 규정이 마련되어 있으나, 이를 준수하지 않고 개인정보가 게시된 사례가 다수 존재한 경우", + "사례 3 : 게시판 등의 웹 응용프로그램에서 타인이 작성한 글을 임의로 수정·삭제하거나 비밀번호로 보호된 글을 열람할 수 있는 경우" + ] + } + ] + }, + { + "Id": "2.10.4", + "Name": "전자거래 및 핀테크 보안", + "Description": "전자거래 및 핀테크 서비스 제공 시 정보유출이나 데이터 조작·사기 등의 침해사고 예방을 위하여 인증·암호화 등의 보호대책을 수립하고, 결제시스템 등 외부 시스템과 연계할 경우 안전성을 점검하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.10. 시스템 및 서비스 보안관리", + "Section": "2.10.4 전자거래 및 핀테크 보안", + "AuditChecklist": [ + "전자거래 및 핀테크 서비스를 제공하는 경우 거래의 안전성과 신뢰성 확보를 위한 보호대책을 수립·이행하고 있는가?", + "전자거래 및 핀테크 서비스 제공을 위하여 결제시스템 등 외부 시스템과 연계하는 경우 송수신되는 관련 정보의 보호를 위한 대책을 수립·이행하고 안전성을 점검하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "전자거래 및 핀테크 서비스 보호대책", + "결제시스템 연계 시 보안성 검토 결과" + ], + "NonComplianceCases": [ + "사례 1 : 전자결제대행업체와 위탁 계약을 맺고 연계를 하였으나, 적절한 인증 및 접근제한 없이 특정 URL을 통하여 결제 관련 정보가 모두 평문으로 전송되는 경우", + "사례 2 : 전자결제대행업체와 외부 연계 시스템이 전용망으로 연결되어 있으나, 해당 연계 시스템에서 내부 업무 시스템으로의 접근이 침입차단시스템 등으로 적절히 통제되지 않고 있는 경우", + "사례 3 : 내부 지침에는 외부 핀테크 서비스 연계 시 정보보호팀의 보안성 검토를 받도록 되어 있으나, 최근에 신규 핀테크 서비스를 연계하면서 일정상 이유로 보안성 검토를 수행하지 않은 경우" + ] + } + ] + }, + { + "Id": "2.10.5", + "Name": "정보전송 보안", + "Description": "다른 조직에 개인정보 및 중요정보를 전송할 경우 안전한 전송 정책을 수립하고 조직 간 합의를 통하여 관리 책임, 전송방법, 개인정보 및 중요정보 보호를 위한 기술적 보호조치 등을 협약하고 이행하여야 한다.", + "Checks": [ + "glue_database_connections_ssl_enabled", + "s3_bucket_secure_transport_policy", + "sns_subscription_not_using_http_endpoints", + "cloudfront_distributions_https_enabled", + "cloudfront_distributions_using_deprecated_ssl_protocols", + "opensearch_service_domains_https_communications_enforced", + "opensearch_service_domains_node_to_node_encryption_enabled", + "directoryservice_radius_server_security_protocol", + "elb_insecure_ssl_ciphers", + "elb_ssl_listeners", + "kafka_cluster_mutual_tls_authentication_enabled", + "kafka_cluster_in_transit_encryption_enabled", + "elbv2_insecure_ssl_ciphers", + "elbv2_ssl_listeners", + "elasticache_redis_cluster_in_transit_encryption_enabled", + "rds_instance_transport_encrypted", + "apigateway_restapi_client_certificate_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.10. 시스템 및 서비스 보안관리", + "Section": "2.10.5 정보전송 보안", + "AuditChecklist": [ + "외부 조직에 개인정보 및 중요정보를 전송할 경우 안전한 전송 정책을 수립하고 있는가?", + "업무상 조직 간 개인정보 및 중요정보를 상호교환하는 경우 안전한 전송을 위한 협약체결 등 보호대책을 수립·이행하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "정보전송 협약서 또는 계약서", + "정보전송 기술표준", + "정보전송 관련 구성도, 인터페이스 정의서" + ], + "NonComplianceCases": [ + "사례 1 : 대외 기관과 연계 시 전용망 또는 VPN을 적용하고 중계서버와 인증서 적용 등을 통하여 안전하게 정보를 전송하고 있으나, 외부 기관별 연계 시기, 방식, 담당자 및 책임자, 연계 정보, 법적 근거 등에 대한 현황관리가 적절히 이루어지지 않고 있는 경우", + "사례 2 : 중계과정에서의 암호 해제 구간 또는 취약한 암호화 알고리즘(DES, 3DES) 사용 등에 대한 보안성 검토, 보안표준 및 조치방안 수립 등에 대한 협의가 이행되고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.10.6", + "Name": "업무용 단말기기 보안", + "Description": "PC, 모바일 기기 등 단말기기를 업무 목적으로 네트워크에 연결할 경우 기기 인증 및 승인, 접근 범위, 기기 보안설정 등의 접근통제 대책을 수립하고 주기적으로 점검하여야 한다.", + "Checks": [ + "workspaces_vpc_2private_1public_subnets_nat", + "workspaces_volume_encryption_enabled", + "appstream_fleet_maximum_session_duration", + "appstream_fleet_default_internet_access_disabled", + "appstream_fleet_session_idle_disconnect_timeout", + "appstream_fleet_session_disconnect_timeout" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.10. 시스템 및 서비스 보안관리", + "Section": "2.10.6 업무용 단말기기 보안", + "AuditChecklist": [ + "PC, 노트북, 가상PC, 태블릿 등 업무에 사용되는 단말기에 대하여 기기인증, 승인, 접근범위 설정, 기기 보안설정 등의 보안 통제 정책을 수립·이행하고 있는가?", + "업무용 단말기를 통하여 개인정보 및 중요정보가 유출되는 것을 방지하기 위하여 자료공유프로그램 사용 금지, 공유설정 제한, 무선망 이용 통제 등의 정책을 수립 및 이행하고 있는가?", + "업무용 모바일 기기의 분실, 도난 등으로 인한 개인정보 및 중요정보의 유·노출을 방지하기 위하여 보안대책을 적용하고 있는가?", + "업무용 단말기기에 대한 접근통제 대책의 적절성에 대하여 주기적으로 점검하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제6조(접근통제)" + ], + "AuditEvidence": [ + "업무용 단말기 보안통제 지침 및 절차", + "업무용 단말기 등록현황", + "업무용 단말기 보안설정", + "업무용 단말기 기기인증 및 승인 이력", + "업무용 단말기 보안점검 현황" + ], + "NonComplianceCases": [ + "사례 1 : 업무적인 목적으로 노트북, 태블릿PC 등 모바일 기기를 사용하고 있으나, 업무용 모바일 기기에 대한 허용 기준, 사용 범위, 승인 절차, 인증 방법 등에 대한 정책이 수립되어 있지 않은 경우", + "사례 2 : 모바일 기기 보안관리 지침에서는 모바일 기기의 업무용 사용을 원칙적으로 금지하고 필요시 승인 절차를 통하여 제한된 기간 동안 허가된 모바일 기기만 사용하도록 정하고 있으나, 허가된 모바일 기기가 식별·관리되지 않고 승인되지 않은 모바일 기기에서도 내부 정보시스템 접속이 가능한 경우", + "사례 3 : 개인정보 처리업무에 이용되는 모바일 기기에 대하여 비밀번호 설정 등 도난·분실에 대한 보호대책이 적용되어 있지 않은 경우", + "사례 4 : 내부 규정에서는 업무용 단말기의 공유폴더 사용을 금지하고 있으나, 이에 대한 주기적인 점검이 이루어지고 있지 않아 다수의 업무용 단말기에서 과도하게 공유폴더를 설정하여 사용하고 있는 경우" + ] + } + ] + }, + { + "Id": "2.10.7", + "Name": "보조저장매체 관리", + "Description": "보조저장매체를 통하여 개인정보 또는 중요정보의 유출이 발생하거나 악성코드가 감염되지 않도록 관리 절차를 수립·이행하고, 개인정보 또는 중요정보가 포함된 보조저장 매체는 안전한 장소에 보관하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.10. 시스템 및 서비스 보안관리", + "Section": "2.10.7 보조저장매체 관리", + "AuditChecklist": [ + "외장하드, USB메모리, CD 등 보조저장매체 취급(사용), 보관, 폐기, 재사용에 대한 정책 및 절차를 수립·이행하고 있는가?", + "보조저장매체 보유현황, 사용 및 관리실태를 주기적으로 점검하고 있는가?", + "주요 정보시스템이 위치한 통제구역, 중요 제한구역 등에서 보조저장매체 사용을 제한하고 있는가?", + "보조저장매체를 통한 악성코드 감염 및 중요정보 유출 방지를 위한 대책을 마련하고 있는가?", + "개인정보 또는 중요정보가 포함된 보조저장매체를 잠금장치가 있는 안전한 장소에 보관하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제10조(물리적 안전조치)" + ], + "AuditEvidence": [ + "보조저장매체(USB, CD 등) 차단 정책", + "보조저장매체 관리대장", + "보조저장매체 실태점검 이력" + ], + "NonComplianceCases": [ + "사례 1 : 통제구역인 서버실에서의 보조저장매체 사용을 제한하는 정책을 수립하여 운영하고 있으나, 예외 승인 절차를 준수하지 않고 보조저장매체를 사용한 이력이 다수 확인되었으며, 보조저장매체 관리실태에 대한 주기적 점검이 실시되지 않아 보조저장매체 관리대장의 현행화가 미흡한 경우", + "사례 2 : 개인정보가 포함된 보조저장매체를 잠금장치가 있는 안전한 장소에 보관하지 않고 사무실 서랍 등에 방치하고 있는 경우", + "사례 3 : 보조저장매체 통제 솔루션을 도입·운영하고 있으나, 일부 사용자에 대하여 적절한 승인 절차 없이 예외처리되어 쓰기 등이 허용된 경우", + "사례 4 : 전산실에 위치한 일부 공용 PC 및 전산장비에서 일반 USB메모리에 대한 쓰기가 가능한 상황이나 매체 반입 및 사용 제한, 사용이력 기록 및 검토 등 통제가 적용되고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.10.8", + "Name": "패치관리", + "Description": "소프트웨어, 운영체제, 보안시스템 등의 취약점으로 인한 침해사고를 예방하기 위하여 최신 패치를 적용하여야 한다. 다만 서비스 영향을 검토하여 최신 패치 적용이 어려울 경우 별도의 보완대책을 마련하여 이행하여야 한다.", + "Checks": [ + "ssm_managed_compliant_patching", + "cloudfront_distributions_using_deprecated_ssl_protocols", + "opensearch_service_domains_updated_to_the_latest_service_software_version", + "redshift_cluster_automatic_upgrades", + "awslambda_function_using_supported_runtimes", + "eks_cluster_uses_a_supported_version", + "kafka_cluster_uses_latest_version", + "dms_instance_minor_version_upgrade_enabled", + "elasticache_redis_cluster_auto_minor_version_upgrades", + "rds_instance_deprecated_engine_version", + "rds_cluster_minor_version_upgrade_enabled", + "rds_instance_minor_version_upgrade_enabled", + "ec2_instance_account_imdsv2_enabled", + "ec2_instance_older_than_specific_days" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.10. 시스템 및 서비스 보안관리", + "Section": "2.10.8 패치관리", + "AuditChecklist": [ + "서버, 네트워크시스템, 보안시스템, PC 등 자산별 특성 및 중요도에 따라 운영체제(OS)와 소프트웨어의 패치관리 정책 및 절차를 수립·이행하고 있는가?", + "주요 서버, 네트워크시스템, 보안시스템 등의 경우 설치된 OS, 소프트웨어 패치 적용 현황을 주기적으로 관리하고 있는가?", + "서비스 영향도 등에 따라 취약점을 조치하기 위한 최신의 패치 적용이 어려운 경우 보완대책을 마련하고 있는가?", + "주요 서버, 네트워크시스템, 보안시스템 등의 경우 공개 인터넷 접속을 통한 패치를 제한하고 있는가?", + "패치관리시스템을 활용하는 경우 접근통제 등 충분한 보호대책을 마련하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제9조(악성프로그램 등 방지)" + ], + "AuditEvidence": [ + "패치 적용 관리 정책·절차", + "시스템별 패치 적용 현황", + "패치 적용 관련 영향도 분석 결과" + ], + "NonComplianceCases": [ + "사례 1 : 일부 시스템에서 타당한 사유나 책임자 승인 없이 OS패치가 장기간 적용되고 있지 않은 경우", + "사례 2 : 일부 시스템에 서비스 지원이 종료(EOS)된 OS버전을 사용 중이나, 이에 따른 대응계획이나 보완대책이 수립되어 있지 않은 경우", + "사례 3 : 상용 소프트웨어 및 OS에 대해서는 최신 패치가 적용되고 있으나, 오픈소스 프로그램(openssl, openssh, Apache 등)에 대해서는 최신 패치를 확인하고 적용하는 절차 및 담당자가 지정되어 있지 않아 최신 보안패치가 적용되고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.10.9", + "Name": "악성코드 통제", + "Description": "바이러스·웜·트로이목마·랜섬웨어 등의 악성코드로부터 개인정보 및 중요정보, 정보시스템 및 업무용 단말기 등을 보호하기 위하여 악성코드 예방·탐지·대응 등의 보호대책을 수립 및 이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.10. 시스템 및 서비스 보안관리", + "Section": "2.10.9 악성코드 통제", + "AuditChecklist": [ + "바이러스, 웜, 트로이목마, 랜섬웨어 등의 악성코드로부터 정보시스템 및 업무용단말기 등을 보호하기 위하여 보호대책을 수립·이행하고 있는가?", + "백신 소프트웨어 등 보안프로그램을 통하여 최신 악성코드 예방·탐지 활동을 지속적으로 수행하고 있는가?", + "백신 소프트웨어 등 보안프로그램은 최신의 상태로 유지하고 필요시 긴급 보안 업데이트를 수행하고 있는가?", + "악성코드 감염 발견 시 악성코드 확산 및 피해 최소화 등의 대응절차를 수립·이행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제9조(악성프로그램 등 방지)" + ], + "AuditEvidence": [ + "악성프로그램 대응 지침·절차·매뉴얼", + "백신프로그램 설치 현황", + "백신프로그램 설정 화면", + "악성프로그램 대응 이력(대응 보고서 등)" + ], + "NonComplianceCases": [ + "사례 1 : 일부 PC 및 서버에 백신이 설치되어 있지 않거나, 백신 엔진이 장기간 최신 버전으로 업데이트되지 않은 경우", + "사례 2 : 백신 프로그램의 환경설정(실시간 검사, 예약검사, 업데이트 설정 등)을 이용자가 임의로 변경할 수 있음에도 그에 따른 추가 보호대책이 수립되어 있지 않은 경우", + "사례 3 : 백신 중앙관리시스템에 접근통제 등 보호대책이 미비하여 중앙관리시스템을 통한 침해사고발생 가능성이 있는 경우 또는 백신 패턴에 대한 무결성 검증을 하지 않아 악의적인 사용자에 의한 악성코드 전파 가능성이 있는 경우", + "사례 4 : 일부 내부망 PC 및 서버에서 다수의 악성코드 감염이력이 확인되었으나, 감염 현황, 감염 경로 및 원인 분석, 그에 따른 조치내역 등이 확인되지 않은 경우" + ] + } + ] + }, + { + "Id": "2.11.1", + "Name": "사고 예방 및 대응체계 구축", + "Description": "침해사고 및 개인정보 유출 등을 예방하고 사고 발생 시 신속하고 효과적으로 대응할 수 있도록 내·외부 침해시도의 탐지·대응·분석 및 공유를 위한 체계와 절차를 수립하고, 관련 외부기관 및 전문가들과 협조체계를 구축하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.11. 사고 예방 및 대응", + "Section": "2.11.1 사고 예방 및 대응체계 구축", + "AuditChecklist": [ + "침해사고 및 개인정보 유출사고를 예방하고 사고 발생 시 신속하고 효과적으로 대응하기 위한 체계와 절차를 마련하고 있는가?", + "보안관제서비스 등 외부 기관을 통하여 침해사고 대응체계를 구축·운영하는 경우 침해사고 대응절차의 세부사항을 계약서에 반영하고 있는가?", + "침해사고의 모니터링, 대응 및 처리를 위하여 외부전문가, 전문업체, 전문기관 등과의 협조체계를 수립하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제34조(개인정보의 유출 등의 통지·신고)", + "정보통신망법 제48조의3(침해사고의 신고 등), 제48조의4(침해사고의 원인분석 등)" + ], + "AuditEvidence": [ + "침해사고 대응 지침·절차·매뉴얼", + "침해사고 대응 조직도 및 비상연락망", + "보안관제서비스 계약서(SLA 등)" + ], + "NonComplianceCases": [ + "사례 1 : 침해사고에 대비한 침해사고 대응 조직 및 대응 절차를 명확히 정의하고 있지 않은 경우", + "사례 2 : 내부 지침 및 절차에 침해사고 단계별(사고 전, 인지, 처리, 복구, 보고 등) 대응 절차를 수립하여 명시하고 있으나, 침해사고 발생 시 사고 유형 및 심각도에 따른 신고·통지 절차, 대응 및 복구 절차의 일부 또는 전부를 수립하고 있지 않은 경우", + "사례 3 : 침해사고 대응 조직도 및 비상연락망 등을 현행화하지 않고 있거나, 담당자별 역할과 책임이 명확히 정의되어 있지 않은 경우", + "사례 4 : 침해사고 신고·통지 및 대응 협조를 위한 대외기관 연락처에 기관명, 홈페이지, 연락처 등이 잘못 명시되어 있거나, 일부 기관 관련 정보가 누락 또는 현행화되지 않은 경우", + "사례 5 : 외부 보안관제 전문업체 등 유관기관에 침해사고 탐지 및 대응을 위탁하여 운영하고 있으나, 침해사고 대응에 대한 상호 간 관련 역할 및 책임 범위가 계약서나 SLA에 명확하게 정의되지 않은 경우", + "사례 6 : 침해사고 대응절차를 수립하였으나, 개인정보 침해 신고 기준, 시점 등이 법적 요구사항을 준수하지 못하는 경우" + ] + } + ] + }, + { + "Id": "2.11.2", + "Name": "취약점 점검 및 조치", + "Description": "정보시스템의 취약점이 노출되어 있는지를 확인하기 위하여 정기적으로 취약점 점검을 수행하고, 발견된 취약점에 대해서는 신속하게 조치하여야 한다. 또한 최신 보안취약점의 발생 여부를 지속적으로 파악하고, 정보시스템에 미치는 영향을 분석하여 조치하여야 한다.", + "Checks": [ + "codebuild_project_no_secrets_in_variables", + "codebuild_project_source_repo_url_no_sensitive_credentials", + "ssm_document_secrets", + "cloudwatch_log_group_no_secrets_in_logs", + "inspector2_is_enabled", + "inspector2_active_findings_exist", + "ecr_registry_scan_images_on_push_enabled", + "ecr_repositories_scan_images_on_push_enabled", + "ecr_repositories_scan_vulnerabilities_in_latest_image", + "ecs_task_definitions_no_environment_secrets", + "awslambda_function_no_secrets_in_code", + "awslambda_function_no_secrets_in_variables", + "autoscaling_find_secrets_ec2_launch_configuration", + "guardduty_no_high_severity_findings", + "guardduty_is_enabled", + "guardduty_centrally_managed", + "trustedadvisor_premium_support_plan_subscribed", + "trustedadvisor_errors_and_warnings", + "securityhub_enabled", + "cloudformation_stack_outputs_find_secrets", + "ec2_instance_secrets_user_data", + "ec2_launch_template_no_secrets", + "ec2_instance_imdsv2_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.11. 사고 예방 및 대응", + "Section": "2.11.2 취약점 점검 및 조치", + "AuditChecklist": [ + "정보시스템 취약점 점검 절차를 수립하고, 정기적으로 점검을 수행하고 있는가?", + "발견된 취약점에 대한 조치를 수행하고, 그 결과를 책임자에게 보고하고 있는가?", + "최신 보안취약점 발생 여부를 지속적으로 파악하고, 정보시스템에 미치는 영향을 분석하여 조치하고 있는가?", + "취약점 점검 이력을 기록관리하여 전년도에 도출된 취약점이 재발생하는 등의 문제점에 대하여 보호대책을 마련하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제4조(내부 관리계획의 수립·시행 및 점검), 제6조(접근통제)" + ], + "AuditEvidence": [ + "취약점 점검 계획서", + "취약점 점검 결과보고서(웹, 모바일 앱, 서버, 네트워크시스템, 보안시스템, DBMS 등)", + "취약점 점검 이력", + "취약점 조치계획서", + "취약점 조치완료보고서", + "모의해킹 계획서·결과보고서" + ], + "NonComplianceCases": [ + "사례 1 : 내부 규정에 연 1회 이상 주요 시스템에 대한 기술적 취약점 점검을 하도록 정하고 있으나, 주요 시스템 중 일부가 취약점 점검 대상에서 누락된 경우", + "사례 2 : 취약점 점검에서 발견된 취약점에 대한 보완조치를 이행하지 않았거나, 단기간 내에 조치할 수 없는 취약점에 대한 타당성 검토 및 승인 이력이 없는 경우" + ] + } + ] + }, + { + "Id": "2.11.3", + "Name": "이상행위 분석 및 모니터링", + "Description": "내·외부에 의한 침해시도, 개인정보유출 시도, 부정행위 등을 신속하게 탐지·대응할 수 있도록 네트워크 및 데이터 흐름 등을 수집하여 분석하며, 모니터링 및 점검 결과에 따른 사후조치는 적시에 이루어져야 한다.", + "Checks": [ + "cloudwatch_log_metric_filter_unauthorized_api_calls", + "cloudwatch_changes_to_network_route_tables_alarm_configured", + "cloudwatch_changes_to_network_gateways_alarm_configured", + "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", + "cloudwatch_log_group_kms_encryption_enabled", + "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", + "cloudwatch_cross_account_sharing_disabled", + "cloudwatch_log_group_retention_policy_specific_days_enabled", + "cloudwatch_changes_to_vpcs_alarm_configured", + "cloudwatch_log_group_no_secrets_in_logs", + "cloudwatch_log_metric_filter_aws_organizations_changes", + "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", + "cloudwatch_log_metric_filter_authentication_failures", + "cloudwatch_log_metric_filter_sign_in_without_mfa", + "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_changes_to_network_acls_alarm_configured", + "cloudwatch_log_metric_filter_root_usage", + "cloudwatch_log_metric_filter_policy_changes", + "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", + "cognito_user_pool_client_prevent_user_existence_errors", + "fms_policy_compliant", + "cloudtrail_insights_exist", + "cloudtrail_threat_detection_enumeration", + "networkfirewall_in_all_vpc", + "vpc_flow_logs_enabled", + "guardduty_no_high_severity_findings", + "trustedadvisor_errors_and_warnings", + "securityhub_enabled" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.11. 사고 예방 및 대응", + "Section": "2.11.3 이상행위 분석 및 모니터링", + "AuditChecklist": [ + "내·외부에 의한 침해시도, 개인정보유출 시도, 부정행위 등 이상행위를 탐지할 수 있도록 주요 정보시스템, 응용프로그램, 네트워크, 보안시스템 등에서 발생한 네트워크 트래픽, 데이터 흐름, 이벤트 로그 등을 수집하여 분석 및 모니터링하고 있는가?", + "침해시도, 개인정보유출시도, 부정행위 등의 여부를 판단하기 위한 기준 및 임계치를 정의하고 이에 따라 이상행위의 판단 및 조사 등 후속 조치가 적시에 이루어지고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제6조(접근통제)" + ], + "AuditEvidence": [ + "이상행위 분석 및 모니터링 현황", + "이상행위 발견 시 대응 증거자료" + ], + "NonComplianceCases": [ + "사례 1 : 외부로부터의 서버, 네트워크, 데이터베이스, 보안시스템에 대한 침해 시도를 인지할 수 있도록 하는 상시 또는 정기적 모니터링 체계 및 절차를 마련하고 있지 않은 경우", + "사례 2 : 외부 보안관제 전문업체 등 외부 기관에 침해시도 모니터링 업무를 위탁하고 있으나, 위탁업체가 제공한 관련 보고서를 검토한 이력이 확인되지 않거나, 위탁 대상에서 제외된 시스템에 대한 자체 모니터링 체계를 갖추고 있지 않은 경우", + "사례 3 : 내부적으로 정의한 임계치를 초과하는 이상 트래픽이 지속적으로 발견되고 있으나, 이에 대한 대응조치가 이루어지고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "2.11.4", + "Name": "사고 대응 훈련 및 개선", + "Description": "침해사고 및 개인정보 유출사고 대응 절차를 임직원과 이해관계자가 숙지하도록 시나리오에 따른 모의훈련을 연 1회 이상 실시하고 훈련결과를 반영하여 대응체계를 개선하여야 한다.", + "Checks": [ + "ssmincidents_enabled_with_plans" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.11. 사고 예방 및 대응", + "Section": "2.11.4 사고 대응 훈련 및 개선", + "AuditChecklist": [ + "침해사고 및 개인정보 유출사고 대응 절차에 관한 모의훈련계획을 수립하고 이에 따라 연 1회 이상 주기적으로 훈련을 실시하고 있는가?", + "침해사고 및 개인정보 유출사고 훈련 결과를 반영하여 침해사고 및 개인정보 유출사고 대응체계를 개선하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "침해사고 및 개인정보 유출사고 대응 모의훈련 계획서", + "침해사고 및 개인정보 유출사고 대응 모의훈련 결과서", + "침해사고 대응 절차" + ], + "NonComplianceCases": [ + "사례 1 : 침해사고 모의훈련을 수행하지 않았거나 관련 계획서 및 결과보고서가 확인되지 않은 경우", + "사례 2 : 연간 침해사고 모의훈련 계획을 수립하였으나 타당한 사유 또는 승인 없이 해당 기간 내에 실시하지 않은 경우", + "사례 3 : 모의훈련을 계획하여 실시하였으나, 관련 내부 지침에 정한 절차 및 서식에 따라 수행하지 않은 경우" + ] + } + ] + }, + { + "Id": "2.11.5", + "Name": "사고 대응 및 복구", + "Description": "침해사고 및 개인정보 유출 징후나 발생을 인지한 때에는 법적 통지 및 신고 의무를 준수하여야 하며, 절차에 따라 신속하게 대응 및 복구하고 사고분석 후 재발방지 대책을 수립하여 대응체계에 반영하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.11. 사고 예방 및 대응", + "Section": "2.11.5 사고 대응 및 복구", + "AuditChecklist": [ + "침해사고 및 개인정보 유출의 징후 또는 발생을 인지한 경우 정의된 침해사고 대응 절차에 따라 신속하게 대응 및 보고가 이루어지고 있는가?", + "개인정보 침해사고 발생 시 관련 법령에 따라 정보주체 통지 및 관계기관 신고 절차를 이행하고 있는가?", + "침해사고가 종결된 후 사고의 원인을 분석하여 그 결과를 보고하고 관련 조직 및 인력과 공유하고 있는가?", + "침해사고 분석을 통하여 얻은 정보를 활용하여 유사 사고가 재발하지 않도록 대책을 수립하고 필요한 경우 침해사고 대응절차 등을 변경하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제34조(개인정보의 유출 등의 통지·신고)", + "정보통신망법 제48조의3(침해사고의 신고 등), 제48조의4(침해사고의 원인분석 등)" + ], + "AuditEvidence": [ + "침해사고 대응 절차", + "침해사고 대응보고서", + "침해사고 관리대장", + "개인정보 유출신고서", + "비상연락망" + ], + "NonComplianceCases": [ + "사례 1 : 내부 침해사고 대응지침에는 침해사고 발생 시 내부 정보보호위원회 및 이해관계 부서에게 보고하도록 정하고 있으나, 침해사고 발생 시 담당 부서에서 자체적으로 대응 조치 후 정보보호위원회 및 이해관계 부서에 보고하지 않은 경우", + "사례 2 : 최근 DDoS 공격으로 의심되는 침해사고로 인하여 서비스 일부가 중단된 사례가 있으나, 이에 대한 원인분석 및 재발방지 대책이 수립되지 않은 경우", + "사례 3 : 외부 해킹에 의해 개인정보 유출사고가 발생하였으나, 유출된 개인정보 건수가 소량이라는 이유로 72시간 이내에 통지 및 신고가 이루어지지 않은 경우", + "사례 4 : 담당자의 실수에 의해 인터넷 홈페이지 게시판을 통해 1천명 이상 정보주체에 대한 개인정보 유출이 발생하였으나, 해당 정보주체에 대한 유출 통지가 이루어지지 않은 경우" + ] + } + ] + }, + { + "Id": "2.12.1", + "Name": "재해·재난 대비 안전조치", + "Description": "자연재해, 통신·전력 장애, 해킹 등 조직의 핵심 서비스 및 시스템의 운영 연속성을 위협할 수 있는 재해 유형을 식별하고, 유형별 예상 피해규모 및 영향을 분석하여야 한다. 또한 복구 목표시간, 복구 목표시점을 정의하고 복구 전략 및 대책, 비상시 복구 조직, 비상연락체계, 복구 절차 등 재해 복구체계를 구축하여야 한다.", + "Checks": [ + "kms_cmk_not_deleted_unintentionally", + "s3_bucket_lifecycle_enabled", + "s3_bucket_object_versioning", + "s3_bucket_cross_region_replication", + "s3_bucket_object_lock", + "documentdb_cluster_backup_enabled", + "documentdb_cluster_cloudwatch_log_export", + "lightsail_instance_automated_snapshots", + "neptune_cluster_multi_az", + "neptune_cluster_deletion_protection", + "dynamodb_tables_pitr_enabled", + "elb_is_in_multiple_az", + "elb_cross_zone_load_balancing_enabled", + "redshift_cluster_automated_snapshot", + "drs_job_exist", + "dlm_ebs_snapshot_lifecycle_policy_exists", + "vpc_subnet_different_az", + "vpc_different_regions", + "dms_instance_multi_az_enabled", + "autoscaling_group_multiple_az", + "elbv2_is_in_multiple_az", + "elbv2_deletion_protection", + "backup_reportplans_exist", + "backup_plans_exist", + "elasticache_redis_cluster_backup_enabled", + "elasticache_redis_cluster_multi_az_enabled", + "rds_instance_multi_az", + "rds_instance_deletion_protection", + "rds_cluster_deletion_protection", + "rds_cluster_multi_az", + "rds_instance_backup_enabled", + "efs_have_backup_enabled", + "ec2_ebs_volume_snapshots_exists" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.12. 재해 복구", + "Section": "2.12.1 재해·재난 대비 안전조치", + "AuditChecklist": [ + "조직의 핵심 서비스(업무) 연속성을 위협할 수 있는 IT 재해 유형을 식별하고, 유형별 피해규모 및 업무에 미치는 영향을 분석하여 핵심 IT 서비스(업무) 및 시스템을 식별하고 있는가?", + "핵심 IT 서비스 및 시스템의 중요도 및 특성에 따른 복구 목표시간, 복구 목표시점을 정의하고 있는가?", + "재해·재난 발생 시에도 핵심 서비스 및 시스템의 연속성을 보장할 수 있도록 복구 전략 및 대책, 비상시 복구 조직, 비상연락체계, 복구 절차 등 재해 복구 계획을 수립 및 이행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제29조(안전조치의무)", + "개인정보의 안전성 확보조치 기준 제11조(재해·재난 대비 안전조치)" + ], + "AuditEvidence": [ + "IT 재해 복구 지침·절차", + "IT 재해 복구 계획(RTO, RPO 정의 포함)", + "비상연락망", + "개인정보처리시스템 위기대응 매뉴얼" + ], + "NonComplianceCases": [ + "사례 1 : IT 재해 복구 절차서 내에 IT 재해 복구 조직 및 역할 정의, 비상연락체계, 복구 절차 및 방법 등 중요한 내용이 누락되어 있는 경우", + "사례 2 : 비상사태 발생 시 정보시스템의 연속성 확보 및 피해 최소화를 위하여 백업센터를 구축하여 운영하고 있으나, 관련 정책에 백업센터를 활용한 재해 복구 절차 등이 수립되어 있지 않아 재해 복구 시험 및 복구가 효과적으로 진행되기 어려운 경우", + "사례 3 : 서비스 운영과 관련된 일부 중요 시스템에 대한 복구 목표시간이 정의되어 있지 않으며, 이에 대한 적절한 복구 대책을 마련하고 있지 않은 경우", + "사례 4 : 재해 복구 관련 지침서 등에 IT 서비스 또는 시스템에 대한 복구 우선순위, 복구 목표시간, 복구 목표시점 등이 정의되어 있지 않은 경우", + "사례 5 : 현실적 대책 없이 복구 목표시간을 과도 또는 과소하게 설정하고 있거나, 복구 목표시점과 백업정책(대상, 주기 등)이 적절히 연계되지 않아 복구 효과성을 보장할 수 없는 경우" + ] + } + ] + }, + { + "Id": "2.12.2", + "Name": "재해 복구 시험 및 개선", + "Description": "재해 복구 전략 및 대책의 적정성을 정기적으로 시험하여 시험결과, 정보시스템 환경변화, 법규 등에 따른 변화를 반영하여 복구전략 및 대책을 보완하여야 한다.", + "Checks": [ + "kms_cmk_not_deleted_unintentionally", + "s3_bucket_lifecycle_enabled", + "s3_bucket_object_versioning", + "s3_bucket_cross_region_replication", + "s3_bucket_object_lock", + "documentdb_cluster_backup_enabled", + "documentdb_cluster_cloudwatch_log_export", + "lightsail_instance_automated_snapshots", + "neptune_cluster_multi_az", + "neptune_cluster_deletion_protection", + "dynamodb_tables_pitr_enabled", + "elb_is_in_multiple_az", + "elb_cross_zone_load_balancing_enabled", + "redshift_cluster_automated_snapshot", + "drs_job_exist", + "dlm_ebs_snapshot_lifecycle_policy_exists", + "vpc_subnet_different_az", + "vpc_different_regions", + "dms_instance_multi_az_enabled", + "autoscaling_group_multiple_az", + "elbv2_is_in_multiple_az", + "elbv2_deletion_protection", + "backup_reportplans_exist", + "backup_plans_exist", + "elasticache_redis_cluster_backup_enabled", + "elasticache_redis_cluster_multi_az_enabled", + "rds_instance_multi_az", + "rds_instance_deletion_protection", + "rds_cluster_deletion_protection", + "rds_cluster_multi_az", + "rds_instance_backup_enabled", + "efs_have_backup_enabled", + "ec2_ebs_volume_snapshots_exists" + ], + "Attributes": [ + { + "Domain": "2. 보호대책 요구사항", + "Subdomain": "2.12. 재해 복구", + "Section": "2.12.2 재해 복구 시험 및 개선", + "AuditChecklist": [ + "수립된 IT 재해 복구체계의 실효성을 판단하기 위하여 재해 복구 시험계획을 수립 및 이행하고 있는가?", + "시험결과, 정보시스템 환경변화, 법률 등에 따른 변화를 반영할 수 있도록 복구전략 및 대책을 정기적으로 검토·보완하고 있는가?" + ], + "RelatedRegulations": [], + "AuditEvidence": [ + "IT 재해 복구 절차서", + "IT 재해 복구 시험 계획서", + "IT 재해 복구 시험 결과서" + ], + "NonComplianceCases": [ + "사례 1 : 재해 복구 훈련을 계획·시행하지 않았거나 관련 계획서 및 결과보고서가 확인되지 않은 경우", + "사례 2 : 재해 복구 훈련 계획을 수립하였으나, 타당한 사유 또는 승인 없이 계획대로 실시하지 않았거나 관련 결과보고가 확인되지 않은 경우", + "사례 3 : 재해 복구 훈련을 계획하여 실시하였으나, 내부 관련 지침에 정한 절차 및 서식에 따라 이행되지 않아 수립한 재해 복구 절차의 적정성 및 효과성을 평가하기 위한 훈련으로 보기 어려운 경우" + ] + } + ] + }, + { + "Id": "3.1.1", + "Name": "개인정보 수집·이용", + "Description": "개인정보는 적법하고 정당하게 수집·이용하여야 하며, 정보주체의 동의를 근거로 수집하는 경우에는 적법한 방법으로 정보주체의 동의를 받아야 한다. 또한 만 14세 미만 아동의 개인정보를 수집하는 경우에는 그 법정대리인의 동의를 받아야 하며 법정대리인이 동의하였는지를 확인하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.1. 개인정보 수집 시 보호조치", + "Section": "3.1.1 개인정보 수집·이용", + "AuditChecklist": [ + "개인정보를 수집하는 경우 정보주체 동의, 법령상 의무준수, 계약 체결·이행 등 적법 요건에 따라 수집하고 있는가?", + "정보주체에게 개인정보 수집 동의를 받는 경우 동의방법 및 시점은 적절하게 되어 있는가?", + "정보주체에게 개인정보 수집 동의를 받는 경우 관련 내용을 명확하게 고지하고 법령에서 정한 중요한 내용에 대해 알아보기 쉽게 표시하고 있는가?", + "만 14세 미만 아동의 개인정보에 대해 수집·이용·제공 등의 동의를 받는 경우 법정대리인에게 필요한 사항에 대하여 고지하고 동의를 받고 있는가?", + "법정대리인의 동의를 받기 위하여 필요한 최소한의 개인정보만을 수집하고 있으며, 법정대리인이 자격 요건을 갖추고 있는지 확인하는 절차와 방법을 마련하고 있는가?", + "만 14세 미만의 아동에게 개인정보 처리와 관련한 사항 등의 고지 시 이해하기 쉬운 양식과 명확하고 알기 쉬운 언어로 표현하고 있는가?", + "정보주체 및 법정대리인에게 동의를 받은 기록을 보관하고 있는가?", + "정보주체의 동의 없이 처리할 수 있는 개인정보에 대해서는 그 항목과 처리의 법적 근거를 정보주체의 동의를 받아 처리하는 개인정보와 구분하여 개인정보 처리방침에 공개하거나 정보주체에게 알리고 있는가?", + "정보주체의 동의 없이 개인정보의 추가적인 이용 시 당초 수집 목적과의 관련성, 예측 가능성, 이익 침해 여부, 안전성 확보조치 등의 고려사항에 대한 판단기준을 수립 및 이행하고, 추가적인 이용이 지속적으로 발생하는 경우 고려사항에 대한 판단기준을 개인정보 처리방침에 공개하고 이를 점검하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제15조(개인정보의 수집·이용), 제22조(동의를 받는 방법), 제22조의2(아동의 개인정보 보호)", + "개인정보 처리 방법에 관한 고시" + ], + "AuditEvidence": [ + "온라인 개인정보 수집 양식(홈페이지 회원가입 화면, 모바일앱 회원가입 화면, 이벤트 참여 등)", + "오프라인 개인정보 수집 양식(회원가입신청서 등)", + "개인정보 수집 동의 기록(회원 데이터베이스 등)", + "법정대리인 동의 기록", + "개인정보 처리방침" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보 보호법을 적용받는 개인정보처리자가 개인정보 수집 동의 시 고지 사항에 ʻ동의 거부 권리 및 동의 거부에 따른 불이익 내용ʼ을 누락한 경우", + "사례 2 : 개인정보 수집 동의 시 수집하는 개인정보 항목을 구체적으로 명시하지 않고 ʻ~ 등ʼ과 같이 포괄적으로 안내하는 경우", + "사례 3 : 쇼핑몰 홈페이지에서 회원가입 시 회원가입에 필요한 개인정보 외에 추후 물품 구매 시 필요한 결제·배송 정보를 미리 필수 항목으로 수집하는 경우", + "사례 4 : Q&A, 게시판을 통하여 비회원의 개인정보(이름, 이메일, 휴대폰번호)를 수집하면서 개인정보 수집 동의 절차를 거치지 않은 경우", + "사례 5 : 만 14세 미만 아동의 개인정보를 수집하면서 법정대리인의 동의를 받지 않은 경우", + "사례 6 : 만 14세 미만 아동에 대하여 서비스를 제공하고 있지 않지만, 회원가입 단계에서 입력받는 생년월일을 통하여 나이 체크를 하지 않아 법정대리인 동의 없이 가입된 만 14세 미만 아동 회원이 존재한 경우", + "사례 7 : 법정대리인의 진위 여부를 확인하는 절차가 미흡하여 미성년자 등 아동의 법정대리인으로 보기 어려운데도 법정대리인 동의가 가능한 경우", + "사례 8 : 만 14세 미만 아동으로부터 법정대리인 동의를 받는 목적으로 법정대리인의 개인정보(이름, 휴대폰번호)를 수집한 이후 법정대리인의 동의가 장기간 확인되지 않았음에도 이를 파기하지 않고 계속 보유하고 있는 경우", + "사례 9 : 법정대리인 동의에 근거하여 만 14세 미만 아동의 개인정보를 수집하였으나, 관련 기록을 보존하지 않아 법정대리인 동의와 관련된 사항(법정대리인 이름, 동의 일시 등)을 확인할 수 없는 경우" + ] + } + ] + }, + { + "Id": "3.1.2", + "Name": "개인정보 수집 제한", + "Description": "개인정보를 수집하는 경우 처리 목적에 필요한 최소한의 개인정보만을 수집하여야 하며, 정보주체가 선택적으로 동의할 수 있는 사항 등에 동의하지 아니한다는 이유로 정보주체에게 재화 또는 서비스의 제공을 거부하지 않아야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.1. 개인정보 수집 시 보호조치", + "Section": "3.1.2 개인정보 수집 제한", + "AuditChecklist": [ + "개인정보를 수집하는 경우 그 목적에 필요한 범위에서 최소한의 정보만을 수집하고 있는가?", + "정보주체의 동의를 받아 개인정보를 수집하는 경우 필요한 최소한의 정보 외의 개인정보 수집에는 동의하지 않을 수 있다는 사실을 구체적으로 알리고 있는가?", + "정보주체가 수집 목적에 필요한 최소한의 정보 이외의 개인정보 수집에 동의하지 않는다는 이유로 서비스 또는 재화의 제공을 거부하지 않도록 하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제16조(개인정보의 수집제한), 제22조(동의를 받는 방법)" + ], + "AuditEvidence": [ + "온라인 개인정보 수집 양식(홈페이지 회원가입 화면, 이벤트 참여 화면 등)", + "오프라인 개인정보 수집 양식(멤버십 가입신청서 등)", + "개인정보 처리방침" + ], + "NonComplianceCases": [ + "사례 1 : 계약의 체결 및 이행을 근거로 정보주체 동의 없이 개인정보를 수집하면서 계약의 체결 및 이행을 위해 반드시 필요하지 않은 개인정보 항목까지 과도하게 수집하는 경우", + "사례 2 : 정보주체로부터 선택사항에 대한 동의를 받으면서 해당 개인정보 수집에는 동의하지 아니할 수 있다는 사실을 구체적으로 알리지 않은 경우", + "사례 3 : 회원가입 양식에서 필수와 선택 정보를 구분하여 별도 동의를 받도록 되어 있었으나, 선택정보에 대하여 동의하지 않아도 회원가입이 가능함을 정보주체가 인지할 수 있도록 구체적으로 알리지 않은 경우(개인정보 입력 양식에 개인정보 항목별로 필수, 선택 여부가 표시되어 있지 않은 경우 등)", + "사례 4 : 홈페이지 회원가입 화면에서 선택사항에 대하여 동의하지 않거나 선택정보를 입력하지 않으면 다음 단계로 넘어가지 않거나 회원가입이 차단되는 경우", + "사례 5 : 채용 계약 시 채용 예정 직무와 직접 관련이 없는 가족사항 등 과도한 개인정보를 수집하는 경우" + ] + } + ] + }, + { + "Id": "3.1.3", + "Name": "주민등록번호 처리 제한", + "Description": "주민등록번호는 법적 근거가 있는 경우를 제외하고는 수집·이용 등 처리할 수 없으며, 주민등록번호의 처리가 허용된 경우라 하더라도 인터넷 홈페이지 등에서 대체수단을 제공하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.1. 개인정보 수집 시 보호조치", + "Section": "3.1.3 주민등록번호 처리 제한", + "AuditChecklist": [ + "주민등록번호는 명확한 법적 근거가 있는 경우에만 처리하고 있는가?", + "주민등록번호의 수집 근거가 되는 법조항을 구체적으로 식별하고 있는가?", + "법적 근거에 따라 주민등록번호를 처리하는 경우에도 정보주체가 인터넷 홈페이지를 통하여 회원으로 가입하는 단계에서는 주민등록번호를 사용하지 아니하고도 회원으로 가입할 수 있는 방법을 제공하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제24조의2(주민등록번호 처리의 제한)", + "정보통신망법 제23조의2(주민등록번호의 사용 제한)" + ], + "AuditEvidence": [ + "개인정보 수집 양식(홈페이지 회원가입 화면, 이벤트 참여, 멤버십 가입신청서 등)", + "온라인 개인정보 수집 양식(본인확인 등 대체가입수단 제공 화면)", + "주민등록번호를 처리하는 경우 주민등록번호 처리 근거 증거자료", + "개인정보 처리방침" + ], + "NonComplianceCases": [ + "사례 1 : 홈페이지 가입과 관련하여 실명확인 등 단순 회원관리 목적을 위하여 정보주체의 동의에 근거하여 주민등록번호를 수집한 경우", + "사례 2 : 정보주체의 주민등록번호를 시행규칙이나 지방자치단체의 조례에 근거하여 수집한 경우", + "사례 3 : 비밀번호 분실 시 본인확인 등의 목적으로 주민등록번호 뒤 6자리를 수집하지만, 관련된 법적 근거가 없는 경우", + "사례 4 : 채용전형 진행단계에서 법적 근거 없이 입사지원자의 주민등록번호를 수집한 경우", + "사례 5 : 콜센터에 상품, 서비스 관련 문의 시 본인확인을 위하여 주민등록번호를 수집한 경우", + "사례 6 : 주민등록번호 수집의 법적 근거가 있다는 사유로 홈페이지 회원가입 단계에서 대체가입수단을 제공하지 않고 주민등록번호를 입력받는 본인확인 및 회원가입 방법만을 제공한 경우" + ] + } + ] + }, + { + "Id": "3.1.4", + "Name": "민감정보 및 고유식별정보의 처리 제한", + "Description": "민감정보와 고유식별정보(주민등록번호 제외)를 처리하기 위해서는 법령에서 구체적으로 처리를 요구하거나 허용하는 경우를 제외하고는 정보주체의 별도 동의를 받아야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.1. 개인정보 수집 시 보호조치", + "Section": "3.1.4 민감정보 및 고유식별정보의 처리 제한", + "AuditChecklist": [ + "민감정보는 정보주체로부터 별도의 동의를 받거나 관련 법령에 근거가 있는 경우에만 처리하고 있는가?", + "고유식별정보(주민등록번호 제외)는 정보주체로부터 별도의 동의를 받거나 관련 법령에 구체적인 근거가 있는 경우에만 처리하고 있는가?", + "재화 또는 서비스를 제공하는 과정에서 공개되는 정보에 정보주체의 민감정보가 포함됨으로써 사생활 침해의 위험성이 있다고 판단하는 때에는 재화 또는 서비스의 제공 전에 민감정보의 공개 가능성 및 비공개를 선택하는 방법을 정보주체가 알아보기 쉽게 알리고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제23조(민감정보의 처리제한), 제24조(고유식별정보의 처리 제한)" + ], + "AuditEvidence": [ + "온라인 개인정보 수집 양식(홈페이지 회원가입 화면, 이벤트 참여 등)", + "오프라인 개인정보 수집 양식(회원가입신청서 등)", + "개인정보 처리방침" + ], + "NonComplianceCases": [ + "사례 1 : 장애인에 대한 요금감면 등 혜택 부여를 위하여 장애 여부 등 건강에 관한 민감정보를 수집하면서 다른 개인정보 항목에 포함하여 일괄 동의를 받은 경우", + "사례 2 : 회원가입 시 외국인에 한하여 외국인등록번호를 수집하면서 다른 개인정보 항목에 포함하여 일괄 동의를 받은 경우", + "사례 3 : 민감정보 또는 고유식별정보의 수집에 대해 별도의 동의를 받으면서 고지하여야 할 4가지 사항 중에 일부를 누락하거나 잘못된 내용으로 고지하는 경우(동의 거부 권리 및 동의 거부에 따른 불이익 사항을 고지하지 않은 경우 등)" + ] + } + ] + }, + { + "Id": "3.1.5", + "Name": "개인정보 간접수집", + "Description": "정보주체 이외로부터 개인정보를 수집하거나 제3자로부터 제공받는 경우에는 업무에 필요한 최소한의 개인정보를 수집하거나 제공받아야 하며, 법령에 근거하거나 정보주체의 요구가 있으면 개인정보의 수집 출처, 처리목적, 처리정지의 요구권리를 알려야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.1. 개인정보 수집 시 보호조치", + "Section": "3.1.5 개인정보 간접수집", + "AuditChecklist": [ + "정보주체 이외의 제3자로부터 개인정보를 제공받는 경우 개인정보 수집에 대한 동의획득 책임이 개인정보를 제공하는 자에게 있음을 계약을 통하여 명시하고 있는가?", + "공개된 매체 및 장소에서 개인정보를 수집하는 경우 정보주체의 공개 목적·범위 및 사회 통념상 동의 의사가 있다고 인정되는 범위 내에서만 수집·이용하고 있는가?", + "서비스 계약 이행을 위해 필요한 경우로서, 서비스 제공 과정에서 자동수집장치 등에 의하여 수집·생성하는 개인정보의 경우에도 최소수집 원칙을 적용하고 있는가?", + "정보주체 이외로부터 수집하는 개인정보에 대해 정보주체의 요구가 있는 경우 즉시 필요한 사항을 정보주체에게 알리고 있는가?", + "정보주체 이외로부터 수집한 개인정보를 처리하는 경우 개인정보의 종류·규모 등이 법적 요건에 해당하는 경우 필요한 사항을 정보주체에게 알리고 있는가?", + "정보주체에게 수집 출처에 대해 알린 기록을 해당 개인정보의 파기 시까지 보관 및 관리하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제16조(개인정보의 수집 제한), 제19조(개인정보를 제공받은 자의 이용·제공 제한), 제20조(정보주체 이외로부터 수집한 개인정보의 수집 출처 등 통지)" + ], + "AuditEvidence": [ + "개인정보 제공 관련 계약서(제공하는 자와의 계약 사항)", + "개인정보 수집출처에 대한 정보주체 통지 내역", + "개인정보 처리방침" + ], + "NonComplianceCases": [ + "사례 1 : 인터넷 홈페이지, SNS에 공개된 개인정보를 수집하고 있는 상태에서 정보주체의 수집 출처 요구에 대한 처리절차가 존재하지 않은 경우", + "사례 2 : 개인정보 보호법 제17조제1항제1호에 따라 다른 사업자로부터 개인정보 제공동의를 근거로 개인정보를 제공받았으나, 이에 대하여 해당 정보주체에게 3개월 내에 통지하지 않은 경우(다만 제공받은 자가 5만 명 이상 정보주체의 민감정보 또는 고유식별정보를 처리하거나 100만 명 이상 정보주체의 개인정보를 처리하는 경우)", + "사례 3 : 법적 의무 대상자에 해당되어 개인정보 수집 출처를 정보주체에게 통지하면서 개인정보의 처리목적 또는 동의를 철회할 권리가 있다는 사실 등 필수 통지사항을 일부 누락한 경우", + "사례 4 : 법적 의무 대상자에 해당되어 개인정보 수집 출처를 정보주체에게 통지하였으나, 수집 출처 통지에 관한 기록을 해당 개인정보의 파기 시까지 보관하지 않은 경우" + ] + } + ] + }, + { + "Id": "3.1.6", + "Name": "영상정보처리기기 설치·운영", + "Description": "고정형 영상정보처리기기를 공개된 장소에 설치·운영하거나 이동형 영상정보처리기기를 공개된 장소에서 업무를 목적으로 운영하는 경우 설치 목적 및 위치에 따라 법적 요구사항을 준수하고, 적절한 보호대책을 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.1. 개인정보 수집 시 보호조치", + "Section": "3.1.6 영상정보처리기기 설치·운영", + "AuditChecklist": [ + "공개된 장소에 고정형 영상정보처리기기를 설치·운영할 경우 법적 허용 요건에 해당하는지를 검토하고 있는가?", + "공공기관 등이 공개된 장소에 고정형 영상정보처리기기를 설치·운영하려는 경우 공청회·설명회 개최 등의 법령에 따른 절차를 거쳐 관계 전문가 및 이해관계자의 의견을 수렴하고 있는가?", + "고정형 영상정보처리기기 설치·운영 시 정보주체가 쉽게 인식할 수 있도록 안내판 설치 등 필요한 조치를 하고 있는가?", + "업무를 목적으로 공개된 장소에서 이동형 영상정보처리기기를 운영하는 경우 법적 허용 요건에 해당하는지를 검토하고 있는가?", + "업무를 목적으로 공개된 장소에서 이동형 영상정보처리기기로 사람 또는 그 사람과 관련된 사물의 영상을 촬영하는 경우 불빛, 소리, 안내판 등의 방법으로 촬영 사실을 표시하고 알리고 있는가?", + "영상정보처리기기 및 영상정보의 안전한 관리를 위한 영상정보처리기기 운영·관리 방침을 마련하여 시행하고 있는가?", + "영상정보의 보관 기간을 정하고 있으며, 보관 기간 만료 시 지체 없이 파기하고 있는가?", + "영상정보처리기기 설치·운영에 관한 사무를 위탁하는 경우 관련 절차 및 요건에 따라 계약서에 반영하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제25조(고정형 영상정보처리기기의 설치·운영 제한), 제25조의2(이동형 영상정보처리기기의 운영 제한)" + ], + "AuditEvidence": [ + "영상정보처리기기 운영 현황", + "영상정보처리기기 안내판", + "영상정보처리기기 운영·관리방침", + "영상정보처리기기 관리화면(계정·권한 내역, 영상정보 보존기간 등)", + "영상정보처리기기 운영 수탁자와의 계약서 및 점검 이력" + ], + "NonComplianceCases": [ + "사례 1 : 영상정보처리기기 안내판의 고지 문구가 일부 누락되어 운영되고 있거나, 영상정보처리기기 운영 및 관리 방침을 수립·운영하고 있지 않은 경우", + "사례 2 : 영상정보처리기기 운영·관리 방침을 수립 운영하고 있으나, 방침 내용과 달리 보관기간을 준수하지 않고 운영되거나, 영상정보 보호를 위한 접근통제 및 로깅 등 방침에 기술한 사항이 준수되지 않는 등 관리가 미흡한 경우", + "사례 3 : 영상정보처리기기의 설치·운영 사무를 외부업체에 위탁하고 있으나, 영상정보의 관리 현황 점검에 관한 사항, 손해배상 책임에 관한 사항 등 법령에서 요구하는 내용을 영상정보처리기기 업무 위탁 계약서에 명시하지 않은 경우", + "사례 4 : 영상정보처리기기의 설치·운영 사무를 외부업체에 위탁하고 있으나, 영상정보처리기기 안내판에 수탁자의 명칭과 연락처를 누락하여 고지한 경우" + ] + } + ] + }, + { + "Id": "3.1.7", + "Name": "마케팅 목적의 개인정보 수집·이용", + "Description": "재화나 서비스의 홍보, 판매 권유, 광고성 정보전송 등 마케팅 목적으로 개인정보를 수집 및이용하는 경우 그 목적을 정보주체가 명확하게 인지할 수 있도록 고지하고 동의를 받아야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.1. 개인정보 수집 시 보호조치", + "Section": "3.1.7 마케팅 목적의 개인정보 수집·이용", + "AuditChecklist": [ + "정보주체에게 재화나 서비스를 홍보하거나 판매를 권유하기 위하여 개인정보 처리에 대한 동의를 받는 경우 정보주체가 이를 명확하게 인지할 수 있도록 알리고 별도의 동의를 받고 있는가?", + "전자적 전송매체를 이용하여 영리목적의 광고성 정보를 전송하는 경우 수신자의 명시적인 사전 동의를 받고 있으며, 2년마다 정기적으로 수신자의 수신동의 여부를 확인하고 있는가?", + "전자적 전송매체를 이용한 영리목적의 광고성 정보 전송에 대하여 수신자가 수신거부의사를 표시하거나 사전 동의를 철회한 경우 영리목적의 광고성 정보 전송을 중단하도록 하고 있는가?", + "영리목적의 광고성 정보를 전송하는 경우 전송자의 명칭, 수신거부 방법 등을 구체적으로 밝히고 있으며, 야간시간에는 전송하지 않도록 하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제22조(동의를 받는 방법)", + "정보통신망법 제50조(광고성 정보 전송 제한)" + ], + "AuditEvidence": [ + "온라인 개인정보 수집 양식(홈페이지 회원가입 화면, 모바일앱 회원가입 화면, 이벤트 참여 등)", + "오프라인 개인정보 수집 양식(회원가입신청서 등)", + "마케팅 동의 기록", + "광고성 정보전송 수신동의 기록 및 수신동의 의사확인 기록", + "광고성 정보 발송 시스템 관리자 화면(메일, SMS, 앱 푸시 등)", + "광고성 정보 발송 문구", + "개인정보 처리방침" + ], + "NonComplianceCases": [ + "사례 1 : ʻ홍보 및 마케팅ʼ 목적으로 개인정보를 수집하면서 ʻ부가서비스 제공ʼ, ʻ제휴 서비스 제공ʼ 등과 같이 목적을 모호하게 안내하는 경우 또는 다른 목적으로 수집하는 개인정보와 구분하지 않고 포괄 동의를 받는 경우", + "사례 2 : 모바일 앱에서 광고성 정보전송(앱 푸시)에 대하여 거부 의사를 밝혔으나, 프로그램 오류 등의 이유로 광고성 앱 푸시가 이루어지는 경우", + "사례 3 : 온라인 회원가입 화면에서 문자, 이메일에 의한 광고성 정보 전송에 대하여 디폴트로 체크되어 있는 경우", + "사례 4 : 광고성 정보 수신동의 여부에 대하여 2년마다 확인하지 않은 경우", + "사례 5 : 영리목적의 광고성 정보를 전자우편으로 전송하면서 제목이 시작되는 부분에 ʻ(광고)ʼ 표시를 하지 않은 경우" + ] + } + ] + }, + { + "Id": "3.2.1", + "Name": "개인정보 현황관리", + "Description": "수집·보유하는 개인정보의 항목, 보유량, 처리 목적 및 방법, 보유기간 등 현황을 정기적으로 관리하여야 하며, 공공기관의 경우 이를 법률에서 정한 관계기관의 장에게 등록하여야 한다.", + "Checks": [ + "s3_bucket_lifecycle_enabled", + "macie_is_enabled" + ], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.2. 개인정보 보유 및 이용 시 보호조치", + "Section": "3.2.1 개인정보 현황관리", + "AuditChecklist": [ + "수집·보유하고 있는 개인정보의 항목, 보유량, 처리 목적 및 방법, 보유기간 등 현황을 정기적으로 관리하고 있는가?", + "공공기관이 개인정보파일을 운용하거나 변경하는 경우 관련된 사항을 법률에서 정한 관계기관의 장에게 등록하고 있는가?", + "공공기관은 개인정보파일의 보유 현황을 개인정보 처리방침에 공개하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제32조(개인정보파일의 등록 및 공개)" + ], + "AuditEvidence": [ + "개인정보 현황표", + "개인정보 흐름표·흐름도", + "개인정보파일 등록 현황", + "개인정보파일 관리대장", + "개인정보 처리방침에 관한 사항을 기록한 개인정보파일", + "「조세범처벌법」에 따른 범칙행위 조사 및 「관세법」에 따른 범칙행위 조사에 관한 사항을 기록한 개인정보파일", + "일회성으로 운영되는 파일 등 지속적으로 관리할 필요가 낮다고 인정되어 대통령령으로 정하는 개인정보파일", + "회의 참석 수당 지급, 자료·물품의 송부, 금전의 정산 등 단순 업무 수행을 위해 운영되는 개인정보파일로서 지속적 관리 필요성이 낮은 개인정보파일", + "공중위생 등 공공의 안전과 안녕을 위하여 긴급히 필요한 경우로서 일시적으로 처리되는 개인정보파일", + "그 밖에 일회적 업무 처리만을 위해 수집된 개인정보파일로서 저장되거나 기록되지 않는 개인정보파일", + "다른 법령에 따라 비밀로 분류된 개인정보파일", + "국가안전보장과 관련된 정보 분석을 목적으로 수집 또는 제공 요청되는 개인정보파일", + "영상정보처리기기를 통하여 처리되는 개인영상정보파일", + "「금융실명거래 및 비밀보장에 관한 법률」에 따른 금융기관이 금융업무 취급을 위하여 보유하는 개인정보파일" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보파일을 홈페이지의 개인정보파일 등록 메뉴를 통하여 목록을 관리하고 있으나, 그 중 일부 홈페이지 서비스와 관련된 개인정보파일의 내용이 개인정보 처리방침에 누락되어 있는 경우", + "사례 2 : 신규 개인정보파일을 구축한 지 2개월이 경과하였으나, 해당 개인정보파일을 개인정보 보호위원회에 등록하지 않은 경우", + "사례 3 : 개인정보 보호위원회에 등록되어 공개된 개인정보파일의 내용(수집하는 개인정보의 항목 등)이 실제 처리하고 있는 개인정보파일 현황과 상이한 경우", + "사례 4 : 공공기관이 임직원의 개인정보파일, 통계법에 따라 수집되는 개인정보파일에 대해 개인정보파일 등록 예외사항에 해당되지 않음에도 불구하고 해당 개인정보파일을 개인정보 보호위원회에 등록하지 않은 경우" + ] + } + ] + }, + { + "Id": "3.2.2", + "Name": "개인정보 품질보장", + "Description": "수집된 개인정보는 처리 목적에 필요한 범위에서 개인정보의 정확성·완전성·최신성이 보장되도록 정보주체에게 관리절차를 제공하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.2. 개인정보 보유 및 이용 시 보호조치", + "Section": "3.2.2 개인정보 품질보장", + "AuditChecklist": [ + "개인정보를 최신의 상태로 정확하게 유지하기 위한 절차 및 방안을 수립·이행하고 있는가?", + "정보주체가 본인의 개인정보에 대하여 정확성, 완전성 및 최신성을 유지할 수 있는 방법을 제공하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제3조(개인정보 보호 원칙)" + ], + "AuditEvidence": [ + "정보주체 개인정보 수정·변경 양식(온라인, 오프라인)", + "개인정보 최신성 유지 절차" + ], + "NonComplianceCases": [ + "사례 1 : 인터넷 홈페이지를 통하여 회원정보를 변경할 때는 본인확인 절차를 거치고 있으나, 고객센터 상담원과의 통화를 통한 회원 정보 변경 시에는 본인확인 절차가 미흡하여 회원정보의 불법적인 변경이 가능한 경우", + "사례 2 : 온라인 회원에 대해서는 개인정보를 변경할 수 있는 방법을 제공하고 있으나, 오프라인 회원에 대해서는 개인정보를 변경할 수 있는 방법을 제공하고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "3.2.3", + "Name": "이용자 단말기 접근 보호", + "Description": "정보주체(이용자)의 이동통신단말장치 내에 저장되어 있는 정보 및 이동통신단말장치에 설치된 기능에 접근이 필요한 경우 이를 명확하게 인지할 수 있도록 알리고 정보주체(이용자)의 동의를 받아야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.2. 개인정보 보유 및 이용 시 보호조치", + "Section": "3.2.3 이용자 단말기 접근 보호", + "AuditChecklist": [ + "정보주체(이용자)의 이동통신단말장치 내에 저장되어 있는 정보 및 이동통신단말장치에 설치된 기능에 대하여 접근할 수 있는 권한이 필요한 경우 명확하게 인지할 수 있도록 알리고 정보주체(이용자)의 동의를 받고 있는가?", + "이동통신단말장치 내에서 해당 서비스를 제공하기 위하여 반드시 필요한 접근권한이 아닌 경우, 정보주체(이용자)가 동의하지 않아도 서비스 제공을 거부하지 않도록 하고 있는가?", + "이동통신단말장치 내에서 해당 접근권한에 대한 정보주체(이용자)의 동의 및 철회방법을 마련하고 있는가?" + ], + "RelatedRegulations": [ + "정보통신망법 제22조의2(접근권한에 대한 동의)" + ], + "AuditEvidence": [ + "앱 접근권한 동의 화면", + "앱 접근권한 설정 현황" + ], + "NonComplianceCases": [ + "사례 1 : 스마트폰 앱에서 서비스에 불필요함에도 불구하고 주소록, 사진, 문자 등 스마트폰 내 개인정보 영역에 접근할 수 있는 권한을 과도하게 설정한 경우", + "사례 2 : 정보통신서비스 제공자의 스마트폰 앱에서 스마트폰 내에 저장되어 있는 정보 및 설치된 기능에 접근하면서 접근권한에 대한 고지 및 동의를 받지 않고 있는 경우", + "사례 3 : 스마트폰 앱의 접근권한에 대한 동의를 받으면서 선택사항에 해당하는 권한을 필수권한으로 고지하여 동의를 받는 경우", + "사례 4 : 접근권한에 대한 개별동의가 불가능한 안드로이드 6.0 미만 버전을 지원하는 스마트폰 앱을 배포하면서 선택적 접근권한을 함께 설정하여, 선택적 접근권한에 대하여 거부할 수 없도록 하고 있는 경우" + ] + } + ] + }, + { + "Id": "3.2.4", + "Name": "개인정보 목적 외 이용 및 제공", + "Description": "개인정보는 수집 시의 정보주체에게 고지·동의를 받은 목적 또는 법령에 근거한 범위 내에서만 이용 또는 제공하여야 하며, 이를 초과하여 이용·제공하려는 때에는 정보주체의 추가 동의를 받거나 관계 법령에 따른 적법한 경우인지 확인하고 적절한 보호대책을 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.2. 개인정보 보유 및 이용 시 보호조치", + "Section": "3.2.4 개인정보 목적 외 이용 및 제공", + "AuditChecklist": [ + "개인정보는 최초 수집 시 정보주체로부터 동의받은 목적 또는 법령에 근거한 범위 내에서만 이용·제공하고 있는가?", + "개인정보처리자로부터 개인정보를 제공받은 경우 제공받은 목적의 범위 내에서만 이용·제공하고 있는가?", + "개인정보를 수집 목적 또는 개인정보처리자로부터 제공받은 목적의 범위를 초과하여 이용하거나 제공하는 경우 정보주체에게 별도의 동의를 받거나 법적 근거가 있는 경우로 제한하고 있는가?", + "개인정보를 목적 외의 용도로 제3자에게 제공하는 경우 제공받는 자에게 이용목적 및 방법 등을 제한하거나 안전성 확보를 위하여 필요한 조치를 마련하도록 요청하고 있는가?", + "공공기관이 개인정보를 목적 외의 용도로 이용하거나 제3자에게 제공하는 경우 그 이용 또는 제공의 법적 근거, 목적 및 범위 등에 관하여 필요한 사항을 관보 또는 인터넷 홈페이지 등에 게재하고 있는가?", + "공공기관 등이 개인정보를 목적 외의 용도로 이용하거나 제3자에게 제공하는 경우 목적 외 이용 및 제3자 제공대장에 기록·관리하는 등 절차를 마련하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제18조(개인정보의 목적 외 이용·제공 제한), 제19조(개인정보를 제공받은 자의 이용·제공 제한)" + ], + "AuditEvidence": [ + "개인정보 목적 외 이용 및 제3자 제공 내역(요청서 등 관련 증거자료 포함)", + "개인정보 목적 외 이용 및 제3자 제공 대장(공공기관인 경우)", + "홈페이지 또는 관보 게재 내역(공공기관인 경우)", + "자료 제공 요청 대응 지침", + "자료 제공 요청 공문 및 개인정보 제공내역, 대장 등" + ], + "NonComplianceCases": [ + "사례 1 : 상품배송을 목적으로 수집한 개인정보를 사전에 동의 받지 않은 자사 상품의 통신판매 광고에 이용한 경우", + "사례 2 : 고객 만족도 조사, 경품 행사에 응모하기 위하여 수집한 개인정보를 자사의 할인판매행사 안내용 광고 발송에 이용한 경우", + "사례 3 : 공공기관이 다른 법률에 근거하여 민원인의 개인정보를 목적 외로 타 기관에 제공하면서 관련 사항을 관보 또는 인터넷 홈페이지에 게시하지 않은 경우", + "사례 4 : 공공기관이 범죄 수사의 목적으로 경찰서에 개인정보를 제공하면서 ʻ개인정보 목적 외 이용 및 제3자 제공 대장ʼ에 관련 사항을 기록하지 않은 경우" + ] + } + ] + }, + { + "Id": "3.2.5", + "Name": "가명정보 처리", + "Description": "가명정보를 처리하는 경우 목적제한, 결합제한, 안전조치, 금지의무 등 법적 요건을 준수하고 적정 수준의 가명처리를 보장할 수 있도록 가명처리 절차를 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.2. 개인정보 보유 및 이용 시 보호조치", + "Section": "3.2.5 가명정보 처리", + "AuditChecklist": [ + "가명정보를 처리하는 경우 목적 제한, 가명처리 방법 및 기준, 적정성 검토, 재식별 금지 및 재식별 발생 시 조치사항 등 가명정보를 적정하게 처리하기 위한 절차를 수립하고 있는가?", + "개인정보를 가명처리하여 이용·제공 시 추가 정보의 사용·결합 없이는 개인을 알아볼 수 없도록 적정한 수준으로 가명처리를 수행하고 있는가?", + "다른 개인정보처리자와 가명정보를 결합하는 경우 결합전문기관 또는 데이터전문기관을 통해 결합하고 있는가?", + "가명정보를 처리하는 경우 추가 정보를 삭제 또는 별도로 분리하여 보관·관리, 관련 기록의 작성·보관 등 안전성 확보에 필요한 기술적·관리적 및 물리적 조치를 하고 있는가?", + "가명정보 처리목적 등을 고려하여 가명정보의 처리 기간을 적정한 기간으로 정하고 있으며, 해당 기간이 경과한 경우 지체 없이 파기하고 있는가?", + "개인정보를 익명처리하는 경우 시간·비용·기술 등을 합리적으로 고려할 때 다른 정보를 사용하여도 더 이상 특정 개인을 알아볼 수 없도록 적정한 수준으로 익명처리하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제2조(정의), 제28조의2(가명정보의 처리 등), 제28조의3(가명정보의 결합 제한), 제28조의4(가명정보에 대한 안전조치의무 등), 제28조의5(가명정보 처리 시 금지의무 등), 제28조의7(적용범위), 제58조의2(적용제외)" + ], + "AuditEvidence": [ + "가명처리·익명처리 적정성 평가 절차 및 결과", + "가명정보 처리 기록", + "개인정보 처리방침(가명정보 이용·제공에 관한 사항) 등" + ], + "NonComplianceCases": [ + "사례 1 : 통계작성 및 과학적 연구를 위하여 정보주체 동의 없이 가명정보를 처리하면서 가명정보 처리에 관한 기록을 남기고 있지 않거나, 또는 개인정보 처리방침에 관련 사항을 공개하지 않은 경우", + "사례 2 : 가명정보와 동일한 데이터베이스 내에 추가 정보를 분리하지 않고 보관하고 있거나, 또는 가명 정보와 추가 정보에 대한 접근권한이 적절히 분리되지 않은 경우", + "사례 3 : 개인정보를 가명처리하여 활용하고 있으나 적정한 수준의 가명처리가 수행되지 않아 추가 정보의 사용 없이도 다른 정보와의 결합 등을 통하여 특정 개인을 알아볼 수 있는 가능성이 존재하는 경우", + "사례 4 : 테스트 데이터 생성, 외부 공개 등을 위하여 개인정보를 익명처리하였으나, 특이치 등으로 인하여 특정 개인에 대한 식별가능성이 존재하는 등 익명처리가 적정하게 수행되었다고 보기 어려운 경우" + ] + } + ] + }, + { + "Id": "3.3.1", + "Name": "개인정보 제3자 제공", + "Description": "개인정보를 제3자에게 제공하는 경우 법적 근거에 의하거나 정보주체의 동의를 받아야 하며, 제3자에게 개인정보의 접근을 허용하는 등 제공 과정에서 개인정보를 안전하게 보호하기 위한 보호대책을 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.3. 개인정보 제공 시 보호조치", + "Section": "3.3.1 개인정보 제3자 제공", + "AuditChecklist": [ + "개인정보를 제3자에게 제공하는 경우 정보주체 동의, 법령상 의무준수 등 적법 요건을 명확히 식별하고 이를 준수하고 있는가?", + "정보주체에게 개인정보 제3자 제공 동의를 받는 경우 관련 사항을 명확하게 고지하고 다른 동의사항과 구분하여 적법하게 동의를 받고 있는가?", + "정보주체에게 개인정보 제3자 제공 동의를 받는 경우 관련 내용을 명확하게 고지하고 법령에서 정한 중요한 내용에 대해 명확히 표시하여 알아보기 쉽게 하고 있는가?", + "개인정보를 제3자에게 제공하는 경우 제공 목적에 맞는 최소한의 개인정보 항목으로 제한하고 있는가?", + "개인정보를 제3자에게 제공하는 경우 안전한 절차와 방법을 통해 제공하고 제공 내역을 기록하여 보관하고 있는가?", + "제3자에게 개인정보의 접근을 허용하는 경우 개인정보를 안전하게 보호하기 위한 보호절차에 따라 통제하고 있는가?", + "정보주체의 동의 없이 개인정보의 추가적인 제공 시 당초 수집 목적과의 관련성, 예측 가능성, 이익 침해 여부, 안전성 확보조치 등의 고려사항에 대한 판단기준을 수립 및 이행하고, 추가적인 제공이 지속적으로 발생하는 경우 고려사항에 대한 판단기준을 개인정보 처리방침에 공개하고 이를 점검하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제17조(개인정보의 제공), 제22조(동의를 받는 방법)", + "개인정보 처리 방법에 관한 고시" + ], + "AuditEvidence": [ + "온라인 개인정보 제3자 제공 관련 양식(홈페이지 회원가입 화면, 개인정보 제3자 제공 동의 화면 등)", + "오프라인 개인정보 제3자 제공 관련 양식(회원가입신청서, 개인정보 제3자 제공 동의서 등)", + "제3자 제공 내역", + "개인정보 처리방침" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보처리자가 개인정보 제3자 제공 동의를 받을 때 정보주체에게 고지하는 사항 중에 일부 사항(동의 거부권, 제공하는 항목 등)을 누락한 경우", + "사례 2 : 개인정보를 제3자에게 제공하는 과정에서 제3자 제공 동의 여부를 적절히 확인하지 못하여 동의하지 않은 정보주체의 개인정보가 함께 제공된 경우", + "사례 3 : 개인정보를 제공 동의를 받을 때, 제공받는 자를 특정하지 않고 ʻ~ 등ʼ과 같이 포괄적으로 안내하고 동의를 받은 경우", + "사례 4 : 회원 가입 단계에서 선택사항으로 제3자 제공 동의를 받고 있으나, 제3자 제공에 동의하지 않으면 회원 가입 절차가 더 이상 진행되지 않도록 되어 있는 경우", + "사례 5 : 제공받는 자의 이용 목적과 관련 없이 지나치게 많은 개인정보를 제공하는 경우" + ] + } + ] + }, + { + "Id": "3.3.2", + "Name": "개인정보 처리 업무 위탁", + "Description": "개인정보 처리업무를 제3자에게 위탁하는 경우 위탁하는 업무의 내용과 수탁자 등 관련사항을 공개하여야 한다. 또한 재화 또는 서비스를 홍보하거나 판매를 권유하는 업무를 위탁하는 경우 위탁하는 업무의 내용과 수탁자를 정보주체에게 알려야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.3. 개인정보 제공 시 보호조치", + "Section": "3.3.2 개인정보 처리 업무 위탁", + "AuditChecklist": [ + "개인정보 처리업무를 제3자에게 위탁(재위탁 포함)하는 경우 인터넷 홈페이지 등에 위탁하는 업무의 내용과 수탁자를 현행화하여 공개하고 있는가?", + "재화 또는 서비스를 홍보하거나 판매를 권유하는 업무를 위탁하는 경우에는 서면, 전자우편, 문자전송 등의 방법으로 위탁하는 업무의 내용과 수탁자를 정보주체에게 알리고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제26조(업무위탁에 따른 개인정보의 처리 제한)" + ], + "AuditEvidence": [ + "개인정보 처리방침(개인정보 처리업무 위탁 관련 공개 내역)", + "개인정보 수집 양식", + "개인정보 처리 위탁 계약서", + "재화 또는 서비스 홍보·판매 권유 업무 위탁 관련 정보주체 통지 내역" + ], + "NonComplianceCases": [ + "사례 1 : 홈페이지 개인정보 처리방침에 개인정보 처리업무 위탁 사항을 공개하고 있으나, 일부 수탁자와 위탁하는 업무의 내용이 누락된 경우", + "사례 2 : 재화 또는 서비스를 홍보하거나 판매를 권유하는 업무를 위탁하면서, 위탁하는 업무의 내용과 수탁자를 서면등의 방법으로 정보주체에게 알리지 않고 개인정보 처리방침에 공개하는 것으로 갈음한 경우", + "사례 3 : 기존 개인정보 처리업무 수탁자와의 계약 해지에 따라 개인정보 처리업무 수탁자가 변경되었으나, 이에 대하여 개인정보 처리방침에 지체 없이 반영하지 않은 경우", + "사례 4 : 개인정보 처리업무를 위탁받은 자가 해당 업무를 제3자에게 재위탁을 하고 있지만, 재위탁에 관한 사항을 인터넷 홈페이지 등에 공개하고 있지 않은 경우" + ] + } + ] + }, + { + "Id": "3.3.3", + "Name": "영업의 양도 등에 따른 개인정보 이전", + "Description": "영업의 양도·합병 등으로 개인정보를 이전하거나 이전받는 경우 정보주체 통지 등 적절한 보호조치를 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.3. 개인정보 제공 시 보호조치", + "Section": "3.3.3 영업의 양도 등에 따른 개인정보 이전", + "AuditChecklist": [ + "영업의 전부 또는 일부의 양도·합병 등으로 개인정보를 다른 사람에게 이전하는 경우 필요한 사항을 사전에 정보주체에게 알리고 있는가?", + "개인정보를 이전받는 자는 법적 통지 요건에 해당될 경우 개인정보를 이전받은 사실 등 필요한 사항을 정보주체에게 지체 없이 알리고 있는가?", + "개인정보를 이전받는 자는 이전 당시의 본래 목적으로만 개인정보를 이용하거나 제3자에게 제공하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제27조(영업양도 등에 따른 개인정보의 이전 제한)" + ], + "AuditEvidence": [ + "개인정보 이전 관련 정보주체 고지 내역(영업 양수도 시)", + "개인정보 처리방침" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보처리자가 영업 양수를 통하여 개인정보를 이전받으면서 양도자가 개인정보 이전 사실을 알리지 않았음에도 개인정보 이전 사실을 정보주체에게 알리지 않은 경우", + "사례 2 : 영업 양수도 등에 의하여 개인정보를 이전받으면서 정보주체가 이전을 원하지 않은 경우 조치할 수 있는 방법과 절차를 마련하지 않거나, 이를 정보주체에게 알리지 않은 경우" + ] + } + ] + }, + { + "Id": "3.3.4", + "Name": "개인정보 국외이전", + "Description": "개인정보를 국외로 이전하는 경우 국외 이전에 대한 동의, 관련 사항에 대한 공개 등 적절한 보호조치를 수립·이행하여야 한다.", + "Checks": [ + "s3_bucket_cross_region_replication" + ], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.3. 개인정보 제공 시 보호조치", + "Section": "3.3.4 개인정보 국외이전", + "AuditChecklist": [ + "개인정보를 국외로 이전하는 경우 정보주체에게 국외 이전에 관한 고지 사항을 모두 알리고 별도 동의를 받거나, 인증 또는 인정 등 적법 요건을 준수하고 있는가?", + "정보주체와의 계약의 체결 및 이행을 위한 개인정보의 국외 처리위탁·보관에 대해 정보주체에게 알리는 경우 필요한 사항을 모두 포함하여 적절한 방법으로 알리고 있는가?", + "개인정보 보호 관련 법령 준수 및 개인정보 보호 등에 관한 사항을 포함하여 국외 이전에 관한 계약을 체결하고 있는가?", + "개인정보를 국외로 이전하는 경우 개인정보 보호를 위하여 필요한 조치를 취하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제28조의8(개인정보의 국외 이전), 제28조의9(개인정보의 국외 이전 중지 명령), 제28조의10(상호주의), 제28조의11(준용규정)", + "개인정보 국외 이전 운영 등에 관한 규정" + ], + "AuditEvidence": [ + "개인정보 국외 이전 관련 동의 양식", + "개인정보 국외 이전 관련 계약서", + "개인정보 처리방침", + "개인정보 국외 처리위탁·보관 관련 통지 또는 공개 내역" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보를 처리하는 과정에서 국외 사업자에게 개인정보 제3자 제공이 발생하였으나, 인증, 대상국 인정 등 동의 예외 요건에 해당되지 않음에도 불구하고 개인정보 국외 이전에 대한 별도 동의를 받지 않은 경우", + "사례 2 : 국외 클라우드 서비스(국외 리전)를 이용하여 개인정보 처리위탁 및 보관을 하면서 이전되는 국가, 이전 방법 등 관련 사항을 개인정보 처리방침에 공개하거나 정보주체에게 알리지 않은 경우", + "사례 3 : 개인정보 국외 이전에 대한 동의를 받으면서 이전받는 자의 명칭(업체명)만 고지하고 이전되는 국가 등에 대하여 알리지 않은 경우" + ] + } + ] + }, + { + "Id": "3.4.1", + "Name": "개인정보 파기", + "Description": "개인정보의 보유기간 및 파기 관련 내부 정책을 수립하고 개인정보의 보유기간 경과, 처리목적 달성 등 파기 시점이 도달한 때에는 파기의 안전성 및 완전성이 보장될 수 있는 방법으로 지체 없이 파기하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.4. 개인정보 파기 시 보호조치", + "Section": "3.4.1 개인정보 파기", + "AuditChecklist": [ + "개인정보의 보유기간 및 파기와 관련된 내부 정책을 수립하고 있는가?", + "개인정보의 처리목적이 달성되거나 보유기간이 경과한 경우 지체 없이 해당 개인정보를 파기하고 있는가?", + "개인정보를 파기할 때에는 복구·재생되지 않도록 안전한 방법으로 파기하고 있는가?", + "개인정보 파기에 대한 기록을 남기고 관리하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제21조(개인정보의 파기)", + "개인정보의 안전성 확보조치 기준 제13조(개인정보의 파기)" + ], + "AuditEvidence": [ + "개인정보 보유기간 및 파기 관련 규정", + "개인정보 파기 결과(회원 데이터베이스 등)", + "개인정보 파기관리대장" + ], + "NonComplianceCases": [ + "사례 1 : 회원 탈퇴 등 목적이 달성되거나 보유기간이 경과된 경우 회원 데이터베이스에서는 해당 개인정보를 파기하였으나, CRM·DW 등 연계된 개인정보처리시스템에 복제되어 저장되어 있는 개인정보를 파기하지 않은 경우", + "사례 2 : 특정 기간 동안 이벤트를 하면서 수집된 개인정보에 대하여 이벤트가 종료된 이후에도 파기 기준이 수립되어 있지 않거나 파기가 이루어지고 있지 않은 경우", + "사례 3 : 콜센터에서 수집되는 민원처리 관련 개인정보(상담이력, 녹취 등)를 전자상거래법을 근거로 3년간 보존하고 있으나, 3년이 경과한 후에도 파기하지 않고 보관하고 있는 경우", + "사례 4 : 블록체인 등 기술적 특성으로 인하여 목적이 달성된 개인정보의 완전 파기가 어려워 완전파기 대신 익명처리를 하였으나, 익명처리가 적절하게 수행되지 않아 일부 개인정보의 재식별 등 복원이 가능한 경우" + ] + } + ] + }, + { + "Id": "3.4.2", + "Name": "처리목적 달성 후 보유 시 조치", + "Description": "개인정보의 보유기간 경과 또는 처리목적 달성 후에도 관련 법령 등에 따라 파기하지 않고 보존하는 경우에는 해당 목적에 필요한 최소한의 항목으로 제한하고 다른 개인정보와 분리하여 저장·관리하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.4. 개인정보 파기 시 보호조치", + "Section": "3.4.2 처리목적 달성 후 보유 시 조치", + "AuditChecklist": [ + "개인정보의 보유기간 경과 또는 처리목적 달성 후에도 관련 법령 등에 따라 파기하지 않고 보존하는 경우, 관련 법령에 따른 최소한의 기간으로 한정하여 최소한의 정보만을 보존하도록 관리하고 있는가?", + "개인정보의 보유기간 경과 또는 처리목적 달성 후에도 관련 법령 등에 따라 파기하지 않고 보존하는 경우 해당 개인정보 또는 개인정보파일을 다른 개인정보와 분리하여 저장·관리하고 있는가?", + "분리 보관하고 있는 개인정보에 대하여 법령에서 정한 목적 범위 내에서만 처리 가능하도록 관리하고 있는가?", + "분리 보관하고 있는 개인정보에 대하여 접근권한을 최소한의 인원으로 제한하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제21조(개인정보의 파기)" + ], + "AuditEvidence": [ + "개인정보 보유기간 및 파기 관련 규정", + "분리 데이터베이스 현황(테이블 구조 등)", + "분리 데이터베이스 접근권한 현황" + ], + "NonComplianceCases": [ + "사례 1 : 탈퇴회원 정보를 파기하지 않고 전자상거래법에 따라 일정기간 보관하면서 Flag값만 변경하여 다른 회원정보와 동일한 테이블에 보관하고 있는 경우", + "사례 2 : 전자상거래법에 따른 소비자 불만 및 분쟁처리에 관한 기록에 대해 관련 법적 요건을 잘못 적용하여 3년이 아닌 5년간 보존하도록 정하고 있는 경우", + "사례 3 : 분리 데이터베이스를 구성하였으나 접근권한을 별도로 설정하지 않아 업무상 접근이 불필요한 인원도 분리 데이터베이스에 자유롭게 접근이 가능한 경우", + "사례 4 : 탈퇴회원 정보를 파기하지 않고 전자상거래법에 따라 계약 또는 청약철회, 대금결제 및 재화 공급에 관한 기록을 분리하여 보존하였으나, 전자상거래법에 따른 보존의무가 없는 선택정보까지 과도하게 보존한 경우" + ] + } + ] + }, + { + "Id": "3.5.1", + "Name": "개인정보 처리방침 공개", + "Description": "개인정보의 처리 목적 등 필요한 사항을 모두 포함하여 정보주체가 알기 쉽도록 개인정보 처리방침을 수립하고, 이를 정보주체가 언제든지 쉽게 확인할 수 있도록 적절한 방법에 따라 공개하고 지속적으로 현행화하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.5. 정보주체 권리보호", + "Section": "3.5.1 개인정보 처리방침 공개", + "AuditChecklist": [ + "개인정보 처리방침을 법령에서 요구하는 내용을 모두 포함하여 알기 쉬운 용어로 구체적이고 명확하게 작성하였는가?", + "개인정보 처리방침을 정보주체가 쉽게 확인할 수 있도록 인터넷 홈페이지 등에 지속적으로 현행화하여 공개하고 있는가?", + "개인정보 처리방침이 변경되는 경우 사유 및 변경 내용을 지체 없이 공지하고 정보주체가 언제든지 변경된 사항을 쉽게 알아 볼 수 있도록 조치하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제30조(개인정보 처리방침의 수립 및 공개), 제30조의2(개인정보 처리방침의 평가 및 개선권고)" + ], + "AuditEvidence": [ + "개인정보 처리방침", + "개인정보 처리방침 개정 관련 공지 내역(게시판 등)" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보 처리방침에 공개되어 있는 개인정보 수집, 제3자 제공 내역이 실제 수집 및 제공하는 내역과 다른 경우", + "사례 2 : 개인정보 보호책임자의 변경, 수탁자 변경 등 개인정보 처리방침 공개 내용 중에 변경사항이 발생하였음에도 이를 반영하여 변경하지 않은 경우", + "사례 3 : 개인정보 처리방침이 공개는 되어 있으나, 명칭이 ʻ개인정보 처리방침ʼ이 아니라 ʻ개인정보 보호정책ʼ으로 되어 있고 글자 크기, 색상 등을 활용하여 정보주체가 쉽게 찾을 수 있도록 되어 있지 않은 경우", + "사례 4 : 개인정보 처리방침이 몇 차례 개정되었으나, 예전에 작성된 개인정보 처리방침의 내용을 확인할 수 있도록 공개되어 있지 않은 경우", + "사례 5 : 전자상거래법, 상법 등 다른 법령에 따라 개인정보를 파기하지 아니하고 일정기간 보관하고 있으나, 이에 따른 보존근거와 보존하는 개인정보 항목을 개인정보 처리방침에 공개하지 않은 경우" + ] + } + ] + }, + { + "Id": "3.5.2", + "Name": "정보주체 권리보장", + "Description": "정보주체가 개인정보의 열람, 정정·삭제, 처리정지, 이의제기, 동의철회 등 요구를 수집 방법·절차보다 쉽게 할 수 있도록 권리행사 방법 및 절차를 수립·이행하고, 정보주체의 요구를 받은 경우 지체 없이 처리하고 관련 기록을 남겨야 한다. 또한 정보주체의 사생활 침해, 명예훼손 등 타인의 권리를 침해하는 정보가 유통되지 않도록 삭제요청, 임시조치 등의 기준을 수립·이행하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.5. 정보주체 권리보호", + "Section": "3.5.2 정보주체 권리보장", + "AuditChecklist": [ + "정보주체 또는 그 대리인이 개인정보에 대한 열람, 정정·삭제, 처리정지 및 동의 철회 등(이하 '열람등요구'라 함)을 개인정보 수집방법·절차보다 어렵지 아니하도록 권리 행사 방법 및 절차를 마련하여 공개하고 있는가?", + "정보주체 또는 그 대리인이 개인정보 열람등요구를 하는 경우 기간 내에 열람등요구에 따른 필요한 조치를 하고 있는가?", + "정보주체 또는 그 대리인이 개인정보 수집·이용·제공 등의 동의를 철회하는 경우 지체 없이 수집된 개인정보를 파기하는 등 필요한 조치를 취하고 있는가?", + "정보주체의 열람등요구에 대한 조치에 불복이 있는 경우 이의를 제기할 수 있도록 필요한 절차를 마련하여 안내하고 있는가?", + "정보주체의 열람등요구 및 처리 결과에 대하여 기록을 남기고 있는가?", + "정보통신망에서 사생활 침해 또는 명예훼손 등 타인의 권리를 침해한 경우 침해를 받은 자가 정보통신서비스 제공자에게 정보의 삭제 요청 등을 할 수 있는 절차를 마련하여 시행하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제34조의2(노출된 개인정보의 삭제·차단), 제35조(개인정보의 열람), 제35조의2(개인정보의 전송 요구), 제36조(개인정보의 정정·삭제), 제37조(개인정보의 처리정지 등), 제37조의2(자동화된 결정에 대한 정보주체의 권리 등), 제38조(권리행사의 방법 및 절차)", + "정보통신망법 제44조(정보통신망에서의 권리보호), 제44조의2(정보의 삭제요청 등), 제44조의3(임의의 임시조치)" + ], + "AuditEvidence": [ + "개인정보 처리방침", + "개인정보 열람등요구 처리 절차, 관련 양식", + "개인정보 열람등요구 시 조치 내역", + "회원 탈퇴 및 동의 철회 절차" + ], + "NonComplianceCases": [ + "사례 1 : 개인정보의 열람, 정정·삭제, 처리정지 요구 방법을 정보주체가 알 수 있도록 공개하지 않은 경우", + "사례 2 : 개인정보의 열람 요구에 대하여 정당한 사유의 통지 없이 열람 요구를 접수받은 날로부터 10일을 초과하여 회신하고 있는 경우", + "사례 3 : 개인정보의 열람 민원에 대한 처리 내역 기록 및 보관이 이루어지지 않은 경우", + "사례 4 : 정보주체 당사자 또는 정당한 대리인이 맞는지에 대한 확인 절차 없이 열람 통지가 이루어지는 경우", + "사례 5 : 개인정보의 정정·삭제 요구에 대하여 정정·삭제 요구를 접수받은 날로부터 10일을 초과하여 회신하는 경우", + "사례 6 : 회원 가입 시에는 온라인을 통하여 쉽게 회원 가입이 가능하였으나, 회원 탈퇴 시에는 신분증 등 추가 서류를 제출하게 하거나 오프라인 방문을 통해서만 가능하도록 하는 경우" + ] + } + ] + }, + { + "Id": "3.5.3", + "Name": "정보주체에 대한 통지", + "Description": "개인정보의 이용·제공 내역 등 정보주체에게 통지하여야 할 사항을 파악하여 그 내용을 주기적으로 통지하여야 한다.", + "Checks": [], + "Attributes": [ + { + "Domain": "3. 개인정보 처리 단계별 요구사항", + "Subdomain": "3.5. 정보주체 권리보호", + "Section": "3.5.3 정보주체에 대한 통지", + "AuditChecklist": [ + "법적 의무 대상자에 해당하는 경우 개인정보 이용·제공 내역 또는 그 내역을 확인할 수 있는 정보시스템에 접속하는 방법을 정보주체에게 주기적으로 통지하고 있는가?", + "개인정보 이용·제공 내역 통지 항목은 법적 요구항목을 모두 포함하고 있는가?" + ], + "RelatedRegulations": [ + "개인정보 보호법 제20조의2(개인정보 이용·제공 내역의 통지)" + ], + "AuditEvidence": [ + "개인정보 이용·제공 내역 통지 기록", + "개인정보 이용·제공 내역 통지 양식 및 문구" + ], + "NonComplianceCases": [ + "사례 1 : 전년도 말 기준 직전 3개월 간 일일 평균 저장·관리하고 있는 개인정보가 100만명 이상으로서 개인정보 이용제공 내역 통지 의무 대상자에 해당 됨에도 불구하고 금년도에 개인정보 이용 및내역을 통지하지 않은 경우", + "사례 2 : 개인정보 이용·제공 내역을 개별 정보주체에게 직접적으로 통지하는 대신 홈페이지에서 단순 팝업창이나 별도 공지사항으로 안내만 한 경우" + ] + } + ] + } + ] +} diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index fd980aa93f..a577f052fc 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -58,6 +58,7 @@ aws: ] # AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries) + # AWS SSM Configuration (aws.ssm_documents_set_as_public) # Single account environment: No action required. The AWS account number will be automatically added by the checks. # Multi account environment: Any additional trusted account number should be added as a space separated list, e.g. # trusted_account_ids : ["123456789012", "098765432109", "678901234567"] @@ -101,6 +102,8 @@ aws: "ruby2.5", "ruby2.7", ] + # aws.awslambda_function_vpc_is_in_multi_azs + lambda_min_azs: 2 # AWS Organizations # aws.organizations_scp_check_deny_regions diff --git a/prowler/lib/check/check.py b/prowler/lib/check/check.py index 32a40349db..da11f231c8 100644 --- a/prowler/lib/check/check.py +++ b/prowler/lib/check/check.py @@ -325,35 +325,6 @@ def import_check(check_path: str) -> ModuleType: return lib -def run_check(check: Check, verbose: bool = False, only_logs: bool = False) -> list: - """ - Run the check and return the findings - Args: - check (Check): check class - output_options (Any): output options - Returns: - list: list of findings - """ - findings = [] - if verbose: - print( - f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}" - ) - logger.debug(f"Executing check: {check.CheckID}") - try: - findings = check.execute() - except Exception as error: - if not only_logs: - print( - f"Something went wrong in {check.CheckID}, please use --log-level ERROR" - ) - logger.error( - f"{check.CheckID} -- {error.__class__.__name__}[{traceback.extract_tb(error.__traceback__)[-1].lineno}]: {error}" - ) - finally: - return findings - - def run_fixer(check_findings: list) -> int: """ Run the fixer for the check if it exists and there are any FAIL findings @@ -435,6 +406,7 @@ def execute_checks( global_provider: Any, custom_checks_metadata: Any, config_file: str, + output_options: Any, ) -> list: # List to store all the check's findings all_findings = [] @@ -470,18 +442,42 @@ def execute_checks( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + # Set verbose flag + verbose = False + if hasattr(output_options, "verbose"): + verbose = output_options.verbose + elif hasattr(output_options, "fixer"): + verbose = output_options.fixer + # Execution with the --only-logs flag - if global_provider.output_options.only_logs: + if output_options.only_logs: for check_name in checks_to_execute: # Recover service from check name service = check_name.split("_")[0] try: + try: + # Import check module + check_module_path = f"prowler.providers.{global_provider.type}.services.{service}.{check_name}.{check_name}" + lib = import_check(check_module_path) + # Recover functions from check + check_to_execute = getattr(lib, check_name) + check = check_to_execute() + except ModuleNotFoundError: + logger.error( + f"Check '{check_name}' was not found for the {global_provider.type.upper()} provider" + ) + continue + if verbose: + print( + f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}" + ) check_findings = execute( - service, - check_name, + check, global_provider, custom_checks_metadata, + output_options, ) + report(check_findings, global_provider, output_options) all_findings.extend(check_findings) # Update Audit Status @@ -539,12 +535,31 @@ def execute_checks( f"-> Scanning {orange_color}{service}{Style.RESET_ALL} service" ) try: + try: + # Import check module + check_module_path = f"prowler.providers.{global_provider.type}.services.{service}.{check_name}.{check_name}" + lib = import_check(check_module_path) + # Recover functions from check + check_to_execute = getattr(lib, check_name) + check = check_to_execute() + except ModuleNotFoundError: + logger.error( + f"Check '{check_name}' was not found for the {global_provider.type.upper()} provider" + ) + continue + if verbose: + print( + f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}" + ) check_findings = execute( - service, - check_name, + check, global_provider, custom_checks_metadata, + output_options, ) + + report(check_findings, global_provider, output_options) + all_findings.extend(check_findings) services_executed.add(service) checks_executed.add(check_name) @@ -567,51 +582,79 @@ def execute_checks( ) bar() bar.title = f"-> {Fore.GREEN}Scan completed!{Style.RESET_ALL}" + + # Custom report interface + if os.environ.get("PROWLER_REPORT_LIB_PATH"): + try: + logger.info("Using custom report interface ...") + lib = os.environ["PROWLER_REPORT_LIB_PATH"] + outputs_module = importlib.import_module(lib) + custom_report_interface = getattr(outputs_module, "report") + + # TODO: review this call and see if we can remove the global_provider.output_options since it is contained in the global_provider + custom_report_interface(check_findings, output_options, global_provider) + except Exception: + sys.exit(1) + return all_findings def execute( - service: str, - check_name: str, + check: Check, global_provider: Any, custom_checks_metadata: Any, + output_options: Any = None, ): - try: - # Import check module - check_module_path = f"prowler.providers.{global_provider.type}.services.{service}.{check_name}.{check_name}" - lib = import_check(check_module_path) - # Recover functions from check - check_to_execute = getattr(lib, check_name) - check_class = check_to_execute() + """ + Execute the check and report the findings + Args: + service (str): service name + check_name (str): check name + global_provider (Any): provider object + custom_checks_metadata (Any): custom checks metadata + output_options (Any): output options, depending on the provider + + Returns: + list: list of findings + """ + try: # Update check metadata to reflect that in the outputs if custom_checks_metadata and custom_checks_metadata["Checks"].get( - check_class.CheckID + check.CheckID ): - check_class = update_check_metadata( - check_class, custom_checks_metadata["Checks"][check_class.CheckID] + check = update_check_metadata( + check, custom_checks_metadata["Checks"][check.CheckID] ) - # Run check - verbose = ( - global_provider.output_options.verbose - or global_provider.output_options.fixer - ) - check_findings = run_check( - check_class, verbose, global_provider.output_options.only_logs - ) + only_logs = False + if hasattr(output_options, "only_logs"): + only_logs = output_options.only_logs + + # Execute the check + check_findings = [] + logger.debug(f"Executing check: {check.CheckID}") + try: + check_findings = check.execute() + except Exception as error: + if not only_logs: + print( + f"Something went wrong in {check.CheckID}, please use --log-level ERROR" + ) + logger.error( + f"{check.CheckID} -- {error.__class__.__name__}[{traceback.extract_tb(error.__traceback__)[-1].lineno}]: {error}" + ) # Exclude findings per status - if global_provider.output_options.status: + if hasattr(output_options, "status") and output_options.status: check_findings = [ finding for finding in check_findings - if finding.status in global_provider.output_options.status + if finding.status in output_options.status ] - # Mutelist findings + # Before returning the findings, we need to apply the mute list logic if hasattr(global_provider, "mutelist") and global_provider.mutelist.mutelist: - # TODO: make this prettier is_finding_muted_args = {} if global_provider.type == "aws": is_finding_muted_args["aws_account_id"] = ( @@ -626,27 +669,9 @@ def execute( **is_finding_muted_args ) - # Refactor(Outputs) - # Report the check's findings - report(check_findings, global_provider) - - # Refactor(Outputs) - if os.environ.get("PROWLER_REPORT_LIB_PATH"): - try: - logger.info("Using custom report interface ...") - lib = os.environ["PROWLER_REPORT_LIB_PATH"] - outputs_module = importlib.import_module(lib) - custom_report_interface = getattr(outputs_module, "report") - - # TODO: review this call and see if we can remove the global_provider.output_options since it is contained in the global_provider - custom_report_interface( - check_findings, global_provider.output_options, global_provider - ) - except Exception: - sys.exit(1) except ModuleNotFoundError: logger.error( - f"Check '{check_name}' was not found for the {global_provider.type.upper()} provider" + f"Check '{check.CheckID}' was not found for the {global_provider.type.upper()} provider" ) check_findings = [] except Exception as error: diff --git a/prowler/lib/check/compliance_models.py b/prowler/lib/check/compliance_models.py index f53e49ab17..45a9b6fc76 100644 --- a/prowler/lib/check/compliance_models.py +++ b/prowler/lib/check/compliance_models.py @@ -169,6 +169,19 @@ class Mitre_Requirement(BaseModel): Checks: list[str] +# KISA-ISMS-P Requirement Attribute +class KISA_ISMSP_Requirement_Attribute(BaseModel): + """KISA ISMS-P Requirement Attribute""" + + Domain: str + Subdomain: str + Section: str + AuditChecklist: Optional[list[str]] + RelatedRegulations: Optional[list[str]] + AuditEvidence: Optional[list[str]] + NonComplianceCases: Optional[list[str]] + + # Base Compliance Model # TODO: move this to compliance folder class Compliance_Requirement(BaseModel): @@ -183,6 +196,7 @@ class Compliance_Requirement(BaseModel): ENS_Requirement_Attribute, ISO27001_2013_Requirement_Attribute, AWS_Well_Architected_Requirement_Attribute, + KISA_ISMSP_Requirement_Attribute, # Generic_Compliance_Requirement_Attribute must be the last one since it is the fallback for generic compliance framework Generic_Compliance_Requirement_Attribute, ] diff --git a/prowler/lib/outputs/asff/asff.py b/prowler/lib/outputs/asff/asff.py index b3ee6f3b2f..7ddff3fe0d 100644 --- a/prowler/lib/outputs/asff/asff.py +++ b/prowler/lib/outputs/asff/asff.py @@ -89,7 +89,11 @@ class ASFF(Output): CreatedAt=timestamp, Severity=Severity(Label=finding.severity.value), Title=finding.check_title, - Description=finding.description, + Description=( + (finding.status_extended[:1000] + "...") + if len(finding.status_extended) > 1000 + else finding.status_extended + ), Resources=[ Resource( Id=finding.resource_uid, diff --git a/prowler/lib/outputs/compliance/compliance.py b/prowler/lib/outputs/compliance/compliance.py index 2d68e85964..b74eafa97f 100644 --- a/prowler/lib/outputs/compliance/compliance.py +++ b/prowler/lib/outputs/compliance/compliance.py @@ -7,6 +7,7 @@ from prowler.lib.outputs.compliance.ens.ens import get_ens_table from prowler.lib.outputs.compliance.generic.generic_table import ( get_generic_compliance_table, ) +from prowler.lib.outputs.compliance.kisa_ismsp.kisa_ismsp import get_kisa_ismsp_table from prowler.lib.outputs.compliance.mitre_attack.mitre_attack import ( get_mitre_attack_table, ) @@ -62,6 +63,15 @@ def display_compliance_table( output_directory, compliance_overview, ) + elif "kisa_isms_" in compliance_framework: + get_kisa_ismsp_table( + findings, + bulk_checks_metadata, + compliance_framework, + output_filename, + output_directory, + compliance_overview, + ) else: get_generic_compliance_table( findings, diff --git a/prowler/providers/aws/lib/policy_condition_parser/__init__.py b/prowler/lib/outputs/compliance/kisa_ismsp/__init__.py similarity index 100% rename from prowler/providers/aws/lib/policy_condition_parser/__init__.py rename to prowler/lib/outputs/compliance/kisa_ismsp/__init__.py diff --git a/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py new file mode 100644 index 0000000000..8bfbff5b54 --- /dev/null +++ b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp.py @@ -0,0 +1,89 @@ +from colorama import Fore, Style +from tabulate import tabulate + +from prowler.config.config import orange_color + + +def get_kisa_ismsp_table( + findings: list, + bulk_checks_metadata: dict, + compliance_framework: str, + output_filename: str, + output_directory: str, + compliance_overview: bool, +): + sections = {} + kisa_ismsp_compliance_table = { + "Provider": [], + "Section": [], + "Status": [], + "Muted": [], + } + pass_count = [] + fail_count = [] + muted_count = [] + for index, finding in enumerate(findings): + check = bulk_checks_metadata[finding.check_metadata.CheckID] + check_compliances = check.Compliance + for compliance in check_compliances: + if ( + compliance.Framework.startswith("KISA") + and compliance.Version in compliance_framework + ): + for requirement in compliance.Requirements: + for attribute in requirement.Attributes: + section = attribute.Section + # Check if Section exists + if section not in sections: + sections[section] = { + "Status": f"{Fore.GREEN}PASS{Style.RESET_ALL}", + "Muted": 0, + } + if finding.muted: + if index not in muted_count: + muted_count.append(index) + sections[section]["Muted"] += 1 + else: + if finding.status == "FAIL" and index not in fail_count: + fail_count.append(index) + elif finding.status == "PASS" and index not in pass_count: + pass_count.append(index) + + # Add results to table + sections = dict(sorted(sections.items())) + for section in sections: + kisa_ismsp_compliance_table["Provider"].append(compliance.Provider) + kisa_ismsp_compliance_table["Section"].append(section) + kisa_ismsp_compliance_table["Muted"].append( + f"{orange_color}{sections[section]['Muted']}{Style.RESET_ALL}" + ) + if len(fail_count) + len(pass_count) + len(muted_count) > 1: + print( + f"\nCompliance Status of {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Framework:" + ) + overview_table = [ + [ + f"{Fore.RED}{round(len(fail_count) / len(findings) * 100, 2)}% ({len(fail_count)}) FAIL{Style.RESET_ALL}", + f"{Fore.GREEN}{round(len(pass_count) / len(findings) * 100, 2)}% ({len(pass_count)}) PASS{Style.RESET_ALL}", + f"{orange_color}{round(len(muted_count) / len(findings) * 100, 2)}% ({len(muted_count)}) MUTED{Style.RESET_ALL}", + ] + ] + print(tabulate(overview_table, tablefmt="rounded_grid")) + if not compliance_overview: + print( + f"\nFramework {Fore.YELLOW}{compliance_framework.upper()}{Style.RESET_ALL} Results:" + ) + print( + tabulate( + kisa_ismsp_compliance_table, + headers="keys", + tablefmt="rounded_grid", + ) + ) + print( + f"{Style.BRIGHT}* Only sections containing results appear.{Style.RESET_ALL}" + ) + print(f"\nDetailed results of {compliance_framework.upper()} are in:") + print( + f" - CSV: {output_directory}/compliance/{output_filename}_{compliance_framework}.csv\n" + ) diff --git a/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py new file mode 100644 index 0000000000..f23aac87b2 --- /dev/null +++ b/prowler/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws.py @@ -0,0 +1,93 @@ +from prowler.lib.check.compliance_models import Compliance +from prowler.lib.outputs.compliance.compliance_output import ComplianceOutput +from prowler.lib.outputs.compliance.kisa_ismsp.models import AWSKISAISMSPModel +from prowler.lib.outputs.finding import Finding + + +class AWSKISAISMSP(ComplianceOutput): + """ + This class represents the AWS KISA-ISMS-P compliance output. + + Attributes: + - _data (list): A list to store transformed data from findings. + - _file_descriptor (TextIOWrapper): A file descriptor to write data to a file. + + Methods: + - transform: Transforms findings into AWS KISA-ISMS-P compliance format. + """ + + def transform( + self, + findings: list[Finding], + compliance: Compliance, + compliance_name: str, + ) -> None: + """ + Transforms a list of findings into AWS KISA-ISMS-P compliance format. + + Parameters: + - findings (list): A list of findings. + - compliance (Compliance): A compliance model. + - compliance_name (str): The name of the compliance model. + + Returns: + - None + """ + for finding in findings: + # Get the compliance requirements for the finding + finding_requirements = finding.compliance.get(compliance_name, []) + for requirement in compliance.Requirements: + if requirement.Id in finding_requirements: + for attribute in requirement.Attributes: + compliance_row = AWSKISAISMSPModel( + Provider=finding.provider, + Description=compliance.Description, + AccountId=finding.account_uid, + Region=finding.region, + AssessmentDate=str(finding.timestamp), + Requirements_Id=requirement.Id, + Requirements_Name=requirement.Name, + Requirements_Description=requirement.Description, + Requirements_Attributes_Domain=attribute.Domain, + Requirements_Attributes_Subdomain=attribute.Subdomain, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_AuditChecklist=attribute.AuditChecklist, + Requirements_Attributes_RelatedRegulations=attribute.RelatedRegulations, + Requirements_Attributes_AuditEvidence=attribute.AuditEvidence, + Requirements_Attributes_NonComplianceCases=attribute.NonComplianceCases, + Status=finding.status, + StatusExtended=finding.status_extended, + ResourceId=finding.resource_uid, + ResourceName=finding.resource_name, + CheckId=finding.check_id, + Muted=finding.muted, + ) + self._data.append(compliance_row) + # Add manual requirements to the compliance output + for requirement in compliance.Requirements: + if not requirement.Checks: + for attribute in requirement.Attributes: + compliance_row = AWSKISAISMSPModel( + Provider=compliance.Provider.lower(), + Description=compliance.Description, + AccountId="", + Region="", + AssessmentDate=str(finding.timestamp), + Requirements_Id=requirement.Id, + Requirements_Name=requirement.Name, + Requirements_Description=requirement.Description, + Requirements_Attributes_Domain=attribute.Domain, + Requirements_Attributes_Subdomain=attribute.Subdomain, + Requirements_Attributes_Section=attribute.Section, + Requirements_Attributes_AuditChecklist=attribute.AuditChecklist, + Requirements_Attributes_RelatedRegulations=attribute.RelatedRegulations, + Requirements_Attributes_AuditEvidence=attribute.AuditEvidence, + Requirements_Attributes_NonComplianceCases=attribute.NonComplianceCases, + Status="MANUAL", + StatusExtended="Manual check", + ResourceId="manual_check", + ResourceName="Manual check", + CheckId="manual", + Muted=False, + ) + self._data.append(compliance_row) diff --git a/prowler/lib/outputs/compliance/kisa_ismsp/models.py b/prowler/lib/outputs/compliance/kisa_ismsp/models.py new file mode 100644 index 0000000000..98b1f00a78 --- /dev/null +++ b/prowler/lib/outputs/compliance/kisa_ismsp/models.py @@ -0,0 +1,31 @@ +from typing import Optional + +from pydantic import BaseModel + + +class AWSKISAISMSPModel(BaseModel): + """ + The AWS KISA-ISMS-P Model outputs findings in a format compliant with the AWS KISA-ISMS-P standard + """ + + Provider: str + Description: str + AccountId: str + Region: str + AssessmentDate: str + Requirements_Id: str + Requirements_Name: str + Requirements_Description: str + Requirements_Attributes_Domain: str + Requirements_Attributes_Subdomain: str + Requirements_Attributes_Section: str + Requirements_Attributes_AuditChecklist: Optional[list[str]] + Requirements_Attributes_RelatedRegulations: Optional[list[str]] + Requirements_Attributes_AuditEvidence: Optional[list[str]] + Requirements_Attributes_NonComplianceCases: Optional[list[str]] + Status: str + StatusExtended: str + ResourceId: str + ResourceName: str + CheckId: str + Muted: bool diff --git a/prowler/lib/outputs/finding.py b/prowler/lib/outputs/finding.py index f7695221c2..aea7c13a59 100644 --- a/prowler/lib/outputs/finding.py +++ b/prowler/lib/outputs/finding.py @@ -88,29 +88,37 @@ class Finding(BaseModel): @classmethod def generate_output( - cls, provider: Provider, check_output: Check_Report + cls, provider: Provider, check_output: Check_Report, output_options ) -> "Finding": """Generates the output for a finding based on the provider and output options Args: provider (Provider): the provider object check_output (Check_Report): the check output object + output_options: the output options object, depending on the provider Returns: finding_output (Finding): the finding output object """ - output_options = provider.output_options # TODO: think about get_provider_data_mapping provider_data_mapping = get_provider_data_mapping(provider) + # TODO: move fill_common_finding_data - common_finding_data = fill_common_finding_data( - check_output, output_options.unix_timestamp - ) + unix_timestamp = False + if hasattr(output_options, "unix_timestamp"): + unix_timestamp = output_options.unix_timestamp + + common_finding_data = fill_common_finding_data(check_output, unix_timestamp) output_data = {} output_data.update(provider_data_mapping) output_data.update(common_finding_data) + + bulk_checks_metadata = {} + if hasattr(output_options, "bulk_checks_metadata"): + bulk_checks_metadata = output_options.bulk_checks_metadata + output_data["compliance"] = get_check_compliance( - check_output, provider.type, output_options.bulk_checks_metadata + check_output, provider.type, bulk_checks_metadata ) try: if provider.type == "aws": diff --git a/prowler/lib/outputs/outputs.py b/prowler/lib/outputs/outputs.py index 0f99ef7de5..d600c851a6 100644 --- a/prowler/lib/outputs/outputs.py +++ b/prowler/lib/outputs/outputs.py @@ -25,10 +25,12 @@ def stdout_report(finding, color, verbose, status, fix): ) -# TODO: Only pass check_findings, provider.output_options and provider.type -def report(check_findings, provider): +# TODO: Only pass check_findings, output_options and provider.type +def report(check_findings, provider, output_options): try: - output_options = provider.output_options + verbose = False + if hasattr(output_options, "verbose"): + verbose = output_options.verbose if check_findings: # TO-DO Generic Function if provider.type == "aws": @@ -39,21 +41,27 @@ def report(check_findings, provider): for finding in check_findings: # Print findings by stdout + status = [] + if hasattr(output_options, "status"): + status = output_options.status + fixer = False + if hasattr(output_options, "fixer"): + fixer = output_options.fixer color = set_report_color(finding.status, finding.muted) stdout_report( finding, color, - output_options.verbose, - output_options.status, - output_options.fixer, + verbose, + status, + fixer, ) else: # No service resources in the whole account color = set_report_color("MANUAL") - if output_options.verbose: + if verbose: print(f"\t{color}INFO{Style.RESET_ALL} There are no resources") # Separator between findings and bar - if output_options.verbose: + if verbose: print() except Exception as error: logger.error( @@ -82,9 +90,14 @@ def extract_findings_statistics(findings: list) -> dict: extract_findings_statistics takes a list of findings and returns the following dict with the aggregated statistics { "total_pass": 0, + "total_muted_pass": 0, "total_fail": 0, + "total_muted_fail": 0, "resources_count": 0, "findings_count": 0, + "critical_failed_findings": [], + "critical_passed_findings": [] + "all_fails_are_muted": False } """ logger.info("Extracting audit statistics...") @@ -96,18 +109,42 @@ def extract_findings_statistics(findings: list) -> dict: resources = set() findings_count = 0 all_fails_are_muted = True + critical_severity_pass = 0 + critical_severity_fail = 0 + high_severity_pass = 0 + high_severity_fail = 0 + medium_severity_pass = 0 + medium_severity_fail = 0 + low_severity_pass = 0 + low_severity_fail = 0 for finding in findings: # Save the resource_id resources.add(finding.resource_id) if finding.status == "PASS": + if finding.check_metadata.Severity == "critical": + critical_severity_pass += 1 + if finding.check_metadata.Severity == "high": + high_severity_pass += 1 + if finding.check_metadata.Severity == "medium": + medium_severity_pass += 1 + if finding.check_metadata.Severity == "low": + low_severity_pass += 1 total_pass += 1 findings_count += 1 if finding.muted is True: muted_pass += 1 if finding.status == "FAIL": + if finding.check_metadata.Severity == "critical": + critical_severity_fail += 1 + if finding.check_metadata.Severity == "high": + high_severity_fail += 1 + if finding.check_metadata.Severity == "medium": + medium_severity_fail += 1 + if finding.check_metadata.Severity == "low": + low_severity_fail += 1 total_fail += 1 findings_count += 1 if finding.muted is True: @@ -121,6 +158,14 @@ def extract_findings_statistics(findings: list) -> dict: stats["total_muted_fail"] = muted_fail stats["resources_count"] = len(resources) stats["findings_count"] = findings_count + stats["total_critical_severity_fail"] = critical_severity_fail + stats["total_critical_severity_pass"] = critical_severity_pass + stats["total_high_severity_fail"] = high_severity_fail + stats["total_high_severity_pass"] = high_severity_pass + stats["total_medium_severity_fail"] = medium_severity_fail + stats["total_medium_severity_pass"] = medium_severity_pass + stats["total_low_severity_fail"] = medium_severity_fail + stats["total_low_severity_pass"] = medium_severity_pass stats["all_fails_are_muted"] = all_fails_are_muted return stats diff --git a/prowler/lib/outputs/slack/slack.py b/prowler/lib/outputs/slack/slack.py index a4d75b42ae..6cb1e8b7e6 100644 --- a/prowler/lib/outputs/slack/slack.py +++ b/prowler/lib/outputs/slack/slack.py @@ -121,6 +121,19 @@ class Slack: "text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100 , 2)}%)\n", }, }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "*Severities:*\n" + f"• *Critical:* {stats['total_critical_severity_pass']} " + f"• *High:* {stats['total_high_severity_pass']} " + f"• *Medium:* {stats['total_medium_severity_pass']} " + f"• *Low:* {stats['total_low_severity_pass']}" + ), + }, + }, { "type": "section", "text": { @@ -128,6 +141,19 @@ class Slack: "text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ", }, }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "*Severities:*\n" + f"• *Critical:* {stats['total_critical_severity_fail']} " + f"• *High:* {stats['total_high_severity_fail']} " + f"• *Medium:* {stats['total_medium_severity_fail']} " + f"• *Low:* {stats['total_low_severity_fail']}" + ), + }, + }, { "type": "section", "text": { diff --git a/prowler/lib/scan/scan.py b/prowler/lib/scan/scan.py index f3f664b987..48cd9335fb 100644 --- a/prowler/lib/scan/scan.py +++ b/prowler/lib/scan/scan.py @@ -1,6 +1,6 @@ from typing import Generator -from prowler.lib.check.check import execute, update_audit_metadata +from prowler.lib.check.check import execute, import_check, update_audit_metadata from prowler.lib.logger import logger from prowler.lib.outputs.finding import Finding from prowler.providers.common.models import Audit_Metadata @@ -99,13 +99,24 @@ class Scan: try: # Recover service from check name service = get_service_name_from_check_name(check_name) - + try: + # Import check module + check_module_path = f"prowler.providers.{self._provider.type}.services.{service}.{check_name}.{check_name}" + lib = import_check(check_module_path) + # Recover functions from check + check_to_execute = getattr(lib, check_name) + check = check_to_execute() + except ModuleNotFoundError: + logger.error( + f"Check '{check_name}' was not found for the {self._provider.type.upper()} provider" + ) + continue # Execute the check check_findings = execute( - service, - check_name, + check, self._provider, custom_checks_metadata, + output_options=None, ) # Store findings @@ -131,7 +142,9 @@ class Scan: ) findings = [ - Finding.generate_output(self._provider, finding) + Finding.generate_output( + self._provider, finding, output_options=None + ) for finding in check_findings ] diff --git a/prowler/providers/aws/aws_provider.py b/prowler/providers/aws/aws_provider.py index 6cce0bf582..83091299a8 100644 --- a/prowler/providers/aws/aws_provider.py +++ b/prowler/providers/aws/aws_provider.py @@ -52,7 +52,6 @@ from prowler.providers.aws.models import ( AWSIdentityInfo, AWSMFAInfo, AWSOrganizationsInfo, - AWSOutputOptions, AWSSession, ) from prowler.providers.common.models import Audit_Metadata, Connection @@ -68,7 +67,6 @@ class AwsProvider(Provider): _audit_config: dict _scan_unused_services: bool = False _enabled_regions: set = set() - _output_options: AWSOutputOptions # TODO: this is not optional, enforce for all providers audit_metadata: Audit_Metadata @@ -270,6 +268,8 @@ class AwsProvider(Provider): # Fixer Config self._fixer_config = fixer_config + Provider.set_global_provider(self) + @property def identity(self): return self._identity @@ -302,17 +302,6 @@ class AwsProvider(Provider): def fixer_config(self): return self._fixer_config - @property - def output_options(self): - return self._output_options - - @output_options.setter - def output_options(self, options: tuple): - arguments, bulk_checks_metadata = options - self._output_options = AWSOutputOptions( - arguments, bulk_checks_metadata, self._identity - ) - @property def mutelist(self) -> AWSMutelist: """ diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 53089bf6d7..07e41483e5 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -174,6 +174,8 @@ "regions": { "aws": [ "ap-south-1", + "ap-southeast-2", + "eu-west-2", "us-east-1", "us-east-2", "us-west-2" @@ -617,6 +619,7 @@ "ap-southeast-3", "ap-southeast-4", "ca-central-1", + "ca-west-1", "eu-central-1", "eu-central-2", "eu-north-1", @@ -1873,6 +1876,7 @@ "eu-central-1", "eu-north-1", "eu-south-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", @@ -2221,27 +2225,6 @@ ] } }, - "codestar": { - "regions": { - "aws": [ - "ap-northeast-1", - "ap-northeast-2", - "ap-southeast-1", - "ap-southeast-2", - "ca-central-1", - "eu-central-1", - "eu-north-1", - "eu-west-1", - "eu-west-2", - "us-east-1", - "us-east-2", - "us-west-1", - "us-west-2" - ], - "aws-cn": [], - "aws-us-gov": [] - } - }, "codestar-connections": { "regions": { "aws": [ @@ -3189,6 +3172,7 @@ "docdb": { "regions": { "aws": [ + "af-south-1", "ap-east-1", "ap-northeast-1", "ap-northeast-2", @@ -3199,6 +3183,7 @@ "ca-central-1", "eu-central-1", "eu-south-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", @@ -3588,6 +3573,7 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", "ca-west-1", "eu-central-1", @@ -4963,15 +4949,6 @@ ] } }, - "honeycode": { - "regions": { - "aws": [ - "us-west-2" - ], - "aws-cn": [], - "aws-us-gov": [] - } - }, "iam": { "regions": { "aws": [ @@ -7226,9 +7203,11 @@ "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-3", "ca-central-1", "eu-central-1", "eu-north-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", @@ -8454,12 +8433,22 @@ "redshift-serverless": { "regions": { "aws": [ + "ap-east-1", + "ap-northeast-1", + "ap-northeast-2", "ap-south-1", + "ap-southeast-1", + "ap-southeast-2", "ap-southeast-3", "ca-central-1", + "eu-central-1", "eu-central-2", + "eu-north-1", "eu-south-2", + "eu-west-1", + "eu-west-2", "eu-west-3", + "il-central-1", "me-central-1", "sa-east-1", "us-east-1", @@ -9470,6 +9459,7 @@ "ap-southeast-2", "ap-southeast-3", "ap-southeast-4", + "ap-southeast-5", "ca-central-1", "ca-west-1", "eu-central-1", @@ -10153,16 +10143,22 @@ "ap-northeast-2", "ap-northeast-3", "ap-south-1", + "ap-south-2", "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", + "ap-southeast-4", "ca-central-1", "eu-central-1", + "eu-central-2", "eu-north-1", "eu-south-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "il-central-1", + "me-central-1", "me-south-1", "sa-east-1", "us-east-1", @@ -10562,12 +10558,16 @@ "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-3", "ca-central-1", "eu-central-1", "eu-north-1", + "eu-south-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "me-central-1", "us-east-1", "us-east-2", "us-west-2" @@ -10969,6 +10969,7 @@ "regions": { "aws": [ "af-south-1", + "ap-east-1", "ap-northeast-1", "ap-northeast-2", "ap-south-1", @@ -10981,6 +10982,7 @@ "eu-west-1", "eu-west-2", "eu-west-3", + "me-south-1", "sa-east-1", "us-east-1", "us-east-2", diff --git a/prowler/providers/aws/lib/policy_condition_parser/policy_condition_parser.py b/prowler/providers/aws/lib/policy_condition_parser/policy_condition_parser.py deleted file mode 100644 index 3d313a1f71..0000000000 --- a/prowler/providers/aws/lib/policy_condition_parser/policy_condition_parser.py +++ /dev/null @@ -1,157 +0,0 @@ -def is_condition_block_restrictive( - condition_statement: dict, - source_account: str, - is_cross_account_allowed=False, -): - """ - is_condition_block_restrictive parses the IAM Condition policy block and, by default, returns True if the source_account passed as argument is within, False if not. - - If argument is_cross_account_allowed is True it tests if the Condition block includes any of the operators allowlisted returning True if does, False if not. - - - @param condition_statement: dict with an IAM Condition block, e.g.: - { - "StringLike": { - "AWS:SourceAccount": 111122223333 - } - } - - @param source_account: str with a 12-digit AWS Account number, e.g.: 111122223333 - - @param is_cross_account_allowed: bool to allow cross-account access, e.g.: True - - """ - is_condition_valid = False - - # The conditions must be defined in lowercase since the context key names are not case-sensitive. - # For example, including the aws:SourceAccount context key is equivalent to testing for AWS:SourceAccount - # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html - valid_condition_options = { - "StringEquals": [ - "aws:sourceaccount", - "aws:sourceowner", - "s3:resourceaccount", - "aws:principalaccount", - "aws:resourceaccount", - "aws:sourcearn", - "aws:sourcevpc", - "aws:sourcevpce", - ], - "StringLike": [ - "aws:sourceaccount", - "aws:sourceowner", - "aws:sourcearn", - "aws:principalarn", - "aws:resourceaccount", - "aws:principalaccount", - "aws:sourcevpc", - "aws:sourcevpce", - ], - "ArnLike": ["aws:sourcearn", "aws:principalarn"], - "ArnEquals": ["aws:sourcearn", "aws:principalarn"], - } - - for condition_operator, condition_operator_key in valid_condition_options.items(): - if condition_operator in condition_statement: - for value in condition_operator_key: - # We need to transform the condition_statement into lowercase - condition_statement[condition_operator] = { - k.lower(): v - for k, v in condition_statement[condition_operator].items() - } - - if value in condition_statement[condition_operator]: - # values are a list - if isinstance( - condition_statement[condition_operator][value], - list, - ): - is_condition_key_restrictive = True - # if cross account is not allowed check for each condition block looking for accounts - # different than default - if not is_cross_account_allowed: - # if there is an arn/account without the source account -> we do not consider it safe - # here by default we assume is true and look for false entries - for item in condition_statement[condition_operator][value]: - if source_account not in item: - is_condition_key_restrictive = False - break - - if is_condition_key_restrictive: - is_condition_valid = True - - # value is a string - elif isinstance( - condition_statement[condition_operator][value], - str, - ): - if is_cross_account_allowed: - is_condition_valid = True - else: - if ( - source_account - in condition_statement[condition_operator][value] - ): - is_condition_valid = True - - return is_condition_valid - - -def is_condition_block_restrictive_organization( - condition_statement: dict, -): - """ - is_condition_block_restrictive_organization parses the IAM Condition policy block and returns True if the condition_statement is restrictive for the organization, False if not. - - @param condition_statement: dict with an IAM Condition block, e.g.: - { - "StringLike": { - "AWS:PrincipalOrgID": "o-111122223333" - } - } - - """ - is_condition_valid = False - - # The conditions must be defined in lowercase since the context key names are not case-sensitive. - # For example, including the aws:PrincipalOrgID context key is equivalent to testing for AWS:PrincipalOrgID - # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html - valid_condition_options = { - "StringEquals": [ - "aws:principalorgid", - ], - "StringLike": [ - "aws:principalorgid", - ], - } - - for condition_operator, condition_operator_key in valid_condition_options.items(): - if condition_operator in condition_statement: - for value in condition_operator_key: - # We need to transform the condition_statement into lowercase - condition_statement[condition_operator] = { - k.lower(): v - for k, v in condition_statement[condition_operator].items() - } - - if value in condition_statement[condition_operator]: - # values are a list - if isinstance( - condition_statement[condition_operator][value], - list, - ): - is_condition_valid = True - for item in condition_statement[condition_operator][value]: - if item == "*": - is_condition_valid = False - break - - # value is a string - elif isinstance( - condition_statement[condition_operator][value], - str, - ): - if "*" not in condition_statement[condition_operator][value]: - is_condition_valid = True - - return is_condition_valid diff --git a/prowler/providers/aws/services/apigateway/apigateway_restapi_client_certificate_enabled/apigateway_restapi_client_certificate_enabled.metadata.json b/prowler/providers/aws/services/apigateway/apigateway_restapi_client_certificate_enabled/apigateway_restapi_client_certificate_enabled.metadata.json index 77c5734ff8..36f095f0e8 100644 --- a/prowler/providers/aws/services/apigateway/apigateway_restapi_client_certificate_enabled/apigateway_restapi_client_certificate_enabled.metadata.json +++ b/prowler/providers/aws/services/apigateway/apigateway_restapi_client_certificate_enabled/apigateway_restapi_client_certificate_enabled.metadata.json @@ -12,7 +12,7 @@ "SubServiceName": "rest_api", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsApiGatewayStage", + "ResourceType": "AwsApiGatewayRestApi", "Description": "Check if API Gateway Stage has client certificate enabled to access your backend endpoint.", "Risk": "Possible man in the middle attacks and other similar risks.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/apigateway/apigateway_restapi_logging_enabled/apigateway_restapi_logging_enabled.metadata.json b/prowler/providers/aws/services/apigateway/apigateway_restapi_logging_enabled/apigateway_restapi_logging_enabled.metadata.json index accc6102e0..1d79e8c083 100644 --- a/prowler/providers/aws/services/apigateway/apigateway_restapi_logging_enabled/apigateway_restapi_logging_enabled.metadata.json +++ b/prowler/providers/aws/services/apigateway/apigateway_restapi_logging_enabled/apigateway_restapi_logging_enabled.metadata.json @@ -12,7 +12,7 @@ "SubServiceName": "rest_api", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsApiGatewayStage", + "ResourceType": "AwsApiGatewayRestApi", "Description": "Check if API Gateway Stage has logging enabled.", "Risk": "If not enabled, monitoring of service use is not possible. Real-time monitoring of API calls can be achieved by directing CloudTrail Logs to CloudWatch Logs and establishing corresponding metric filters and alarms.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/apigateway/apigateway_restapi_public/apigateway_restapi_public.py b/prowler/providers/aws/services/apigateway/apigateway_restapi_public/apigateway_restapi_public.py index a640653d31..22cfeca49d 100644 --- a/prowler/providers/aws/services/apigateway/apigateway_restapi_public/apigateway_restapi_public.py +++ b/prowler/providers/aws/services/apigateway/apigateway_restapi_public/apigateway_restapi_public.py @@ -15,7 +15,7 @@ class apigateway_restapi_public(Check): report.resource_tags = rest_api.tags if rest_api.public_endpoint: report.status = "FAIL" - report.status_extended = f"API Gateway {rest_api.name} ID {rest_api.id} is internet accesible." + report.status_extended = f"API Gateway {rest_api.name} ID {rest_api.id} is internet accessible." else: report.status = "PASS" report.status_extended = ( diff --git a/prowler/providers/aws/services/apigateway/apigateway_restapi_waf_acl_attached/apigateway_restapi_waf_acl_attached.metadata.json b/prowler/providers/aws/services/apigateway/apigateway_restapi_waf_acl_attached/apigateway_restapi_waf_acl_attached.metadata.json index 62ba94c68a..ba097a6df1 100644 --- a/prowler/providers/aws/services/apigateway/apigateway_restapi_waf_acl_attached/apigateway_restapi_waf_acl_attached.metadata.json +++ b/prowler/providers/aws/services/apigateway/apigateway_restapi_waf_acl_attached/apigateway_restapi_waf_acl_attached.metadata.json @@ -12,7 +12,7 @@ "SubServiceName": "rest_api", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsApiGatewayStage", + "ResourceType": "AwsApiGatewayRestApi", "Description": "Check if API Gateway Stage has a WAF ACL attached.", "Risk": "Potential attacks and / or abuse of service, more even for even for internet reachable services.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/appstream/appstream_fleet_default_internet_access_disabled/appstream_fleet_default_internet_access_disabled.metadata.json b/prowler/providers/aws/services/appstream/appstream_fleet_default_internet_access_disabled/appstream_fleet_default_internet_access_disabled.metadata.json index ab72954b83..8e4c94a601 100644 --- a/prowler/providers/aws/services/appstream/appstream_fleet_default_internet_access_disabled/appstream_fleet_default_internet_access_disabled.metadata.json +++ b/prowler/providers/aws/services/appstream/appstream_fleet_default_internet_access_disabled/appstream_fleet_default_internet_access_disabled.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:appstream:region:account-id:fleet/resource-id", "Severity": "medium", - "ResourceType": "AppStream", + "ResourceType": "Other", "Description": "Ensure default Internet Access from your Amazon AppStream fleet streaming instances should remain unchecked.", "Risk": "Default Internet Access from your fleet streaming instances should be controlled using a NAT gateway in the VPC.", "RelatedUrl": "https://docs.aws.amazon.com/appstream2/latest/developerguide/set-up-stacks-fleets.html", diff --git a/prowler/providers/aws/services/appstream/appstream_fleet_maximum_session_duration/appstream_fleet_maximum_session_duration.metadata.json b/prowler/providers/aws/services/appstream/appstream_fleet_maximum_session_duration/appstream_fleet_maximum_session_duration.metadata.json index 0057d829c6..bb623a8dd8 100644 --- a/prowler/providers/aws/services/appstream/appstream_fleet_maximum_session_duration/appstream_fleet_maximum_session_duration.metadata.json +++ b/prowler/providers/aws/services/appstream/appstream_fleet_maximum_session_duration/appstream_fleet_maximum_session_duration.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:appstream:region:account-id:fleet/resource-id", "Severity": "medium", - "ResourceType": "AppStream", + "ResourceType": "Other", "Description": "Ensure user maximum session duration is no longer than 10 hours.", "Risk": "Having a session duration lasting longer than 10 hours should not be necessary and if running for any malicious reasons provides a greater time for usage than should be allowed.", "RelatedUrl": "https://docs.aws.amazon.com/appstream2/latest/developerguide/set-up-stacks-fleets.html", diff --git a/prowler/providers/aws/services/appstream/appstream_fleet_session_disconnect_timeout/appstream_fleet_session_disconnect_timeout.metadata.json b/prowler/providers/aws/services/appstream/appstream_fleet_session_disconnect_timeout/appstream_fleet_session_disconnect_timeout.metadata.json index 58d6856939..b73618e6a7 100644 --- a/prowler/providers/aws/services/appstream/appstream_fleet_session_disconnect_timeout/appstream_fleet_session_disconnect_timeout.metadata.json +++ b/prowler/providers/aws/services/appstream/appstream_fleet_session_disconnect_timeout/appstream_fleet_session_disconnect_timeout.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:appstream:region:account-id:fleet/resource-id", "Severity": "medium", - "ResourceType": "AppStream", + "ResourceType": "Other", "Description": "Ensure session disconnect timeout is set to 5 minutes or less", "Risk": "Disconnect timeout in minutes, is the amount of of time that a streaming session remains active after users disconnect.", "RelatedUrl": "https://docs.aws.amazon.com/appstream2/latest/developerguide/set-up-stacks-fleets.html", diff --git a/prowler/providers/aws/services/appstream/appstream_fleet_session_idle_disconnect_timeout/appstream_fleet_session_idle_disconnect_timeout.metadata.json b/prowler/providers/aws/services/appstream/appstream_fleet_session_idle_disconnect_timeout/appstream_fleet_session_idle_disconnect_timeout.metadata.json index cbe7cf1d39..d8a5b4935a 100644 --- a/prowler/providers/aws/services/appstream/appstream_fleet_session_idle_disconnect_timeout/appstream_fleet_session_idle_disconnect_timeout.metadata.json +++ b/prowler/providers/aws/services/appstream/appstream_fleet_session_idle_disconnect_timeout/appstream_fleet_session_idle_disconnect_timeout.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:appstream:region:account-id:fleet/resource-id", "Severity": "medium", - "ResourceType": "AppStream", + "ResourceType": "Other", "Description": "Ensure session idle disconnect timeout is set to 10 minutes or less.", "Risk": "Idle disconnect timeout in minutes is the amount of time that users can be inactive before they are disconnected from their streaming session and the Disconnect timeout in minutes time begins.", "RelatedUrl": "https://docs.aws.amazon.com/appstream2/latest/developerguide/set-up-stacks-fleets.html", diff --git a/prowler/providers/aws/services/athena/athena_workgroup_encryption/athena_workgroup_encryption.metadata.json b/prowler/providers/aws/services/athena/athena_workgroup_encryption/athena_workgroup_encryption.metadata.json index 197ce59b67..80f55ec0ba 100644 --- a/prowler/providers/aws/services/athena/athena_workgroup_encryption/athena_workgroup_encryption.metadata.json +++ b/prowler/providers/aws/services/athena/athena_workgroup_encryption/athena_workgroup_encryption.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:athena:region:account-id:workgroup/resource-id", "Severity": "medium", - "ResourceType": "WorkGroup", + "ResourceType": "AwsAthenaWorkGroup", "Description": "Ensure that encryption at rest is enabled for Amazon Athena query results stored in Amazon S3 in order to secure data and meet compliance requirements for data-at-rest encryption.", "Risk": "If not enabled sensitive information at rest is not protected.", "RelatedUrl": "https://docs.aws.amazon.com/athena/latest/ug/encryption.html", diff --git a/prowler/providers/aws/services/athena/athena_workgroup_enforce_configuration/athena_workgroup_enforce_configuration.metadata.json b/prowler/providers/aws/services/athena/athena_workgroup_enforce_configuration/athena_workgroup_enforce_configuration.metadata.json index 46f2e305f2..0888fe3d22 100644 --- a/prowler/providers/aws/services/athena/athena_workgroup_enforce_configuration/athena_workgroup_enforce_configuration.metadata.json +++ b/prowler/providers/aws/services/athena/athena_workgroup_enforce_configuration/athena_workgroup_enforce_configuration.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:athena:region:account-id:workgroup/resource-id", "Severity": "medium", - "ResourceType": "WorkGroup", + "ResourceType": "AwsAthenaWorkGroup", "Description": "Ensure that workgroup configuration is enforced so it cannot be overriden by client-side settings.", "Risk": "If workgroup configuration is not enforced security settings like encryption can be overriden by client-side settings.", "RelatedUrl": "https://docs.aws.amazon.com/athena/latest/ug/workgroups-settings-override.html", diff --git a/prowler/providers/aws/services/autoscaling/autoscaling_find_secrets_ec2_launch_configuration/autoscaling_find_secrets_ec2_launch_configuration.metadata.json b/prowler/providers/aws/services/autoscaling/autoscaling_find_secrets_ec2_launch_configuration/autoscaling_find_secrets_ec2_launch_configuration.metadata.json index b2a6140728..b98d761e49 100644 --- a/prowler/providers/aws/services/autoscaling/autoscaling_find_secrets_ec2_launch_configuration/autoscaling_find_secrets_ec2_launch_configuration.metadata.json +++ b/prowler/providers/aws/services/autoscaling/autoscaling_find_secrets_ec2_launch_configuration/autoscaling_find_secrets_ec2_launch_configuration.metadata.json @@ -1,7 +1,7 @@ { "Provider": "aws", "CheckID": "autoscaling_find_secrets_ec2_launch_configuration", - "CheckTitle": "Find secrets in EC2 Auto Scaling Launch Configuration", + "CheckTitle": "[DEPRECATED] Find secrets in EC2 Auto Scaling Launch Configuration", "CheckType": [ "IAM" ], @@ -9,8 +9,8 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:autoscaling:region:account-id:autoScalingGroupName/resource-name", "Severity": "critical", - "ResourceType": "Other", - "Description": "Find secrets in EC2 Auto Scaling Launch Configuration", + "ResourceType": "AwsAutoScalingLaunchConfiguration", + "Description": "[DEPRECATED] Find secrets in EC2 Auto Scaling Launch Configuration", "Risk": "The use of a hard-coded password increases the possibility of password guessing. If hard-coded passwords are used, it is possible that malicious users gain access through the account in question.", "RelatedUrl": "", "Remediation": { diff --git a/prowler/providers/aws/services/autoscaling/autoscaling_group_multiple_az/autoscaling_group_multiple_az.metadata.json b/prowler/providers/aws/services/autoscaling/autoscaling_group_multiple_az/autoscaling_group_multiple_az.metadata.json index cdd1b58fd7..1b04b2463a 100644 --- a/prowler/providers/aws/services/autoscaling/autoscaling_group_multiple_az/autoscaling_group_multiple_az.metadata.json +++ b/prowler/providers/aws/services/autoscaling/autoscaling_group_multiple_az/autoscaling_group_multiple_az.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:autoscaling:region:account-id:autoScalingGroupName/resource-name", "Severity": "medium", - "ResourceType": "Other", + "ResourceType": "AwsAutoScalingAutoScalingGroup", "Description": "EC2 Auto Scaling Group should use multiple Availability Zones", "Risk": "In case of a failure in a single Availability Zone, the Auto Scaling Group will not be able to launch new instances to replace the failed ones.", "RelatedUrl": "https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-add-availability-zone.html", diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_inside_vpc/awslambda_function_inside_vpc.py b/prowler/providers/aws/services/awslambda/awslambda_function_inside_vpc/awslambda_function_inside_vpc.py index c48e7d4d50..30beadcbc5 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_inside_vpc/awslambda_function_inside_vpc.py +++ b/prowler/providers/aws/services/awslambda/awslambda_function_inside_vpc/awslambda_function_inside_vpc.py @@ -13,14 +13,20 @@ class awslambda_function_inside_vpc(Check): report.resource_id = function.name report.resource_arn = function_arn report.resource_tags = function.tags - report.status = "FAIL" + report.status = "PASS" report.status_extended = ( - f"Lambda function {function.name} is not inside a VPC" + f"Lambda function {function.name} is inside of VPC {function.vpc_id}" ) - if function.vpc_id: - report.status = "PASS" - report.status_extended = f"Lambda function {function.name} is inside of VPC {function.vpc_id}" + if not function.vpc_id: + awslambda_client.set_failed_check( + self.__class__.__name__, + function_arn, + ) + report.status = "FAIL" + report.status_extended = ( + f"Lambda function {function.name} is not inside a VPC" + ) findings.append(report) diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled.py b/prowler/providers/aws/services/awslambda/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled.py index 22ee6aaa31..f5806a6463 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled.py +++ b/prowler/providers/aws/services/awslambda/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled/awslambda_function_invoke_api_operations_cloudtrail_logging_enabled.py @@ -28,7 +28,8 @@ class awslambda_function_invoke_api_operations_cloudtrail_logging_enabled(Check) for resource in data_event.event_selector["DataResources"]: if resource["Type"] == "AWS::Lambda::Function" and ( function.arn in resource["Values"] - or "arn:aws:lambda" in resource["Values"] + or f"arn:{awslambda_client.audited_partition}:lambda" + in resource["Values"] ): lambda_recorded_cloudtrail = True break diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py b/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py index 20c6526b92..778c44be93 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py +++ b/prowler/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible.py @@ -1,5 +1,6 @@ from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.awslambda.awslambda_client import awslambda_client +from prowler.providers.aws.services.iam.lib.policy import is_policy_public class awslambda_function_not_publicly_accessible(Check): @@ -14,37 +15,11 @@ class awslambda_function_not_publicly_accessible(Check): report.status = "PASS" report.status_extended = f"Lambda function {function.name} has a policy resource-based policy not public." - - public_access = False - if function.policy: - for statement in function.policy["Statement"]: - # Only check allow statements - if statement["Effect"] == "Allow" and ( - "*" in statement["Principal"] - or ( - isinstance(statement["Principal"], dict) - and ( - "*" in statement["Principal"].get("AWS", "") - or "*" - in statement["Principal"].get("CanonicalUser", "") - or ( # Check if function can be invoked by other AWS services - ( - ".amazonaws.com" - in statement["Principal"].get("Service", "") - ) - and ( - "*" in statement.get("Action", "") - or "InvokeFunction" - in statement.get("Action", "") - ) - ) - ) - ) - ): - public_access = True - break - - if public_access: + if is_policy_public( + function.policy, + awslambda_client.audited_account, + is_cross_account_allowed=True, + ): report.status = "FAIL" report.status_extended = f"Lambda function {function.name} has a policy resource-based policy with public access." diff --git a/prowler/providers/aws/services/efs/lib/__init__.py b/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/__init__.py similarity index 100% rename from prowler/providers/aws/services/efs/lib/__init__.py rename to prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/__init__.py diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.metadata.json b/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.metadata.json new file mode 100644 index 0000000000..312023e4da --- /dev/null +++ b/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "awslambda_function_vpc_multi_az", + "CheckTitle": "Check if AWS Lambda Function VPC is deployed Across Multiple Availability Zones", + "CheckType": [], + "ServiceName": "lambda", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:lambda:region:account-id:function/function-name", + "Severity": "medium", + "ResourceType": "AwsLambdaFunction", + "Description": "This control checks whether an AWS Lambda function connected to a VPC operates in at least the specified number of Availability Zones (AZs). A failure occurs if the function does not operate in the required number of AZs, which by default is two.", + "Risk": "A Lambda function not deployed across multiple AZs increases the risk of a single point of failure, which can result in a complete disruption of the function's operations if an AZ becomes unavailable.", + "RelatedUrl": "https://docs.aws.amazon.com/lambda/latest/operatorguide/networking-vpc.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/lambda-controls.html#lambda-5", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that your AWS Lambda functions connected to a VPC are distributed across multiple Availability Zones (AZs) to enhance availability and resilience.", + "Url": "https://docs.aws.amazon.com/lambda/latest/dg/configuration-vpc.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.py b/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.py new file mode 100644 index 0000000000..26fd6e40cd --- /dev/null +++ b/prowler/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az.py @@ -0,0 +1,48 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.awslambda.awslambda_client import awslambda_client +from prowler.providers.aws.services.awslambda.awslambda_function_inside_vpc.awslambda_function_inside_vpc import ( + awslambda_function_inside_vpc, +) +from prowler.providers.aws.services.vpc.vpc_client import vpc_client + + +class awslambda_function_vpc_multi_az(Check): + def execute(self) -> list[Check_Report_AWS]: + findings = [] + LAMBDA_MIN_AZS = awslambda_client.audit_config.get("lambda_min_azs", 2) + for function_arn, function in awslambda_client.functions.items(): + # only proceed if check "awslambda_function_inside_vpc" did not run or did not FAIL to avoid to report that the function is not inside a VPC twice + if not awslambda_client.is_failed_check( + awslambda_function_inside_vpc.__name__, + function_arn, + ): + report = Check_Report_AWS(self.metadata()) + report.region = function.region + report.resource_id = function.name + report.resource_arn = function_arn + report.resource_tags = function.tags + report.status = "FAIL" + report.status_extended = ( + f"Lambda function {function.name} is not inside a VPC." + ) + + if function.vpc_id: + function_availability_zones = { + getattr( + vpc_client.vpc_subnets.get(subnet_id), + "availability_zone", + None, + ) + for subnet_id in function.subnet_ids + if subnet_id in vpc_client.vpc_subnets + } + + if len(function_availability_zones) >= LAMBDA_MIN_AZS: + report.status = "PASS" + report.status_extended = f"Lambda function {function.name} is inside of VPC {function.vpc_id} that spans in at least {LAMBDA_MIN_AZS} AZs: {', '.join(function_availability_zones)}." + else: + report.status_extended = f"Lambda function {function.name} is inside of VPC {function.vpc_id} that spans only in {len(function_availability_zones)} AZs: {', '.join(function_availability_zones)}. Must span in at least {LAMBDA_MIN_AZS} AZs." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/awslambda/awslambda_service.py b/prowler/providers/aws/services/awslambda/awslambda_service.py index 0033c929f2..32150b7313 100644 --- a/prowler/providers/aws/services/awslambda/awslambda_service.py +++ b/prowler/providers/aws/services/awslambda/awslambda_service.py @@ -44,6 +44,7 @@ class Lambda(AWSService): arn=lambda_arn, security_groups=vpc_config.get("SecurityGroupIds", []), vpc_id=vpc_config.get("VpcId"), + subnet_ids=set(vpc_config.get("SubnetIds", [])), region=regional_client.region, ) if "Runtime" in function: @@ -202,4 +203,5 @@ class Function(BaseModel): code: LambdaCode = None url_config: URLConfig = None vpc_id: Optional[str] + subnet_ids: Optional[set] tags: Optional[list] = [] diff --git a/prowler/providers/aws/services/backup/backup_plans_exist/backup_plans_exist.py b/prowler/providers/aws/services/backup/backup_plans_exist/backup_plans_exist.py index 3823b95a59..4309985757 100644 --- a/prowler/providers/aws/services/backup/backup_plans_exist/backup_plans_exist.py +++ b/prowler/providers/aws/services/backup/backup_plans_exist/backup_plans_exist.py @@ -12,6 +12,7 @@ class backup_plans_exist(Check): report.resource_arn = backup_client.backup_plans[0].arn report.resource_id = backup_client.backup_plans[0].name report.region = backup_client.backup_plans[0].region + report.resource_tags = backup_client.backup_plans[0].tags findings.append(report) elif backup_client.backup_vaults: report = Check_Report_AWS(self.metadata()) @@ -20,5 +21,6 @@ class backup_plans_exist(Check): report.resource_arn = backup_client.backup_plan_arn_template report.resource_id = backup_client.audited_account report.region = backup_client.region + report.resource_tags = [] findings.append(report) return findings diff --git a/prowler/providers/aws/services/backup/backup_reportplans_exist/backup_reportplans_exist.metadata.json b/prowler/providers/aws/services/backup/backup_reportplans_exist/backup_reportplans_exist.metadata.json index 0e84b55f3a..c600682e1d 100644 --- a/prowler/providers/aws/services/backup/backup_reportplans_exist/backup_reportplans_exist.metadata.json +++ b/prowler/providers/aws/services/backup/backup_reportplans_exist/backup_reportplans_exist.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:backup-report-plan:backup-report-plan-id", "Severity": "low", - "ResourceType": "Other", + "ResourceType": "AwsBackupBackupPlan", "Description": "This check ensures that there is at least one backup report plan in place.", "Risk": "Without a backup report plan, an organization may lack visibility into the success or failure of backup operations.", "RelatedUrl": "https://docs.aws.amazon.com/aws-backup/latest/devguide/create-report-plan-console.html", diff --git a/prowler/providers/aws/services/backup/backup_service.py b/prowler/providers/aws/services/backup/backup_service.py index 712c75e611..ee18e91944 100644 --- a/prowler/providers/aws/services/backup/backup_service.py +++ b/prowler/providers/aws/services/backup/backup_service.py @@ -9,7 +9,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## Backup class Backup(AWSService): def __init__(self, provider): # Call AWSService's __init__ @@ -19,12 +18,14 @@ class Backup(AWSService): self.backup_vault_arn_template = f"arn:{self.audited_partition}:backup:{self.region}:{self.audited_account}:backup-vault" self.backup_vaults = [] self.__threading_call__(self._list_backup_vaults) + self.__threading_call__(self._list_tags, self.backup_vaults) self.backup_plans = [] self.__threading_call__(self._list_backup_plans) + self.__threading_call__(self._list_tags, self.backup_plans) self.backup_report_plans = [] self.__threading_call__(self._list_backup_report_plans) - self.protected_resources = {} - self.__threading_call__(self._list_protected_resources) + self.protected_resources = [] + self.__threading_call__(self._list_backup_selections) def _list_backup_vaults(self, regional_client): logger.info("Backup - Listing Backup Vaults...") @@ -140,33 +141,43 @@ class Backup(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - def _list_protected_resources(self, regional_client): - logger.info("Backup - Listing Protected Resources...") - + def _list_backup_selections(self, regional_client): + logger.info("Backup - Listing Backup Selections...") try: - list_protected_resources_paginator = regional_client.get_paginator( - "list_protected_resources" + for backup_plan in self.backup_plans: + paginator = regional_client.get_paginator("list_backup_selections") + for page in paginator.paginate(BackupPlanId=backup_plan.id): + for selection in page.get("BackupSelectionsList", []): + selection_id = selection.get("SelectionId") + if selection_id: + backup_selection = regional_client.get_backup_selection( + BackupPlanId=backup_plan.id, SelectionId=selection_id + )["BackupSelection"] + + self.protected_resources.extend( + backup_selection.get("Resources", []) + ) + + except ClientError as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - for page in list_protected_resources_paginator.paginate(): - for resource in page.get("Results", []): - arn = resource.get("ResourceArn", "") - if not self.audit_resources or ( - is_resource_filtered( - arn, - self.audit_resources, - ) - ): - self.protected_resources[arn] = ProtectedResource( - arn=arn, - resource_type=resource.get("ResourceType"), - region=regional_client.region, - last_backup_time=resource.get("LastBackupTime"), - ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _list_tags(self, resource): + try: + tags = self.regional_clients[resource.region].list_tags( + ResourceArn=resource.arn + )["Tags"] + resource.tags = [tags] if tags else [] + except Exception as error: + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class BackupVault(BaseModel): arn: str @@ -177,6 +188,7 @@ class BackupVault(BaseModel): locked: bool min_retention_days: int = None max_retention_days: int = None + tags: Optional[list] class BackupPlan(BaseModel): @@ -187,6 +199,7 @@ class BackupPlan(BaseModel): version_id: str last_execution_date: Optional[datetime] advanced_settings: list + tags: Optional[list] class BackupReportPlan(BaseModel): @@ -195,10 +208,3 @@ class BackupReportPlan(BaseModel): name: str last_attempted_execution_date: Optional[datetime] last_successful_execution_date: Optional[datetime] - - -class ProtectedResource(BaseModel): - arn: str - resource_type: str - region: str - last_backup_time: Optional[datetime] diff --git a/prowler/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted.py b/prowler/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted.py index c9f909170d..d53d8e00a0 100644 --- a/prowler/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted.py +++ b/prowler/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted.py @@ -16,6 +16,7 @@ class backup_vaults_encrypted(Check): report.resource_arn = backup_vault.arn report.resource_id = backup_vault.name report.region = backup_vault.region + report.resource_tags = backup_vault.tags # if it is encrypted we only change the status and the status extended if backup_vault.encryption: report.status = "PASS" diff --git a/prowler/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist.py b/prowler/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist.py index c05e9c5ab1..8a59c70dbc 100644 --- a/prowler/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist.py +++ b/prowler/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist.py @@ -12,12 +12,14 @@ class backup_vaults_exist(Check): report.resource_arn = backup_client.backup_vault_arn_template report.resource_id = backup_client.audited_account report.region = backup_client.region + report.resource_tags = [] if backup_client.backup_vaults: report.status = "PASS" report.status_extended = f"At least one backup vault exists: {backup_client.backup_vaults[0].name}." report.resource_arn = backup_client.backup_vaults[0].arn report.resource_id = backup_client.backup_vaults[0].name report.region = backup_client.backup_vaults[0].region + report.resource_tags = backup_client.backup_vaults[0].tags findings.append(report) return findings diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/__init__.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate.metadata.json b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate.metadata.json new file mode 100644 index 0000000000..9ef0cfca0a --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "cloudfront_distributions_custom_ssl_certificate", + "CheckTitle": "CloudFront distributions should use custom SSL/TLS certificates.", + "CheckType": [], + "ServiceName": "cloudfront", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "medium", + "ResourceType": "AwsCloudFrontDistribution", + "Description": "Ensure that your Amazon CloudFront distributions are configured to use a custom SSL/TLS certificate instead of the default one.", + "Risk": "Using the default SSL/TLS certificate provided by CloudFront can limit your ability to use custom domain names and may not align with your organization's security policies or branding requirements.", + "RelatedUrl": "https://aws.amazon.com/what-is/ssl-certificate/", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "https://docs.prowler.com/checks/aws/networking-policies/ensure-aws-cloudfront-distribution-uses-custom-ssl-certificate/", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/cloudfront-controls.html#cloudfront-7", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/cloudfront-distro-custom-tls.html" + }, + "Recommendation": { + "Text": "Configure your CloudFront distributions to use a custom SSL/TLS certificate to enable secure access via your own domain names and meet specific security and branding needs. This allows for more control over encryption and authentication settings.", + "Url": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#CreatingCNAME" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate.py new file mode 100644 index 0000000000..e7ddaa8b98 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.cloudfront.cloudfront_client import ( + cloudfront_client, +) + + +class cloudfront_distributions_custom_ssl_certificate(Check): + def execute(self): + findings = [] + for distribution in cloudfront_client.distributions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = distribution.region + report.resource_arn = distribution.arn + report.resource_id = distribution.id + report.resource_tags = distribution.tags + report.status = "PASS" + report.status_extended = f"CloudFront Distribution {distribution.id} is using a custom SSL/TLS certificate." + + if distribution.default_certificate: + report.status = "FAIL" + report.status_extended = f"CloudFront Distribution {distribution.id} is using the default SSL/TLS certificate." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/__init__.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.metadata.json b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.metadata.json new file mode 100644 index 0000000000..d8247d0ff6 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "cloudfront_distributions_default_root_object", + "CheckTitle": "Check if CloudFront distributions have a default root object.", + "CheckType": [], + "ServiceName": "cloudfront", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:cloudfront:region:account-id:distribution/resource-id", + "Severity": "high", + "ResourceType": "AwsCloudFrontDistribution", + "Description": "Check if CloudFront distributions have a default root object.", + "Risk": "Without a default root object, requests to the root URL may result in an error or expose unintended content, leading to potential security risks and a poor user experience.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DefaultRootObject.html#DefaultRootObjectHow", + "Remediation": { + "Code": { + "CLI": "aws cloudfront update-distribution --id --default-root-object ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/cloudfront-controls.html#cloudfront-1", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/cloudfront-default-object.html" + }, + "Recommendation": { + "Text": "Configure a default root object for your CloudFront distribution to ensure that a specific file (such as index.html) is returned when users access the root URL. This improves user experience and ensures that sensitive content isn't accidentally exposed.", + "Url": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/DefaultRootObject.html#DefaultRootObjectHowToDefine" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.py new file mode 100644 index 0000000000..1931625808 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object.py @@ -0,0 +1,26 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.cloudfront.cloudfront_client import ( + cloudfront_client, +) + + +class cloudfront_distributions_default_root_object(Check): + def execute(self): + findings = [] + for distribution in cloudfront_client.distributions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = distribution.region + report.resource_arn = distribution.arn + report.resource_id = distribution.id + report.resource_tags = distribution.tags + + if distribution.default_root_object: + report.status = "PASS" + report.status_extended = f"CloudFront Distribution {distribution.id} does have a default root object ({distribution.default_root_object}) configured." + else: + report.status = "FAIL" + report.status_extended = f"CloudFront Distribution {distribution.id} does not have a default root object configured." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/__init__.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled.metadata.json b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled.metadata.json new file mode 100644 index 0000000000..6a4deed4cc --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "cloudfront_distributions_https_sni_enabled", + "CheckTitle": "Check if CloudFront distributions are using SNI to serve HTTPS requests.", + "CheckType": [], + "ServiceName": "cloudfront", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:cloudfront:region:account-id:distribution/resource-id", + "Severity": "low", + "ResourceType": "AwsCloudFrontDistribution", + "Description": "Check if CloudFront distributions are using SNI to serve HTTPS requests.", + "Risk": "If SNI is not used, CloudFront will allocate a dedicated IP address for each SSL certificate, leading to higher costs and inefficient IP address utilization. This could also complicate scaling and managing multiple distributions, especially if your domain requires multiple SSL certificates.", + "RelatedUrl": "https://www.cloudflare.com/es-es/learning/ssl/what-is-sni/", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/cloudfront-controls.html#cloudfront-8", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/cloudfront-sni.html" + }, + "Recommendation": { + "Text": "Ensure that your CloudFront distributions are configured to use Server Name Indication (SNI) when serving HTTPS requests with custom SSL/TLS certificates. This is the recommended approach for reducing costs and optimizing IP address usage.", + "Url": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cnames-https-dedicated-ip-or-sni.html#cnames-https-sni" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled.py new file mode 100644 index 0000000000..0c323812de --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled.py @@ -0,0 +1,30 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.cloudfront.cloudfront_client import ( + cloudfront_client, +) +from prowler.providers.aws.services.cloudfront.cloudfront_service import ( + SSLSupportMethod, +) + + +class cloudfront_distributions_https_sni_enabled(Check): + def execute(self): + findings = [] + for distribution in cloudfront_client.distributions.values(): + if distribution.certificate: + report = Check_Report_AWS(self.metadata()) + report.region = distribution.region + report.resource_arn = distribution.arn + report.resource_id = distribution.id + report.resource_tags = distribution.tags + + if distribution.ssl_support_method == SSLSupportMethod.sni_only: + report.status = "PASS" + report.status_extended = f"CloudFront Distribution {distribution.id} is serving HTTPS requests using SNI." + else: + report.status = "FAIL" + report.status_extended = f"CloudFront Distribution {distribution.id} is not serving HTTPS requests using SNI." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/__init__.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured.metadata.json b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured.metadata.json new file mode 100644 index 0000000000..2d5e83b1e8 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured.metadata.json @@ -0,0 +1,36 @@ +{ + "Provider": "aws", + "CheckID": "cloudfront_distributions_multiple_origin_failover_configured", + "CheckTitle": "Check if CloudFront distributions have origin failover enabled.", + "CheckType": [ + "Software and Configuration Checks", + "Industry and Regulatory Standards", + "NIST 800-53 Controls" + ], + "ServiceName": "cloudfront", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:cloudfront:region:account-id:distribution/resource-id", + "Severity": "low", + "ResourceType": "AWSCloudFrontDistribution", + "Description": "Check if CloudFront distributions have origin failover enabled.", + "Risk": "Without origin failover, if the primary origin becomes unavailable, your CloudFront distribution may experience downtime, leading to potential service interruptions and a poor user experience.", + "RelatedUrl": "https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_OriginGroup.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/cloudfront-controls.html#cloudfront-4", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/origin-failover-enabled.html" + }, + "Recommendation": { + "Text": "Configure origin failover in your CloudFront distribution by setting up an origin group with at least two origins to enhance availability and ensure traffic is redirected if the primary origin fails.", + "Url": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/high_availability_origin_failover.html#concept_origin_groups.creating" + } + }, + "Categories": [ + "redundancy" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured.py new file mode 100644 index 0000000000..1426a62508 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.cloudfront.cloudfront_client import ( + cloudfront_client, +) + + +class cloudfront_distributions_multiple_origin_failover_configured(Check): + def execute(self): + findings = [] + for distribution in cloudfront_client.distributions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = distribution.region + report.resource_arn = distribution.arn + report.resource_id = distribution.id + report.resource_tags = distribution.tags + report.status = "FAIL" + report.status_extended = f"CloudFront Distribution {distribution.id} does not have an origin group configured with at least 2 origins." + + if distribution.origin_failover: + report.status = "PASS" + report.status_extended = f"CloudFront Distribution {distribution.id} has an origin group with at least 2 origins configured." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/__init__.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.metadata.json b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.metadata.json new file mode 100644 index 0000000000..8864cd74e2 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "cloudfront_distributions_origin_traffic_encrypted", + "CheckTitle": "Check if CloudFront distributions encrypt traffic to custom origins.", + "CheckType": [], + "ServiceName": "cloudfront", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:cloudfront:region:account-id:distribution/resource-id", + "Severity": "medium", + "ResourceType": "AWSCloudFrontDistribution", + "Description": "Check if CloudFront distributions encrypt traffic to custom origins.", + "Risk": "Allowing unencrypted HTTP traffic between CloudFront and custom origins can expose data to potential eavesdropping and manipulation, compromising data security and integrity.", + "RelatedUrl": "https://docs.aws.amazon.com/whitepapers/latest/secure-content-delivery-amazon-cloudfront/custom-origin-with-cloudfront.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/cloudfront-controls.html#cloudfront-9", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/cloudfront-traffic-to-origin-unencrypted.html" + }, + "Recommendation": { + "Text": "Configure your CloudFront distributions to require HTTPS (TLS) for traffic to custom origins, ensuring all data transmitted between CloudFront and the origin is encrypted and protected from unauthorized access.", + "Url": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-https-cloudfront-to-custom-origin.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.py new file mode 100644 index 0000000000..887237b4fb --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted.py @@ -0,0 +1,36 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.cloudfront.cloudfront_client import ( + cloudfront_client, +) + + +class cloudfront_distributions_origin_traffic_encrypted(Check): + def execute(self): + findings = [] + for distribution in cloudfront_client.distributions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = distribution.region + report.resource_arn = distribution.arn + report.resource_id = distribution.id + report.resource_tags = distribution.tags + report.status = "PASS" + report.status_extended = f"CloudFront Distribution {distribution.id} does encrypt traffic to custom origins." + unencrypted_origins = [] + + for origin in distribution.origins: + if ( + origin.origin_protocol_policy == "" + or origin.origin_protocol_policy == "http-only" + ) or ( + origin.origin_protocol_policy == "match-viewer" + and distribution.viewer_protocol_policy == "allow-all" + ): + unencrypted_origins.append(origin.id) + + if unencrypted_origins: + report.status = "FAIL" + report.status_extended = f"CloudFront Distribution {distribution.id} does not encrypt traffic to custom origins {', '.join(unencrypted_origins)}." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/__init__.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control.metadata.json b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control.metadata.json new file mode 100644 index 0000000000..8cd0c296b6 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "cloudfront_distributions_s3_origin_access_control", + "CheckTitle": "Check if CloudFront distributions with S3 origin use OAC.", + "CheckType": [ + "Data Exposure" + ], + "ServiceName": "cloudfront", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:cloudfront:region:account-id:distribution/resource-id", + "Severity": "medium", + "ResourceType": "AWSCloudFrontDistribution", + "Description": "Check if CloudFront distributions use origin access control.", + "Risk": "Without OAC, your S3 bucket could be accessed directly, bypassing CloudFront, which could expose your content to unauthorized access. Additionally, relying on Origin Access Identity (OAI) may limit functionality and security features, making your distribution less secure and more difficult to manage.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html#migrate-from-oai-to-oac", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "https://docs.prowler.com/checks/aws/iam-policies/ensure-aws-cloudfromt-distribution-with-s3-have-origin-access-set-to-enabled/", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/cloudfront-controls.html#cloudfront-13", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/s3-origin.html" + }, + "Recommendation": { + "Text": "Configure Origin Access Control (OAC) for CloudFront distributions that use an Amazon S3 origin. This will ensure that the content in your S3 bucket is accessible only through the specified CloudFront distribution, enhancing security by preventing direct access to the bucket.", + "Url": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control.py new file mode 100644 index 0000000000..8d93868b43 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control.py @@ -0,0 +1,35 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.cloudfront.cloudfront_client import ( + cloudfront_client, +) + + +class cloudfront_distributions_s3_origin_access_control(Check): + def execute(self): + findings = [] + for distribution in cloudfront_client.distributions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = distribution.region + report.resource_arn = distribution.arn + report.resource_id = distribution.id + report.resource_tags = distribution.tags + + if any(origin.s3_origin_config for origin in distribution.origins): + s3_buckets_with_no_oac = [] + report.status = "PASS" + report.status_extended = f"CloudFront Distribution {distribution.id} is using origin access control (OAC) for S3 origins." + + for origin in distribution.origins: + if ( + origin.s3_origin_config != {} + and origin.origin_access_control == "" + ): + s3_buckets_with_no_oac.append(origin.id) + + if s3_buckets_with_no_oac: + report.status = "FAIL" + report.status_extended = f"CloudFront Distribution {distribution.id} is not using origin access control (OAC) in S3 origins {', '.join(s3_buckets_with_no_oac)}." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/__init__.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.metadata.json b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.metadata.json new file mode 100644 index 0000000000..5910db0594 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "cloudfront_distributions_s3_origin_non_existent_bucket", + "CheckTitle": "CloudFront distributions should not point to non-existent S3 origins.", + "CheckType": [ + "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls" + ], + "ServiceName": "cloudfront", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:cloudfront:region:account-id:distribution/resource-id", + "Severity": "high", + "ResourceType": "AwsCloudFrontDistribution", + "Description": "This control checks whether Amazon CloudFront distributions are pointing to non-existent Amazon S3 origins. The control fails if the origin is configured to point to a non-existent bucket.", + "Risk": "Pointing a CloudFront distribution to a non-existent S3 bucket can allow malicious actors to create the bucket and potentially serve unauthorized content through your distribution, leading to security and integrity issues.", + "RelatedUrl": "https://docs.aws.amazon.com/whitepapers/latest/secure-content-delivery-amazon-cloudfront/s3-origin-with-cloudfront.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/cloudfront-controls.html#cloudfront-12", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/CloudFront/cloudfront-existing-s3-bucket.html" + }, + "Recommendation": { + "Text": "Verify that all CloudFront distributions are configured to point to valid, existing S3 buckets. Update the origin settings as needed to ensure that your distributions are linked to appropriate and secure origins.", + "Url": "https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/HowToUpdateDistribution.html" + } + }, + "Categories": [ + "trustboundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.py b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.py new file mode 100644 index 0000000000..1ba7fc41b1 --- /dev/null +++ b/prowler/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket.py @@ -0,0 +1,32 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.cloudfront.cloudfront_client import ( + cloudfront_client, +) +from prowler.providers.aws.services.s3.s3_client import s3_client + + +class cloudfront_distributions_s3_origin_non_existent_bucket(Check): + def execute(self): + findings = [] + for distribution in cloudfront_client.distributions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = distribution.region + report.resource_arn = distribution.arn + report.resource_id = distribution.id + report.resource_tags = distribution.tags + report.status = "PASS" + report.status_extended = f"CloudFront Distribution {distribution.id} does not have non-existent S3 buckets as origins." + non_existent_buckets = [] + + for origin in distribution.origins: + bucket_name = origin.domain_name.split(".")[0] + if not s3_client._head_bucket(bucket_name): + non_existent_buckets.append(bucket_name) + + if non_existent_buckets: + report.status = "FAIL" + report.status_extended = f"CloudFront Distribution {distribution.id} has non-existent S3 buckets as origins: {','.join(non_existent_buckets)}." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/cloudfront/cloudfront_service.py b/prowler/providers/aws/services/cloudfront/cloudfront_service.py index de11b3cb17..f6d8f63365 100644 --- a/prowler/providers/aws/services/cloudfront/cloudfront_service.py +++ b/prowler/providers/aws/services/cloudfront/cloudfront_service.py @@ -8,7 +8,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## CloudFront class CloudFront(AWSService): def __init__(self, provider): # Call AWSService's __init__ @@ -30,6 +29,24 @@ class CloudFront(AWSService): ): distribution_id = item["Id"] distribution_arn = item["ARN"] + origin_groups = item.get("OriginGroups", {}).get( + "Items", [] + ) + origin_failover = all( + origin_group.get("Members", {}).get("Quantity", 0) >= 2 + for origin_group in origin_groups + ) + default_certificate = item["ViewerCertificate"][ + "CloudFrontDefaultCertificate" + ] + certificate = item["ViewerCertificate"].get( + "Certificate", "" + ) + ssl_support_method = SSLSupportMethod( + item["ViewerCertificate"].get( + "SSLSupportMethod", "static-ip" + ) + ) origins = [] for origin in item.get("Origins", {}).get("Items", []): origins.append( @@ -44,6 +61,12 @@ class CloudFront(AWSService): ) .get("OriginSslProtocols", {}) .get("Items", []), + origin_access_control=origin.get( + "OriginAccessControlId", "" + ), + s3_origin_config=origin.get( + "S3OriginConfig", {} + ), ) ) distribution = Distribution( @@ -51,6 +74,10 @@ class CloudFront(AWSService): id=distribution_id, origins=origins, region=region, + origin_failover=origin_failover, + ssl_support_method=ssl_support_method, + default_certificate=default_certificate, + certificate=certificate, ) self.distributions[distribution_id] = distribution @@ -64,6 +91,7 @@ class CloudFront(AWSService): try: for distribution_id in distributions.keys(): distribution_config = client.get_distribution_config(Id=distribution_id) + # Global Config distributions[distribution_id].logging_enabled = distribution_config[ "DistributionConfig" @@ -78,6 +106,16 @@ class CloudFront(AWSService): distributions[distribution_id].web_acl_id = distribution_config[ "DistributionConfig" ]["WebACLId"] + distributions[distribution_id].default_root_object = ( + distribution_config["DistributionConfig"].get( + "DefaultRootObject", "" + ) + ) + distributions[distribution_id].viewer_protocol_policy = ( + distribution_config["DistributionConfig"][ + "DefaultCacheBehavior" + ].get("ViewerProtocolPolicy", "") + ) # Default Cache Config default_cache_config = DefaultCacheConfigBehaviour( @@ -139,6 +177,14 @@ class GeoRestrictionType(Enum): whitelist = "whitelist" +class SSLSupportMethod(Enum): + """Method types that viewer want to accept HTTPS requests from""" + + static_ip = "static-ip" + sni_only = "sni-only" + vip = "vip" + + class DefaultCacheConfigBehaviour(BaseModel): realtime_log_config_arn: Optional[str] viewer_protocol_policy: ViewerProtocolPolicy @@ -150,6 +196,8 @@ class Origin(BaseModel): domain_name: str origin_protocol_policy: str origin_ssl_protocols: list[str] + origin_access_control: Optional[str] + s3_origin_config: Optional[dict] class Distribution(BaseModel): @@ -163,4 +211,10 @@ class Distribution(BaseModel): geo_restriction_type: Optional[GeoRestrictionType] origins: list[Origin] web_acl_id: str = "" + default_certificate: Optional[bool] + default_root_object: Optional[str] + viewer_protocol_policy: Optional[str] tags: Optional[list] = [] + origin_failover: Optional[bool] + ssl_support_method: Optional[SSLSupportMethod] + certificate: Optional[str] diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_s3_dataevents_read_enabled/cloudtrail_s3_dataevents_read_enabled.metadata.json b/prowler/providers/aws/services/cloudtrail/cloudtrail_s3_dataevents_read_enabled/cloudtrail_s3_dataevents_read_enabled.metadata.json index e8c213ca80..3e3f074c5f 100644 --- a/prowler/providers/aws/services/cloudtrail/cloudtrail_s3_dataevents_read_enabled/cloudtrail_s3_dataevents_read_enabled.metadata.json +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_s3_dataevents_read_enabled/cloudtrail_s3_dataevents_read_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "low", - "ResourceType": "AwsS3Bucket", + "ResourceType": "AwsCloudTrailTrail", "Description": "Ensure that all your AWS CloudTrail trails are configured to log Data events in order to record S3 object-level API operations, such as GetObject, DeleteObject and PutObject, for individual S3 buckets or for all current and future S3 buckets provisioned in your AWS account.", "Risk": "If logs are not enabled, monitoring of service use and threat analysis is not possible.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_s3_dataevents_write_enabled/cloudtrail_s3_dataevents_write_enabled.metadata.json b/prowler/providers/aws/services/cloudtrail/cloudtrail_s3_dataevents_write_enabled/cloudtrail_s3_dataevents_write_enabled.metadata.json index 122bad81f4..42c2801cc4 100644 --- a/prowler/providers/aws/services/cloudtrail/cloudtrail_s3_dataevents_write_enabled/cloudtrail_s3_dataevents_write_enabled.metadata.json +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_s3_dataevents_write_enabled/cloudtrail_s3_dataevents_write_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "low", - "ResourceType": "AwsS3Bucket", + "ResourceType": "AwsCloudTrailTrail", "Description": "Ensure that all your AWS CloudTrail trails are configured to log Data events in order to record S3 object-level API operations, such as GetObject, DeleteObject and PutObject, for individual S3 buckets or for all current and future S3 buckets provisioned in your AWS account.", "Risk": "If logs are not enabled, monitoring of service use and threat analysis is not possible.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py index 82eddb6372..fe6e965be7 100644 --- a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration.py @@ -39,14 +39,27 @@ class cloudtrail_threat_detection_enumeration(Check): minutes=threat_detection_minutes, ): event_log = json.loads(event_log["CloudTrailEvent"]) - if ".amazonaws.com" not in event_log["sourceIPAddress"]: - if event_log["sourceIPAddress"] not in potential_enumeration: - potential_enumeration[event_log["sourceIPAddress"]] = set() - potential_enumeration[event_log["sourceIPAddress"]].add( - event_name + if ( + event_log["userIdentity"]["arn"], + event_log["userIdentity"]["type"], + ) not in potential_enumeration: + potential_enumeration[ + ( + event_log["userIdentity"]["arn"], + event_log["userIdentity"]["type"], + ) + ] = set() + potential_enumeration[ + ( + event_log["userIdentity"]["arn"], + event_log["userIdentity"]["type"], ) - for source_ip, actions in potential_enumeration.items(): - ip_threshold = round(len(actions) / len(enumeration_actions), 2) + ].add(event_name) + + for aws_identity, actions in potential_enumeration.items(): + identity_threshold = round(len(actions) / len(enumeration_actions), 2) + aws_identity_type = aws_identity[1] + aws_identity_arn = aws_identity[0] if len(actions) / len(enumeration_actions) > threshold: found_potential_enumeration = True report = Check_Report_AWS(self.metadata()) @@ -56,7 +69,7 @@ class cloudtrail_threat_detection_enumeration(Check): cloudtrail_client.region ) report.status = "FAIL" - report.status_extended = f"Potential enumeration attack detected from source IP {source_ip} with an threshold of {ip_threshold}." + report.status_extended = f"Potential enumeration attack detected from AWS {aws_identity_type} {aws_identity_arn.split('/')[-1]} with an threshold of {identity_threshold}." findings.append(report) if not found_potential_enumeration: report = Check_Report_AWS(self.metadata()) diff --git a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation.py b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation.py index 1af9f0f1bc..b295534f90 100644 --- a/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation.py +++ b/prowler/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation.py @@ -40,19 +40,28 @@ class cloudtrail_threat_detection_privilege_escalation(Check): minutes=threat_detection_minutes, ): event_log = json.loads(event_log["CloudTrailEvent"]) - if ".amazonaws.com" not in event_log["sourceIPAddress"]: - if ( - event_log["sourceIPAddress"] - not in potential_privilege_escalation - ): - potential_privilege_escalation[ - event_log["sourceIPAddress"] - ] = set() + if ( + event_log["userIdentity"]["arn"], + event_log["userIdentity"]["type"], + ) not in potential_privilege_escalation: potential_privilege_escalation[ - event_log["sourceIPAddress"] - ].add(event_name) - for source_ip, actions in potential_privilege_escalation.items(): - ip_threshold = round(len(actions) / len(privilege_escalation_actions), 2) + ( + event_log["userIdentity"]["arn"], + event_log["userIdentity"]["type"], + ) + ] = set() + potential_privilege_escalation[ + ( + event_log["userIdentity"]["arn"], + event_log["userIdentity"]["type"], + ) + ].add(event_name) + for aws_identity, actions in potential_privilege_escalation.items(): + identity_threshold = round( + len(actions) / len(privilege_escalation_actions), 2 + ) + aws_identity_type = aws_identity[1] + aws_identity_arn = aws_identity[0] if len(actions) / len(privilege_escalation_actions) > threshold: found_potential_privilege_escalation = True report = Check_Report_AWS(self.metadata()) @@ -62,7 +71,7 @@ class cloudtrail_threat_detection_privilege_escalation(Check): cloudtrail_client.region ) report.status = "FAIL" - report.status_extended = f"Potential privilege escalation attack detected from source IP {source_ip} with an threshold of {ip_threshold}." + report.status_extended = f"Potential privilege escalation attack detected from AWS {aws_identity_type} {aws_identity_arn.split('/')[-1]} with an threshold of {identity_threshold}." findings.append(report) if not found_potential_privilege_escalation: report = Check_Report_AWS(self.metadata()) diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_acls_alarm_configured/cloudwatch_changes_to_network_acls_alarm_configured.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_acls_alarm_configured/cloudwatch_changes_to_network_acls_alarm_configured.metadata.json index d88b650108..c1aa9d9ef1 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_acls_alarm_configured/cloudwatch_changes_to_network_acls_alarm_configured.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_acls_alarm_configured/cloudwatch_changes_to_network_acls_alarm_configured.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for changes to Network Access Control Lists (NACL).", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_gateways_alarm_configured/cloudwatch_changes_to_network_gateways_alarm_configured.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_gateways_alarm_configured/cloudwatch_changes_to_network_gateways_alarm_configured.metadata.json index 51d7c54e96..8b7a76ab68 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_gateways_alarm_configured/cloudwatch_changes_to_network_gateways_alarm_configured.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_gateways_alarm_configured/cloudwatch_changes_to_network_gateways_alarm_configured.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for changes to network gateways.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_route_tables_alarm_configured/cloudwatch_changes_to_network_route_tables_alarm_configured.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_route_tables_alarm_configured/cloudwatch_changes_to_network_route_tables_alarm_configured.metadata.json index 0430c46cd5..61994d1564 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_route_tables_alarm_configured/cloudwatch_changes_to_network_route_tables_alarm_configured.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_network_route_tables_alarm_configured/cloudwatch_changes_to_network_route_tables_alarm_configured.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Real-time monitoring of API calls can be achieved by directing Cloud Trail Logs to CloudWatch Logs, or an external Security information and event management (SIEM)environment, and establishing corresponding metric filters and alarms. Routing tablesare used to route network traffic between subnets and to network gateways. It isrecommended that a metric filter and alarm be established for changes to route tables.", "Risk": "CloudWatch is an AWS native service that allows you to ob serve and monitor resources and applications. CloudTrail Logs can also be sent to an external Security informationand event management (SIEM) environment for monitoring and alerting.Monitoring changes to route tables will help ensure that all VPC traffic flows through anexpected path and prevent any accidental or intentional modifications that may lead touncontrolled network traffic. An alarm should be triggered every time an AWS API call isperformed to create, replace, delete, or disassociate a Route Table.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_vpcs_alarm_configured/cloudwatch_changes_to_vpcs_alarm_configured.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_vpcs_alarm_configured/cloudwatch_changes_to_vpcs_alarm_configured.metadata.json index d280c58eed..c0ccb4d86d 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_vpcs_alarm_configured/cloudwatch_changes_to_vpcs_alarm_configured.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_changes_to_vpcs_alarm_configured/cloudwatch_changes_to_vpcs_alarm_configured.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for VPC changes.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_cross_account_sharing_disabled/cloudwatch_cross_account_sharing_disabled.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_cross_account_sharing_disabled/cloudwatch_cross_account_sharing_disabled.metadata.json index 6a0a5215ca..a6a9e1839c 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_cross_account_sharing_disabled/cloudwatch_cross_account_sharing_disabled.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_cross_account_sharing_disabled/cloudwatch_cross_account_sharing_disabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudWatch", + "ResourceType": "AwsAccount", "Description": "Check if CloudWatch has allowed cross-account sharing.", "Risk": "Cross-Account access to CloudWatch could increase the risk of compromising information between accounts.", "RelatedUrl": "https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Cross-Account-Cross-Region.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_group_kms_encryption_enabled/cloudwatch_log_group_kms_encryption_enabled.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_group_kms_encryption_enabled/cloudwatch_log_group_kms_encryption_enabled.metadata.json index dcfff3163a..c4a6601f6f 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_group_kms_encryption_enabled/cloudwatch_log_group_kms_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_group_kms_encryption_enabled/cloudwatch_log_group_kms_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "logs", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsLogsLogGroup", + "ResourceType": "Other", "Description": "Check if CloudWatch log groups are protected by AWS KMS.", "Risk": "Using customer managed KMS to encrypt CloudWatch log group provide additional confidentiality and control over the log data.", "RelatedUrl": "https://docs.aws.amazon.com/cli/latest/reference/logs/associate-kms-key.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_group_no_secrets_in_logs/cloudwatch_log_group_no_secrets_in_logs.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_group_no_secrets_in_logs/cloudwatch_log_group_no_secrets_in_logs.metadata.json index 6536ad95c4..087b16a89f 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_group_no_secrets_in_logs/cloudwatch_log_group_no_secrets_in_logs.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_group_no_secrets_in_logs/cloudwatch_log_group_no_secrets_in_logs.metadata.json @@ -10,7 +10,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:log-group/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailLogGroup", + "ResourceType": "Other", "Description": "Check if secrets exists in CloudWatch logs", "Risk": "Storing sensitive data in CloudWatch logs could allow an attacker with read-only access to escalate their privileges or gain unauthorised access to systems.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled/cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled/cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled.metadata.json index 78dc472dfb..51df80cbed 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled/cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled/cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for AWS Config configuration changes.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled/cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled/cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled.metadata.json index 902f1454d3..a960fc4cb6 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled/cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled/cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for CloudTrail configuration changes.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_authentication_failures/cloudwatch_log_metric_filter_authentication_failures.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_authentication_failures/cloudwatch_log_metric_filter_authentication_failures.metadata.json index 5926234887..44cd9074c5 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_authentication_failures/cloudwatch_log_metric_filter_authentication_failures.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_authentication_failures/cloudwatch_log_metric_filter_authentication_failures.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for AWS Management Console authentication failures.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_aws_organizations_changes/cloudwatch_log_metric_filter_aws_organizations_changes.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_aws_organizations_changes/cloudwatch_log_metric_filter_aws_organizations_changes.metadata.json index c53926e924..bc182ce688 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_aws_organizations_changes/cloudwatch_log_metric_filter_aws_organizations_changes.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_aws_organizations_changes/cloudwatch_log_metric_filter_aws_organizations_changes.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for AWS Organizations changes.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk/cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk/cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk.metadata.json index b7b749897e..482d15b56c 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk/cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk/cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for disabling or scheduled deletion of customer created KMS CMKs.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_for_s3_bucket_policy_changes/cloudwatch_log_metric_filter_for_s3_bucket_policy_changes.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_for_s3_bucket_policy_changes/cloudwatch_log_metric_filter_for_s3_bucket_policy_changes.metadata.json index 01ec5890ac..764e31cf6e 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_for_s3_bucket_policy_changes/cloudwatch_log_metric_filter_for_s3_bucket_policy_changes.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_for_s3_bucket_policy_changes/cloudwatch_log_metric_filter_for_s3_bucket_policy_changes.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for S3 bucket policy changes.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_policy_changes/cloudwatch_log_metric_filter_policy_changes.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_policy_changes/cloudwatch_log_metric_filter_policy_changes.metadata.json index a8adb8ae6a..36c4d42234 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_policy_changes/cloudwatch_log_metric_filter_policy_changes.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_policy_changes/cloudwatch_log_metric_filter_policy_changes.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for IAM policy changes.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_root_usage/cloudwatch_log_metric_filter_root_usage.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_root_usage/cloudwatch_log_metric_filter_root_usage.metadata.json index 6596d01a6e..fed23762ed 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_root_usage/cloudwatch_log_metric_filter_root_usage.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_root_usage/cloudwatch_log_metric_filter_root_usage.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for usage of root account.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_security_group_changes/cloudwatch_log_metric_filter_security_group_changes.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_security_group_changes/cloudwatch_log_metric_filter_security_group_changes.metadata.json index 5195617961..6041c493ce 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_security_group_changes/cloudwatch_log_metric_filter_security_group_changes.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_security_group_changes/cloudwatch_log_metric_filter_security_group_changes.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for security group changes.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_sign_in_without_mfa/cloudwatch_log_metric_filter_sign_in_without_mfa.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_sign_in_without_mfa/cloudwatch_log_metric_filter_sign_in_without_mfa.metadata.json index 2f3c4459f9..1d605dde4b 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_sign_in_without_mfa/cloudwatch_log_metric_filter_sign_in_without_mfa.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_sign_in_without_mfa/cloudwatch_log_metric_filter_sign_in_without_mfa.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for Management Console sign-in without MFA.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_unauthorized_api_calls/cloudwatch_log_metric_filter_unauthorized_api_calls.metadata.json b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_unauthorized_api_calls/cloudwatch_log_metric_filter_unauthorized_api_calls.metadata.json index 63ca78288f..c143c09770 100644 --- a/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_unauthorized_api_calls/cloudwatch_log_metric_filter_unauthorized_api_calls.metadata.json +++ b/prowler/providers/aws/services/cloudwatch/cloudwatch_log_metric_filter_unauthorized_api_calls/cloudwatch_log_metric_filter_unauthorized_api_calls.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:cloudwatch:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsCloudTrailTrail", + "ResourceType": "AwsCloudWatchAlarm", "Description": "Ensure a log metric filter and alarm exist for unauthorized API calls.", "Risk": "Monitoring unauthorized API calls will help reveal application errors and may reduce time to detect malicious activity.", "RelatedUrl": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudwatch-alarms-for-cloudtrail.html", diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_no_secrets_in_variables/codebuild_project_no_secrets_in_variables.py b/prowler/providers/aws/services/codebuild/codebuild_project_no_secrets_in_variables/codebuild_project_no_secrets_in_variables.py index a8057fcd17..d21dc660b5 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_project_no_secrets_in_variables/codebuild_project_no_secrets_in_variables.py +++ b/prowler/providers/aws/services/codebuild/codebuild_project_no_secrets_in_variables/codebuild_project_no_secrets_in_variables.py @@ -19,6 +19,7 @@ class codebuild_project_no_secrets_in_variables(Check): report.region = project.region report.resource_id = project.name report.resource_arn = project.arn + report.resource_tags = project.tags report.status = "PASS" report.status_extended = f"CodeBuild project {project.name} does not have sensitive environment plaintext credentials." secrets_found = [] diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days.py b/prowler/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days.py index bc730a61bf..91567cb2cc 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days.py +++ b/prowler/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days.py @@ -12,6 +12,7 @@ class codebuild_project_older_90_days(Check): report.region = project.region report.resource_id = project.name report.resource_arn = project.arn + report.resource_tags = project.tags report.status = "PASS" report.status_extended = f"CodeBuild project {project.name} has been invoked in the last 90 days." if project.last_invoked_time: diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.py b/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.py index 4473279a4f..efd98f8bc1 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.py +++ b/prowler/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials.py @@ -14,6 +14,7 @@ class codebuild_project_source_repo_url_no_sensitive_credentials(Check): report.region = project.region report.resource_id = project.name report.resource_arn = project.arn + report.resource_tags = project.tags report.status = "PASS" report.status_extended = f"CodeBuild project {project.name} does not contain sensitive credentials in any source repository URLs." secrets_found = [] diff --git a/prowler/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec.py b/prowler/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec.py index d41d76641e..b687de137f 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec.py +++ b/prowler/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec.py @@ -12,6 +12,7 @@ class codebuild_project_user_controlled_buildspec(Check): report.region = project.region report.resource_id = project.name report.resource_arn = project.arn + report.resource_tags = project.tags report.status = "PASS" report.status_extended = f"CodeBuild project {project.name} does not use an user controlled buildspec." if project.buildspec: diff --git a/prowler/providers/aws/services/codebuild/codebuild_service.py b/prowler/providers/aws/services/codebuild/codebuild_service.py index 541293f6fb..5704078fdd 100644 --- a/prowler/providers/aws/services/codebuild/codebuild_service.py +++ b/prowler/providers/aws/services/codebuild/codebuild_service.py @@ -93,6 +93,7 @@ class Codebuild(AWSService): EnvironmentVariable(**var) for var in env_vars ] project.buildspec = project_info.get("source", {}).get("buildspec", "") + project.tags = project_info.get("tags", []) except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -124,3 +125,4 @@ class Project(BaseModel): source: Optional[Source] secondary_sources: Optional[list[Source]] = [] environment_variables: Optional[List[EnvironmentVariable]] + tags: Optional[list] diff --git a/prowler/providers/aws/services/cognito/cognito_identity_pool_guest_access_disabled/cognito_identity_pool_guest_access_disabled.metadata.json b/prowler/providers/aws/services/cognito/cognito_identity_pool_guest_access_disabled/cognito_identity_pool_guest_access_disabled.metadata.json index 097def74ee..382ceaf26f 100644 --- a/prowler/providers/aws/services/cognito/cognito_identity_pool_guest_access_disabled/cognito_identity_pool_guest_access_disabled.metadata.json +++ b/prowler/providers/aws/services/cognito/cognito_identity_pool_guest_access_disabled/cognito_identity_pool_guest_access_disabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:cognito-idp:region:account:identitypool/identitypool-id", "Severity": "medium", - "ResourceType": "AwsCognitoIdentityPool", + "ResourceType": "Other", "Description": "Guest access allows unauthenticated users to access your identity pool. This is useful for public websites that allow users to sign in with a social identity provider, but it can also be a security risk. If you don't need guest access, you should disable it.", "Risk": "If guest access is enabled, unauthenticated users can access your identity pool. This can be a security risk if you don't need guest access.", "RelatedUrl": "https://docs.aws.amazon.com/location/latest/developerguide/authenticating-using-cognito.html", diff --git a/prowler/providers/aws/services/directoryservice/directoryservice_directory_log_forwarding_enabled/directoryservice_directory_log_forwarding_enabled.metadata.json b/prowler/providers/aws/services/directoryservice/directoryservice_directory_log_forwarding_enabled/directoryservice_directory_log_forwarding_enabled.metadata.json index 01cfbd12f8..f0fc8b8cf1 100644 --- a/prowler/providers/aws/services/directoryservice/directoryservice_directory_log_forwarding_enabled/directoryservice_directory_log_forwarding_enabled.metadata.json +++ b/prowler/providers/aws/services/directoryservice/directoryservice_directory_log_forwarding_enabled/directoryservice_directory_log_forwarding_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:codeartifact:region:account-id:directory/directory-id", "Severity": "medium", - "ResourceType": "AwsDirectoryService", + "ResourceType": "Other", "Description": "Directory Service monitoring with CloudWatch logs.", "Risk": "As a best practice, monitor your organization to ensure that changes are logged. This helps you to ensure that any unexpected change can be investigated and unwanted changes can be rolled back.", "RelatedUrl": "https://docs.aws.amazon.com/directoryservice/latest/admin-guide/incident-response.html", diff --git a/prowler/providers/aws/services/directoryservice/directoryservice_directory_monitor_notifications/directoryservice_directory_monitor_notifications.metadata.json b/prowler/providers/aws/services/directoryservice/directoryservice_directory_monitor_notifications/directoryservice_directory_monitor_notifications.metadata.json index f79827ae12..55d1c393c0 100644 --- a/prowler/providers/aws/services/directoryservice/directoryservice_directory_monitor_notifications/directoryservice_directory_monitor_notifications.metadata.json +++ b/prowler/providers/aws/services/directoryservice/directoryservice_directory_monitor_notifications/directoryservice_directory_monitor_notifications.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:codeartifact:region:account-id:directory/directory-id", "Severity": "medium", - "ResourceType": "AwsDirectoryService", + "ResourceType": "Other", "Description": "Directory Service has SNS Notifications enabled.", "Risk": "As a best practice, monitor status of Directory Service. This helps to avoid late actions to fix Directory Service issues.", "RelatedUrl": "https://docs.aws.amazon.com/directoryservice/latest/admin-guide/ms_ad_enable_notifications.html", diff --git a/prowler/providers/aws/services/directoryservice/directoryservice_directory_snapshots_limit/directoryservice_directory_snapshots_limit.metadata.json b/prowler/providers/aws/services/directoryservice/directoryservice_directory_snapshots_limit/directoryservice_directory_snapshots_limit.metadata.json index 33f96a8263..7693461252 100644 --- a/prowler/providers/aws/services/directoryservice/directoryservice_directory_snapshots_limit/directoryservice_directory_snapshots_limit.metadata.json +++ b/prowler/providers/aws/services/directoryservice/directoryservice_directory_snapshots_limit/directoryservice_directory_snapshots_limit.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:codeartifact:region:account-id:directory/directory-id", "Severity": "low", - "ResourceType": "AwsDirectoryService", + "ResourceType": "Other", "Description": "Directory Service Manual Snapshots limit reached.", "Risk": "A limit reached can bring unwanted results. The maximum number of manual snapshots is a hard limit.", "RelatedUrl": "https://docs.aws.amazon.com/general/latest/gr/ds_region.html", diff --git a/prowler/providers/aws/services/directoryservice/directoryservice_ldap_certificate_expiration/directoryservice_ldap_certificate_expiration.metadata.json b/prowler/providers/aws/services/directoryservice/directoryservice_ldap_certificate_expiration/directoryservice_ldap_certificate_expiration.metadata.json index 30f023d786..982ab9091f 100644 --- a/prowler/providers/aws/services/directoryservice/directoryservice_ldap_certificate_expiration/directoryservice_ldap_certificate_expiration.metadata.json +++ b/prowler/providers/aws/services/directoryservice/directoryservice_ldap_certificate_expiration/directoryservice_ldap_certificate_expiration.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:codeartifact:region:account-id:directory/directory-id", "Severity": "medium", - "ResourceType": "AwsDirectoryService", + "ResourceType": "Other", "Description": "Directory Service Manual Snapshots limit reached.", "Risk": "Expired certificates can impact service availability.", "RelatedUrl": "https://docs.aws.amazon.com/directoryservice/latest/admin-guide/ms_ad_ldap.html", diff --git a/prowler/providers/aws/services/directoryservice/directoryservice_radius_server_security_protocol/directoryservice_radius_server_security_protocol.metadata.json b/prowler/providers/aws/services/directoryservice/directoryservice_radius_server_security_protocol/directoryservice_radius_server_security_protocol.metadata.json index 3fc929a8d8..a3f04834e3 100644 --- a/prowler/providers/aws/services/directoryservice/directoryservice_radius_server_security_protocol/directoryservice_radius_server_security_protocol.metadata.json +++ b/prowler/providers/aws/services/directoryservice/directoryservice_radius_server_security_protocol/directoryservice_radius_server_security_protocol.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:codeartifact:region:account-id:directory/directory-id", "Severity": "medium", - "ResourceType": "AwsDirectoryService", + "ResourceType": "Other", "Description": "Ensure Radius server in DS is using the recommended security protocol.", "Risk": "As a best practice, you might need to configure the authentication protocol between the Microsoft AD DCs and the RADIUS/MFA server. Supported protocols are PAP, CHAP MS-CHAPv1, and MS-CHAPv2. MS-CHAPv2 is recommended because it provides the strongest security of the three options.", "RelatedUrl": "https://aws.amazon.com/blogs/security/how-to-enable-multi-factor-authentication-for-amazon-workspaces-and-amazon-quicksight-by-using-microsoft-ad-and-on-premises-credentials/", diff --git a/prowler/providers/aws/services/directoryservice/directoryservice_supported_mfa_radius_enabled/directoryservice_supported_mfa_radius_enabled.metadata.json b/prowler/providers/aws/services/directoryservice/directoryservice_supported_mfa_radius_enabled/directoryservice_supported_mfa_radius_enabled.metadata.json index d2185cace3..a5db8db7ad 100644 --- a/prowler/providers/aws/services/directoryservice/directoryservice_supported_mfa_radius_enabled/directoryservice_supported_mfa_radius_enabled.metadata.json +++ b/prowler/providers/aws/services/directoryservice/directoryservice_supported_mfa_radius_enabled/directoryservice_supported_mfa_radius_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:codeartifact:region:account-id:directory/directory-id", "Severity": "medium", - "ResourceType": "AwsDirectoryService", + "ResourceType": "Other", "Description": "Ensure Multi-Factor Authentication (MFA) using Radius Server is enabled in DS.", "Risk": "Multi-Factor Authentication (MFA) adds an extra layer of authentication assurance beyond traditional username and password.", "RelatedUrl": "https://docs.aws.amazon.com/directoryservice/latest/admin-guide/ms_ad_mfa.html", diff --git a/prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/__init__.py b/prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/dms_endpoint_ssl_enabled.metadata.json b/prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/dms_endpoint_ssl_enabled.metadata.json new file mode 100644 index 0000000000..3392ac2d64 --- /dev/null +++ b/prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/dms_endpoint_ssl_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "dms_endpoint_ssl_enabled", + "CheckTitle": "Ensure SSL mode is enabled in DMS endpoint", + "CheckType": ["Effects", "Data Exposure"], + "ServiceName": "dms", + "SubServiceName": "endpoint", + "ResourceIdTemplate": "arn:partition:dms:region:account-id:endpoint:resource-id", + "Severity": "high", + "ResourceType": "AwsDmsEndpoint", + "Description": "This check ensures that SSL mode is enabled for all AWS Database Migration Service (DMS) endpoints. Enabling SSL provides encryption in transit for data transferred through these endpoints.", + "Risk": "Without SSL enabled, data transferred through DMS endpoints is not encrypted, potentially exposing sensitive information to unauthorized access or interception during transit.", + "RelatedUrl": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Security.SSL.html", + "Remediation": { + "Code": { + "CLI": "aws dms modify-endpoint --endpoint-arn --ssl-mode require", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dms-controls.html#dms-9", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable SSL mode for all DMS endpoints. Use 'require' as the minimum SSL mode, and consider using 'verify-ca' or 'verify-full' for higher security.", + "Url": "https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Security.SSL.html" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} \ No newline at end of file diff --git a/prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/dms_endpoint_ssl_enabled.py b/prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/dms_endpoint_ssl_enabled.py new file mode 100644 index 0000000000..465923b60c --- /dev/null +++ b/prowler/providers/aws/services/dms/dms_endpoint_ssl_enabled/dms_endpoint_ssl_enabled.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.dms.dms_client import dms_client + + +class dms_endpoint_ssl_enabled(Check): + def execute(self): + findings = [] + for endpoint_arn, endpoint in dms_client.endpoints.items(): + report = Check_Report_AWS(self.metadata()) + report.resource_id = endpoint.id + report.resource_arn = endpoint_arn + report.region = endpoint.region + report.resource_tags = endpoint.tags + + if endpoint.ssl_mode == "none": + report.status = "FAIL" + report.status_extended = f"DMS Endpoint {endpoint.id} is not using SSL." + else: + report.status = "PASS" + report.status_extended = f"DMS Endpoint {endpoint.id} is using SSL with mode: {endpoint.ssl_mode}." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/dms/dms_instance_minor_version_upgrade_enabled/dms_instance_minor_version_upgrade_enabled.py b/prowler/providers/aws/services/dms/dms_instance_minor_version_upgrade_enabled/dms_instance_minor_version_upgrade_enabled.py index a63fbe7118..b89e72ec89 100644 --- a/prowler/providers/aws/services/dms/dms_instance_minor_version_upgrade_enabled/dms_instance_minor_version_upgrade_enabled.py +++ b/prowler/providers/aws/services/dms/dms_instance_minor_version_upgrade_enabled/dms_instance_minor_version_upgrade_enabled.py @@ -10,6 +10,7 @@ class dms_instance_minor_version_upgrade_enabled(Check): report.region = instance.region report.resource_id = instance.id report.resource_arn = instance.arn + report.resource_tags = instance.tags report.status = "FAIL" report.status_extended = f"DMS Replication Instance {instance.id} does not have auto minor version upgrade enabled." if instance.auto_minor_version_upgrade: diff --git a/prowler/providers/aws/services/dms/dms_instance_multi_az_enabled/dms_instance_multi_az_enabled.py b/prowler/providers/aws/services/dms/dms_instance_multi_az_enabled/dms_instance_multi_az_enabled.py index 56be3cd468..d632ff225b 100644 --- a/prowler/providers/aws/services/dms/dms_instance_multi_az_enabled/dms_instance_multi_az_enabled.py +++ b/prowler/providers/aws/services/dms/dms_instance_multi_az_enabled/dms_instance_multi_az_enabled.py @@ -10,6 +10,7 @@ class dms_instance_multi_az_enabled(Check): report.region = instance.region report.resource_id = instance.id report.resource_arn = instance.arn + report.resource_tags = instance.tags report.status = "FAIL" report.status_extended = f"DMS Replication Instance {instance.id} does not have multi az enabled." if instance.multi_az: diff --git a/prowler/providers/aws/services/dms/dms_instance_no_public_access/dms_instance_no_public_access.py b/prowler/providers/aws/services/dms/dms_instance_no_public_access/dms_instance_no_public_access.py index ee7a9f22e3..de383196a7 100644 --- a/prowler/providers/aws/services/dms/dms_instance_no_public_access/dms_instance_no_public_access.py +++ b/prowler/providers/aws/services/dms/dms_instance_no_public_access/dms_instance_no_public_access.py @@ -12,6 +12,7 @@ class dms_instance_no_public_access(Check): report.region = instance.region report.resource_id = instance.id report.resource_arn = instance.arn + report.resource_tags = instance.tags report.status = "PASS" report.status_extended = ( f"DMS Replication Instance {instance.id} is not publicly accessible." diff --git a/prowler/providers/aws/services/dms/dms_service.py b/prowler/providers/aws/services/dms/dms_service.py index 999f790001..a424c23037 100644 --- a/prowler/providers/aws/services/dms/dms_service.py +++ b/prowler/providers/aws/services/dms/dms_service.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic import BaseModel from prowler.lib.logger import logger @@ -5,13 +7,16 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## Database Migration Service class DMS(AWSService): def __init__(self, provider): # Call AWSService's __init__ super().__init__(__class__.__name__, provider) self.instances = [] + self.endpoints = {} self.__threading_call__(self._describe_replication_instances) + self.__threading_call__(self._list_tags, self.instances) + self.__threading_call__(self._describe_endpoints) + self.__threading_call__(self._list_tags, self.endpoints.values()) def _describe_replication_instances(self, regional_client): logger.info("DMS - Describing DMS Replication Instances...") @@ -49,6 +54,47 @@ class DMS(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _describe_endpoints(self, regional_client): + logger.info("DMS - Describing DMS Endpoints...") + try: + describe_endpoints_paginator = regional_client.get_paginator( + "describe_endpoints" + ) + for page in describe_endpoints_paginator.paginate(): + for endpoint in page["Endpoints"]: + arn = endpoint["EndpointArn"] + if not self.audit_resources or ( + is_resource_filtered(arn, self.audit_resources) + ): + self.endpoints[arn] = Endpoint( + arn=arn, + id=endpoint["EndpointIdentifier"], + region=regional_client.region, + ssl_mode=endpoint.get("SslMode", False), + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _list_tags(self, resource: any): + try: + resource.tags = self.regional_clients[ + resource.region + ].list_tags_for_resource(ResourceArn=resource.arn)["TagList"] + except Exception as error: + logger.error( + f"{resource.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + +class Endpoint(BaseModel): + arn: str + id: str + region: str + ssl_mode: str + tags: Optional[list] + class RepInstance(BaseModel): id: str @@ -60,3 +106,4 @@ class RepInstance(BaseModel): security_groups: list[str] = [] multi_az: bool region: str + tags: Optional[list] diff --git a/prowler/providers/aws/services/documentdb/documentdb_cluster_cloudwatch_log_export/documentdb_cluster_cloudwatch_log_export.metadata.json b/prowler/providers/aws/services/documentdb/documentdb_cluster_cloudwatch_log_export/documentdb_cluster_cloudwatch_log_export.metadata.json index b128973b0d..6e0bc5cf96 100644 --- a/prowler/providers/aws/services/documentdb/documentdb_cluster_cloudwatch_log_export/documentdb_cluster_cloudwatch_log_export.metadata.json +++ b/prowler/providers/aws/services/documentdb/documentdb_cluster_cloudwatch_log_export/documentdb_cluster_cloudwatch_log_export.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsRDSDBCluster", + "ResourceType": "AwsRdsDbCluster", "Description": "Check if DocumentDB clusters are using the log export feature.", "Risk": "Ensure that all your Amazon DocumentDB clusters are using the Log Exports feature in order to publish audit logs directly to CloudWatch Logs. The events recorded by Log Exports include events such as successful and failed authentication attempts, creating indexes, or dropping collections in DocumentDB databases.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-4", diff --git a/prowler/providers/aws/services/documentdb/documentdb_cluster_deletion_protection/documentdb_cluster_deletion_protection.metadata.json b/prowler/providers/aws/services/documentdb/documentdb_cluster_deletion_protection/documentdb_cluster_deletion_protection.metadata.json index e68e465133..c5bec6df50 100644 --- a/prowler/providers/aws/services/documentdb/documentdb_cluster_deletion_protection/documentdb_cluster_deletion_protection.metadata.json +++ b/prowler/providers/aws/services/documentdb/documentdb_cluster_deletion_protection/documentdb_cluster_deletion_protection.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", "Severity": "medium", - "ResourceType": "AWSDocumentDBClusterSnapshot", + "ResourceType": "AwsRdsDbCluster", "Description": "Check if DocumentDB Clusters has deletion protection enabled.", "Risk": "Enabling cluster deletion protection offers an additional layer of protection against accidental database deletion or deletion by an unauthorized user. A DocumentDB cluster can't be deleted while deletion protection is enabled. You must first disable deletion protection before a delete request can succeed.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-5", diff --git a/prowler/providers/aws/services/documentdb/documentdb_cluster_public_snapshot/documentdb_cluster_public_snapshot.metadata.json b/prowler/providers/aws/services/documentdb/documentdb_cluster_public_snapshot/documentdb_cluster_public_snapshot.metadata.json index 26d4529c3b..cff48d521b 100644 --- a/prowler/providers/aws/services/documentdb/documentdb_cluster_public_snapshot/documentdb_cluster_public_snapshot.metadata.json +++ b/prowler/providers/aws/services/documentdb/documentdb_cluster_public_snapshot/documentdb_cluster_public_snapshot.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "critical", - "ResourceType": "AwsRDSDBClusterSnapshot", + "ResourceType": "AwsRdsDbClusterSnapshot", "Description": "Check if DocumentDB manual cluster snapshot is public.", "Risk": "If you share an unencrypted manual snapshot as public, the snapshot is available to all AWS accounts. Public snapshots may result in unintended data exposure.", "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/docdb-cluster-snapshot-public-prohibited.html", diff --git a/prowler/providers/aws/services/documentdb/documentdb_cluster_storage_encrypted/documentdb_cluster_storage_encrypted.metadata.json b/prowler/providers/aws/services/documentdb/documentdb_cluster_storage_encrypted/documentdb_cluster_storage_encrypted.metadata.json index b0a1d411e5..421f9e5019 100644 --- a/prowler/providers/aws/services/documentdb/documentdb_cluster_storage_encrypted/documentdb_cluster_storage_encrypted.metadata.json +++ b/prowler/providers/aws/services/documentdb/documentdb_cluster_storage_encrypted/documentdb_cluster_storage_encrypted.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsRDSDBCluster", + "ResourceType": "AwsRdsDbCluster", "Description": "Check if DocumentDB cluster storage is encrypted.", "Risk": "Ensure that encryption of data at rest is enabled for your Amazon DocumentDB (with MongoDB compatibility) database clusters for additional data security and regulatory compliance.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/documentdb-controls.html#documentdb-1", diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_encryption_enabled/dynamodb_accelerator_cluster_encryption_enabled.metadata.json b/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_encryption_enabled/dynamodb_accelerator_cluster_encryption_enabled.metadata.json index edd28d3a37..fd755c6489 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_encryption_enabled/dynamodb_accelerator_cluster_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_encryption_enabled/dynamodb_accelerator_cluster_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:dax:region:account-id:cache/resource-id", "Severity": "medium", - "ResourceType": "AwsDaxCluster", + "ResourceType": "Other", "Description": "Check if DynamoDB DAX Clusters are encrypted at rest.", "Risk": "Encryption at rest provides an additional layer of data protection by securing your data from unauthorized access to the underlying storage.", "RelatedUrl": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAXEncryptionAtRest.html", diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_encryption_enabled/dynamodb_accelerator_cluster_encryption_enabled.py b/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_encryption_enabled/dynamodb_accelerator_cluster_encryption_enabled.py index 2961c83993..9fda39d391 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_encryption_enabled/dynamodb_accelerator_cluster_encryption_enabled.py +++ b/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_encryption_enabled/dynamodb_accelerator_cluster_encryption_enabled.py @@ -12,11 +12,13 @@ class dynamodb_accelerator_cluster_encryption_enabled(Check): report.region = cluster.region report.resource_tags = cluster.tags report.status = "FAIL" - report.status_extended = f"DynamoDB cluster {cluster.name} does not have encryption at rest enabled." + report.status_extended = ( + f"DAX cluster {cluster.name} does not have encryption at rest enabled." + ) if cluster.encryption: report.status = "PASS" report.status_extended = ( - f"DynamoDB cluster {cluster.name} has encryption at rest enabled." + f"DAX cluster {cluster.name} has encryption at rest enabled." ) findings.append(report) return findings diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/__init__.py b/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled.metadata.json b/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled.metadata.json new file mode 100644 index 0000000000..50741ae1c2 --- /dev/null +++ b/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "dynamodb_accelerator_cluster_in_transit_encryption_enabled", + "CheckTitle": "Check if DynamoDB Accelerator (DAX) clusters are encrypted in transit.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "dynamodb", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:dynamodb:region:account-id:table/table-name", + "Severity": "medium", + "ResourceType": "Other", + "Description": "This control checks whether an Amazon DynamoDB Accelerator (DAX) cluster is encrypted in transit, with the endpoint encryption type set to TLS. The control fails if the DAX cluster isn't encrypted in transit.", + "Risk": "Without encryption in transit, DAX clusters are vulnerable to person-in-the-middle attacks or eavesdropping on network traffic, which could lead to unauthorized access or manipulation of sensitive data.", + "RelatedUrl": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAXEncryptionInTransit.html", + "Remediation": { + "Code": { + "CLI": "aws dax create-cluster --cluster-name --node-type --replication-factor --cluster-endpoint-encryption-type TLS", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dynamodb-controls.html#dynamodb-7", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that DynamoDB Accelerator (DAX) clusters are encrypted in transit by enabling TLS during cluster creation.", + "Url": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAXEncryptionInTransit.html" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled.py b/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled.py new file mode 100644 index 0000000000..c509f3645d --- /dev/null +++ b/prowler/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.dynamodb.dax_client import dax_client + + +class dynamodb_accelerator_cluster_in_transit_encryption_enabled(Check): + def execute(self): + findings = [] + for cluster in dax_client.clusters: + report = Check_Report_AWS(self.metadata()) + report.resource_id = cluster.name + report.resource_arn = cluster.arn + report.region = cluster.region + report.resource_tags = cluster.tags + report.status = "FAIL" + report.status_extended = f"DAX cluster {cluster.name} does not have encryption in transit enabled." + if cluster.tls_encryption: + report.status = "PASS" + report.status_extended = ( + f"DAX cluster {cluster.name} has encryption in transit enabled." + ) + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_service.py b/prowler/providers/aws/services/dynamodb/dynamodb_service.py index aded58e5b7..27861b2244 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_service.py +++ b/prowler/providers/aws/services/dynamodb/dynamodb_service.py @@ -9,12 +9,11 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## DynamoDB class DynamoDB(AWSService): def __init__(self, provider): # Call AWSService's __init__ super().__init__(__class__.__name__, provider) - self.tables = [] + self.tables = {} self.__threading_call__(self._list_tables) self._describe_table() self._describe_continuous_backups() @@ -31,14 +30,11 @@ class DynamoDB(AWSService): if not self.audit_resources or ( is_resource_filtered(arn, self.audit_resources) ): - self.tables.append( - Table( - arn=arn, - name=table, - encryption_type=None, - kms_arn=None, - region=regional_client.region, - ) + self.tables[arn] = Table( + name=table, + encryption_type=None, + kms_arn=None, + region=regional_client.region, ) except Exception as error: logger.error( @@ -48,7 +44,7 @@ class DynamoDB(AWSService): def _describe_table(self): logger.info("DynamoDB - Describing Table...") try: - for table in self.tables: + for table in self.tables.values(): regional_client = self.regional_clients[table.region] properties = regional_client.describe_table(TableName=table.name)[ "Table" @@ -58,6 +54,10 @@ class DynamoDB(AWSService): table.encryption_type = properties["SSEDescription"]["SSEType"] if table.encryption_type == "KMS": table.kms_arn = properties["SSEDescription"]["KMSMasterKeyArn"] + + table.deletion_protection = properties.get( + "DeletionProtectionEnabled", False + ) except Exception as error: logger.error( f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" @@ -66,7 +66,7 @@ class DynamoDB(AWSService): def _describe_continuous_backups(self): logger.info("DynamoDB - Describing Continuous Backups...") try: - for table in self.tables: + for table in self.tables.values(): try: regional_client = self.regional_clients[table.region] properties = regional_client.describe_continuous_backups( @@ -98,11 +98,11 @@ class DynamoDB(AWSService): def _get_resource_policy(self): logger.info("DynamoDB - Get Resource Policy...") try: - for table in self.tables: + for table_arn, table in self.tables.items(): try: regional_client = self.regional_clients[table.region] response = regional_client.get_resource_policy( - ResourceArn=table.arn + ResourceArn=table_arn ) table.policy = json.loads(response["Policy"]) except ClientError as error: @@ -127,11 +127,11 @@ class DynamoDB(AWSService): def _list_tags_for_resource(self): logger.info("DynamoDB - List Tags...") try: - for table in self.tables: + for table_arn, table in self.tables.items(): try: regional_client = self.regional_clients[table.region] response = regional_client.list_tags_of_resource( - ResourceArn=table.arn + ResourceArn=table_arn )["Tags"] table.tags = response except ClientError as error: @@ -150,7 +150,6 @@ class DynamoDB(AWSService): ) -################## DynamoDB DAX class DAX(AWSService): def __init__(self, provider): # Call AWSService's __init__ @@ -173,15 +172,20 @@ class DAX(AWSService): ) ): encryption = False + tls_encryption = False if "SSEDescription" in cluster: if cluster["SSEDescription"]["Status"] == "ENABLED": encryption = True + if "ClusterEndpointEncryptionType" in cluster: + if cluster["ClusterEndpointEncryptionType"] == "TLS": + tls_encryption = True self.clusters.append( Cluster( arn=cluster["ClusterArn"], name=cluster["ClusterName"], encryption=encryption, region=regional_client.region, + tls_encryption=tls_encryption, ) ) except Exception as error: @@ -212,7 +216,6 @@ class DAX(AWSService): class Table(BaseModel): - arn: str name: str encryption_type: Optional[str] kms_arn: Optional[str] @@ -220,6 +223,7 @@ class Table(BaseModel): policy: Optional[dict] = None region: str tags: Optional[list] = [] + deletion_protection: bool = False class Cluster(BaseModel): @@ -228,3 +232,4 @@ class Cluster(BaseModel): encryption: bool region: str tags: Optional[list] = [] + tls_encryption: bool diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.metadata.json b/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.metadata.json index 53941c4e32..1ab8b56140 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.metadata.json +++ b/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:dynamodb:region:account-id:table/resource-id", "Severity": "medium", - "ResourceType": "AwsDynamoDBTable", + "ResourceType": "AwsDynamoDbTable", "Description": "This check determines if the DynamoDB table is accessible from other AWS accounts.", "Risk": "If the DynamoDB table is accessible from other AWS accounts, it may lead to unauthorized access to the data stored in the table.", "RelatedUrl": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/access-control-resource-based.html", diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.py b/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.py index e7492fb221..3f47461cd9 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.py +++ b/prowler/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access.py @@ -1,17 +1,15 @@ from prowler.lib.check.models import Check, Check_Report_AWS -from prowler.providers.aws.lib.policy_condition_parser.policy_condition_parser import ( - is_condition_block_restrictive, -) from prowler.providers.aws.services.dynamodb.dynamodb_client import dynamodb_client +from prowler.providers.aws.services.iam.lib.policy import is_policy_public class dynamodb_table_cross_account_access(Check): def execute(self): findings = [] - for table in dynamodb_client.tables: + for table_arn, table in dynamodb_client.tables.items(): report = Check_Report_AWS(self.metadata()) report.resource_id = table.name - report.resource_arn = table.arn + report.resource_arn = table_arn report.resource_tags = table.tags report.region = table.region report.status = "PASS" @@ -20,36 +18,10 @@ class dynamodb_table_cross_account_access(Check): ) if table.policy: report.status_extended = f"DynamoDB table {table.name} has a resource-based policy but is not cross account." - cross_account_access = False - policy_statements = table.policy["Statement"] - if isinstance( - policy_statements, dict - ): # Normalize single statement to list - policy_statements = [policy_statements] - for statement in policy_statements: - if not cross_account_access: - if statement["Effect"] == "Allow": - if "AWS" in statement["Principal"]: - principals = statement["Principal"]["AWS"] - if not isinstance(principals, list): - principals = [principals] - else: - principals = [statement["Principal"]] - for aws_account in principals: - if ( - dynamodb_client.audited_account not in aws_account - or "*" == aws_account - ): - cross_account_access = True - # Check if the condition block is restrictive - conditions = statement.get("Condition", {}) - if is_condition_block_restrictive( - conditions, dynamodb_client.audited_account - ): - cross_account_access = False - else: - break - if cross_account_access: + if is_policy_public( + table.policy, + source_account=dynamodb_client.audited_account, + ): report.status = "FAIL" report.status_extended = f"DynamoDB table {table.name} has a resource-based policy allowing cross account access." findings.append(report) diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/__init__.py b/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.metadata.json b/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.metadata.json new file mode 100644 index 0000000000..5516e61c76 --- /dev/null +++ b/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "dynamodb_table_deletion_protection_enabled", + "CheckTitle": "Check if DynamoDB tables have deletion protection enabled.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "dynamodb", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:dynamodb:region:account-id:table/table-name", + "Severity": "medium", + "ResourceType": "AwsDynamoDbTable", + "Description": "This control checks whether an Amazon DynamoDB table has deletion protection enabled to prevent accidental deletion during regular table management operations.", + "Risk": "If deletion protection is not enabled, a DynamoDB table could be accidentally deleted, leading to data loss and potential disruption of business operations.", + "RelatedUrl": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.Basics.html#WorkingWithTables.Basics.DeletionProtection", + "Remediation": { + "Code": { + "CLI": "aws dynamodb update-table --table-name --deletion-protection-enabled", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dynamodb-controls.html#dynamodb-6", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable deletion protection for your DynamoDB tables to prevent accidental deletion.", + "Url": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithTables.Basics.html#WorkingWithTables.Basics.DeletionProtection" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.py b/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.py new file mode 100644 index 0000000000..e1e79caba8 --- /dev/null +++ b/prowler/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.dynamodb.dynamodb_client import dynamodb_client + + +class dynamodb_table_deletion_protection_enabled(Check): + def execute(self): + findings = [] + for table_arn, table in dynamodb_client.tables.items(): + report = Check_Report_AWS(self.metadata()) + report.region = table.region + report.resource_id = table.name + report.resource_arn = table_arn + report.resource_tags = table.tags + report.status = "FAIL" + report.status_extended = f"DynamoDB table {table.name} does not have deletion protection enabled." + + if table.deletion_protection: + report.status = "PASS" + report.status_extended = ( + f"DynamoDB table {table.name} has deletion protection enabled." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/__init__.py b/prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan.metadata.json b/prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan.metadata.json new file mode 100644 index 0000000000..663c7f7e84 --- /dev/null +++ b/prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "dynamodb_table_protected_by_backup_plan", + "CheckTitle": "Check if DynamoDB tables are included in a backup plan.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "dynamodb", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:dynamodb:region:account-id:table/table-name", + "Severity": "medium", + "ResourceType": "AwsDynamoDbTable", + "Description": "This control checks whether an Amazon DynamoDB table is covered by a backup plan. The control fails if the DynamoDB table isn't included in a backup plan.", + "Risk": "If a DynamoDB table is not covered by a backup plan, data loss may occur due to accidental deletion, corruption, or unexpected failure, compromising the resilience of your application.", + "RelatedUrl": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/dynamodb-controls.html#dynamodb-4", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that all active DynamoDB tables are included in a backup plan to safeguard against data loss.", + "Url": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html" + } + }, + "Categories": [ + "redundancy" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan.py b/prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan.py new file mode 100644 index 0000000000..1c62e2fd12 --- /dev/null +++ b/prowler/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan.py @@ -0,0 +1,32 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.backup.backup_client import backup_client +from prowler.providers.aws.services.dynamodb.dynamodb_client import dynamodb_client + + +class dynamodb_table_protected_by_backup_plan(Check): + def execute(self): + findings = [] + for table_arn, table in dynamodb_client.tables.items(): + report = Check_Report_AWS(self.metadata()) + report.resource_id = table.name + report.resource_arn = table_arn + report.resource_tags = table.tags + report.region = table.region + report.status = "FAIL" + report.status_extended = ( + f"DynamoDB table {table.name} is not protected by a backup plan." + ) + + if ( + table_arn in backup_client.protected_resources + or "arn:aws:dynamodb:*:*:table/*" in backup_client.protected_resources + or "*" in backup_client.protected_resources + ): + report.status = "PASS" + report.status_extended = ( + f"DynamoDB table {table.name} is protected by a backup plan." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_tables_kms_cmk_encryption_enabled/dynamodb_tables_kms_cmk_encryption_enabled.metadata.json b/prowler/providers/aws/services/dynamodb/dynamodb_tables_kms_cmk_encryption_enabled/dynamodb_tables_kms_cmk_encryption_enabled.metadata.json index 35321b6555..db93936b6b 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_tables_kms_cmk_encryption_enabled/dynamodb_tables_kms_cmk_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/dynamodb/dynamodb_tables_kms_cmk_encryption_enabled/dynamodb_tables_kms_cmk_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:dynamodb:region:account-id:table/resource-id", "Severity": "medium", - "ResourceType": "AwsDynamoDBTable", + "ResourceType": "AwsDynamoDbTable", "Description": "Check if DynamoDB table has encryption at rest enabled using CMK KMS.", "Risk": "All user data stored in Amazon DynamoDB is fully encrypted at rest. This functionality helps reduce the operational burden and complexity involved in protecting sensitive data.", "RelatedUrl": "https://docs.aws.amazon.com/amazondynamodbdb/latest/developerguide/EncryptionAtRest.html", diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_tables_kms_cmk_encryption_enabled/dynamodb_tables_kms_cmk_encryption_enabled.py b/prowler/providers/aws/services/dynamodb/dynamodb_tables_kms_cmk_encryption_enabled/dynamodb_tables_kms_cmk_encryption_enabled.py index 30a0ffe7ae..921e410171 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_tables_kms_cmk_encryption_enabled/dynamodb_tables_kms_cmk_encryption_enabled.py +++ b/prowler/providers/aws/services/dynamodb/dynamodb_tables_kms_cmk_encryption_enabled/dynamodb_tables_kms_cmk_encryption_enabled.py @@ -5,10 +5,10 @@ from prowler.providers.aws.services.dynamodb.dynamodb_client import dynamodb_cli class dynamodb_tables_kms_cmk_encryption_enabled(Check): def execute(self): findings = [] - for table in dynamodb_client.tables: + for table_arn, table in dynamodb_client.tables.items(): report = Check_Report_AWS(self.metadata()) report.resource_id = table.name - report.resource_arn = table.arn + report.resource_arn = table_arn report.resource_tags = table.tags report.region = table.region report.status = "FAIL" diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_tables_pitr_enabled/dynamodb_tables_pitr_enabled.metadata.json b/prowler/providers/aws/services/dynamodb/dynamodb_tables_pitr_enabled/dynamodb_tables_pitr_enabled.metadata.json index 1c8fadbadd..9fc7112c6e 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_tables_pitr_enabled/dynamodb_tables_pitr_enabled.metadata.json +++ b/prowler/providers/aws/services/dynamodb/dynamodb_tables_pitr_enabled/dynamodb_tables_pitr_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:dynamodb:region:account-id:table/resource-id", "Severity": "medium", - "ResourceType": "AwsDynamoDBTable", + "ResourceType": "AwsDynamoDbTable", "Description": "Check if DynamoDB tables point-in-time recovery (PITR) is enabled.", "Risk": "If the DynamoDB Table does not have point-in-time recovery enabled, it is vulnerable to accidental write or delete operations.", "RelatedUrl": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/PointInTimeRecovery_Howitworks.html", diff --git a/prowler/providers/aws/services/dynamodb/dynamodb_tables_pitr_enabled/dynamodb_tables_pitr_enabled.py b/prowler/providers/aws/services/dynamodb/dynamodb_tables_pitr_enabled/dynamodb_tables_pitr_enabled.py index ff109d5295..285f4e49ca 100644 --- a/prowler/providers/aws/services/dynamodb/dynamodb_tables_pitr_enabled/dynamodb_tables_pitr_enabled.py +++ b/prowler/providers/aws/services/dynamodb/dynamodb_tables_pitr_enabled/dynamodb_tables_pitr_enabled.py @@ -5,10 +5,10 @@ from prowler.providers.aws.services.dynamodb.dynamodb_client import dynamodb_cli class dynamodb_tables_pitr_enabled(Check): def execute(self): findings = [] - for table in dynamodb_client.tables: + for table_arn, table in dynamodb_client.tables.items(): report = Check_Report_AWS(self.metadata()) report.resource_id = table.name - report.resource_arn = table.arn + report.resource_arn = table_arn report.resource_tags = table.tags report.region = table.region report.status = "FAIL" diff --git a/prowler/providers/aws/services/ec2/ec2_ebs_public_snapshot/ec2_ebs_public_snapshot.metadata.json b/prowler/providers/aws/services/ec2/ec2_ebs_public_snapshot/ec2_ebs_public_snapshot.metadata.json index d0324b5816..e2f759d999 100644 --- a/prowler/providers/aws/services/ec2/ec2_ebs_public_snapshot/ec2_ebs_public_snapshot.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_ebs_public_snapshot/ec2_ebs_public_snapshot.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "snapshot", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "critical", - "ResourceType": "AwsEc2Snapshot", + "ResourceType": "Other", "Description": "Ensure there are no EBS Snapshots set as Public.", "Risk": "When you share a snapshot, you are giving others access to all of the data on the snapshot. Share snapshots only with people with whom you want to share all of your snapshot data.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/ec2/ec2_ebs_snapshot_account_block_public_access/ec2_ebs_snapshot_account_block_public_access.metadata.json b/prowler/providers/aws/services/ec2/ec2_ebs_snapshot_account_block_public_access/ec2_ebs_snapshot_account_block_public_access.metadata.json index 3cb41e9ad6..ab44f43b47 100644 --- a/prowler/providers/aws/services/ec2/ec2_ebs_snapshot_account_block_public_access/ec2_ebs_snapshot_account_block_public_access.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_ebs_snapshot_account_block_public_access/ec2_ebs_snapshot_account_block_public_access.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "snapshot", "ResourceIdTemplate": "arn:partition:service:region:account-id", "Severity": "high", - "ResourceType": "AwsEc2Snapshot", + "ResourceType": "Other", "Description": "EBS snapshots can be shared with other AWS accounts or made public. By default, EBS snapshots are private and only the AWS account that created the snapshot can access it. If an EBS snapshot is shared with another AWS account or made public, the data in the snapshot can be accessed by the other account or by anyone on the internet. Ensure that public access to EBS snapshots is disabled.", "Risk": "If public access to EBS snapshots is enabled, the data in the snapshot can be accessed by anyone on the internet.", "RelatedUrl": "https://docs.aws.amazon.com/ebs/latest/userguide/block-public-access-snapshots-work.html#block-public-access-snapshots-enable", diff --git a/prowler/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.metadata.json b/prowler/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.metadata.json index d50e3a933a..d44b9c95e2 100644 --- a/prowler/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_ebs_snapshots_encrypted/ec2_ebs_snapshots_encrypted.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "snapshot", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsEc2Snapshot", + "ResourceType": "Other", "Description": "Check if EBS snapshots are encrypted.", "Risk": "Data encryption at rest prevents data visibility in the event of its unauthorized access or theft.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/__init__.py b/prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan.metadata.json b/prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan.metadata.json new file mode 100644 index 0000000000..692fe40849 --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "ec2_ebs_volume_protected_by_backup_plan", + "CheckTitle": "Amazon EBS volumes should be protected by a backup plan.", + "CheckType": [ + "Software and Configuration Checks, AWS Security Best Practices" + ], + "ServiceName": "ec2", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:volume/volume-id", + "Severity": "low", + "ResourceType": "AwsEc2Volume", + "Description": "Evaluates if an Amazon EBS volume in in-use state is covered by a backup plan. The check fails if an EBS volume isn't covered by a backup plan. If you set the backupVaultLockCheck parameter equal to true, the control passes only if the EBS volume is backed up in an AWS Backup locked vault.", + "Risk": "Without backup coverage, Amazon EBS volumes are vulnerable to data loss or deletion, reducing the resilience of your systems and making recovery from incidents more difficult.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ebs-resources-protected-by-backup-plan.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-28", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that all in-use Amazon EBS volumes are included in a backup plan, and consider using AWS Backup Vault Lock for additional protection.", + "Url": "https://docs.aws.amazon.com/aws-backup/latest/devguide/assigning-resources.html" + } + }, + "Categories": [ + "redundancy" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan.py b/prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan.py new file mode 100644 index 0000000000..72796a4bc9 --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan.py @@ -0,0 +1,31 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.backup.backup_client import backup_client +from prowler.providers.aws.services.ec2.ec2_client import ec2_client + + +class ec2_ebs_volume_protected_by_backup_plan(Check): + def execute(self): + findings = [] + for volume in ec2_client.volumes: + report = Check_Report_AWS(self.metadata()) + report.region = volume.region + report.resource_id = volume.id + report.resource_arn = volume.arn + report.resource_tags = volume.tags + report.status = "FAIL" + report.status_extended = ( + f"EBS Volume {volume.id} is not protected by a backup plan." + ) + if ( + volume.arn in backup_client.protected_resources + or "arn:aws:ec2:*:*:volume/*" in backup_client.protected_resources + or "*" in backup_client.protected_resources + ): + report.status = "PASS" + report.status_extended = ( + f"EBS Volume {volume.id} is protected by a backup plan." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/__init__.py b/prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type.metadata.json b/prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type.metadata.json new file mode 100644 index 0000000000..3750e16f80 --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "ec2_instance_paravirtual_type", + "CheckTitle": "Amazon EC2 paravirtual virtualization type should not be used.", + "CheckType": [], + "ServiceName": "ec2", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "medium", + "ResourceType": "AwsEc2Instance", + "Description": "Ensure that the virtualization type of an EC2 instance is not paravirtual. The control fails if the virtualizationType of the EC2 instance is set to paravirtual.", + "Risk": "Using paravirtual instances can limit performance and security benefits offered by hardware virtual machine (HVM) instances, such as improved CPU, network, and storage efficiency.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ec2-paravirtual-instance-check.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-24", + "Terraform": "" + }, + "Recommendation": { + "Text": "To update an EC2 instance to a new instance type, see Change the instance type in the Amazon EC2 User Guide.", + "Url": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-resize.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type.py b/prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type.py new file mode 100644 index 0000000000..8186b02e05 --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type.py @@ -0,0 +1,26 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ec2.ec2_client import ec2_client + + +class ec2_instance_paravirtual_type(Check): + def execute(self): + findings = [] + for instance in ec2_client.instances: + if instance.state != "terminated": + report = Check_Report_AWS(self.metadata()) + report.region = instance.region + report.resource_arn = instance.arn + report.resource_tags = instance.tags + report.status = "PASS" + report.status_extended = ( + f"EC2 Instance {instance.id} virtualization type is set to HVM." + ) + report.resource_id = instance.id + if instance.virtualization_type == "paravirtual": + report.status = "FAIL" + report.status_extended = f"EC2 Instance {instance.id} virtualization type is set to paravirtual." + report.resource_id = instance.id + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ec2/ec2_instance_port_memcached_exposed_to_internet/ec2_instance_port_memcached_exposed_to_internet.metadata.json b/prowler/providers/aws/services/ec2/ec2_instance_port_memcached_exposed_to_internet/ec2_instance_port_memcached_exposed_to_internet.metadata.json index 8054c189a6..a55e794e77 100644 --- a/prowler/providers/aws/services/ec2/ec2_instance_port_memcached_exposed_to_internet/ec2_instance_port_memcached_exposed_to_internet.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_instance_port_memcached_exposed_to_internet/ec2_instance_port_memcached_exposed_to_internet.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "instance", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "critical", - "ResourceType": "AwsEc2Instance", + "ResourceType": "AwsEc2SecurityGroup", "Description": "Ensure no EC2 instances allow ingress from the internet to TCP port 11211 (Memcached).", "Risk": "Memcached is an open-source, high-performance, distributed memory object caching system. It is often used to speed up dynamic database-driven websites by caching data and objects in RAM to reduce the number of times an external data source must be read. Memcached is designed to be used in trusted environments and should not be exposed to the internet. If Memcached is exposed to the internet, it can be exploited by attackers to perform distributed denial-of-service (DDoS) attacks, data exfiltration, and other malicious activities.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/__init__.py b/prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni.metadata.json b/prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni.metadata.json new file mode 100644 index 0000000000..e4c7f5825d --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "ec2_instance_uses_single_eni", + "CheckTitle": "Amazon EC2 instances should not use multiple ENIs", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ec2", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:instance/resource-id", + "Severity": "low", + "ResourceType": "AwsEc2Instance", + "Description": "This control checks whether an EC2 instance uses multiple Elastic Network Interfaces (ENIs) or Elastic Fabric Adapters (EFAs). This control passes if a single network adapter is used. The control includes an optional parameter list to identify the allowed ENIs. This control also fails if an EC2 instance that belongs to an Amazon EKS cluster uses more than one ENI. If your EC2 instances need to have multiple ENIs as part of an Amazon EKS cluster, you can suppress those control findings.", + "Risk": "Multiple ENIs can cause dual-homed instances, meaning instances that have multiple subnets. This can add network security complexity and introduce unintended network paths and access.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ec2-instance-multiple-eni-check.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-17", + "Terraform": "" + }, + "Recommendation": { + "Text": "To detach a network interface from an EC2 instance, follow the instructions in the Amazon EC2 User Guide.", + "Url": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html#detach_eni" + } + }, + "Categories": [ + "trustboundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni.py b/prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni.py new file mode 100644 index 0000000000..ba2ab55ffc --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni.py @@ -0,0 +1,54 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ec2.ec2_client import ec2_client + + +class ec2_instance_uses_single_eni(Check): + def execute(self): + findings = [] + for instance in ec2_client.instances: + report = Check_Report_AWS(self.metadata()) + report.region = instance.region + report.resource_id = instance.id + report.resource_arn = instance.arn + report.resource_tags = instance.tags + eni_types = {"efa": [], "interface": [], "trunk": []} + if not instance.network_interfaces: + report.status = "PASS" + report.status_extended = ( + f"EC2 Instance {instance.id} has no network interfaces attached." + ) + else: + for eni_id in instance.network_interfaces: + if ec2_client.network_interfaces[eni_id].type in eni_types: + eni_types[ec2_client.network_interfaces[eni_id].type].append( + eni_id + ) + + message_status_extended = "" + if ( + len(eni_types["efa"]) + + len(eni_types["interface"]) + + len(eni_types["trunk"]) + > 1 + ): + report.status = "FAIL" + message_status_extended = ( + f"EC2 Instance {instance.id} uses multiple ENIs: (" + ) + else: + report.status = "PASS" + message_status_extended = ( + f"EC2 Instance {instance.id} uses only one ENI: (" + ) + + if eni_types["efa"]: + message_status_extended += f" EFAs: {eni_types['efa']}" + if eni_types["interface"]: + message_status_extended += f" Interfaces: {eni_types['interface']}" + if eni_types["trunk"]: + message_status_extended += f" Trunks: {eni_types['trunk']}" + report.status_extended = message_status_extended + " )." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/__init__.py b/prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip.metadata.json b/prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip.metadata.json new file mode 100644 index 0000000000..a94f3e1f77 --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "ec2_launch_template_no_public_ip", + "CheckTitle": "Amazon EC2 launch templates should not assign public IPs to network interfaces.", + "CheckType": [], + "ServiceName": "ec2", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "high", + "ResourceType": "AwsEc2LaunchTemplate", + "Description": "This control checks if Amazon EC2 launch templates are configured to assign public IP addresses to network interfaces upon launch. The control fails if an EC2 launch template is configured to assign a public IP address to network interfaces or if there is at least one network interface that has a public IP address.", + "Risk": "A public IP address is reachable from the internet, making associated resources potentially accessible from the internet. EC2 resources should not be publicly accessible to avoid unintended access to workloads.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ec2-launch-template-public-ip-disabled.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-25", + "Terraform": "" + }, + "Recommendation": { + "Text": "To update an EC2 launch template, see Change the default network interface settings in the Amazon EC2 Auto Scaling User Guide.", + "Url": "https://docs.aws.amazon.com/autoscaling/ec2/userguide/create-launch-template.html#change-network-interface" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip.py b/prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip.py new file mode 100644 index 0000000000..b0913ac81c --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip.py @@ -0,0 +1,52 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ec2.ec2_client import ec2_client + + +class ec2_launch_template_no_public_ip(Check): + def execute(self): + findings = [] + for template in ec2_client.launch_templates: + report = Check_Report_AWS(self.metadata()) + report.region = template.region + report.resource_id = template.id + report.resource_arn = template.arn + report.resource_tags = template.tags + + versions_with_autoassign_public_ip = [] + versions_with_network_interfaces_public_ip = [] + + for version in template.versions: + # Check if the launch template version assigns a public IP address + if version.template_data.associate_public_ip_address: + versions_with_autoassign_public_ip.append( + str(version.version_number) + ) + if version.template_data.network_interfaces: + for network_interface in version.template_data.network_interfaces: + if network_interface.public_ip_addresses: + versions_with_network_interfaces_public_ip.append( + str(version.version_number) + ) + break + + if ( + versions_with_autoassign_public_ip + or versions_with_network_interfaces_public_ip + ): + report.status = "FAIL" + + if ( + versions_with_autoassign_public_ip + and versions_with_network_interfaces_public_ip + ): + report.status_extended = f"EC2 Launch Template {template.name} is configured to assign a public IP address to network interfaces upon launch in template versions: {', '.join(versions_with_autoassign_public_ip)} and is using a network interface with public IP addresses in template versions: {', '.join(versions_with_network_interfaces_public_ip)}." + elif versions_with_autoassign_public_ip: + report.status_extended = f"EC2 Launch Template {template.name} is configured to assign a public IP address to network interfaces upon launch in template versions: {', '.join(versions_with_autoassign_public_ip)}." + elif versions_with_network_interfaces_public_ip: + report.status_extended = f"EC2 Launch Template {template.name} is using a network interface with public IP addresses in template versions: {', '.join(versions_with_network_interfaces_public_ip)}." + else: + report.status = "PASS" + report.status_extended = f"EC2 Launch Template {template.name} is neither configured to assign a public IP address to network interfaces upon launch nor using a network interface with public IP addresses." + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ec2/ec2_launch_template_no_secrets/ec2_launch_template_no_secrets.py b/prowler/providers/aws/services/ec2/ec2_launch_template_no_secrets/ec2_launch_template_no_secrets.py index b78ecc2284..bc51cdec3c 100644 --- a/prowler/providers/aws/services/ec2/ec2_launch_template_no_secrets/ec2_launch_template_no_secrets.py +++ b/prowler/providers/aws/services/ec2/ec2_launch_template_no_secrets/ec2_launch_template_no_secrets.py @@ -19,13 +19,13 @@ class ec2_launch_template_no_secrets(Check): report.region = template.region report.resource_id = template.id report.resource_arn = template.arn + report.resource_tags = template.tags versions_with_secrets = [] for version in template.versions: if not version.template_data.user_data: continue - user_data = b64decode(version.template_data.user_data) try: diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_any_port/ec2_networkacl_allow_ingress_any_port.py b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_any_port/ec2_networkacl_allow_ingress_any_port.py index 444f73527c..abd5857e83 100644 --- a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_any_port/ec2_networkacl_allow_ingress_any_port.py +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_any_port/ec2_networkacl_allow_ingress_any_port.py @@ -8,7 +8,7 @@ class ec2_networkacl_allow_ingress_any_port(Check): findings = [] tcp_protocol = "-1" check_port = 0 - for network_acl in ec2_client.network_acls: + for arn, network_acl in ec2_client.network_acls.items(): if ( ec2_client.provider.scan_unused_services or network_acl.region in ec2_client.regions_with_sgs @@ -18,7 +18,7 @@ class ec2_networkacl_allow_ingress_any_port(Check): report = Check_Report_AWS(self.metadata()) report.resource_id = network_acl.id report.region = network_acl.region - report.resource_arn = network_acl.arn + report.resource_arn = arn report.resource_tags = network_acl.tags report.status = "FAIL" report.status_extended = f"Network ACL {network_acl.name if network_acl.name else network_acl.id} has every port open to the Internet." diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_22/ec2_networkacl_allow_ingress_tcp_port_22.py b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_22/ec2_networkacl_allow_ingress_tcp_port_22.py index 13d580c3f5..1d3f01ef6e 100644 --- a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_22/ec2_networkacl_allow_ingress_tcp_port_22.py +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_22/ec2_networkacl_allow_ingress_tcp_port_22.py @@ -8,7 +8,7 @@ class ec2_networkacl_allow_ingress_tcp_port_22(Check): findings = [] tcp_protocol = "6" check_port = 22 - for network_acl in ec2_client.network_acls: + for arn, network_acl in ec2_client.network_acls.items(): if ( ec2_client.provider.scan_unused_services or network_acl.region in ec2_client.regions_with_sgs @@ -18,7 +18,7 @@ class ec2_networkacl_allow_ingress_tcp_port_22(Check): report = Check_Report_AWS(self.metadata()) report.resource_id = network_acl.id report.region = network_acl.region - report.resource_arn = network_acl.arn + report.resource_arn = arn report.resource_tags = network_acl.tags report.status = "FAIL" report.status_extended = f"Network ACL {network_acl.name if network_acl.name else network_acl.id} has SSH port 22 open to the Internet." diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_3389/ec2_networkacl_allow_ingress_tcp_port_3389.py b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_3389/ec2_networkacl_allow_ingress_tcp_port_3389.py index 11adad1d69..580a1f75a8 100644 --- a/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_3389/ec2_networkacl_allow_ingress_tcp_port_3389.py +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_allow_ingress_tcp_port_3389/ec2_networkacl_allow_ingress_tcp_port_3389.py @@ -8,7 +8,7 @@ class ec2_networkacl_allow_ingress_tcp_port_3389(Check): findings = [] tcp_protocol = "6" check_port = 3389 - for network_acl in ec2_client.network_acls: + for arn, network_acl in ec2_client.network_acls.items(): if ( ec2_client.provider.scan_unused_services or network_acl.region in ec2_client.regions_with_sgs @@ -18,7 +18,7 @@ class ec2_networkacl_allow_ingress_tcp_port_3389(Check): report = Check_Report_AWS(self.metadata()) report.resource_id = network_acl.id report.region = network_acl.region - report.resource_arn = network_acl.arn + report.resource_arn = arn report.resource_tags = network_acl.tags report.status = "FAIL" report.status_extended = f"Network ACL {network_acl.name if network_acl.name else network_acl.id} has Microsoft RDP port 3389 open to the Internet." diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_unused/__init__.py b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json new file mode 100644 index 0000000000..ec120d7780 --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "ec2_networkacl_unused", + "CheckTitle": "Unused Network Access Control Lists should be removed.", + "CheckType": [], + "ServiceName": "ec2", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "low", + "ResourceType": "AwsEc2NetworkAcl", + "Description": "Ensure that there are no unused network access control lists (network ACLs) in your virtual private cloud (VPC). The control fails if the network ACL isn't associated with a subnet. The control doesn't generate findings for an unused default network ACL.", + "Risk": "Unused network ACLs may represent a potential security risk if left in place without purpose, as they could be mistakenly associated with subnets later.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/vpc-network-acl-unused-check.html", + "Remediation": { + "Code": { + "CLI": "aws ec2 delete-network-acl --network-acl-id ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-16", + "Terraform": "" + }, + "Recommendation": { + "Text": "For instructions on deleting an unused network ACL, see Deleting a network ACL in the Amazon VPC User Guide.", + "Url": "https://docs.aws.amazon.com/vpc/latest/userguide/vpc-network-acls.html#vpc-network-acl-delete" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "Infrastructure Security" +} diff --git a/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.py b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.py new file mode 100644 index 0000000000..036cb8036e --- /dev/null +++ b/prowler/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ec2.ec2_client import ec2_client + + +class ec2_networkacl_unused(Check): + def execute(self): + findings = [] + for arn, network_acl in ec2_client.network_acls.items(): + if not network_acl.default: + report = Check_Report_AWS(self.metadata()) + report.resource_id = network_acl.id + report.region = network_acl.region + report.resource_arn = arn + report.resource_tags = network_acl.tags + + if not network_acl.in_use: + report.status = "FAIL" + report.status_extended = f"Network ACL {network_acl.name if network_acl.name else network_acl.id} is not associated with any subnet and is not the default network ACL." + else: + report.status = "PASS" + report.status_extended = f"Network ACL {network_acl.name if network_acl.name else network_acl.id} is associated with a subnet." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.metadata.json b/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.metadata.json index cc0eae560f..2cf5f6cb3e 100644 --- a/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.metadata.json +++ b/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.metadata.json @@ -10,8 +10,8 @@ "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "high", "ResourceType": "AwsEc2SecurityGroup", - "Description": "Ensure no security groups allow ingress from 0.0.0.0/0 or ::/0 to any port.", - "Risk": "If Security groups are not properly configured the attack surface is increased.", + "Description": "Ensure no security groups allow ingress from 0.0.0.0/0 or ::/0 to any port and not attached to a network interface with not allowed network interface types or instance owners. By default, the allowed network interface types are 'api_gateway_managed' and 'vpc_endpoint', and the allowed instance owners are 'amazon-elb', you can customize these values by setting the 'ec2_allowed_interface_types' and 'ec2_allowed_instance_owners' variables.", + "Risk": "The security group allows all traffic from the internet to any port. This could allow an attacker to access the instance.", "RelatedUrl": "", "Remediation": { "Code": { diff --git a/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.py b/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.py index 4a340eccc7..b9b504e6c4 100644 --- a/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.py +++ b/prowler/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port.py @@ -12,26 +12,25 @@ class ec2_securitygroup_allow_ingress_from_internet_to_any_port(Check): def execute(self): findings = [] for security_group_arn, security_group in ec2_client.security_groups.items(): - # Check if ignoring flag is set and if the VPC and the SG is in use - if ec2_client.provider.scan_unused_services or ( - security_group.vpc_id in vpc_client.vpcs - and vpc_client.vpcs[security_group.vpc_id].in_use - and len(security_group.network_interfaces) > 0 + # Only execute the check if the check ec2_securitygroup_allow_ingress_from_internet_to_all_ports has not failed + if not ec2_client.is_failed_check( + ec2_securitygroup_allow_ingress_from_internet_to_all_ports.__name__, + security_group_arn, ): - report = Check_Report_AWS(self.metadata()) - report.region = security_group.region - report.status = "PASS" - report.status_extended = f"Security group {security_group.name} ({security_group.id}) does not have any port open to the Internet." - report.resource_details = security_group.name - report.resource_id = security_group.id - report.resource_arn = security_group_arn - report.resource_tags = security_group.tags - # only proceed if check "..._to_all_ports" did not run or did not FAIL to avoid to report open ports twice - if not ec2_client.is_failed_check( - ec2_securitygroup_allow_ingress_from_internet_to_all_ports.__name__, - security_group_arn, + # Check if ignoring flag is set and if the VPC and the SG is in use + if ec2_client.provider.scan_unused_services or ( + security_group.vpc_id in vpc_client.vpcs + and vpc_client.vpcs[security_group.vpc_id].in_use + and len(security_group.network_interfaces) > 0 ): - # Loop through every security group's ingress rule and check it + report = Check_Report_AWS(self.metadata()) + report.region = security_group.region + report.status = "PASS" + report.status_extended = f"Security group {security_group.name} ({security_group.id}) does not have any port open to the Internet." + report.resource_details = security_group.name + report.resource_id = security_group.id + report.resource_arn = security_group_arn + report.resource_tags = security_group.tags for ingress_rule in security_group.ingress_rules: if check_security_group( ingress_rule, "-1", ports=None, any_address=True @@ -44,11 +43,8 @@ class ec2_securitygroup_allow_ingress_from_internet_to_any_port(Check): ) if report.status == "FAIL": - break # no need to check other ingress rules because at least one failed already - else: - report.status_extended = f"Security group {security_group.name} ({security_group.id}) has all ports open to the Internet and therefore was not checked against a specific port." - - findings.append(report) + break + findings.append(report) return findings @@ -57,46 +53,34 @@ class ec2_securitygroup_allow_ingress_from_internet_to_any_port(Check): report, security_group_name: str, security_group_id: str, - enis: [NetworkInterface], + enis: list[NetworkInterface], ): - report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet but is exclusively not attached to any network interface." + report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet but it is not attached to any network interface." for eni in enis: - if self.is_allowed_eni_type(eni_type=eni.type): + if eni.type in ec2_client.audit_config.get( + "ec2_allowed_interface_types", [] + ): report.status = "PASS" - report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet but is exclusively attached to an allowed network interface type ({eni.type})." + report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet and it is attached to an allowed network interface type ({eni.type})." continue - - eni_owner = self.get_eni_owner(eni=eni) - if self.is_allowed_eni_owner(eni_owner=eni_owner): + if eni.attachment.instance_owner_id in ec2_client.audit_config.get( + "ec2_allowed_instance_owners", [] + ): report.status = "PASS" - report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet but is exclusively attached to an allowed network interface instance owner ({eni_owner})." + report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet and it is attached to an allowed network interface instance owner ({eni.attachment.instance_owner_id})." continue + if eni.type not in ec2_client.audit_config.get( + "ec2_allowed_interface_types", [] + ): + report.status = "FAIL" + report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet but its network interface type ({eni.type}) is not allowed." + elif eni.attachment.instance_owner_id not in ec2_client.audit_config.get( + "ec2_allowed_instance_owners", [] + ): + report.status = "FAIL" + report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet but its network interface instance owner ({eni.attachment.instance_owner_id}) is not allowed." else: report.status = "FAIL" - report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet and neither its network interface type ({eni.type}) nor its network interface instance owner ({eni_owner}) are part of the allowed network interfaces." - - break # no need to check other network interfaces because at least one failed already - - @staticmethod - def is_allowed_eni_type(eni_type: str) -> bool: - return eni_type in ec2_client.audit_config.get( - "ec2_allowed_interface_types", [] - ) - - @staticmethod - def get_eni_owner(eni) -> str: - eni_owner = "" - if ( - hasattr(eni, "attachment") - and isinstance(eni.attachment, dict) - and "InstanceOwnerId" in eni.attachment - ): - eni_owner = eni.attachment["InstanceOwnerId"] - - return eni_owner - - @staticmethod - def is_allowed_eni_owner(eni_owner: str) -> bool: - return eni_owner in ec2_client.audit_config.get( - "ec2_allowed_instance_owners", [] - ) + report.status_extended = f"Security group {security_group_name} ({security_group_id}) has at least one port open to the Internet but neither its network interface type ({eni.type}) nor its network interface instance owner ({eni.attachment.instance_owner_id}) are allowed." + if report.status == "FAIL": + break diff --git a/prowler/providers/aws/services/ec2/ec2_service.py b/prowler/providers/aws/services/ec2/ec2_service.py index c90943b932..7ca7b7719f 100644 --- a/prowler/providers/aws/services/ec2/ec2_service.py +++ b/prowler/providers/aws/services/ec2/ec2_service.py @@ -22,7 +22,7 @@ class EC2(AWSService): self.security_groups = {} self.regions_with_sgs = [] self.__threading_call__(self._describe_security_groups) - self.network_acls = [] + self.network_acls = {} self.__threading_call__(self._describe_network_acls) self.snapshots = [] self.volumes_with_snapshots = {} @@ -72,6 +72,11 @@ class EC2(AWSService): if not self.audit_resources or ( is_resource_filtered(arn, self.audit_resources) ): + enis = [] + for eni in instance.get("NetworkInterfaces", []): + network_interface_id = eni.get("NetworkInterfaceId") + if network_interface_id: + enis.append(network_interface_id) self.instances.append( Instance( id=instance["InstanceId"], @@ -100,6 +105,10 @@ class EC2(AWSService): for sg in instance.get("SecurityGroups", []) ], subnet_id=instance.get("SubnetId", ""), + network_interfaces=enis, + virtualization_type=instance.get( + "VirtualizationType" + ), tags=instance.get("Tags"), ) ) @@ -157,15 +166,20 @@ class EC2(AWSService): for tag in nacl.get("Tags", []): if tag["Key"] == "Name": nacl_name = tag["Value"] - self.network_acls.append( - NetworkACL( - id=nacl["NetworkAclId"], - arn=arn, - name=nacl_name, - region=regional_client.region, - entries=nacl["Entries"], - tags=nacl.get("Tags"), - ) + in_use = False + for subnet in nacl["Associations"]: + if subnet["SubnetId"]: + in_use = True + break + self.network_acls[arn] = NetworkACL( + id=nacl["NetworkAclId"], + arn=arn, + name=nacl_name, + region=regional_client.region, + entries=nacl["Entries"], + tags=nacl.get("Tags"), + in_use=in_use, + default=nacl["IsDefault"], ) except Exception as error: logger.error( @@ -261,11 +275,22 @@ class EC2(AWSService): ipv6_address = ip_address(ipv6_address_str) if ipv6_address.is_global: public_ip_addresses.append(ipv6_address) - + attachment = Attachment( + attachment_id=interface.get("Attachment", {}).get( + "AttachmentId", "" + ), + instance_id=interface.get("Attachment", {}).get( + "InstanceId", "" + ), + instance_owner_id=interface.get("Attachment", {}).get( + "InstanceOwnerId", "" + ), + status=interface.get("Attachment", {}).get("Status", ""), + ) self.network_interfaces[id] = NetworkInterface( id=id, association=interface.get("Association", {}), - attachment=interface.get("Attachment", {}), + attachment=attachment, private_ip=interface.get("PrivateIpAddress"), type=interface["InterfaceType"], subnet_id=interface["SubnetId"], @@ -494,7 +519,7 @@ class EC2(AWSService): for page in describe_launch_templates_paginator.paginate(): for template in page["LaunchTemplates"]: - template_arn = f"arn:aws:ec2:{regional_client.region}:{self.audited_account}:launch-template/{template['LaunchTemplateId']}" + template_arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:launch-template/{template['LaunchTemplateId']}" if not self.audit_resources or ( is_resource_filtered(template_arn, self.audit_resources) ): @@ -530,7 +555,7 @@ class EC2(AWSService): for eni in template_version["LaunchTemplateData"].get( "NetworkInterfaces", [] ): - network_interface_id = eni.get("NetworkInterfaceId") + network_interface_id = eni.get("NetworkInterfaceId", "") if network_interface_id in self.network_interfaces: enis.append(self.network_interfaces[network_interface_id]) if eni.get("AssociatePublicIpAddress", False): @@ -561,7 +586,7 @@ class EC2(AWSService): for page in describe_client_vpn_endpoints_paginator.paginate(): for vpn_endpoint in page["ClientVpnEndpoints"]: - vpn_endpoint_arn = f"arn:aws:ec2:{regional_client.region}:{self.audited_account}:client-vpn-endpoint/{vpn_endpoint['ClientVpnEndpointId']}" + vpn_endpoint_arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:client-vpn-endpoint/{vpn_endpoint['ClientVpnEndpointId']}" if not self.audit_resources or ( is_resource_filtered(vpn_endpoint_arn, self.audit_resources) ): @@ -632,6 +657,8 @@ class Instance(BaseModel): security_groups: list[str] subnet_id: str instance_profile: Optional[dict] + network_interfaces: Optional[list] + virtualization_type: Optional[str] tags: Optional[list] = [] @@ -653,10 +680,17 @@ class Volume(BaseModel): tags: Optional[list] = [] +class Attachment(BaseModel): + attachment_id: str = "" + instance_id: str = "" + instance_owner_id: str = "" + status: str = "" + + class NetworkInterface(BaseModel): id: str association: dict - attachment: dict + attachment: Attachment private_ip: Optional[str] public_ip_addresses: list[Union[IPv4Address, IPv6Address]] type: str @@ -684,6 +718,8 @@ class NetworkACL(BaseModel): name: str region: str entries: list[dict] + default: bool + in_use: bool tags: Optional[list] = [] diff --git a/prowler/providers/aws/services/ecr/ecr_registry_scan_images_on_push_enabled/ecr_registry_scan_images_on_push_enabled.metadata.json b/prowler/providers/aws/services/ecr/ecr_registry_scan_images_on_push_enabled/ecr_registry_scan_images_on_push_enabled.metadata.json index 2549b6826b..3055d2263b 100644 --- a/prowler/providers/aws/services/ecr/ecr_registry_scan_images_on_push_enabled/ecr_registry_scan_images_on_push_enabled.metadata.json +++ b/prowler/providers/aws/services/ecr/ecr_registry_scan_images_on_push_enabled/ecr_registry_scan_images_on_push_enabled.metadata.json @@ -10,7 +10,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:ecr:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsEcrRegistry", + "ResourceType": "Other", "Description": "Check if ECR Registry has scan on push enabled", "Risk": "Amazon ECR image scanning helps in identifying software vulnerabilities in your container images. Amazon ECR uses the Common Vulnerabilities and Exposures (CVEs) database from the open-source Clair project and provides a list of scan findings. ", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py b/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py index 18f3fb9472..17a6742087 100644 --- a/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py +++ b/prowler/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible.py @@ -1,5 +1,6 @@ from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.ecr.ecr_client import ecr_client +from prowler.providers.aws.services.iam.lib.policy import is_policy_public class ecr_repositories_not_publicly_accessible(Check): @@ -14,18 +15,14 @@ class ecr_repositories_not_publicly_accessible(Check): report.resource_tags = repository.tags report.status = "PASS" report.status_extended = ( - f"Repository {repository.name} is not publicly accesible." + f"Repository {repository.name} is not publicly accessible." ) if repository.policy: - for statement in repository.policy["Statement"]: - if statement["Effect"] == "Allow": - if "*" in statement["Principal"] or ( - "AWS" in statement["Principal"] - and "*" in statement["Principal"]["AWS"] - ): - report.status = "FAIL" - report.status_extended = f"Repository {repository.name} policy may allow anonymous users to perform actions (Principal: '*')." - break + if is_policy_public(repository.policy): + report.status = "FAIL" + report.status_extended = ( + f"Repository {repository.name} is publicly accessible." + ) findings.append(report) diff --git a/prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/__init__.py b/prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability.metadata.json b/prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability.metadata.json new file mode 100644 index 0000000000..99eb6a7fa0 --- /dev/null +++ b/prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "ecr_repositories_tag_immutability", + "CheckTitle": "ECR repositories should have tag immutability configured", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ecr", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ecr:{region}:{account-id}:repository/{repository-name}", + "Severity": "medium", + "ResourceType": "AwsEcrRepository", + "Description": "This control checks whether a ECR repository has tag immutability enabled. The control fails if a ECR repository has tag immutability disabled and passes if tag immutability is set to IMMUTABLE.", + "Risk": "Disabling tag immutability in an ECR repository increases the risk of image tampering by allowing tags to be overridden, which can lead to deploying unintended images and security vulnerabilities.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecr-private-tag-immutability-enabled.html", + "Remediation": { + "Code": { + "CLI": "aws ecr put-image-tag-mutability --repository-name --image-tag-mutability IMMUTABLE", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecr-controls.html#ecr-2", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable tag immutability on your ECR repositories to prevent image tags from being overwritten.", + "Url": "https://docs.aws.amazon.com/AmazonECR/latest/userguide/image-tag-mutability.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability.py b/prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability.py new file mode 100644 index 0000000000..c721c2c504 --- /dev/null +++ b/prowler/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ecr.ecr_client import ecr_client + + +class ecr_repositories_tag_immutability(Check): + def execute(self): + findings = [] + for registry in ecr_client.registries.values(): + for repository in registry.repositories: + report = Check_Report_AWS(self.metadata()) + report.region = repository.region + report.resource_id = repository.name + report.resource_arn = repository.arn + report.resource_tags = repository.tags + + report.status = "PASS" + report.status_extended = ( + f"Repository {repository.name} has immutability configured." + ) + + if repository.immutability == "MUTABLE": + report.status = "FAIL" + report.status_extended = f"Repository {repository.name} does not have immutability configured." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ecr/ecr_service.py b/prowler/providers/aws/services/ecr/ecr_service.py index a87e964d08..ea5d6486a8 100644 --- a/prowler/providers/aws/services/ecr/ecr_service.py +++ b/prowler/providers/aws/services/ecr/ecr_service.py @@ -47,6 +47,9 @@ class ECR(AWSService): scan_on_push=repository["imageScanningConfiguration"][ "scanOnPush" ], + immutability=repository.get( + "imageTagMutability", "MUTABLE" + ), policy=None, images_details=[], lifecycle_policy=None, @@ -372,6 +375,7 @@ class Repository(BaseModel): region: str registry_id = str scan_on_push: bool + immutability: Optional[str] policy: Optional[dict] images_details: Optional[list[ImageDetails]] lifecycle_policy: Optional[str] diff --git a/prowler/providers/aws/services/ecs/ecs_service.py b/prowler/providers/aws/services/ecs/ecs_service.py index 7280402cf2..ea2c1b4309 100644 --- a/prowler/providers/aws/services/ecs/ecs_service.py +++ b/prowler/providers/aws/services/ecs/ecs_service.py @@ -13,9 +13,16 @@ class ECS(AWSService): def __init__(self, provider): # Call AWSService's __init__ super().__init__(__class__.__name__, provider) - self.task_definitions = [] + self.task_definitions = {} + self.services = {} + self.clusters = {} self.__threading_call__(self._list_task_definitions) - self._describe_task_definition() + self.__threading_call__( + self._describe_task_definition, self.task_definitions.values() + ) + self.__threading_call__(self._list_clusters) + self.__threading_call__(self._describe_clusters, self.clusters.values()) + self.__threading_call__(self._describe_services, self.clusters.values()) def _list_task_definitions(self, regional_client): logger.info("ECS - Listing Task Definitions...") @@ -26,47 +33,132 @@ class ECS(AWSService): if not self.audit_resources or ( is_resource_filtered(task_definition, self.audit_resources) ): - self.task_definitions.append( - TaskDefinition( - # we want the family name without the revision - name=sub(":.*", "", task_definition.split("/")[1]), - arn=task_definition, - revision=task_definition.split(":")[-1], - region=regional_client.region, - environment_variables=[], - ) + self.task_definitions[task_definition] = TaskDefinition( + # we want the family name without the revision + name=sub(":.*", "", task_definition.split("/")[1]), + arn=task_definition, + revision=task_definition.split(":")[-1], + region=regional_client.region, + environment_variables=[], ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - def _describe_task_definition(self): - logger.info("ECS - Describing Task Definitions...") + def _describe_task_definition(self, task_definition): + logger.info("ECS - Describing Task Definition...") try: - for task_definition in self.task_definitions: - client = self.regional_clients[task_definition.region] - response = client.describe_task_definition( - taskDefinition=task_definition.arn, - include=[ - "TAGS", - ], - ) - container_definitions = response["taskDefinition"][ - "containerDefinitions" - ] - for container in container_definitions: - if "environment" in container: - for env_var in container["environment"]: - task_definition.environment_variables.append( - ContainerEnvVariable( - name=env_var["name"], value=env_var["value"] - ) + client = self.regional_clients[task_definition.region] + response = client.describe_task_definition( + taskDefinition=task_definition.arn, + include=[ + "TAGS", + ], + ) + container_definitions = response["taskDefinition"]["containerDefinitions"] + for container in container_definitions: + environment = [] + if "environment" in container: + for env_var in container["environment"]: + environment.append( + ContainerEnvVariable( + name=env_var["name"], value=env_var["value"] ) - task_definition.tags = response.get("tags") - task_definition.network_mode = response["taskDefinition"].get( - "networkMode" + ) + task_definition.container_definitions.append( + ContainerDefinition( + name=container["name"], + privileged=container.get("privileged", False), + readonly_rootfilesystem=container.get( + "readonlyRootFilesystem", False + ), + user=container.get("user", ""), + environment=environment, + log_driver=container.get("logConfiguration", {}).get( + "logDriver", "" + ), + ) ) + task_definition.pid_mode = response["taskDefinition"].get("pidMode", "") + task_definition.tags = response.get("tags") + task_definition.network_mode = response["taskDefinition"].get( + "networkMode", "bridge" + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _describe_services(self, cluster): + logger.info("ECS - Describing Services for each Cluster...") + try: + client = self.regional_clients[cluster.region] + + list_ecs_paginator = client.get_paginator("list_services") + service_arns = [] + for page in list_ecs_paginator.paginate(cluster=cluster.arn): + service_arns.extend(page["serviceArns"]) + + if service_arns: + for service_arn in service_arns: + describe_response = client.describe_services( + cluster=cluster.arn, + services=[service_arn], + include=["TAGS"], + ) + + service_desc = describe_response["services"][0] + service_arn = service_desc["serviceArn"] + service_obj = Service( + name=sub(":.*", "", service_arn.split("/")[2]), + arn=service_arn, + region=cluster.region, + assign_public_ip=( + service_desc.get("networkConfiguration", {}) + .get("awsvpcConfiguration", {}) + .get("assignPublicIp", "DISABLED") + == "ENABLED" + ), + tags=service_desc.get("tags", []), + ) + cluster.services[service_arn] = service_obj + self.services[service_arn] = service_obj + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _list_clusters(self, regional_client): + logger.info("ECS - Listing Clusters...") + try: + list_ecs_paginator = regional_client.get_paginator("list_clusters") + for page in list_ecs_paginator.paginate(): + for cluster in page["clusterArns"]: + if not self.audit_resources or ( + is_resource_filtered(cluster, self.audit_resources) + ): + self.clusters[cluster] = Cluster( + name=sub(":.*", "", cluster.split("/")[1]), + arn=cluster, + region=regional_client.region, + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _describe_clusters(self, cluster): + logger.info("ECS - Describing Clusters...") + try: + client = self.regional_clients[cluster.region] + response = client.describe_clusters( + clusters=[cluster.arn], + include=[ + "TAGS", + ], + ) + cluster.tags = response["clusters"][0].get("tags", []) except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -78,11 +170,37 @@ class ContainerEnvVariable(BaseModel): value: str +class ContainerDefinition(BaseModel): + name: str + privileged: bool + readonly_rootfilesystem: bool = False + user: str + environment: list[ContainerEnvVariable] + log_driver: Optional[str] + + class TaskDefinition(BaseModel): name: str arn: str revision: str region: str - environment_variables: list[ContainerEnvVariable] + container_definitions: list[ContainerDefinition] = [] + pid_mode: Optional[str] tags: Optional[list] = [] network_mode: Optional[str] + + +class Service(BaseModel): + name: str + arn: str + region: str + assign_public_ip: Optional[bool] + tags: Optional[list] = [] + + +class Cluster(BaseModel): + name: str + arn: str + region: str + services: dict = {} + tags: Optional[list] = [] diff --git a/prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/__init__.py b/prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip.metadata.json b/prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip.metadata.json new file mode 100644 index 0000000000..42b7f36cb1 --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "ecs_service_no_assign_public_ip", + "CheckTitle": "ECS services should not assign public IPs automatically", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ecs", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ecs:{region}:{account-id}:service/{service-name}", + "Severity": "high", + "ResourceType": "AwsEcsService", + "Description": "This control checks whether Amazon ECS services are configured to automatically assign public IP addresses. The control fails if AssignPublicIP is ENABLED and passes if it is DISABLED.", + "Risk": "Having public IP addresses assigned to ECS services automatically can expose services to the internet, increasing the risk of unauthorized access, data breaches, and cyberattacks.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html", + "Remediation": { + "Code": { + "CLI": "aws ecs update-service --cluster --service --network-configuration 'awsvpcConfiguration={assignPublicIp=DISABLED}'", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-2", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable automatic public IP address assignment for your ECS services to ensure they are not publicly accessible.", + "Url": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/security.html" + } + }, + "Categories": [ + "internet-exposed" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip.py b/prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip.py new file mode 100644 index 0000000000..89eacae7cb --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ecs.ecs_client import ecs_client + + +class ecs_service_no_assign_public_ip(Check): + def execute(self): + findings = [] + for service in ecs_client.services.values(): + report = Check_Report_AWS(self.metadata()) + report.region = service.region + report.resource_id = service.name + report.resource_arn = service.arn + report.resource_tags = service.tags + report.status = "PASS" + report.status_extended = f"ECS Service {service.name} does not have automatic public IP assignment." + + if service.assign_public_ip: + report.status = "FAIL" + report.status_extended = ( + f"ECS Service {service.name} has automatic public IP assignment." + ) + + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/__init__.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.metadata.json b/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.metadata.json new file mode 100644 index 0000000000..9e320892f6 --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "ecs_task_definitions_containers_readonly_access", + "CheckTitle": "ECS containers should be limited to read-only access to root filesystems", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ecs", + "SubServiceName": "taskDefinition", + "ResourceIdTemplate": "arn:aws:ecs:{region}:{account-id}:task-definition/{task-definition-name}", + "Severity": "high", + "ResourceType": "AwsEcsTaskDefinition", + "Description": "This control checks if Amazon ECS containers are limited to read-only access to mounted root filesystems. The control fails if the readonlyRootFilesystem parameter is set to false or if the parameter doesn't exist in the container definition.", + "Risk": "If ECS containers have write access to root filesystems, it increases the risk of filesystem tampering and exploitation of vulnerabilities, violating the principle of least privilege.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecs-containers-readonly-access.html", + "Remediation": { + "Code": { + "CLI": "aws ecs register-task-definition --family --container-definitions '[{\"name\":\"\",\"image\":\"\",\"readonlyRootFilesystem\":true}]'", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-5", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that containers have read-only access to their root filesystems to limit write access and reduce the attack surface.", + "Url": "https://docs.aws.amazon.com/AmazonECS/latest/userguide/task_definition_parameters.html#container_definition_readonly" + } + }, + "Categories": [ + "vulnerabilities" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.py new file mode 100644 index 0000000000..cf423f8b13 --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ecs.ecs_client import ecs_client + + +class ecs_task_definitions_containers_readonly_access(Check): + def execute(self): + findings = [] + for task_definition in ecs_client.task_definitions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = task_definition.region + report.resource_id = f"{task_definition.name}:{task_definition.revision}" + report.resource_arn = task_definition.arn + report.resource_tags = task_definition.tags + report.status = "PASS" + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} does not have containers with write access to the root filesystems." + + failed_containers = [] + for container in task_definition.container_definitions: + if not container.readonly_rootfilesystem: + report.status = "FAIL" + failed_containers.append(container.name) + + if failed_containers: + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} has containers with write access to the root filesystem: {', '.join(failed_containers)}" + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/__init__.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared.metadata.json b/prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared.metadata.json new file mode 100644 index 0000000000..b296ca930c --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "ecs_task_definitions_host_namespace_not_shared", + "CheckTitle": "ECS task definitions should not share the host's process namespace", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ecs", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ecs:{region}:{account-id}:task-definition/{task-definition-name}", + "Severity": "high", + "ResourceType": "AwsEcsTaskDefinition", + "Description": "This control checks if Amazon ECS task definitions are configured to share a host's process namespace with its containers. The control fails if the task definition shares the host's process namespace.", + "Risk": "Sharing the host's process namespace with containers exposes host processes, potentially allowing unauthorized access or manipulation of host-level processes, undermining container isolation.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecs-task-definition-pid-mode-check.html", + "Remediation": { + "Code": { + "CLI": "aws ecs register-task-definition --family --container-definitions '[{\"name\":\"\",\"image\":\"\",\"pidMode\":\"task\"}]'", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-3", + "Terraform": "" + }, + "Recommendation": { + "Text": "Avoid sharing the host's process namespace with containers to maintain process isolation and enhance security.", + "Url": "https://docs.aws.amazon.com/AmazonECS/latest/userguide/task_definition_parameters.html#container_definition_pid_mode" + } + }, + "Categories": [ + "vulnerabilities" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared.py new file mode 100644 index 0000000000..e85c9d1c17 --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared.py @@ -0,0 +1,20 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ecs.ecs_client import ecs_client + + +class ecs_task_definitions_host_namespace_not_shared(Check): + def execute(self): + findings = [] + for task_definition in ecs_client.task_definitions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = task_definition.region + report.resource_id = f"{task_definition.name}:{task_definition.revision}" + report.resource_arn = task_definition.arn + report.resource_tags = task_definition.tags + report.status = "PASS" + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} does not share a host's process namespace with its containers." + if task_definition.pid_mode == "host": + report.status = "FAIL" + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} is configured to share a host's process namespace with its containers." + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/__init__.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users.metadata.json b/prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users.metadata.json new file mode 100644 index 0000000000..6c1121ceb4 --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "ecs_task_definitions_host_networking_mode_users", + "CheckTitle": "Amazon ECS task definitions should have secure networking modes and user definitions", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ecs", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ecs:region:account-id:task-definition/resource-id", + "Severity": "high", + "ResourceType": "AwsEcsTaskDefinition", + "Description": "This control checks whether an active Amazon ECS task definition with host networking mode has privileged or user container definitions. The control fails for task definitions that have host network mode and container definitions of privileged=false or empty, and user=root or empty.", + "Risk": "If ECS tasks are configured with host networking and either lack a defined user or run with elevated privileges, this can lead to privilege escalation, unauthorized access to resources, and increased exposure to vulnerabilities.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecs-task-definition-user-for-host-mode-check.html", + "Remediation": { + "Code": { + "CLI": "aws ecs update-task-definition --task-definition --network-mode awsvpc --requires-compatibilities FARGATE --user ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-6", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure your ECS task definitions use the awsvpc networking mode and have secure user configurations when using host networking mode.", + "Url": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/update-task-definition-console-v2.html" + } + }, + "Categories": [ + "trustboundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users.py new file mode 100644 index 0000000000..5b4ed9b153 --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users.py @@ -0,0 +1,30 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ecs.ecs_client import ecs_client + + +class ecs_task_definitions_host_networking_mode_users(Check): + def execute(self): + findings = [] + for task_definition in ecs_client.task_definitions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = task_definition.region + report.resource_id = f"{task_definition.name}:{task_definition.revision}" + report.resource_arn = task_definition.arn + report.resource_tags = task_definition.tags + report.status = "PASS" + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} does not have host network mode." + failed_containers = [] + if task_definition.network_mode == "host": + for container in task_definition.container_definitions: + if not container.privileged and ( + container.user == "root" or container.user == "" + ): + report.status = "FAIL" + failed_containers.append(container.name) + + if failed_containers: + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} has containers with host network mode and non-privileged containers running as root or with no user specified: {', '.join(failed_containers)}" + else: + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} has host network mode but no containers running as root or with no user specified." + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/__init__.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled.metadata.json b/prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled.metadata.json new file mode 100644 index 0000000000..f639f04e8e --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "ecs_task_definitions_logging_enabled", + "CheckTitle": "ECS task definitions containers should have a logging configuration", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ecs", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:ecs:{region}:{account-id}:task-definition/{task-definition-name}", + "Severity": "high", + "ResourceType": "AwsEcsTaskDefinition", + "Description": "This control checks if the latest active Amazon ECS task definition has a logging configuration specified. The control fails if the task definition doesn't have the logConfiguration property defined or if the value for logDriver is null in at least one container definition.", + "Risk": "Without a logging configuration, important data may be lost, making it difficult to troubleshoot issues, monitor performance, and ensure compliance with auditing requirements.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecs-task-definition-log-configuration.html", + "Remediation": { + "Code": { + "CLI": "aws ecs register-task-definition --family --container-definitions '[{\"name\":\"\",\"image\":\"\",\"logConfiguration\":{\"logDriver\":\"awslogs\",\"options\":{\"awslogs-group\":\"\",\"awslogs-region\":\"\",\"awslogs-stream-prefix\":\"ecs\"}}}]'", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-9", + "Terraform": "" + }, + "Recommendation": { + "Text": "Define a logging configuration in the ECS task definition to ensure important data is captured and available for debugging, monitoring, and auditing purposes.", + "Url": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_awslogs.html#specify-log-config" + } + }, + "Categories": [ + "logging" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled.py new file mode 100644 index 0000000000..8180c3a83e --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled.py @@ -0,0 +1,26 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ecs.ecs_client import ecs_client + + +class ecs_task_definitions_logging_enabled(Check): + def execute(self): + findings = [] + for task_definition in ecs_client.task_definitions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = task_definition.region + report.resource_id = f"{task_definition.name}:{task_definition.revision}" + report.resource_arn = task_definition.arn + report.resource_tags = task_definition.tags + report.status = "PASS" + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} containers have logging configured." + failed_containers = [] + for container in task_definition.container_definitions: + if not container.log_driver: + report.status = "FAIL" + failed_containers.append(container.name) + + if failed_containers: + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} has containers running with no logging configuration: {', '.join(failed_containers)}" + + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets.py index 3c223c1c4b..53fe501f9c 100644 --- a/prowler/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets.py +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets.py @@ -11,33 +11,48 @@ class ecs_task_definitions_no_environment_secrets(Check): secrets_ignore_patterns = ecs_client.audit_config.get( "secrets_ignore_patterns", [] ) - for task_definition in ecs_client.task_definitions: + for task_definition in ecs_client.task_definitions.values(): report = Check_Report_AWS(self.metadata()) report.region = task_definition.region report.resource_id = f"{task_definition.name}:{task_definition.revision}" report.resource_arn = task_definition.arn report.resource_tags = task_definition.tags report.status = "PASS" - report.status_extended = f"No secrets found in variables of ECS task definition {task_definition.name} with revision {task_definition.revision}." - if task_definition.environment_variables: - dump_env_vars = {} - for env_var in task_definition.environment_variables: - dump_env_vars.update({env_var.name: env_var.value}) + extended_status_parts = [] - env_data = dumps(dump_env_vars, indent=2) - detect_secrets_output = detect_secrets_scan( - data=env_data, excluded_secrets=secrets_ignore_patterns - ) - if detect_secrets_output: - secrets_string = ", ".join( - [ - f"{secret['type']} on line {secret['line_number']}" - for secret in detect_secrets_output - ] + for container in task_definition.container_definitions: + container_secrets_found = [] + + if container.environment: + dump_env_vars = {} + for env_var in container.environment: + dump_env_vars.update({env_var.name: env_var.value}) + + env_data = dumps(dump_env_vars, indent=2) + detect_secrets_output = detect_secrets_scan( + data=env_data, excluded_secrets=secrets_ignore_patterns ) + if detect_secrets_output: + secrets_string = ", ".join( + [ + f"{secret['type']} on line {secret['line_number']}" + for secret in detect_secrets_output + ] + ) + container_secrets_found.append( + f"Secrets in container {container.name} -> {secrets_string}" + ) + if container_secrets_found: report.status = "FAIL" - report.status_extended = f"Potential secret found in variables of ECS task definition {task_definition.name} with revision {task_definition.revision} -> {secrets_string}." - + extended_status_parts.extend(container_secrets_found) + if report.status == "FAIL": + report.status_extended = ( + f"Potential secrets found in ECS task definition {task_definition.name} with revision {task_definition.revision}: " + + "; ".join(extended_status_parts) + + "." + ) + else: + report.status_extended = f"No secrets found in variables of ECS task definition {task_definition.name} with revision {task_definition.revision}." findings.append(report) return findings diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/__init__.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers.metadata.json b/prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers.metadata.json new file mode 100644 index 0000000000..79cb560d0b --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "ecs_task_definitions_no_privileged_containers", + "CheckTitle": "ECS task definitions shouldn't have privileged containers", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "ecs", + "SubServiceName": "taskDefinition", + "ResourceIdTemplate": "arn:aws:ecs:{region}:{account-id}:task-definition/{task-definition-name}", + "Severity": "high", + "ResourceType": "AwsEcsTaskDefinition", + "Description": "This control checks if the privileged parameter in the container definition of Amazon ECS Task Definitions is set to true. The control fails if this parameter is equal to true.", + "Risk": "Running containers with elevated privileges increases the risk of privilege escalation attacks, potentially allowing unauthorized access to the host and other containers.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/ecs-containers-nonprivileged.html", + "Remediation": { + "Code": { + "CLI": "aws ecs register-task-definition --family --container-definitions '[{\"name\":\"\",\"image\":\"\",\"privileged\":false}]'", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ecs-controls.html#ecs-4", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that containers are running without elevated privileges to minimize the risk of privilege escalation.", + "Url": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_security" + } + }, + "Categories": [ + "vulnerabilities" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers.py b/prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers.py new file mode 100644 index 0000000000..3618889c39 --- /dev/null +++ b/prowler/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ecs.ecs_client import ecs_client + + +class ecs_task_definitions_no_privileged_containers(Check): + def execute(self): + findings = [] + for task_definition in ecs_client.task_definitions.values(): + report = Check_Report_AWS(self.metadata()) + report.region = task_definition.region + report.resource_id = f"{task_definition.name}:{task_definition.revision}" + report.resource_arn = task_definition.arn + report.resource_tags = task_definition.tags + report.status = "PASS" + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} does not have privileged containers." + failed_containers = [] + for container in task_definition.container_definitions: + if container.privileged: + report.status = "FAIL" + failed_containers.append(container.name) + + if failed_containers: + report.status_extended = f"ECS task definition {task_definition.name} with revision {task_definition.revision} has privileged containers: {', '.join(failed_containers)}" + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/efs/efs_encryption_at_rest_enabled/efs_encryption_at_rest_enabled.metadata.json b/prowler/providers/aws/services/efs/efs_encryption_at_rest_enabled/efs_encryption_at_rest_enabled.metadata.json index 23938f0390..b6c6b8a207 100644 --- a/prowler/providers/aws/services/efs/efs_encryption_at_rest_enabled/efs_encryption_at_rest_enabled.metadata.json +++ b/prowler/providers/aws/services/efs/efs_encryption_at_rest_enabled/efs_encryption_at_rest_enabled.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsEFSFileSystem", + "ResourceType": "AwsEfsFileSystem", "Description": "Check if EFS protects sensitive data with encryption at rest", "Risk": "EFS should be encrypted at rest to prevent exposure of sensitive data to bad actors", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/efs/efs_have_backup_enabled/efs_have_backup_enabled.metadata.json b/prowler/providers/aws/services/efs/efs_have_backup_enabled/efs_have_backup_enabled.metadata.json index 9bb2816148..d1b62abbb0 100644 --- a/prowler/providers/aws/services/efs/efs_have_backup_enabled/efs_have_backup_enabled.metadata.json +++ b/prowler/providers/aws/services/efs/efs_have_backup_enabled/efs_have_backup_enabled.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsEFSFileSystem", + "ResourceType": "AwsEfsFileSystem", "Description": "Check if EFS File systems have backup enabled", "Risk": "If backup is not enabled, data is vulnerable. Human error or bad actors could erase or modify data.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.metadata.json b/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.metadata.json index a20ee155cb..8b99804160 100644 --- a/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.metadata.json +++ b/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.metadata.json @@ -10,7 +10,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsEFSFileSystem", + "ResourceType": "AwsEfsFileSystem", "Description": "Check if EFS have policies which allow access to any client within the VPC", "Risk": "Restricting access to EFS file systems is a security best practice. Allowing access to any client within the VPC can lead to unauthorized access to the file system.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.py b/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.py index 98c348e139..73d89b938e 100644 --- a/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.py +++ b/prowler/providers/aws/services/efs/efs_not_publicly_accessible/efs_not_publicly_accessible.py @@ -1,6 +1,6 @@ from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.efs.efs_client import efs_client -from prowler.providers.aws.services.efs.lib.lib import is_public_access_allowed +from prowler.providers.aws.services.iam.lib.policy import is_policy_public class efs_not_publicly_accessible(Check): @@ -17,13 +17,14 @@ class efs_not_publicly_accessible(Check): if not fs.policy: report.status = "FAIL" report.status_extended = f"EFS {fs.id} doesn't have any policy which means it grants full access to any client within the VPC." - else: - for statement in fs.policy.get("Statement", []): - if statement.get("Effect") == "Allow" and is_public_access_allowed( - statement - ): - report.status = "FAIL" - report.status_extended = f"EFS {fs.id} has a policy which allows access to any client within the VPC." - break + elif is_policy_public(fs.policy) and any( + statement.get("Condition", {}) + .get("Bool", {}) + .get("elasticfilesystem:AccessedViaMountTarget", "false") + != "true" + for statement in fs.policy.get("Statement", []) + ): + report.status = "FAIL" + report.status_extended = f"EFS {fs.id} has a policy which allows access to any client within the VPC." findings.append(report) return findings diff --git a/prowler/providers/aws/services/efs/lib/lib.py b/prowler/providers/aws/services/efs/lib/lib.py deleted file mode 100644 index 2d310fc369..0000000000 --- a/prowler/providers/aws/services/efs/lib/lib.py +++ /dev/null @@ -1,45 +0,0 @@ -def is_public_access_allowed(statement): - """ - Check if the statement allows public access - Args: - statement: dict: Statement from the policy - Returns: - bool: True if the statement allows public access, False otherwise - """ - principal = statement.get("Principal") - if principal == "*" or (isinstance(principal, dict) and "*" in principal.values()): - return not has_secure_conditions(statement) - return False - - -def has_secure_conditions(statement): - """ - Check if the statement has secure conditions - Args: - statement: dict: Statement from the policy - Returns: - bool: True if the statement has secure conditions, False otherwise - """ - conditions = statement.get("Condition", {}) - allowed_conditions = { - "aws:SourceArn", - "aws:SourceVpc", - "aws:SourceVpce", - "aws:SourceOwner", - "aws:SourceAccount", - } - if ( - "Bool" in conditions - and conditions["Bool"].get("elasticfilesystem:AccessedViaMountTarget") == "true" - ): - return True - - # Check for conditions with nested keys - for _, conditions_dict in conditions.items(): - for key, value in conditions_dict.items(): - if isinstance(value, dict): - if set(value.keys()).intersection(allowed_conditions): - return True - elif key in allowed_conditions: - return True - return False diff --git a/prowler/providers/aws/services/elasticache/elasticache_cluster_uses_public_subnet/elasticache_cluster_uses_public_subnet.metadata.json b/prowler/providers/aws/services/elasticache/elasticache_cluster_uses_public_subnet/elasticache_cluster_uses_public_subnet.metadata.json index e6b0e497c3..8c29c80813 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_cluster_uses_public_subnet/elasticache_cluster_uses_public_subnet.metadata.json +++ b/prowler/providers/aws/services/elasticache/elasticache_cluster_uses_public_subnet/elasticache_cluster_uses_public_subnet.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AWSElastiCacheCacheCluster", + "ResourceType": "Other", "Description": "Ensure Elasticache Cluster is not using a public subnet", "Risk": "There is a risk of exposing sensitive data if Elasticache Cluster uses a public subnet.", "RelatedUrl": "https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/VPCs.html", diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.metadata.json b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.metadata.json index 80cef26283..88148ce229 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.metadata.json +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "high", - "ResourceType": "AWSElastiCacheClusters", + "ResourceType": "Other", "Description": "Ensure Elasticache Redis cache clusters have automatic minor upgrades enabled.", "Risk": "Not enabling automatic minor version upgrades can expose your Redis cluster to security vulnerabilities, performance issues, and increased operational overhead due to the need for manual updates.", "RelatedUrl": "https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/VersionManagement.html", diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/__init__.py b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled.metadata.json b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled.metadata.json new file mode 100644 index 0000000000..d0c0cc9333 --- /dev/null +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "elasticache_redis_cluster_automatic_failover_enabled", + "CheckTitle": "Ensure Elasticache Redis clusters have automatic failover enabled.", + "CheckType": [], + "ServiceName": "elasticache", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "medium", + "ResourceType": "Other", + "Description": "Ensure Elasticache Redis OSS cache clusters use automatic failover.", + "Risk": "If automatic failover is not enabled, a failure in the primary node could result in significant downtime, impacting the availability and resilience of your application.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/AutoFailover.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/elasticache-controls.html#elasticache-3", + "Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-aws-elasticache-redis-cluster-with-multi-az-automatic-failover-feature-set-to-enabled/" + }, + "Recommendation": { + "Text": "Enable automatic failover for ElastiCache (Redis OSS) clusters to ensure high availability and minimize downtime during failures.", + "Url": "https://redis.io/blog/highly-available-in-memory-cloud-datastores/" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled.py b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled.py new file mode 100644 index 0000000000..e3dd36fc33 --- /dev/null +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.elasticache.elasticache_client import ( + elasticache_client, +) + + +class elasticache_redis_cluster_automatic_failover_enabled(Check): + def execute(self): + findings = [] + for repl_group in elasticache_client.replication_groups.values(): + report = Check_Report_AWS(self.metadata()) + report.region = repl_group.region + report.resource_id = repl_group.id + report.resource_arn = repl_group.arn + report.resource_tags = repl_group.tags + report.status = "FAIL" + report.status_extended = f"Elasticache Redis cache cluster {repl_group.id} does not have automatic failover enabled." + + if repl_group.automatic_failover == "enabled": + report.status = "PASS" + report.status_extended = f"Elasticache Redis cache cluster {repl_group.id} does have automatic failover enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_backup_enabled/elasticache_redis_cluster_backup_enabled.metadata.json b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_backup_enabled/elasticache_redis_cluster_backup_enabled.metadata.json index 45606f0718..bf8762ee6b 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_backup_enabled/elasticache_redis_cluster_backup_enabled.metadata.json +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_backup_enabled/elasticache_redis_cluster_backup_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "high", - "ResourceType": "AWSElastiCacheClusters", + "ResourceType": "Other", "Description": "Ensure Elasticache Redis cache cluster has automatic backups enabled.", "Risk": "Ensure that your Amazon ElastiCache Redis cache clusters have a sufficient backup retention period set in order to fulfill your organization's compliance requirements. The retention period represents the number of days for which Amazon ElastiCache service retains automatic Redis cluster backups before deleting them.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/elasticache-controls.html#elasticache-1", diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_in_transit_encryption_enabled/elasticache_redis_cluster_in_transit_encryption_enabled.metadata.json b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_in_transit_encryption_enabled/elasticache_redis_cluster_in_transit_encryption_enabled.metadata.json index 02c405ff94..891fdd4112 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_in_transit_encryption_enabled/elasticache_redis_cluster_in_transit_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_in_transit_encryption_enabled/elasticache_redis_cluster_in_transit_encryption_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AWSElastiCacheReplicationGroups", + "ResourceType": "Other", "Description": "Ensure Elasticache Redis cache clusters have in transit encryption enabled.", "Risk": "There is a risk of exposing sensitive data if Elasticache Redis cache cluster does not have in transit encryption enabled.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/elasticache-controls.html#elasticache-5", diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_multi_az_enabled/elasticache_redis_cluster_multi_az_enabled.metadata.json b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_multi_az_enabled/elasticache_redis_cluster_multi_az_enabled.metadata.json index 9246952f40..362f7e0a01 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_multi_az_enabled/elasticache_redis_cluster_multi_az_enabled.metadata.json +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_multi_az_enabled/elasticache_redis_cluster_multi_az_enabled.metadata.json @@ -1,14 +1,14 @@ { "Provider": "aws", "CheckID": "elasticache_redis_cluster_multi_az_enabled", - "CheckTitle": "Ensure Elasticache Elasticache Redis cache cluster has Multi-AZ enabled.", + "CheckTitle": "Ensure Elasticache Redis cache cluster has Multi-AZ enabled.", "CheckType": [], "ServiceName": "elasticache", "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", "ResourceType": "AWSElastiCacheClusters", - "Description": "Ensure Elasticache Elasticache Redis cache cluster has Multi-AZ enabled.", + "Description": "Ensure Elasticache Redis cache cluster has Multi-AZ enabled.", "Risk": "Ensure that your Amazon ElastiCache Redis cache clusters has Multi-AZ enabled.", "RelatedUrl": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/ElastiCache/elasticache-multi-az.html#", "Remediation": { @@ -19,7 +19,7 @@ "Terraform": "https://docs.prowler.com/checks/aws/general-policies/ensure-aws-elasticache-redis-cluster-with-multi-az-automatic-failover-feature-set-to-enabled/" }, "Recommendation": { - "Text": "Ensure Elasticache Elasticache Redis cache cluster has Multi-AZ enabled.", + "Text": "Ensure Elasticache Redis cache cluster has Multi-AZ enabled.", "Url": "https://www.trendmicro.com/cloudoneconformity-staging/knowledge-base/aws/ElastiCache/elasticache-multi-az.html#" } }, diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_rest_encryption_enabled/elasticache_redis_cluster_rest_encryption_enabled.metadata.json b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_rest_encryption_enabled/elasticache_redis_cluster_rest_encryption_enabled.metadata.json index 8540350e28..bacef75443 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_rest_encryption_enabled/elasticache_redis_cluster_rest_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_cluster_rest_encryption_enabled/elasticache_redis_cluster_rest_encryption_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AWSElastiCacheReplicationGroups", + "ResourceType": "Other", "Description": "Ensure Elasticache Redis cache clusters have at rest encryption enabled.", "Risk": "There is a risk of exposing sensitive data if Elasticache Redis cache clusters does not have at rest encryption enabled.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/elasticache-controls.html#elasticache-4", diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/__init__.py b/prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled.metadata.json b/prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled.metadata.json new file mode 100644 index 0000000000..941799aa18 --- /dev/null +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "elasticache_redis_replication_group_auth_enabled", + "CheckTitle": "Ensure Elasticache Elasticache Redis replication groups of earlier versions should have Redis OSS AUTH enabled.", + "CheckType": [ + "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53 Controls" + ], + "ServiceName": "elasticache", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "medium", + "ResourceType": "Other", + "Description": "Ensure Elasticache Redis replication groups of earlier versions use Redis OSS AUTH.", + "Risk": "Without Redis AUTH enabled, your ElastiCache (Redis) instance is vulnerable to unauthorized access and potential data breaches.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/auth.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/elasticache-controls.html#elasticache-6", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable Redis AUTH to require authentication before accessing your Redis instance, and for Redis 6.0 and later, consider implementing Role-Based Access Control (RBAC) for enhanced security.", + "Url": "https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/auth.html#auth-modifyng-token" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled.py b/prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled.py new file mode 100644 index 0000000000..6bdaa1170b --- /dev/null +++ b/prowler/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled.py @@ -0,0 +1,33 @@ +from packaging import version + +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.elasticache.elasticache_client import ( + elasticache_client, +) + + +class elasticache_redis_replication_group_auth_enabled(Check): + def execute(self): + findings = [] + for repl_group in elasticache_client.replication_groups.values(): + report = Check_Report_AWS(self.metadata()) + report.region = repl_group.region + report.resource_id = repl_group.id + report.resource_arn = repl_group.arn + report.resource_tags = repl_group.tags + + if version.parse(repl_group.engine_version) < version.parse("6.0"): + if not repl_group.auth_token_enabled: + report.status = "FAIL" + report.status_extended = f"Elasticache Redis replication group {repl_group.id}(v{repl_group.engine_version}) does not have AUTH enabled." + + else: + report.status = "PASS" + report.status_extended = f"Elasticache Redis replication group {repl_group.id}(v{repl_group.engine_version}) does have AUTH enabled." + else: + report.status = "MANUAL" + report.status_extended = f"Elasticache Redis replication group {repl_group.id} has version {repl_group.engine_version} which supports Redis ACLs. Please review the ACL configuration." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/elasticache/elasticache_service.py b/prowler/providers/aws/services/elasticache/elasticache_service.py index f594c93da0..5fc295630e 100644 --- a/prowler/providers/aws/services/elasticache/elasticache_service.py +++ b/prowler/providers/aws/services/elasticache/elasticache_service.py @@ -41,6 +41,10 @@ class ElastiCache(AWSService): auto_minor_version_upgrade=cache_cluster.get( "AutoMinorVersionUpgrade", False ), + engine_version=cache_cluster.get("EngineVersion", "0.0"), + auth_token_enabled=cache_cluster.get( + "AuthTokenEnabled", False + ), ) except Exception as error: logger.error( @@ -90,6 +94,13 @@ class ElastiCache(AWSService): if not self.audit_resources or ( is_resource_filtered(replication_arn, self.audit_resources) ): + # Get first cluster version as they all have the same unless an upgrade is being made + member_clusters = repl_group.get("MemberClusters", []) + engine_version = "0.0" + if member_clusters: + cluster_arn = f"arn:{self.audited_partition}:elasticache:{regional_client.region}:{self.audited_account}:cluster:{member_clusters[0]}" + engine_version = self.clusters[cluster_arn].engine_version + self.replication_groups[replication_arn] = ReplicationGroup( id=repl_group["ReplicationGroupId"], arn=replication_arn, @@ -106,6 +117,13 @@ class ElastiCache(AWSService): auto_minor_version_upgrade=repl_group.get( "AutoMinorVersionUpgrade", False ), + automatic_failover=repl_group.get( + "AutomaticFailover", "disabled" + ), + auth_token_enabled=repl_group.get( + "AuthTokenEnabled", False + ), + engine_version=engine_version, ) except Exception as error: logger.error( @@ -164,6 +182,8 @@ class Cluster(BaseModel): subnets: list = [] tags: Optional[list] auto_minor_version_upgrade: bool = False + engine_version: Optional[str] + auth_token_enabled: Optional[bool] class ReplicationGroup(BaseModel): @@ -176,4 +196,7 @@ class ReplicationGroup(BaseModel): transit_encryption: bool multi_az: str tags: Optional[list] - auto_minor_version_upgrade: bool = False + auth_token_enabled: bool + auto_minor_version_upgrade: bool + automatic_failover: str + engine_version: str diff --git a/prowler/providers/aws/services/elb/elb_connection_draining_enabled/__init__.py b/prowler/providers/aws/services/elb/elb_connection_draining_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.metadata.json b/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.metadata.json new file mode 100644 index 0000000000..1876ecc34c --- /dev/null +++ b/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "elb_connection_draining_enabled", + "CheckTitle": "Classic Load Balancer Connection Draining Enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Network Reachability" + ], + "ServiceName": "elb", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "medium", + "ResourceType": "AwsElbLoadBalancer", + "Description": "Checks if connection draining is enabled for Classic Load Balancers. Connection draining ensures that the load balancer stops sending requests to instances that are de-registering or unhealthy, while keeping existing connections open. This is particularly useful for instances in Auto Scaling groups, to ensure that connections aren't severed abruptly.", + "Risk": "Disabling connection draining can lead to abrupt connection termination for users, impacting the user experience and potentially causing application errors.", + "RelatedUrl": "https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/config-conn-drain.html", + "Remediation": { + "Code": { + "CLI": "aws elb modify-load-balancer-attributes --load-balancer-name --load-balancer-attributes '{'ConnectionDraining':{'Enabled':true,'Timeout':300}}'", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/elb-controls.html#elb-7", + "Terraform": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/ELB/elb-connection-draining-enabled.html#" + }, + "Recommendation": { + "Text": "Enable connection draining for all Classic Load Balancers. This ensures that existing connections are not abruptly terminated when instances are removed from the load balancer.", + "Url": "" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.py b/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.py new file mode 100644 index 0000000000..0c146fb516 --- /dev/null +++ b/prowler/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.elb.elb_client import elb_client + + +class elb_connection_draining_enabled(Check): + def execute(self) -> list[Check_Report_AWS]: + findings = [] + for loadbalancer_arn, load_balancer in elb_client.loadbalancers.items(): + report = Check_Report_AWS(self.metadata()) + report.region = load_balancer.region + report.resource_id = load_balancer.name + report.resource_arn = loadbalancer_arn + report.resource_tags = load_balancer.tags + report.status = "PASS" + report.status_extended = ( + f"ELB {load_balancer.name} has connection draining enabled." + ) + + if not load_balancer.connection_draining: + report.status = "FAIL" + report.status_extended = f"ELB {load_balancer.name} does not have connection draining enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/elb/elb_cross_zone_load_balancing_enabled/elb_cross_zone_load_balancing_enabled.metadata.json b/prowler/providers/aws/services/elb/elb_cross_zone_load_balancing_enabled/elb_cross_zone_load_balancing_enabled.metadata.json index 006a67d7ae..f107add36c 100644 --- a/prowler/providers/aws/services/elb/elb_cross_zone_load_balancing_enabled/elb_cross_zone_load_balancing_enabled.metadata.json +++ b/prowler/providers/aws/services/elb/elb_cross_zone_load_balancing_enabled/elb_cross_zone_load_balancing_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AWSElasticLoadBalancingLoadBalancer", + "ResourceType": "AwsElbLoadBalancer", "Description": "Checks whether cross-zone load balancing is enabled for Classic Load Balancers (CLBs). Cross-zone load balancing ensures even distribution of traffic across all registered targets in all Availability Zones, improving fault tolerance and load distribution.", "Risk": "If cross-zone load balancing is not enabled, traffic may not be evenly distributed across Availability Zones, leading to over-utilization of resources in certain zones and potential application performance degradation or outages.", "RelatedUrl": "https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-disable-crosszone-lb.html", diff --git a/prowler/providers/aws/services/elb/elb_service.py b/prowler/providers/aws/services/elb/elb_service.py index 49b2494354..e2bfceffa8 100644 --- a/prowler/providers/aws/services/elb/elb_service.py +++ b/prowler/providers/aws/services/elb/elb_service.py @@ -64,6 +64,9 @@ class ELB(AWSService): load_balancer.cross_zone_load_balancing = attributes.get( "CrossZoneLoadBalancing", {} ).get("Enabled") + load_balancer.connection_draining = attributes.get( + "ConnectionDraining", {} + ).get("Enabled", False) except Exception as error: logger.error( @@ -101,4 +104,5 @@ class LoadBalancer(BaseModel): listeners: list[Listener] cross_zone_load_balancing: Optional[bool] availability_zones: set[str] + connection_draining: Optional[bool] tags: Optional[list] = [] diff --git a/prowler/providers/aws/services/elbv2/elbv2_deletion_protection/elbv2_deletion_protection.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_deletion_protection/elbv2_deletion_protection.metadata.json index 4f5f5784f0..a07f838e9d 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_deletion_protection/elbv2_deletion_protection.metadata.json +++ b/prowler/providers/aws/services/elbv2/elbv2_deletion_protection/elbv2_deletion_protection.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "ResourceType": "AwsElbv2LoadBalancer", "Description": "Check if Elastic Load Balancers have deletion protection enabled.", "Risk": "If deletion protection is not enabled, the resource is not protected against deletion.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/elbv2/elbv2_desync_mitigation_mode/elbv2_desync_mitigation_mode.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_desync_mitigation_mode/elbv2_desync_mitigation_mode.metadata.json index 1fca9ff65a..635f84efd6 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_desync_mitigation_mode/elbv2_desync_mitigation_mode.metadata.json +++ b/prowler/providers/aws/services/elbv2/elbv2_desync_mitigation_mode/elbv2_desync_mitigation_mode.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "ResourceType": "AwsElbv2LoadBalancer", "Description": "Check whether the Application Load Balancer is configured with strictest desync mitigation mode, if not check if at least is configured with the drop_invalid_header_fields attribute", "Risk": "HTTP Desync issues can lead to request smuggling and make your applications vulnerable to request queue or cache poisoning, which could lead to credential hijacking or execution of unauthorized commands.", "RelatedUrl": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#desync-mitigation-mode", diff --git a/prowler/providers/aws/services/elbv2/elbv2_insecure_ssl_ciphers/elbv2_insecure_ssl_ciphers.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_insecure_ssl_ciphers/elbv2_insecure_ssl_ciphers.metadata.json index aa2686ac75..f8b47367d5 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_insecure_ssl_ciphers/elbv2_insecure_ssl_ciphers.metadata.json +++ b/prowler/providers/aws/services/elbv2/elbv2_insecure_ssl_ciphers/elbv2_insecure_ssl_ciphers.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "ResourceType": "AwsElbv2LoadBalancer", "Description": "Check if Elastic Load Balancers have insecure SSL ciphers.", "Risk": "Using insecure ciphers could affect privacy of in transit information.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/elbv2/elbv2_internet_facing/elbv2_internet_facing.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_internet_facing/elbv2_internet_facing.metadata.json index 66b5db75a4..2aea137671 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_internet_facing/elbv2_internet_facing.metadata.json +++ b/prowler/providers/aws/services/elbv2/elbv2_internet_facing/elbv2_internet_facing.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "ResourceType": "AwsElbv2LoadBalancer", "Description": "Check for internet facing Elastic Load Balancers.", "Risk": "Publicly accessible load balancers could expose sensitive data to bad actors.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.metadata.json index 1ef2cbf508..aa83988cff 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.metadata.json +++ b/prowler/providers/aws/services/elbv2/elbv2_is_in_multiple_az/elbv2_is_in_multiple_az.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "ResourceType": "AwsElbv2LoadBalancer", "Description": "Ensure whether Elastic Load Balancer V2 (Application, Network, or Gateway Load Balancer) is configured to operate across multiple Availability Zones (AZs). Ensuring that your load balancer is spread across at least two AZs helps maintain high availability and fault tolerance in case of an AZ failure.", "Risk": "If an ELBv2 is not configured across multiple AZs, there is a risk that an Availability Zone failure could lead to downtime for your application. This could result in a single point of failure, impacting the availability and reliability of your services.", "RelatedUrl": "https://docs.aws.amazon.com/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html#availability-zones", diff --git a/prowler/providers/aws/services/elbv2/elbv2_listeners_underneath/elbv2_listeners_underneath.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_listeners_underneath/elbv2_listeners_underneath.metadata.json index 2841e8e3c8..9b7a297221 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_listeners_underneath/elbv2_listeners_underneath.metadata.json +++ b/prowler/providers/aws/services/elbv2/elbv2_listeners_underneath/elbv2_listeners_underneath.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "ResourceType": "AwsElbv2LoadBalancer", "Description": "Check if ELBV2 has listeners underneath.", "Risk": "The rules that are defined for a listener determine how the load balancer routes requests to its registered targets.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/elbv2/elbv2_logging_enabled/elbv2_logging_enabled.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_logging_enabled/elbv2_logging_enabled.metadata.json index fe9a89e3e1..3117bbb6c5 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_logging_enabled/elbv2_logging_enabled.metadata.json +++ b/prowler/providers/aws/services/elbv2/elbv2_logging_enabled/elbv2_logging_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "ResourceType": "AwsElbv2LoadBalancer", "Description": "Check if Elastic Load Balancers have logging enabled.", "Risk": "If logs are not enabled monitoring of service use and threat analysis is not possible.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/elbv2/elbv2_ssl_listeners/elbv2_ssl_listeners.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_ssl_listeners/elbv2_ssl_listeners.metadata.json index 9f3e196734..f92a9dde68 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_ssl_listeners/elbv2_ssl_listeners.metadata.json +++ b/prowler/providers/aws/services/elbv2/elbv2_ssl_listeners/elbv2_ssl_listeners.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "ResourceType": "AwsElbv2LoadBalancer", "Description": "Check if Elastic Load Balancers have SSL listeners.", "Risk": "Clear text communication could affect privacy of information in transit.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/elb-controls.html#elb-1", diff --git a/prowler/providers/aws/services/elbv2/elbv2_waf_acl_attached/elbv2_waf_acl_attached.metadata.json b/prowler/providers/aws/services/elbv2/elbv2_waf_acl_attached/elbv2_waf_acl_attached.metadata.json index 2dd6140424..677e9059f4 100644 --- a/prowler/providers/aws/services/elbv2/elbv2_waf_acl_attached/elbv2_waf_acl_attached.metadata.json +++ b/prowler/providers/aws/services/elbv2/elbv2_waf_acl_attached/elbv2_waf_acl_attached.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "ResourceType": "AwsElbv2LoadBalancer", "Description": "Check if Application Load Balancer has a WAF ACL attached.", "Risk": "If not WAF ACL is attached risk of web attacks increases.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/emr/emr_cluster_account_public_block_enabled/emr_cluster_account_public_block_enabled.metadata.json b/prowler/providers/aws/services/emr/emr_cluster_account_public_block_enabled/emr_cluster_account_public_block_enabled.metadata.json index a9f1ff8e63..e7b123c22d 100644 --- a/prowler/providers/aws/services/emr/emr_cluster_account_public_block_enabled/emr_cluster_account_public_block_enabled.metadata.json +++ b/prowler/providers/aws/services/emr/emr_cluster_account_public_block_enabled/emr_cluster_account_public_block_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:emr:region:account-id", "Severity": "high", - "ResourceType": "AwsEMR", + "ResourceType": "AwsAccount", "Description": "EMR Account Public Access Block enabled.", "Risk": "EMR Clusters must have Account Public Access Block enabled.", "RelatedUrl": "https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-block-public-access.html", diff --git a/prowler/providers/aws/services/emr/emr_cluster_master_nodes_no_public_ip/emr_cluster_master_nodes_no_public_ip.metadata.json b/prowler/providers/aws/services/emr/emr_cluster_master_nodes_no_public_ip/emr_cluster_master_nodes_no_public_ip.metadata.json index c913fc534a..0cc84e87a6 100644 --- a/prowler/providers/aws/services/emr/emr_cluster_master_nodes_no_public_ip/emr_cluster_master_nodes_no_public_ip.metadata.json +++ b/prowler/providers/aws/services/emr/emr_cluster_master_nodes_no_public_ip/emr_cluster_master_nodes_no_public_ip.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:emr:region:account-id", "Severity": "medium", - "ResourceType": "AwsEMR", + "ResourceType": "Other", "Description": "EMR Cluster without Public IP.", "Risk": "EMR Cluster should not have Public IP.", "RelatedUrl": "https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-block-public-access.html", diff --git a/prowler/providers/aws/services/emr/emr_cluster_publicly_accesible/emr_cluster_publicly_accesible.metadata.json b/prowler/providers/aws/services/emr/emr_cluster_publicly_accesible/emr_cluster_publicly_accesible.metadata.json index a207a575ba..623f403343 100644 --- a/prowler/providers/aws/services/emr/emr_cluster_publicly_accesible/emr_cluster_publicly_accesible.metadata.json +++ b/prowler/providers/aws/services/emr/emr_cluster_publicly_accesible/emr_cluster_publicly_accesible.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:emr:region:account-id", "Severity": "medium", - "ResourceType": "AwsEMR", + "ResourceType": "Other", "Description": "Publicly accessible EMR Cluster.", "Risk": "EMR Clusters should not be publicly accessible.", "RelatedUrl": "https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-block-public-access.html", diff --git a/prowler/providers/aws/services/eventbridge/eventbridge_service.py b/prowler/providers/aws/services/eventbridge/eventbridge_service.py index 57a7c0bf2c..b17f2b17f7 100644 --- a/prowler/providers/aws/services/eventbridge/eventbridge_service.py +++ b/prowler/providers/aws/services/eventbridge/eventbridge_service.py @@ -107,7 +107,7 @@ class Schema(AWSService): for registry in regional_client.list_registries()["Registries"]: registry_arn = registry.get( "RegistryArn", - f"arn:aws:schemas:{regional_client.region}:{self.audited_account}:registry/{registry.get('RegistryName', '')}", + f"arn:{self.audited_partition}:schemas:{regional_client.region}:{self.audited_account}:registry/{registry.get('RegistryName', '')}", ) if not self.audit_resources or ( is_resource_filtered(registry_arn, self.audit_resources) diff --git a/prowler/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access.metadata.json b/prowler/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access.metadata.json index 2e8333cd68..7ff3cf6921 100644 --- a/prowler/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access.metadata.json +++ b/prowler/providers/aws/services/glacier/glacier_vaults_policy_public_access/glacier_vaults_policy_public_access.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:glacier:region:account-id:vaults/vault-name", "Severity": "critical", - "ResourceType": "AwsGlacierVault", + "ResourceType": "Other", "Description": "Ensure CodeArtifact internal packages do not allow external public source publishing.", "Risk": "Vaults accessible to everyone could expose sensitive data to bad actors.", "RelatedUrl": "https://docs.aws.amazon.com/amazonglacier/latest/dev/access-control-overview.html", diff --git a/prowler/providers/aws/services/globalaccelerator/globalaccelerator_service.py b/prowler/providers/aws/services/globalaccelerator/globalaccelerator_service.py index 3695561782..706056c5b3 100644 --- a/prowler/providers/aws/services/globalaccelerator/globalaccelerator_service.py +++ b/prowler/providers/aws/services/globalaccelerator/globalaccelerator_service.py @@ -1,3 +1,5 @@ +from typing import Optional + from pydantic import BaseModel from prowler.lib.logger import logger @@ -5,7 +7,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################### GlobalAccelerator class GlobalAccelerator(AWSService): def __init__(self, provider): # Call AWSService's __init__ @@ -18,6 +19,7 @@ class GlobalAccelerator(AWSService): self.region = "us-west-2" self.client = self.session.client(self.service, self.region) self._list_accelerators() + self.__threading_call__(self._list_tags, self.accelerators.values()) def _list_accelerators(self): logger.info("GlobalAccelerator - Listing Accelerators...") @@ -46,9 +48,23 @@ class GlobalAccelerator(AWSService): f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _list_tags(self, resource: any): + try: + resource.tags = ( + self.regional_clients[resource.region] + .list_tags_for_resource(ResourceArn=resource.arn) + .get("Tags", []) + ) + + except Exception as error: + logger.error( + f"{resource.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class Accelerator(BaseModel): arn: str name: str region: str enabled: bool + tags: Optional[list] diff --git a/prowler/providers/aws/services/glue/glue_data_catalogs_connection_passwords_encryption_enabled/glue_data_catalogs_connection_passwords_encryption_enabled.metadata.json b/prowler/providers/aws/services/glue/glue_data_catalogs_connection_passwords_encryption_enabled/glue_data_catalogs_connection_passwords_encryption_enabled.metadata.json index 202daa40ba..3079c2e9b2 100644 --- a/prowler/providers/aws/services/glue/glue_data_catalogs_connection_passwords_encryption_enabled/glue_data_catalogs_connection_passwords_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/glue/glue_data_catalogs_connection_passwords_encryption_enabled/glue_data_catalogs_connection_passwords_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsGlue", + "ResourceType": "Other", "Description": "Check if Glue data catalog settings have encrypt connection password enabled.", "Risk": "If not enabled sensitive information at rest is not protected.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/glue/glue_data_catalogs_metadata_encryption_enabled/glue_data_catalogs_metadata_encryption_enabled.metadata.json b/prowler/providers/aws/services/glue/glue_data_catalogs_metadata_encryption_enabled/glue_data_catalogs_metadata_encryption_enabled.metadata.json index 5653ae151e..1f3af4b0fb 100644 --- a/prowler/providers/aws/services/glue/glue_data_catalogs_metadata_encryption_enabled/glue_data_catalogs_metadata_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/glue/glue_data_catalogs_metadata_encryption_enabled/glue_data_catalogs_metadata_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsGlue", + "ResourceType": "Other", "Description": "Check if Glue data catalog settings have metadata encryption enabled.", "Risk": "If not enabled sensitive information at rest is not protected.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled.metadata.json b/prowler/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled.metadata.json index 40869ae64b..66500dee38 100644 --- a/prowler/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled.metadata.json +++ b/prowler/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsGlue", + "ResourceType": "Other", "Description": "Check if Glue database connection has SSL connection enabled.", "Risk": "Data exfiltration could happen if information is not protected in transit.", "RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/encryption-in-transit.html", diff --git a/prowler/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled.py b/prowler/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled.py index 67cd3e5ec2..f2ccf2feed 100644 --- a/prowler/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled.py +++ b/prowler/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled.py @@ -10,6 +10,7 @@ class glue_database_connections_ssl_enabled(Check): report.resource_id = conn.name report.resource_arn = conn.arn report.region = conn.region + report.resource_tags = conn.tags report.status = "FAIL" report.status_extended = ( f"Glue connection {conn.name} has SSL connection disabled." diff --git a/prowler/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled.metadata.json b/prowler/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled.metadata.json index dbba2e8f5a..622bb6c6fb 100644 --- a/prowler/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsGlue", + "ResourceType": "Other", "Description": "Check if Glue development endpoints have CloudWatch logs encryption enabled.", "Risk": "If not enabled sensitive information at rest is not protected.", "RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html", diff --git a/prowler/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled.py b/prowler/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled.py index 4b92fc8032..01f022a605 100644 --- a/prowler/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled.py +++ b/prowler/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled.py @@ -11,6 +11,7 @@ class glue_development_endpoints_cloudwatch_logs_encryption_enabled(Check): report.resource_id = endpoint.name report.resource_arn = endpoint.arn report.region = endpoint.region + report.resource_tags = endpoint.tags for sec_config in glue_client.security_configs: if sec_config.name == endpoint.security: no_sec_configs = False diff --git a/prowler/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled.metadata.json b/prowler/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled.metadata.json index d62d2be5c1..eb724b942c 100644 --- a/prowler/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsGlue", + "ResourceType": "Other", "Description": "Check if Glue development endpoints have Job bookmark encryption enabled.", "Risk": "If not enabled sensitive information at rest is not protected.", "RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html", diff --git a/prowler/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled.py b/prowler/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled.py index a115294d64..c1011d4c74 100644 --- a/prowler/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled.py +++ b/prowler/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled.py @@ -11,6 +11,7 @@ class glue_development_endpoints_job_bookmark_encryption_enabled(Check): report.resource_id = endpoint.name report.resource_arn = endpoint.arn report.region = endpoint.region + report.resource_tags = endpoint.tags for sec_config in glue_client.security_configs: if sec_config.name == endpoint.security: no_sec_configs = False diff --git a/prowler/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled.metadata.json b/prowler/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled.metadata.json index 56931fad5f..9b958b4d6a 100644 --- a/prowler/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsGlue", + "ResourceType": "Other", "Description": "Check if Glue development endpoints have S3 encryption enabled.", "Risk": "Data exfiltration could happen if information is not protected. KMS keys provide additional security level to IAM policies.", "RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/encryption-security-configuration.html", diff --git a/prowler/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled.py b/prowler/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled.py index fe3285209f..5e929ee7de 100644 --- a/prowler/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled.py +++ b/prowler/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled.py @@ -11,6 +11,7 @@ class glue_development_endpoints_s3_encryption_enabled(Check): report.resource_id = endpoint.name report.resource_arn = endpoint.arn report.region = endpoint.region + report.resource_tags = endpoint.tags for sec_config in glue_client.security_configs: if sec_config.name == endpoint.security: no_sec_configs = False diff --git a/prowler/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled.metadata.json b/prowler/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled.metadata.json index 2bbcf97789..6b23dffecf 100644 --- a/prowler/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsGlue", + "ResourceType": "AwsGlueJob", "Description": "Check if Glue ETL Jobs have S3 encryption enabled.", "Risk": "If not enabled sensitive information at rest is not protected.", "RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html", diff --git a/prowler/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled.py b/prowler/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled.py index 37f298e3b3..b76fa9f49f 100644 --- a/prowler/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled.py +++ b/prowler/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled.py @@ -11,6 +11,7 @@ class glue_etl_jobs_amazon_s3_encryption_enabled(Check): report.resource_id = job.name report.resource_arn = job.arn report.region = job.region + report.resource_tags = job.tags for sec_config in glue_client.security_configs: if sec_config.name == job.security: no_sec_configs = False diff --git a/prowler/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled.metadata.json b/prowler/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled.metadata.json index 99130b8bc3..0686c7e290 100644 --- a/prowler/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsGlue", + "ResourceType": "AwsGlueJob", "Description": "Check if Glue ETL Jobs have CloudWatch Logs encryption enabled.", "Risk": "If not enabled sensitive information at rest is not protected.", "RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html", diff --git a/prowler/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled.py b/prowler/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled.py index c1c316870b..90eb6a5c08 100644 --- a/prowler/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled.py +++ b/prowler/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled.py @@ -11,6 +11,7 @@ class glue_etl_jobs_cloudwatch_logs_encryption_enabled(Check): report.resource_id = job.name report.resource_arn = job.arn report.region = job.region + report.resource_tags = job.tags for sec_config in glue_client.security_configs: if sec_config.name == job.security: no_sec_configs = False diff --git a/prowler/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled.metadata.json b/prowler/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled.metadata.json index 70673bc0c0..78a1757509 100644 --- a/prowler/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:glue:region:account-id:certificate/resource-id", "Severity": "medium", - "ResourceType": "AwsGlue", + "ResourceType": "AwsGlueJob", "Description": "Check if Glue ETL Jobs have Job bookmark encryption enabled.", "Risk": "If not enabled sensitive information at rest is not protected.", "RelatedUrl": "https://docs.aws.amazon.com/glue/latest/dg/console-security-configurations.html", diff --git a/prowler/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled.py b/prowler/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled.py index 6bba16eb8b..3c3f1b1c58 100644 --- a/prowler/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled.py +++ b/prowler/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled.py @@ -11,6 +11,7 @@ class glue_etl_jobs_job_bookmark_encryption_enabled(Check): report.resource_id = job.name report.resource_arn = job.arn report.region = job.region + report.resource_tags = job.tags for sec_config in glue_client.security_configs: if sec_config.name == job.security: no_sec_configs = False diff --git a/prowler/providers/aws/services/glue/glue_service.py b/prowler/providers/aws/services/glue/glue_service.py index 68a0c66fd1..a11b137fcd 100644 --- a/prowler/providers/aws/services/glue/glue_service.py +++ b/prowler/providers/aws/services/glue/glue_service.py @@ -8,23 +8,25 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## Glue class Glue(AWSService): def __init__(self, provider): # Call AWSService's __init__ super().__init__(__class__.__name__, provider) self.connections = [] self.__threading_call__(self._get_connections) + self.__threading_call__(self._list_tags, self.connections) self.tables = [] self.__threading_call__(self._search_tables) self.catalog_encryption_settings = [] self.__threading_call__(self._get_data_catalog_encryption_settings) self.dev_endpoints = [] self.__threading_call__(self._get_dev_endpoints) + self.__threading_call__(self._list_tags, self.dev_endpoints) self.security_configs = [] self.__threading_call__(self._get_security_configurations) self.jobs = [] self.__threading_call__(self._get_jobs) + self.__threading_call__(self._list_tags, self.jobs) def _get_data_catalog_arn_template(self, region): return f"arn:{self.audited_partition}:glue:{region}:{self.audited_account}:data-catalog" @@ -205,6 +207,18 @@ class Glue(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _list_tags(self, resource: any): + try: + resource.tags = [ + self.regional_clients[resource.region].get_tags( + ResourceArn=resource.arn + )["Tags"] + ] + except Exception as error: + logger.error( + f"{resource.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class Connection(BaseModel): name: str @@ -212,6 +226,7 @@ class Connection(BaseModel): type: str properties: dict region: str + tags: Optional[list] class Table(BaseModel): @@ -236,6 +251,7 @@ class DevEndpoint(BaseModel): arn: str security: Optional[str] region: str + tags: Optional[list] class Job(BaseModel): @@ -244,6 +260,7 @@ class Job(BaseModel): security: Optional[str] arguments: Optional[dict] region: str + tags: Optional[list] class SecurityConfig(BaseModel): diff --git a/prowler/providers/aws/services/guardduty/guardduty_centrally_managed/guardduty_centrally_managed.metadata.json b/prowler/providers/aws/services/guardduty/guardduty_centrally_managed/guardduty_centrally_managed.metadata.json index 473fecce17..e0298b1fef 100644 --- a/prowler/providers/aws/services/guardduty/guardduty_centrally_managed/guardduty_centrally_managed.metadata.json +++ b/prowler/providers/aws/services/guardduty/guardduty_centrally_managed/guardduty_centrally_managed.metadata.json @@ -9,7 +9,7 @@ "Severity": "medium", "ResourceType": "AwsGuardDutyDetector", "Description": "GuardDuty is centrally managed", - "Risk": "If GuardDuty is not centrally managed, it is not possible to centrally manage the GuardDuty findings, settings, and member accounts.", + "Risk": "If GuardDuty is not centrally managed, it is not possible to centrally manage the GuardDuty findings, settings, and member accounts.", "RelatedUrl": "https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_accounts.html", "Remediation": { "Code": { diff --git a/prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/__init__.py b/prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled.metadata.json b/prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled.metadata.json new file mode 100644 index 0000000000..445e334de8 --- /dev/null +++ b/prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "guardduty_rds_protection_enabled", + "CheckTitle": "Check if GuardDuty RDS Protection is enabled.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "guardduty", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:guardduty:::detector/", + "Severity": "high", + "ResourceType": "AwsGuardDutyDetector", + "Description": "Check if GuardDuty RDS Protection is enabled to ensure monitoring and threat detection for RDS activity.", + "Risk": "Without GuardDuty RDS Protection enabled, suspicious login activities to your databases may go undetected, increasing the risk of unauthorized access, data breaches, or compromised database security.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/guard-duty-rds-protection.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/guardduty-controls.html#guardduty-9", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable GuardDuty RDS Protection to continuously monitor and detect anomalous login behaviors on your Aurora databases, helping to identify and respond to potential access threats without impacting database performance.", + "Url": "https://docs.aws.amazon.com/guardduty/latest/ug/rds-protection.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled.py b/prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled.py new file mode 100644 index 0000000000..d765c86848 --- /dev/null +++ b/prowler/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.guardduty.guardduty_client import guardduty_client + + +class guardduty_rds_protection_enabled(Check): + def execute(self): + findings = [] + for detector in guardduty_client.detectors: + if detector.status: + report = Check_Report_AWS(self.metadata()) + report.region = detector.region + report.resource_id = detector.id + report.resource_arn = detector.arn + report.resource_tags = detector.tags + report.status = "FAIL" + report.status_extended = ( + "GuardDuty detector does not have RDS Protection enabled." + ) + if detector.rds_protection: + report.status = "PASS" + report.status_extended = ( + "GuardDuty detector has RDS Protection enabled." + ) + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/__init__.py b/prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled.metadata.json b/prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled.metadata.json new file mode 100644 index 0000000000..94687e6c99 --- /dev/null +++ b/prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "guardduty_s3_protection_enabled", + "CheckTitle": "Check if GuardDuty S3 Protection is enabled.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "guardduty", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:guardduty:::detector/", + "Severity": "high", + "ResourceType": "AwsGuardDutyDetector", + "Description": "This control checks whether GuardDuty S3 Protection is enabled in the account.", + "Risk": "Without GuardDuty S3 Protection enabled, your S3 buckets are not monitored for potential security risks at the object level, which may lead to undetected malicious activities and data breaches.", + "RelatedUrl": "https://docs.aws.amazon.com/guardduty/latest/ug/s3_detection.html", + "Remediation": { + "Code": { + "CLI": "aws guardduty update-detector --detector-id --data-sources '{\"S3Logs\": {\"Enable\": true}}'", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/guardduty-controls.html#guardduty-10", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable GuardDuty S3 Protection to monitor object-level API operations in your S3 buckets.", + "Url": "https://docs.aws.amazon.com/guardduty/latest/ug/s3_detection.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled.py b/prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled.py new file mode 100644 index 0000000000..5aaf134b37 --- /dev/null +++ b/prowler/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.guardduty.guardduty_client import guardduty_client + + +class guardduty_s3_protection_enabled(Check): + def execute(self): + findings = [] + for detector in guardduty_client.detectors: + if detector.status: + report = Check_Report_AWS(self.metadata()) + report.region = detector.region + report.resource_id = detector.id + report.resource_arn = detector.arn + report.resource_tags = detector.tags + report.status = "FAIL" + report.status_extended = ( + "GuardDuty detector does not have S3 Protection enabled." + ) + if detector.s3_protection: + report.status = "PASS" + report.status_extended = ( + "GuardDuty detector has S3 Protection enabled." + ) + findings.append(report) + return findings diff --git a/prowler/providers/aws/services/guardduty/guardduty_service.py b/prowler/providers/aws/services/guardduty/guardduty_service.py index 4dc7e3da6e..0ee0f7d26c 100644 --- a/prowler/providers/aws/services/guardduty/guardduty_service.py +++ b/prowler/providers/aws/services/guardduty/guardduty_service.py @@ -7,7 +7,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################################ GuardDuty class GuardDuty(AWSService): def __init__(self, provider): # Call AWSService's __init__ @@ -66,6 +65,19 @@ class GuardDuty(AWSService): and detector_info["Status"] == "ENABLED" ): detector.status = True + + data_sources = detector_info.get("DataSources", {}) + s3_logs = data_sources.get("S3Logs", {}) + if s3_logs.get("Status") == "ENABLED": + detector.s3_protection = True + + for feat in detector_info.get("Features", []): + if ( + feat.get("Name") == "RDS_LOGIN_EVENTS" + and feat.get("Status", "DISABLED") == "ENABLED" + ): + detector.rds_protection = True + except Exception as error: logger.error( f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" @@ -190,3 +202,5 @@ class Detector(BaseModel): member_accounts: list = [] administrator_account: str = None tags: Optional[list] = [] + s3_protection: bool = False + rds_protection: bool = False diff --git a/prowler/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa.py b/prowler/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa.py index 4ff08f32b2..1853de9531 100644 --- a/prowler/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa.py +++ b/prowler/providers/aws/services/iam/iam_administrator_access_with_mfa/iam_administrator_access_with_mfa.py @@ -22,7 +22,7 @@ class iam_administrator_access_with_mfa(Check): for group_policy in group.attached_policies: if ( group_policy["PolicyArn"] - == "arn:aws:iam::aws:policy/AdministratorAccess" + == f"arn:{iam_client.audited_partition}:iam::aws:policy/AdministratorAccess" ): # users in group are Administrators if group.users: diff --git a/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.py b/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.py index 2d14c0d6f5..03577c7e25 100644 --- a/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.py +++ b/prowler/providers/aws/services/iam/iam_aws_attached_policy_no_administrative_privileges/iam_aws_attached_policy_no_administrative_privileges.py @@ -1,5 +1,6 @@ from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import check_admin_access class iam_aws_attached_policy_no_administrative_privileges(Check): @@ -16,27 +17,9 @@ class iam_aws_attached_policy_no_administrative_privileges(Check): report.status = "PASS" report.status_extended = f"{policy.type} policy {policy.name} is attached but does not allow '*:*' administrative privileges." if policy.document: - # Check the statements, if one includes *:* stop iterating over the rest - if not isinstance(policy.document["Statement"], list): - policy_statements = [policy.document["Statement"]] - else: - policy_statements = policy.document["Statement"] - for statement in policy_statements: - # Check policies with "Effect": "Allow" with "Action": "*" over "Resource": "*". - if ( - statement["Effect"] == "Allow" - and "Action" in statement - and ( - statement["Action"] == "*" - or statement["Action"] == ["*"] - ) - and ( - statement["Resource"] == "*" - or statement["Resource"] == ["*"] - ) - ): - report.status = "FAIL" - report.status_extended = f"{policy.type} policy {policy.name} is attached and allows '*:*' administrative privileges." - break + if check_admin_access(policy.document): + report.status = "FAIL" + report.status_extended = f"{policy.type} policy {policy.name} is attached and allows '*:*' administrative privileges." + break findings.append(report) return findings diff --git a/prowler/providers/aws/services/iam/iam_customer_attached_policy_no_administrative_privileges/iam_customer_attached_policy_no_administrative_privileges.py b/prowler/providers/aws/services/iam/iam_customer_attached_policy_no_administrative_privileges/iam_customer_attached_policy_no_administrative_privileges.py index dc6a1285d5..44dfd0f3c5 100644 --- a/prowler/providers/aws/services/iam/iam_customer_attached_policy_no_administrative_privileges/iam_customer_attached_policy_no_administrative_privileges.py +++ b/prowler/providers/aws/services/iam/iam_customer_attached_policy_no_administrative_privileges/iam_customer_attached_policy_no_administrative_privileges.py @@ -1,5 +1,6 @@ from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import check_admin_access class iam_customer_attached_policy_no_administrative_privileges(Check): @@ -16,27 +17,9 @@ class iam_customer_attached_policy_no_administrative_privileges(Check): report.status = "PASS" report.status_extended = f"{policy.type} policy {policy.name} is attached but does not allow '*:*' administrative privileges." if policy.document: - # Check the statements, if one includes *:* stop iterating over the rest - if not isinstance(policy.document["Statement"], list): - policy_statements = [policy.document["Statement"]] - else: - policy_statements = policy.document["Statement"] - for statement in policy_statements: - # Check policies with "Effect": "Allow" with "Action": "*" over "Resource": "*". - if ( - statement["Effect"] == "Allow" - and "Action" in statement - and ( - statement["Action"] == "*" - or statement["Action"] == ["*"] - ) - and ( - statement["Resource"] == "*" - or statement["Resource"] == ["*"] - ) - ): - report.status = "FAIL" - report.status_extended = f"{policy.type} policy {policy.name} is attached and allows '*:*' administrative privileges." - break + if check_admin_access(policy.document): + report.status = "FAIL" + report.status_extended = f"{policy.type} policy {policy.name} is attached and allows '*:*' administrative privileges." + break findings.append(report) return findings diff --git a/prowler/providers/aws/services/iam/iam_customer_unattached_policy_no_administrative_privileges/iam_customer_unattached_policy_no_administrative_privileges.py b/prowler/providers/aws/services/iam/iam_customer_unattached_policy_no_administrative_privileges/iam_customer_unattached_policy_no_administrative_privileges.py index f37c7538dd..f01b564d02 100644 --- a/prowler/providers/aws/services/iam/iam_customer_unattached_policy_no_administrative_privileges/iam_customer_unattached_policy_no_administrative_privileges.py +++ b/prowler/providers/aws/services/iam/iam_customer_unattached_policy_no_administrative_privileges/iam_customer_unattached_policy_no_administrative_privileges.py @@ -1,5 +1,6 @@ from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import check_admin_access class iam_customer_unattached_policy_no_administrative_privileges(Check): @@ -16,27 +17,9 @@ class iam_customer_unattached_policy_no_administrative_privileges(Check): report.status = "PASS" report.status_extended = f"{policy.type} policy {policy.name} is unattached and does not allow '*:*' administrative privileges." if policy.document: - # Check the statements, if one includes *:* stop iterating over the rest - if not isinstance(policy.document["Statement"], list): - policy_statements = [policy.document["Statement"]] - else: - policy_statements = policy.document["Statement"] - for statement in policy_statements: - # Check policies with "Effect": "Allow" with "Action": "*" over "Resource": "*". - if ( - statement["Effect"] == "Allow" - and "Action" in statement - and ( - statement["Action"] == "*" - or statement["Action"] == ["*"] - ) - and ( - statement["Resource"] == "*" - or statement["Resource"] == ["*"] - ) - ): - report.status = "FAIL" - report.status_extended = f"{policy.type} policy {policy.name} is unattached and allows '*:*' administrative privileges." - break + if check_admin_access(policy.document): + report.status = "FAIL" + report.status_extended = f"{policy.type} policy {policy.name} is unattached and allows '*:*' administrative privileges." + break findings.append(report) return findings diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.metadata.json b/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.metadata.json index 3a5fe7b755..47f43cda3d 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.metadata.json +++ b/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.metadata.json @@ -1,7 +1,7 @@ { "Provider": "aws", "CheckID": "iam_inline_policy_allows_privilege_escalation", - "CheckTitle": "Ensure no Inline IAM policies allow actions that may lead into Privilege Escalation", + "CheckTitle": "Ensure no IAM Inline policies allow actions that may lead into Privilege Escalation", "CheckType": [ "Software and Configuration Checks", "Industry and Regulatory Standards" diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.py b/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.py index df41b509b8..2042b8343a 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.py +++ b/prowler/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation.py @@ -12,22 +12,16 @@ class iam_inline_policy_allows_privilege_escalation(Check): for policy in iam_client.policies: if policy.type == "Inline": report = Check_Report_AWS(self.metadata()) - report.resource_id = policy.name + report.resource_id = f"{policy.entity}/{policy.name}" report.resource_arn = policy.arn report.region = iam_client.region report.resource_tags = policy.tags report.status = "PASS" - if "role" in report.resource_arn: - resource_type_str = "role" - elif "group" in report.resource_arn: - resource_type_str = "group" - elif "user" in report.resource_arn: - resource_type_str = "user" - else: - resource_type_str = "resource" + resource_type_str = report.resource_arn.split(":")[-1].split("/")[0] + resource_attached = report.resource_arn.split("/")[-1] - report.status_extended = f"Inline Policy '{report.resource_id}'{' attached to ' + resource_type_str + ' ' + report.resource_arn if policy.attached else ''} does not allow privilege escalation." + report.status_extended = f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} does not allow privilege escalation." policies_affected = check_privilege_escalation( getattr(policy, "document", {}) @@ -37,7 +31,7 @@ class iam_inline_policy_allows_privilege_escalation(Check): report.status = "FAIL" report.status_extended = ( - f"Inline Policy '{report.resource_id}'{' attached to ' + resource_type_str + ' ' + report.resource_arn if policy.attached else ''} allows privilege escalation using the following actions: {policies_affected}".rstrip() + f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} allows privilege escalation using the following actions: {policies_affected}".rstrip() + "." ) diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.metadata.json b/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.metadata.json index 20eb42eb41..68cd1c4898 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.metadata.json +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.metadata.json @@ -1,7 +1,7 @@ { "Provider": "aws", "CheckID": "iam_inline_policy_no_administrative_privileges", - "CheckTitle": "Ensure inline policies that allow full \"*:*\" administrative privileges are not associated to IAM identities", + "CheckTitle": "Ensure IAM inline policies that allow full \"*:*\" administrative privileges are not associated to IAM identities", "CheckType": [ "Software and Configuration Checks", "Industry and Regulatory Standards", @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "high", - "ResourceType": "AwsIamRole", + "ResourceType": "AwsIamPolicy", "Description": "Ensure inline policies that allow full \"*:*\" administrative privileges are not associated to IAM identities", "Risk": "IAM policies are the means by which privileges are granted to users, groups or roles. It is recommended and considered a standard security advice to grant least privilege—that is, granting only the permissions required to perform a task. Determine what users need to do and then craft policies for them that let the users perform only those tasks instead of allowing full administrative privileges. Providing full administrative privileges instead of restricting to the minimum set of permissions that the user is required to do exposes the resources to potentially unwanted actions.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.py b/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.py index 655c24d3b9..daab80b03d 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.py +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges.py @@ -1,5 +1,6 @@ from prowler.lib.check.models import Check, Check_Report_AWS from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import check_admin_access class iam_inline_policy_no_administrative_privileges(Check): @@ -14,38 +15,12 @@ class iam_inline_policy_no_administrative_privileges(Check): report.resource_tags = policy.tags report.status = "PASS" - if "role" in report.resource_arn: - resource_type_str = "role" - elif "group" in report.resource_arn: - resource_type_str = "group" - elif "user" in report.resource_arn: - resource_type_str = "user" - else: - resource_type_str = "resource" + resource_type_str = report.resource_arn.split(":")[-1].split("/")[0] + resource_attached = report.resource_arn.split("/")[-1] - report.status_extended = f"{policy.type} policy {policy.name} attached to {resource_type_str} {report.resource_arn} does not allow '*:*' administrative privileges." - if policy.document: - # Check the statements, if one includes *:* stop iterating over the rest - if not isinstance(policy.document["Statement"], list): - policy_statements = [policy.document["Statement"]] - else: - policy_statements = policy.document["Statement"] - for statement in policy_statements: - # Check policies with "Effect": "Allow" with "Action": "*" over "Resource": "*". - if ( - statement["Effect"] == "Allow" - and "Action" in statement - and ( - statement["Action"] == "*" - or statement["Action"] == ["*"] - ) - and ( - statement["Resource"] == "*" - or statement["Resource"] == ["*"] - ) - ): - report.status = "FAIL" - report.status_extended = f"{policy.type} policy {policy.name} attached to {resource_type_str} {report.resource_arn} allows '*:*' administrative privileges." - break + report.status_extended = f"{policy.type} policy {policy.name} attached to {resource_type_str} {resource_attached} does not allow '*:*' administrative privileges." + if policy.document and check_admin_access(policy.document): + report.status = "FAIL" + report.status_extended = f"{policy.type} policy {policy.name} attached to {resource_type_str} {resource_attached} allows '*:*' administrative privileges." findings.append(report) return findings diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail.py b/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail.py index 460856d1cb..06df9a3b6f 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail.py +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail.py @@ -15,16 +15,20 @@ class iam_inline_policy_no_full_access_to_cloudtrail(Check): report = Check_Report_AWS(self.metadata()) report.region = iam_client.region report.resource_arn = policy.arn - report.resource_id = policy.name + report.resource_id = f"{policy.entity}/{policy.name}" report.resource_tags = policy.tags report.status = "PASS" - report.status_extended = f"Inline Policy {policy.name} does not allow '{critical_service}:*' privileges." + + resource_type_str = report.resource_arn.split(":")[-1].split("/")[0] + resource_attached = report.resource_arn.split("/")[-1] + + report.status_extended = f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} does not allow '{critical_service}:*' privileges." if policy.document and check_full_service_access( critical_service, policy.document ): report.status = "FAIL" - report.status_extended = f"Inline Policy {policy.name} allows '{critical_service}:*' privileges to all resources." + report.status_extended = f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} allows '{critical_service}:*' privileges to all resources." findings.append(report) diff --git a/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.py b/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.py index db33562364..b938a72654 100644 --- a/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.py +++ b/prowler/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms.py @@ -14,16 +14,20 @@ class iam_inline_policy_no_full_access_to_kms(Check): report = Check_Report_AWS(self.metadata()) report.region = iam_client.region report.resource_arn = policy.arn - report.resource_id = policy.name + report.resource_id = f"{policy.entity}/{policy.name}" report.resource_tags = policy.tags report.status = "PASS" - report.status_extended = f"Inline Policy {policy.name} does not allow '{critical_service}:*' privileges." + + resource_type_str = report.resource_arn.split(":")[-1].split("/")[0] + resource_attached = report.resource_arn.split("/")[-1] + + report.status_extended = f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} does not allow '{critical_service}:*' privileges." if policy.document and check_full_service_access( critical_service, policy.document ): report.status = "FAIL" - report.status_extended = f"Inline Policy {policy.name} allows '{critical_service}:*' privileges." + report.status_extended = f"{policy.type} policy {policy.name}{' attached to ' + resource_type_str + ' ' + resource_attached if policy.attached else ''} allows '{critical_service}:*' privileges." findings.append(report) diff --git a/prowler/providers/aws/services/iam/iam_no_expired_server_certificates_stored/iam_no_expired_server_certificates_stored.metadata.json b/prowler/providers/aws/services/iam/iam_no_expired_server_certificates_stored/iam_no_expired_server_certificates_stored.metadata.json index aa2582927b..744ca0b276 100644 --- a/prowler/providers/aws/services/iam/iam_no_expired_server_certificates_stored/iam_no_expired_server_certificates_stored.metadata.json +++ b/prowler/providers/aws/services/iam/iam_no_expired_server_certificates_stored/iam_no_expired_server_certificates_stored.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "critical", - "ResourceType": "AwsIamUser", + "ResourceType": "Other", "Description": "Ensure that all the expired SSL/TLS certificates stored in AWS IAM are removed.", "Risk": "Removing expired SSL/TLS certificates eliminates the risk that an invalid certificate will be deployed accidentally to a resource such as AWS Elastic Load Balancer (ELB), which can damage the credibility of the application/website behind the ELB.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_no_root_access_key/iam_no_root_access_key.metadata.json b/prowler/providers/aws/services/iam/iam_no_root_access_key/iam_no_root_access_key.metadata.json index bc7173a3b1..933ffee047 100644 --- a/prowler/providers/aws/services/iam/iam_no_root_access_key/iam_no_root_access_key.metadata.json +++ b/prowler/providers/aws/services/iam/iam_no_root_access_key/iam_no_root_access_key.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "critical", - "ResourceType": "AwsIamUser", + "ResourceType": "AwsIamAccessKey", "Description": "Ensure no root account access key exists", "Risk": "The root account is the most privileged user in an AWS account. AWS Access Keys provide programmatic access to a given AWS account. It is recommended that all access keys associated with the root account be removed. Removing access keys associated with the root account limits vectors by which the account can be compromised. Removing the root access keys encourages the creation and use of role based accounts that are least privileged.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_password_policy_expires_passwords_within_90_days_or_less/iam_password_policy_expires_passwords_within_90_days_or_less.metadata.json b/prowler/providers/aws/services/iam/iam_password_policy_expires_passwords_within_90_days_or_less/iam_password_policy_expires_passwords_within_90_days_or_less.metadata.json index ac112cea19..1f30be3869 100644 --- a/prowler/providers/aws/services/iam/iam_password_policy_expires_passwords_within_90_days_or_less/iam_password_policy_expires_passwords_within_90_days_or_less.metadata.json +++ b/prowler/providers/aws/services/iam/iam_password_policy_expires_passwords_within_90_days_or_less/iam_password_policy_expires_passwords_within_90_days_or_less.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsIamPasswordPolicy", + "ResourceType": "Other", "Description": "Ensure IAM password policy expires passwords within 90 days or less", "Risk": "Password policies are used to enforce password complexity requirements. IAM password policies can be used to ensure password are comprised of different character sets. It is recommended that the password policy require at least one uppercase letter.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_password_policy_lowercase/iam_password_policy_lowercase.metadata.json b/prowler/providers/aws/services/iam/iam_password_policy_lowercase/iam_password_policy_lowercase.metadata.json index b9a90107f5..da7f9ca9e5 100644 --- a/prowler/providers/aws/services/iam/iam_password_policy_lowercase/iam_password_policy_lowercase.metadata.json +++ b/prowler/providers/aws/services/iam/iam_password_policy_lowercase/iam_password_policy_lowercase.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsIamPasswordPolicy", + "ResourceType": "Other", "Description": "Ensure IAM password policy requires at least one uppercase letter", "Risk": "Password policies are used to enforce password complexity requirements. IAM password policies can be used to ensure password are comprised of different character sets. It is recommended that the password policy require at least one lowercase letter.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_password_policy_minimum_length_14/iam_password_policy_minimum_length_14.metadata.json b/prowler/providers/aws/services/iam/iam_password_policy_minimum_length_14/iam_password_policy_minimum_length_14.metadata.json index 7eb4cc024e..e4ef273d94 100644 --- a/prowler/providers/aws/services/iam/iam_password_policy_minimum_length_14/iam_password_policy_minimum_length_14.metadata.json +++ b/prowler/providers/aws/services/iam/iam_password_policy_minimum_length_14/iam_password_policy_minimum_length_14.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsIamPasswordPolicy", + "ResourceType": "Other", "Description": "Ensure IAM password policy requires minimum length of 14 or greater", "Risk": "Password policies are used to enforce password complexity requirements. IAM password policies can be used to ensure password are comprised of different character sets. It is recommended that the password policy require minimum length of 14 or greater.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_password_policy_number/iam_password_policy_number.metadata.json b/prowler/providers/aws/services/iam/iam_password_policy_number/iam_password_policy_number.metadata.json index 300922184f..98d9e50426 100644 --- a/prowler/providers/aws/services/iam/iam_password_policy_number/iam_password_policy_number.metadata.json +++ b/prowler/providers/aws/services/iam/iam_password_policy_number/iam_password_policy_number.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsIamPasswordPolicy", + "ResourceType": "Other", "Description": "Ensure IAM password policy require at least one number", "Risk": "Password policies are used to enforce password complexity requirements. IAM password policies can be used to ensure password are comprised of different character sets. It is recommended that the password policy require at least one number.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_password_policy_reuse_24/iam_password_policy_reuse_24.metadata.json b/prowler/providers/aws/services/iam/iam_password_policy_reuse_24/iam_password_policy_reuse_24.metadata.json index 79d49e146a..321008db6b 100644 --- a/prowler/providers/aws/services/iam/iam_password_policy_reuse_24/iam_password_policy_reuse_24.metadata.json +++ b/prowler/providers/aws/services/iam/iam_password_policy_reuse_24/iam_password_policy_reuse_24.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsIamPasswordPolicy", + "ResourceType": "Other", "Description": "Ensure IAM password policy prevents password reuse: 24 or greater", "Risk": "Password policies are used to enforce password complexity requirements. IAM password policies can be used to ensure password are comprised of different character sets. It is recommended that the password policy prevents at least password reuse of 24 or greater.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_password_policy_symbol/iam_password_policy_symbol.metadata.json b/prowler/providers/aws/services/iam/iam_password_policy_symbol/iam_password_policy_symbol.metadata.json index a8d41bb9c0..45645479a7 100644 --- a/prowler/providers/aws/services/iam/iam_password_policy_symbol/iam_password_policy_symbol.metadata.json +++ b/prowler/providers/aws/services/iam/iam_password_policy_symbol/iam_password_policy_symbol.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsIamPasswordPolicy", + "ResourceType": "Other", "Description": "Ensure IAM password policy require at least one symbol", "Risk": "Password policies are used to enforce password complexity requirements. IAM password policies can be used to ensure password are comprised of different character sets. It is recommended that the password policy require at least one non-alphanumeric character.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_password_policy_uppercase/iam_password_policy_uppercase.metadata.json b/prowler/providers/aws/services/iam/iam_password_policy_uppercase/iam_password_policy_uppercase.metadata.json index 16964a0cdc..a1653fddb5 100644 --- a/prowler/providers/aws/services/iam/iam_password_policy_uppercase/iam_password_policy_uppercase.metadata.json +++ b/prowler/providers/aws/services/iam/iam_password_policy_uppercase/iam_password_policy_uppercase.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsIamPasswordPolicy", + "ResourceType": "Other", "Description": "Ensure IAM password policy requires at least one uppercase letter", "Risk": "Password policies are used to enforce password complexity requirements. IAM password policies can be used to ensure password are comprised of different character sets. It is recommended that the password policy require at least one uppercase letter.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_policy_attached_only_to_group_or_roles/iam_policy_attached_only_to_group_or_roles.metadata.json b/prowler/providers/aws/services/iam/iam_policy_attached_only_to_group_or_roles/iam_policy_attached_only_to_group_or_roles.metadata.json index 0796299558..bfd435bb16 100644 --- a/prowler/providers/aws/services/iam/iam_policy_attached_only_to_group_or_roles/iam_policy_attached_only_to_group_or_roles.metadata.json +++ b/prowler/providers/aws/services/iam/iam_policy_attached_only_to_group_or_roles/iam_policy_attached_only_to_group_or_roles.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "low", - "ResourceType": "AwsIamUser", + "ResourceType": "AwsIamPolicy", "Description": "Ensure IAM policies are attached only to groups or roles", "Risk": "By default IAM users, groups, and roles have no access to AWS resources. IAM policies are the means by which privileges are granted to users, groups, or roles. It is recommended that IAM policies be applied directly to groups and roles but not users. Assigning privileges at the group or role level reduces the complexity of access management as the number of users grow. Reducing access management complexity may in-turn reduce opportunity for a principal to inadvertently receive or retain excessive privileges.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.py b/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.py index 9717101927..11d660a03e 100644 --- a/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.py +++ b/prowler/providers/aws/services/iam/iam_role_cross_service_confused_deputy_prevention/iam_role_cross_service_confused_deputy_prevention.py @@ -1,8 +1,6 @@ from prowler.lib.check.models import Check, Check_Report_AWS -from prowler.providers.aws.lib.policy_condition_parser.policy_condition_parser import ( - is_condition_block_restrictive, -) from prowler.providers.aws.services.iam.iam_client import iam_client +from prowler.providers.aws.services.iam.lib.policy import is_policy_public class iam_role_cross_service_confused_deputy_prevention(Check): @@ -19,25 +17,12 @@ class iam_role_cross_service_confused_deputy_prevention(Check): report.resource_tags = role.tags report.status = "FAIL" report.status_extended = f"IAM Service Role {role.name} does not prevent against a cross-service confused deputy attack." - for statement in role.assume_role_policy["Statement"]: - if ( - statement["Effect"] == "Allow" - and ( - "sts:AssumeRole" in statement["Action"] - or "sts:*" in statement["Action"] - or "*" in statement["Action"] - ) - # Need to make sure we are checking the part of the assume role policy document that provides a service access - and "Service" in statement["Principal"] - # Check to see if the appropriate condition statements have been implemented - and "Condition" in statement - and is_condition_block_restrictive( - statement["Condition"], iam_client.audited_account - ) - ): - report.status = "PASS" - report.status_extended = f"IAM Service Role {role.name} prevents against a cross-service confused deputy attack." - break + if not is_policy_public( + role.assume_role_policy, + not_allowed_actions=["sts:AssumeRole", "sts:*"], + ): + report.status = "PASS" + report.status_extended = f"IAM Service Role {role.name} prevents against a cross-service confused deputy attack." findings.append(report) diff --git a/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.metadata.json b/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.metadata.json index 2f7044a6a4..ff99046fd9 100644 --- a/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.metadata.json +++ b/prowler/providers/aws/services/iam/iam_rotate_access_key_90_days/iam_rotate_access_key_90_days.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsIamUser", + "ResourceType": "AwsIamAccessKey", "Description": "Ensure access keys are rotated every 90 days or less", "Risk": "Access keys consist of an access key ID and secret access key which are used to sign programmatic requests that you make to AWS. AWS users need their own access keys to make programmatic calls to AWS from the AWS Command Line Interface (AWS CLI)- Tools for Windows PowerShell- the AWS SDKs- or direct HTTP calls using the APIs for individual AWS services. It is recommended that all access keys be regularly rotated.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/iam_securityaudit_role_created/iam_securityaudit_role_created.py b/prowler/providers/aws/services/iam/iam_securityaudit_role_created/iam_securityaudit_role_created.py index 8b6a2d55ed..863b363dfe 100644 --- a/prowler/providers/aws/services/iam/iam_securityaudit_role_created/iam_securityaudit_role_created.py +++ b/prowler/providers/aws/services/iam/iam_securityaudit_role_created/iam_securityaudit_role_created.py @@ -9,7 +9,9 @@ class iam_securityaudit_role_created(Check): report = Check_Report_AWS(self.metadata()) report.region = iam_client.region report.resource_id = "SecurityAudit" - report.resource_arn = "arn:aws:iam::aws:policy/SecurityAudit" + report.resource_arn = ( + f"arn:{iam_client.audited_partition}:iam::aws:policy/SecurityAudit" + ) if iam_client.entities_role_attached_to_securityaudit_policy: report.status = "PASS" report.status_extended = f"SecurityAudit policy attached to role {iam_client.entities_role_attached_to_securityaudit_policy[0]['RoleName']}." diff --git a/prowler/providers/aws/services/iam/iam_service.py b/prowler/providers/aws/services/iam/iam_service.py index 37b255e514..565867e565 100644 --- a/prowler/providers/aws/services/iam/iam_service.py +++ b/prowler/providers/aws/services/iam/iam_service.py @@ -69,13 +69,13 @@ class IAM(AWSService): self._list_attached_role_policies() self._list_mfa_devices() self.password_policy = self._get_password_policy() - support_policy_arn = ( - "arn:aws:iam::aws:policy/aws-service-role/AWSSupportServiceRolePolicy" - ) + support_policy_arn = f"arn:{self.audited_partition}:iam::aws:policy/aws-service-role/AWSSupportServiceRolePolicy" self.entities_role_attached_to_support_policy = ( self._list_entities_role_for_policy(support_policy_arn) ) - securityaudit_policy_arn = "arn:aws:iam::aws:policy/SecurityAudit" + securityaudit_policy_arn = ( + f"arn:{self.audited_partition}:iam::aws:policy/SecurityAudit" + ) self.entities_role_attached_to_securityaudit_policy = ( self._list_entities_role_for_policy(securityaudit_policy_arn) ) diff --git a/prowler/providers/aws/services/iam/iam_support_role_created/iam_support_role_created.py b/prowler/providers/aws/services/iam/iam_support_role_created/iam_support_role_created.py index c17bb03543..6763f6d354 100644 --- a/prowler/providers/aws/services/iam/iam_support_role_created/iam_support_role_created.py +++ b/prowler/providers/aws/services/iam/iam_support_role_created/iam_support_role_created.py @@ -9,9 +9,7 @@ class iam_support_role_created(Check): report = Check_Report_AWS(self.metadata()) report.region = iam_client.region report.resource_id = "AWSSupportServiceRolePolicy" - report.resource_arn = ( - "arn:aws:iam::aws:policy/aws-service-role/AWSSupportServiceRolePolicy" - ) + report.resource_arn = f"arn:{iam_client.audited_partition}:iam::aws:policy/aws-service-role/AWSSupportServiceRolePolicy" if iam_client.entities_role_attached_to_support_policy: report.status = "PASS" report.status_extended = f"Support policy attached to role {iam_client.entities_role_attached_to_support_policy[0]['RoleName']}." diff --git a/prowler/providers/aws/services/iam/iam_user_no_setup_initial_access_key/iam_user_no_setup_initial_access_key.metadata.json b/prowler/providers/aws/services/iam/iam_user_no_setup_initial_access_key/iam_user_no_setup_initial_access_key.metadata.json index 6bce20df15..971870b6a9 100644 --- a/prowler/providers/aws/services/iam/iam_user_no_setup_initial_access_key/iam_user_no_setup_initial_access_key.metadata.json +++ b/prowler/providers/aws/services/iam/iam_user_no_setup_initial_access_key/iam_user_no_setup_initial_access_key.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsIamUser", + "ResourceType": "AwsIamAccessKey", "Description": "Do not setup access keys during initial user setup for all IAM users that have a console password", "Risk": "AWS console defaults the checkbox for creating access keys to enabled. This results in many access keys being generated unnecessarily. In addition to unnecessary credentials, it also generates unnecessary management work in auditing and rotating these keys. Requiring that additional steps be taken by the user after their profile has been created will give a stronger indication of intent that access keys are (a) necessary for their work and (b) once the access key is established on an account that the keys may be in use somewhere in the organization.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/iam/lib/policy.py b/prowler/providers/aws/services/iam/lib/policy.py index ab5b716d57..95af304b3f 100644 --- a/prowler/providers/aws/services/iam/lib/policy.py +++ b/prowler/providers/aws/services/iam/lib/policy.py @@ -1,6 +1,7 @@ from ipaddress import ip_address, ip_network from prowler.lib.logger import logger +from prowler.providers.aws.aws_provider import read_aws_regions_file def is_policy_cross_account(policy: dict, audited_account: str) -> bool: @@ -42,39 +43,6 @@ def is_policy_cross_account(policy: dict, audited_account: str) -> bool: return False -def is_policy_public(policy: dict) -> bool: - """ - is_policy_public checks if the policy is publicly accessible. - If the "Principal" element value is set to { "AWS": "*" } and the policy statement is not using any Condition clauses to filter the access, the selected policy is publicly accessible. - Args: - policy (dict): The policy to check. - Returns: - bool: True if the policy is publicly accessible, False otherwise. - """ - if policy and "Statement" in policy: - for statement in policy["Statement"]: - if ( - "Principal" in statement - and ( - "*" == statement["Principal"] - or "arn:aws:iam::*:root" in statement["Principal"] - ) - and "Condition" not in statement - ): - return True - elif "Principal" in statement and "AWS" in statement["Principal"]: - if isinstance(statement["Principal"]["AWS"], str): - principals = [statement["Principal"]["AWS"]] - else: - principals = statement["Principal"]["AWS"] - for principal_arn in principals: - if ( - principal_arn == "*" or principal_arn == "arn:aws:iam::*:root" - ) and "Condition" not in statement: - return True - return False - - def check_full_service_access(service: str, policy: dict) -> bool: """ check_full_service_access checks if the policy allows full access to a service. @@ -174,3 +142,363 @@ def is_condition_restricting_from_private_ip(condition_statement: dict) -> bool: ) return is_from_private_ip + + +# TODO: Add logic for deny statements +def is_policy_public( + policy: dict, + source_account: str = "", + is_cross_account_allowed=False, + not_allowed_actions: list = [], +) -> bool: + """ + Check if the policy allows public access to the resource. + If the policy gives access to an AWS service principal is considered public if the policy is not pair with conditions since it can be invoked by AWS services in other accounts. + Args: + policy (dict): The AWS policy to check + source_account (str): The account to check if the access is restricted to it, default: "" + is_cross_account_allowed (bool): If the policy can allow cross-account access, default: False + not_allowed_actions (list): List of actions that are not allowed, default: []. If not_allowed_actions is empty, the function will not consider the actions in the policy. + Returns: + bool: True if the policy allows public access, False otherwise + """ + is_public = False + for statement in policy.get("Statement", []): + # Only check allow statements + if statement["Effect"] == "Allow": + principal = statement.get("Principal", "") + if ( + "*" in principal + or "arn:aws:iam::*:root" in principal + or ( + isinstance(principal, dict) + and ( + "*" in principal.get("AWS", "") + or "arn:aws:iam::*:root" in principal.get("AWS", "") + or ( + isinstance(principal.get("AWS"), list) + and ( + "*" in principal["AWS"] + or "arn:aws:iam::*:root" in principal["AWS"] + ) + ) + or "*" in principal.get("CanonicalUser", "") + or "arn:aws:iam::*:root" in principal.get("CanonicalUser", "") + or ( # Check if function can be invoked by other AWS services + ( + ".amazonaws.com" in principal.get("Service", "") + or "*" in principal.get("Service", "") + ) + ) + ) + ) + ) and ( + not not_allowed_actions # If not_allowed_actions is empty, the function will not consider the actions in the policy + or ( + statement.get( + "Action" + ) # If the statement has no action, it is not public + and ( + ( + ( + isinstance(statement.get("Action", ""), list) + and "*" in statement["Action"] + ) + or ( + isinstance(statement.get("Action", ""), str) + and statement.get("Action", "") == "*" + ) + ) + or ( + isinstance(statement.get("Action", ""), list) + and any( + action in not_allowed_actions + for action in statement["Action"] + ) + ) + or (statement.get("Action", "") in not_allowed_actions) + ) + ) + ): + is_public = ( + not is_condition_block_restrictive( + statement.get("Condition", {}), + source_account, + is_cross_account_allowed, + ) + and not is_condition_block_restrictive_organization( + statement.get("Condition", {}) + ) + and not is_condition_restricting_from_private_ip( + statement.get("Condition", {}) + ) + ) + if is_public: + break + return is_public + + +def is_condition_block_restrictive( + condition_statement: dict, + source_account: str = "", + is_cross_account_allowed=False, +): + """ + is_condition_block_restrictive parses the IAM Condition policy block and, by default, returns True if the source_account passed as argument is within, False if not. + + If argument is_cross_account_allowed is True it tests if the Condition block includes any of the operators allowlisted returning True if does, False if not. + + Args: + condition_statement: dict with an IAM Condition block, e.g.: + { + "StringLike": { + "AWS:SourceAccount": 111122223333 + } + } + + source_account: str with a 12-digit AWS Account number, e.g.: 111122223333, default: "" + + is_cross_account_allowed: bool to allow cross-account access, e.g.: True, default: False + + """ + is_condition_valid = False + + # The conditions must be defined in lowercase since the context key names are not case-sensitive. + # For example, including the aws:SourceAccount context key is equivalent to testing for AWS:SourceAccount + # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html + valid_condition_options = { + "StringEquals": [ + "aws:sourceaccount", + "aws:sourceowner", + "s3:resourceaccount", + "aws:principalaccount", + "aws:resourceaccount", + "aws:sourcearn", + "aws:sourcevpc", + "aws:sourcevpce", + ], + "StringLike": [ + "aws:sourceaccount", + "aws:sourceowner", + "aws:sourcearn", + "aws:principalarn", + "aws:resourceaccount", + "aws:principalaccount", + "aws:sourcevpc", + "aws:sourcevpce", + ], + "ArnLike": ["aws:sourcearn", "aws:principalarn"], + "ArnEquals": ["aws:sourcearn", "aws:principalarn"], + } + + for condition_operator, condition_operator_key in valid_condition_options.items(): + if condition_operator in condition_statement: + for value in condition_operator_key: + # We need to transform the condition_statement into lowercase + condition_statement[condition_operator] = { + k.lower(): v + for k, v in condition_statement[condition_operator].items() + } + + if value in condition_statement[condition_operator]: + # values are a list + if isinstance( + condition_statement[condition_operator][value], + list, + ): + is_condition_key_restrictive = True + # if cross account is not allowed check for each condition block looking for accounts + # different than default + if not is_cross_account_allowed: + # if there is an arn/account without the source account -> we do not consider it safe + # here by default we assume is true and look for false entries + for item in condition_statement[condition_operator][value]: + if source_account not in item: + is_condition_key_restrictive = False + break + + if is_condition_key_restrictive: + is_condition_valid = True + + # value is a string + elif isinstance( + condition_statement[condition_operator][value], + str, + ): + if is_cross_account_allowed: + is_condition_valid = True + else: + if ( + source_account + in condition_statement[condition_operator][value] + ): + is_condition_valid = True + + return is_condition_valid + + +def is_condition_block_restrictive_organization( + condition_statement: dict, +): + """ + is_condition_block_restrictive_organization parses the IAM Condition policy block and returns True if the condition_statement is restrictive for the organization, False if not. + + @param condition_statement: dict with an IAM Condition block, e.g.: + { + "StringLike": { + "AWS:PrincipalOrgID": "o-111122223333" + } + } + + """ + is_condition_valid = False + + # The conditions must be defined in lowercase since the context key names are not case-sensitive. + # For example, including the aws:PrincipalOrgID context key is equivalent to testing for AWS:PrincipalOrgID + # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html + valid_condition_options = { + "StringEquals": [ + "aws:principalorgid", + ], + "StringLike": [ + "aws:principalorgid", + ], + } + + for condition_operator, condition_operator_key in valid_condition_options.items(): + if condition_operator in condition_statement: + for value in condition_operator_key: + # We need to transform the condition_statement into lowercase + condition_statement[condition_operator] = { + k.lower(): v + for k, v in condition_statement[condition_operator].items() + } + + if value in condition_statement[condition_operator]: + # values are a list + if isinstance( + condition_statement[condition_operator][value], + list, + ): + is_condition_valid = True + for item in condition_statement[condition_operator][value]: + if item == "*": + is_condition_valid = False + break + + # value is a string + elif isinstance( + condition_statement[condition_operator][value], + str, + ): + if "*" not in condition_statement[condition_operator][value]: + is_condition_valid = True + + return is_condition_valid + + +def process_actions(effect, actions, target_set): + """ + process_actions processes the actions in the policy. + Args: + effect (str): The effect of the policy. + actions (str or list): The actions to process. + target_set (set): The set to store the actions. + """ + if effect in ["Allow", "Deny"] and actions: + if isinstance(actions, str): + target_set.add(actions) + elif isinstance(actions, list): + target_set.update(actions) + + +def check_admin_access(policy: dict) -> bool: + """ + check_admin_access checks if the policy allows admin access. + Args: + policy (dict): The policy to check. + Returns: + bool: True if the policy allows admin access, False otherwise. + """ + + if policy: + allowed_actions = set() + allowed_not_actions = set() + denied_actions = set() + denied_not_actions = set() + + statements = policy.get("Statement", []) + if not isinstance(statements, list): + statements = [statements] + + for statement in statements: + if statement.get("Resource") in [ + "*", + ["*"], + ["*/*"], + "*/*", + ["*:*"], + "*:*", + ] or ( + statement.get("NotResource") + and statement.get("NotResource") not in ["*", ["*"]] + ): + effect = statement.get("Effect") + actions = statement.get("Action") + not_actions = statement.get("NotAction") + if effect == "Allow": + process_actions(effect, actions, allowed_actions) + process_actions(effect, not_actions, allowed_not_actions) + elif effect == "Deny": + process_actions(effect, actions, denied_actions) + process_actions(effect, not_actions, denied_not_actions) + + # If there is only NotAction, it allows the rest of the actions + if not allowed_actions and allowed_not_actions: + allowed_actions.add("*") + # Check for invalid services in allowed NotAction + if allowed_not_actions: + invalid_not_actions = check_invalid_not_actions(allowed_not_actions) + if invalid_not_actions: + # Since it is an invalid NotAction, it allows all AWS actions + allowed_actions.add("*") + + if "*" in allowed_actions: + return True + return False + + +def check_invalid_not_actions(not_actions): + """ + Checks if the actions in NotAction have services that are not part of AWS. + Args: + not_actions (str or list): The NotAction to check. + Returns: + dict: A dictionary with invalid services and their actions. + """ + invalid_services = {} + + if isinstance(not_actions, str): + not_actions = [not_actions] + + for action in not_actions: + service = action.split(":")[0] + if not is_valid_aws_service(service): + if service not in invalid_services: + invalid_services[service] = [] + invalid_services[service].append(action) + + return invalid_services + + +def is_valid_aws_service(service): + """ + Checks if a service is a valid AWS service using aws_regions_by_service.json. + Args: + service (str): The service to check. + Returns: + bool: True if the service is valid, False otherwise. + """ + if service in read_aws_regions_file()["services"]: + return True + return False diff --git a/prowler/providers/aws/services/iam/lib/privilege_escalation.py b/prowler/providers/aws/services/iam/lib/privilege_escalation.py index 35b1b8dc35..3ed2b67093 100644 --- a/prowler/providers/aws/services/iam/lib/privilege_escalation.py +++ b/prowler/providers/aws/services/iam/lib/privilege_escalation.py @@ -1,4 +1,8 @@ from prowler.lib.logger import logger +from prowler.providers.aws.services.iam.lib.policy import ( + check_invalid_not_actions, + process_actions, +) # Does the tool analyze both users and roles, or just one or the other? --> Everything using AttachementCount. # Does the tool take a principal-centric or policy-centric approach? --> Policy-centric approach. @@ -7,6 +11,7 @@ from prowler.lib.logger import logger # Does the tool handle transitive privesc paths (i.e., attack chains)? --> Not yet. # Does the tool handle the DENY effect as expected? --> Yes, it checks DENY's statements with Action and NotAction. # Does the tool handle NotAction as expected? --> Yes +# Does the tool handle NotAction with invalid actions as expected? --> Yes # Does the tool handle Condition constraints? --> Not yet. # Does the tool handle service control policy (SCP) restrictions? --> No, SCP are within Organizations AWS API. @@ -89,13 +94,17 @@ privilege_escalation_policies_combination = { def find_privilege_escalation_combinations( - allowed_actions: set, denied_actions: set, denied_not_actions: set + allowed_actions: set, + denied_actions: set, + allowed_not_actions: set, + denied_not_actions: set, ) -> set: """ find_privilege_escalation_combinations finds the privilege escalation combinations. Args: allowed_actions (set): The allowed actions. denied_actions (set): The denied actions. + allowed_not_actions (set): The allowed not actions. denied_not_actions (set): The denied not actions. Returns: set: The privilege escalation combinations. @@ -103,27 +112,33 @@ def find_privilege_escalation_combinations( # Store all the action's combinations policies_combination = set() + hard_allowed_not_actions = set() try: - # First, we need to perform a left join with ALLOWED_ACTIONS and DENIED_ACTIONS - left_actions = allowed_actions.difference(denied_actions) - # Then, we need to find the DENIED_NOT_ACTIONS in LEFT_ACTIONS + # First, we need to perform a difference with allowed_actions and denied_actions + allowed_actions = allowed_actions.difference(denied_actions) + # Then, we need to do perform a difference with allowed_not_actions and denied_not_actions + allowed_not_actions = allowed_not_actions.difference(denied_not_actions) + # If there are allowed_not_actions, we have to check if there are allowed_actions that are not allowed by allowed_not_actions + if allowed_not_actions: + # If allowed_actions is *, we need to save allowed_not_actions since we cannot subtract them + if "*" in allowed_actions: + hard_allowed_not_actions = allowed_not_actions + else: + allowed_actions = allowed_actions - allowed_not_actions + # If there are denied_not_actions, means that every other action is denied if denied_not_actions: - privileged_actions = left_actions.intersection(denied_not_actions) - # If there is no Denied Not Actions - else: - privileged_actions = left_actions - + allowed_actions = allowed_actions.intersection(denied_not_actions) for values in privilege_escalation_policies_combination.values(): for val in values: val_set = set() val_set.add(val) # Look for specific api:action - if privileged_actions.intersection(val_set) == val_set: + if allowed_actions.intersection(val_set) == val_set: policies_combination.add(val) # Look for api:* else: - for permission in privileged_actions: + for permission in allowed_actions: # Here we have to handle if the api-action is admin, so "*" api_action = permission.split(":") # len() == 2, so api:action @@ -138,10 +153,15 @@ def find_privilege_escalation_combinations( # len() == 1, so * elif len(api_action) == 1: - api = api_action[0] - # Add permissions if the API is present - if api == "*": - policies_combination.add(val) + # Unless the action is *, we have to check if the action to evaluate is in the hard_allowed_not_actions + if ( + not hard_allowed_not_actions + or val not in hard_allowed_not_actions + ): + api = api_action[0] + # Add permissions if the API is present + if api == "*": + policies_combination.add(val) except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -150,21 +170,6 @@ def find_privilege_escalation_combinations( return policies_combination -def process_actions(effect, actions, target_set): - """ - process_actions processes the actions in the policy. - Args: - effect (str): The effect of the policy. - actions (str or list): The actions to process. - target_set (set): The set to store the actions. - """ - if effect in ["Allow", "Deny"] and actions: - if isinstance(actions, str): - target_set.add(actions) - elif isinstance(actions, list): - target_set.update(actions) - - def check_privilege_escalation(policy: dict) -> str: """ check_privilege_escalation checks if the policy allows privilege escalation. @@ -178,6 +183,7 @@ def check_privilege_escalation(policy: dict) -> str: if policy: allowed_actions = set() + allowed_not_actions = set() denied_actions = set() denied_not_actions = set() @@ -192,16 +198,26 @@ def check_privilege_escalation(policy: dict) -> str: if effect == "Allow": process_actions(effect, actions, allowed_actions) - process_actions(effect, not_actions, denied_not_actions) + process_actions(effect, not_actions, allowed_not_actions) elif effect == "Deny": process_actions(effect, actions, denied_actions) process_actions(effect, not_actions, denied_not_actions) + # If there is only NotAction, it allows the rest of the actions + if not allowed_actions and allowed_not_actions: + allowed_actions.add("*") + # Check for invalid services in allowed NotAction + if allowed_not_actions: + invalid_not_actions = check_invalid_not_actions(allowed_not_actions) + if invalid_not_actions: + # Since it is an invalid NotAction, it allows all AWS actions + allowed_actions.add("*") + policies_combination = find_privilege_escalation_combinations( - allowed_actions, denied_actions, denied_not_actions + allowed_actions, denied_actions, allowed_not_actions, denied_not_actions ) - # Check all policies combinations and see if matchs with some combo key + # Check all policies combinations and see if matches with some combo key combos = set() for ( key, diff --git a/prowler/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled.metadata.json b/prowler/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled.metadata.json index ec548c3401..0fc6c26267 100644 --- a/prowler/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled.metadata.json +++ b/prowler/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled.metadata.json @@ -1,29 +1,31 @@ { "Provider": "aws", "CheckID": "inspector2_is_enabled", - "CheckTitle": "Check if Inspector2 is enabled", + "CheckTitle": "Check if Inspector2 is enabled for Amazon EC2 instances, ECR container images and Lambda functions.", "CheckAliases": [ "inspector2_findings_exist" ], - "CheckType": [], + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], "ServiceName": "inspector2", "SubServiceName": "", "ResourceIdTemplate": "arn:aws:inspector2:region:account-id/detector-id", "Severity": "medium", - "ResourceType": "Other", - "Description": "Check if Inspector2 is enabled", + "ResourceType": "AwsAccount", + "Description": "Ensure that the new version of Amazon Inspector is enabled in order to help you improve the security and compliance of your AWS cloud environment. Amazon Inspector 2 is a vulnerability management solution that continually scans scans your Amazon EC2 instances, ECR container images, and Lambda functions to identify software vulnerabilities and instances of unintended network exposure.", "Risk": "Without using AWS Inspector, you may not be aware of all the security vulnerabilities in your AWS resources, which could lead to unauthorized access, data breaches, or other security incidents.", "RelatedUrl": "https://docs.aws.amazon.com/inspector/latest/user/findings-understanding.html", "Remediation": { "Code": { - "CLI": "aws inspector2 enable", + "CLI": "aws inspector2 enable --resource-types 'EC2' 'ECR' 'LAMBDA' 'LAMBDA_CODE'", "NativeIaC": "", - "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Inspector/amazon-inspector-findings.html", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/aws/Inspector2/enable-amazon-inspector2.html", "Terraform": "" }, "Recommendation": { - "Text": "Enable Inspector2", - "Url": "https://docs.aws.amazon.com/inspector/latest/user/what-is-inspector.html" + "Text": "Enable Amazon Inspector 2 for your AWS account.", + "Url": "https://docs.aws.amazon.com/inspector/latest/user/getting_started_tutorial.html" } }, "Categories": [], diff --git a/prowler/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled.py b/prowler/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled.py index 95969653c3..e735f42661 100644 --- a/prowler/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled.py +++ b/prowler/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled.py @@ -17,25 +17,44 @@ class inspector2_is_enabled(Check): report.region = inspector.region if inspector.status == "ENABLED": report.status = "PASS" - report.status_extended = "Inspector2 is enabled." + report.status_extended = "Inspector2 is enabled for EC2 instances, ECR container images, Lambda functions and code." + funtions_in_region = False + ec2_in_region = False + for function in awslambda_client.functions.values(): + if function.region == inspector.region: + funtions_in_region = True + for instance in ec2_client.instances: + if instance == inspector.region: + ec2_in_region = True + failed_services = [] + + if inspector.ec2_status != "ENABLED" and ( + inspector2_client.provider.scan_unused_services or ec2_in_region + ): + failed_services.append("EC2") + if inspector.ecr_status != "ENABLED" and ( + inspector2_client.provider.scan_unused_services + or ecr_client.registries[inspector.region].repositories + ): + failed_services.append("ECR") + if inspector.lambda_status != "ENABLED" and ( + inspector2_client.provider.scan_unused_services + or funtions_in_region + ): + failed_services.append("Lambda") + if inspector.lambda_code_status != "ENABLED" and ( + inspector2_client.provider.scan_unused_services + or funtions_in_region + ): + failed_services.append("Lambda Code") + + if failed_services: + report.status = "FAIL" + report.status_extended = f"Inspector2 is not enabled for the following services: {', '.join(failed_services)}." findings.append(report) else: - if not inspector2_client.provider.scan_unused_services: - funtions_in_region = False - ec2_in_region = False - for function in awslambda_client.functions.values(): - if function.region == inspector.region: - funtions_in_region = True - for instance in ec2_client.instances: - if instance == inspector.region: - ec2_in_region = True - if inspector2_client.provider.scan_unused_services or ( - funtions_in_region - or ecr_client.registries[inspector.region].repositories - or ec2_in_region - ): - report.status = "FAIL" - report.status_extended = "Inspector2 is not enabled." - findings.append(report) + report.status = "FAIL" + report.status_extended = "Inspector2 is not enabled in this account." + findings.append(report) return findings diff --git a/prowler/providers/aws/services/inspector2/inspector2_service.py b/prowler/providers/aws/services/inspector2/inspector2_service.py index 1c17806392..35626bc926 100644 --- a/prowler/providers/aws/services/inspector2/inspector2_service.py +++ b/prowler/providers/aws/services/inspector2/inspector2_service.py @@ -20,11 +20,18 @@ class Inspector2(AWSService): batch_get_account_status = regional_client.batch_get_account_status( accountIds=[self.audited_account] )["accounts"][0] + resourceStates = batch_get_account_status.get("resourceState") self.inspectors.append( Inspector( id="Inspector2", arn=f"arn:{self.audited_partition}:inspector2:{regional_client.region}:{self.audited_account}:inspector2", status=batch_get_account_status.get("state").get("status"), + ec2_status=resourceStates.get("ec2", {}).get("status"), + ecr_status=resourceStates.get("ecr", {}).get("status"), + lambda_status=resourceStates.get("lambda", {}).get("status"), + lambda_code_status=resourceStates.get("lambdaCode", {}).get( + "status" + ), region=regional_client.region, ) ) @@ -59,4 +66,8 @@ class Inspector(BaseModel): arn: str region: str status: str + ec2_status: str + ecr_status: str + lambda_status: str + lambda_code_status: str active_findings: bool = False diff --git a/prowler/providers/aws/services/kafka/kafka_cluster_encryption_at_rest_uses_cmk/kafka_cluster_encryption_at_rest_uses_cmk.metadata.json b/prowler/providers/aws/services/kafka/kafka_cluster_encryption_at_rest_uses_cmk/kafka_cluster_encryption_at_rest_uses_cmk.metadata.json index 536513a556..9154bfd8d0 100644 --- a/prowler/providers/aws/services/kafka/kafka_cluster_encryption_at_rest_uses_cmk/kafka_cluster_encryption_at_rest_uses_cmk.metadata.json +++ b/prowler/providers/aws/services/kafka/kafka_cluster_encryption_at_rest_uses_cmk/kafka_cluster_encryption_at_rest_uses_cmk.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "Kafka Cluster", "ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster", "Severity": "medium", - "ResourceType": "", + "ResourceType": "AwsMskCluster", "Description": "Kafka Cluster data stored at rest should be encrypted using Customer Managed Keys (CMK) for enhanced security and control over the encryption process.", "Risk": "Using default AWS-managed encryption keys might not meet certain compliance or regulatory requirements. With CMKs, you have more control over the encryption process and can rotate keys, define access policies, and enable key auditing.", "RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-encryption.html", diff --git a/prowler/providers/aws/services/kafka/kafka_cluster_enhanced_monitoring_enabled/kafka_cluster_enhanced_monitoring_enabled.metadata.json b/prowler/providers/aws/services/kafka/kafka_cluster_enhanced_monitoring_enabled/kafka_cluster_enhanced_monitoring_enabled.metadata.json index 55c8f9c14d..007644fa7c 100644 --- a/prowler/providers/aws/services/kafka/kafka_cluster_enhanced_monitoring_enabled/kafka_cluster_enhanced_monitoring_enabled.metadata.json +++ b/prowler/providers/aws/services/kafka/kafka_cluster_enhanced_monitoring_enabled/kafka_cluster_enhanced_monitoring_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "cluster", "ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster", "Severity": "medium", - "ResourceType": "", + "ResourceType": "AwsMskCluster", "Description": "Enhanced monitoring provides additional visibility into the performance and behavior of MSK (Kafka) brokers. By enabling enhanced monitoring, you can gain insights into potential issues and optimize the performance of your Kafka clusters.", "Risk": "Without enhanced monitoring, you may have limited visibility into the performance and health of your MSK brokers, which could lead to undetected issues and potential performance degradation.", "RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/monitoring.html", diff --git a/prowler/providers/aws/services/kafka/kafka_cluster_in_transit_encryption_enabled/kafka_cluster_in_transit_encryption_enabled.metadata.json b/prowler/providers/aws/services/kafka/kafka_cluster_in_transit_encryption_enabled/kafka_cluster_in_transit_encryption_enabled.metadata.json index 5559455929..bce398462f 100644 --- a/prowler/providers/aws/services/kafka/kafka_cluster_in_transit_encryption_enabled/kafka_cluster_in_transit_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/kafka/kafka_cluster_in_transit_encryption_enabled/kafka_cluster_in_transit_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "cluster", "ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster", "Severity": "medium", - "ResourceType": "AWS::MSK::Cluster", + "ResourceType": "AwsMskCluster", "Description": "Kafka clusters should have encryption in transit enabled to protect data as it travels across the network. This ensures that data is encrypted when transmitted between clients and brokers, preventing unauthorized access or data breaches.", "Risk": "If encryption in transit is not enabled, data transmitted over the network could be vulnerable to eavesdropping or man-in-the-middle attacks.", "RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-encryption.html", diff --git a/prowler/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public.metadata.json b/prowler/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public.metadata.json index c411409f35..1a03ed835e 100644 --- a/prowler/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public.metadata.json +++ b/prowler/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "cluster", "ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster", "Severity": "high", - "ResourceType": "AWS::MSK::Cluster", + "ResourceType": "AwsMskCluster", "Description": "The Kafka cluster is publicly accessible, which can expose sensitive data and increase the attack surface.", "Risk": "Exposing the Kafka cluster to the public can lead to unauthorized access, data breaches, and potential security threats.", "RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/client-access.html", diff --git a/prowler/providers/aws/services/kafka/kafka_cluster_mutual_tls_authentication_enabled/kafka_cluster_mutual_tls_authentication_enabled.metadata.json b/prowler/providers/aws/services/kafka/kafka_cluster_mutual_tls_authentication_enabled/kafka_cluster_mutual_tls_authentication_enabled.metadata.json index 687dd1e3c0..5f995fde9b 100644 --- a/prowler/providers/aws/services/kafka/kafka_cluster_mutual_tls_authentication_enabled/kafka_cluster_mutual_tls_authentication_enabled.metadata.json +++ b/prowler/providers/aws/services/kafka/kafka_cluster_mutual_tls_authentication_enabled/kafka_cluster_mutual_tls_authentication_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "cluster", "ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster", "Severity": "medium", - "ResourceType": "AWS::MSK::Cluster", + "ResourceType": "AwsMskCluster", "Description": "Mutual TLS Authentication ensures that both the client and the server are authenticated, providing an additional layer of security for communication within the Kafka cluster.", "Risk": "Without Mutual TLS Authentication, the cluster is vulnerable to man-in-the-middle attacks, and unauthorized clients may be able to access the cluster.", "RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-authentication.html", diff --git a/prowler/providers/aws/services/kafka/kafka_cluster_unrestricted_access_disabled/kafka_cluster_unrestricted_access_disabled.metadata.json b/prowler/providers/aws/services/kafka/kafka_cluster_unrestricted_access_disabled/kafka_cluster_unrestricted_access_disabled.metadata.json index 467fd8f1d5..bbe59cc8c1 100644 --- a/prowler/providers/aws/services/kafka/kafka_cluster_unrestricted_access_disabled/kafka_cluster_unrestricted_access_disabled.metadata.json +++ b/prowler/providers/aws/services/kafka/kafka_cluster_unrestricted_access_disabled/kafka_cluster_unrestricted_access_disabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "cluster", "ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster", "Severity": "high", - "ResourceType": "AWS::MSK::Cluster", + "ResourceType": "AwsMskCluster", "Description": "Kafka Clusters should not have unrestricted access enabled. Unrestricted access allows anyone to access the Kafka Cluster without any authentication. It is recommended to disable unrestricted access to prevent unauthorized access to the Kafka Cluster.", "Risk": "Unrestricted access to Kafka Clusters can lead to unauthorized access to the cluster and its data. It is recommended to restrict access to Kafka Clusters to only authorized entities.", "RelatedUrl": "https://docs.aws.amazon.com/msk/latest/developerguide/msk-configure-security.html", diff --git a/prowler/providers/aws/services/kafka/kafka_cluster_uses_latest_version/kafka_cluster_uses_latest_version.metadata.json b/prowler/providers/aws/services/kafka/kafka_cluster_uses_latest_version/kafka_cluster_uses_latest_version.metadata.json index 64cf77e06f..37f9accdd2 100644 --- a/prowler/providers/aws/services/kafka/kafka_cluster_uses_latest_version/kafka_cluster_uses_latest_version.metadata.json +++ b/prowler/providers/aws/services/kafka/kafka_cluster_uses_latest_version/kafka_cluster_uses_latest_version.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "cluster", "ResourceIdTemplate": "arn:partition:kafka:region:account-id:cluster", "Severity": "medium", - "ResourceType": "", + "ResourceType": "AwsMskCluster", "Description": "Ensure that your Amazon Managed Streaming for Apache Kafka (MSK) cluster is using the latest version to benefit from the latest security features, bug fixes, and performance improvements.", "Risk": "Running an outdated version of Amazon MSK may expose your cluster to security vulnerabilities, bugs, and performance issues.", "RelatedUrl": "https://docs.aws.amazon.com/lightsail/latest/userguide/amazon-lightsail-databases.html", diff --git a/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py b/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py index 62ed3fac24..e821c6bc13 100644 --- a/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py +++ b/prowler/providers/aws/services/kms/kms_key_not_publicly_accessible/kms_key_not_publicly_accessible.py @@ -1,4 +1,5 @@ from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.lib.policy import is_policy_public from prowler.providers.aws.services.kms.kms_client import kms_client @@ -17,32 +18,10 @@ class kms_key_not_publicly_accessible(Check): report.resource_tags = key.tags report.region = key.region # If the "Principal" element value is set to { "AWS": "*" } and the policy statement is not using any Condition clauses to filter the access, the selected AWS KMS master key is publicly accessible. - if key.policy and "Statement" in key.policy: - for statement in key.policy["Statement"]: - if ( - "Principal" in statement - and "*" == statement["Principal"] - and "Condition" not in statement - ): - report.status = "FAIL" - report.status_extended = ( - f"KMS key {key.id} may be publicly accessible." - ) - elif ( - "Principal" in statement and "AWS" in statement["Principal"] - ): - if isinstance(statement["Principal"]["AWS"], str): - principals = [statement["Principal"]["AWS"]] - else: - principals = statement["Principal"]["AWS"] - for principal_arn in principals: - if ( - principal_arn == "*" - and "Condition" not in statement - ): - report.status = "FAIL" - report.status_extended = ( - f"KMS key {key.id} may be publicly accessible." - ) + if is_policy_public(key.policy, not_allowed_actions=["kms:*"]): + report.status = "FAIL" + report.status_extended = ( + f"KMS key {key.id} may be publicly accessible." + ) findings.append(report) return findings diff --git a/prowler/providers/aws/services/lightsail/lightsail_database_public/lightsail_database_public.metadata.json b/prowler/providers/aws/services/lightsail/lightsail_database_public/lightsail_database_public.metadata.json index 34b975e85e..e01e15ceec 100644 --- a/prowler/providers/aws/services/lightsail/lightsail_database_public/lightsail_database_public.metadata.json +++ b/prowler/providers/aws/services/lightsail/lightsail_database_public/lightsail_database_public.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "database", "ResourceIdTemplate": "arn:partition:lightsail:region:account:RelationalDatabase/database-id", "Severity": "high", - "ResourceType": "", + "ResourceType": "Other", "Description": "The database is in public mode, which means it is exposed to the internet.", "Risk": "This can lead to unauthorized access to the database.", "RelatedUrl": "https://docs.aws.amazon.com/lightsail/latest/userguide/amazon-lightsail-databases.html", diff --git a/prowler/providers/aws/services/lightsail/lightsail_instance_automated_snapshots/lightsail_instance_automated_snapshots.metadata.json b/prowler/providers/aws/services/lightsail/lightsail_instance_automated_snapshots/lightsail_instance_automated_snapshots.metadata.json index 07243fa1b2..fb25928b31 100644 --- a/prowler/providers/aws/services/lightsail/lightsail_instance_automated_snapshots/lightsail_instance_automated_snapshots.metadata.json +++ b/prowler/providers/aws/services/lightsail/lightsail_instance_automated_snapshots/lightsail_instance_automated_snapshots.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:lightsail:region:account:Instance/instance-id", "Severity": "medium", - "ResourceType": "Instance", + "ResourceType": "Other", "Description": "Amazon Lightsail automatically creates daily snapshots of your instances. These snapshots are used for automatic backups and are stored at no additional cost. It is recommended to enable automatic snapshots for your Lightsail instances.", "Risk": "If automatic snapshots are not enabled, you may lose data in case of accidental deletion or corruption.", "RelatedUrl": "https://docs.aws.amazon.com/lightsail/latest/userguide/amazon-lightsail-configuring-automatic-snapshots.html", diff --git a/prowler/providers/aws/services/lightsail/lightsail_instance_public/lightsail_instance_public.metadata.json b/prowler/providers/aws/services/lightsail/lightsail_instance_public/lightsail_instance_public.metadata.json index fb4f985b9c..24e4933a85 100644 --- a/prowler/providers/aws/services/lightsail/lightsail_instance_public/lightsail_instance_public.metadata.json +++ b/prowler/providers/aws/services/lightsail/lightsail_instance_public/lightsail_instance_public.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "instance", "ResourceIdTemplate": "arn:partition:lightsail:region:account:Instance/instance-id", "Severity": "high", - "ResourceType": "Instance", + "ResourceType": "Other", "Description": "Ensure that Lightsail instances are not publicly accessible", "Risk": "If an instance is publicly accessible, it can be accessed by anyone on the internet. This can lead to unauthorized access to the instance and its data.", "RelatedUrl": "https://docs.aws.amazon.com/lightsail/latest/userguide/understanding-public-ip-and-private-ip-addresses-in-amazon-lightsail.html#ipv4-addresses", diff --git a/prowler/providers/aws/services/lightsail/lightsail_service.py b/prowler/providers/aws/services/lightsail/lightsail_service.py index 404cccbf43..e6f0d5f903 100644 --- a/prowler/providers/aws/services/lightsail/lightsail_service.py +++ b/prowler/providers/aws/services/lightsail/lightsail_service.py @@ -28,7 +28,7 @@ class Lightsail(AWSService): f"arn:{self.audited_partition}:lightsail:{regional_client.region}:{self.audited_account}:Instance", ) - if not self.audit_resources or is_resource_filtered( + if not self.audit_resources or ( is_resource_filtered(arn, self.audit_resources) ): ports = [] diff --git a/prowler/providers/aws/services/lightsail/lightsail_static_ip_unused/lightsail_static_ip_unused.metadata.json b/prowler/providers/aws/services/lightsail/lightsail_static_ip_unused/lightsail_static_ip_unused.metadata.json index bc6bfb9208..7c708eda18 100644 --- a/prowler/providers/aws/services/lightsail/lightsail_static_ip_unused/lightsail_static_ip_unused.metadata.json +++ b/prowler/providers/aws/services/lightsail/lightsail_static_ip_unused/lightsail_static_ip_unused.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "static_ip", "ResourceIdTemplate": "arn:partition:lightsail:region:account:static-ip/static-ip-id", "Severity": "low", - "ResourceType": "StaticIP", + "ResourceType": "Other", "Description": "Static IPs that are allocated but not attached to any instance are wasting resources and may pose a security risk if left unused for extended periods.", "Risk": "Unattached static IPs can be potential entry points for unauthorized access or DDoS attacks if not properly secured.", "RelatedUrl": "https://docs.aws.amazon.com/lightsail/latest/userguide/understanding-public-ip-and-private-ip-addresses-in-amazon-lightsail.html", diff --git a/prowler/providers/aws/services/macie/macie_is_enabled/macie_is_enabled.metadata.json b/prowler/providers/aws/services/macie/macie_is_enabled/macie_is_enabled.metadata.json index 411e635efb..034a9e0f8e 100644 --- a/prowler/providers/aws/services/macie/macie_is_enabled/macie_is_enabled.metadata.json +++ b/prowler/providers/aws/services/macie/macie_is_enabled/macie_is_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:access-analyzer:region:account-id:analyzer/resource-id", "Severity": "low", - "ResourceType": "AwsMacieSession", + "ResourceType": "Other", "Description": "Check if Amazon Macie is enabled.", "Risk": "Amazon Macie is a fully managed data security and data privacy service that uses machine learning and pattern matching to help you discover, monitor and protect your sensitive data in AWS.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/__init__.py b/prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots.metadata.json b/prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots.metadata.json new file mode 100644 index 0000000000..28eaa61069 --- /dev/null +++ b/prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "neptune_cluster_copy_tags_to_snapshots", + "CheckTitle": "Check if Neptune DB clusters are configured to copy tags to snapshots.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "neptune", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:rds:region:account-id:cluster:db-cluster-identifier", + "Severity": "low", + "ResourceType": "AwsRdsDbCluster", + "Description": "This check ensures that Neptune DB clusters are configured to copy all tags to snapshots when the snapshots are created.", + "Risk": "If tags are not copied to snapshots, the snapshots may lack necessary metadata for identification, governance, and access control, leading to potential mismanagement and security risks.", + "RelatedUrl": "https://docs.aws.amazon.com/neptune/latest/userguide/tagging.html#tagging-overview", + "Remediation": { + "Code": { + "CLI": "aws neptune modify-db-cluster --db-cluster-identifier --copy-tags-to-snapshot --apply-immediately", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/neptune-controls.html#neptune-8", + "Terraform": "" + }, + "Recommendation": { + "Text": "Configure your Neptune DB clusters to copy tags to snapshots when the snapshots are created.", + "Url": "https://docs.aws.amazon.com/neptune/latest/userguide/tagging.html#tagging-overview" + } + }, + "Categories": [ + "trustboundaries" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots.py b/prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots.py new file mode 100644 index 0000000000..e9982b663b --- /dev/null +++ b/prowler/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots.py @@ -0,0 +1,22 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.neptune.neptune_client import neptune_client + + +class neptune_cluster_copy_tags_to_snapshots(Check): + def execute(self): + findings = [] + for cluster_arn, cluster in neptune_client.clusters.items(): + report = Check_Report_AWS(self.metadata()) + report.region = cluster.region + report.resource_id = cluster.id + report.resource_arn = cluster.arn + report.resource_tags = cluster.tags + report.status = "FAIL" + report.status_extended = f"Neptune DB Cluster {cluster.id} is not configured to copy tags to snapshots." + if cluster.copy_tags_to_snapshot: + report.status = "PASS" + report.status_extended = f"Neptune DB Cluster {cluster.id} is configured to copy tags to snapshots." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/__init__.py b/prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs.metadata.json b/prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs.metadata.json new file mode 100644 index 0000000000..12f90b666e --- /dev/null +++ b/prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "neptune_cluster_integration_cloudwatch_logs", + "CheckTitle": "Check if Neptune Clusters have audit cloudwatch logs enabled.", + "CheckType": [ + "Software and Configuration Checks, AWS Security Best Practices" + ], + "ServiceName": "neptune", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", + "Severity": "medium", + "ResourceType": "AwsRdsDbCluster", + "Description": "Check if Neptune Clusters have audit cloudwatch logs enabled.", + "Risk": "If audit logs are not enabled, it is difficult to determine the root cause of security incidents.", + "RelatedUrl": "https://docs.aws.amazon.com/neptune/latest/userguide/auditing.html", + "Remediation": { + "Code": { + "CLI": "aws neptune modify-db-cluster --db-cluster-identifier --cloudwatch-logs-export-configuration '{\"EnableLogTypes\":[\"audit\"]}'", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/neptune-controls.html#neptune-2", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable audit logs for Neptune Clusters.", + "Url": "https://docs.aws.amazon.com/neptune/latest/userguide/cloudwatch-logs.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs.py b/prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs.py new file mode 100644 index 0000000000..90d6fbbb33 --- /dev/null +++ b/prowler/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.neptune.neptune_client import neptune_client + + +class neptune_cluster_integration_cloudwatch_logs(Check): + def execute(self): + findings = [] + for cluster in neptune_client.clusters.values(): + report = Check_Report_AWS(self.metadata()) + report.region = cluster.region + report.resource_id = cluster.name + report.resource_arn = cluster.arn + report.resource_tags = cluster.tags + report.status = "FAIL" + report.status_extended = f"Neptune Cluster {cluster.name} does not have cloudwatch audit logs enabled." + if "audit" in cluster.cloudwatch_logs: + report.status = "PASS" + report.status_extended = ( + f"Neptune Cluster {cluster.name} has cloudwatch audit logs enabled." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_public_snapshot/neptune_cluster_public_snapshot.metadata.json b/prowler/providers/aws/services/neptune/neptune_cluster_public_snapshot/neptune_cluster_public_snapshot.metadata.json index a67bd57531..5a3468e439 100644 --- a/prowler/providers/aws/services/neptune/neptune_cluster_public_snapshot/neptune_cluster_public_snapshot.metadata.json +++ b/prowler/providers/aws/services/neptune/neptune_cluster_public_snapshot/neptune_cluster_public_snapshot.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "critical", - "ResourceType": "AwsNeptuneDBClusterSnapshot", + "ResourceType": "AwsRdsDbClusterSnapshot", "Description": "Check if NeptuneDB manual cluster snapshot is public.", "Risk": "If you share an unencrypted manual snapshot as public, the snapshot is available to all AWS accounts. Public snapshots may result in unintended data exposure.", "RelatedUrl": "https://docs.aws.amazon.com/neptune/latest/userguide/security-considerations.html", diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/__init__.py b/prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted.metadata.json b/prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted.metadata.json new file mode 100644 index 0000000000..efd3dd70b2 --- /dev/null +++ b/prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "neptune_cluster_snapshot_encrypted", + "CheckTitle": "Check if Neptune DB cluster snapshots are encrypted at rest.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "neptune", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:rds:region:account-id:cluster-snapshot:db-cluster-snapshot-identifier", + "Severity": "medium", + "ResourceType": "AwsRdsDbClusterSnapshot", + "Description": "This check ensures that Neptune DB cluster snapshots are encrypted at rest to protect sensitive data from unauthorized access.", + "Risk": "If Neptune DB cluster snapshots are not encrypted, sensitive data might be exposed in case of unauthorized access, leading to potential data breaches and non-compliance with data protection regulations.", + "RelatedUrl": "https://docs.aws.amazon.com/neptune/latest/userguide/backup-restore-create-snapshot.html", + "Remediation": { + "Code": { + "CLI": "aws rds copy-db-cluster-snapshot --source-db-cluster-snapshot-identifier --target-db-cluster-snapshot-identifier --kms-key-id ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/neptune-controls.html#neptune-6", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that all Neptune DB cluster snapshots are encrypted at rest by enabling encryption on the cluster before creating snapshots or by copying unencrypted snapshots to encrypted ones.", + "Url": "https://docs.aws.amazon.com/neptune/latest/userguide/backup-restore-create-snapshot.html" + } + }, + "Categories": [ + "encryption" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted.py b/prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted.py new file mode 100644 index 0000000000..20d6dde603 --- /dev/null +++ b/prowler/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted.py @@ -0,0 +1,26 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.neptune.neptune_client import neptune_client + + +class neptune_cluster_snapshot_encrypted(Check): + def execute(self): + findings = [] + for snapshot in neptune_client.db_cluster_snapshots: + report = Check_Report_AWS(self.metadata()) + report.region = snapshot.region + report.resource_id = snapshot.id + report.resource_arn = snapshot.arn + report.resource_tags = snapshot.tags + report.status = "FAIL" + report.status_extended = ( + f"Neptune Cluster Snapshot {snapshot.id} is not encrypted at rest." + ) + if snapshot.encrypted: + report.status = "PASS" + report.status_extended = ( + f"Neptune Cluster Snapshot {snapshot.id} is encrypted at rest." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_storage_encrypted/neptune_cluster_storage_encrypted.metadata.json b/prowler/providers/aws/services/neptune/neptune_cluster_storage_encrypted/neptune_cluster_storage_encrypted.metadata.json index 282f9ee8d3..71e238dfb8 100644 --- a/prowler/providers/aws/services/neptune/neptune_cluster_storage_encrypted/neptune_cluster_storage_encrypted.metadata.json +++ b/prowler/providers/aws/services/neptune/neptune_cluster_storage_encrypted/neptune_cluster_storage_encrypted.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", "Severity": "high", - "ResourceType": "AwsRdsDbClusters", + "ResourceType": "AwsRdsDbCluster", "Description": "Check if Neptune Clusters storage is encrypted at rest.", "Risk": "Ensure that the data available on your Amazon Neptune database instances is encrypted in order to meet regulatory requirements and prevent unauthorized users from accessing sensitive information. Encryption provides an additional layer of protection by securing your Neptune databases from unauthorized access to the underlying storage. Neptune is a fast, scalable, highly secure and fully-managed graph database service that makes it easy to build and run applications that work with deeply connected datasets.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/neptune-controls.html#neptune-1", diff --git a/prowler/providers/aws/services/neptune/neptune_cluster_uses_public_subnet/neptune_cluster_uses_public_subnet.metadata.json b/prowler/providers/aws/services/neptune/neptune_cluster_uses_public_subnet/neptune_cluster_uses_public_subnet.metadata.json index 86f9cc0bb7..b13e2111ce 100644 --- a/prowler/providers/aws/services/neptune/neptune_cluster_uses_public_subnet/neptune_cluster_uses_public_subnet.metadata.json +++ b/prowler/providers/aws/services/neptune/neptune_cluster_uses_public_subnet/neptune_cluster_uses_public_subnet.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:rds:::cluster:", "Severity": "medium", - "ResourceType": "AWSNeptuneDBCluster", + "ResourceType": "AwsRdsDbCluster", "Description": "Ensure Neptune Cluster is not using a public subnet", "Risk": "There is a risk of exposing sensitive data if Neptune Cluster uses a public subnet.", "RelatedUrl": "https://docs.aws.amazon.com/neptune/latest/userguide/get-started-vpc.html", diff --git a/prowler/providers/aws/services/neptune/neptune_service.py b/prowler/providers/aws/services/neptune/neptune_service.py index d6d706961b..90b8969aa4 100644 --- a/prowler/providers/aws/services/neptune/neptune_service.py +++ b/prowler/providers/aws/services/neptune/neptune_service.py @@ -46,13 +46,14 @@ class Neptune(AWSService): backup_retention_period=cluster.get("BackupRetentionPeriod", 0), encrypted=cluster.get("StorageEncrypted", False), kms_key=cluster.get("KmsKeyId", ""), + cloudwatch_logs=cluster.get("EnabledCloudwatchLogsExports", []), multi_az=cluster["MultiAZ"], iam_auth=cluster.get("IAMDatabaseAuthenticationEnabled", False), deletion_protection=cluster.get("DeletionProtection", False), + copy_tags_to_snapshot=cluster.get("CopyTagsToSnapshot", False), db_subnet_group_id=cluster["DBSubnetGroup"], region=regional_client.region, ) - except Exception as error: logger.error( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -168,10 +169,12 @@ class Cluster(BaseModel): multi_az: bool iam_auth: bool deletion_protection: bool + copy_tags_to_snapshot: Optional[bool] region: str db_subnet_group_id: str subnets: Optional[list] tags: Optional[list] + cloudwatch_logs: Optional[list] class ClusterSnapshot(BaseModel): diff --git a/prowler/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection.metadata.json b/prowler/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection.metadata.json index 91bbb4344d..4346c2ee8d 100644 --- a/prowler/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection.metadata.json +++ b/prowler/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall/firewall-name", "Severity": "medium", - "ResourceType": "Other", + "ResourceType": "AwsNetworkFirewallFirewall", "Description": "Ensure that Deletion Protection safety feature is enabled for your Amazon VPC network firewalls in order to protect the firewalls from being accidentally deleted. By default, Deletion Protection is disabled for VPC network firewalls.", "Risk": "Without a network firewall, it can be difficult to monitor and control traffic within the VPC. This can make it harder to detect and prevent attacks or unauthorized access to resources.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/networkfirewall-controls.html#networkfirewall-9", diff --git a/prowler/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection.py b/prowler/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection.py index 991105e120..bf3cfa808d 100644 --- a/prowler/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection.py +++ b/prowler/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection.py @@ -7,11 +7,11 @@ from prowler.providers.aws.services.networkfirewall.networkfirewall_client impor class networkfirewall_deletion_protection(Check): def execute(self): findings = [] - for firewall in networkfirewall_client.network_firewalls: + for arn, firewall in networkfirewall_client.network_firewalls.items(): report = Check_Report_AWS(self.metadata()) report.region = firewall.region report.resource_id = firewall.name - report.resource_arn = firewall.arn + report.resource_arn = arn report.resource_tags = firewall.tags report.status = "FAIL" report.status_extended = f"Network Firewall {firewall.name} does not have deletion protection enabled." diff --git a/prowler/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc.metadata.json b/prowler/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc.metadata.json index c627fcf2af..7806387317 100644 --- a/prowler/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc.metadata.json +++ b/prowler/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall/firewall-name", "Severity": "medium", - "ResourceType": "Other", + "ResourceType": "AwsEc2Vpc", "Description": "Ensure all VPCs have Network Firewall enabled", "Risk": "Without a network firewall, it can be difficult to monitor and control traffic within the VPC. This can make it harder to detect and prevent attacks or unauthorized access to resources.", "RelatedUrl": "https://docs.aws.amazon.com/network-firewall/latest/developerguide/setting-up.html", diff --git a/prowler/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc.py b/prowler/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc.py index 6e68988f4f..94b9eac18f 100644 --- a/prowler/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc.py +++ b/prowler/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc.py @@ -17,7 +17,7 @@ class networkfirewall_in_all_vpc(Check): report.resource_tags = vpc.tags report.status = "FAIL" report.status_extended = f"VPC {vpc.name if vpc.name else vpc.id} does not have Network Firewall enabled." - for firewall in networkfirewall_client.network_firewalls: + for firewall in networkfirewall_client.network_firewalls.values(): if firewall.vpc_id == vpc.id: report.status = "PASS" report.status_extended = f"VPC {vpc.name if vpc.name else vpc.id} has Network Firewall enabled." diff --git a/prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/__init__.py b/prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated.metadata.json b/prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated.metadata.json new file mode 100644 index 0000000000..34f204f637 --- /dev/null +++ b/prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "networkfirewall_policy_rule_group_associated", + "CheckTitle": "Ensure Network Firewall Policies Have at Least One Rule Group Associated", + "CheckType": [ + "Software and Configuration Checks/Industry and Regulatory Standards/NIST 800-53" + ], + "ServiceName": "network-firewall", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:network-firewall::account-id:firewall-policy/policy-name", + "Severity": "medium", + "ResourceType": "AwsNetworkFirewallFirewallPolicy", + "Description": "This control checks whether a Network Firewall policy has any stateful or stateless rule groups associated. The control fails if stateless or stateful rule groups are not assigned.", + "Risk": "If a Network Firewall policy does not have at least one rule group associated, it may fail to properly filter and handle traffic, leaving the network vulnerable to unauthorized access and attacks.", + "RelatedUrl": "https://docs.aws.amazon.com/network-firewall/latest/developerguide/rule-groups.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/networkfirewall-controls.html#networkfirewall-3", + "Terraform": "" + }, + "Recommendation": { + "Text": "Regularly review your Network Firewall policies to ensure they have the necessary rule groups associated. Create and manage rule groups as needed to maintain effective traffic filtering and security.", + "Url": "https://docs.aws.amazon.com/network-firewall/latest/developerguide/firewall-policy-updating.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated.py b/prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated.py new file mode 100644 index 0000000000..1efb722c41 --- /dev/null +++ b/prowler/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.networkfirewall.networkfirewall_client import ( + networkfirewall_client, +) + + +class networkfirewall_policy_rule_group_associated(Check): + def execute(self): + findings = [] + for firewall in networkfirewall_client.network_firewalls.values(): + report = Check_Report_AWS(self.metadata()) + report.region = firewall.region + report.resource_id = firewall.name + report.resource_arn = firewall.arn + report.resource_tags = firewall.tags + report.status = "PASS" + report.status_extended = f"Network Firewall {firewall.name} policy has at least one rule group associated." + + if not firewall.stateful_rule_groups and not firewall.stateless_rule_groups: + report.status = "FAIL" + report.status_extended = f"Network Firewall {firewall.name} policy does not have rule groups associated." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/networkfirewall/networkfirewall_service.py b/prowler/providers/aws/services/networkfirewall/networkfirewall_service.py index f6a4bb56ee..d140c367a2 100644 --- a/prowler/providers/aws/services/networkfirewall/networkfirewall_service.py +++ b/prowler/providers/aws/services/networkfirewall/networkfirewall_service.py @@ -5,14 +5,18 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## NetworkFirewall class NetworkFirewall(AWSService): def __init__(self, provider): # Call AWSService's __init__ super().__init__("network-firewall", provider) - self.network_firewalls = [] + self.network_firewalls = {} self.__threading_call__(self._list_firewalls) - self._describe_firewall() + self.__threading_call__( + self._describe_firewall, self.network_firewalls.values() + ) + self.__threading_call__( + self._describe_firewall_policy, self.network_firewalls.values() + ) def _list_firewalls(self, regional_client): logger.info("Network Firewall - Listing Network Firewalls...") @@ -27,35 +31,54 @@ class NetworkFirewall(AWSService): network_firewall["FirewallArn"], self.audit_resources ) ): - self.network_firewalls.append( - Firewall( - arn=network_firewall.get("FirewallArn"), - region=regional_client.region, - name=network_firewall.get("FirewallName"), - ) + self.network_firewalls[ + network_firewall.get("FirewallArn", "") + ] = Firewall( + arn=network_firewall.get("FirewallArn"), + region=regional_client.region, + name=network_firewall.get("FirewallName"), ) except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - def _describe_firewall(self): + def _describe_firewall(self, network_firewall): logger.info("Network Firewall - Describe Network Firewalls...") try: - for network_firewall in self.network_firewalls: - regional_client = self.regional_clients[network_firewall.region] - describe_firewall = regional_client.describe_firewall( - FirewallArn=network_firewall.arn - )["Firewall"] - network_firewall.policy_arn = describe_firewall.get("FirewallPolicyArn") - network_firewall.vpc_id = describe_firewall.get("VpcId") - network_firewall.tags = describe_firewall.get("Tags") - network_firewall.encryption_type = describe_firewall.get( - "EncryptionConfiguration" - ).get("Type") - network_firewall.deletion_protection = describe_firewall.get( - "DeleteProtection" - ) + regional_client = self.regional_clients[network_firewall.region] + describe_firewall = regional_client.describe_firewall( + FirewallArn=network_firewall.arn, + )["Firewall"] + network_firewall.policy_arn = describe_firewall.get("FirewallPolicyArn") + network_firewall.vpc_id = describe_firewall.get("VpcId") + network_firewall.tags = describe_firewall.get("Tags", []) + encryption_config = describe_firewall.get("EncryptionConfiguration", {}) + network_firewall.encryption_type = encryption_config.get("Type") + network_firewall.deletion_protection = describe_firewall.get( + "DeleteProtection", False + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" + ) + + def _describe_firewall_policy(self, network_firewall): + logger.info("Network Firewall - Describe Network Firewall Policies...") + try: + regional_client = self.regional_clients[network_firewall.region] + describe_firewall_policy = regional_client.describe_firewall_policy( + FirewallPolicyArn=network_firewall.policy_arn, + ) + firewall_policy = describe_firewall_policy.get("FirewallPolicy", {}) + network_firewall.stateless_rule_groups = [ + group.get("ResourceArn", "") + for group in firewall_policy.get("StatelessRuleGroupReferences", []) + ] + network_firewall.stateful_rule_groups = [ + group.get("ResourceArn", "") + for group in firewall_policy.get("StatefulRuleGroupReferences", []) + ] except Exception as error: logger.error( f"{error.__class__.__name__}:{error.__traceback__.tb_lineno} -- {error}" @@ -71,3 +94,5 @@ class Firewall(BaseModel): tags: list = [] encryption_type: str = None deletion_protection: bool = False + stateless_rule_groups: list[str] = [] + stateful_rule_groups: list[str] = [] diff --git a/prowler/providers/aws/services/opensearch/opensearch_service.py b/prowler/providers/aws/services/opensearch/opensearch_service.py index ddb8cfdf8e..5b4c3fb162 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service.py +++ b/prowler/providers/aws/services/opensearch/opensearch_service.py @@ -127,6 +127,9 @@ class OpenSearchService(AWSService): "ServiceSoftwareOptions" ]["UpdateAvailable"] domain.version = describe_domain["DomainStatus"]["EngineVersion"] + domain.advanced_settings_enabled = describe_domain["DomainStatus"][ + "AdvancedSecurityOptions" + ]["Enabled"] except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -169,3 +172,4 @@ class OpenSearchDomain(BaseModel): update_available: bool = None version: str = None tags: Optional[list] = [] + advanced_settings_enabled: bool = None diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.metadata.json new file mode 100644 index 0000000000..710f09535f --- /dev/null +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "opensearch_service_domains_access_control_enabled", + "CheckTitle": "Check if Amazon Elasticsearch/Opensearch Service domains have fine grained access control enabled", + "CheckType": [], + "ServiceName": "opensearch", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "high", + "ResourceType": "AwsOpenSearchServiceDomain", + "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have fine grained access control enabled", + "Risk": "Amazon ES's fine graine access control enhances security by verifying that access to OpenSearch domains is controlled at a granular level, allowing for more precise permissions management and reducing the risk of unauthorised access.", + "RelatedUrl": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/fgac.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/opensearch-controls.html#opensearch-7", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable fine grained access control for your OpenSearch domains", + "Url": "https://docs.aws.amazon.com/opensearch-service/latest/developerguide/fgac.html#fgac-enabling" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.py b/prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.py new file mode 100644 index 0000000000..2427be6878 --- /dev/null +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled.py @@ -0,0 +1,24 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.opensearch.opensearch_client import ( + opensearch_client, +) + + +class opensearch_service_domains_access_control_enabled(Check): + def execute(self): + findings = [] + for domain in opensearch_client.opensearch_domains: + report = Check_Report_AWS(self.metadata()) + report.region = domain.region + report.resource_id = domain.name + report.resource_arn = domain.arn + report.resource_tags = domain.tags + report.status = "FAIL" + report.status_extended = f"Opensearch domain {domain.name} does not have fine grained access control enabled." + if domain.advanced_settings_enabled: + report.status = "PASS" + report.status_extended = f"Opensearch domain {domain.name} has fine grained access control enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_audit_logging_enabled/opensearch_service_domains_audit_logging_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_audit_logging_enabled/opensearch_service_domains_audit_logging_enabled.metadata.json index ed067062ee..91fba0e956 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_audit_logging_enabled/opensearch_service_domains_audit_logging_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_audit_logging_enabled/opensearch_service_domains_audit_logging_enabled.metadata.json @@ -10,7 +10,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "low", - "ResourceType": "AwsOpenSearchDomain", + "ResourceType": "AwsOpenSearchServiceDomain", "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have audit logging enabled", "Risk": "If logs are not enabled, monitoring of service use and threat analysis is not possible.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_cloudwatch_logging_enabled/opensearch_service_domains_cloudwatch_logging_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_cloudwatch_logging_enabled/opensearch_service_domains_cloudwatch_logging_enabled.metadata.json index 0a127e6bb5..fe9f312a34 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_cloudwatch_logging_enabled/opensearch_service_domains_cloudwatch_logging_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_cloudwatch_logging_enabled/opensearch_service_domains_cloudwatch_logging_enabled.metadata.json @@ -10,7 +10,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsOpenSearchDomain", + "ResourceType": "AwsOpenSearchServiceDomain", "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have logging enabled", "Risk": "Amazon ES exposes four Elasticsearch/Opensearch logs through Amazon CloudWatch Logs: error logs, search slow logs, index slow logs, and audit logs.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_encryption_at_rest_enabled/opensearch_service_domains_encryption_at_rest_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_encryption_at_rest_enabled/opensearch_service_domains_encryption_at_rest_enabled.metadata.json index 3099f98f41..d8aa1cd206 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_encryption_at_rest_enabled/opensearch_service_domains_encryption_at_rest_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_encryption_at_rest_enabled/opensearch_service_domains_encryption_at_rest_enabled.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsOpenSearchDomain", + "ResourceType": "AwsOpenSearchServiceDomain", "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have encryption at-rest enabled", "Risk": "If not enable unauthorized access to your data could risk increases.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_https_communications_enforced/opensearch_service_domains_https_communications_enforced.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_https_communications_enforced/opensearch_service_domains_https_communications_enforced.metadata.json index c6478288bc..79f696feac 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_https_communications_enforced/opensearch_service_domains_https_communications_enforced.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_https_communications_enforced/opensearch_service_domains_https_communications_enforced.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsOpenSearchDomain", + "ResourceType": "AwsOpenSearchServiceDomain", "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have enforce HTTPS enabled", "Risk": "If not enable unauthorized access to your data could risk increases.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_internal_user_database_enabled/opensearch_service_domains_internal_user_database_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_internal_user_database_enabled/opensearch_service_domains_internal_user_database_enabled.metadata.json index a4ec40e05e..2e1b2da9be 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_internal_user_database_enabled/opensearch_service_domains_internal_user_database_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_internal_user_database_enabled/opensearch_service_domains_internal_user_database_enabled.metadata.json @@ -10,7 +10,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsOpenSearchDomain", + "ResourceType": "AwsOpenSearchServiceDomain", "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have internal user database enabled", "Risk": "Internal User Database is convenient for demos, for production environment use Federated authentication.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_node_to_node_encryption_enabled/opensearch_service_domains_node_to_node_encryption_enabled.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_node_to_node_encryption_enabled/opensearch_service_domains_node_to_node_encryption_enabled.metadata.json index 8085f11d2b..b505352460 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_node_to_node_encryption_enabled/opensearch_service_domains_node_to_node_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_node_to_node_encryption_enabled/opensearch_service_domains_node_to_node_encryption_enabled.metadata.json @@ -11,7 +11,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsOpenSearchDomain", + "ResourceType": "AwsOpenSearchServiceDomain", "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have node-to-node encryption enabled", "Risk": "Node-to-node encryption provides an additional layer of security on top of the default features of Amazon ES. This architecture prevents potential attackers from intercepting traffic between Elasticsearch nodes and keeps the cluster secure.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.metadata.json index 4d6d5263c2..2a71338586 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_not_publicly_accessible/opensearch_service_domains_not_publicly_accessible.metadata.json @@ -10,7 +10,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "critical", - "ResourceType": "AwsOpenSearchDomain", + "ResourceType": "AwsOpenSearchServiceDomain", "Description": "Check if Amazon Opensearch/Elasticsearch domains are set as Public or if it has open policy access", "Risk": "Publicly accessible services could expose sensitive data to bad actors.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_updated_to_the_latest_service_software_version/opensearch_service_domains_updated_to_the_latest_service_software_version.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_updated_to_the_latest_service_software_version/opensearch_service_domains_updated_to_the_latest_service_software_version.metadata.json index 6f7a1189d2..ea2b9d705f 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_updated_to_the_latest_service_software_version/opensearch_service_domains_updated_to_the_latest_service_software_version.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_updated_to_the_latest_service_software_version/opensearch_service_domains_updated_to_the_latest_service_software_version.metadata.json @@ -10,7 +10,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "low", - "ResourceType": "AwsOpenSearchDomain", + "ResourceType": "AwsOpenSearchServiceDomain", "Description": "Check if Amazon Elasticsearch/Opensearch Service domains have updates available", "Risk": "Amazon ES regularly releases system software updates that add features or otherwise improve your domains.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/opensearch/opensearch_service_domains_use_cognito_authentication_for_kibana/opensearch_service_domains_use_cognito_authentication_for_kibana.metadata.json b/prowler/providers/aws/services/opensearch/opensearch_service_domains_use_cognito_authentication_for_kibana/opensearch_service_domains_use_cognito_authentication_for_kibana.metadata.json index 57b47ffe24..dd4eab9f61 100644 --- a/prowler/providers/aws/services/opensearch/opensearch_service_domains_use_cognito_authentication_for_kibana/opensearch_service_domains_use_cognito_authentication_for_kibana.metadata.json +++ b/prowler/providers/aws/services/opensearch/opensearch_service_domains_use_cognito_authentication_for_kibana/opensearch_service_domains_use_cognito_authentication_for_kibana.metadata.json @@ -10,7 +10,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "high", - "ResourceType": "AwsOpenSearchDomain", + "ResourceType": "AwsOpenSearchServiceDomain", "Description": "Check if Amazon Elasticsearch/Opensearch Service domains has Amazon Cognito or SAML authentication for Kibana enabled", "Risk": "Not enabling Amazon Cognito or SAML authentication for Kibana in AWS Elasticsearch/OpenSearch Service domains increases the likelihood of unauthorized access to sensitive data, potentially compromising system integrity.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/__init__.py b/prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy.metadata.json b/prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy.metadata.json new file mode 100644 index 0000000000..ef3e8c536e --- /dev/null +++ b/prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "organizations_opt_out_ai_services_policy", + "CheckTitle": "Ensure that AWS Organizations opt-out of AI services policy is enabled.", + "CheckType": [], + "ServiceName": "organizations", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service::account-id:organization/organization-id", + "Severity": "low", + "ResourceType": "Other", + "Description": "This control checks whether the AWS Organizations opt-out of AI services policy is enabled. The control fails if the policy is not enabled.", + "Risk": "By default, AWS may be using your data to train its AI models. This may include data from your AWS CloudTrail logs, AWS Config rules, and AWS GuardDuty findings. If you opt out of AI services, AWS will not use your data to train its AI models.", + "RelatedUrl": "https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_ai-opt-out_all.html", + "Remediation": { + "Code": { + "CLI": "aws organizations enable-policy-type --root-id --policy-type AI_SERVICES_OPT_OUT {'services': {'default': {'opt_out_policy': {'@@assign': 'optOut'}}}}", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Artificial Intelligence (AI) services opt-out policies enable you to control whether AWS AI services can store and use your content. Enable the AWS Organizations opt-out of AI services policy.", + "Url": "https://docs.aws.amazon.com/organizations/latest/userguide/disable-policy-type.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy.py b/prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy.py new file mode 100644 index 0000000000..f42263891a --- /dev/null +++ b/prowler/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy.py @@ -0,0 +1,37 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.organizations.organizations_client import ( + organizations_client, +) + + +class organizations_opt_out_ai_services_policy(Check): + def execute(self): + findings = [] + + for org in organizations_client.organizations: + report = Check_Report_AWS(self.metadata()) + report.resource_id = org.id + report.resource_arn = org.arn + report.region = organizations_client.region + report.status = "FAIL" + report.status_extended = ( + "AWS Organizations is not in-use for this AWS Account." + ) + if org.status == "ACTIVE": + report.status_extended = f"AWS Organization {org.id} has not opted out of all AI services, granting consent for AWS to access its data." + if org.policies is not None: # Access Denied to list_policies + for policy in org.policies.get("AISERVICES_OPT_OUT_POLICY", []): + if ( + policy.content.get("services", {}) + .get("default", {}) + .get("opt_out_policy", {}) + .get("@@assign") + == "optOut" + ): + report.status = "PASS" + report.status_extended = f"AWS Organization {org.id} has opted out of all AI services, not granting consent for AWS to access its data." + break + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions.py b/prowler/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions.py index 7c74be208c..39b341bc0b 100644 --- a/prowler/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions.py +++ b/prowler/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions.py @@ -12,27 +12,24 @@ class organizations_scp_check_deny_regions(Check): ) for org in organizations_client.organizations: - report = Check_Report_AWS(self.metadata()) - report.resource_id = org.id - report.resource_arn = org.arn - report.region = organizations_client.region - if org.status == "ACTIVE": - if org.policies is None: - # Access Denied to list_policies - continue - if not org.policies: - report.status = "FAIL" + if org.policies is not None: # Access denied to list policies + report = Check_Report_AWS(self.metadata()) + report.resource_id = org.id + report.resource_arn = org.arn + report.region = organizations_client.region + report.status = "FAIL" + report.status_extended = ( + "AWS Organizations is not in-use for this AWS Account." + ) + + if org.status == "ACTIVE": report.status_extended = ( - f"AWS Organization {org.id} has no SCP policies." + f"AWS Organizations {org.id} does not have SCP policies." ) - else: # We use this flag if we find a statement that is restricting regions but not all the configured ones: is_region_restricted_statement = False - for policy in org.policies: - # We only check SCP policies here - if policy.type != "SERVICE_CONTROL_POLICY": - continue + for policy in org.policies.get("SERVICE_CONTROL_POLICY", []): # Statements are not always list statements = policy.content.get("Statement") @@ -96,12 +93,6 @@ class organizations_scp_check_deny_regions(Check): report.status = "FAIL" report.status_extended = f"AWS Organization {org.id} has SCP policies but don't restrict AWS Regions." - else: - report.status = "FAIL" - report.status_extended = ( - "AWS Organizations is not in-use for this AWS Account." - ) - - findings.append(report) + findings.append(report) return findings diff --git a/prowler/providers/aws/services/organizations/organizations_service.py b/prowler/providers/aws/services/organizations/organizations_service.py index 9f749280c4..27b9124431 100644 --- a/prowler/providers/aws/services/organizations/organizations_service.py +++ b/prowler/providers/aws/services/organizations/organizations_service.py @@ -8,7 +8,7 @@ from prowler.lib.logger import logger from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -available_organizations_policies = [ +AVAILABLE_ORGANIZATIONS_POLICIES = [ "SERVICE_CONTROL_POLICY", "TAG_POLICY", "BACKUP_POLICY", @@ -16,13 +16,12 @@ available_organizations_policies = [ ] -################## Organizations class Organizations(AWSService): def __init__(self, provider): # Call AWSService's __init__ super().__init__(__class__.__name__, provider) self.organizations = [] - self.policies = [] + self.policies = {} self.delegated_administrators = [] self._describe_organization() @@ -30,15 +29,12 @@ class Organizations(AWSService): logger.info("Organizations - Describe Organization...") try: - # Check if Organizations is in-use try: organization_desc = self.client.describe_organization()["Organization"] organization_arn = organization_desc.get("Arn") organization_id = organization_desc.get("Id") organization_master_id = organization_desc.get("MasterAccountId") - # Fetch policies for organization: organization_policies = self._list_policies() - # Fetch delegated administrators for organization: organization_delegated_administrator = ( self._list_delegated_administrators() ) @@ -89,23 +85,24 @@ class Organizations(AWSService): f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - # I'm using list_policies instead of list_policies_for_target, because the last one only returns "Attached directly" policies but not "Inherited from..." policies. def _list_policies(self): logger.info("Organizations - List policies...") try: list_policies_paginator = self.client.get_paginator("list_policies") - for policy_type in available_organizations_policies: + policies = {} + for policy_type in AVAILABLE_ORGANIZATIONS_POLICIES: logger.info( "Organizations - List policies... - Type: %s", policy_type, ) + policies[policy_type] = [] for page in list_policies_paginator.paginate(Filter=policy_type): for policy in page["Policies"]: policy_id = policy.get("Id") policy_content = self._describe_policy(policy_id) policy_targets = self._list_targets_for_policy(policy_id) - self.policies.append( + policies[policy_type].append( Policy( arn=policy.get("Arn"), id=policy_id, @@ -118,7 +115,10 @@ class Organizations(AWSService): except ClientError as error: if error.response["Error"]["Code"] == "AccessDeniedException": - self.policies = None + policies = None + logger.error( + f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) except Exception as error: logger.error( @@ -126,12 +126,10 @@ class Organizations(AWSService): ) finally: - return self.policies + return policies def _describe_policy(self, policy_id) -> dict: logger.info("Organizations - Describe policy: %s ...", policy_id) - - # This operation can be called only from the organization’s management account or by a member account that is a delegated administrator for an Amazon Web Services service. try: policy_content = {} if policy_id: @@ -143,8 +141,7 @@ class Organizations(AWSService): if isinstance(policy_content, str): policy_content = json.loads(policy_content) - return policy_content # This could be not be a dict, because json.loads could return a list or a string depending on the content of policy_content object. - + return policy_content except Exception as error: logger.error( f"{self.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -170,7 +167,7 @@ class Organizations(AWSService): return [] def _list_delegated_administrators(self): - logger.info("Organizations - List Delegated Administrators") + logger.info("Organizations - List Delegated Administrators...") try: list_delegated_administrators_paginator = self.client.get_paginator( @@ -225,5 +222,5 @@ class Organization(BaseModel): id: str status: str master_id: str - policies: list[Policy] = None + policies: Optional[dict[str, list[Policy]]] = {} delegated_administrators: list[DelegatedAdministrator] = None diff --git a/prowler/providers/aws/services/organizations/organizations_tags_policies_enabled_and_attached/organizations_tags_policies_enabled_and_attached.py b/prowler/providers/aws/services/organizations/organizations_tags_policies_enabled_and_attached/organizations_tags_policies_enabled_and_attached.py index 7dcc4d3e95..f3cb419ed3 100644 --- a/prowler/providers/aws/services/organizations/organizations_tags_policies_enabled_and_attached/organizations_tags_policies_enabled_and_attached.py +++ b/prowler/providers/aws/services/organizations/organizations_tags_policies_enabled_and_attached/organizations_tags_policies_enabled_and_attached.py @@ -17,20 +17,17 @@ class organizations_tags_policies_enabled_and_attached(Check): report.status_extended = ( "AWS Organizations is not in-use for this AWS Account." ) + if org.status == "ACTIVE": - if org.policies is None: - # Access Denied to list_policies - continue - for policy in org.policies: - # We only check SCP policies here - if policy.type != "TAG_POLICY": - continue - - report.status_extended = f"AWS Organization {org.id} has tag policies enabled but not attached." - - if policy.targets: - report.status = "PASS" - report.status_extended = f"AWS Organization {org.id} has tag policies enabled and attached to an AWS account." + report.status_extended = ( + f"AWS Organizations {org.id} does not have tag policies." + ) + if org.policies is not None: # Access Denied to list_policies + for policy in org.policies.get("TAG_POLICY", []): + report.status_extended = f"AWS Organization {org.id} has tag policies enabled but not attached." + if policy.targets: + report.status = "PASS" + report.status_extended = f"AWS Organization {org.id} has tag policies enabled and attached to an AWS account." findings.append(report) diff --git a/prowler/providers/aws/services/rds/rds_cluster_backtrack_enabled/rds_cluster_backtrack_enabled.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_backtrack_enabled/rds_cluster_backtrack_enabled.metadata.json index 3b4a87aa50..a630b8fd5f 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_backtrack_enabled/rds_cluster_backtrack_enabled.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_backtrack_enabled/rds_cluster_backtrack_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", "Severity": "medium", - "ResourceType": "AwsRdsDbClsuter", + "ResourceType": "AwsRdsDbCluster", "Description": "Ensure that the Backtrack feature is enabled for your Amazon Aurora (with MySQL compatibility) database clusters in order to backtrack your clusters to a specific time, without using backups. Backtrack is an Amazon RDS feature that allows you to specify the amount of time that an Aurora MySQL database cluster needs to retain change records, in order to have a fast way to recover from user errors, such as dropping the wrong table or deleting the wrong row by moving your MySQL database to a prior point in time without the need to restore from a recent backup.", "Risk": "Once the Backtrack feature is enabled, Amazon RDS can quickly 'rewind' your Aurora MySQL database cluster to a point in time that you specify. In contrast to the backup and restore method, with Backtrack you can easily undo a destructive action, such as a DELETE query without a WHERE clause, with minimal downtime, you can rewind your Aurora cluster in just few minutes, and you can repeatedly backtrack a database cluster back and forth in time to help determine when a particular data change occurred.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-14", diff --git a/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.metadata.json index 0b6ff6c1ea..a035222b2e 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.metadata.json +++ b/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", "Severity": "low", - "ResourceType": "AwsRdsEventSubscription", + "ResourceType": "AwsRdsDbCluster", "Description": "Ensure that Amazon RDS event notification subscriptions are enabled for database cluster events, particularly maintenance and failure.", "Risk": "Without event subscriptions for critical events, such as maintenance and failures, you may not be aware of issues affecting your RDS clusters, leading to downtime or security vulnerabilities.", "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.html", diff --git a/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.py b/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.py index 66bfd6ec14..ebfdaf5248 100644 --- a/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.py +++ b/prowler/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription.py @@ -13,6 +13,7 @@ class rds_cluster_critical_event_subscription(Check): report.resource_id = rds_client.audited_account report.resource_arn = rds_client._get_rds_arn_template(db_event.region) report.region = db_event.region + report.resource_tags = db_event.tags if db_event.source_type == "db-cluster" and db_event.enabled: if db_event.event_list == [] or set(db_event.event_list) == { "maintenance", @@ -20,12 +21,14 @@ class rds_cluster_critical_event_subscription(Check): }: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "PASS" report.status_extended = "RDS cluster events are subscribed." elif db_event.event_list == ["maintenance"]: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "FAIL" report.status_extended = ( "RDS cluster event category of failure is not subscribed." @@ -34,6 +37,7 @@ class rds_cluster_critical_event_subscription(Check): elif db_event.event_list == ["failure"]: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "FAIL" report.status_extended = "RDS cluster event category of maintenance is not subscribed." diff --git a/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/__init__.py b/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.metadata.json new file mode 100644 index 0000000000..dc0feb7358 --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "rds_cluster_integration_cloudwatch_logs", + "CheckTitle": "Check if RDS cluster is integrated with CloudWatch Logs.", + "CheckType": [], + "ServiceName": "rds", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-cluster", + "Severity": "medium", + "ResourceType": "AwsRdsDbCluster", + "Description": "Check if RDS cluster is integrated with CloudWatch Logs. The types valid are Aurora MySQL, Aurora PostgreSQL, MySQL, PostgreSQL.", + "Risk": "If logs are not enabled, monitoring of service use and threat analysis is not possible.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_LogAccess.html", + "Remediation": { + "Code": { + "CLI": "aws rds modify-db-cluster --db-cluster-identifier --cloudwatch-logs-export-configuration {'EnableLogTypes':['audit',error','general','slowquery']} --apply-immediately", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-34", + "Terraform": "" + }, + "Recommendation": { + "Text": "Use CloudWatch Logs to perform real-time analysis of the log data. Create alarms and view metrics.", + "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/publishing_cloudwatchlogs.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.py b/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.py new file mode 100644 index 0000000000..6b39edc598 --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.rds.rds_client import rds_client + + +class rds_cluster_integration_cloudwatch_logs(Check): + def execute(self): + findings = [] + valid_engines = ["aurora-mysql", "aurora-postgresql", "mysql", "postgres"] + for db_cluster_arn, db_cluster in rds_client.db_clusters.items(): + if db_cluster.engine in valid_engines: + report = Check_Report_AWS(self.metadata()) + report.region = db_cluster.region + report.resource_id = db_cluster.id + report.resource_arn = db_cluster_arn + report.resource_tags = db_cluster.tags + if db_cluster.cloudwatch_logs: + report.status = "PASS" + report.status_extended = f"RDS Cluster {db_cluster.id} is shipping {', '.join(db_cluster.cloudwatch_logs)} logs to CloudWatch Logs." + else: + report.status = "FAIL" + report.status_extended = f"RDS Cluster {db_cluster.id} does not have CloudWatch Logs enabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/rds/rds_cluster_non_default_port/__init__.py b/prowler/providers/aws/services/rds/rds_cluster_non_default_port/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.metadata.json b/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.metadata.json new file mode 100644 index 0000000000..8e7122b2fb --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "rds_cluster_non_default_port", + "CheckTitle": "Check if RDS clusters are using non-default ports.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "rds", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:rds:region:account-id:cluster:db-cluster", + "Severity": "low", + "ResourceType": "AwsRdsDbCluster", + "Description": "Checks if an cluster uses a port other than the default port of the database engine. The control fails if the RDS cluster uses the default port.", + "Risk": "Using a default database port exposes the cluster to potential security vulnerabilities, as attackers are more likely to target known, commonly-used ports. This may result in unauthorized access to the database or increased susceptibility to automated attacks.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html", + "Remediation": { + "Code": { + "CLI": "aws rds modify-db-cluster --db-cluster-identifier --port ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-23", + "Terraform": "" + }, + "Recommendation": { + "Text": "Modify the RDS cluster to use a non-default port, and ensure that the security group permits access to the new port.", + "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.py b/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.py new file mode 100644 index 0000000000..dfbb39fb77 --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port.py @@ -0,0 +1,38 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.rds.rds_client import rds_client + + +class rds_cluster_non_default_port(Check): + def execute(self): + findings = [] + default_ports = { + 3306: ["mysql", "mariadb"], + 5432: ["postgres"], + 1521: ["oracle"], + 1433: ["sqlserver"], + 50000: ["db2"], + } + for db_cluster_arn, db_cluster in rds_client.db_clusters.items(): + report = Check_Report_AWS(self.metadata()) + report.region = db_cluster.region + report.resource_id = db_cluster.id + report.resource_arn = db_cluster_arn + report.resource_tags = db_cluster.tags + report.status = "PASS" + report.status_extended = ( + f"RDS Cluster {db_cluster.id} is not using the default port " + f"{db_cluster.port} for {db_cluster.engine}." + ) + if db_cluster.port in default_ports: + default_engines = default_ports[db_cluster.port] + for default_engine in default_engines: + if default_engine in db_cluster.engine.lower(): + report.status = "FAIL" + report.status_extended = ( + f"RDS Cluster {db_cluster.id} is using the default port " + f"{db_cluster.port} for {db_cluster.engine}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py b/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py index 1e9b416599..3d8edbae8b 100644 --- a/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py +++ b/prowler/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription.py @@ -13,6 +13,7 @@ class rds_instance_critical_event_subscription(Check): report.resource_id = rds_client.audited_account report.resource_arn = rds_client._get_rds_arn_template(db_event.region) report.region = db_event.region + report.resource_tags = db_event.tags if db_event.source_type == "db-instance" and db_event.enabled: if db_event.event_list == [] or set(db_event.event_list) == { "maintenance", @@ -21,21 +22,25 @@ class rds_instance_critical_event_subscription(Check): }: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "PASS" report.status_extended = "RDS instance events are subscribed." elif set(db_event.event_list) == {"maintenance"}: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "FAIL" report.status_extended = "RDS instance event categories of configuration change and failure are not subscribed." elif set(db_event.event_list) == {"configuration change"}: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "FAIL" report.status_extended = "RDS instance event categories of maintenance and failure are not subscribed." elif set(db_event.event_list) == {"failure"}: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "FAIL" report.status_extended = "RDS instance event categories of maintenance and configuration change are not subscribed." elif set(db_event.event_list) == { @@ -44,6 +49,7 @@ class rds_instance_critical_event_subscription(Check): }: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "FAIL" report.status_extended = ( "RDS instance event category of failure is not subscribed." @@ -54,6 +60,7 @@ class rds_instance_critical_event_subscription(Check): }: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "FAIL" report.status_extended = "RDS instance event category of configuration change is not subscribed." elif set(db_event.event_list) == { @@ -62,6 +69,7 @@ class rds_instance_critical_event_subscription(Check): }: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "FAIL" report.status_extended = "RDS instance event category of maintenance is not subscribed." findings.append(report) diff --git a/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.py b/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.py index 0e20c0674f..5628db91c2 100644 --- a/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.py +++ b/prowler/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups.py @@ -13,12 +13,14 @@ class rds_instance_event_subscription_parameter_groups(Check): report.resource_id = rds_client.audited_account report.resource_arn = rds_client._get_rds_arn_template(db_event.region) report.region = db_event.region + report.resource_tags = [] if db_event.source_type == "db-parameter-group" and db_event.enabled: if db_event.event_list == [] or db_event.event_list == [ "configuration change", ]: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "PASS" report.status_extended = ( "RDS parameter group events are subscribed." diff --git a/prowler/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups.py b/prowler/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups.py index 0cee7f58d1..a6d3eebe56 100644 --- a/prowler/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups.py +++ b/prowler/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups.py @@ -13,6 +13,7 @@ class rds_instance_event_subscription_security_groups(Check): report.resource_id = rds_client.audited_account report.resource_arn = rds_client._get_rds_arn_template(db_event.region) report.region = db_event.region + report.resource_tags = [] if db_event.source_type == "db-security-group" and db_event.enabled: if db_event.event_list == [] or set(db_event.event_list) == { "failure", @@ -20,6 +21,7 @@ class rds_instance_event_subscription_security_groups(Check): }: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "PASS" report.status_extended = ( "RDS security group events are subscribed." @@ -28,12 +30,14 @@ class rds_instance_event_subscription_security_groups(Check): elif db_event.event_list == ["configuration change"]: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "FAIL" report.status_extended = "RDS security group event category of failure is not subscribed." elif db_event.event_list == ["failure"]: report.resource_id = db_event.id report.resource_arn = db_event.arn + report.resource_tags = db_event.tags report.status = "FAIL" report.status_extended = "RDS security group event category of configuration change is not subscribed." diff --git a/prowler/providers/aws/services/rds/rds_instance_inside_vpc/__init__.py b/prowler/providers/aws/services/rds/rds_instance_inside_vpc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.metadata.json b/prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.metadata.json new file mode 100644 index 0000000000..f2dd511b64 --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "rds_instance_inside_vpc", + "CheckTitle": "Check if RDS instances are deployed within a VPC.", + "CheckType": [ + "Software and Configuration Checks, AWS Security Best Practices" + ], + "ServiceName": "rds", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "Severity": "high", + "ResourceType": "AwsRdsDbInstance", + "Description": "Check if RDS instances are deployed within a VPC.", + "Risk": "If your RDS instances are not deployed within a VPC, they are not isolated from the public internet and are exposed to potential security threats. Deploying RDS instances within a VPC allows you to control inbound and outbound traffic to and from the instances, and provides an additional layer of security to your database instances.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_VPC.WorkingWithRDSInstanceinaVPC.html#USER_VPC.Subnets", + "Remediation": { + "Code": { + "CLI": "aws rds modify-db-instance --db-instance-identifier --vpc-security-group-ids ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-18", + "Terraform": "" + }, + "Recommendation": { + "Text": "Ensure that your RDS instances are deployed within a VPC to provide an additional layer of security to your database instances.", + "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.py b/prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.py new file mode 100644 index 0000000000..98a6f1e13d --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc.py @@ -0,0 +1,25 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.rds.rds_client import rds_client + + +class rds_instance_inside_vpc(Check): + def execute(self): + findings = [] + for db_instance_arn, db_instance in rds_client.db_instances.items(): + report = Check_Report_AWS(self.metadata()) + report.region = db_instance.region + report.resource_id = db_instance.id + report.resource_arn = db_instance_arn + report.resource_tags = db_instance.tags + if db_instance.vpc_id: + report.status = "PASS" + report.status_extended = f"RDS Instance {db_instance.id} is deployed in a VPC {db_instance.vpc_id}." + else: + report.status = "FAIL" + report.status_extended = ( + f"RDS Instance {db_instance.id} is not deployed in a VPC." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs.py b/prowler/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs.py index 5d0a88d4fe..48bd76cac9 100644 --- a/prowler/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs.py +++ b/prowler/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs.py @@ -13,7 +13,7 @@ class rds_instance_integration_cloudwatch_logs(Check): report.resource_tags = db_instance.tags if db_instance.cloudwatch_logs: report.status = "PASS" - report.status_extended = f"RDS Instance {db_instance.id} is shipping {' '.join(db_instance.cloudwatch_logs)} to CloudWatch Logs." + report.status_extended = f"RDS Instance {db_instance.id} is shipping {', '.join(db_instance.cloudwatch_logs)} logs to CloudWatch Logs." else: report.status = "FAIL" report.status_extended = f"RDS Instance {db_instance.id} does not have CloudWatch Logs enabled." diff --git a/prowler/providers/aws/services/rds/rds_instance_non_default_port/__init__.py b/prowler/providers/aws/services/rds/rds_instance_non_default_port/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.metadata.json b/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.metadata.json new file mode 100644 index 0000000000..eba9298365 --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "rds_instance_non_default_port", + "CheckTitle": "Check if RDS instances are using non-default ports.", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "rds", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:rds:region:account-id:db-instance", + "Severity": "low", + "ResourceType": "AwsRdsDbInstance", + "Description": "Checks if an instance uses a port other than the default port of the database engine. The control fails if the RDS instance uses the default port.", + "Risk": "Using a default database port exposes the instance to potential security vulnerabilities, as attackers are more likely to target known, commonly-used ports. This may result in unauthorized access to the database or increased susceptibility to automated attacks.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html", + "Remediation": { + "Code": { + "CLI": "aws rds modify-db-instance --db-instance-identifier --port ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/rds-controls.html#rds-23", + "Terraform": "" + }, + "Recommendation": { + "Text": "Modify the RDS instance to use a non-default port, and ensure that the security group permits access to the new port.", + "Url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Overview.DBInstance.Modifying.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.py b/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.py new file mode 100644 index 0000000000..9e2dda34ed --- /dev/null +++ b/prowler/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port.py @@ -0,0 +1,38 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.rds.rds_client import rds_client + + +class rds_instance_non_default_port(Check): + def execute(self): + findings = [] + default_ports = { + 3306: ["mysql", "mariadb"], + 5432: ["postgres"], + 1521: ["oracle"], + 1433: ["sqlserver"], + 50000: ["db2"], + } + for db_instance_arn, db_instance in rds_client.db_instances.items(): + report = Check_Report_AWS(self.metadata()) + report.region = db_instance.region + report.resource_id = db_instance.id + report.resource_arn = db_instance_arn + report.resource_tags = db_instance.tags + report.status = "PASS" + report.status_extended = ( + f"RDS Instance {db_instance.id} is not using the default port " + f"{db_instance.port} for {db_instance.engine}." + ) + if db_instance.port in default_ports: + default_engines = default_ports[db_instance.port] + for default_engine in default_engines: + if default_engine in db_instance.engine.lower(): + report.status = "FAIL" + report.status_extended = ( + f"RDS Instance {db_instance.id} is using the default port " + f"{db_instance.port} for {db_instance.engine}." + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan.py b/prowler/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan.py index 8d60f8e972..f3e94d82c5 100644 --- a/prowler/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan.py +++ b/prowler/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan.py @@ -12,15 +12,21 @@ class rds_instance_protected_by_backup_plan(Check): report.resource_id = db_instance.id report.resource_arn = db_instance_arn report.resource_tags = db_instance.tags - if db_instance_arn in backup_client.protected_resources: + report.status = "FAIL" + report.status_extended = ( + f"RDS Instance {db_instance.id} is not protected by a backup plan." + ) + + if ( + db_instance_arn in backup_client.protected_resources + or "arn:aws:dynamodb:*:*:instance:*" + in backup_client.protected_resources + or "*" in backup_client.protected_resources + ): report.status = "PASS" report.status_extended = ( f"RDS Instance {db_instance.id} is protected by a backup plan." ) - else: - report.status = "FAIL" - report.status_extended = ( - f"RDS Instance {db_instance.id} is not protected by a backup plan." - ) + findings.append(report) return findings diff --git a/prowler/providers/aws/services/rds/rds_service.py b/prowler/providers/aws/services/rds/rds_service.py index 95e0da8d5b..f2bdbcc850 100644 --- a/prowler/providers/aws/services/rds/rds_service.py +++ b/prowler/providers/aws/services/rds/rds_service.py @@ -32,6 +32,7 @@ class RDS(AWSService): self.__threading_call__(self._describe_db_cluster_snapshot_attributes) self.__threading_call__(self._describe_db_engine_versions) self.__threading_call__(self._describe_db_event_subscriptions) + self.__threading_call__(self._list_tags, self.db_event_subscriptions) def _get_rds_arn_template(self, region): return ( @@ -100,6 +101,8 @@ class RDS(AWSService): copy_tags_to_snapshot=instance.get( "CopyTagsToSnapshot" ), + port=instance.get("Endpoint", {}).get("Port"), + vpc_id=instance.get("DBSubnetGroup", {}).get("VpcId"), ) except Exception as error: logger.error( @@ -109,10 +112,7 @@ class RDS(AWSService): def _describe_db_parameters(self, regional_client): logger.info("RDS - Describe DB Parameters...") try: - for ( - instance_arn, - instance, - ) in self.db_instances.items(): + for instance in self.db_instances.values(): if instance.region == regional_client.region: for parameter_group in instance.parameter_groups: describe_db_parameters_paginator = ( @@ -137,23 +137,26 @@ class RDS(AWSService): describe_db_certificates_paginator = regional_client.get_paginator( "describe_certificates" ) - for page in describe_db_certificates_paginator.paginate( - CertificateIdentifier=instance.ca_cert - ): - for certificate in page["Certificates"]: - instance.cert.append( - Certificate( - id=certificate["CertificateIdentifier"], - arn=certificate["CertificateArn"], - type=certificate["CertificateType"], - valid_from=certificate["ValidFrom"], - valid_till=certificate["ValidTill"], - customer_override=certificate["CustomerOverride"], - customer_override_valid_till=certificate.get( - "CustomerOverrideValidTill" - ), + if instance.ca_cert: + for page in describe_db_certificates_paginator.paginate( + CertificateIdentifier=instance.ca_cert + ): + for certificate in page["Certificates"]: + instance.cert.append( + Certificate( + id=certificate["CertificateIdentifier"], + arn=certificate["CertificateArn"], + type=certificate["CertificateType"], + valid_from=certificate["ValidFrom"], + valid_till=certificate["ValidTill"], + customer_override=certificate[ + "CustomerOverride" + ], + customer_override_valid_till=certificate.get( + "CustomerOverrideValidTill" + ), + ) ) - ) except Exception as error: logger.error( @@ -261,6 +264,7 @@ class RDS(AWSService): copy_tags_to_snapshot=cluster.get( "CopyTagsToSnapshot" ), + port=cluster.get("Port"), ) # We must use a unique value as the dict key to have unique keys self.db_clusters[db_cluster_arn] = db_cluster @@ -460,6 +464,7 @@ class RDS(AWSService): event_list=[], enabled=False, region=regional_client.region, + tags=[], ) ) except Exception as error: @@ -467,6 +472,19 @@ class RDS(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _list_tags(self, resource: any): + try: + if getattr(resource, "region", "") and getattr(resource, "arn", ""): + resource.tags = ( + self.regional_clients[resource.region] + .list_tags_for_resource(ResourceName=resource.arn) + .get("TagList", []) + ) + except Exception as error: + logger.error( + f"{resource.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class Certificate(BaseModel): id: str @@ -507,6 +525,8 @@ class DBInstance(BaseModel): ca_cert: Optional[str] cert: list[Certificate] = [] copy_tags_to_snapshot: Optional[bool] + port: Optional[int] + vpc_id: Optional[str] class DBCluster(BaseModel): @@ -531,6 +551,7 @@ class DBCluster(BaseModel): region: str tags: Optional[list] = [] copy_tags_to_snapshot: Optional[bool] + port: Optional[int] class DBSnapshot(BaseModel): @@ -572,3 +593,4 @@ class EventSubscription(BaseModel): event_list: list enabled: bool region: str + tags: Optional[list] diff --git a/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.metadata.json b/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.metadata.json index fb0e3e5d43..8185b26780 100644 --- a/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.metadata.json +++ b/prowler/providers/aws/services/route53/route53_dangling_ip_subdomain_takeover/route53_dangling_ip_subdomain_takeover.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "high", - "ResourceType": "AWSRoute53RecordSet", + "ResourceType": "Other", "Description": "Check if Route53 Records contains dangling IPs.", "Risk": "When an ephemeral AWS resource such as an Elastic IP (EIP) is released into the Amazon's Elastic IP pool, an attacker may acquire the EIP resource and effectively control the domain/subdomain associated with that EIP in your Route 53 DNS records.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/route53/route53_domains_privacy_protection_enabled/route53_domains_privacy_protection_enabled.metadata.json b/prowler/providers/aws/services/route53/route53_domains_privacy_protection_enabled/route53_domains_privacy_protection_enabled.metadata.json index 78928f3efb..cf1524157a 100644 --- a/prowler/providers/aws/services/route53/route53_domains_privacy_protection_enabled/route53_domains_privacy_protection_enabled.metadata.json +++ b/prowler/providers/aws/services/route53/route53_domains_privacy_protection_enabled/route53_domains_privacy_protection_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AwsRoute53Domain", + "ResourceType": "Other", "Description": "Enable Privacy Protection for for a Route53 Domain.", "Risk": "Without privacy protection enabled, ones personal information is published to the public WHOIS database.", "RelatedUrl": "https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-privacy-protection.html", diff --git a/prowler/providers/aws/services/route53/route53_domains_transferlock_enabled/route53_domains_transferlock_enabled.metadata.json b/prowler/providers/aws/services/route53/route53_domains_transferlock_enabled/route53_domains_transferlock_enabled.metadata.json index 76942fea27..046dd65060 100644 --- a/prowler/providers/aws/services/route53/route53_domains_transferlock_enabled/route53_domains_transferlock_enabled.metadata.json +++ b/prowler/providers/aws/services/route53/route53_domains_transferlock_enabled/route53_domains_transferlock_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AwsRoute53Domain", + "ResourceType": "Other", "Description": "Enable Transfer Lock for a Route53 Domain.", "Risk": "Without transfer lock enabled, a domain name could be incorrectly moved to a new registrar.", "RelatedUrl": "https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-lock.html", diff --git a/prowler/providers/aws/services/s3/s3_account_level_public_access_blocks/s3_account_level_public_access_blocks.metadata.json b/prowler/providers/aws/services/s3/s3_account_level_public_access_blocks/s3_account_level_public_access_blocks.metadata.json index 4aa5332b1b..96745af0d0 100644 --- a/prowler/providers/aws/services/s3/s3_account_level_public_access_blocks/s3_account_level_public_access_blocks.metadata.json +++ b/prowler/providers/aws/services/s3/s3_account_level_public_access_blocks/s3_account_level_public_access_blocks.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:s3:::bucket_name", "Severity": "high", - "ResourceType": "AwsS3Bucket", + "ResourceType": "AwsS3AccountPublicAccessBlock", "Description": "Check S3 Account Level Public Access Block.", "Risk": "Public access policies may be applied to sensitive data buckets.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/__init__.py b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json new file mode 100644 index 0000000000..5881dcf67f --- /dev/null +++ b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "s3_bucket_cross_account_access", + "CheckTitle": "Ensure that general-purpose bucket policies restrict access to other AWS accounts.", + "CheckType": [ + "Effects/Data Exposure" + ], + "ServiceName": "s3", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:s3:::bucket_name", + "Severity": "high", + "ResourceType": "AwsS3Bucket", + "Description": "This check verifies that S3 bucket policies are configured in a way that limits access to the intended AWS accounts only, preventing unauthorized access by external or unintended accounts.", + "Risk": "Allowing other AWS accounts to perform sensitive actions (e.g., modifying bucket policies, ACLs, or encryption settings) on your S3 buckets can lead to data exposure, unauthorized access, or misconfigurations, increasing the risk of insider threats or attacks.", + "RelatedUrl": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/s3-controls.html#s3-6", + "Terraform": "" + }, + "Recommendation": { + "Text": "Review and update your S3 bucket policies to remove permissions that grant external AWS accounts access to critical actions and implement least privilege principles to ensure sensitive operations are restricted to trusted accounts only", + "Url": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py new file mode 100644 index 0000000000..ea1c812436 --- /dev/null +++ b/prowler/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access.py @@ -0,0 +1,31 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.lib.policy import is_policy_public +from prowler.providers.aws.services.s3.s3_client import s3_client + + +class s3_bucket_cross_account_access(Check): + def execute(self): + findings = [] + for arn, bucket in s3_client.buckets.items(): + report = Check_Report_AWS(self.metadata()) + report.region = bucket.region + report.resource_id = bucket.name + report.resource_arn = arn + report.resource_tags = bucket.tags + report.status = "PASS" + report.status_extended = f"S3 Bucket {bucket.name} has a bucket policy but it does not allow cross account access." + + if not bucket.policy: + report.status = "PASS" + report.status_extended = ( + f"S3 Bucket {bucket.name} does not have a bucket policy." + ) + elif is_policy_public( + bucket.policy, source_account=s3_client.audited_account + ): + report.status = "FAIL" + report.status_extended = f"S3 Bucket {bucket.name} has a bucket policy allowing cross account access." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.py b/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.py index 33353d243b..8ad4011ebb 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.py +++ b/prowler/providers/aws/services/s3/s3_bucket_policy_public_write_access/s3_bucket_policy_public_write_access.py @@ -1,4 +1,5 @@ from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.iam.lib.policy import is_policy_public from prowler.providers.aws.services.s3.s3_client import s3_client from prowler.providers.aws.services.s3.s3control_client import s3control_client @@ -37,37 +38,18 @@ class s3_bucket_policy_public_write_access(Check): else: report.status = "PASS" report.status_extended = f"S3 Bucket {bucket.name} does not allow public write access in the bucket policy." - for statement in bucket.policy["Statement"]: - if ( - statement["Effect"] == "Allow" - and "Condition" not in statement - and ( - "Principal" in statement - and "*" in str(statement["Principal"]) - ) - and ( - ( - isinstance(statement["Action"], list) - and ( - "s3:PutObject" in statement["Action"] - or "*" in statement["Action"] - or "s3:*" in statement["Action"] - or "s3:Put*" in statement["Action"] - ) - ) - or ( - isinstance(statement["Action"], str) - and ( - "s3:PutObject" == statement["Action"] - or "*" == statement["Action"] - or "s3:*" == statement["Action"] - or "s3:Put*" == statement["Action"] - ) - ) - ) - ): - report.status = "FAIL" - report.status_extended = f"S3 Bucket {bucket.name} allows public write access in the bucket policy." + if is_policy_public( + bucket.policy, + not_allowed_actions=[ + "s3:PutObject", + "s3:DeleteObject", + "s3:*", + "s3:Put*", + "s3:Delete*", + ], + ): + report.status = "FAIL" + report.status_extended = f"S3 Bucket {bucket.name} allows public write access in the bucket policy." findings.append(report) return findings diff --git a/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py b/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py index a8658c094d..4278688724 100644 --- a/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py +++ b/prowler/providers/aws/services/s3/s3_bucket_public_access/s3_bucket_public_access.py @@ -1,10 +1,5 @@ from prowler.lib.check.models import Check, Check_Report_AWS -from prowler.providers.aws.lib.policy_condition_parser.policy_condition_parser import ( - is_condition_block_restrictive, -) -from prowler.providers.aws.services.iam.lib.policy import ( - is_condition_restricting_from_private_ip, -) +from prowler.providers.aws.services.iam.lib.policy import is_policy_public from prowler.providers.aws.services.s3.s3_client import s3_client from prowler.providers.aws.services.s3.s3control_client import s3control_client @@ -51,39 +46,8 @@ class s3_bucket_public_access(Check): report.status_extended = f"S3 Bucket {bucket.name} has public access due to bucket ACL." # 4. Check bucket policy - if bucket.policy: - for statement in bucket.policy.get("Statement", []): - if ( - "Principal" in statement - and statement["Effect"] == "Allow" - and not is_condition_block_restrictive( - statement.get("Condition", {}), "", True - ) - and ( - not is_condition_restricting_from_private_ip( - statement.get("Condition", {}) - ) - if statement.get("Condition", {}).get( - "IpAddress", {} - ) - else True - ) - ): - if "*" == statement["Principal"]: - report.status = "FAIL" - report.status_extended = f"S3 Bucket {bucket.name} has public access due to bucket policy." - elif "AWS" in statement["Principal"]: - principals = ( - statement["Principal"]["AWS"] - if isinstance( - statement["Principal"]["AWS"], list - ) - else [statement["Principal"]["AWS"]] - ) - - for principal_arn in principals: - if principal_arn == "*": - report.status = "FAIL" - report.status_extended = f"S3 Bucket {bucket.name} has public access due to bucket policy." + if is_policy_public(bucket.policy): + report.status = "FAIL" + report.status_extended = f"S3 Bucket {bucket.name} has public access due to bucket policy." findings.append(report) return findings diff --git a/prowler/providers/aws/services/s3/s3_service.py b/prowler/providers/aws/services/s3/s3_service.py index a208c01686..fdf87d8ed3 100644 --- a/prowler/providers/aws/services/s3/s3_service.py +++ b/prowler/providers/aws/services/s3/s3_service.py @@ -442,6 +442,27 @@ class S3(AWSService): f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _head_bucket(self, bucket_name): + logger.info("S3 - Checking if bucket exists...") + try: + self.client.head_bucket(Bucket=bucket_name) + return True + except ClientError as error: + if error.response["Error"]["Message"] == "Not Found": + logger.warning( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return False + else: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return True + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class S3Control(AWSService): def __init__(self, provider): diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/__init__.py b/prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances.metadata.json b/prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances.metadata.json new file mode 100644 index 0000000000..6a4622167b --- /dev/null +++ b/prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "sagemaker_endpoint_config_prod_variant_instances", + "CheckTitle": "SageMaker endpoint production variants should have at least two initial instances", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "sagemaker", + "SubServiceName": "", + "ResourceIdTemplate": "arn:aws:sagemaker:region:account-id:endpoint-config/resource-id", + "Severity": "medium", + "ResourceType": "Other", + "Description": "This control checks whether production variants of an Amazon SageMaker endpoint have an initial instance count greater than 1. A single instance creates a single point of failure and reduces availability.", + "Risk": "Having only one instance for a SageMaker endpoint production variant can lead to reduced availability, single points of failure, and slow recovery during incidents, especially if the instance becomes unavailable due to failure or security incidents.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/sagemaker-endpoint-config-prod-instance-count.html", + "Remediation": { + "Code": { + "CLI": "aws sagemaker update-endpoint --endpoint-name --endpoint-config-name ", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/sagemaker-controls.html#sagemaker-4", + "Terraform": "" + }, + "Recommendation": { + "Text": "To increase the initial instance count, configure your SageMaker endpoint to use more than 1 instance in the production variant for high availability.", + "Url": "https://docs.aws.amazon.com/sagemaker/latest/dg/serverless-endpoints-create.html#serverless-endpoints-create-config" + } + }, + "Categories": [ + "redundancy" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances.py b/prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances.py new file mode 100644 index 0000000000..43f07c6cf5 --- /dev/null +++ b/prowler/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.sagemaker.sagemaker_client import sagemaker_client + + +class sagemaker_endpoint_config_prod_variant_instances(Check): + def execute(self): + findings = [] + for endpoint_config in sagemaker_client.endpoint_configs.values(): + report = Check_Report_AWS(self.metadata()) + report.region = endpoint_config.region + report.resource_id = endpoint_config.name + report.resource_arn = endpoint_config.arn + report.resource_tags = endpoint_config.tags + report.status = "PASS" + report.status_extended = f"Sagemaker Endpoint Config {endpoint_config.name} has all production variants with more than one initial instance." + non_compliant_production_variants = [] + for production_variant in endpoint_config.production_variants: + if production_variant.initial_instance_count <= 1: + non_compliant_production_variants.append(production_variant.name) + + if non_compliant_production_variants: + report.status = "FAIL" + report.status_extended = f"Sagemaker Endpoint Config {endpoint_config.name}'s production variants {', '.join(non_compliant_production_variants)} with less than two initial instance." + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_models_network_isolation_enabled/sagemaker_models_network_isolation_enabled.metadata.json b/prowler/providers/aws/services/sagemaker/sagemaker_models_network_isolation_enabled/sagemaker_models_network_isolation_enabled.metadata.json index ca8608f54d..fdb7175757 100644 --- a/prowler/providers/aws/services/sagemaker/sagemaker_models_network_isolation_enabled/sagemaker_models_network_isolation_enabled.metadata.json +++ b/prowler/providers/aws/services/sagemaker/sagemaker_models_network_isolation_enabled/sagemaker_models_network_isolation_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:sagemaker:region:account-id:model", "Severity": "medium", - "ResourceType": "AwsSageMakerModel", + "ResourceType": "Other", "Description": "Check if Amazon SageMaker Models have network isolation enabled", "Risk": "This could provide an avenue for unauthorized access to your data.", "RelatedUrl": "https://docs.aws.amazon.com/sagemaker/latest/dg/studio-notebooks-and-internet-access.html", diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_models_vpc_settings_configured/sagemaker_models_vpc_settings_configured.metadata.json b/prowler/providers/aws/services/sagemaker/sagemaker_models_vpc_settings_configured/sagemaker_models_vpc_settings_configured.metadata.json index 9e1b3ae591..4e932ba4ef 100644 --- a/prowler/providers/aws/services/sagemaker/sagemaker_models_vpc_settings_configured/sagemaker_models_vpc_settings_configured.metadata.json +++ b/prowler/providers/aws/services/sagemaker/sagemaker_models_vpc_settings_configured/sagemaker_models_vpc_settings_configured.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:sagemaker:region:account-id:model", "Severity": "medium", - "ResourceType": "AwsSageMakerModel", + "ResourceType": "Other", "Description": "Check if Amazon SageMaker Models have VPC settings configured", "Risk": "This could provide an avenue for unauthorized access to your data.", "RelatedUrl": "https://docs.aws.amazon.com/sagemaker/latest/dg/studio-notebooks-and-internet-access.html", diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_service.py b/prowler/providers/aws/services/sagemaker/sagemaker_service.py index 4954c18fa8..4591fa994d 100644 --- a/prowler/providers/aws/services/sagemaker/sagemaker_service.py +++ b/prowler/providers/aws/services/sagemaker/sagemaker_service.py @@ -16,12 +16,21 @@ class SageMaker(AWSService): self.sagemaker_notebook_instances = [] self.sagemaker_models = [] self.sagemaker_training_jobs = [] + self.endpoint_configs = {} self.__threading_call__(self._list_notebook_instances) self.__threading_call__(self._list_models) self.__threading_call__(self._list_training_jobs) - self._describe_model(self.regional_clients) - self._describe_notebook_instance(self.regional_clients) - self._describe_training_job(self.regional_clients) + self.__threading_call__(self._list_endpoint_configs) + self.__threading_call__(self._describe_model, self.sagemaker_models) + self.__threading_call__( + self._describe_notebook_instance, self.sagemaker_notebook_instances + ) + self.__threading_call__( + self._describe_training_job, self.sagemaker_training_jobs + ) + self.__threading_call__( + self._describe_endpoint_config, self.endpoint_configs.values() + ) self._list_tags_for_resource() def _list_notebook_instances(self, regional_client): @@ -96,93 +105,84 @@ class SageMaker(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - def _describe_notebook_instance(self, regional_clients): + def _describe_notebook_instance(self, notebook_instance): logger.info("SageMaker - describing notebook instances...") try: - for notebook_instance in self.sagemaker_notebook_instances: - regional_client = regional_clients[notebook_instance.region] - try: - describe_notebook_instance = ( - regional_client.describe_notebook_instance( - NotebookInstanceName=notebook_instance.name - ) + regional_client = self.regional_clients[notebook_instance.region] + try: + describe_notebook_instance = regional_client.describe_notebook_instance( + NotebookInstanceName=notebook_instance.name + ) + except ClientError as error: + if error.response["Error"]["Code"] == "ValidationException": + logger.warning( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - except ClientError as error: - if error.response["Error"]["Code"] == "ValidationException": - logger.warning( - f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - - continue - if ( - "RootAccess" in describe_notebook_instance - and describe_notebook_instance["RootAccess"] == "Enabled" - ): - notebook_instance.root_access = True - if "SubnetId" in describe_notebook_instance: - notebook_instance.subnet_id = describe_notebook_instance["SubnetId"] - if ( - "DirectInternetAccess" in describe_notebook_instance - and describe_notebook_instance["RootAccess"] == "Enabled" - ): - notebook_instance.direct_internet_access = True - if "KmsKeyId" in describe_notebook_instance: - notebook_instance.kms_key_id = describe_notebook_instance[ - "KmsKeyId" - ] + if ( + "RootAccess" in describe_notebook_instance + and describe_notebook_instance["RootAccess"] == "Enabled" + ): + notebook_instance.root_access = True + if "SubnetId" in describe_notebook_instance: + notebook_instance.subnet_id = describe_notebook_instance["SubnetId"] + if ( + "DirectInternetAccess" in describe_notebook_instance + and describe_notebook_instance["RootAccess"] == "Enabled" + ): + notebook_instance.direct_internet_access = True + if "KmsKeyId" in describe_notebook_instance: + notebook_instance.kms_key_id = describe_notebook_instance["KmsKeyId"] except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - def _describe_model(self, regional_clients): + def _describe_model(self, model): logger.info("SageMaker - describing models...") try: - for model in self.sagemaker_models: - regional_client = regional_clients[model.region] - describe_model = regional_client.describe_model(ModelName=model.name) - if "EnableNetworkIsolation" in describe_model: - model.network_isolation = describe_model["EnableNetworkIsolation"] - if ( - "VpcConfig" in describe_model - and "Subnets" in describe_model["VpcConfig"] - ): - model.vpc_config_subnets = describe_model["VpcConfig"]["Subnets"] + regional_client = self.regional_clients[model.region] + describe_model = regional_client.describe_model(ModelName=model.name) + if "EnableNetworkIsolation" in describe_model: + model.network_isolation = describe_model["EnableNetworkIsolation"] + if ( + "VpcConfig" in describe_model + and "Subnets" in describe_model["VpcConfig"] + ): + model.vpc_config_subnets = describe_model["VpcConfig"]["Subnets"] except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - def _describe_training_job(self, regional_clients): + def _describe_training_job(self, training_job): logger.info("SageMaker - describing training jobs...") try: - for training_job in self.sagemaker_training_jobs: - regional_client = regional_clients[training_job.region] - describe_training_job = regional_client.describe_training_job( - TrainingJobName=training_job.name - ) - if "EnableInterContainerTrafficEncryption" in describe_training_job: - training_job.container_traffic_encryption = describe_training_job[ - "EnableInterContainerTrafficEncryption" - ] - if ( - "ResourceConfig" in describe_training_job - and "VolumeKmsKeyId" in describe_training_job["ResourceConfig"] - ): - training_job.volume_kms_key_id = describe_training_job[ - "ResourceConfig" - ]["VolumeKmsKeyId"] - if "EnableNetworkIsolation" in describe_training_job: - training_job.network_isolation = describe_training_job[ - "EnableNetworkIsolation" - ] - if ( - "VpcConfig" in describe_training_job - and "Subnets" in describe_training_job["VpcConfig"] - ): - training_job.vpc_config_subnets = describe_training_job[ - "VpcConfig" - ]["Subnets"] + regional_client = self.regional_clients[training_job.region] + describe_training_job = regional_client.describe_training_job( + TrainingJobName=training_job.name + ) + if "EnableInterContainerTrafficEncryption" in describe_training_job: + training_job.container_traffic_encryption = describe_training_job[ + "EnableInterContainerTrafficEncryption" + ] + if ( + "ResourceConfig" in describe_training_job + and "VolumeKmsKeyId" in describe_training_job["ResourceConfig"] + ): + training_job.volume_kms_key_id = describe_training_job[ + "ResourceConfig" + ]["VolumeKmsKeyId"] + if "EnableNetworkIsolation" in describe_training_job: + training_job.network_isolation = describe_training_job[ + "EnableNetworkIsolation" + ] + if ( + "VpcConfig" in describe_training_job + and "Subnets" in describe_training_job["VpcConfig"] + ): + training_job.vpc_config_subnets = describe_training_job["VpcConfig"][ + "Subnets" + ] except Exception as error: logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -217,6 +217,63 @@ class SageMaker(AWSService): logger.error( f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + try: + for endpoint in self.endpoint_configs.values(): + regional_client = self.regional_clients[endpoint.region] + response = regional_client.list_tags(ResourceArn=endpoint.arn)["Tags"] + endpoint.tags = response + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _list_endpoint_configs(self, regional_client): + logger.info("SageMaker - listing endpoint configs...") + try: + list_endpoint_config_paginator = regional_client.get_paginator( + "list_endpoint_configs" + ) + for page in list_endpoint_config_paginator.paginate(): + for endpoint_config in page["EndpointConfigs"]: + if not self.audit_resources or ( + is_resource_filtered( + endpoint_config["EndpointConfigArn"], self.audit_resources + ) + ): + self.endpoint_configs[endpoint_config["EndpointConfigArn"]] = ( + EndpointConfig( + name=endpoint_config["EndpointConfigName"], + region=regional_client.region, + arn=endpoint_config["EndpointConfigArn"], + ) + ) + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + def _describe_endpoint_config(self, endpoint_config): + logger.info("SageMaker - describing endpoint configs...") + try: + regional_client = self.regional_clients[endpoint_config.region] + describe_endpoint_config = regional_client.describe_endpoint_config( + EndpointConfigName=endpoint_config.name + ) + production_variants = [] + for production_variant in describe_endpoint_config["ProductionVariants"]: + production_variants.append( + ProductionVariant( + name=production_variant["VariantName"], + initial_instance_count=production_variant[ + "InitialInstanceCount" + ], + ) + ) + endpoint_config.production_variants = production_variants + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) class NotebookInstance(BaseModel): @@ -248,3 +305,16 @@ class TrainingJob(BaseModel): network_isolation: bool = None vpc_config_subnets: list[str] = [] tags: Optional[list] = [] + + +class ProductionVariant(BaseModel): + name: str + initial_instance_count: int + + +class EndpointConfig(BaseModel): + name: str + region: str + arn: str + production_variants: list[ProductionVariant] = [] + tags: Optional[list] = [] diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled.metadata.json b/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled.metadata.json index fbb9ca4d42..7e3d6f30ed 100644 --- a/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:sagemaker:region:account-id:training-job", "Severity": "medium", - "ResourceType": "AwsSageMakerTrainingJob", + "ResourceType": "Other", "Description": "Check if Amazon SageMaker Training jobs have intercontainer encryption enabled", "Risk": "If not restricted unintended access could happen.", "RelatedUrl": "https://docs.aws.amazon.com/sagemaker/latest/dg/interface-vpc-endpoint.html", diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_network_isolation_enabled/sagemaker_training_jobs_network_isolation_enabled.metadata.json b/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_network_isolation_enabled/sagemaker_training_jobs_network_isolation_enabled.metadata.json index f3a07bba23..7f1c2cd943 100644 --- a/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_network_isolation_enabled/sagemaker_training_jobs_network_isolation_enabled.metadata.json +++ b/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_network_isolation_enabled/sagemaker_training_jobs_network_isolation_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:sagemaker:region:account-id:training-job", "Severity": "medium", - "ResourceType": "AwsSageMakerTrainingJob", + "ResourceType": "Other", "Description": "Check if Amazon SageMaker Training jobs have network isolation enabled", "Risk": "This could provide an avenue for unauthorized access to your data.", "RelatedUrl": "https://docs.aws.amazon.com/sagemaker/latest/dg/interface-vpc-endpoint.html", diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_volume_and_output_encryption_enabled/sagemaker_training_jobs_volume_and_output_encryption_enabled.metadata.json b/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_volume_and_output_encryption_enabled/sagemaker_training_jobs_volume_and_output_encryption_enabled.metadata.json index aa17f24e18..03bbb7e573 100644 --- a/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_volume_and_output_encryption_enabled/sagemaker_training_jobs_volume_and_output_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_volume_and_output_encryption_enabled/sagemaker_training_jobs_volume_and_output_encryption_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:sagemaker:region:account-id:training-job", "Severity": "medium", - "ResourceType": "AwsSageMakerTrainingJob", + "ResourceType": "Other", "Description": "Check if Amazon SageMaker Training jobs have volume and output with KMS encryption enabled", "Risk": "Data exfiltration could happen if information is not protected. KMS keys provide additional security level to IAM policies.", "RelatedUrl": "https://docs.aws.amazon.com/sagemaker/latest/dg/key-management.html", diff --git a/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_vpc_settings_configured/sagemaker_training_jobs_vpc_settings_configured.metadata.json b/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_vpc_settings_configured/sagemaker_training_jobs_vpc_settings_configured.metadata.json index 9d4b4995c3..4366126d38 100644 --- a/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_vpc_settings_configured/sagemaker_training_jobs_vpc_settings_configured.metadata.json +++ b/prowler/providers/aws/services/sagemaker/sagemaker_training_jobs_vpc_settings_configured/sagemaker_training_jobs_vpc_settings_configured.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:sagemaker:region:account-id:training-job", "Severity": "medium", - "ResourceType": "AwsSageMakerTrainingJob", + "ResourceType": "Other", "Description": "Check if Amazon SageMaker Training job have VPC settings configured.", "Risk": "This could provide an avenue for unauthorized access to your data.", "RelatedUrl": "https://docs.aws.amazon.com/sagemaker/latest/dg/interface-vpc-endpoint.html", diff --git a/prowler/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.metadata.json b/prowler/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.metadata.json index 00bcea51fa..87a279ce6d 100644 --- a/prowler/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.metadata.json +++ b/prowler/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:partition:securityhub:region:account-id:hub/hub-id", "Severity": "medium", - "ResourceType": "AwsSecurityHubHub", + "ResourceType": "Other", "Description": "Check if Security Hub is enabled and its standard subscriptions.", "Risk": "AWS Security Hub gives you a comprehensive view of your security alerts and security posture across your AWS accounts.", "RelatedUrl": "https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-standards-enable-disable.html", diff --git a/prowler/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.py b/prowler/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.py index 20f9ae1f56..0c81f4a9d9 100644 --- a/prowler/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.py +++ b/prowler/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled.py @@ -12,6 +12,7 @@ class securityhub_enabled(Check): report.region = securityhub.region report.resource_id = securityhub.id report.resource_arn = securityhub.arn + report.resource_tags = securityhub.tags if securityhub.status == "ACTIVE": report.status = "PASS" if securityhub.standards: diff --git a/prowler/providers/aws/services/securityhub/securityhub_service.py b/prowler/providers/aws/services/securityhub/securityhub_service.py index 1711f63a94..b3050e0afe 100644 --- a/prowler/providers/aws/services/securityhub/securityhub_service.py +++ b/prowler/providers/aws/services/securityhub/securityhub_service.py @@ -1,3 +1,5 @@ +from typing import Optional + from botocore.client import ClientError from pydantic import BaseModel @@ -6,13 +8,13 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################## SecurityHub class SecurityHub(AWSService): def __init__(self, provider): # Call AWSService's __init__ super().__init__(__class__.__name__, provider) self.securityhubs = [] self.__threading_call__(self._describe_hub) + self.__threading_call__(self._list_tags, self.securityhubs) def _describe_hub(self, regional_client): logger.info("SecurityHub - Describing Hub...") @@ -85,6 +87,19 @@ class SecurityHub(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _list_tags(self, resource: any): + try: + if resource.status != "NOT_AVAILABLE": + resource.tags = [ + self.regional_clients[resource.region].list_tags_for_resource( + ResourceArn=resource.arn + )["Tags"] + ] + except Exception as error: + logger.error( + f"{resource.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class SecurityHubHub(BaseModel): arn: str @@ -93,3 +108,4 @@ class SecurityHubHub(BaseModel): standards: str integrations: str region: str + tags: Optional[list] diff --git a/prowler/providers/aws/services/shield/shield_advanced_protection_in_classic_load_balancers/shield_advanced_protection_in_classic_load_balancers.metadata.json b/prowler/providers/aws/services/shield/shield_advanced_protection_in_classic_load_balancers/shield_advanced_protection_in_classic_load_balancers.metadata.json index b6ab21f618..20a31de19a 100644 --- a/prowler/providers/aws/services/shield/shield_advanced_protection_in_classic_load_balancers/shield_advanced_protection_in_classic_load_balancers.metadata.json +++ b/prowler/providers/aws/services/shield/shield_advanced_protection_in_classic_load_balancers/shield_advanced_protection_in_classic_load_balancers.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingLoadBalancer", + "ResourceType": "AwsElbLoadBalancer", "Description": "Check if Classic Load Balancers are protected by AWS Shield Advanced.", "Risk": "AWS Shield Advanced provides expanded DDoS attack protection for your resources.", "RelatedUrl": "https://docs.aws.amazon.com/waf/latest/developerguide/configure-new-protection.html", diff --git a/prowler/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators.metadata.json b/prowler/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators.metadata.json index 19c07e8574..48c1225b52 100644 --- a/prowler/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators.metadata.json +++ b/prowler/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AwsGlobalAccelerator", + "ResourceType": "Other", "Description": "Check if Global Accelerators are protected by AWS Shield Advanced.", "Risk": "AWS Shield Advanced provides expanded DDoS attack protection for your resources.", "RelatedUrl": "https://docs.aws.amazon.com/waf/latest/developerguide/configure-new-protection.html", diff --git a/prowler/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators.py b/prowler/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators.py index 2c1b8869ed..9fdadc561d 100644 --- a/prowler/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators.py +++ b/prowler/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators.py @@ -14,6 +14,7 @@ class shield_advanced_protection_in_global_accelerators(Check): report.region = shield_client.region report.resource_id = accelerator.name report.resource_arn = accelerator.arn + report.resource_tags = accelerator.tags report.status = "FAIL" report.status_extended = f"Global Accelerator {accelerator.name} is not protected by AWS Shield Advanced." diff --git a/prowler/providers/aws/services/shield/shield_advanced_protection_in_internet_facing_load_balancers/shield_advanced_protection_in_internet_facing_load_balancers.metadata.json b/prowler/providers/aws/services/shield/shield_advanced_protection_in_internet_facing_load_balancers/shield_advanced_protection_in_internet_facing_load_balancers.metadata.json index 914fc88044..f31e8ef29a 100644 --- a/prowler/providers/aws/services/shield/shield_advanced_protection_in_internet_facing_load_balancers/shield_advanced_protection_in_internet_facing_load_balancers.metadata.json +++ b/prowler/providers/aws/services/shield/shield_advanced_protection_in_internet_facing_load_balancers/shield_advanced_protection_in_internet_facing_load_balancers.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AwsElasticLoadBalancingV2LoadBalancer", + "ResourceType": "AwsElbv2LoadBalancer", "Description": "Check if internet-facing Application Load Balancers are protected by AWS Shield Advanced.", "Risk": "AWS Shield Advanced provides expanded DDoS attack protection for your resources.", "RelatedUrl": "https://docs.aws.amazon.com/waf/latest/developerguide/configure-new-protection.html", diff --git a/prowler/providers/aws/services/shield/shield_advanced_protection_in_route53_hosted_zones/shield_advanced_protection_in_route53_hosted_zones.metadata.json b/prowler/providers/aws/services/shield/shield_advanced_protection_in_route53_hosted_zones/shield_advanced_protection_in_route53_hosted_zones.metadata.json index 27f6b2a3a0..8fbf144d42 100644 --- a/prowler/providers/aws/services/shield/shield_advanced_protection_in_route53_hosted_zones/shield_advanced_protection_in_route53_hosted_zones.metadata.json +++ b/prowler/providers/aws/services/shield/shield_advanced_protection_in_route53_hosted_zones/shield_advanced_protection_in_route53_hosted_zones.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "", "Severity": "medium", - "ResourceType": "AwsRoute53Domain", + "ResourceType": "AwsRoute53HostedZone", "Description": "Check if Route53 hosted zones are protected by AWS Shield Advanced.", "Risk": "AWS Shield Advanced provides expanded DDoS attack protection for your resources.", "RelatedUrl": "https://docs.aws.amazon.com/waf/latest/developerguide/configure-new-protection.html", diff --git a/prowler/providers/aws/services/sns/sns_subscription_not_using_http_endpoints/sns_subscription_not_using_http_endpoints.metadata.json b/prowler/providers/aws/services/sns/sns_subscription_not_using_http_endpoints/sns_subscription_not_using_http_endpoints.metadata.json index 7466d9099e..ecf858163d 100644 --- a/prowler/providers/aws/services/sns/sns_subscription_not_using_http_endpoints/sns_subscription_not_using_http_endpoints.metadata.json +++ b/prowler/providers/aws/services/sns/sns_subscription_not_using_http_endpoints/sns_subscription_not_using_http_endpoints.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:sns:region:account-id:topic", "Severity": "high", - "ResourceType": "AwsSNSTopic", + "ResourceType": "AwsSnsTopic", "Description": "Ensure there are no SNS subscriptions using HTTP endpoints", "Risk": "When you use HTTPS, messages are automatically encrypted during transit, even if the SNS topic itself isn't encrypted. Without HTTPS, a network-based attacker can eavesdrop on network traffic or manipulate it using an attack such as man-in-the-middle.", "RelatedUrl": "https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit", diff --git a/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.py b/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.py index fc93cbbcc4..196979883b 100644 --- a/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.py +++ b/prowler/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible.py @@ -1,5 +1,5 @@ from prowler.lib.check.models import Check, Check_Report_AWS -from prowler.providers.aws.lib.policy_condition_parser.policy_condition_parser import ( +from prowler.providers.aws.services.iam.lib.policy import ( is_condition_block_restrictive, is_condition_block_restrictive_organization, ) @@ -17,7 +17,7 @@ class sns_topics_not_publicly_accessible(Check): report.resource_tags = topic.tags report.status = "PASS" report.status_extended = ( - f"SNS topic {topic.name} is not publicly accesible." + f"SNS topic {topic.name} is not publicly accessible." ) if topic.policy: for statement in topic.policy["Statement"]: diff --git a/prowler/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible.py b/prowler/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible.py index a21e5a8c00..e649498b77 100644 --- a/prowler/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible.py +++ b/prowler/providers/aws/services/sqs/sqs_queues_not_publicly_accessible/sqs_queues_not_publicly_accessible.py @@ -1,7 +1,5 @@ from prowler.lib.check.models import Check, Check_Report_AWS -from prowler.providers.aws.lib.policy_condition_parser.policy_condition_parser import ( - is_condition_block_restrictive, -) +from prowler.providers.aws.services.iam.lib.policy import is_condition_block_restrictive from prowler.providers.aws.services.sqs.sqs_client import sqs_client diff --git a/prowler/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public.py b/prowler/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public.py index f13d493705..3cbc1082af 100644 --- a/prowler/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public.py +++ b/prowler/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public.py @@ -11,12 +11,25 @@ class ssm_documents_set_as_public(Check): report.resource_arn = document.arn report.resource_id = document.name report.resource_tags = document.tags - if document.account_owners: - report.status = "FAIL" - report.status_extended = f"SSM Document {document.name} is public." - else: + trusted_account_ids = ssm_client.audit_config.get("trusted_account_ids", []) + if ssm_client.audited_account not in trusted_account_ids: + trusted_account_ids.append(ssm_client.audited_account) + if not document.account_owners or document.account_owners == [ + ssm_client.audited_account + ]: report.status = "PASS" report.status_extended = f"SSM Document {document.name} is not public." + elif document.account_owners == ["all"]: + report.status = "FAIL" + report.status_extended = f"SSM Document {document.name} is public." + elif all(owner in trusted_account_ids for owner in document.account_owners): + report.status = "PASS" + report.status_extended = f"SSM Document {document.name} is shared to trusted AWS accounts: {', '.join(document.account_owners)}." + elif not all( + owner in trusted_account_ids for owner in document.account_owners + ): + report.status = "FAIL" + report.status_extended = f"SSM Document {document.name} is shared to non-trusted AWS accounts: {', '.join(document.account_owners)}." findings.append(report) diff --git a/prowler/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.metadata.json b/prowler/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.metadata.json index 38f33ebb15..a6c6b037d5 100644 --- a/prowler/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.metadata.json +++ b/prowler/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:ec2:region:account-id:instance/instance-id", "Severity": "high", - "ResourceType": "AwsEc2Instance", + "ResourceType": "AwsSsmPatchCompliance", "Description": "Check if EC2 instances managed by Systems Manager are compliant with patching requirements.", "Risk": "Without the most recent security patches your system is potentially vulnerable to cyberattacks. Even the best-designed software can not anticipate every future threat to cybersecurity. Poor patch management can leave an organizations data exposed subjecting them to malware and ransomware attacks.", "RelatedUrl": "https://docs.aws.amazon.com/systems-manager/latest/userguide/patch-compliance-identify.html", diff --git a/prowler/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.py b/prowler/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.py index 178775c227..b61e1d6119 100644 --- a/prowler/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.py +++ b/prowler/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching.py @@ -1,4 +1,5 @@ from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.ec2.ec2_client import ec2_client from prowler.providers.aws.services.ssm.ssm_client import ssm_client from prowler.providers.aws.services.ssm.ssm_service import ResourceStatus @@ -11,6 +12,11 @@ class ssm_managed_compliant_patching(Check): report.region = resource.region report.resource_id = resource.id report.resource_arn = resource.arn + # Find tags of the instance in ec2_client + for instance in ec2_client.instances: + if instance.id == resource.id: + report.resource_tags = instance.tags + if resource.status == ResourceStatus.COMPLIANT: report.status = "PASS" report.status_extended = ( diff --git a/prowler/providers/aws/services/storagegateway/storagegateway_fileshare_encryption_enabled/storagegateway_fileshare_encryption_enabled.metadata.json b/prowler/providers/aws/services/storagegateway/storagegateway_fileshare_encryption_enabled/storagegateway_fileshare_encryption_enabled.metadata.json index 3779f480c2..1d4409704e 100644 --- a/prowler/providers/aws/services/storagegateway/storagegateway_fileshare_encryption_enabled/storagegateway_fileshare_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/storagegateway/storagegateway_fileshare_encryption_enabled/storagegateway_fileshare_encryption_enabled.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "filegateway", "ResourceIdTemplate": "arn:aws:storagegateway:region:account-id:share", "Severity": "low", - "ResourceType": "AwsStorageGatewayFileShares", + "ResourceType": "Other", "Description": "Ensure that Amazon Storage Gateway service is using AWS KMS Customer Master Keys (CMKs) instead of AWS managed-keys (i.e. default keys) for file share data encryption, in order to have a fine-grained control over data-at-rest encryption/decryption process and meet compliance requirements. An AWS Storage Gateway file share is a file system mount point backed by Amazon S3 cloud storage.", "Risk": "This could provide an avenue for unauthorized access to your data by not having fine-grained control over data-at-rest encryption/decryption process and meet compliance requirements.", "RelatedUrl": "https://docs.aws.amazon.com/filegateway/latest/files3/encrypt-objects-stored-by-file-gateway-in-amazon-s3.html", diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json b/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json index 18c8c12945..23bee8a441 100644 --- a/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json +++ b/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.metadata.json @@ -9,7 +9,7 @@ "SubServiceName": "endpoint", "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", "Severity": "medium", - "ResourceType": "AwsEc2Vpc", + "ResourceType": "AwsEc2VpcEndpointService", "Description": "Find trust boundaries in VPC endpoint connections.", "Risk": "Account VPC could be linked to other accounts.", "RelatedUrl": "", diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.py b/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.py index 936057e657..4cac25481b 100644 --- a/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.py +++ b/prowler/providers/aws/services/vpc/vpc_endpoint_connections_trust_boundaries/vpc_endpoint_connections_trust_boundaries.py @@ -1,9 +1,7 @@ from re import compile from prowler.lib.check.models import Check, Check_Report_AWS -from prowler.providers.aws.lib.policy_condition_parser.policy_condition_parser import ( - is_condition_block_restrictive, -) +from prowler.providers.aws.services.iam.lib.policy import is_condition_block_restrictive from prowler.providers.aws.services.vpc.vpc_client import vpc_client diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/__init__.py b/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.metadata.json b/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.metadata.json new file mode 100644 index 0000000000..2a12db3c4e --- /dev/null +++ b/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "aws", + "CheckID": "vpc_endpoint_for_ec2_enabled", + "CheckTitle": "Amazon EC2 should be configured to use VPC endpoints that are created for the Amazon EC2 service.", + "CheckType": [], + "ServiceName": "ec2", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "medium", + "ResourceType": "AwsEc2VpcEndpointService", + "Description": "Ensure that a service endpoint for Amazon EC2 is created for each VPC. The check fails if a VPC does not have a VPC endpoint created for the Amazon EC2 service.", + "Risk": "Without VPC endpoints, network traffic between your VPC and Amazon EC2 may traverse the public internet, increasing the risk of unintended access or data exposure.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/service-vpc-endpoint-enabled.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-10", + "Terraform": "" + }, + "Recommendation": { + "Text": "To improve the security posture of your VPC, configure Amazon EC2 to use an interface VPC endpoint powered by AWS PrivateLink.", + "Url": "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/interface-vpc-endpoints.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.py b/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.py new file mode 100644 index 0000000000..0aa75d638a --- /dev/null +++ b/prowler/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.vpc.vpc_client import vpc_client + + +class vpc_endpoint_for_ec2_enabled(Check): + def execute(self): + findings = [] + for vpc_id, vpc in vpc_client.vpcs.items(): + if vpc_client.provider.scan_unused_services or vpc.in_use: + report = Check_Report_AWS(self.metadata()) + report.region = vpc.region + report.resource_tags = vpc.tags + report.status = "FAIL" + report.status_extended = f"VPC {vpc.id} has no EC2 endpoint." + report.resource_id = vpc.id + report.resource_arn = vpc.arn + for endpoint in vpc_client.vpc_endpoints: + if endpoint.vpc_id == vpc_id and "ec2" in endpoint.service_name: + report.status = "PASS" + report.status_extended = ( + f"VPC {vpc.id} has an EC2 {endpoint.type} endpoint." + ) + break + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/vpc/vpc_service.py b/prowler/providers/aws/services/vpc/vpc_service.py index 7e295f5870..194b7f85a4 100644 --- a/prowler/providers/aws/services/vpc/vpc_service.py +++ b/prowler/providers/aws/services/vpc/vpc_service.py @@ -31,6 +31,8 @@ class VPC(AWSService): self.vpc_subnets = {} self.__threading_call__(self._describe_vpc_subnets) self._describe_network_interfaces() + self.vpn_connections = {} + self.__threading_call__(self._describe_vpn_connections) def _describe_vpcs(self, regional_client): logger.info("VPC - Describing VPCs...") @@ -239,6 +241,7 @@ class VPC(AWSService): state=endpoint["State"], policy_document=endpoint_policy, owner_id=endpoint["OwnerId"], + type=endpoint["VpcEndpointType"], region=regional_client.region, tags=endpoint.get("Tags"), ) @@ -328,6 +331,8 @@ class VPC(AWSService): regional_client_for_subnet = self.regional_clients[ regional_client.region ] + public = False + nat_gateway = False route_tables_for_subnet = ( regional_client_for_subnet.describe_route_tables( Filters=[ @@ -350,21 +355,20 @@ class VPC(AWSService): ] ) ) - public = False - nat_gateway = False - for route in route_tables_for_subnet.get("RouteTables")[ - 0 - ].get("Routes"): - if ( - "GatewayId" in route - and "igw" in route["GatewayId"] - and route.get("DestinationCidrBlock", "") - == "0.0.0.0/0" - ): - # If the route table has a default route to an internet gateway, the subnet is public - public = True - if "NatGatewayId" in route: - nat_gateway = True + for route_table in route_tables_for_subnet.get( + "RouteTables" + ): + for route in route_table.get("Routes"): + if ( + "GatewayId" in route + and "igw" in route["GatewayId"] + and route.get("DestinationCidrBlock", "") + == "0.0.0.0/0" + ): + # If the route table has a default route to an internet gateway, the subnet is public + public = True + if "NatGatewayId" in route: + nat_gateway = True subnet_name = "" for tag in subnet.get("Tags", []): if tag["Key"] == "Name": @@ -398,6 +402,35 @@ class VPC(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _describe_vpn_connections(self, regional_client): + try: + describe_vpn_connections = regional_client.describe_vpn_connections() + + for vpn_connection in describe_vpn_connections["VpnConnections"]: + arn = f"arn:{self.audited_partition}:ec2:{regional_client.region}:{self.audited_account}:vpn-connection/{vpn_connection['VpnConnectionId']}" + if not self.audit_resources or ( + is_resource_filtered(arn, self.audit_resources) + ): + tunnels = [] + for tunnel in vpn_connection["VgwTelemetry"]: + tunnels.append( + VpnTunnel( + status=tunnel["Status"], + outside_ip_address=tunnel["OutsideIpAddress"], + ) + ) + self.vpn_connections[arn] = VpnConnection( + id=vpn_connection["VpnConnectionId"], + tunnels=tunnels, + region=regional_client.region, + tags=vpn_connection.get("Tags"), + ) + + except Exception as error: + logger.error( + f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class VpcSubnet(BaseModel): arn: str @@ -453,6 +486,7 @@ class VpcEndpoint(BaseModel): state: str policy_document: Optional[dict] owner_id: str + type: str region: str tags: Optional[list] = [] @@ -465,3 +499,15 @@ class VpcEndpointService(BaseModel): allowed_principals: list = [] region: str tags: Optional[list] = [] + + +class VpnTunnel(BaseModel): + status: str + outside_ip_address: str + + +class VpnConnection(BaseModel): + id: str + tunnels: list[VpnTunnel] + region: str + tags: Optional[list] = [] diff --git a/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/__init__.py b/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.metadata.json b/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.metadata.json new file mode 100644 index 0000000000..969f975df5 --- /dev/null +++ b/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.metadata.json @@ -0,0 +1,34 @@ +{ + "Provider": "aws", + "CheckID": "vpc_vpn_connection_tunnels_up", + "CheckTitle": "Both VPN tunnels for an AWS Site-to-Site VPN connection should be up", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices" + ], + "ServiceName": "vpc", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:vpn-connection/resource-id", + "Severity": "medium", + "ResourceType": "AwsEc2ClientVpnEndpoint", + "Description": "A VPN tunnel is an encrypted link where data can pass from the customer network to or from AWS within an AWS Site-to-Site VPN connection. Each VPN connection includes two VPN tunnels which you can simultaneously use for high availability. Ensuring that both VPN tunnels are up for a VPN connection is important for confirming a secure and highly available connection between an AWS VPC and your remote network.", + "Risk": "If one or both VPN tunnels are down, it can compromise the security and availability of the connection between your AWS VPC and your remote network. This could result in connectivity issues and potential data exposure or loss during the downtime, affecting business operations and overall network security.", + "RelatedUrl": "https://docs.aws.amazon.com/config/latest/developerguide/vpc-vpn-2-tunnels-up.html", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://docs.aws.amazon.com/securityhub/latest/userguide/ec2-controls.html#ec2-20", + "Terraform": "" + }, + "Recommendation": { + "Text": "To modify VPN tunnel options, see Modifying Site-to-Site VPN tunnel options in the AWS Site-to-Site VPN User Guide.", + "Url": "https://docs.aws.amazon.com/vpn/latest/s2svpn/modify-vpn-tunnel-options.html" + } + }, + "Categories": [ + "redundancy" + ], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.py b/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.py new file mode 100644 index 0000000000..ff78db6a55 --- /dev/null +++ b/prowler/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up.py @@ -0,0 +1,31 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.vpc.vpc_client import vpc_client + + +class vpc_vpn_connection_tunnels_up(Check): + def execute(self): + findings = [] + for vpn_arn, vpn_connection in vpc_client.vpn_connections.items(): + report = Check_Report_AWS(self.metadata()) + report.region = vpn_connection.region + report.resource_id = vpn_connection.id + report.resource_arn = vpn_arn + report.resource_tags = vpn_connection.tags + + if ( + vpn_connection.tunnels[0].status != "UP" + or vpn_connection.tunnels[1].status != "UP" + ): + report.status = "FAIL" + report.status_extended = ( + f"VPN Connection {vpn_connection.id} has at least one tunnel DOWN. " + ) + else: + report.status = "PASS" + report.status_extended = ( + f"VPN Connection {vpn_connection.id} has both tunnels UP. " + ) + + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/wafv2/wafv2_service.py b/prowler/providers/aws/services/wafv2/wafv2_service.py index efb53e668d..1f1ad3793e 100644 --- a/prowler/providers/aws/services/wafv2/wafv2_service.py +++ b/prowler/providers/aws/services/wafv2/wafv2_service.py @@ -1,3 +1,5 @@ +from typing import Optional + from botocore.exceptions import ClientError from pydantic import BaseModel @@ -6,7 +8,6 @@ from prowler.lib.scan_filters.scan_filters import is_resource_filtered from prowler.providers.aws.lib.service.service import AWSService -################### WAFv2 class WAFv2(AWSService): def __init__(self, provider): # Call AWSService's __init__ @@ -15,6 +16,7 @@ class WAFv2(AWSService): self.__threading_call__(self._list_web_acls) self.__threading_call__(self._list_resources_for_web_acl) self.__threading_call__(self._get_logging_configuration) + self.__threading_call__(self._list_tags, self.web_acls) def _list_web_acls(self, regional_client): logger.info("WAFv2 - Listing Regional Web ACLs...") @@ -84,6 +86,20 @@ class WAFv2(AWSService): f"{regional_client.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) + def _list_tags(self, resource: any): + logger.info("WAFv2 - Listing tags...") + try: + resource.tags = ( + self.regional_clients[resource.region] + .list_tags_for_resource(ResourceARN=resource.arn) + .get("TagInfoForResource", {}) + .get("TagList", []) + ) + except Exception as error: + logger.error( + f"{resource.region} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + class WebAclv2(BaseModel): arn: str @@ -93,3 +109,4 @@ class WebAclv2(BaseModel): user_pools: list[str] region: str logging_enabled: bool = False + tags: Optional[list] diff --git a/prowler/providers/aws/services/wafv2/wafv2_webacl_logging_enabled/wafv2_webacl_logging_enabled.py b/prowler/providers/aws/services/wafv2/wafv2_webacl_logging_enabled/wafv2_webacl_logging_enabled.py index df4220b871..5307e68752 100644 --- a/prowler/providers/aws/services/wafv2/wafv2_webacl_logging_enabled/wafv2_webacl_logging_enabled.py +++ b/prowler/providers/aws/services/wafv2/wafv2_webacl_logging_enabled/wafv2_webacl_logging_enabled.py @@ -10,6 +10,7 @@ class wafv2_webacl_logging_enabled(Check): report.region = web_acl.region report.resource_id = web_acl.id report.resource_arn = web_acl.arn + report.resource_tags = web_acl.tags if web_acl.logging_enabled: report.status = "PASS" diff --git a/prowler/providers/aws/services/workspaces/workspaces_volume_encryption_enabled/workspaces_volume_encryption_enabled.metadata.json b/prowler/providers/aws/services/workspaces/workspaces_volume_encryption_enabled/workspaces_volume_encryption_enabled.metadata.json index 4b152769d3..fd251b5431 100644 --- a/prowler/providers/aws/services/workspaces/workspaces_volume_encryption_enabled/workspaces_volume_encryption_enabled.metadata.json +++ b/prowler/providers/aws/services/workspaces/workspaces_volume_encryption_enabled/workspaces_volume_encryption_enabled.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:workspaces:region:account-id:workspace", "Severity": "high", - "ResourceType": "AwsWorkspaces", + "ResourceType": "AwsWorkSpacesWorkspace", "Description": "Ensure that your Amazon WorkSpaces storage volumes are encrypted in order to meet security and compliance requirements", "Risk": "If the value listed in the Volume Encryption column is Disabled the selected AWS WorkSpaces instance volumes (root and user volumes) are not encrypted. Therefore your data-at-rest is not protected from unauthorized access and does not meet the compliance requirements regarding data encryption.", "RelatedUrl": "https://docs.aws.amazon.com/workspaces/latest/adminguide/encrypt-workspaces.html", diff --git a/prowler/providers/aws/services/workspaces/workspaces_vpc_2private_1public_subnets_nat/workspaces_vpc_2private_1public_subnets_nat.metadata.json b/prowler/providers/aws/services/workspaces/workspaces_vpc_2private_1public_subnets_nat/workspaces_vpc_2private_1public_subnets_nat.metadata.json index f46a6f9943..f6568e5ee2 100644 --- a/prowler/providers/aws/services/workspaces/workspaces_vpc_2private_1public_subnets_nat/workspaces_vpc_2private_1public_subnets_nat.metadata.json +++ b/prowler/providers/aws/services/workspaces/workspaces_vpc_2private_1public_subnets_nat/workspaces_vpc_2private_1public_subnets_nat.metadata.json @@ -7,7 +7,7 @@ "SubServiceName": "", "ResourceIdTemplate": "arn:aws:workspaces:region:account-id:workspace", "Severity": "medium", - "ResourceType": "AwsWorkspaces", + "ResourceType": "AwsWorkSpacesWorkspace", "Description": "Ensure that the Workspaces VPC are deployed following the best practices using 1 public subnet and 2 private subnets with a NAT Gateway attached", "Risk": "Proper network segmentation is a key security best practice. Workspaces VPC should be deployed using 1 public subnet and 2 private subnets with a NAT Gateway attached", "RelatedUrl": "https://docs.aws.amazon.com/workspaces/latest/adminguide/amazon-workspaces-vpc.html", diff --git a/prowler/providers/azure/azure_provider.py b/prowler/providers/azure/azure_provider.py index 9515c1351c..96a0d8e575 100644 --- a/prowler/providers/azure/azure_provider.py +++ b/prowler/providers/azure/azure_provider.py @@ -29,11 +29,7 @@ from prowler.providers.azure.exceptions.exceptions import ( from prowler.providers.azure.lib.arguments.arguments import validate_azure_region from prowler.providers.azure.lib.mutelist.mutelist import AzureMutelist from prowler.providers.azure.lib.regions.regions import get_regions_config -from prowler.providers.azure.models import ( - AzureIdentityInfo, - AzureOutputOptions, - AzureRegionConfig, -) +from prowler.providers.azure.models import AzureIdentityInfo, AzureRegionConfig from prowler.providers.common.models import Audit_Metadata, Connection from prowler.providers.common.provider import Provider @@ -53,7 +49,6 @@ class AzureProvider(Provider): _audit_config (dict): The audit configuration for the Azure provider. _region_config (AzureRegionConfig): The region configuration for the Azure provider. _locations (dict): A dictionary containing the available locations for the Azure provider. - _output_options (AzureOutputOptions): The output options for the Azure provider. _mutelist (AzureMutelist): The mutelist object associated with the Azure provider. audit_metadata (Audit_Metadata): The audit metadata for the Azure provider. @@ -81,7 +76,6 @@ class AzureProvider(Provider): _audit_config: dict _region_config: AzureRegionConfig _locations: dict - _output_options: AzureOutputOptions _mutelist: AzureMutelist # TODO: this is not optional, enforce for all providers audit_metadata: Audit_Metadata @@ -160,6 +154,8 @@ class AzureProvider(Provider): # Fixer Config self._fixer_config = fixer_config + Provider.set_global_provider(self) + @property def identity(self): """Returns the identity of the Azure provider.""" @@ -195,28 +191,6 @@ class AzureProvider(Provider): """Returns the fixer configuration.""" return self._fixer_config - @property - def output_options(self): - """Returns the output options for the Azure provider.""" - return self._output_options - - @output_options.setter - def output_options(self, options: tuple): - """Set output options for the Azure provider. - - Sets the output options for the Azure provider using the provided arguments and bulk checks metadata. - - Args: - options (tuple): A tuple containing the arguments and bulk checks metadata. - - Returns: - None - """ - arguments, bulk_checks_metadata = options - self._output_options = AzureOutputOptions( - arguments, bulk_checks_metadata, self._identity - ) - @property def mutelist(self) -> AzureMutelist: """Mutelist object associated with this Azure provider.""" diff --git a/prowler/providers/azure/services/containerregistry/__init__.py b/prowler/providers/azure/services/containerregistry/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/__init__.py b/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.metadata.json b/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.metadata.json new file mode 100644 index 0000000000..10cd2b18e6 --- /dev/null +++ b/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "containerregistry_admin_user_disabled", + "CheckTitle": "Ensure admin user is disabled for Azure Container Registry", + "CheckType": [], + "ServiceName": "containerregistry", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "high", + "ResourceType": "ContainerRegistry", + "Description": "Ensure that the admin user is disabled and Role-Based Access Control (RBAC) is used instead since it could grant unrestricted access to the registry", + "Risk": "If the admin user is enabled, it may lead to unauthorized access to the container registry and its resources, which could compromise the confidentiality, integrity, and availability of the images stored within.", + "RelatedUrl": "https://learn.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli#admin-account", + "Remediation": { + "Code": { + "CLI": "az acr update --name --resource-group --admin-enabled false", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Disable the admin user on Azure Container Registry through the Azure Portal: 1. Navigate to your Container Registry. 2. In the settings, select 'Access keys'. 3. Ensure the 'Admin user' checkbox is not ticked. For all actions relying on registry access, switch to using Role-Based Access Control.", + "Url": "https://learn.microsoft.com/en-us/azure/container-registry/container-registry-authentication?tabs=azure-cli#admin-account" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "The transition away from using the admin user to RBAC will facilitate a more secure and manageable access model, minimizing the potential risk of unauthorized access to your container images." +} diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.py b/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.py new file mode 100644 index 0000000000..d32b6d2d16 --- /dev/null +++ b/prowler/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled.py @@ -0,0 +1,27 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.containerregistry.containerregistry_client import ( + containerregistry_client, +) + + +class containerregistry_admin_user_disabled(Check): + def execute(self) -> list[Check_Report_Azure]: + findings = [] + + for subscription, registries in containerregistry_client.registries.items(): + for registry_id, container_registry_info in registries.items(): + report = Check_Report_Azure(self.metadata()) + report.subscription = subscription + report.resource_name = container_registry_info.name + report.resource_id = registry_id + report.location = container_registry_info.location + report.status = "FAIL" + report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} has its admin user enabled." + + if not container_registry_info.admin_user_enabled: + report.status = "PASS" + report.status_extended = f"Container Registry {container_registry_info.name} from subscription {subscription} has its admin user disabled." + + findings.append(report) + + return findings diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_client.py b/prowler/providers/azure/services/containerregistry/containerregistry_client.py new file mode 100644 index 0000000000..1a704f1142 --- /dev/null +++ b/prowler/providers/azure/services/containerregistry/containerregistry_client.py @@ -0,0 +1,6 @@ +from prowler.providers.azure.services.containerregistry.containerregistry_service import ( + ContainerRegistry, +) +from prowler.providers.common.provider import Provider + +containerregistry_client = ContainerRegistry(Provider.get_global_provider()) diff --git a/prowler/providers/azure/services/containerregistry/containerregistry_service.py b/prowler/providers/azure/services/containerregistry/containerregistry_service.py new file mode 100644 index 0000000000..63b1b22a23 --- /dev/null +++ b/prowler/providers/azure/services/containerregistry/containerregistry_service.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass + +from azure.mgmt.containerregistry import ContainerRegistryManagementClient + +from prowler.lib.logger import logger +from prowler.providers.azure.azure_provider import AzureProvider +from prowler.providers.azure.lib.service.service import AzureService +from prowler.providers.azure.services.monitor.monitor_client import monitor_client +from prowler.providers.azure.services.monitor.monitor_service import DiagnosticSetting + + +class ContainerRegistry(AzureService): + def __init__(self, provider: AzureProvider): + super().__init__(ContainerRegistryManagementClient, provider) + self.registries = self._get_container_registries() + + def _get_container_registries(self): + logger.info("Container Registry - Getting registries...") + registries = {} + for subscription, client in self.clients.items(): + try: + registries_list = client.registries.list() + registries.update({subscription: {}}) + + for registry in registries_list: + resource_group = self._get_resource_group(registry.id) + registries[subscription].update( + { + registry.id: ContainerRegistryInfo( + id=getattr(registry, "id", ""), + name=getattr(registry, "name", ""), + location=getattr(registry, "location", ""), + resource_group=resource_group, + sku=getattr(registry.sku, "name", ""), + login_server=getattr(registry, "login_server", ""), + public_network_access=getattr( + registry, "public_network_access", "" + ), + admin_user_enabled=getattr( + registry, "admin_user_enabled", False + ), + monitor_diagnostic_settings=self._get_registry_monitor_settings( + registry.name, resource_group, subscription + ), + ) + } + ) + except Exception as error: + logger.error( + f"Subscription name: {subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return registries + + def _get_resource_group(self, registry_id: str) -> str: + """Extract resource group from the registry ID.""" + return registry_id.split("/")[4] + + def _get_registry_monitor_settings( + self, registry_name, resource_group, subscription + ): + logger.info( + f"Container Registry - Getting monitor diagnostics settings for {registry_name}..." + ) + monitor_diagnostics_settings = [] + try: + monitor_diagnostics_settings = monitor_client.diagnostic_settings_with_uri( + self.subscriptions[subscription], + f"subscriptions/{self.subscriptions[subscription]}/resourceGroups/{resource_group}/providers/Microsoft.ContainerRegistry/registries/{registry_name}", + monitor_client.clients[subscription], + ) + except Exception as error: + logger.error( + f"Subscription name: {self.subscription} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return monitor_diagnostics_settings + + +@dataclass +class ContainerRegistryInfo: + id: str + name: str + location: str + resource_group: str + sku: str + login_server: str + public_network_access: str + admin_user_enabled: bool + monitor_diagnostic_settings: list[DiagnosticSetting] = None diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index 4c5ee00ff0..bcea5bcfc4 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -2,6 +2,7 @@ import importlib import pkgutil import sys from abc import ABC, abstractmethod +from argparse import Namespace from importlib import import_module from typing import Any, Optional @@ -36,7 +37,6 @@ class Provider(ABC): identity (property): The identity of the provider for auditing. session (property): The session of the provider for auditing. audit_config (property): The audit configuration of the provider. - output_options (property): The output configuration of the provider for auditing. Methods: print_credentials(): Displays the provider's credentials used for auditing in the command-line interface. @@ -108,26 +108,6 @@ class Provider(ABC): """ raise NotImplementedError() - @property - @abstractmethod - def output_options(self) -> str: - """ - output_options method returns the provider's audit output configuration. - - This method needs to be created in each provider. - """ - raise NotImplementedError() - - @output_options.setter - @abstractmethod - def output_options(self, value: str) -> Any: - """ - output_options.setter sets the provider's audit output configuration. - - This method needs to be created in each provider. - """ - raise NotImplementedError() - @abstractmethod def get_output_mapping(self) -> dict: """ @@ -170,7 +150,11 @@ class Provider(ABC): return Provider._global @staticmethod - def set_global_provider(arguments): + def set_global_provider(global_provider: "Provider") -> None: + Provider._global = global_provider + + @staticmethod + def init_global_provider(arguments: Namespace) -> None: try: provider_class_path = ( f"{providers_path}.{arguments.provider}.{arguments.provider}_provider" @@ -188,7 +172,7 @@ class Provider(ABC): if not isinstance(Provider._global, provider_class): if "aws" in provider_class_name.lower(): - global_provider = provider_class( + provider_class( arguments.aws_retries_max_attempts, arguments.role, arguments.session_duration, @@ -205,7 +189,7 @@ class Provider(ABC): fixer_config, ) elif "azure" in provider_class_name.lower(): - global_provider = provider_class( + provider_class( arguments.az_cli_auth, arguments.sp_env_auth, arguments.browser_auth, @@ -217,7 +201,7 @@ class Provider(ABC): fixer_config, ) elif "gcp" in provider_class_name.lower(): - global_provider = provider_class( + provider_class( arguments.project_id, arguments.excluded_project_id, arguments.credentials_file, @@ -227,7 +211,7 @@ class Provider(ABC): fixer_config, ) elif "kubernetes" in provider_class_name.lower(): - global_provider = provider_class( + provider_class( arguments.kubeconfig_file, arguments.context, arguments.namespace, @@ -235,7 +219,6 @@ class Provider(ABC): fixer_config, ) - Provider._global = global_provider except TypeError as error: logger.critical( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" diff --git a/prowler/providers/gcp/gcp_provider.py b/prowler/providers/gcp/gcp_provider.py index a04980a096..9060c6c72d 100644 --- a/prowler/providers/gcp/gcp_provider.py +++ b/prowler/providers/gcp/gcp_provider.py @@ -23,12 +23,7 @@ from prowler.providers.gcp.exceptions.exceptions import ( GCPTestConnectionError, ) from prowler.providers.gcp.lib.mutelist.mutelist import GCPMutelist -from prowler.providers.gcp.models import ( - GCPIdentityInfo, - GCPOrganization, - GCPOutputOptions, - GCPProject, -) +from prowler.providers.gcp.models import GCPIdentityInfo, GCPOrganization, GCPProject class GcpProvider(Provider): @@ -38,7 +33,6 @@ class GcpProvider(Provider): _excluded_project_ids: list _identity: GCPIdentityInfo _audit_config: dict - _output_options: GCPOutputOptions _mutelist: GCPMutelist # TODO: this is not optional, enforce for all providers audit_metadata: Audit_Metadata @@ -68,7 +62,7 @@ class GcpProvider(Provider): logger.info("Instantiating GCP Provider ...") self._impersonated_service_account = impersonate_service_account - self._session = self.setup_session( + self._session, self._default_project_id = self.setup_session( credentials_file, self._impersonated_service_account ) @@ -136,6 +130,8 @@ class GcpProvider(Provider): self._audit_config = audit_config self._fixer_config = fixer_config + Provider.set_global_provider(self) + @property def identity(self): return self._identity @@ -152,6 +148,10 @@ class GcpProvider(Provider): def projects(self): return self._projects + @property + def default_project_id(self): + return self._default_project_id + @property def impersonated_service_account(self): return self._impersonated_service_account @@ -172,17 +172,6 @@ class GcpProvider(Provider): def fixer_config(self): return self._fixer_config - @property - def output_options(self): - return self._output_options - - @output_options.setter - def output_options(self, options: tuple): - arguments, bulk_checks_metadata = options - self._output_options = GCPOutputOptions( - arguments, bulk_checks_metadata, self._identity - ) - @property def mutelist(self) -> GCPMutelist: """ @@ -223,14 +212,14 @@ class GcpProvider(Provider): } @staticmethod - def setup_session(credentials_file: str, service_account: str) -> Credentials: + def setup_session(credentials_file: str, service_account: str) -> tuple: """ Setup the GCP session with the provided credentials file or service account to impersonate Args: credentials_file: str service_account: str Returns: - Credentials object + Credentials object and default project ID """ try: scopes = ["https://www.googleapis.com/auth/cloud-platform"] @@ -244,7 +233,7 @@ class GcpProvider(Provider): os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = client_secrets_path # Get default credentials - credentials, _ = default(scopes=scopes) + credentials, default_project_id = default(scopes=scopes) # Refresh the credentials to ensure they are valid credentials.refresh(Request()) @@ -260,7 +249,7 @@ class GcpProvider(Provider): ) logger.info(f"Impersonated credentials: {credentials}") - return credentials + return credentials, default_project_id except Exception as error: logger.critical( f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" @@ -285,7 +274,7 @@ class GcpProvider(Provider): Connection object with is_connected set to True if the connection is successful, or error set to the exception if the connection fails """ try: - session = GcpProvider.setup_session(credentials_file, service_account) + session, _ = GcpProvider.setup_session(credentials_file, service_account) service = discovery.build("cloudresourcemanager", "v1", credentials=session) request = service.projects().list() request.execute() diff --git a/prowler/providers/gcp/lib/service/service.py b/prowler/providers/gcp/lib/service/service.py index dac7411875..dbb7f22638 100644 --- a/prowler/providers/gcp/lib/service/service.py +++ b/prowler/providers/gcp/lib/service/service.py @@ -30,6 +30,7 @@ class GCPService: ) # Only project ids that have their API enabled will be scanned self.project_ids = self.__is_api_active__(provider.project_ids) + self.default_project_id = provider.default_project_id self.audit_config = provider.audit_config self.fixer_config = provider.fixer_config diff --git a/prowler/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured.py b/prowler/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured.py index b354996c08..e2f8442992 100644 --- a/prowler/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured.py +++ b/prowler/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured.py @@ -16,12 +16,12 @@ class apikeys_api_restrictions_configured(Check): if key.restrictions == {} or any( [ target.get("service") == "cloudapis.googleapis.com" - for target in key.restrictions["apiTargets"] + for target in key.restrictions.get("apiTargets", []) ] ): report.status = "FAIL" report.status_extended = ( - f"API key {key.name} doens't have restrictions configured." + f"API key {key.name} does not have restrictions configured." ) findings.append(report) diff --git a/prowler/providers/gcp/services/compute/compute_service.py b/prowler/providers/gcp/services/compute/compute_service.py index bef0d8179d..7691933e90 100644 --- a/prowler/providers/gcp/services/compute/compute_service.py +++ b/prowler/providers/gcp/services/compute/compute_service.py @@ -283,20 +283,23 @@ class Compute(GCPService): def _describe_backend_service(self): for balancer in self.load_balancers: - try: - response = ( - self.client.backendServices() - .get( - project=balancer.project_id, - backendService=balancer.service.split("/")[-1], + if balancer.service: + try: + response = ( + self.client.backendServices() + .get( + project=balancer.project_id, + backendService=balancer.service.split("/")[-1], + ) + .execute() + ) + balancer.logging = response.get("logConfig", {}).get( + "enable", False + ) + except Exception as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - .execute() - ) - balancer.logging = response.get("logConfig", {}).get("enable", False) - except Exception as error: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) class Instance(BaseModel): diff --git a/prowler/providers/gcp/services/dns/dns_service.py b/prowler/providers/gcp/services/dns/dns_service.py index 064ac7ad63..15c853448d 100644 --- a/prowler/providers/gcp/services/dns/dns_service.py +++ b/prowler/providers/gcp/services/dns/dns_service.py @@ -25,8 +25,9 @@ class DNS(GCPService): ManagedZone( name=managed_zone["name"], id=managed_zone["id"], - dnssec=managed_zone["dnssecConfig"]["state"] == "on", - key_specs=managed_zone["dnssecConfig"][ + dnssec=managed_zone.get("dnssecConfig", {})["state"] + == "on", + key_specs=managed_zone.get("dnssecConfig", {})[ "defaultKeySpecs" ], project_id=project_id, diff --git a/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.py b/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.py index 17cca80647..875ad13c22 100644 --- a/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.py +++ b/prowler/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured.py @@ -9,7 +9,7 @@ class iam_organization_essential_contacts_configured(Check): findings = [] for org in essentialcontacts_client.organizations: report = Check_Report_GCP(self.metadata()) - report.project_id = org.id + report.project_id = essentialcontacts_client.default_project_id report.resource_id = org.id report.resource_name = org.name report.location = essentialcontacts_client.region diff --git a/prowler/providers/gcp/services/iam/iam_service.py b/prowler/providers/gcp/services/iam/iam_service.py index 36dc5ea7d6..cac50fcb0b 100644 --- a/prowler/providers/gcp/services/iam/iam_service.py +++ b/prowler/providers/gcp/services/iam/iam_service.py @@ -29,12 +29,12 @@ class IAM(GCPService): while request is not None: response = request.execute() - for account in response["accounts"]: + for account in response.get("accounts", []): self.service_accounts.append( ServiceAccount( name=account["name"], email=account["email"], - display_name=account.get("displayName", ""), + display_name=account["displayName"], project_id=project_id, ) ) @@ -65,7 +65,7 @@ class IAM(GCPService): ) response = request.execute() - for key in response["keys"]: + for key in response.get("keys", []): sa.keys.append( Key( name=key["name"].split("/")[-1], @@ -149,7 +149,7 @@ class EssentialContacts(GCPService): .contacts() .list(parent="organizations/" + org.id) ).execute() - if len(response["contacts"]) > 0: + if len(response.get("contacts", [])) > 0: contacts = True self.organizations.append( diff --git a/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.py b/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.py index 822449b5c8..44128ce68f 100644 --- a/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.py +++ b/prowler/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled.py @@ -16,9 +16,14 @@ class kms_key_rotation_enabled(Check): now = datetime.datetime.now() condition_next_rotation_time = False if key.next_rotation_time: - next_rotation_time = datetime.datetime.strptime( - key.next_rotation_time, "%Y-%m-%dT%H:%M:%SZ" - ) + try: + next_rotation_time = datetime.datetime.strptime( + key.next_rotation_time, "%Y-%m-%dT%H:%M:%S.%fZ" + ) + except ValueError: + next_rotation_time = datetime.datetime.strptime( + key.next_rotation_time, "%Y-%m-%dT%H:%M:%SZ" + ) condition_next_rotation_time = ( abs((next_rotation_time - now).days) <= 90 ) diff --git a/prowler/providers/kubernetes/kubernetes_provider.py b/prowler/providers/kubernetes/kubernetes_provider.py index a229cdfa5c..8f7a377516 100644 --- a/prowler/providers/kubernetes/kubernetes_provider.py +++ b/prowler/providers/kubernetes/kubernetes_provider.py @@ -21,7 +21,6 @@ from prowler.providers.kubernetes.exceptions.exceptions import ( from prowler.providers.kubernetes.lib.mutelist.mutelist import KubernetesMutelist from prowler.providers.kubernetes.models import ( KubernetesIdentityInfo, - KubernetesOutputOptions, KubernetesSession, ) @@ -32,7 +31,6 @@ class KubernetesProvider(Provider): _namespaces: list _audit_config: dict _identity: KubernetesIdentityInfo - _output_options: KubernetesOutputOptions _mutelist: dict # TODO: this is not optional, enforce for all providers audit_metadata: Audit_Metadata @@ -80,6 +78,8 @@ class KubernetesProvider(Provider): # Fixer Config self._fixer_config = fixer_config + Provider.set_global_provider(self) + @property def type(self): return self._type @@ -104,17 +104,6 @@ class KubernetesProvider(Provider): def fixer_config(self): return self._fixer_config - @property - def output_options(self): - return self._output_options - - @output_options.setter - def output_options(self, options: tuple): - arguments, bulk_checks_metadata = options - self._output_options = KubernetesOutputOptions( - arguments, bulk_checks_metadata, self._identity - ) - @property def mutelist(self) -> KubernetesMutelist: """ diff --git a/pyproject.toml b/pyproject.toml index 19b3580212..c1917f01c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,16 +28,17 @@ version = "4.4.0" [tool.poetry.dependencies] alive-progress = "3.1.5" awsipranges = "0.3.3" -azure-identity = "1.17.1" +azure-identity = "1.18.0" azure-keyvault-keys = "4.9.0" azure-mgmt-applicationinsights = "4.0.0" azure-mgmt-authorization = "4.0.0" azure-mgmt-compute = "33.0.0" -azure-mgmt-containerservice = "31.0.0" -azure-mgmt-cosmosdb = "9.5.1" +azure-mgmt-containerregistry = "10.3.0" +azure-mgmt-containerservice = "32.0.0" +azure-mgmt-cosmosdb = "9.6.0" azure-mgmt-keyvault = "10.3.1" azure-mgmt-monitor = "6.0.2" -azure-mgmt-network = "26.0.0" +azure-mgmt-network = "27.0.0" azure-mgmt-rdbms = "10.1.0" azure-mgmt-resource = "23.1.1" azure-mgmt-security = "7.0.0" @@ -45,60 +46,60 @@ azure-mgmt-sql = "3.0.1" azure-mgmt-storage = "21.2.1" azure-mgmt-subscription = "3.1.1" azure-mgmt-web = "7.3.1" -azure-storage-blob = "12.22.0" -boto3 = "1.35.15" -botocore = "1.35.15" +azure-storage-blob = "12.23.1" +boto3 = "1.35.28" +botocore = "1.35.29" colorama = "0.4.6" cryptography = "43.0.1" -dash = "2.18.0" +dash = "2.18.1" dash-bootstrap-components = "1.6.0" detect-secrets = "1.5.0" -google-api-python-client = "2.144.0" +google-api-python-client = "2.147.0" google-auth-httplib2 = ">=0.1,<0.3" jsonschema = "4.23.0" -kubernetes = "30.1.0" +kubernetes = "31.0.0" microsoft-kiota-abstractions = "1.3.3" -msgraph-sdk = "1.6.0" +msgraph-sdk = "1.8.0" numpy = "2.0.2" -pandas = "2.2.2" +pandas = "2.2.3" py-ocsf-models = "0.1.1" pydantic = "1.10.15" python = ">=3.9,<3.13" python-dateutil = "^2.9.0.post0" -pytz = "2024.1" +pytz = "2024.2" schema = "0.7.7" shodan = "1.31.0" -slack-sdk = "3.32.0" +slack-sdk = "3.33.1" tabulate = "0.9.0" tzlocal = "5.2" [tool.poetry.group.dev.dependencies] -bandit = "1.7.9" +bandit = "1.7.10" black = "24.8.0" coverage = "7.6.1" docker = "7.1.0" flake8 = "7.1.1" freezegun = "1.5.1" mock = "5.1.0" -moto = {extras = ["all"], version = "5.0.14"} +moto = {extras = ["all"], version = "5.0.15"} openapi-schema-validator = "0.6.2" openapi-spec-validator = "0.7.1" -pylint = "3.2.7" +pylint = "3.3.1" pytest = "8.3.3" pytest-cov = "5.0.0" -pytest-env = "1.1.4" +pytest-env = "1.1.5" pytest-randomly = "3.15.0" pytest-xdist = "3.6.1" -safety = "3.2.7" -vulture = "2.11" +safety = "3.2.8" +vulture = "2.12" [tool.poetry.group.docs] optional = true [tool.poetry.group.docs.dependencies] -mkdocs = "1.5.3" -mkdocs-git-revision-date-localized-plugin = "1.2.8" -mkdocs-material = "9.5.18" +mkdocs = "1.6.1" +mkdocs-git-revision-date-localized-plugin = "1.2.9" +mkdocs-material = "9.5.38" mkdocs-material-extensions = "1.3.1" [tool.poetry.scripts] diff --git a/tests/config/config_test.py b/tests/config/config_test.py index 9b5850ff4d..1e1d235ac2 100644 --- a/tests/config/config_test.py +++ b/tests/config/config_test.py @@ -121,6 +121,7 @@ config_aws = { "ruby2.5", "ruby2.7", ], + "lambda_min_azs": 2, "organizations_enabled_regions": [], "organizations_trusted_delegated_administrators": [], "ecr_repository_vulnerability_minimum_severity": "MEDIUM", @@ -389,6 +390,8 @@ class Test_Config: "fedramp_low_revision_4_aws", "cis_2.0_gcp", "cis_1.8_kubernetes", + "kisa_isms-p_2023_aws", + "kisa_isms-p_2023-korean_aws", ] assert ( get_available_compliance_frameworks().sort() == compliance_frameworks.sort() diff --git a/tests/config/fixtures/config.yaml b/tests/config/fixtures/config.yaml index cc648e1a2a..46e499453a 100644 --- a/tests/config/fixtures/config.yaml +++ b/tests/config/fixtures/config.yaml @@ -58,6 +58,7 @@ aws: ] # AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries) + # AWS SSM Configuration (aws.ssm_documents_set_as_public) # Single account environment: No action required. The AWS account number will be automatically added by the checks. # Multi account environment: Any additional trusted account number should be added as a space separated list, e.g. # trusted_account_ids : ["123456789012", "098765432109", "678901234567"] @@ -101,6 +102,8 @@ aws: "ruby2.5", "ruby2.7", ] + # aws.awslambda_function_vpc_is_in_multi_azs + lambda_min_azs: 2 # AWS Organizations # aws.organizations_scp_check_deny_regions diff --git a/tests/config/fixtures/config_old.yaml b/tests/config/fixtures/config_old.yaml index e91da41e39..cbd3bf4fa0 100644 --- a/tests/config/fixtures/config_old.yaml +++ b/tests/config/fixtures/config_old.yaml @@ -19,6 +19,7 @@ ec2_allowed_instance_owners: ] # AWS VPC Configuration (vpc_endpoint_connections_trust_boundaries, vpc_endpoint_services_allowed_principals_trust_boundaries) +# AWS SSM Configuration (aws.ssm_documents_set_as_public) # Single account environment: No action required. The AWS account number will be automatically added by the checks. # Multi account environment: Any additional trusted account number should be added as a space separated list, e.g. # trusted_account_ids : ["123456789012", "098765432109", "678901234567"] diff --git a/tests/lib/check/check_test.py b/tests/lib/check/check_test.py index 16348cdca8..f2603fb708 100644 --- a/tests/lib/check/check_test.py +++ b/tests/lib/check/check_test.py @@ -1,14 +1,12 @@ import json import os import pathlib -import traceback from importlib.machinery import FileFinder -from logging import DEBUG, ERROR +from logging import ERROR from pkgutil import ModuleInfo from unittest import mock from boto3 import client -from colorama import Fore, Style from mock import Mock, patch from moto import mock_aws @@ -16,13 +14,13 @@ from prowler.lib.check.check import ( exclude_checks_to_run, exclude_services_to_run, execute, + execute_checks, list_categories, list_checks_json, list_services, parse_checks_from_file, parse_checks_from_folder, remove_custom_checks_module, - run_check, update_audit_metadata, ) from prowler.lib.check.models import load_check_metadata @@ -815,17 +813,24 @@ class TestCheck: region=AWS_REGION_US_EAST_1, ) ] - with mock.patch( + finding = Mock() + finding.status = "PASS" + findings = [finding] + check = Mock() + check.CheckID = "test-check" + check.execute = Mock(return_value=findings) + + with patch("prowler.lib.check.check.execute", return_value=findings), patch( "prowler.providers.aws.services.accessanalyzer.accessanalyzer_service.AccessAnalyzer", accessanalyzer_client, ): findings = execute( - service="accessanalyzer", - check_name="accessanalyzer_enabled", + check=check, global_provider=set_mocked_aws_provider( expected_checks=["accessanalyzer_enabled"] ), custom_checks_metadata=None, + output_options=None, ) assert len(findings) == 1 @@ -843,106 +848,25 @@ class TestCheck: ) ] status = ["PASS"] + check = mock.MagicMock() + check.CheckID = "accessanalyzer_enabled" + check.Status = "PASS" + output_options = mock.MagicMock() + output_options.status = status with mock.patch( "prowler.providers.aws.services.accessanalyzer.accessanalyzer_service.AccessAnalyzer", accessanalyzer_client, ): findings = execute( - service="accessanalyzer", - check_name="accessanalyzer_enabled", + check=check, global_provider=set_mocked_aws_provider( - status=status, expected_checks=["accessanalyzer_enabled"] + expected_checks=["accessanalyzer_enabled"] ), custom_checks_metadata=None, + output_options=output_options, ) assert len(findings) == 0 - def test_run_check(self, caplog): - caplog.set_level(DEBUG) - - findings = [] - check = Mock() - check.CheckID = "test-check" - check.execute = Mock(return_value=findings) - - with patch("prowler.lib.check.check.execute", return_value=findings): - assert run_check(check) == findings - assert caplog.record_tuples == [ - ( - "root", - DEBUG, - f"Executing check: {check.CheckID}", - ) - ] - - def test_run_check_verbose(self, capsys): - - findings = [] - check = Mock() - check.CheckID = "test-check" - check.ServiceName = "test-service" - check.Severity = "test-severity" - check.execute = Mock(return_value=findings) - - with patch("prowler.lib.check.check.execute", return_value=findings): - assert run_check(check, verbose=True) == findings - assert ( - capsys.readouterr().out - == f"\nCheck ID: {check.CheckID} - {Fore.MAGENTA}{check.ServiceName}{Fore.YELLOW} [{check.Severity}]{Style.RESET_ALL}\n" - ) - - def test_run_check_exception_only_logs(self, caplog): - caplog.set_level(ERROR) - - findings = [] - check = Mock() - check.CheckID = "test-check" - check.ServiceName = "test-service" - check.Severity = "test-severity" - error = Exception() - check.execute = Mock(side_effect=error) - - with patch("prowler.lib.check.check.execute", return_value=findings): - assert run_check(check, only_logs=True) == findings - assert caplog.record_tuples == [ - ( - "root", - ERROR, - f"{check.CheckID} -- {error.__class__.__name__}[{traceback.extract_tb(error.__traceback__)[-1].lineno}]: {error}", - ) - ] - - def test_run_check_exception(self, caplog, capsys): - caplog.set_level(ERROR) - - findings = [] - check = Mock() - check.CheckID = "test-check" - check.ServiceName = "test-service" - check.Severity = "test-severity" - error = Exception() - check.execute = Mock(side_effect=error) - - with patch("prowler.lib.check.check.execute", return_value=findings): - assert ( - run_check( - check, - verbose=False, - ) - == findings - ) - assert caplog.record_tuples == [ - ( - "root", - ERROR, - f"{check.CheckID} -- {error.__class__.__name__}[{traceback.extract_tb(error.__traceback__)[-1].lineno}]: {error}", - ) - ] - assert ( - capsys.readouterr().out - == f"Something went wrong in {check.CheckID}, please use --log-level ERROR\n" - ) - def test_aws_checks_metadata_is_valid(self): # Check if the checkID in the metadata.json of the checks is correct # Define the base directory for the checks @@ -1016,3 +940,33 @@ class TestCheck: assert ( check_id == check_dir ), f"CheckID in metadata does not match the check name in {check_directory}. Found CheckID: {check_id}" + + def test_execute_check_exception_only_logs(self, caplog): + caplog.set_level(ERROR) + + findings = [] + check = Mock() + checks = ["test-check"] + + provider = mock.MagicMock() + provider.type = "aws" + + output_options = mock.MagicMock() + output_options.only_logs = True + error = Exception() + check.execute = Mock(side_effect=error) + + with patch("prowler.lib.check.check.execute", return_value=findings): + assert ( + execute_checks( + checks, + provider, + custom_checks_metadata=None, + config_file=None, + output_options=output_options, + ) + == findings + ) + assert caplog.record_tuples == [ + ("root", 40, f"Check '{checks[0]}' was not found for the AWS provider") + ] diff --git a/tests/lib/outputs/asff/asff_test.py b/tests/lib/outputs/asff/asff_test.py index a60f06e769..9f8ee6a97b 100644 --- a/tests/lib/outputs/asff/asff_test.py +++ b/tests/lib/outputs/asff/asff_test.py @@ -84,7 +84,7 @@ class TestASFF: Url=finding.remediation_recommendation_url, ) ), - Description=finding.description, + Description=finding.status_extended, ) asff = ASFF(findings=[finding]) @@ -150,7 +150,7 @@ class TestASFF: Url="https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html", ) ), - Description=finding.description, + Description=finding.status_extended, ) asff = ASFF(findings=[finding]) @@ -215,7 +215,7 @@ class TestASFF: Url="https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html", ) ), - Description=finding.description, + Description=finding.status_extended, ) asff = ASFF(findings=[finding]) @@ -284,7 +284,7 @@ class TestASFF: Url="https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html", ) ), - Description=finding.description, + Description=finding.status_extended, ) asff = ASFF(findings=[finding]) @@ -447,6 +447,8 @@ class TestASFF: "si-4-5", ], "FedRAMP-Low-Revision-4": ["ac-2", "au-2", "ca-7"], + "KISA-ISMS-P-2023": ["2.6.1"], + "KISA-ISMS-P-2023-korean": ["2.6.1"], }, ) @@ -491,7 +493,7 @@ class TestASFF: Url=finding.remediation_recommendation_url, ) ), - Description=finding.description, + Description=finding.status_extended, ) asff = ASFF(findings=[finding]) @@ -538,7 +540,7 @@ class TestASFF: "CreatedAt": timestamp, "Severity": {"Label": "HIGH"}, "Title": "test-check-id", - "Description": "check description", + "Description": "This is a test", "Resources": [ { "Type": "test-resource", diff --git a/tests/lib/outputs/compliance/fixtures.py b/tests/lib/outputs/compliance/fixtures.py index ddf0dc7e6b..7b1d069c1e 100644 --- a/tests/lib/outputs/compliance/fixtures.py +++ b/tests/lib/outputs/compliance/fixtures.py @@ -12,6 +12,7 @@ from prowler.lib.check.compliance_models import ( Mitre_Requirement_Attribute_AWS, Mitre_Requirement_Attribute_Azure, Mitre_Requirement_Attribute_GCP, + KISA_ISMSP_Requirement_Attribute, ) CIS_1_4_AWS_NAME = "cis_1.4_aws" @@ -632,3 +633,73 @@ NIST_800_53_REVISION_4_AWS = Compliance( ), ], ) +KISA_ISMSP_AWS_NAME = "kisa_isms-p_2023_aws" +KISA_ISMSP_AWS = Compliance( + Framework="KISA-ISMS-P", + Provider="AWS", + Version="2023", + Description="The ISMS-P certification, established by KISA Korea Internet & Security Agency", + Requirements=[ + Compliance_Requirement( + Id="2.5.3", + Name="User Authentication", + Description="User access to information systems", + Attributes=[ + KISA_ISMSP_Requirement_Attribute( + Domain="2. Protection Measure Requirements", + Subdomain="2.5. Authentication and Authorization Management", + Section="2.5.3 User Authentication", + AuditChecklist=[ + "Is access to information systems and personal information controlled through secure authentication?", + "Are login attempt limitations enforced?", + ], + RelatedRegulations=[ + "Personal Information Protection Act, Article 29", + "Standards for Ensuring the Safety of Personal Information, Article 5", + ], + AuditEvidence=[ + "Login screen for information systems", + "Login failure message screen", + ], + NonComplianceCases=[ + "Case 1: Insufficient authentication when accessing information systems externally.", + "Case 2: No limitation on login failure attempts.", + ], + ) + ], + Checks=[ + "cloudwatch_log_metric_filter_authentication_failures", + "cognito_user_pool_mfa_enabled", + ], + ), + Compliance_Requirement( + Id="2.5.4", + Name="User Authentication", + Description="User access to information systems", + Attributes=[ + KISA_ISMSP_Requirement_Attribute( + Domain="2. Protection Measure Requirements", + Subdomain="2.5. Authentication and Authorization Management", + Section="2.5.3 User Authentication", + AuditChecklist=[ + "Is access to information systems and personal information controlled through secure authentication?", + "Are login attempt limitations enforced?", + ], + RelatedRegulations=[ + "Personal Information Protection Act, Article 29", + "Standards for Ensuring the Safety of Personal Information, Article 5", + ], + AuditEvidence=[ + "Login screen for information systems", + "Login failure message screen", + ], + NonComplianceCases=[ + "Case 1: Insufficient authentication when accessing information systems externally.", + "Case 2: No limitation on login failure attempts.", + ], + ) + ], + Checks=[], + ), + ], +) diff --git a/tests/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws_test.py b/tests/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws_test.py new file mode 100644 index 0000000000..8e64325f2e --- /dev/null +++ b/tests/lib/outputs/compliance/kisa_ismsp/kisa_ismsp_aws_test.py @@ -0,0 +1,122 @@ +from datetime import datetime +from io import StringIO + +from freezegun import freeze_time +from mock import patch + +from prowler.lib.outputs.compliance.kisa_ismsp.kisa_ismsp_aws import AWSKISAISMSP +from prowler.lib.outputs.compliance.kisa_ismsp.models import AWSKISAISMSPModel +from tests.lib.outputs.compliance.fixtures import KISA_ISMSP_AWS +from tests.lib.outputs.fixtures.fixtures import generate_finding_output +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 + + +class TestAWSKISAISMSP: + def test_output_transform(self): + findings = [generate_finding_output(compliance={"KISA-ISMS-P-2023": ["2.5.3"]})] + + output = AWSKISAISMSP(findings, KISA_ISMSP_AWS) + output_data = output.data[0] + assert isinstance(output_data, AWSKISAISMSPModel) + assert output_data.Provider == "aws" + assert output_data.AccountId == AWS_ACCOUNT_NUMBER + assert output_data.Region == AWS_REGION_EU_WEST_1 + assert output_data.Description == KISA_ISMSP_AWS.Description + assert output_data.Requirements_Id == KISA_ISMSP_AWS.Requirements[0].Id + assert ( + output_data.Requirements_Description + == KISA_ISMSP_AWS.Requirements[0].Description + ) + assert ( + output_data.Requirements_Attributes_Domain + == KISA_ISMSP_AWS.Requirements[0].Attributes[0].Domain + ) + assert ( + output_data.Requirements_Attributes_Subdomain + == KISA_ISMSP_AWS.Requirements[0].Attributes[0].Subdomain + ) + assert ( + output_data.Requirements_Attributes_Section + == KISA_ISMSP_AWS.Requirements[0].Attributes[0].Section + ) + assert ( + output_data.Requirements_Attributes_AuditChecklist + == KISA_ISMSP_AWS.Requirements[0].Attributes[0].AuditChecklist + ) + assert ( + output_data.Requirements_Attributes_RelatedRegulations + == KISA_ISMSP_AWS.Requirements[0].Attributes[0].RelatedRegulations + ) + assert ( + output_data.Requirements_Attributes_AuditEvidence + == KISA_ISMSP_AWS.Requirements[0].Attributes[0].AuditEvidence + ) + assert ( + output_data.Requirements_Attributes_NonComplianceCases + == KISA_ISMSP_AWS.Requirements[0].Attributes[0].NonComplianceCases + ) + assert output_data.Status == "PASS" + assert output_data.StatusExtended == "" + assert output_data.ResourceId == "" + assert output_data.ResourceName == "" + assert output_data.CheckId == "test-check-id" + assert output_data.Muted is False + # Test manual check + output_data_manual = output.data[1] + assert output_data_manual.Provider == "aws" + assert output_data_manual.AccountId == "" + assert output_data_manual.Region == "" + assert output_data_manual.Requirements_Id == KISA_ISMSP_AWS.Requirements[1].Id + assert ( + output_data_manual.Requirements_Description + == KISA_ISMSP_AWS.Requirements[1].Description + ) + assert ( + output_data_manual.Requirements_Attributes_Domain + == KISA_ISMSP_AWS.Requirements[1].Attributes[0].Domain + ) + assert ( + output_data_manual.Requirements_Attributes_Subdomain + == KISA_ISMSP_AWS.Requirements[1].Attributes[0].Subdomain + ) + assert ( + output_data_manual.Requirements_Attributes_Section + == KISA_ISMSP_AWS.Requirements[1].Attributes[0].Section + ) + assert ( + output_data_manual.Requirements_Attributes_AuditChecklist + == KISA_ISMSP_AWS.Requirements[1].Attributes[0].AuditChecklist + ) + assert ( + output_data_manual.Requirements_Attributes_RelatedRegulations + == KISA_ISMSP_AWS.Requirements[1].Attributes[0].RelatedRegulations + ) + assert ( + output_data_manual.Requirements_Attributes_AuditEvidence + == KISA_ISMSP_AWS.Requirements[1].Attributes[0].AuditEvidence + ) + assert ( + output_data_manual.Requirements_Attributes_NonComplianceCases + == KISA_ISMSP_AWS.Requirements[1].Attributes[0].NonComplianceCases + ) + assert output_data_manual.Status == "MANUAL" + assert output_data_manual.StatusExtended == "Manual check" + assert output_data_manual.ResourceId == "manual_check" + assert output_data_manual.ResourceName == "Manual check" + assert output_data_manual.CheckId == "manual" + assert output_data_manual.Muted is False + + @freeze_time(datetime.now()) + def test_batch_write_data_to_file(self): + mock_file = StringIO() + findings = [generate_finding_output(compliance={"KISA-ISMS-P-2023": ["2.5.3"]})] + output = AWSKISAISMSP(findings, KISA_ISMSP_AWS) + output._file_descriptor = mock_file + + with patch.object(mock_file, "close", return_value=None): + output.batch_write_data_to_file() + + mock_file.seek(0) + content = mock_file.read() + expected_csv = f"PROVIDER;DESCRIPTION;ACCOUNTID;REGION;ASSESSMENTDATE;REQUIREMENTS_ID;REQUIREMENTS_NAME;REQUIREMENTS_DESCRIPTION;REQUIREMENTS_ATTRIBUTES_DOMAIN;REQUIREMENTS_ATTRIBUTES_SUBDOMAIN;REQUIREMENTS_ATTRIBUTES_SECTION;REQUIREMENTS_ATTRIBUTES_AUDITCHECKLIST;REQUIREMENTS_ATTRIBUTES_RELATEDREGULATIONS;REQUIREMENTS_ATTRIBUTES_AUDITEVIDENCE;REQUIREMENTS_ATTRIBUTES_NONCOMPLIANCECASES;STATUS;STATUSEXTENDED;RESOURCEID;RESOURCENAME;CHECKID;MUTED\r\naws;The ISMS-P certification, established by KISA Korea Internet & Security Agency;123456789012;eu-west-1;{datetime.now()};2.5.3;User Authentication;User access to information systems;2. Protection Measure Requirements;2.5. Authentication and Authorization Management;2.5.3 User Authentication;['Is access to information systems and personal information controlled through secure authentication?', 'Are login attempt limitations enforced?'];['Personal Information Protection Act, Article 29', 'Standards for Ensuring the Safety of Personal Information, Article 5'];['Login screen for information systems', 'Login failure message screen'];['Case 1: Insufficient authentication when accessing information systems externally.', 'Case 2: No limitation on login failure attempts.'];PASS;;;;test-check-id;False\r\naws;The ISMS-P certification, established by KISA Korea Internet & Security Agency;;;{datetime.now()};2.5.4;User Authentication;User access to information systems;2. Protection Measure Requirements;2.5. Authentication and Authorization Management;2.5.3 User Authentication;['Is access to information systems and personal information controlled through secure authentication?', 'Are login attempt limitations enforced?'];['Personal Information Protection Act, Article 29', 'Standards for Ensuring the Safety of Personal Information, Article 5'];['Login screen for information systems', 'Login failure message screen'];['Case 1: Insufficient authentication when accessing information systems externally.', 'Case 2: No limitation on login failure attempts.'];MANUAL;Manual check;manual_check;Manual check;manual;False\r\n" + assert content == expected_csv diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index ecd0a368d7..40041b405e 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -215,10 +215,8 @@ class TestFinding: check_output.region = "us-west-1" output_options = MagicMock() output_options.unix_timestamp = 1234567890 - global_provider = MagicMock() - global_provider.output_options = output_options # Call the method under test - finding_output = Finding.generate_output(provider, check_output) + finding_output = Finding.generate_output(provider, check_output, output_options) # Assertions to verify expected behavior assert finding_output is not None @@ -293,10 +291,8 @@ class TestFinding: check_output.region = "us-west-1" output_options = MagicMock() output_options.unix_timestamp = 1234567890 - global_provider = MagicMock() - global_provider.output_options = output_options # Call the method under test - finding_output = Finding.generate_output(provider, check_output) + finding_output = Finding.generate_output(provider, check_output, output_options) # Assertions to verify expected behavior assert finding_output is not None @@ -367,7 +363,7 @@ class TestFinding: output_options = MagicMock() output_options.unix_timestamp = 1234567890 - finding_output = Finding.generate_output(provider, check_output) + finding_output = Finding.generate_output(provider, check_output, output_options) assert finding_output is not None assert finding_output.auth_method == "Principal: mock_auth" @@ -438,7 +434,7 @@ class TestFinding: output_options = MagicMock() output_options.unix_timestamp = 1234567890 - finding_output = Finding.generate_output(provider, check_output) + finding_output = Finding.generate_output(provider, check_output, output_options) assert finding_output is not None assert finding_output.auth_method == "in-cluster" diff --git a/tests/lib/outputs/output_options/output_options_test.py b/tests/lib/outputs/output_options/output_options_test.py new file mode 100644 index 0000000000..9e0d409606 --- /dev/null +++ b/tests/lib/outputs/output_options/output_options_test.py @@ -0,0 +1,185 @@ +from argparse import Namespace +from datetime import datetime +from os import rmdir +from unittest import mock + +from freezegun import freeze_time + +from prowler.config.config import output_file_timestamp +from prowler.providers.aws.models import AWSOutputOptions +from prowler.providers.azure.models import AzureOutputOptions +from prowler.providers.gcp.models import GCPOutputOptions +from prowler.providers.kubernetes.models import KubernetesOutputOptions + + +class Test_Output_Options: + @freeze_time(datetime.today()) + def test_set_output_options_aws_no_output_filename(self): + arguments = Namespace() + arguments.status = ["FAIL"] + arguments.output_formats = ["csv"] + arguments.output_directory = "output_test_directory" + arguments.verbose = True + arguments.security_hub = True + arguments.shodan = None + arguments.only_logs = False + arguments.unix_timestamp = False + arguments.send_sh_only_fails = True + + identity = mock.MagicMock() + identity.account = "123456789012" + + output_options = AWSOutputOptions(arguments, {}, identity) + + assert output_options.status == ["FAIL"] + assert output_options.output_modes == ["csv", "json-asff"] + assert output_options.output_directory == "output_test_directory" + assert output_options.verbose + assert output_options.security_hub_enabled + assert not output_options.shodan_api_key + assert not output_options.only_logs + assert not output_options.unix_timestamp + assert output_options.send_sh_only_fails + assert ( + output_options.output_filename + == f"prowler-output-{identity.account}-{output_file_timestamp}" + ) + assert output_options.bulk_checks_metadata == {} + + rmdir(f"{arguments.output_directory}/compliance") + rmdir(arguments.output_directory) + + @freeze_time(datetime.today()) + def test_set_output_options_aws(self): + arguments = Namespace() + arguments.status = [] + arguments.output_formats = ["csv"] + arguments.output_directory = "output_test_directory" + arguments.verbose = True + arguments.output_filename = "output_test_filename" + arguments.security_hub = True + arguments.shodan = None + arguments.only_logs = False + arguments.unix_timestamp = False + arguments.send_sh_only_fails = True + + identity = mock.MagicMock() + identity.account = "123456789012" + + output_options = AWSOutputOptions(arguments, {}, identity) + + assert isinstance(output_options, AWSOutputOptions) + assert output_options.security_hub_enabled + assert output_options.send_sh_only_fails + assert output_options.status == [] + assert output_options.output_modes == ["csv", "json-asff"] + assert output_options.output_directory == arguments.output_directory + assert output_options.bulk_checks_metadata == {} + assert output_options.verbose + assert output_options.output_filename == arguments.output_filename + + rmdir(f"{arguments.output_directory}/compliance") + rmdir(arguments.output_directory) + + @freeze_time(datetime.today()) + def test_azure_provider_output_options_with_domain(self): + arguments = Namespace() + # Output Options + arguments.output_formats = ["csv"] + arguments.output_directory = "output_test_directory" + output_directory = arguments.output_directory + arguments.status = [] + arguments.verbose = True + arguments.only_logs = False + arguments.unix_timestamp = False + arguments.shodan = None + + identity = mock.MagicMock() + identity.tenant_domain = "test-domain" + + output_options = AzureOutputOptions( + arguments, + {}, + identity, + ) + + assert isinstance(output_options, AzureOutputOptions) + assert output_options.status == [] + assert output_options.output_modes == [ + "csv", + ] + assert output_options.output_directory == output_directory + assert output_options.bulk_checks_metadata == {} + assert output_options.verbose + assert ( + output_options.output_filename + == f"prowler-output-{identity.tenant_domain}-{output_file_timestamp}" + ) + + rmdir(f"{arguments.output_directory}/compliance") + rmdir(arguments.output_directory) + + @freeze_time(datetime.today()) + def test_gcp_output_options(self): + arguments = Namespace() + # Output options + arguments.status = [] + arguments.output_formats = ["csv"] + arguments.output_directory = "output_test_directory" + arguments.verbose = True + arguments.only_logs = False + arguments.unix_timestamp = False + arguments.shodan = None + + identity = mock.MagicMock() + identity.profile = "test-profile" + + output_optionss = GCPOutputOptions( + arguments, + {}, + identity, + ) + + assert isinstance(output_optionss, GCPOutputOptions) + assert output_optionss.status == [] + assert output_optionss.output_modes == [ + "csv", + ] + assert output_optionss.output_directory == arguments.output_directory + assert output_optionss.bulk_checks_metadata == {} + assert output_optionss.verbose + assert f"prowler-output-{identity.profile}" in output_optionss.output_filename + + rmdir(f"{arguments.output_directory}/compliance") + rmdir(arguments.output_directory) + + def test_set_output_options_kubernetes(self): + arguments = Namespace() + arguments.status = [] + arguments.output_formats = ["csv"] + arguments.output_directory = "output_test_directory" + arguments.verbose = True + arguments.output_filename = "output_test_filename" + arguments.only_logs = False + arguments.unix_timestamp = False + arguments.shodan = None + + identity = mock.MagicMock() + identity.context = "test-context" + + output_options = KubernetesOutputOptions( + arguments, + {}, + identity, + ) + + assert isinstance(output_options, KubernetesOutputOptions) + assert output_options.status == [] + assert output_options.output_modes == ["csv"] + assert output_options.output_directory == arguments.output_directory + assert output_options.bulk_checks_metadata == {} + assert output_options.verbose + assert output_options.output_filename == arguments.output_filename + + rmdir(f"{arguments.output_directory}/compliance") + rmdir(arguments.output_directory) diff --git a/tests/lib/outputs/outputs_test.py b/tests/lib/outputs/outputs_test.py index 4419a4d44c..624366af5e 100644 --- a/tests/lib/outputs/outputs_test.py +++ b/tests/lib/outputs/outputs_test.py @@ -49,7 +49,7 @@ class TestOutputs: assert unroll_list(list, ",") == "test, test1, test2" def test_parse_html_string(self): - string = "CISA: your-systems-3, your-data-1, your-data-2 | CIS-1.4: 2.1.1 | CIS-1.5: 2.1.1 | GDPR: article_32 | AWS-Foundational-Security-Best-Practices: s3 | HIPAA: 164_308_a_1_ii_b, 164_308_a_4_ii_a, 164_312_a_2_iv, 164_312_c_1, 164_312_c_2, 164_312_e_2_ii | GxP-21-CFR-Part-11: 11.10-c, 11.30 | GxP-EU-Annex-11: 7.1-data-storage-damage-protection | NIST-800-171-Revision-2: 3_3_8, 3_5_10, 3_13_11, 3_13_16 | NIST-800-53-Revision-4: sc_28 | NIST-800-53-Revision-5: au_9_3, cm_6_a, cm_9_b, cp_9_d, cp_9_8, pm_11_b, sc_8_3, sc_8_4, sc_13_a, sc_16_1, sc_28_1, si_19_4 | ENS-RD2022: mp.si.2.aws.s3.1 | NIST-CSF-1.1: ds_1 | RBI-Cyber-Security-Framework: annex_i_1_3 | FFIEC: d3-pc-am-b-12 | PCI-3.2.1: s3 | FedRamp-Moderate-Revision-4: sc-13, sc-28 | FedRAMP-Low-Revision-4: sc-13" + string = "CISA: your-systems-3, your-data-1, your-data-2 | CIS-1.4: 2.1.1 | CIS-1.5: 2.1.1 | GDPR: article_32 | AWS-Foundational-Security-Best-Practices: s3 | HIPAA: 164_308_a_1_ii_b, 164_308_a_4_ii_a, 164_312_a_2_iv, 164_312_c_1, 164_312_c_2, 164_312_e_2_ii | GxP-21-CFR-Part-11: 11.10-c, 11.30 | GxP-EU-Annex-11: 7.1-data-storage-damage-protection | NIST-800-171-Revision-2: 3_3_8, 3_5_10, 3_13_11, 3_13_16 | NIST-800-53-Revision-4: sc_28 | NIST-800-53-Revision-5: au_9_3, cm_6_a, cm_9_b, cp_9_d, cp_9_8, pm_11_b, sc_8_3, sc_8_4, sc_13_a, sc_16_1, sc_28_1, si_19_4 | ENS-RD2022: mp.si.2.aws.s3.1 | NIST-CSF-1.1: ds_1 | RBI-Cyber-Security-Framework: annex_i_1_3 | FFIEC: d3-pc-am-b-12 | PCI-3.2.1: s3 | FedRamp-Moderate-Revision-4: sc-13, sc-28 | FedRAMP-Low-Revision-4: sc-13 | KISA-ISMS-P-2023: 2.6.1 | KISA-ISMS-P-2023-korean: 2.6.1" assert ( parse_html_string(string) == """ @@ -88,6 +88,10 @@ class TestOutputs: •FedRamp-Moderate-Revision-4: sc-13, sc-28 •FedRAMP-Low-Revision-4: sc-13 + +•KISA-ISMS-P-2023: 2.6.1 + +•KISA-ISMS-P-2023-korean: 2.6.1 """ ) @@ -204,10 +208,12 @@ class TestOutputs: "PCI-3.2.1": ["s3"], "FedRamp-Moderate-Revision-4": ["sc-13", "sc-28"], "FedRAMP-Low-Revision-4": ["sc-13"], + "KISA-ISMS-P-2023": ["2.6.1"], + "KISA-ISMS-P-2023-korean": ["2.6.1"], } assert ( unroll_dict(test_compliance_dict, separator=": ") - == "CISA: your-systems-3, your-data-1, your-data-2 | CIS-1.4: 2.1.1 | CIS-1.5: 2.1.1 | GDPR: article_32 | AWS-Foundational-Security-Best-Practices: s3 | HIPAA: 164_308_a_1_ii_b, 164_308_a_4_ii_a, 164_312_a_2_iv, 164_312_c_1, 164_312_c_2, 164_312_e_2_ii | GxP-21-CFR-Part-11: 11.10-c, 11.30 | GxP-EU-Annex-11: 7.1-data-storage-damage-protection | NIST-800-171-Revision-2: 3_3_8, 3_5_10, 3_13_11, 3_13_16 | NIST-800-53-Revision-4: sc_28 | NIST-800-53-Revision-5: au_9_3, cm_6_a, cm_9_b, cp_9_d, cp_9_8, pm_11_b, sc_8_3, sc_8_4, sc_13_a, sc_16_1, sc_28_1, si_19_4 | ENS-RD2022: mp.si.2.aws.s3.1 | NIST-CSF-1.1: ds_1 | RBI-Cyber-Security-Framework: annex_i_1_3 | FFIEC: d3-pc-am-b-12 | PCI-3.2.1: s3 | FedRamp-Moderate-Revision-4: sc-13, sc-28 | FedRAMP-Low-Revision-4: sc-13" + == "CISA: your-systems-3, your-data-1, your-data-2 | CIS-1.4: 2.1.1 | CIS-1.5: 2.1.1 | GDPR: article_32 | AWS-Foundational-Security-Best-Practices: s3 | HIPAA: 164_308_a_1_ii_b, 164_308_a_4_ii_a, 164_312_a_2_iv, 164_312_c_1, 164_312_c_2, 164_312_e_2_ii | GxP-21-CFR-Part-11: 11.10-c, 11.30 | GxP-EU-Annex-11: 7.1-data-storage-damage-protection | NIST-800-171-Revision-2: 3_3_8, 3_5_10, 3_13_11, 3_13_16 | NIST-800-53-Revision-4: sc_28 | NIST-800-53-Revision-5: au_9_3, cm_6_a, cm_9_b, cp_9_d, cp_9_8, pm_11_b, sc_8_3, sc_8_4, sc_13_a, sc_16_1, sc_28_1, si_19_4 | ENS-RD2022: mp.si.2.aws.s3.1 | NIST-CSF-1.1: ds_1 | RBI-Cyber-Security-Framework: annex_i_1_3 | FFIEC: d3-pc-am-b-12 | PCI-3.2.1: s3 | FedRamp-Moderate-Revision-4: sc-13, sc-28 | FedRAMP-Low-Revision-4: sc-13 | KISA-ISMS-P-2023: 2.6.1 | KISA-ISMS-P-2023-korean: 2.6.1" ) def test_unroll_dict_to_list(self): @@ -299,6 +305,14 @@ class TestOutputs: assert stats["total_fail"] == 0 assert stats["total_muted_pass"] == 0 assert stats["total_muted_fail"] == 0 + assert stats["total_critical_severity_fail"] == 0 + assert stats["total_critical_severity_pass"] == 0 + assert stats["total_high_severity_fail"] == 0 + assert stats["total_high_severity_pass"] == 0 + assert stats["total_medium_severity_fail"] == 0 + assert stats["total_medium_severity_pass"] == 0 + assert stats["total_low_severity_fail"] == 0 + assert stats["total_low_severity_pass"] == 0 assert stats["resources_count"] == 0 assert stats["findings_count"] == 0 @@ -307,15 +321,31 @@ class TestOutputs: finding_1.status = "FAIL" finding_1.muted = True finding_1.resource_id = "test_resource_1" - findings = [finding_1] + finding_1.check_metadata.Severity = "medium" + finding_1.check_metadata.CheckID = "glue_etl_jobs_amazon_s3_encryption_enabled" + finding_2 = mock.MagicMock() + finding_2.status = "FAIL" + finding_2.muted = True + finding_2.resource_id = "test_resource_2" + finding_2.check_metadata.Severity = "low" + finding_2.check_metadata.CheckID = "lightsail_static_ip_unused" + findings = [finding_1, finding_2] stats = extract_findings_statistics(findings) assert stats["total_pass"] == 0 - assert stats["total_fail"] == 1 + assert stats["total_fail"] == 2 assert stats["total_muted_pass"] == 0 - assert stats["total_muted_fail"] == 1 - assert stats["resources_count"] == 1 - assert stats["findings_count"] == 1 + assert stats["total_muted_fail"] == 2 + assert stats["resources_count"] == 2 + assert stats["findings_count"] == 2 + assert stats["total_critical_severity_fail"] == 0 + assert stats["total_critical_severity_pass"] == 0 + assert stats["total_high_severity_fail"] == 0 + assert stats["total_high_severity_pass"] == 0 + assert stats["total_medium_severity_fail"] == 1 + assert stats["total_medium_severity_pass"] == 0 + assert stats["total_low_severity_fail"] == 1 + assert stats["total_low_severity_pass"] == 0 assert stats["all_fails_are_muted"] def test_extract_findings_statistics_all_fail_are_not_muted(self): @@ -323,10 +353,16 @@ class TestOutputs: finding_1.status = "FAIL" finding_1.muted = True finding_1.resource_id = "test_resource_1" + finding_1.check_metadata.Severity = "critical" + finding_1.check_metadata.CheckID = "rds_instance_certificate_expiration" finding_2 = mock.MagicMock() finding_2.status = "FAIL" finding_2.muted = False finding_2.resource_id = "test_resource_1" + finding_2.check_metadata.Severity = "critical" + finding_2.check_metadata.CheckID = ( + "autoscaling_find_secrets_ec2_launch_configuration" + ) findings = [finding_1, finding_2] stats = extract_findings_statistics(findings) @@ -335,6 +371,14 @@ class TestOutputs: assert stats["total_muted_pass"] == 0 assert stats["total_muted_fail"] == 1 assert stats["resources_count"] == 1 + assert stats["total_critical_severity_fail"] == 2 + assert stats["total_critical_severity_pass"] == 0 + assert stats["total_high_severity_fail"] == 0 + assert stats["total_high_severity_pass"] == 0 + assert stats["total_medium_severity_fail"] == 0 + assert stats["total_medium_severity_pass"] == 0 + assert stats["total_low_severity_fail"] == 0 + assert stats["total_low_severity_pass"] == 0 assert stats["findings_count"] == 2 assert not stats["all_fails_are_muted"] @@ -343,10 +387,17 @@ class TestOutputs: finding_1.status = "PASS" finding_1.muted = True finding_1.resource_id = "test_resource_1" + finding_1.check_metadata.Severity = "critical" + finding_1.check_metadata.CheckID = ( + "autoscaling_find_secrets_ec2_launch_configuration" + ) + finding_2 = mock.MagicMock() finding_2.status = "PASS" finding_2.muted = False finding_2.resource_id = "test_resource_1" + finding_2.check_metadata.Severity = "high" + finding_2.check_metadata.CheckID = "acm_certificates_expiration_check" findings = [finding_1, finding_2] stats = extract_findings_statistics(findings) @@ -355,12 +406,22 @@ class TestOutputs: assert stats["total_muted_pass"] == 1 assert stats["total_muted_fail"] == 0 assert stats["resources_count"] == 1 + assert stats["total_critical_severity_fail"] == 0 + assert stats["total_critical_severity_pass"] == 1 + assert stats["total_high_severity_fail"] == 0 + assert stats["total_high_severity_pass"] == 1 + assert stats["total_medium_severity_fail"] == 0 + assert stats["total_medium_severity_pass"] == 0 + assert stats["total_low_severity_fail"] == 0 + assert stats["total_low_severity_pass"] == 0 assert stats["findings_count"] == 2 def test_extract_findings_statistics_all_passes_are_muted(self): finding_1 = mock.MagicMock() finding_1.status = "PASS" finding_1.muted = True + finding_1.check_metadata.Severity = "critical" + finding_1.check_metadata.CheckID = "rds_instance_certificate_expiration" finding_1.resource_id = "test_resource_1" findings = [finding_1] @@ -370,6 +431,14 @@ class TestOutputs: assert stats["total_muted_pass"] == 1 assert stats["total_muted_fail"] == 0 assert stats["resources_count"] == 1 + assert stats["total_critical_severity_fail"] == 0 + assert stats["total_critical_severity_pass"] == 1 + assert stats["total_high_severity_fail"] == 0 + assert stats["total_high_severity_pass"] == 0 + assert stats["total_medium_severity_fail"] == 0 + assert stats["total_medium_severity_pass"] == 0 + assert stats["total_low_severity_fail"] == 0 + assert stats["total_low_severity_pass"] == 0 assert stats["findings_count"] == 1 def test_report_with_aws_provider_not_muted_pass(self): @@ -397,12 +466,11 @@ class TestOutputs: provider = MagicMock() provider.type = "aws" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_1, finding_2] @@ -440,12 +508,11 @@ class TestOutputs: provider = MagicMock() provider.type = "aws" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_1, finding_2] @@ -483,12 +550,11 @@ class TestOutputs: provider = MagicMock() provider.type = "aws" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_1, finding_2] @@ -526,12 +592,11 @@ class TestOutputs: provider = MagicMock() provider.type = "aws" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_1, finding_2] @@ -571,12 +636,11 @@ class TestOutputs: provider = MagicMock() provider.type = "azure" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -616,12 +680,11 @@ class TestOutputs: provider = MagicMock() provider.type = "azure" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -661,12 +724,11 @@ class TestOutputs: provider = MagicMock() provider.type = "azure" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -706,12 +768,11 @@ class TestOutputs: provider = MagicMock() provider.type = "azure" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -749,12 +810,11 @@ class TestOutputs: provider = MagicMock() provider.type = "gcp" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -792,12 +852,11 @@ class TestOutputs: provider = MagicMock() provider.type = "gcp" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -835,12 +894,11 @@ class TestOutputs: provider = MagicMock() provider.type = "gcp" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -878,12 +936,11 @@ class TestOutputs: provider = MagicMock() provider.type = "gcp" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -921,12 +978,11 @@ class TestOutputs: provider = MagicMock() provider.type = "kubernetes" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -964,12 +1020,11 @@ class TestOutputs: provider = MagicMock() provider.type = "kubernetes" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -1007,12 +1062,11 @@ class TestOutputs: provider = MagicMock() provider.type = "kubernetes" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -1050,12 +1104,11 @@ class TestOutputs: provider = MagicMock() provider.type = "kubernetes" - provider.output_options = output_options # Assertions with mock.patch("builtins.print") as mocked_print: # Call the report method - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions check_findings_sorted = [finding_2, finding_1] @@ -1079,10 +1132,9 @@ class TestOutputs: provider = MagicMock() provider.type = "azure" - provider.output_options = output_options with mock.patch("builtins.print") as mocked_print: - report(check_findings, provider) + report(check_findings, provider, output_options) # Assertions mocked_print.assert_any_call( diff --git a/tests/lib/outputs/slack/slack_test.py b/tests/lib/outputs/slack/slack_test.py index a0afc0c96b..083e8807e7 100644 --- a/tests/lib/outputs/slack/slack_test.py +++ b/tests/lib/outputs/slack/slack_test.py @@ -51,6 +51,14 @@ class TestSlackIntegration: stats = {} stats["total_pass"] = 12 stats["total_fail"] = 10 + stats["total_critical_severity_pass"] = 4 + stats["total_critical_severity_fail"] = 4 + stats["total_high_severity_fail"] = 1 + stats["total_high_severity_pass"] = 1 + stats["total_medium_severity_fail"] = 2 + stats["total_medium_severity_pass"] = 1 + stats["total_low_severity_fail"] = 3 + stats["total_low_severity_pass"] = 3 stats["resources_count"] = 20 stats["findings_count"] = 22 @@ -70,6 +78,14 @@ class TestSlackIntegration: stats = {} stats["total_pass"] = 12 stats["total_fail"] = 10 + stats["total_critical_severity_pass"] = 2 + stats["total_critical_severity_fail"] = 4 + stats["total_high_severity_fail"] = 1 + stats["total_high_severity_pass"] = 1 + stats["total_medium_severity_fail"] = 2 + stats["total_medium_severity_pass"] = 1 + stats["total_low_severity_fail"] = 2 + stats["total_low_severity_pass"] = 3 stats["resources_count"] = 20 stats["findings_count"] = 22 @@ -96,6 +112,19 @@ class TestSlackIntegration: "text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100 , 2)}%)\n", }, }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "*Severities:*\n" + "• *Critical:* 2 " + "• *High:* 1 " + "• *Medium:* 1 " + "• *Low:* 3" + ), + }, + }, { "type": "section", "text": { @@ -103,6 +132,19 @@ class TestSlackIntegration: "text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ", }, }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "*Severities:*\n" + "• *Critical:* 4 " + "• *High:* 1 " + "• *Medium:* 2 " + "• *Low:* 2" + ), + }, + }, { "type": "section", "text": { @@ -157,12 +199,20 @@ class TestSlackIntegration: ] def test_create_message_blocks_azure(self): - aws_provider = set_mocked_aws_provider() + aws_provider = set_mocked_azure_provider() slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider) args = "--slack" stats = {} stats["total_pass"] = 12 stats["total_fail"] = 10 + stats["total_critical_severity_pass"] = 2 + stats["total_critical_severity_fail"] = 4 + stats["total_high_severity_fail"] = 1 + stats["total_high_severity_pass"] = 1 + stats["total_medium_severity_fail"] = 2 + stats["total_medium_severity_pass"] = 1 + stats["total_low_severity_fail"] = 2 + stats["total_low_severity_pass"] = 3 stats["resources_count"] = 20 stats["findings_count"] = 22 @@ -191,6 +241,19 @@ class TestSlackIntegration: "text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100 , 2)}%)\n", }, }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "*Severities:*\n" + "• *Critical:* 2 " + "• *High:* 1 " + "• *Medium:* 1 " + "• *Low:* 3" + ), + }, + }, { "type": "section", "text": { @@ -198,6 +261,19 @@ class TestSlackIntegration: "text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ", }, }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "*Severities:*\n" + "• *Critical:* 4 " + "• *High:* 1 " + "• *Medium:* 2 " + "• *Low:* 2" + ), + }, + }, { "type": "section", "text": { @@ -252,12 +328,20 @@ class TestSlackIntegration: ] def test_create_message_blocks_gcp(self): - aws_provider = set_mocked_aws_provider() + aws_provider = set_mocked_gcp_provider() slack = Slack(SLACK_TOKEN, SLACK_CHANNEL, aws_provider) args = "--slack" stats = {} stats["total_pass"] = 12 stats["total_fail"] = 10 + stats["total_critical_severity_pass"] = 2 + stats["total_critical_severity_fail"] = 4 + stats["total_high_severity_fail"] = 1 + stats["total_high_severity_pass"] = 1 + stats["total_medium_severity_fail"] = 2 + stats["total_medium_severity_pass"] = 1 + stats["total_low_severity_fail"] = 2 + stats["total_low_severity_pass"] = 3 stats["resources_count"] = 20 stats["findings_count"] = 22 @@ -284,6 +368,19 @@ class TestSlackIntegration: "text": f"\n:white_check_mark: *{stats['total_pass']} Passed findings* ({round(stats['total_pass'] / stats['findings_count'] * 100 , 2)}%)\n", }, }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "*Severities:*\n" + "• *Critical:* 2 " + "• *High:* 1 " + "• *Medium:* 1 " + "• *Low:* 3" + ), + }, + }, { "type": "section", "text": { @@ -291,6 +388,19 @@ class TestSlackIntegration: "text": f"\n:x: *{stats['total_fail']} Failed findings* ({round(stats['total_fail'] / stats['findings_count'] * 100 , 2)}%)\n ", }, }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "*Severities:*\n" + "• *Critical:* 4 " + "• *High:* 1 " + "• *Medium:* 2 " + "• *Low:* 2" + ), + }, + }, { "type": "section", "text": { diff --git a/tests/lib/scan/scan_test.py b/tests/lib/scan/scan_test.py index 15d45ba6e0..2ccf92f57e 100644 --- a/tests/lib/scan/scan_test.py +++ b/tests/lib/scan/scan_test.py @@ -1,6 +1,7 @@ from unittest import mock import pytest +from mock import MagicMock, patch from prowler.lib.scan.scan import Scan, get_service_checks_to_execute from tests.lib.outputs.fixtures.fixtures import generate_finding_output @@ -64,7 +65,7 @@ def mock_generate_output(): with mock.patch( "prowler.lib.outputs.finding.Finding.generate_output", autospec=True ) as mock_gen_output: - mock_gen_output.side_effect = lambda provider, finding: finding + mock_gen_output.side_effect = lambda provider, finding, output_options: finding yield mock_gen_output @@ -203,42 +204,40 @@ class TestScan: assert scan.get_completed_services() == set() assert scan.get_completed_checks() == set() + @patch("importlib.import_module") def test_scan( - mock_global_provider, mock_execute, mock_logger, mock_generate_output + mock_import_module, + mock_global_provider, + mock_execute, + mock_logger, + mock_generate_output, ): - checks_to_execute = {"accessanalyzer_enabled", "ec2_instance_public"} + mock_check_class = MagicMock() + mock_check_instance = mock_check_class.return_value + mock_check_instance.Provider = "aws" + mock_check_instance.CheckID = "accessanalyzer_enabled" + mock_check_instance.CheckTitle = "Check if IAM Access Analyzer is enabled" + + mock_import_module.return_value = MagicMock( + accessanalyzer_enabled=mock_check_class + ) + + checks_to_execute = {"accessanalyzer_enabled"} custom_checks_metadata = {} + mock_global_provider.type = "aws" - # Create a Scan object scan = Scan(mock_global_provider, checks_to_execute) - - # Execute the scan results = list(scan.scan(custom_checks_metadata)) - # Verify that generate_output was called with the correct findings - assert mock_generate_output.call_count == 2 * len(mock_execute.side_effect()) - - # Verify that execute was called twice - assert mock_execute.call_count == 2 - - assert len(results) == 2 + assert mock_generate_output.call_count == 1 * len(mock_execute.side_effect()) + assert mock_execute.call_count == 1 + assert len(results) == 1 assert results[0][1] == mock_execute.side_effect() - assert results[1][1] == mock_execute.side_effect() - - # Check the audit progress for the last result - assert results[1][0] == 100.0 - - # Verify that the progress is 100.0 - assert scan.progress == 100.0 # 100% progress is 100 - assert scan._number_of_checks_completed == 2 - assert scan.service_checks_to_execute == {} + assert results[0][0] == 100.0 + assert scan.progress == 100.0 + assert scan._number_of_checks_completed == 1 assert scan.service_checks_completed == { - "ec2": {"ec2_instance_public"}, "accessanalyzer": {"accessanalyzer_enabled"}, } - - # Verify that the findings are correct - assert scan.findings == mock_execute.side_effect() + mock_execute.side_effect() - - # Verify that no error was logged + assert scan.findings == mock_execute.side_effect() mock_logger.error.assert_not_called() diff --git a/tests/providers/aws/aws_provider_test.py b/tests/providers/aws/aws_provider_test.py index 0d855a99e3..e4e5fe491a 100644 --- a/tests/providers/aws/aws_provider_test.py +++ b/tests/providers/aws/aws_provider_test.py @@ -2,17 +2,14 @@ import json import os import re import tempfile -from argparse import Namespace from datetime import datetime, timedelta from json import dumps -from os import rmdir from re import search from unittest import mock import botocore import botocore.exceptions from boto3 import client, resource, session -from freezegun import freeze_time from mock import patch from moto import mock_aws from pytest import raises @@ -42,7 +39,6 @@ from prowler.providers.aws.models import ( AWSCredentials, AWSMFAInfo, AWSOrganizationsInfo, - AWSOutputOptions, ) from prowler.providers.common.models import Connection from prowler.providers.common.provider import Provider @@ -785,7 +781,7 @@ aws: aws_provider = AwsProvider() response = aws_provider.generate_regional_clients("ec2") - assert len(response.keys()) == 29 + assert len(response.keys()) == 30 @mock_aws def test_generate_regional_clients_with_enabled_regions(self): @@ -1038,97 +1034,6 @@ aws: assert aws_provider.audit_resources == [AWS_ACCOUNT_ARN] - @mock_aws - @freeze_time(datetime.today()) - def test_set_provider_output_options_aws_no_output_filename(self): - arguments = Namespace() - arguments.status = ["FAIL"] - arguments.output_formats = ["csv"] - arguments.output_directory = "output_test_directory" - arguments.verbose = True - arguments.security_hub = True - arguments.shodan = "test-api-key" - arguments.only_logs = False - arguments.unix_timestamp = False - arguments.send_sh_only_fails = True - - aws_provider = AwsProvider() - # This is needed since the output_options requires to get the global provider to get the audit config - with patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ): - - aws_provider.output_options = arguments, {} - - assert isinstance(aws_provider.output_options, AWSOutputOptions) - assert aws_provider.output_options.security_hub_enabled - assert aws_provider.output_options.send_sh_only_fails - assert aws_provider.output_options.status == ["FAIL"] - assert aws_provider.output_options.output_modes == ["csv", "json-asff"] - assert ( - aws_provider.output_options.output_directory - == arguments.output_directory - ) - assert aws_provider.output_options.bulk_checks_metadata == {} - assert aws_provider.output_options.verbose - assert ( - f"prowler-output-{AWS_ACCOUNT_NUMBER}" - in aws_provider.output_options.output_filename - ) - # Flaky due to the millisecond part of the timestamp - # assert ( - # aws_provider.output_options.output_filename - # == f"prowler-output-{AWS_ACCOUNT_NUMBER}-{datetime.today().strftime('%Y%m%d%H%M%S')}" - # ) - - # Delete testing directory - rmdir(f"{arguments.output_directory}/compliance") - rmdir(arguments.output_directory) - - @mock_aws - @freeze_time(datetime.today()) - def test_set_provider_output_options_aws(self): - arguments = Namespace() - arguments.status = [] - arguments.output_formats = ["csv"] - arguments.output_directory = "output_test_directory" - arguments.verbose = True - arguments.output_filename = "output_test_filename" - arguments.security_hub = True - arguments.shodan = "test-api-key" - arguments.only_logs = False - arguments.unix_timestamp = False - arguments.send_sh_only_fails = True - - aws_provider = AwsProvider() - # This is needed since the output_options requires to get the global provider to get the audit config - with patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, - ): - - aws_provider.output_options = arguments, {} - - assert isinstance(aws_provider.output_options, AWSOutputOptions) - assert aws_provider.output_options.security_hub_enabled - assert aws_provider.output_options.send_sh_only_fails - assert aws_provider.output_options.status == [] - assert aws_provider.output_options.output_modes == ["csv", "json-asff"] - assert ( - aws_provider.output_options.output_directory - == arguments.output_directory - ) - assert aws_provider.output_options.bulk_checks_metadata == {} - assert aws_provider.output_options.verbose - assert ( - aws_provider.output_options.output_filename == arguments.output_filename - ) - - # Delete testing directory - rmdir(f"{arguments.output_directory}/compliance") - rmdir(arguments.output_directory) - @mock_aws def test_validate_credentials_commercial_partition_with_regions(self): # Create a mock IAM user diff --git a/tests/providers/aws/lib/policy_condition_parser/policy_condition_parser_test.py b/tests/providers/aws/lib/policy_condition_parser/policy_condition_parser_test.py deleted file mode 100644 index 0162064bd2..0000000000 --- a/tests/providers/aws/lib/policy_condition_parser/policy_condition_parser_test.py +++ /dev/null @@ -1,1439 +0,0 @@ -from prowler.providers.aws.lib.policy_condition_parser.policy_condition_parser import ( - is_condition_block_restrictive, - is_condition_block_restrictive_organization, -) - -TRUSTED_AWS_ACCOUNT_NUMBER = "123456789012" -NON_TRUSTED_AWS_ACCOUNT_NUMBER = "111222333444" - -TRUSTED_ORGANIZATION_ID = "o-123456789012" -NON_TRUSTED_ORGANIZATION_ID = "o-111222333444" - -ALL_ORGS = "*" - - -class Test_policy_condition_parser: - # Test lowercase context key name --> aws - def test_condition_parser_string_equals_aws_SourceAccount_list(self): - condition_statement = { - "StringEquals": {"aws:SourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_SourceAccount_list_not_valid(self): - condition_statement = { - "StringEquals": { - "aws:SourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_SourceAccount_str(self): - condition_statement = { - "StringEquals": {"aws:SourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_SourceAccount_str_not_valid(self): - condition_statement = { - "StringEquals": {"aws:SourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceAccount_list(self): - condition_statement = { - "StringLike": {"aws:SourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceAccount_list_not_valid(self): - condition_statement = { - "StringLike": { - "aws:SourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceAccount_str(self): - condition_statement = { - "StringLike": {"aws:SourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceAccount_str_not_valid(self): - condition_statement = { - "StringLike": {"aws:SourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_SourceOwner_str(self): - condition_statement = { - "StringEquals": {"aws:SourceOwner": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_SourceOwner_str_not_valid(self): - condition_statement = { - "StringEquals": {"aws:SourceOwner": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_SourceOwner_list(self): - condition_statement = { - "StringEquals": {"aws:SourceOwner": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_SourceOwner_list_not_valid(self): - condition_statement = { - "StringEquals": { - "aws:SourceOwner": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceOwner_list(self): - condition_statement = { - "StringLike": {"aws:SourceOwner": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceOwner_list_not_valid(self): - condition_statement = { - "StringLike": { - "aws:SourceOwner": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceOwner_str(self): - condition_statement = { - "StringLike": {"aws:SourceOwner": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceOwner_str_not_valid(self): - condition_statement = { - "StringLike": {"aws:SourceOwner": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_s3_ResourceAccount_list(self): - condition_statement = { - "StringEquals": {"s3:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_s3_ResourceAccount_list_not_valid(self): - condition_statement = { - "StringEquals": { - "s3:ResourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_s3_ResourceAccount_str(self): - condition_statement = { - "StringEquals": {"s3:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_s3_ResourceAccount_str_not_valid(self): - condition_statement = { - "StringEquals": {"s3:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_PrincipalAccount_list(self): - condition_statement = { - "StringEquals": {"aws:PrincipalAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_PrincipalAccount_list_not_valid(self): - condition_statement = { - "StringEquals": { - "aws:PrincipalAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_PrincipalAccount_str(self): - condition_statement = { - "StringEquals": {"aws:PrincipalAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_PrincipalAccount_str_not_valid(self): - condition_statement = { - "StringEquals": {"aws:PrincipalAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_SourceArn_str(self): - condition_statement = { - "StringEquals": { - "aws:SourceArn": f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - } - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_SourceArn_str_not_valid(self): - condition_statement = { - "StringEquals": { - "aws:SourceArn": f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_PrincipalAccount_list(self): - condition_statement = { - "StringLike": {"aws:PrincipalAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_PrincipalAccount_list_not_valid(self): - condition_statement = { - "StringLike": { - "aws:PrincipalAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_PrincipalAccount_str(self): - condition_statement = { - "StringLike": {"aws:PrincipalAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_PrincipalAccount_str_not_valid(self): - condition_statement = { - "StringLike": {"aws:PrincipalAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_aws_SourceArn_list(self): - condition_statement = { - "ArnLike": { - "aws:SourceArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_aws_SourceArn_list_not_valid(self): - condition_statement = { - "ArnLike": { - "aws:SourceArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_aws_SourceArn_str(self): - condition_statement = { - "ArnLike": { - "aws:SourceArn": f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_aws_SourceArn_str_not_valid(self): - condition_statement = { - "ArnLike": { - "aws:SourceArn": f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_aws_PrincipalArn_list(self): - condition_statement = { - "ArnLike": { - "aws:PrincipalArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_aws_PrincipalArn_list_not_valid(self): - condition_statement = { - "ArnLike": { - "aws:PrincipalArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_aws_PrincipalArn_str(self): - condition_statement = { - "ArnLike": { - "aws:PrincipalArn": f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_aws_PrincipalArn_str_not_valid(self): - condition_statement = { - "ArnLike": { - "aws:PrincipalArn": f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_aws_SourceArn_list(self): - condition_statement = { - "ArnEquals": { - "aws:SourceArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_aws_SourceArn_list_not_valid(self): - condition_statement = { - "ArnEquals": { - "aws:SourceArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_aws_SourceArn_str(self): - condition_statement = { - "ArnEquals": { - "aws:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_aws_SourceArn_str_not_valid(self): - condition_statement = { - "ArnEquals": { - "aws:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_aws_PrincipalArn_list(self): - condition_statement = { - "ArnEquals": { - "aws:PrincipalArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_aws_PrincipalArn_list_not_valid(self): - condition_statement = { - "ArnEquals": { - "aws:PrincipalArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_aws_PrincipalArn_str(self): - condition_statement = { - "ArnEquals": { - "aws:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_aws_PrincipalArn_str_not_valid(self): - condition_statement = { - "ArnEquals": { - "aws:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceArn_list(self): - condition_statement = { - "StringLike": { - "aws:SourceArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceArn_list_not_valid(self): - condition_statement = { - "StringLike": { - "aws:SourceArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceArn_str(self): - condition_statement = { - "StringLike": { - "aws:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_SourceArn_str_not_valid(self): - condition_statement = { - "StringLike": { - "aws:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_PrincipalArn_list(self): - condition_statement = { - "StringLike": { - "aws:PrincipalArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_PrincipalArn_list_not_valid(self): - condition_statement = { - "StringLike": { - "aws:PrincipalArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_PrincipalArn_str(self): - condition_statement = { - "StringLike": { - "aws:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_PrincipalArn_str_not_valid(self): - condition_statement = { - "StringLike": { - "aws:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_ResourceAccount_list(self): - condition_statement = { - "StringEquals": {"aws:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_ResourceAccount_list_not_valid(self): - condition_statement = { - "StringEquals": { - "aws:ResourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_ResourceAccount_str(self): - condition_statement = { - "StringEquals": {"aws:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_aws_ResourceAccount_str_not_valid(self): - condition_statement = { - "StringEquals": {"aws:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_ResourceAccount_list(self): - condition_statement = { - "StringLike": {"aws:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_ResourceAccount_list_not_valid(self): - condition_statement = { - "StringLike": { - "aws:ResourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_ResourceAccount_str(self): - condition_statement = { - "StringLike": {"aws:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_aws_ResourceAccount_str_not_valid(self): - condition_statement = { - "StringLike": {"aws:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - # Test uppercase context key name --> AWS - def test_condition_parser_string_equals_AWS_SourceAccount_list(self): - condition_statement = { - "StringEquals": {"AWS:SourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_SourceAccount_list_not_valid(self): - condition_statement = { - "StringEquals": { - "AWS:SourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_SourceAccount_str(self): - condition_statement = { - "StringEquals": {"AWS:SourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_SourceAccount_str_not_valid(self): - condition_statement = { - "StringEquals": {"AWS:SourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceAccount_list(self): - condition_statement = { - "StringLike": {"AWS:SourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceAccount_list_not_valid(self): - condition_statement = { - "StringLike": { - "AWS:SourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceAccount_str(self): - condition_statement = { - "StringLike": {"AWS:SourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceAccount_str_not_valid(self): - condition_statement = { - "StringLike": {"AWS:SourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_SourceOwner_str(self): - condition_statement = { - "StringEquals": {"AWS:SourceOwner": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_SourceOwner_str_not_valid(self): - condition_statement = { - "StringEquals": {"AWS:SourceOwner": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_SourceOwner_list(self): - condition_statement = { - "StringEquals": {"AWS:SourceOwner": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_SourceOwner_list_not_valid(self): - condition_statement = { - "StringEquals": { - "AWS:SourceOwner": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceOwner_list(self): - condition_statement = { - "StringLike": {"AWS:SourceOwner": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceOwner_list_not_valid(self): - condition_statement = { - "StringLike": { - "AWS:SourceOwner": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceOwner_str(self): - condition_statement = { - "StringLike": {"AWS:SourceOwner": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceOwner_str_not_valid(self): - condition_statement = { - "StringLike": {"AWS:SourceOwner": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_S3_ResourceAccount_list(self): - condition_statement = { - "StringEquals": {"S3:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_S3_ResourceAccount_list_not_valid(self): - condition_statement = { - "StringEquals": { - "S3:ResourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_S3_ResourceAccount_str(self): - condition_statement = { - "StringEquals": {"S3:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_S3_ResourceAccount_str_not_valid(self): - condition_statement = { - "StringEquals": {"S3:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_PrincipalAccount_list(self): - condition_statement = { - "StringEquals": {"AWS:PrincipalAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_PrincipalAccount_list_not_valid(self): - condition_statement = { - "StringEquals": { - "AWS:PrincipalAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_PrincipalAccount_str(self): - condition_statement = { - "StringEquals": {"AWS:PrincipalAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_PrincipalAccount_str_not_valid(self): - condition_statement = { - "StringEquals": {"AWS:PrincipalAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_PrincipalAccount_list(self): - condition_statement = { - "StringLike": {"AWS:PrincipalAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_PrincipalAccount_list_not_valid(self): - condition_statement = { - "StringLike": { - "AWS:PrincipalAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_PrincipalAccount_str(self): - condition_statement = { - "StringLike": {"AWS:PrincipalAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_PrincipalAccount_str_not_valid(self): - condition_statement = { - "StringLike": {"AWS:PrincipalAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_AWS_SourceArn_list(self): - condition_statement = { - "ArnLike": { - "AWS:SourceArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_AWS_SourceArn_list_not_valid(self): - condition_statement = { - "ArnLike": { - "AWS:SourceArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_AWS_SourceArn_str(self): - condition_statement = { - "ArnLike": { - "AWS:SourceArn": f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_AWS_SourceArn_str_not_valid(self): - condition_statement = { - "ArnLike": { - "AWS:SourceArn": f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_AWS_PrincipalArn_list(self): - condition_statement = { - "ArnLike": { - "AWS:PrincipalArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_AWS_PrincipalArn_list_not_valid(self): - condition_statement = { - "ArnLike": { - "AWS:PrincipalArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_AWS_PrincipalArn_str(self): - condition_statement = { - "ArnLike": { - "AWS:PrincipalArn": f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_like_AWS_PrincipalArn_str_not_valid(self): - condition_statement = { - "ArnLike": { - "AWS:PrincipalArn": f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_AWS_SourceArn_list(self): - condition_statement = { - "ArnEquals": { - "AWS:SourceArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_AWS_SourceArn_list_not_valid(self): - condition_statement = { - "ArnEquals": { - "AWS:SourceArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_AWS_SourceArn_str(self): - condition_statement = { - "ArnEquals": { - "AWS:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_AWS_SourceArn_str_not_valid(self): - condition_statement = { - "ArnEquals": { - "AWS:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_AWS_PrincipalArn_list(self): - condition_statement = { - "ArnEquals": { - "AWS:PrincipalArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_AWS_PrincipalArn_list_not_valid(self): - condition_statement = { - "ArnEquals": { - "AWS:PrincipalArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_AWS_PrincipalArn_str(self): - condition_statement = { - "ArnEquals": { - "AWS:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_arn_equals_AWS_PrincipalArn_str_not_valid(self): - condition_statement = { - "ArnEquals": { - "AWS:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceArn_list(self): - condition_statement = { - "StringLike": { - "AWS:SourceArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceArn_list_not_valid(self): - condition_statement = { - "StringLike": { - "AWS:SourceArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceArn_str(self): - condition_statement = { - "StringLike": { - "AWS:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_SourceArn_str_not_valid(self): - condition_statement = { - "StringLike": { - "AWS:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_PrincipalArn_list(self): - condition_statement = { - "StringLike": { - "AWS:PrincipalArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - ] - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_PrincipalArn_list_not_valid(self): - condition_statement = { - "StringLike": { - "AWS:PrincipalArn": [ - f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", - ] - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_PrincipalArn_str(self): - condition_statement = { - "StringLike": { - "AWS:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_PrincipalArn_str_not_valid(self): - condition_statement = { - "StringLike": { - "AWS:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" - } - } - - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_ResourceAccount_list(self): - condition_statement = { - "StringEquals": {"AWS:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_ResourceAccount_list_not_valid(self): - condition_statement = { - "StringEquals": { - "AWS:ResourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_ResourceAccount_str(self): - condition_statement = { - "StringEquals": {"AWS:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_equals_AWS_ResourceAccount_str_not_valid(self): - condition_statement = { - "StringEquals": {"AWS:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_ResourceAccount_list(self): - condition_statement = { - "StringLike": {"AWS:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_ResourceAccount_list_not_valid(self): - condition_statement = { - "StringLike": { - "AWS:ResourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - } - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_ResourceAccount_str(self): - condition_statement = { - "StringLike": {"AWS:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_string_like_AWS_ResourceAccount_str_not_valid(self): - condition_statement = { - "StringLike": {"AWS:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_two_lists_unrestrictive(self): - condition_statement = { - "StringLike": { - "AWS:ResourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - }, - "ArnLike": { - "AWS:SourceArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - ] - }, - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_two_lists_both_restrictive(self): - condition_statement = { - "StringLike": { - "AWS:ResourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - ] - }, - "ArnLike": { - "AWS:SourceArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - ] - }, - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_two_lists_first_restrictive(self): - condition_statement = { - "StringLike": { - "AWS:ResourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - ] - }, - "ArnLike": { - "AWS:SourceArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - ] - }, - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_two_lists_second_restrictive(self): - condition_statement = { - "StringLike": { - "AWS:ResourceAccount": [ - TRUSTED_AWS_ACCOUNT_NUMBER, - NON_TRUSTED_AWS_ACCOUNT_NUMBER, - ] - }, - "ArnLike": { - "AWS:SourceArn": [ - f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", - ] - }, - } - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER - ) - - def test_condition_parser_allowing_cross_account_with_invalid_block(self): - condition_statement = { - "StringLike": { - "s3:prefix": [ - "home/", - ] - }, - } - assert not is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True - ) - - def test_condition_parser_string_equals_vpc(self): - condition_statement = {"StringEquals": {"aws:SourceVpc": "vpc-123456"}} - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True - ) - - def test_condition_parser_string_equals_vpc_list(self): - condition_statement = {"StringEquals": {"aws:sourcevpc": ["vpc-123456"]}} - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True - ) - - def test_condition_parser_string_equals_vpc_list_not_valid(self): - condition_statement = { - "StringEquals": {"aws:SourceVpc": ["vpc-123456", "vpc-654321"]} - } - - assert is_condition_block_restrictive( - condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True - ) - - def test_condition_parser_string_equals_aws_PrincipalOrgID_list(self): - condition_statement = { - "StringEquals": {"aws:PrincipalOrgID": [TRUSTED_ORGANIZATION_ID]} - } - assert is_condition_block_restrictive_organization(condition_statement) - - def test_condition_parser_string_equals_aws_PrincipalOrgID_list_multiple_items( - self, - ): - condition_statement = { - "StringEquals": { - "aws:PrincipalOrgID": [ - TRUSTED_ORGANIZATION_ID, - NON_TRUSTED_ORGANIZATION_ID, - ] - } - } - assert is_condition_block_restrictive_organization(condition_statement) - - def test_condition_parser_string_equals_aws_PrincipalOrgID_str(self): - condition_statement = { - "StringEquals": {"aws:PrincipalOrgID": TRUSTED_ORGANIZATION_ID} - } - assert is_condition_block_restrictive_organization(condition_statement) - - def test_condition_parser_string_equals_aws_All_Orgs_list_multiple_items( - self, - ): - condition_statement = { - "StringEquals": { - "aws:PrincipalOrgID": [ - TRUSTED_ORGANIZATION_ID, - ALL_ORGS, - ] - } - } - assert not is_condition_block_restrictive_organization(condition_statement) - - def test_condition_parser_string_equals_aws_All_Orgs_str(self): - condition_statement = {"StringEquals": {"aws:PrincipalOrgID": ALL_ORGS}} - assert not is_condition_block_restrictive_organization(condition_statement) diff --git a/tests/providers/aws/services/accessanalyzer/accessanalyzer_enabled/accessanalyzer_enabled_fixer_test.py b/tests/providers/aws/services/accessanalyzer/accessanalyzer_enabled/accessanalyzer_enabled_fixer_test.py index d22394a71c..ac600dc828 100644 --- a/tests/providers/aws/services/accessanalyzer/accessanalyzer_enabled/accessanalyzer_enabled_fixer_test.py +++ b/tests/providers/aws/services/accessanalyzer/accessanalyzer_enabled/accessanalyzer_enabled_fixer_test.py @@ -1,19 +1,29 @@ from unittest import mock -AWS_REGION = "eu-west-1" +from tests.providers.aws.utils import AWS_ACCOUNT_ARN, AWS_REGION_EU_WEST_1 class Test_accessanalyzer_enabled_fixer: def test_accessanalyzer_enabled_fixer(self): - accessanalyzer_client = mock.MagicMock + regional_client = mock.MagicMock() + accessanalyzer_client = mock.MagicMock() + + accessanalyzer_client.region = AWS_REGION_EU_WEST_1 accessanalyzer_client.analyzers = [] + accessanalyzer_client.audited_account_arn = AWS_ACCOUNT_ARN + regional_client.create_analyzer.return_value = None + accessanalyzer_client.regional_clients = {AWS_REGION_EU_WEST_1: regional_client} + with mock.patch( "prowler.providers.aws.services.accessanalyzer.accessanalyzer_service.AccessAnalyzer", new=accessanalyzer_client, + ) as accessanalyzer_client, mock.patch( + "prowler.providers.aws.services.accessanalyzer.accessanalyzer_client.accessanalyzer_client", + new=accessanalyzer_client, ): # Test Check from prowler.providers.aws.services.accessanalyzer.accessanalyzer_enabled.accessanalyzer_enabled_fixer import ( fixer, ) - assert fixer(AWS_REGION) + assert fixer(AWS_REGION_EU_WEST_1) diff --git a/tests/providers/aws/services/apigateway/apigateway_endpoint_public/apigateway_endpoint_public_test.py b/tests/providers/aws/services/apigateway/apigateway_endpoint_public/apigateway_endpoint_public_test.py index cfc6bb9ddc..2e09d41d8a 100644 --- a/tests/providers/aws/services/apigateway/apigateway_endpoint_public/apigateway_endpoint_public_test.py +++ b/tests/providers/aws/services/apigateway/apigateway_endpoint_public/apigateway_endpoint_public_test.py @@ -128,7 +128,7 @@ class Test_apigateway_restapi_public: assert len(result) == 1 assert ( result[0].status_extended - == f"API Gateway test-rest-api ID {rest_api['id']} is internet accesible." + == f"API Gateway test-rest-api ID {rest_api['id']} is internet accessible." ) assert result[0].resource_id == "test-rest-api" assert ( diff --git a/tests/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables_test.py b/tests/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables_test.py index a36879d263..432b31e927 100644 --- a/tests/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables_test.py +++ b/tests/providers/aws/services/awslambda/awslambda_function_no_secrets_in_variables/awslambda_function_no_secrets_in_variables_test.py @@ -12,6 +12,7 @@ class Test_awslambda_function_no_secrets_in_variables: def test_no_functions(self): lambda_client = mock.MagicMock lambda_client.functions = {} + lambda_client.audit_config = {"secrets_ignore_patterns": []} with mock.patch( "prowler.providers.common.provider.Provider.get_global_provider", @@ -35,6 +36,7 @@ class Test_awslambda_function_no_secrets_in_variables: function_name = "test-lambda" function_runtime = "nodejs4.3" function_arn = f"arn:aws:lambda:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}" + lambda_client.audit_config = {"secrets_ignore_patterns": []} lambda_client.functions = { "function_name": Function( @@ -167,6 +169,7 @@ class Test_awslambda_function_no_secrets_in_variables: function_name = "test-lambda" function_runtime = "nodejs4.3" function_arn = f"arn:aws:lambda:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}" + lambda_client.audit_config = {"secrets_ignore_patterns": []} lambda_client.functions = { "function_name": Function( diff --git a/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_test.py b/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_test.py index 86c240c8c9..4278b991d8 100644 --- a/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_test.py +++ b/tests/providers/aws/services/awslambda/awslambda_function_not_publicly_accessible/awslambda_function_not_publicly_accessible_test.py @@ -79,7 +79,6 @@ class Test_awslambda_function_not_publicly_accessible: StatementId="public-access", Action="lambda:InvokeFunction", Principal="*", - SourceArn=function_arn, ) aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) @@ -112,6 +111,83 @@ class Test_awslambda_function_not_publicly_accessible: ) assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}] + @mock_aws + def test_function_public_with_source_account(self): + # Create the mock IAM role + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + function_name = "test-lambda" + + # Create the lambda function using boto3 client + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="nodejs4.3", + Role=role_arn, + Handler="index.handler", + Code={"ZipFile": b"fileb://file-path/to/your-deployment-package.zip"}, + Description="Test Lambda function", + Timeout=3, + MemorySize=128, + Publish=True, + Tags={"tag1": "value1", "tag2": "value2"}, + )["FunctionArn"] + + # Attach the policy to the lambda function with a wildcard principal + lambda_client.add_permission( + FunctionName=function_name, + StatementId="non-public-access", + Action="lambda:InvokeFunction", + Principal="*", + SourceArn=function_arn, + SourceAccount=AWS_ACCOUNT_NUMBER, + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible.awslambda_client", + new=Lambda(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible import ( + awslambda_function_not_publicly_accessible, + ) + + check = awslambda_function_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Lambda function {function_name} has a policy resource-based policy not public." + ) + assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}] + @mock_aws def test_function_not_public(self): # Create the mock IAM role @@ -190,6 +266,7 @@ class Test_awslambda_function_not_publicly_accessible: def test_function_public_with_canonical(self): lambda_client = mock.MagicMock + lambda_client.audited_account = AWS_ACCOUNT_NUMBER function_name = "test-lambda" function_runtime = "nodejs4.3" function_arn = f"arn:aws:lambda:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}" @@ -418,59 +495,190 @@ class Test_awslambda_function_not_publicly_accessible: ) assert result[0].resource_tags == [{"tag1": "value1", "tag2": "value2"}] - # def test_function_could_be_invoked_by_specific_aws_account(self): - # lambda_client = mock.MagicMock - # function_name = "test-lambda" - # function_runtime = "nodejs4.3" - # function_arn = f"arn:aws:lambda:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}" - # lambda_policy = { # If there is an ALB or API Gateway in specified AWS Account, the lambda function could be invoked and exposed by them - # "Version": "2012-10-17", - # "Statement": [ - # { - # "Sid": "public-access", - # "Principal": {"AWS": AWS_ACCOUNT_NUMBER}, - # "Effect": "Allow", - # "Action": [ - # "lambda:InvokeFunction", - # ], - # "Resource": [function_arn], - # } - # ], - # } + def test_function_could_be_invoked_by_specific_aws_account(self): + lambda_client = mock.MagicMock + lambda_client.audited_account = AWS_ACCOUNT_NUMBER + function_name = "test-lambda" + function_runtime = "nodejs4.3" + function_arn = f"arn:aws:lambda:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}" + lambda_policy = { + "Version": "2012-10-17", + "Id": "default", + "Statement": [ + { + "Sid": "awslambda-myLambdaScript-LambdaInvokePermission", + "Effect": "Allow", + "Principal": {"Service": "ses.amazonaws.com"}, + "Action": "lambda:InvokeFunction", + "Resource": f"arn:aws:lambda:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:function:{function_name}", + "Condition": { + "StringEquals": {"AWS:SourceAccount": AWS_ACCOUNT_NUMBER} + }, + } + ], + } - # lambda_client.functions = { - # "function_name": Function( - # name=function_name, - # security_groups=[], - # arn=function_arn, - # region=AWS_REGION_EU_WEST_1, - # runtime=function_runtime, - # policy=lambda_policy, - # ) - # } + lambda_client.functions = { + "function_name": Function( + name=function_name, + security_groups=[], + arn=function_arn, + region=AWS_REGION_EU_WEST_1, + runtime=function_runtime, + policy=lambda_policy, + ) + } - # with mock.patch( - # "prowler.providers.common.provider.Provider.get_global_provider", - # return_value=set_mocked_aws_provider(), - # ), mock.patch( - # "prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible.awslambda_client", - # new=lambda_client, - # ): - # # Test Check - # from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible import ( - # awslambda_function_not_publicly_accessible, - # ) + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible.awslambda_client", + new=lambda_client, + ): + from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible import ( + awslambda_function_not_publicly_accessible, + ) - # check = awslambda_function_not_publicly_accessible() - # result = check.execute() + check = awslambda_function_not_publicly_accessible() + result = check.execute() - # assert len(result) == 1 - # assert result[0].region == AWS_REGION_EU_WEST_1 - # assert result[0].resource_id == function_name - # assert result[0].resource_arn == function_arn - # assert result[0].status == "FAIL" - # assert ( - # result[0].status_extended - # == f"Lambda function {function_name} has a policy resource-based policy with public access." - # ) - # assert result[0].resource_tags == [] + assert len(result) == 1 + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Lambda function {function_name} has a policy resource-based policy not public." + ) + assert result[0].resource_tags == [] + + def test_function_could_be_invoked_by_specific_other_aws_account(self): + lambda_client = mock.MagicMock + lambda_client.audited_account = AWS_ACCOUNT_NUMBER + function_name = "test-lambda" + function_runtime = "nodejs4.3" + function_arn = f"arn:aws:lambda:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}" + lambda_policy = { + "Version": "2012-10-17", + "Id": "default", + "Statement": [ + { + "Sid": "awslambda-myLambdaScript-LambdaInvokePermission", + "Effect": "Allow", + "Principal": {"Service": "ses.amazonaws.com"}, + "Action": "lambda:InvokeFunction", + "Resource": f"arn:aws:lambda:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:function:{function_name}", + "Condition": { + "StringEquals": {"AWS:SourceAccount": "000000000000"} + }, + } + ], + } + + lambda_client.functions = { + "function_name": Function( + name=function_name, + security_groups=[], + arn=function_arn, + region=AWS_REGION_EU_WEST_1, + runtime=function_runtime, + policy=lambda_policy, + ) + } + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible.awslambda_client", + new=lambda_client, + ): + from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible import ( + awslambda_function_not_publicly_accessible, + ) + + check = awslambda_function_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Lambda function {function_name} has a policy resource-based policy not public." + ) + assert result[0].resource_tags == [] + + def test_function_public_policy_with_several_statements(self): + lambda_client = mock.MagicMock + lambda_client.audited_account = AWS_ACCOUNT_NUMBER + function_name = "test-lambda" + function_runtime = "nodejs4.3" + function_arn = f"arn:aws:lambda:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:function/{function_name}" + lambda_policy = { + "Version": "2012-10-17", + "Id": "default", + "Statement": [ + { + "Sid": "AllowExecutionFromAPIGateway", + "Effect": "Allow", + "Principal": {"Service": "apigateway.amazonaws.com"}, + "Action": "lambda:InvokeFunction", + "Resource": f"arn:aws:lambda:eu-central-1:{AWS_ACCOUNT_NUMBER}:function:foo", + "Condition": { + "ArnLike": { + "AWS:SourceArn": f"arn:aws:execute-api:eu-central-1:{AWS_ACCOUNT_NUMBER}:bar/*/GET/proxy+" + } + }, + }, + { + "Sid": "FunctionURLAllowPublicAccess", + "Effect": "Allow", + "Principal": "*", + "Action": "lambda:InvokeFunctionUrl", + "Resource": f"arn:aws:lambda:eu-central-1:{AWS_ACCOUNT_NUMBER}:function:foo", + "Condition": { + "StringEquals": {"lambda:FunctionUrlAuthType": "NONE"} + }, + }, + ], + } + + lambda_client.functions = { + "function_name": Function( + name=function_name, + security_groups=[], + arn=function_arn, + region=AWS_REGION_EU_WEST_1, + runtime=function_runtime, + policy=lambda_policy, + ) + } + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible.awslambda_client", + new=lambda_client, + ): + from prowler.providers.aws.services.awslambda.awslambda_function_not_publicly_accessible.awslambda_function_not_publicly_accessible import ( + awslambda_function_not_publicly_accessible, + ) + + check = awslambda_function_not_publicly_accessible() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Lambda function {function_name} has a policy resource-based policy with public access." + ) + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].region == AWS_REGION_EU_WEST_1 diff --git a/tests/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az_test.py b/tests/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az_test.py new file mode 100644 index 0000000000..566124edba --- /dev/null +++ b/tests/providers/aws/services/awslambda/awslambda_function_vpc_multi_az/awslambda_function_vpc_multi_az_test.py @@ -0,0 +1,548 @@ +from json import dumps +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_EU_WEST_1_AZA, + AWS_REGION_EU_WEST_1_AZB, + set_mocked_aws_provider, +) + + +class Test_awslambda_function_vpc_is_in_multi_azs: + @mock_aws + def test_no_functions(self): + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.awslambda_client", + new=Lambda(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az import ( + awslambda_function_vpc_multi_az, + ) + + check = awslambda_function_vpc_multi_az() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_function_outside_vpc(self): + # Create IAM Role for Lambda Function + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + # Create Lambda Function outside VPC + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_name = "test_function_outside_vpc" + function_arn = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=role_arn, + Handler="lambda_function.lambda_handler", + Code={"ZipFile": b"file not used"}, + )["FunctionArn"] + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.awslambda_client", + new=Lambda(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az import ( + awslambda_function_vpc_multi_az, + ) + + check = awslambda_function_vpc_multi_az() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Lambda function {function_name} is not inside a VPC." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == function_name + assert result[0].resource_arn == function_arn + assert result[0].resource_tags == [{}] + + @mock_aws + def test_function_in_vpc_single_az(self): + # Create IAM Role for Lambda Function + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + # Create VPC + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + + # Create Subnet + subnet_id = ec2_client.create_subnet( + VpcId=vpc_id, + CidrBlock="10.0.1.0/24", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + )["Subnet"]["SubnetId"] + + # Create Security Group + security_group_id = ec2_client.create_security_group( + GroupName="test-sg", Description="Test SG", VpcId=vpc_id + )["GroupId"] + + # Create Lambda Function inside VPC + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + + function_name = "test_function_in_vpc_single_az" + + function = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=role_arn, + Handler="lambda_function.lambda_handler", + Code={"ZipFile": b"file not used"}, + VpcConfig={ + "SubnetIds": [subnet_id], + "SecurityGroupIds": [security_group_id], + }, + ) + function_vpc_id = function["VpcConfig"]["VpcId"] + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.awslambda_client", + new=Lambda(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.vpc_client", + new=VPC(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az import ( + awslambda_function_vpc_multi_az, + ) + + check = awslambda_function_vpc_multi_az() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Lambda function {function_name} is inside of VPC {function_vpc_id} that spans only in 1 AZs: {AWS_REGION_EU_WEST_1_AZA}. Must span in at least 2 AZs." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == function_name + assert result[0].resource_arn == function["FunctionArn"] + assert result[0].resource_tags == [{}] + + @mock_aws + def test_function_in_vpc_multiple_az(self): + # Create IAM Role for Lambda Function + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + # Create VPC + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + + # Create Subnets + subnet_id_a = ec2_client.create_subnet( + VpcId=vpc_id, + CidrBlock="10.0.1.0/24", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + )["Subnet"]["SubnetId"] + + subnet_id_b = ec2_client.create_subnet( + VpcId=vpc_id, + CidrBlock="10.0.2.0/24", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZB, + )["Subnet"]["SubnetId"] + + # Create Security Group + security_group_id = ec2_client.create_security_group( + GroupName="test-sg", Description="Test SG", VpcId=vpc_id + )["GroupId"] + + # Create Lambda Function inside VPC + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + + function_name = "test_function_in_vpc_multiple_az" + + function = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=role_arn, + Handler="lambda_function.lambda_handler", + Code={"ZipFile": b"file not used"}, + VpcConfig={ + "SubnetIds": [subnet_id_a, subnet_id_b], + "SecurityGroupIds": [security_group_id], + }, + ) + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.awslambda_client", + new=Lambda(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.vpc_client", + new=VPC(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az import ( + awslambda_function_vpc_multi_az, + ) + + check = awslambda_function_vpc_multi_az() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Lambda function {function_name} is inside of VPC {function['VpcConfig']['VpcId']} that spans in at least 2 AZs: {AWS_REGION_EU_WEST_1_AZB}, {AWS_REGION_EU_WEST_1_AZA}." + ) or ( + result[0].status_extended + == f"Lambda function {function_name} is inside of VPC {function['VpcConfig']['VpcId']} that spans in at least 2 AZs: {AWS_REGION_EU_WEST_1_AZA}, {AWS_REGION_EU_WEST_1_AZB}." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == function_name + assert result[0].resource_arn == function["FunctionArn"] + assert result[0].resource_tags == [{}] + + @mock_aws + def test_function_with_multiple_subnets_in_same_az(self): + # Create IAM Role for Lambda Function + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + # Create VPC + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + + # Create Subnets + subnet_id_a = ec2_client.create_subnet( + VpcId=vpc_id, + CidrBlock="10.0.1.0/24", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + )["Subnet"]["SubnetId"] + + subnet_id_b = ec2_client.create_subnet( + VpcId=vpc_id, + CidrBlock="10.0.2.0/24", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + )["Subnet"]["SubnetId"] + + # Create Security Group + security_group_id = ec2_client.create_security_group( + GroupName="test-sg", Description="Test SG", VpcId=vpc_id + )["GroupId"] + + # Create Lambda Function inside VPC + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + + function_name = "test_function_in_vpc_multiple_subnets_same_az" + + function = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=role_arn, + Handler="lambda_function.lambda_handler", + Code={"ZipFile": b"file not used"}, + VpcConfig={ + "SubnetIds": [subnet_id_a, subnet_id_b], + "SecurityGroupIds": [security_group_id], + }, + ) + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.awslambda_client", + new=Lambda(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.vpc_client", + new=VPC(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az import ( + awslambda_function_vpc_multi_az, + ) + + check = awslambda_function_vpc_multi_az() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Lambda function {function_name} is inside of VPC {function['VpcConfig']['VpcId']} that spans only in 1 AZs: {AWS_REGION_EU_WEST_1_AZA}. Must span in at least 2 AZs." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == function_name + assert result[0].resource_arn == function["FunctionArn"] + assert result[0].resource_tags == [{}] + + @mock_aws + def test_function_no_vpc_pass_to_avoid_fail_twice(self): + # Create IAM Role for Lambda Function + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + # Create Lambda Function outside VPC + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + function_name = "test_function_no_vpc_pass_to_avoid_fail_twice" + lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=role_arn, + Handler="lambda_function.lambda_handler", + Code={"ZipFile": b"file not used"}, + )["FunctionArn"] + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.awslambda_client", + new=Lambda(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.vpc_client", + new=VPC(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_inside_vpc.awslambda_function_inside_vpc.awslambda_client", + new=Lambda(aws_provider), + ): + # Test check inside_vpc first + + from prowler.providers.aws.services.awslambda.awslambda_function_inside_vpc.awslambda_function_inside_vpc import ( + awslambda_function_inside_vpc, + ) + + check = awslambda_function_inside_vpc() + + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + + # Now test if function is in multiple AZs to ensure it does not fail twice + + from prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az import ( + awslambda_function_vpc_multi_az, + ) + + check = awslambda_function_vpc_multi_az() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_resource_filtered(self): + # Create a compliant Lambda Function + + # Create IAM Role for Lambda Function + iam_client = client("iam", region_name=AWS_REGION_EU_WEST_1) + role_name = "test-role" + assume_role_policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"Service": "lambda.amazonaws.com"}, + "Action": "sts:AssumeRole", + } + ], + } + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(assume_role_policy_document), + )["Role"]["Arn"] + + # Create VPC + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + + # Create Subnets + subnet_id_a = ec2_client.create_subnet( + VpcId=vpc_id, + CidrBlock="10.0.1.0/24", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZA, + )["Subnet"]["SubnetId"] + + subnet_id_b = ec2_client.create_subnet( + VpcId=vpc_id, + CidrBlock="10.0.2.0/24", + AvailabilityZone=AWS_REGION_EU_WEST_1_AZB, + )["Subnet"]["SubnetId"] + + # Create Security Group + security_group_id = ec2_client.create_security_group( + GroupName="test-sg", Description="Test SG", VpcId=vpc_id + )["GroupId"] + + # Create Lambda Function inside VPC + lambda_client = client("lambda", region_name=AWS_REGION_EU_WEST_1) + + function_name = "test_function_resource_filtered" + + function = lambda_client.create_function( + FunctionName=function_name, + Runtime="python3.8", + Role=role_arn, + Handler="lambda_function.lambda_handler", + Code={"ZipFile": b"file not used"}, + VpcConfig={ + "SubnetIds": [subnet_id_a, subnet_id_b], + "SecurityGroupIds": [security_group_id], + }, + ) + + from prowler.providers.aws.services.awslambda.awslambda_service import Lambda + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + # Filter the resource in the provider, this is like the --resource-arn option + aws_provider._audit_resources = [function["FunctionArn"]] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.awslambda_client", + new=Lambda(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az.vpc_client", + new=VPC(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.awslambda.awslambda_function_vpc_multi_az.awslambda_function_vpc_multi_az import ( + awslambda_function_vpc_multi_az, + ) + + check = awslambda_function_vpc_multi_az() + result = check.execute() + + assert len(result) == 1 + assert ( + result[0].status == "FAIL" + ) # This should be a PASS, but the resource is filtered so subnets are filtered and this is a bug + assert ( + result[0].status_extended + == f"Lambda function {function_name} is inside of VPC {function['VpcConfig']['VpcId']} that spans only in 0 AZs: . Must span in at least 2 AZs." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == function_name + assert result[0].resource_arn == function["FunctionArn"] + assert result[0].resource_tags == [{}] diff --git a/tests/providers/aws/services/awslambda/awslambda_service_test.py b/tests/providers/aws/services/awslambda/awslambda_service_test.py index e04e3fb09e..f3247f044f 100644 --- a/tests/providers/aws/services/awslambda/awslambda_service_test.py +++ b/tests/providers/aws/services/awslambda/awslambda_service_test.py @@ -221,6 +221,7 @@ class Test_Lambda_Service: lambda_arn_1 ].url_config.cors_config.allow_origins == ["*"] assert awslambda.functions[lambda_arn_1].vpc_id == "vpc-123abc" + assert awslambda.functions[lambda_arn_1].subnet_ids == {"subnet-123abc"} assert awslambda.functions[lambda_arn_1].tags == [{"test": "test"}] diff --git a/tests/providers/aws/services/backup/backup_plans_exist/backup_plans_exist_test.py b/tests/providers/aws/services/backup/backup_plans_exist/backup_plans_exist_test.py index 7cae2c73fa..d1fb5afd1c 100644 --- a/tests/providers/aws/services/backup/backup_plans_exist/backup_plans_exist_test.py +++ b/tests/providers/aws/services/backup/backup_plans_exist/backup_plans_exist_test.py @@ -45,6 +45,7 @@ class Test_backup_plans_exist: == f"arn:aws:backup:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:backup-plan" ) assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] def test_no_backup_plans_not_vaults(self): backup_client = mock.MagicMock @@ -88,6 +89,7 @@ class Test_backup_plans_exist: version_id="version_id", last_execution_date=datetime(2015, 1, 1), advanced_settings=[], + tags=[], ) ] with mock.patch( @@ -117,3 +119,4 @@ class Test_backup_plans_exist: == f"arn:aws:backup:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:plan:{backup_plan_id}" ) assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/backup/backup_service_test.py b/tests/providers/aws/services/backup/backup_service_test.py index 2da4cd505f..8482b33df7 100644 --- a/tests/providers/aws/services/backup/backup_service_test.py +++ b/tests/providers/aws/services/backup/backup_service_test.py @@ -2,10 +2,15 @@ from datetime import datetime from unittest.mock import patch import botocore +from boto3 import client from moto import mock_aws from prowler.providers.aws.services.backup.backup_service import Backup -from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) # Mocking Backup Calls make_api_call = botocore.client.BaseClient._make_api_call @@ -53,16 +58,33 @@ def mock_make_api_call(self, operation_name, kwarg): } ] } - if operation_name == "ListProtectedResources": + if operation_name == "ListBackupSelections": return { - "Results": [ + "BackupSelectionsList": [ { - "ResourceArn": "arn:aws:rds:eu-west-1:123456789012:db:my-db-instance", - "ResourceType": "RDS", - "LastBackupTime": datetime(2015, 1, 1), + "SelectionId": "selection-id-1", + "SelectionName": "TestSelection", + "BackupPlanId": "ID-TestBackupPlan", + "CreationDate": datetime(2015, 1, 1), + "CreatorRequestId": "request-id-1", + "IamRoleArn": "arn:aws:iam::123456789012:role/service-role/AWSBackupDefaultServiceRole", } ] } + if operation_name == "GetBackupSelection": + return { + "BackupSelection": { + "SelectionName": "TestSelection", + "IamRoleArn": "arn:aws:iam::123456789012:role/service-role/AWSBackupDefaultServiceRole", + "Resources": [ + "arn:aws:dynamodb:eu-west-1:123456789012:table/MyDynamoDBTable" + ], + }, + "SelectionId": "selection-id-1", + "BackupPlanId": "ID-TestBackupPlan", + "CreationDate": datetime(2015, 1, 1), + "CreatorRequestId": "request-id-1", + } return make_api_call(self, operation_name, kwarg) @@ -74,15 +96,14 @@ def mock_generate_regional_clients(provider, service): return {AWS_REGION_EU_WEST_1: regional_client} -@mock_aws -# Patch every AWS call using Boto3 and generate_regional_clients to have 1 client -@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) -@patch( - "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", - new=mock_generate_regional_clients, -) -class Test_Backup_Service: +class TestBackupService: # Test Backup Client + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test_get_client(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) backup = Backup(aws_provider) @@ -91,18 +112,36 @@ class Test_Backup_Service: ) # Test Backup Session + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test__get_session__(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) access_analyzer = Backup(aws_provider) assert access_analyzer.session.__class__.__name__ == "Session" - # Test Backup Service + # Test Backup Serviceç + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test__get_service__(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) access_analyzer = Backup(aws_provider) assert access_analyzer.service == "backup" # Test Backup List Backup Vaults + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test_list_backup_vaults(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) backup = Backup(aws_provider) @@ -117,6 +156,12 @@ class Test_Backup_Service: assert backup.backup_vaults[0].max_retention_days == 2 # Test Backup List Backup Plans + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test_list_backup_plans(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) backup = Backup(aws_provider) @@ -130,6 +175,12 @@ class Test_Backup_Service: assert backup.backup_plans[0].advanced_settings == [] # Test Backup List Report Plans + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test_list_backup_report_plans(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) backup = Backup(aws_provider) @@ -144,14 +195,61 @@ class Test_Backup_Service: 2015, 1, 1 ) - def test_list_protected_resources(self): + # Test Backup List Backup Selections + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) + def test_list_backup_selections(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) backup = Backup(aws_provider) assert len(backup.protected_resources) == 1 - arn = "arn:aws:rds:eu-west-1:123456789012:db:my-db-instance" - protected_resource = backup.protected_resources.get(arn) - assert protected_resource is not None - assert protected_resource.arn == arn - assert protected_resource.resource_type == "RDS" - assert protected_resource.region == AWS_REGION_EU_WEST_1 - assert protected_resource.last_backup_time == datetime(2015, 1, 1) + assert ( + "arn:aws:dynamodb:eu-west-1:123456789012:table/MyDynamoDBTable" + in backup.protected_resources + ) + + @mock_aws + def test_list_tags(self): + backup_client = client("backup", region_name=AWS_REGION_EU_WEST_1) + + # Create necessary resources and tags + backup_vault = backup_client.create_backup_vault( + BackupVaultName="TestVault", + EncryptionKeyArn=f"arn:aws:kms:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:key/1234abcd-12ab-34cd-56ef-123456789012", + ) + + tags = {"TestKey": "TestValue"} + + backup_client.tag_resource( + ResourceArn=backup_vault["BackupVaultArn"], Tags=tags + ) + + # Create a backup plan + backup_plan = backup_client.create_backup_plan( + BackupPlan={ + "BackupPlanName": "TestPlan", + "Rules": [ + { + "RuleName": "TestRule", + "TargetBackupVaultName": "TestVault", # Match the vault name + "ScheduleExpression": "cron(0 12 * * ? *)", + } + ], + } + ) + + backup_client.tag_resource(ResourceArn=backup_plan["BackupPlanArn"], Tags=tags) + + # Test list_tags + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + backup = Backup(aws_provider) + + assert len(backup.backup_vaults) == 1 + assert len(backup.backup_vaults[0].tags) == 1 + assert backup.backup_vaults[0].tags[0]["TestKey"] == "TestValue" + assert len(backup.backup_plans) == 1 + assert len(backup.backup_plans[0].tags) == 1 + assert backup.backup_plans[0].tags[0]["TestKey"] == "TestValue" diff --git a/tests/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted_test.py b/tests/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted_test.py index bd37684728..fe0223c4eb 100644 --- a/tests/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted_test.py +++ b/tests/providers/aws/services/backup/backup_vaults_encrypted/backup_vaults_encrypted_test.py @@ -40,6 +40,7 @@ class Test_backup_vaults_encrypted: locked=True, min_retention_days=1, max_retention_days=2, + tags=[], ) ] @@ -67,6 +68,7 @@ class Test_backup_vaults_encrypted: assert result[0].resource_id == "MyBackupVault" assert result[0].resource_arn == backup_vault_arn assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] def test_one_backup_vault_encrypted(self): backup_client = mock.MagicMock @@ -81,6 +83,7 @@ class Test_backup_vaults_encrypted: locked=True, min_retention_days=1, max_retention_days=2, + tags=[], ) ] @@ -108,3 +111,4 @@ class Test_backup_vaults_encrypted: assert result[0].resource_id == "MyBackupVault" assert result[0].resource_arn == backup_vault_arn assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist_test.py b/tests/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist_test.py index 2b682c4be3..8c1c229359 100644 --- a/tests/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist_test.py +++ b/tests/providers/aws/services/backup/backup_vaults_exist/backup_vaults_exist_test.py @@ -42,6 +42,7 @@ class Test_backup_vaults_exist: == f"arn:aws:backup:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:backup-vault" ) assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] def test_one_backup_vault(self): backup_client = mock.MagicMock @@ -64,6 +65,7 @@ class Test_backup_vaults_exist: locked=True, min_retention_days=1, max_retention_days=2, + tags=[], ) ] @@ -91,6 +93,7 @@ class Test_backup_vaults_exist: assert result[0].resource_id == "MyBackupVault" assert result[0].resource_arn == backup_vault_arn assert result[0].region == AWS_REGION + assert result[0].resource_tags == [] def test_access_denied(self): backup_client = mock.MagicMock diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate_test.py new file mode 100644 index 0000000000..bd7bb8dd05 --- /dev/null +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_custom_ssl_certificate/cloudfront_distributions_custom_ssl_certificate_test.py @@ -0,0 +1,99 @@ +from unittest import mock + +from prowler.providers.aws.services.cloudfront.cloudfront_service import Distribution +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER + +DISTRIBUTION_ID = "E27LVI50CSW06W" +DISTRIBUTION_ARN = ( + f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}" +) +REGION = "eu-west-1" + + +class Test_cloudfront_distributions_custom_ssl_certificate: + def test_no_distributions(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = {} + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_custom_ssl_certificate.cloudfront_distributions_custom_ssl_certificate import ( + cloudfront_distributions_custom_ssl_certificate, + ) + + check = cloudfront_distributions_custom_ssl_certificate() + result = check.execute() + + assert len(result) == 0 + + def test_distribution_default_certificate(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + logging_enabled=True, + origins=[], + default_certificate=True, + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_custom_ssl_certificate.cloudfront_distributions_custom_ssl_certificate import ( + cloudfront_distributions_custom_ssl_certificate, + ) + + check = cloudfront_distributions_custom_ssl_certificate() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} is using the default SSL/TLS certificate." + ) + + def test_distribution_custom_certificate(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + logging_enabled=True, + origins=[], + default_certificate=False, + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_custom_ssl_certificate.cloudfront_distributions_custom_ssl_certificate import ( + cloudfront_distributions_custom_ssl_certificate, + ) + + check = cloudfront_distributions_custom_ssl_certificate() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} is using a custom SSL/TLS certificate." + ) diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object_test.py new file mode 100644 index 0000000000..71be6e2fa8 --- /dev/null +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_default_root_object/cloudfront_distributions_default_root_object_test.py @@ -0,0 +1,113 @@ +from unittest import mock + +from prowler.providers.aws.services.cloudfront.cloudfront_service import ( + DefaultCacheConfigBehaviour, + Distribution, + ViewerProtocolPolicy, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER + +DISTRIBUTION_ID = "E27LVI50CSW06W" +DISTRIBUTION_ARN = ( + f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}" +) +REGION = "eu-west-1" + + +class Test_cloudfront_distributions_default_root_object: + def test_no_distributions(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = {} + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_default_root_object.cloudfront_distributions_default_root_object import ( + cloudfront_distributions_default_root_object, + ) + + check = cloudfront_distributions_default_root_object() + result = check.execute() + + assert len(result) == 0 + + def test_distribution_no_root_object(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.allow_all, + field_level_encryption_id="", + ), + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_default_root_object.cloudfront_distributions_default_root_object import ( + cloudfront_distributions_default_root_object, + ) + + check = cloudfront_distributions_default_root_object() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does not have a default root object configured." + ) + assert result[0].resource_tags == [] + + def test_distribution_root_object(self): + cloudfront_client = mock.MagicMock + dro = "index.html" + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.allow_all, + field_level_encryption_id="", + ), + default_root_object=dro, + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_default_root_object.cloudfront_distributions_default_root_object import ( + cloudfront_distributions_default_root_object, + ) + + check = cloudfront_distributions_default_root_object() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does have a default root object ({dro}) configured." + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_field_level_encryption_enabled/cloudfront_distributions_field_level_encryption_enabled_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_field_level_encryption_enabled/cloudfront_distributions_field_level_encryption_enabled_test.py index 7325dfbe23..970f4b6ec2 100644 --- a/tests/providers/aws/services/cloudfront/cloudfront_distributions_field_level_encryption_enabled/cloudfront_distributions_field_level_encryption_enabled_test.py +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_field_level_encryption_enabled/cloudfront_distributions_field_level_encryption_enabled_test.py @@ -35,7 +35,7 @@ class Test_cloudfront_distributions_field_level_encryption_enabled: def test_one_distribution_field_level_encryption_enabled(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -45,6 +45,7 @@ class Test_cloudfront_distributions_field_level_encryption_enabled: viewer_protocol_policy=ViewerProtocolPolicy.https_only, field_level_encryption_id="AAAAAAAA", ), + origin_failover=False, ) } @@ -74,7 +75,7 @@ class Test_cloudfront_distributions_field_level_encryption_enabled: def test_one_distribution_field_level_encryption_disabled(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -84,6 +85,7 @@ class Test_cloudfront_distributions_field_level_encryption_enabled: viewer_protocol_policy=ViewerProtocolPolicy.https_only, field_level_encryption_id="", ), + origin_failover=False, ) } diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_geo_restrictions_enabled/cloudfront_distributions_geo_restrictions_enabled_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_geo_restrictions_enabled/cloudfront_distributions_geo_restrictions_enabled_test.py index 381a73bbca..0b64ad726e 100644 --- a/tests/providers/aws/services/cloudfront/cloudfront_distributions_geo_restrictions_enabled/cloudfront_distributions_geo_restrictions_enabled_test.py +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_geo_restrictions_enabled/cloudfront_distributions_geo_restrictions_enabled_test.py @@ -34,7 +34,7 @@ class Test_cloudfront_distributions_geo_restrictions_enabled: def test_one_distribution_geo_restriction_disabled(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -69,12 +69,13 @@ class Test_cloudfront_distributions_geo_restrictions_enabled: def test_one_distribution_geo_restriction_enabled_whitelist(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, origins=[], geo_restriction_type=GeoRestrictionType.whitelist, + origin_failover=False, ) } @@ -104,12 +105,13 @@ class Test_cloudfront_distributions_geo_restrictions_enabled: def test_one_distribution_geo_restriction_enabled_blacklist(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, origins=[], geo_restriction_type=GeoRestrictionType.blacklist, + origin_failover=False, ) } diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_https_enabled/cloudfront_distributions_https_enabled_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_https_enabled/cloudfront_distributions_https_enabled_test.py index 6aa47e8b8a..7729b82f84 100644 --- a/tests/providers/aws/services/cloudfront/cloudfront_distributions_https_enabled/cloudfront_distributions_https_enabled_test.py +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_https_enabled/cloudfront_distributions_https_enabled_test.py @@ -35,7 +35,7 @@ class Test_cloudfront_distributions_https_enabled: def test_one_distribution_https_disabled(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -45,6 +45,7 @@ class Test_cloudfront_distributions_https_enabled: viewer_protocol_policy=ViewerProtocolPolicy.allow_all, field_level_encryption_id="", ), + origin_failover=False, ) } @@ -74,7 +75,7 @@ class Test_cloudfront_distributions_https_enabled: def test_one_distribution_https_redirect(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -84,6 +85,7 @@ class Test_cloudfront_distributions_https_enabled: viewer_protocol_policy=ViewerProtocolPolicy.redirect_to_https, field_level_encryption_id="", ), + origin_failover=False, ) } @@ -113,7 +115,7 @@ class Test_cloudfront_distributions_https_enabled: def test_one_distribution_https_only(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -123,6 +125,7 @@ class Test_cloudfront_distributions_https_enabled: viewer_protocol_policy=ViewerProtocolPolicy.https_only, field_level_encryption_id="", ), + origin_failover=False, ) } diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled_test.py new file mode 100644 index 0000000000..a70735668a --- /dev/null +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_https_sni_enabled/cloudfront_distributions_https_sni_enabled_test.py @@ -0,0 +1,130 @@ +from unittest import mock + +from prowler.providers.aws.services.cloudfront.cloudfront_service import ( + Distribution, + SSLSupportMethod, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER + +DISTRIBUTION_ID = "E27LVI50CSW06W" +DISTRIBUTION_ARN = ( + f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}" +) +REGION = "us-east-1" + + +class Test_cloudfront_distributions_https_sni_enabled: + def test_no_distributions(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = {} + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_https_sni_enabled.cloudfront_distributions_https_sni_enabled import ( + cloudfront_distributions_https_sni_enabled, + ) + + check = cloudfront_distributions_https_sni_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_distribution_no_certificate(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + logging_enabled=True, + origins=[], + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_https_sni_enabled.cloudfront_distributions_https_sni_enabled import ( + cloudfront_distributions_https_sni_enabled, + ) + + check = cloudfront_distributions_https_sni_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_distribution_certificate_not_set_up(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + logging_enabled=True, + origins=[], + ssl_support_method=SSLSupportMethod.static_ip, + certificate="arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012", + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_https_sni_enabled.cloudfront_distributions_https_sni_enabled import ( + cloudfront_distributions_https_sni_enabled, + ) + + check = cloudfront_distributions_https_sni_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} is not serving HTTPS requests using SNI." + ) + + def test_distribution_valid_configuration(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + logging_enabled=True, + origins=[], + ssl_support_method=SSLSupportMethod.sni_only, + certificate="arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012", + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_https_sni_enabled.cloudfront_distributions_https_sni_enabled import ( + cloudfront_distributions_https_sni_enabled, + ) + + check = cloudfront_distributions_https_sni_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} is serving HTTPS requests using SNI." + ) diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_logging_enabled/cloudfront_distributions_logging_enabled_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_logging_enabled/cloudfront_distributions_logging_enabled_test.py index 764959fa8c..4a084cf71a 100644 --- a/tests/providers/aws/services/cloudfront/cloudfront_distributions_logging_enabled/cloudfront_distributions_logging_enabled_test.py +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_logging_enabled/cloudfront_distributions_logging_enabled_test.py @@ -35,12 +35,13 @@ class Test_cloudfront_distributions_logging_enabled: def test_one_distribution_logging_enabled(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, logging_enabled=True, origins=[], + origin_failover=False, ) } @@ -69,7 +70,7 @@ class Test_cloudfront_distributions_logging_enabled: def test_one_distribution_logging_disabled_realtime_disabled(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -80,6 +81,7 @@ class Test_cloudfront_distributions_logging_enabled: field_level_encryption_id="", ), origins=[], + origin_failover=False, ) } @@ -109,7 +111,7 @@ class Test_cloudfront_distributions_logging_enabled: def test_one_distribution_logging_disabled_realtime_enabled(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -120,6 +122,7 @@ class Test_cloudfront_distributions_logging_enabled: field_level_encryption_id="", ), origins=[], + origin_failover=False, ) } @@ -149,7 +152,7 @@ class Test_cloudfront_distributions_logging_enabled: def test_one_distribution_logging_enabled_realtime_enabled(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -160,6 +163,7 @@ class Test_cloudfront_distributions_logging_enabled: field_level_encryption_id="", ), origins=[], + origin_failover=False, ) } diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured_test.py new file mode 100644 index 0000000000..eaf27e4ebb --- /dev/null +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_multiple_origin_failover_configured/cloudfront_distributions_multiple_origin_failover_configured_test.py @@ -0,0 +1,113 @@ +from unittest import mock + +from prowler.providers.aws.services.cloudfront.cloudfront_service import ( + DefaultCacheConfigBehaviour, + Distribution, + ViewerProtocolPolicy, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER + +DISTRIBUTION_ID = "E27LVI50CSW06W" +DISTRIBUTION_ARN = ( + f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}" +) +REGION = "eu-west-1" + + +class Test_cloudfront_distributions_multiple_origin_failover_configured: + def test_no_distributions(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = {} + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_multiple_origin_failover_configured.cloudfront_distributions_multiple_origin_failover_configured import ( + cloudfront_distributions_multiple_origin_failover_configured, + ) + + check = cloudfront_distributions_multiple_origin_failover_configured() + result = check.execute() + + assert len(result) == 0 + + def test_no_origin_failover(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.allow_all, + field_level_encryption_id="", + ), + origin_failover=False, + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_multiple_origin_failover_configured.cloudfront_distributions_multiple_origin_failover_configured import ( + cloudfront_distributions_multiple_origin_failover_configured, + ) + + check = cloudfront_distributions_multiple_origin_failover_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does not have an origin group configured with at least 2 origins." + ) + assert result[0].resource_tags == [] + + def test_origin_failover(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.allow_all, + field_level_encryption_id="", + ), + origin_failover=True, + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_multiple_origin_failover_configured.cloudfront_distributions_multiple_origin_failover_configured import ( + cloudfront_distributions_multiple_origin_failover_configured, + ) + + check = cloudfront_distributions_multiple_origin_failover_configured() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} has an origin group with at least 2 origins configured." + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted_test.py new file mode 100644 index 0000000000..4063794ee0 --- /dev/null +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_origin_traffic_encrypted/cloudfront_distributions_origin_traffic_encrypted_test.py @@ -0,0 +1,227 @@ +from unittest import mock + +from prowler.providers.aws.services.cloudfront.cloudfront_service import ( + DefaultCacheConfigBehaviour, + Distribution, + Origin, + ViewerProtocolPolicy, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER + +DISTRIBUTION_ID = "E27LVI50CSW06W" +DISTRIBUTION_ARN = ( + f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}" +) +REGION = "eu-west-1" + + +class Test_cloudfront_distributions_origin_traffic_encrypted: + def test_no_distributions(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = {} + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_origin_traffic_encrypted.cloudfront_distributions_origin_traffic_encrypted import ( + cloudfront_distributions_origin_traffic_encrypted, + ) + + check = cloudfront_distributions_origin_traffic_encrypted() + result = check.execute() + + assert len(result) == 0 + + def test_distribution_no_traffic_encryption(self): + cloudfront_client = mock.MagicMock + id = "origin1" + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[ + Origin( + id=id, + domain_name="asdf.s3.us-east-1.amazonaws.com", + origin_protocol_policy="", + origin_ssl_protocols=[], + ) + ], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.allow_all, + field_level_encryption_id="", + ), + default_root_object="", + viewer_protocol_policy="", + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_origin_traffic_encrypted.cloudfront_distributions_origin_traffic_encrypted import ( + cloudfront_distributions_origin_traffic_encrypted, + ) + + check = cloudfront_distributions_origin_traffic_encrypted() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does not encrypt traffic to custom origins {id}." + ) + assert result[0].resource_tags == [] + + def test_distribution_http_only(self): + cloudfront_client = mock.MagicMock + id = "origin1" + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[ + Origin( + id=id, + domain_name="asdf.s3.us-east-1.amazonaws.com", + origin_protocol_policy="http-only", + origin_ssl_protocols=[], + ) + ], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.allow_all, + field_level_encryption_id="", + ), + default_root_object="index.html", + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_origin_traffic_encrypted.cloudfront_distributions_origin_traffic_encrypted import ( + cloudfront_distributions_origin_traffic_encrypted, + ) + + check = cloudfront_distributions_origin_traffic_encrypted() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does not encrypt traffic to custom origins {id}." + ) + assert result[0].resource_tags == [] + + def test_distribution_match_viewer_allow_all(self): + cloudfront_client = mock.MagicMock + id = "origin1" + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[ + Origin( + id=id, + domain_name="asdf.s3.us-east-1.amazonaws.com", + origin_protocol_policy="match-viewer", + origin_ssl_protocols=[], + ) + ], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.allow_all, + field_level_encryption_id="", + ), + default_root_object="index.html", + viewer_protocol_policy="allow-all", + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_origin_traffic_encrypted.cloudfront_distributions_origin_traffic_encrypted import ( + cloudfront_distributions_origin_traffic_encrypted, + ) + + check = cloudfront_distributions_origin_traffic_encrypted() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does not encrypt traffic to custom origins {id}." + ) + assert result[0].resource_tags == [] + + def test_distribution_traffic_encrypted(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[ + Origin( + id="origin1", + domain_name="asdf.s3.us-east-1.amazonaws.com", + origin_protocol_policy="https-only", + origin_ssl_protocols=[], + ) + ], + default_cache_config=DefaultCacheConfigBehaviour( + realtime_log_config_arn="", + viewer_protocol_policy=ViewerProtocolPolicy.allow_all, + field_level_encryption_id="", + ), + default_root_object="index.html", + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_origin_traffic_encrypted.cloudfront_distributions_origin_traffic_encrypted import ( + cloudfront_distributions_origin_traffic_encrypted, + ) + + check = cloudfront_distributions_origin_traffic_encrypted() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does encrypt traffic to custom origins." + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control_test.py new file mode 100644 index 0000000000..c3de591a8c --- /dev/null +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_access_control/cloudfront_distributions_s3_origin_access_control_test.py @@ -0,0 +1,155 @@ +from unittest import mock + +from prowler.providers.aws.services.cloudfront.cloudfront_service import ( + Distribution, + Origin, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER + +DISTRIBUTION_ID = "E27LVI50CSW06W" +DISTRIBUTION_ARN = ( + f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}" +) +REGION = "eu-west-1" + + +class Test_cloudfront_distributions_s3_origin_access_control: + def test_no_distributions(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = {} + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_s3_origin_access_control.cloudfront_distributions_s3_origin_access_control import ( + cloudfront_distributions_s3_origin_access_control, + ) + + check = cloudfront_distributions_s3_origin_access_control() + result = check.execute() + + assert len(result) == 0 + + def test_no_s3_origin_distributions(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[ + Origin( + id="string", + domain_name="string", + origin_protocol_policy="https-only", + origin_ssl_protocols=["TLSv1", "TLSv1.1"], + s3_origin_config={}, + ), + ], + ) + } + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_s3_origin_access_control.cloudfront_distributions_s3_origin_access_control import ( + cloudfront_distributions_s3_origin_access_control, + ) + + check = cloudfront_distributions_s3_origin_access_control() + result = check.execute() + + assert len(result) == 0 + + def test_distribution_using_origin_access_control(self): + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[ + Origin( + id="string", + domain_name="string", + origin_protocol_policy="https-only", + origin_ssl_protocols=["TLSv1", "TLSv1.1"], + origin_access_control="EXAMPLE-OAC-ID", + s3_origin_config={ + "OriginAccessIdentity": "origin-access-identity/cloudfront/EXAMPLE-OAI-ID" + }, + ), + ], + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_s3_origin_access_control.cloudfront_distributions_s3_origin_access_control import ( + cloudfront_distributions_s3_origin_access_control, + ) + + check = cloudfront_distributions_s3_origin_access_control() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} is using origin access control (OAC) for S3 origins." + ) + assert result[0].resource_tags == [] + + def test_distribution_not_using_origin_access_control(self): + cloudfront_client = mock.MagicMock + id = "EXAMPLE-OAC-ID" + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + origins=[ + Origin( + id=id, + domain_name="string", + origin_protocol_policy="https-only", + origin_ssl_protocols=["TLSv1", "TLSv1.1"], + origin_access_control="", + s3_origin_config={ + "OriginAccessIdentity": "origin-access-identity/cloudfront/EXAMPLE-OAI-ID" + }, + ), + ], + ) + } + + with mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_s3_origin_access_control.cloudfront_distributions_s3_origin_access_control import ( + cloudfront_distributions_s3_origin_access_control, + ) + + check = cloudfront_distributions_s3_origin_access_control() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == REGION + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} is not using origin access control (OAC) in S3 origins {id}." + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket_test.py new file mode 100644 index 0000000000..127ea69906 --- /dev/null +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_s3_origin_non_existent_bucket/cloudfront_distributions_s3_origin_non_existent_bucket_test.py @@ -0,0 +1,149 @@ +from unittest import mock + +from prowler.providers.aws.services.cloudfront.cloudfront_service import ( + Distribution, + Origin, +) +from prowler.providers.aws.services.s3.s3_service import Bucket +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 + +DISTRIBUTION_ID = "E27LVI50CSW06W" +DISTRIBUTION_ARN = ( + f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}" +) + + +class Test_cloudfront_s3_origin_non_existent_bucket: + def test_no_distributions(self): + # Distributions + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = {} + s3_client = mock.MagicMock() + # Buckets + s3_client.buckets = {} + + with mock.patch( + "prowler.providers.aws.services.s3.s3_service.S3", new=s3_client + ), mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_s3_origin_non_existent_bucket.cloudfront_distributions_s3_origin_non_existent_bucket import ( + cloudfront_distributions_s3_origin_non_existent_bucket, + ) + + check = cloudfront_distributions_s3_origin_non_existent_bucket() + result = check.execute() + + assert len(result) == 0 + + def test_distribution_nonexistent_origins(self): + # Distributions + domain = "nonexistent-bucket.s3.eu-west-1.amazonaws.com" + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=AWS_REGION_EU_WEST_1, + logging_enabled=True, + origins=[ + Origin( + domain_name=domain, + id="S3-ORIGIN", + origin_protocol_policy="", + origin_ssl_protocols=[], + ), + ], + ) + } + # Buckets + nonexistent_bucket = "nonexistent-bucket" + s3_client = mock.MagicMock() + s3_client.buckets = {} + + with mock.patch( + "prowler.providers.aws.services.s3.s3_service.S3", new=s3_client + ), mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ), mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_distributions_s3_origin_non_existent_bucket.cloudfront_distributions_s3_origin_non_existent_bucket.s3_client._head_bucket", + new=mock.MagicMock(return_value=False), + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_s3_origin_non_existent_bucket.cloudfront_distributions_s3_origin_non_existent_bucket import ( + cloudfront_distributions_s3_origin_non_existent_bucket, + ) + + check = cloudfront_distributions_s3_origin_non_existent_bucket() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} has non-existent S3 buckets as origins: {nonexistent_bucket}." + ) + + def test_distribution_no_nonexistent_origins(self): + # Distributions + domain = "existent-bucket.s3.eu-west-1.amazonaws.com" + cloudfront_client = mock.MagicMock + cloudfront_client.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=AWS_REGION_EU_WEST_1, + logging_enabled=True, + origins=[ + Origin( + domain_name=domain, + id="S3-ORIGIN", + origin_protocol_policy="", + origin_ssl_protocols=[], + ), + ], + ) + } + # Buckets + bucket_name = "existent-bucket" + s3_client = mock.MagicMock() + s3_client.audited_account = AWS_ACCOUNT_NUMBER + s3_client.buckets = { + f"arn:aws:s3:::{bucket_name}": Bucket( + name=bucket_name, + region=AWS_REGION_EU_WEST_1, + ) + } + + with mock.patch( + "prowler.providers.aws.services.s3.s3_service.S3", new=s3_client + ), mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_service.CloudFront", + new=cloudfront_client, + ), mock.patch( + "prowler.providers.aws.services.cloudfront.cloudfront_distributions_s3_origin_non_existent_bucket.cloudfront_distributions_s3_origin_non_existent_bucket.s3_client._head_bucket", + new=mock.MagicMock(return_value=True), + ): + # Test Check + from prowler.providers.aws.services.cloudfront.cloudfront_distributions_s3_origin_non_existent_bucket.cloudfront_distributions_s3_origin_non_existent_bucket import ( + cloudfront_distributions_s3_origin_non_existent_bucket, + ) + + check = cloudfront_distributions_s3_origin_non_existent_bucket() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_arn == DISTRIBUTION_ARN + assert result[0].resource_id == DISTRIBUTION_ID + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"CloudFront Distribution {DISTRIBUTION_ID} does not have non-existent S3 buckets as origins." + ) diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_using_deprecated_ssl_protocols/cloudfront_distributions_using_deprecated_ssl_protocols_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_using_deprecated_ssl_protocols/cloudfront_distributions_using_deprecated_ssl_protocols_test.py index b693595fbf..4115abb17e 100644 --- a/tests/providers/aws/services/cloudfront/cloudfront_distributions_using_deprecated_ssl_protocols/cloudfront_distributions_using_deprecated_ssl_protocols_test.py +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_using_deprecated_ssl_protocols/cloudfront_distributions_using_deprecated_ssl_protocols_test.py @@ -34,7 +34,7 @@ class Test_cloudfront_distributions_using_deprecated_ssl_protocols: def test_one_distribution_using_deprecated_ssl_protocols(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -46,6 +46,7 @@ class Test_cloudfront_distributions_using_deprecated_ssl_protocols: origin_ssl_protocols=["SSLv3"], ) ], + origin_failover=False, ) } @@ -75,7 +76,7 @@ class Test_cloudfront_distributions_using_deprecated_ssl_protocols: def test_one_distribution_using_SSL_and_TLS(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -90,6 +91,7 @@ class Test_cloudfront_distributions_using_deprecated_ssl_protocols: ], ) ], + origin_failover=False, ) } @@ -119,7 +121,7 @@ class Test_cloudfront_distributions_using_deprecated_ssl_protocols: def test_one_distribution_using_SSL_and_bad_TLS(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -134,6 +136,7 @@ class Test_cloudfront_distributions_using_deprecated_ssl_protocols: ], ) ], + origin_failover=False, ) } @@ -163,7 +166,7 @@ class Test_cloudfront_distributions_using_deprecated_ssl_protocols: def test_one_distribution_not_using_deprecated_ssl_protocols(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, @@ -177,6 +180,7 @@ class Test_cloudfront_distributions_using_deprecated_ssl_protocols: ], ) ], + origin_failover=False, ) } diff --git a/tests/providers/aws/services/cloudfront/cloudfront_distributions_using_waf/cloudfront_distributions_using_waf_test.py b/tests/providers/aws/services/cloudfront/cloudfront_distributions_using_waf/cloudfront_distributions_using_waf_test.py index c73fcc830a..a901722f54 100644 --- a/tests/providers/aws/services/cloudfront/cloudfront_distributions_using_waf/cloudfront_distributions_using_waf_test.py +++ b/tests/providers/aws/services/cloudfront/cloudfront_distributions_using_waf/cloudfront_distributions_using_waf_test.py @@ -32,12 +32,13 @@ class Test_cloudfront_distributions_using_waf: wef_acl_id = "TEST-WAF-ACL" cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, web_acl_id=wef_acl_id, origins=[], + origin_failover=False, ) } @@ -67,11 +68,12 @@ class Test_cloudfront_distributions_using_waf: def test_one_distribution_no_waf(self): cloudfront_client = mock.MagicMock cloudfront_client.distributions = { - "DISTRIBUTION_ID": Distribution( + DISTRIBUTION_ID: Distribution( arn=DISTRIBUTION_ARN, id=DISTRIBUTION_ID, region=REGION, origins=[], + origin_failover=False, ) } diff --git a/tests/providers/aws/services/cloudfront/cloudfront_service_test.py b/tests/providers/aws/services/cloudfront/cloudfront_service_test.py index fdf871a87b..2af8dead03 100644 --- a/tests/providers/aws/services/cloudfront/cloudfront_service_test.py +++ b/tests/providers/aws/services/cloudfront/cloudfront_service_test.py @@ -1,12 +1,16 @@ +from unittest import mock from unittest.mock import patch import botocore -from boto3 import client from moto import mock_aws from prowler.providers.aws.services.cloudfront.cloudfront_service import ( CloudFront, + DefaultCacheConfigBehaviour, + Distribution, GeoRestrictionType, + Origin, + SSLSupportMethod, ViewerProtocolPolicy, ) from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider @@ -23,6 +27,28 @@ def example_distribution_config(ref): "Id": "origin1", "DomainName": "asdf.s3.us-east-1.amazonaws.com", "S3OriginConfig": {"OriginAccessIdentity": ""}, + }, + ], + }, + "OriginGroups": { + "Quantity": 1, + "Items": [ + { + "Id": "origin-group1", + "FailoverCriteria": { + "StatusCodes": {"Quantity": 1, "Items": [500]} + }, + "Members": { + "Quantity": 2, + "Items": [ + { + "OriginId": "origin1", + }, + { + "OriginId": "origin2", + }, + ], + }, } ], }, @@ -35,6 +61,10 @@ def example_distribution_config(ref): "Cookies": {"Forward": "none"}, }, }, + "ViewerCertificate": { + "SSLSupportMethod": "static-ip", + "Certificate": "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012", + }, "Comment": "an optional comment that's not actually optional", "Enabled": False, } @@ -171,63 +201,86 @@ class Test_CloudFront_Service: assert len(cloudfront.distributions) == 0 - @mock_aws def test_list_distributionscomplete(self): - cloudfront_client = client("cloudfront") - config = example_distribution_config("ref") - response = cloudfront_client.create_distribution(DistributionConfig=config) - cloudfront_distribution_id = response["Distribution"]["Id"] - cloudfront_distribution_arn = response["Distribution"]["ARN"] - cloudfront = CloudFront(set_mocked_aws_provider()) + from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER + + DISTRIBUTION_ID = "E27LVI50CSW06W" + DISTRIBUTION_ARN = ( + f"arn:aws:cloudfront::{AWS_ACCOUNT_NUMBER}:distribution/{DISTRIBUTION_ID}" + ) + REGION = "us-east-1" + LOGGING_ENABLED = True + ORIGINS = [ + Origin( + id="origin1", + domain_name="asdf.s3.us-east-1.amazonaws.com", + origin_protocol_policy="", + origin_ssl_protocols=[], + ), + ] + DEFAULT_CACHE_CONFIG = DefaultCacheConfigBehaviour( + realtime_log_config_arn="test-log-arn", + viewer_protocol_policy=ViewerProtocolPolicy.https_only, + field_level_encryption_id="enabled", + ) + GEO_RESTRICTION_TYPE = GeoRestrictionType.blacklist + WEB_ACL_ID = "test-web-acl" + TAGS = [ + {"Key": "test", "Value": "test"}, + ] + SSL_SUPPORT_METHOD = SSLSupportMethod.sni_only + CERTIFICATE = "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012" + + cloudfront = mock.MagicMock + cloudfront.distributions = { + DISTRIBUTION_ID: Distribution( + arn=DISTRIBUTION_ARN, + id=DISTRIBUTION_ID, + region=REGION, + logging_enabled=LOGGING_ENABLED, + origins=ORIGINS, + default_cache_config=DEFAULT_CACHE_CONFIG, + geo_restriction_type=GEO_RESTRICTION_TYPE, + web_acl_id=WEB_ACL_ID, + tags=TAGS, + ssl_support_method=SSL_SUPPORT_METHOD, + certificate=CERTIFICATE, + ) + } assert len(cloudfront.distributions) == 1 + assert cloudfront.distributions[DISTRIBUTION_ID].arn == DISTRIBUTION_ARN + assert cloudfront.distributions[DISTRIBUTION_ID].id == DISTRIBUTION_ID + assert cloudfront.distributions[DISTRIBUTION_ID].region == AWS_REGION_US_EAST_1 assert ( - cloudfront.distributions[cloudfront_distribution_id].arn - == cloudfront_distribution_arn + cloudfront.distributions[DISTRIBUTION_ID].logging_enabled is LOGGING_ENABLED ) - assert ( - cloudfront.distributions[cloudfront_distribution_id].id - == cloudfront_distribution_id - ) - assert ( - cloudfront.distributions[cloudfront_distribution_id].region - == AWS_REGION_US_EAST_1 - ) - assert ( - cloudfront.distributions[cloudfront_distribution_id].logging_enabled is True - ) - for origin in cloudfront.distributions[cloudfront_distribution_id].origins: + for origin in cloudfront.distributions[DISTRIBUTION_ID].origins: assert origin.id == "origin1" assert origin.domain_name == "asdf.s3.us-east-1.amazonaws.com" assert origin.origin_protocol_policy == "" assert origin.origin_ssl_protocols == [] assert ( - cloudfront.distributions[cloudfront_distribution_id].geo_restriction_type - == GeoRestrictionType.blacklist - ) - assert ( - cloudfront.distributions[cloudfront_distribution_id].web_acl_id - == "test-web-acl" + cloudfront.distributions[DISTRIBUTION_ID].geo_restriction_type + == GEO_RESTRICTION_TYPE ) + assert cloudfront.distributions[DISTRIBUTION_ID].web_acl_id == "test-web-acl" assert ( cloudfront.distributions[ - cloudfront_distribution_id + DISTRIBUTION_ID ].default_cache_config.realtime_log_config_arn - == "test-log-arn" + == DEFAULT_CACHE_CONFIG.realtime_log_config_arn ) assert ( cloudfront.distributions[ - cloudfront_distribution_id + DISTRIBUTION_ID ].default_cache_config.viewer_protocol_policy - == ViewerProtocolPolicy.https_only + == DEFAULT_CACHE_CONFIG.viewer_protocol_policy ) assert ( cloudfront.distributions[ - cloudfront_distribution_id + DISTRIBUTION_ID ].default_cache_config.field_level_encryption_id - == "enabled" + == DEFAULT_CACHE_CONFIG.field_level_encryption_id ) - - assert cloudfront.distributions[cloudfront_distribution_id].tags == [ - {"Key": "test", "Value": "test"}, - ] + assert cloudfront.distributions[DISTRIBUTION_ID].tags == TAGS diff --git a/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration_test.py b/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration_test.py index e13fa3f736..67399dff35 100644 --- a/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration_test.py +++ b/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_enumeration/cloudtrail_threat_detection_enumeration_test.py @@ -19,10 +19,10 @@ def mock_get_trail_arn_template(region=None, *_) -> str: def mock__get_lookup_events__(trail=None, event_name=None, minutes=None, *_) -> list: return [ { - "CloudTrailEvent": '{"sourceIPAddress": "172.28.7.0/24", "eventName": "DescribeAccessEntry"}' + "CloudTrailEvent": '{"eventName": "DescribeAccessEntry", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Mateo", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Mateo", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' }, { - "CloudTrailEvent": '{"sourceIPAddress": "172.28.7.0/24", "eventName": "DescribeAccountAttributes"}' + "CloudTrailEvent": '{"eventName": "DescribeAccountAttributes", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Mateo", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Mateo", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' }, ] @@ -154,7 +154,7 @@ class Test_cloudtrail_threat_detection_enumeration: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Potential enumeration attack detected from source IP 172.28.7.0/24 with an threshold of 1.0." + == "Potential enumeration attack detected from AWS IAMUser Mateo with an threshold of 1.0." ) assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert result[0].region == AWS_REGION_US_EAST_1 diff --git a/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation_test.py b/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation_test.py index a39deff5ef..fcf4a90965 100644 --- a/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation_test.py +++ b/tests/providers/aws/services/cloudtrail/cloudtrail_threat_detection_privilege_escalation/cloudtrail_threat_detection_privilege_escalation_test.py @@ -19,10 +19,10 @@ def mock_get_trail_arn_template(region=None, *_) -> str: def mock__get_lookup_events__(trail=None, event_name=None, minutes=None, *_) -> list: return [ { - "CloudTrailEvent": '{"sourceIPAddress": "172.28.7.0/24", "eventName": "CreateLoginProfile"}' + "CloudTrailEvent": '{"eventName": "CreateLoginProfile", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Mateo", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Mateo", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' }, { - "CloudTrailEvent": '{"sourceIPAddress": "172.28.7.0/24", "eventName": "UpdateLoginProfile"}' + "CloudTrailEvent": '{"eventName": "UpdateLoginProfile", "userIdentity": {"type": "IAMUser", "principalId": "EXAMPLE6E4XEGITWATV6R", "arn": "arn:aws:iam::123456789012:user/Mateo", "accountId": "123456789012", "accessKeyId": "AKIAIOSFODNN7EXAMPLE", "userName": "Mateo", "sessionContext": {"sessionIssuer": {}, "webIdFederationData": {}, "attributes": {"creationDate": "2023-07-19T21:11:57Z", "mfaAuthenticated": "false"}}}}' }, ] @@ -153,7 +153,7 @@ class Test_cloudtrail_threat_detection_privilege_escalation: assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Potential privilege escalation attack detected from source IP 172.28.7.0/24 with an threshold of 1.0." + == "Potential privilege escalation attack detected from AWS IAMUser Mateo with an threshold of 1.0." ) assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert result[0].region == AWS_REGION_US_EAST_1 diff --git a/tests/providers/aws/services/codebuild/codebuild_project_no_secrets_in_variables/codebuild_project_no_secrets_in_variables_test.py b/tests/providers/aws/services/codebuild/codebuild_project_no_secrets_in_variables/codebuild_project_no_secrets_in_variables_test.py index 7d99d95861..c719626296 100644 --- a/tests/providers/aws/services/codebuild/codebuild_project_no_secrets_in_variables/codebuild_project_no_secrets_in_variables_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_project_no_secrets_in_variables/codebuild_project_no_secrets_in_variables_test.py @@ -41,6 +41,7 @@ class Test_codebuild_project_no_secrets_in_variables: last_invoked_time=None, buildspec=None, environment_variables=[], + tags=[], ) } @@ -69,6 +70,7 @@ class Test_codebuild_project_no_secrets_in_variables: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == "SensitiveProject" assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] def test_project_with_no_plaintext_credentials(self): codebuild_client = mock.MagicMock @@ -90,6 +92,7 @@ class Test_codebuild_project_no_secrets_in_variables: "type": "PARAMETER_STORE", } ], + tags=[], ) } @@ -118,6 +121,7 @@ class Test_codebuild_project_no_secrets_in_variables: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == "SensitiveProject" assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] def test_project_with_plaintext_credentials_but_not_sensitive(self): codebuild_client = mock.MagicMock @@ -139,6 +143,7 @@ class Test_codebuild_project_no_secrets_in_variables: "type": "PLAINTEXT", } ], + tags=[], ) } @@ -167,6 +172,7 @@ class Test_codebuild_project_no_secrets_in_variables: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == "SensitiveProject" assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] def test_project_with_sensitive_plaintext_credentials(self): codebuild_client = mock.MagicMock @@ -188,6 +194,7 @@ class Test_codebuild_project_no_secrets_in_variables: "type": "PLAINTEXT", } ], + tags=[], ) } @@ -216,6 +223,7 @@ class Test_codebuild_project_no_secrets_in_variables: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == "SensitiveProject" assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] def test_project_with_sensitive_plaintext_credentials_exluded(self): codebuild_client = mock.MagicMock @@ -237,6 +245,7 @@ class Test_codebuild_project_no_secrets_in_variables: "type": "PLAINTEXT", } ], + tags=[], ) } @@ -267,6 +276,7 @@ class Test_codebuild_project_no_secrets_in_variables: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == "SensitiveProject" assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] def test_project_with_sensitive_plaintext_credentials_excluded_and_not(self): codebuild_client = mock.MagicMock() @@ -293,6 +303,7 @@ class Test_codebuild_project_no_secrets_in_variables: "type": "PARAMETER_STORE", }, ], + tags=[], ) } @@ -323,6 +334,7 @@ class Test_codebuild_project_no_secrets_in_variables: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == "SensitiveProject" assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] def test_project_with_sensitive_plaintext_credentials_excluded_and_failed(self): codebuild_client = mock.MagicMock() @@ -349,6 +361,7 @@ class Test_codebuild_project_no_secrets_in_variables: "type": "PLAINTEXT", }, ], + tags=[], ) } @@ -379,6 +392,7 @@ class Test_codebuild_project_no_secrets_in_variables: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == "SensitiveProject" assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] def test_project_with_multiple_sensitive_credentials(self): codebuild_client = mock.MagicMock() @@ -405,6 +419,7 @@ class Test_codebuild_project_no_secrets_in_variables: "type": "PLAINTEXT", }, ], + tags=[], ) } @@ -433,3 +448,4 @@ class Test_codebuild_project_no_secrets_in_variables: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == "SensitiveProject" assert result[0].resource_arn == project_arn + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days_test.py b/tests/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days_test.py index 236c0820ae..c89f163e6f 100644 --- a/tests/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_project_older_90_days/codebuild_project_older_90_days_test.py @@ -20,6 +20,7 @@ class Test_codebuild_project_older_90_days: region="eu-west-1", last_invoked_time=datetime.now(timezone.utc) - timedelta(days=100), buildspec=None, + tags=[], ) } @@ -55,6 +56,7 @@ class Test_codebuild_project_older_90_days: region="eu-west-1", last_invoked_time=None, buildspec=None, + tags=[], ) } @@ -88,6 +90,7 @@ class Test_codebuild_project_older_90_days: region="eu-west-1", last_invoked_time=datetime.now(timezone.utc) - timedelta(days=10), buildspec=None, + tags=[], ) } diff --git a/tests/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials_test.py b/tests/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials_test.py index b3e7f1b967..c99e9ecf5c 100644 --- a/tests/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_project_source_repo_url_no_sensitive_credentials/codebuild_project_source_repo_url_no_sensitive_credentials_test.py @@ -20,6 +20,7 @@ class Test_codebuild_project_source_repo_url_no_sensitive_credentials: buildspec="", source=None, secondary_sources=[], + tags=[], ) } with mock.patch( @@ -60,6 +61,7 @@ class Test_codebuild_project_source_repo_url_no_sensitive_credentials: location="https://bitbucket.org/exampleuser/my-repo.git", ), secondary_sources=[], + tags=[], ) } with mock.patch( @@ -100,6 +102,7 @@ class Test_codebuild_project_source_repo_url_no_sensitive_credentials: location="https://user:pass123@bitbucket.org/exampleuser/my-repo2.git", ), secondary_sources=[], + tags=[], ) } with mock.patch( @@ -140,6 +143,7 @@ class Test_codebuild_project_source_repo_url_no_sensitive_credentials: location="https://x-token-auth:7saBEbfXpRg-zlO-YQC9Lvh8vtKmdETITD_-GCqYw0ZHbV7ZbMDbUCybDGM4=053EA782@bitbucket.org/testissue4244/test4244.git", ), secondary_sources=[], + tags=[], ) } with mock.patch( diff --git a/tests/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec_test.py b/tests/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec_test.py index bd213e29bd..61021d704e 100644 --- a/tests/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_project_user_controlled_buildspec/codebuild_project_user_controlled_buildspec_test.py @@ -19,6 +19,7 @@ class Test_codebuild_project_user_controlled_buildspec: region="eu-west-1", last_invoked_time=None, buildspec=None, + tags=[], ) } with mock.patch( @@ -54,6 +55,7 @@ class Test_codebuild_project_user_controlled_buildspec: region="eu-west-1", last_invoked_time=None, buildspec="arn:aws:s3:::my-codebuild-sample2/buildspec.out", + tags=[], ) } @@ -90,6 +92,7 @@ class Test_codebuild_project_user_controlled_buildspec: region="eu-west-1", last_invoked_time=None, buildspec="arn:aws:s3:::my-codebuild-sample2/buildspec.yaml", + tags=[], ) } with mock.patch( @@ -124,6 +127,7 @@ class Test_codebuild_project_user_controlled_buildspec: region="eu-west-1", last_invoked_time=None, buildspec="arn:aws:s3:::my-codebuild-sample2/buildspecyaml", + tags=[], ) } with mock.patch( diff --git a/tests/providers/aws/services/codebuild/codebuild_service_test.py b/tests/providers/aws/services/codebuild/codebuild_service_test.py index 0741f180c1..0ea15166eb 100644 --- a/tests/providers/aws/services/codebuild/codebuild_service_test.py +++ b/tests/providers/aws/services/codebuild/codebuild_service_test.py @@ -51,6 +51,7 @@ def mock_make_api_call(self, operation_name, kwarg): "buildspec": "", } ], + "tags": [{"key": "Name", "value": project_name}], } ] } @@ -66,13 +67,12 @@ def mock_generate_regional_clients(provider, service): return {AWS_REGION_EU_WEST_1: regional_client} -@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) -@patch( - "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", - new=mock_generate_regional_clients, -) class Test_Codebuild_Service: - # Test Codebuild Session + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + @patch( + "prowler.providers.aws.aws_provider.AwsProvider.generate_regional_clients", + new=mock_generate_regional_clients, + ) def test_codebuild_service(self): codebuild = Codebuild(set_mocked_aws_provider()) @@ -93,3 +93,5 @@ class Test_Codebuild_Service: secondary_bitbucket_url in codebuild.projects[project_arn].secondary_sources[0].location ) + assert codebuild.projects[project_arn].tags[0]["key"] == "Name" + assert codebuild.projects[project_arn].tags[0]["value"] == project_name diff --git a/tests/providers/aws/services/dms/dms_endpoint_ssl_enabled_test/dms_endpoint_ssl_enabled_test.py b/tests/providers/aws/services/dms/dms_endpoint_ssl_enabled_test/dms_endpoint_ssl_enabled_test.py new file mode 100644 index 0000000000..4d4f087fce --- /dev/null +++ b/tests/providers/aws/services/dms/dms_endpoint_ssl_enabled_test/dms_endpoint_ssl_enabled_test.py @@ -0,0 +1,185 @@ +from unittest import mock + +from prowler.providers.aws.services.dms.dms_service import Endpoint +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + + +class Test_dms_endpoint_ssl_enabled: + def test_dms_no_endpoints(self): + dms_client = mock.MagicMock + dms_client.endpoints = {} + + with mock.patch( + "prowler.providers.aws.services.dms.dms_service.DMS", + new=dms_client, + ): + from prowler.providers.aws.services.dms.dms_endpoint_ssl_enabled.dms_endpoint_ssl_enabled import ( + dms_endpoint_ssl_enabled, + ) + + check = dms_endpoint_ssl_enabled() + result = check.execute() + assert len(result) == 0 + + def test_dms_endpoint_ssl_none(self): + dms_client = mock.MagicMock + endpoint_arn = f"arn:aws:dms:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:endpoint:test-endpoint-no-ssl" + dms_client.endpoints = { + endpoint_arn: Endpoint( + arn=endpoint_arn, + id="test-endpoint-no-ssl", + region=AWS_REGION_US_EAST_1, + ssl_mode="none", + tags=[{"Key": "Name", "Value": "test-endpoint-no-ssl"}], + ) + } + dms_client.audited_account = AWS_ACCOUNT_NUMBER + dms_client.audited_partition = "aws" + dms_client.audited_region = AWS_REGION_US_EAST_1 + + with mock.patch( + "prowler.providers.aws.services.dms.dms_service.DMS", + new=dms_client, + ): + from prowler.providers.aws.services.dms.dms_endpoint_ssl_enabled.dms_endpoint_ssl_enabled import ( + dms_endpoint_ssl_enabled, + ) + + check = dms_endpoint_ssl_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].resource_id == "test-endpoint-no-ssl" + assert ( + result[0].resource_arn + == f"arn:aws:dms:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:endpoint:test-endpoint-no-ssl" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].status_extended + == "DMS Endpoint test-endpoint-no-ssl is not using SSL." + ) + assert result[0].resource_tags == [ + {"Key": "Name", "Value": "test-endpoint-no-ssl"} + ] + + def test_dms_endpoint_ssl_require(self): + dms_client = mock.MagicMock + endpoint_arn = f"arn:aws:dms:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:endpoint:test-endpoint-ssl-require" + dms_client.endpoints = { + endpoint_arn: Endpoint( + arn=endpoint_arn, + id="test-endpoint-ssl-require", + region=AWS_REGION_US_EAST_1, + ssl_mode="require", + tags=[{"Key": "Name", "Value": "test-endpoint-ssl-require"}], + ) + } + dms_client.audited_account = AWS_ACCOUNT_NUMBER + dms_client.audited_partition = "aws" + dms_client.audited_region = AWS_REGION_US_EAST_1 + + with mock.patch( + "prowler.providers.aws.services.dms.dms_service.DMS", + new=dms_client, + ): + from prowler.providers.aws.services.dms.dms_endpoint_ssl_enabled.dms_endpoint_ssl_enabled import ( + dms_endpoint_ssl_enabled, + ) + + check = dms_endpoint_ssl_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == "test-endpoint-ssl-require" + assert result[0].resource_arn == endpoint_arn + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].status_extended + == "DMS Endpoint test-endpoint-ssl-require is using SSL with mode: require." + ) + assert result[0].resource_tags == [ + {"Key": "Name", "Value": "test-endpoint-ssl-require"} + ] + + def test_dms_endpoint_ssl_verify_ca(self): + dms_client = mock.MagicMock + endpoint_arn = f"arn:aws:dms:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:endpoint:test-endpoint-ssl-verify-ca" + dms_client.endpoints = { + endpoint_arn: Endpoint( + arn=endpoint_arn, + id="test-endpoint-ssl-verify-ca", + region=AWS_REGION_US_EAST_1, + ssl_mode="verify-ca", + tags=[{"Key": "Name", "Value": "test-endpoint-ssl-verify-ca"}], + ) + } + dms_client.audited_account = AWS_ACCOUNT_NUMBER + dms_client.audited_partition = "aws" + dms_client.audited_region = AWS_REGION_US_EAST_1 + + with mock.patch( + "prowler.providers.aws.services.dms.dms_service.DMS", + new=dms_client, + ): + from prowler.providers.aws.services.dms.dms_endpoint_ssl_enabled.dms_endpoint_ssl_enabled import ( + dms_endpoint_ssl_enabled, + ) + + check = dms_endpoint_ssl_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == "test-endpoint-ssl-verify-ca" + assert result[0].resource_arn == endpoint_arn + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].status_extended + == "DMS Endpoint test-endpoint-ssl-verify-ca is using SSL with mode: verify-ca." + ) + assert result[0].resource_tags == [ + {"Key": "Name", "Value": "test-endpoint-ssl-verify-ca"} + ] + + def test_dms_endpoint_ssl_verify_full(self): + dms_client = mock.MagicMock + endpoint_arn = f"arn:aws:dms:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:endpoint:test-endpoint-ssl-verify-full" + dms_client.endpoints = { + endpoint_arn: Endpoint( + arn=endpoint_arn, + id="test-endpoint-ssl-verify-full", + region=AWS_REGION_US_EAST_1, + ssl_mode="verify-full", + tags=[{"Key": "Name", "Value": "test-endpoint-ssl-verify-full"}], + ) + } + dms_client.audited_account = AWS_ACCOUNT_NUMBER + dms_client.audited_partition = "aws" + dms_client.audited_region = AWS_REGION_US_EAST_1 + + with mock.patch( + "prowler.providers.aws.services.dms.dms_service.DMS", + new=dms_client, + ): + from prowler.providers.aws.services.dms.dms_endpoint_ssl_enabled.dms_endpoint_ssl_enabled import ( + dms_endpoint_ssl_enabled, + ) + + check = dms_endpoint_ssl_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == "test-endpoint-ssl-verify-full" + assert result[0].resource_arn == endpoint_arn + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].status_extended + == "DMS Endpoint test-endpoint-ssl-verify-full is using SSL with mode: verify-full." + ) + assert result[0].resource_tags == [ + {"Key": "Name", "Value": "test-endpoint-ssl-verify-full"} + ] diff --git a/tests/providers/aws/services/dms/dms_instance_minor_version_upgrade_enabled/dms_instance_minor_version_upgrade_enabled_test.py b/tests/providers/aws/services/dms/dms_instance_minor_version_upgrade_enabled/dms_instance_minor_version_upgrade_enabled_test.py index 1bc9710078..1022a54bbd 100644 --- a/tests/providers/aws/services/dms/dms_instance_minor_version_upgrade_enabled/dms_instance_minor_version_upgrade_enabled_test.py +++ b/tests/providers/aws/services/dms/dms_instance_minor_version_upgrade_enabled/dms_instance_minor_version_upgrade_enabled_test.py @@ -40,6 +40,7 @@ class Test_dms_instance_minor_version_upgrade_enabled: auto_minor_version_upgrade=False, multi_az=True, region=AWS_REGION_US_EAST_1, + tags=[{"Key": "Name", "Value": DMS_INSTANCE_NAME}], ) ) @@ -62,6 +63,9 @@ class Test_dms_instance_minor_version_upgrade_enabled: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == DMS_INSTANCE_NAME assert result[0].resource_arn == DMS_INSTANCE_ARN + assert result[0].resource_tags == [ + {"Key": "Name", "Value": DMS_INSTANCE_NAME} + ] def test_dms_instance_minor_version_upgrade_enabled(self): dms_client = mock.MagicMock @@ -76,6 +80,7 @@ class Test_dms_instance_minor_version_upgrade_enabled: auto_minor_version_upgrade=True, multi_az=True, region=AWS_REGION_US_EAST_1, + tags=[{"Key": "Name", "Value": DMS_INSTANCE_NAME}], ) ) @@ -98,3 +103,6 @@ class Test_dms_instance_minor_version_upgrade_enabled: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == DMS_INSTANCE_NAME assert result[0].resource_arn == DMS_INSTANCE_ARN + assert result[0].resource_tags == [ + {"Key": "Name", "Value": DMS_INSTANCE_NAME} + ] diff --git a/tests/providers/aws/services/dms/dms_instance_multi_az/dms_instance_multi_az_test.py b/tests/providers/aws/services/dms/dms_instance_multi_az/dms_instance_multi_az_test.py index 3fd5eb6022..3ba0c3befc 100644 --- a/tests/providers/aws/services/dms/dms_instance_multi_az/dms_instance_multi_az_test.py +++ b/tests/providers/aws/services/dms/dms_instance_multi_az/dms_instance_multi_az_test.py @@ -40,6 +40,7 @@ class Test_dms_instance_multi_az: auto_minor_version_upgrade=False, multi_az=False, region=AWS_REGION_US_EAST_1, + tags=[{"Key": "Name", "Value": DMS_INSTANCE_NAME}], ) ) @@ -62,6 +63,9 @@ class Test_dms_instance_multi_az: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == DMS_INSTANCE_NAME assert result[0].resource_arn == DMS_INSTANCE_ARN + assert result[0].resource_tags == [ + {"Key": "Name", "Value": DMS_INSTANCE_NAME} + ] def test_dms_instance_multi_az_enabled(self): dms_client = mock.MagicMock @@ -76,6 +80,7 @@ class Test_dms_instance_multi_az: auto_minor_version_upgrade=True, multi_az=True, region=AWS_REGION_US_EAST_1, + tags=[{"Key": "Name", "Value": DMS_INSTANCE_NAME}], ) ) @@ -98,3 +103,6 @@ class Test_dms_instance_multi_az: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == DMS_INSTANCE_NAME assert result[0].resource_arn == DMS_INSTANCE_ARN + assert result[0].resource_tags == [ + {"Key": "Name", "Value": DMS_INSTANCE_NAME} + ] diff --git a/tests/providers/aws/services/dms/dms_instance_no_public_access/dms_no_public_access_test.py b/tests/providers/aws/services/dms/dms_instance_no_public_access/dms_no_public_access_test.py index 0b5a447fdc..82e54bbda3 100644 --- a/tests/providers/aws/services/dms/dms_instance_no_public_access/dms_no_public_access_test.py +++ b/tests/providers/aws/services/dms/dms_instance_no_public_access/dms_no_public_access_test.py @@ -45,7 +45,6 @@ class Test_dms_instance_no_public_access: result = check.execute() assert len(result) == 0 - @mock_aws def test_dms_private(self): dms_client = mock.MagicMock dms_client.instances = [] @@ -60,6 +59,7 @@ class Test_dms_instance_no_public_access: auto_minor_version_upgrade=False, multi_az=False, region=AWS_REGION_US_EAST_1, + tags=[{"Key": "Name", "Value": DMS_INSTANCE_NAME}], ) ) @@ -88,8 +88,13 @@ class Test_dms_instance_no_public_access: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == DMS_INSTANCE_NAME assert result[0].resource_arn == DMS_INSTANCE_ARN + assert result[0].resource_tags == [ + { + "Key": "Name", + "Value": DMS_INSTANCE_NAME, + } + ] - @mock_aws def test_dms_public(self): dms_client = mock.MagicMock dms_client.instances = [] @@ -104,6 +109,7 @@ class Test_dms_instance_no_public_access: auto_minor_version_upgrade=False, multi_az=False, region=AWS_REGION_US_EAST_1, + tags=[{"Key": "Name", "Value": DMS_INSTANCE_NAME}], ) ) @@ -132,6 +138,12 @@ class Test_dms_instance_no_public_access: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == DMS_INSTANCE_NAME assert result[0].resource_arn == DMS_INSTANCE_ARN + assert result[0].resource_tags == [ + { + "Key": "Name", + "Value": DMS_INSTANCE_NAME, + } + ] @mock_aws def test_dms_public_with_public_sg(self): @@ -163,6 +175,7 @@ class Test_dms_instance_no_public_access: auto_minor_version_upgrade=False, multi_az=False, region=AWS_REGION_US_EAST_1, + tags=[{"Key": "Name", "Value": DMS_INSTANCE_NAME}], ) ) @@ -201,6 +214,12 @@ class Test_dms_instance_no_public_access: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == DMS_INSTANCE_NAME assert result[0].resource_arn == DMS_INSTANCE_ARN + assert result[0].resource_tags == [ + { + "Key": "Name", + "Value": DMS_INSTANCE_NAME, + } + ] @mock_aws def test_dms_public_with_filtered_sg(self): @@ -232,6 +251,7 @@ class Test_dms_instance_no_public_access: auto_minor_version_upgrade=False, multi_az=False, region=AWS_REGION_US_EAST_1, + tags=[{"Key": "Name", "Value": DMS_INSTANCE_NAME}], ) ) @@ -270,3 +290,9 @@ class Test_dms_instance_no_public_access: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == DMS_INSTANCE_NAME assert result[0].resource_arn == DMS_INSTANCE_ARN + assert result[0].resource_tags == [ + { + "Key": "Name", + "Value": DMS_INSTANCE_NAME, + } + ] diff --git a/tests/providers/aws/services/dms/dms_service_test.py b/tests/providers/aws/services/dms/dms_service_test.py index 35d0f09f31..7a13e8a872 100644 --- a/tests/providers/aws/services/dms/dms_service_test.py +++ b/tests/providers/aws/services/dms/dms_service_test.py @@ -14,6 +14,9 @@ DMS_INSTANCE_ARN = ( ) KMS_KEY_ID = f"arn:aws:kms:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:key/abcdabcd-1234-abcd-1234-abcdabcdabcd" +DMS_ENDPOINT_NAME = "dms-endpoint" +DMS_ENDPOINT_ARN = f"arn:aws:dms:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:endpoint:{DMS_ENDPOINT_NAME}" + # Mocking Access Analyzer Calls make_api_call = botocore.client.BaseClient._make_api_call @@ -34,6 +37,31 @@ def mock_make_api_call(self, operation_name, kwargs): }, ] } + elif operation_name == "DescribeEndpoints": + return { + "Endpoints": [ + { + "EndpointIdentifier": DMS_ENDPOINT_NAME, + "EndpointArn": DMS_ENDPOINT_ARN, + "SslMode": "require", + } + ] + } + elif operation_name == "ListTagsForResource": + if kwargs["ResourceArn"] == DMS_INSTANCE_ARN: + return { + "TagList": [ + {"Key": "Name", "Value": "rep-instance"}, + {"Key": "Owner", "Value": "admin"}, + ] + } + elif kwargs["ResourceArn"] == DMS_ENDPOINT_ARN: + return { + "TagList": [ + {"Key": "Name", "Value": "dms-endpoint"}, + {"Key": "Owner", "Value": "admin"}, + ] + } return make_api_call(self, operation_name, kwargs) @@ -84,3 +112,25 @@ class Test_DMS_Service: assert dms.instances[0].auto_minor_version_upgrade assert dms.instances[0].multi_az assert dms.instances[0].security_groups == [] + + # Test DMS Endpoints + def test_describe_endpoints(self): + aws_provider = set_mocked_aws_provider() + dms = DMS(aws_provider) + + assert len(dms.endpoints) == 1 + assert dms.endpoints[DMS_ENDPOINT_ARN].id == DMS_ENDPOINT_NAME + assert dms.endpoints[DMS_ENDPOINT_ARN].ssl_mode == "require" + + def test_list_tags(self): + aws_provider = set_mocked_aws_provider() + dms = DMS(aws_provider) + + assert dms.instances[0].tags == [ + {"Key": "Name", "Value": "rep-instance"}, + {"Key": "Owner", "Value": "admin"}, + ] + assert dms.endpoints[DMS_ENDPOINT_ARN].tags == [ + {"Key": "Name", "Value": "dms-endpoint"}, + {"Key": "Owner", "Value": "admin"}, + ] diff --git a/tests/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled_test.py b/tests/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled_test.py new file mode 100644 index 0000000000..512ce2b93d --- /dev/null +++ b/tests/providers/aws/services/dynamodb/dynamodb_accelerator_cluster_in_transit_encryption_enabled/dynamodb_accelerator_cluster_in_transit_encryption_enabled_test.py @@ -0,0 +1,123 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_dynamodb_accelerator_cluster_in_transit_encryption_enabled: + @mock_aws + def test_dax_no_clusters(self): + from prowler.providers.aws.services.dynamodb.dynamodb_service import DAX + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_accelerator_cluster_in_transit_encryption_enabled.dynamodb_accelerator_cluster_in_transit_encryption_enabled.dax_client", + new=DAX(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_accelerator_cluster_in_transit_encryption_enabled.dynamodb_accelerator_cluster_in_transit_encryption_enabled import ( + dynamodb_accelerator_cluster_in_transit_encryption_enabled, + ) + + check = dynamodb_accelerator_cluster_in_transit_encryption_enabled() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_dax_cluster_no_encryption(self): + dax_client = client("dax", region_name=AWS_REGION_US_EAST_1) + iam_role_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/aws-service-role/dax.amazonaws.com/AWSServiceRoleForDAX" + cluster = dax_client.create_cluster( + ClusterName="daxcluster", + NodeType="dax.t3.small", + ReplicationFactor=3, + IamRoleArn=iam_role_arn, + )["Cluster"] + from prowler.providers.aws.services.dynamodb.dynamodb_service import DAX + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_accelerator_cluster_in_transit_encryption_enabled.dynamodb_accelerator_cluster_in_transit_encryption_enabled.dax_client", + new=DAX(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_accelerator_cluster_in_transit_encryption_enabled.dynamodb_accelerator_cluster_in_transit_encryption_enabled import ( + dynamodb_accelerator_cluster_in_transit_encryption_enabled, + ) + + check = dynamodb_accelerator_cluster_in_transit_encryption_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "DAX cluster daxcluster does not have encryption in transit enabled." + ) + assert result[0].resource_id == cluster["ClusterName"] + assert result[0].resource_arn == cluster["ClusterArn"] + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + @mock_aws + def test_dax_cluster_with_encryption(self): + dax_client = client("dax", region_name=AWS_REGION_US_EAST_1) + iam_role_arn = f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:role/aws-service-role/dax.amazonaws.com/AWSServiceRoleForDAX" + cluster = dax_client.create_cluster( + ClusterName="daxcluster", + NodeType="dax.t3.small", + ReplicationFactor=3, + IamRoleArn=iam_role_arn, + ClusterEndpointEncryptionType="TLS", + )["Cluster"] + from prowler.providers.aws.services.dynamodb.dynamodb_service import DAX + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_accelerator_cluster_in_transit_encryption_enabled.dynamodb_accelerator_cluster_in_transit_encryption_enabled.dax_client", + new=DAX(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_accelerator_cluster_in_transit_encryption_enabled.dynamodb_accelerator_cluster_in_transit_encryption_enabled import ( + dynamodb_accelerator_cluster_in_transit_encryption_enabled, + ) + + check = dynamodb_accelerator_cluster_in_transit_encryption_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "DAX cluster daxcluster has encryption in transit enabled." + ) + assert result[0].resource_id == cluster["ClusterName"] + assert result[0].resource_arn == cluster["ClusterArn"] + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/dynamodb/dynamodb_service_test.py b/tests/providers/aws/services/dynamodb/dynamodb_service_test.py index 65319cc0e0..48ce9c7648 100644 --- a/tests/providers/aws/services/dynamodb/dynamodb_service_test.py +++ b/tests/providers/aws/services/dynamodb/dynamodb_service_test.py @@ -77,10 +77,11 @@ class Test_DynamoDB_Service: aws_provider = set_mocked_aws_provider() dynamo = DynamoDB(aws_provider) assert len(dynamo.tables) == 2 - assert dynamo.tables[0].name == "test1" - assert dynamo.tables[1].name == "test2" - assert dynamo.tables[0].region == AWS_REGION_US_EAST_1 - assert dynamo.tables[1].region == AWS_REGION_US_EAST_1 + table_names = [table.name for table in dynamo.tables.values()] + assert "test1" in table_names + assert "test2" in table_names + for table in dynamo.tables.values(): + assert table.region == AWS_REGION_US_EAST_1 # Test DynamoDB Describe Table @mock_aws @@ -102,17 +103,20 @@ class Test_DynamoDB_Service: Tags=[ {"Key": "test", "Value": "test"}, ], + DeletionProtectionEnabled=True, )["TableDescription"] # DynamoDB client for this test class aws_provider = set_mocked_aws_provider() dynamo = DynamoDB(aws_provider) assert len(dynamo.tables) == 1 - assert dynamo.tables[0].arn == table["TableArn"] - assert dynamo.tables[0].name == "test1" - assert dynamo.tables[0].region == AWS_REGION_US_EAST_1 - assert dynamo.tables[0].tags == [ + tables_arn, tables = next(iter(dynamo.tables.items())) + assert tables_arn == table["TableArn"] + assert tables.name == "test1" + assert tables.region == AWS_REGION_US_EAST_1 + assert tables.tags == [ {"Key": "test", "Value": "test"}, ] + assert tables.deletion_protection # Test DynamoDB Describe Continuous Backups @mock_aws @@ -140,10 +144,11 @@ class Test_DynamoDB_Service: aws_provider = set_mocked_aws_provider() dynamo = DynamoDB(aws_provider) assert len(dynamo.tables) == 1 - assert dynamo.tables[0].arn == table["TableArn"] - assert dynamo.tables[0].name == "test1" - assert dynamo.tables[0].pitr - assert dynamo.tables[0].region == AWS_REGION_US_EAST_1 + tables_arn, tables = next(iter(dynamo.tables.items())) + assert tables_arn == table["TableArn"] + assert tables.name == "test1" + assert tables.pitr + assert tables.region == AWS_REGION_US_EAST_1 # Test DAX Describe Clusters @mock_aws @@ -161,6 +166,7 @@ class Test_DynamoDB_Service: Tags=[ {"Key": "test", "Value": "test"}, ], + ClusterEndpointEncryptionType="TLS", ) dax_client.create_cluster( ClusterName="daxcluster2", @@ -183,6 +189,7 @@ class Test_DynamoDB_Service: assert dax.clusters[0].tags == [ {"Key": "test", "Value": "test"}, ] + assert dax.clusters[0].tls_encryption assert dax.clusters[1].name == "daxcluster2" assert dax.clusters[1].region == AWS_REGION_US_EAST_1 @@ -190,3 +197,4 @@ class Test_DynamoDB_Service: assert dax.clusters[1].tags == [ {"Key": "test", "Value": "test"}, ] + assert not dax.clusters[1].tls_encryption diff --git a/tests/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access_test.py b/tests/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access_test.py index 55849a8cf4..80b1242223 100644 --- a/tests/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access_test.py +++ b/tests/providers/aws/services/dynamodb/dynamodb_table_cross_account_access/dynamodb_table_cross_account_access_test.py @@ -103,7 +103,7 @@ test_public_policy_with_invalid_condition_block = { class Test_dynamodb_table_cross_account_access: def test_no_tables(self): dynamodb_client = mock.MagicMock - dynamodb_client.tables = [] + dynamodb_client.tables = {} with mock.patch( "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", new=dynamodb_client, @@ -124,14 +124,15 @@ class Test_dynamodb_table_cross_account_access: from prowler.providers.aws.services.dynamodb.dynamodb_service import Table dynamodb_client.audited_account = AWS_ACCOUNT_NUMBER - dynamodb_client.tables = [] - dynamodb_client.tables.append( - Table( - arn=test_table_arn, + arn = test_table_arn + dynamodb_client.tables = { + arn: Table( + arn=arn, name=test_table_name, region=AWS_REGION_EU_WEST_1, ) - ) + } + with mock.patch( "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", new=dynamodb_client, @@ -160,15 +161,15 @@ class Test_dynamodb_table_cross_account_access: from prowler.providers.aws.services.dynamodb.dynamodb_service import Table dynamodb_client.audited_account = AWS_ACCOUNT_NUMBER - dynamodb_client.tables = [] - dynamodb_client.tables.append( - Table( - arn=test_table_arn, + arn = test_table_arn + dynamodb_client.tables = { + arn: Table( + arn=arn, name=test_table_name, region=AWS_REGION_EU_WEST_1, policy=test_restricted_policy, ) - ) + } with mock.patch( "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", new=dynamodb_client, @@ -197,15 +198,16 @@ class Test_dynamodb_table_cross_account_access: from prowler.providers.aws.services.dynamodb.dynamodb_service import Table dynamodb_client.audited_account = AWS_ACCOUNT_NUMBER - dynamodb_client.tables = [] - dynamodb_client.tables.append( - Table( + arn = test_table_arn + dynamodb_client.tables = { + arn: Table( arn=test_table_arn, name=test_table_name, region=AWS_REGION_EU_WEST_1, policy=test_public_policy, ) - ) + } + with mock.patch( "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", new=dynamodb_client, @@ -234,16 +236,17 @@ class Test_dynamodb_table_cross_account_access: dynamodb_client = mock.MagicMock from prowler.providers.aws.services.dynamodb.dynamodb_service import Table - dynamodb_client.tables = [] dynamodb_client.audited_account = AWS_ACCOUNT_NUMBER - dynamodb_client.tables.append( - Table( + arn = test_table_arn + dynamodb_client.tables = { + arn: Table( arn=test_table_arn, name=test_table_name, region=AWS_REGION_EU_WEST_1, policy=test_public_policy_with_condition_same_account_not_valid, ) - ) + } + with mock.patch( "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", new=dynamodb_client, @@ -272,16 +275,17 @@ class Test_dynamodb_table_cross_account_access: dynamodb_client = mock.MagicMock from prowler.providers.aws.services.dynamodb.dynamodb_service import Table - dynamodb_client.tables = [] dynamodb_client.audited_account = AWS_ACCOUNT_NUMBER - dynamodb_client.tables.append( - Table( + arn = test_table_arn + dynamodb_client.tables = { + arn: Table( arn=test_table_arn, name=test_table_name, region=AWS_REGION_EU_WEST_1, policy=test_public_policy_with_condition_same_account, ) - ) + } + with mock.patch( "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", new=dynamodb_client, @@ -309,16 +313,17 @@ class Test_dynamodb_table_cross_account_access: dynamodb_client = mock.MagicMock from prowler.providers.aws.services.dynamodb.dynamodb_service import Table - dynamodb_client.tables = [] dynamodb_client.audited_account = AWS_ACCOUNT_NUMBER - dynamodb_client.tables.append( - Table( + arn = test_table_arn + dynamodb_client.tables = { + arn: Table( arn=test_table_arn, name=test_table_name, region=AWS_REGION_EU_WEST_1, policy=test_public_policy_with_condition_diff_account, ) - ) + } + with mock.patch( "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", new=dynamodb_client, @@ -346,16 +351,17 @@ class Test_dynamodb_table_cross_account_access: dynamodb_client = mock.MagicMock from prowler.providers.aws.services.dynamodb.dynamodb_service import Table - dynamodb_client.tables = [] dynamodb_client.audited_account = AWS_ACCOUNT_NUMBER - dynamodb_client.tables.append( - Table( + arn = test_table_arn + dynamodb_client.tables = { + arn: Table( arn=test_table_arn, name=test_table_name, region=AWS_REGION_EU_WEST_1, policy=test_public_policy_with_invalid_condition_block, ) - ) + } + with mock.patch( "prowler.providers.aws.services.dynamodb.dynamodb_service.DynamoDB", new=dynamodb_client, diff --git a/tests/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled_test.py b/tests/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled_test.py new file mode 100644 index 0000000000..8020816aca --- /dev/null +++ b/tests/providers/aws/services/dynamodb/dynamodb_table_deletion_protection_enabled/dynamodb_table_deletion_protection_enabled_test.py @@ -0,0 +1,133 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_dynamodb_table_deletion_protection_enabled: + @mock_aws + def test_dynamodb_no_tables(self): + from prowler.providers.aws.services.dynamodb.dynamodb_service import DynamoDB + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled.dynamodb_client", + new=DynamoDB(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled import ( + dynamodb_table_deletion_protection_enabled, + ) + + check = dynamodb_table_deletion_protection_enabled() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_dynamodb_table_with_deletion_protection_enabled(self): + dynamodb_client = client("dynamodb", region_name=AWS_REGION_US_EAST_1) + table = dynamodb_client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + DeletionProtectionEnabled=True, + BillingMode="PAY_PER_REQUEST", + )["TableDescription"] + + from prowler.providers.aws.services.dynamodb.dynamodb_service import DynamoDB + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled.dynamodb_client", + new=DynamoDB(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled import ( + dynamodb_table_deletion_protection_enabled, + ) + + check = dynamodb_table_deletion_protection_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + "DynamoDB table test1 has deletion protection enabled." + ) + assert result[0].resource_id == table["TableName"] + assert result[0].resource_arn == table["TableArn"] + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + @mock_aws + def test_dynamodb_table_without_deletion_protection_enabled(self): + dynamodb_client = client("dynamodb", region_name=AWS_REGION_US_EAST_1) + table = dynamodb_client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + DeletionProtectionEnabled=False, + BillingMode="PAY_PER_REQUEST", + )["TableDescription"] + + from prowler.providers.aws.services.dynamodb.dynamodb_service import DynamoDB + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled.dynamodb_client", + new=DynamoDB(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_deletion_protection_enabled.dynamodb_table_deletion_protection_enabled import ( + dynamodb_table_deletion_protection_enabled, + ) + + check = dynamodb_table_deletion_protection_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + "DynamoDB table test1 does not have deletion protection enabled." + ) + assert result[0].resource_id == table["TableName"] + assert result[0].resource_arn == table["TableArn"] + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan_test.py b/tests/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan_test.py new file mode 100644 index 0000000000..d92f5e9bd8 --- /dev/null +++ b/tests/providers/aws/services/dynamodb/dynamodb_table_protected_by_backup_plan/dynamodb_table_protected_by_backup_plan_test.py @@ -0,0 +1,329 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_dynamodb_table_protected_by_backup_plan: + @mock_aws + def test_dynamodb_no_tables(self): + from prowler.providers.aws.services.backup.backup_service import Backup + from prowler.providers.aws.services.dynamodb.dynamodb_service import DynamoDB + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.dynamodb_client", + new=DynamoDB(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.backup_client", + new=Backup(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan import ( + dynamodb_table_protected_by_backup_plan, + ) + + check = dynamodb_table_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_dynamodb_table_no_existing_backup_plans(self): + dynamodb_client = client("dynamodb", region_name=AWS_REGION_US_EAST_1) + table = dynamodb_client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + DeletionProtectionEnabled=True, + BillingMode="PAY_PER_REQUEST", + )["TableDescription"] + + from prowler.providers.aws.services.backup.backup_service import Backup + from prowler.providers.aws.services.dynamodb.dynamodb_service import DynamoDB + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.dynamodb_client", + new=DynamoDB(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.backup_client", + new=Backup(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan import ( + dynamodb_table_protected_by_backup_plan, + ) + + check = dynamodb_table_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "DynamoDB table test1 is not protected by a backup plan." + ) + assert result[0].resource_id == table["TableName"] + assert result[0].resource_arn == table["TableArn"] + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + @mock_aws + def test_dynamodb_table_without_backup_plan(self): + backup = client("backup", region_name=AWS_REGION_US_EAST_1) + dynamodb_client = client("dynamodb", region_name=AWS_REGION_US_EAST_1) + table = dynamodb_client.create_table( + TableName="test1", + AttributeDefinitions=[ + {"AttributeName": "client", "AttributeType": "S"}, + {"AttributeName": "app", "AttributeType": "S"}, + ], + KeySchema=[ + {"AttributeName": "client", "KeyType": "HASH"}, + {"AttributeName": "app", "KeyType": "RANGE"}, + ], + DeletionProtectionEnabled=True, + BillingMode="PAY_PER_REQUEST", + )["TableDescription"] + + backup.create_backup_plan( + BackupPlan={ + "BackupPlanName": "test-backup-plan", + "Rules": [ + { + "RuleName": "DailyBackup", + "TargetBackupVaultName": "test-vault", + "ScheduleExpression": "cron(0 12 * * ? *)", + "Lifecycle": {"DeleteAfterDays": 30}, + "RecoveryPointTags": { + "Type": "Daily", + }, + }, + ], + } + ) + + from prowler.providers.aws.services.backup.backup_service import Backup + from prowler.providers.aws.services.dynamodb.dynamodb_service import DynamoDB + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.dynamodb_client", + new=DynamoDB(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.backup_client", + new=Backup(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan import ( + dynamodb_table_protected_by_backup_plan, + ) + + check = dynamodb_table_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "DynamoDB table test1 is not protected by a backup plan." + ) + assert result[0].resource_id == table["TableName"] + assert result[0].resource_arn == table["TableArn"] + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + def test_dynamodb_table_with_backup_plan(self): + dynamodb_client = mock.MagicMock() + from prowler.providers.aws.services.dynamodb.dynamodb_service import Table + + dynamodb_client.tables = { + f"arn:aws:dynamodb:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:table/test1": Table( + name="test1", + region=AWS_REGION_US_EAST_1, + ) + } + + backup = mock.MagicMock() + backup.protected_resources = [ + f"arn:aws:dynamodb:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:table/test1" + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.dynamodb_client", + new=dynamodb_client, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan import ( + dynamodb_table_protected_by_backup_plan, + ) + + check = dynamodb_table_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "DynamoDB table test1 is protected by a backup plan." + ) + assert result[0].resource_id == "test1" + assert ( + result[0].resource_arn + == f"arn:aws:dynamodb:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:table/test1" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + def test_dynamodb_table_with_backup_plan_via_wildcard_all_tables(self): + dynamodb_client = mock.MagicMock() + from prowler.providers.aws.services.dynamodb.dynamodb_service import Table + + dynamodb_client.tables = { + f"arn:aws:dynamodb:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:table/test1": Table( + name="test1", + region=AWS_REGION_US_EAST_1, + ) + } + + backup = mock.MagicMock() + backup.protected_resources = ["arn:aws:dynamodb:*:*:table/*"] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.dynamodb_client", + new=dynamodb_client, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan import ( + dynamodb_table_protected_by_backup_plan, + ) + + check = dynamodb_table_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "DynamoDB table test1 is protected by a backup plan." + ) + assert result[0].resource_id == "test1" + assert ( + result[0].resource_arn + == f"arn:aws:dynamodb:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:table/test1" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + def test_dynamodb_table_with_backup_plan_via_wildcard_all_resources(self): + dynamodb_client = mock.MagicMock() + from prowler.providers.aws.services.dynamodb.dynamodb_service import Table + + dynamodb_client.tables = { + f"arn:aws:dynamodb:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:table/test1": Table( + name="test1", + region=AWS_REGION_US_EAST_1, + ) + } + + backup = mock.MagicMock() + backup.protected_resources = ["*"] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.dynamodb_client", + new=dynamodb_client, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_client.dynamodb_client", + new=dynamodb_client, + ), mock.patch( + "prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, + ): + # Test Check + from prowler.providers.aws.services.dynamodb.dynamodb_table_protected_by_backup_plan.dynamodb_table_protected_by_backup_plan import ( + dynamodb_table_protected_by_backup_plan, + ) + + check = dynamodb_table_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "DynamoDB table test1 is protected by a backup plan." + ) + assert result[0].resource_id == "test1" + assert ( + result[0].resource_arn + == f"arn:aws:dynamodb:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:table/test1" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan_test.py b/tests/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan_test.py new file mode 100644 index 0000000000..601b51b554 --- /dev/null +++ b/tests/providers/aws/services/ec2/ec2_ebs_volume_protected_by_backup_plan/ec2_ebs_volume_protected_by_backup_plan_test.py @@ -0,0 +1,323 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_ec2_ebs_volume_protected_by_backup_plan: + @mock_aws + def test_ec2_no_volumes(self): + from prowler.providers.aws.services.backup.backup_service import Backup + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.ec2_client", + new=EC2(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.backup_client", + new=Backup(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan import ( + ec2_ebs_volume_protected_by_backup_plan, + ) + + check = ec2_ebs_volume_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_ec2_ebs_volume_no_backup_plans(self): + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + volume = ec2_client.create_volume( + AvailabilityZone="us-east-1a", Size=10, VolumeType="gp2" + ) + + from prowler.providers.aws.services.backup.backup_service import Backup + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.ec2_client", + new=EC2(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.backup_client", + new=Backup(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan import ( + ec2_ebs_volume_protected_by_backup_plan, + ) + + check = ec2_ebs_volume_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"EBS Volume {volume['VolumeId']} is not protected by a backup plan." + ) + assert result[0].resource_id == volume["VolumeId"] + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:volume/{volume['VolumeId']}" + ) + assert result[0].resource_tags is None + + @mock_aws + def test_ec2_ebs_volume_without_backup_plan(self): + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + backup_client = client("backup", region_name=AWS_REGION_US_EAST_1) + volume = ec2_client.create_volume( + AvailabilityZone="us-east-1a", Size=10, VolumeType="gp2" + ) + backup_client.create_backup_plan( + BackupPlan={ + "BackupPlanName": "test-backup-plan", + "Rules": [ + { + "RuleName": "DailyBackup", + "TargetBackupVaultName": "test-vault", + "ScheduleExpression": "cron(0 12 * * ? *)", + "Lifecycle": {"DeleteAfterDays": 30}, + "RecoveryPointTags": { + "Type": "Daily", + }, + }, + ], + } + ) + + from prowler.providers.aws.services.backup.backup_service import Backup + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.ec2_client", + new=EC2(aws_provider), + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.backup_client", + new=Backup(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan import ( + ec2_ebs_volume_protected_by_backup_plan, + ) + + check = ec2_ebs_volume_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"EBS Volume {volume['VolumeId']} is not protected by a backup plan." + ) + assert result[0].resource_id == volume["VolumeId"] + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:volume/{volume['VolumeId']}" + ) + assert result[0].resource_tags is None + + def test_ec2_instance_with_backup_plan(self): + ec2_client = mock.MagicMock() + from prowler.providers.aws.services.ec2.ec2_service import Volume + + ec2_client.volumes = [ + Volume( + id="volume-tester", + arn=f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:volume/volume-tester", + tags=None, + region=AWS_REGION_US_EAST_1, + encrypted=False, + ) + ] + + backup_client = mock.MagicMock() + backup_client.protected_resources = [ + f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:volume/volume-tester" + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.ec2_client", + new=ec2_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_client.ec2_client", + new=ec2_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.backup_client", + new=backup_client, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup_client, + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan import ( + ec2_ebs_volume_protected_by_backup_plan, + ) + + check = ec2_ebs_volume_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "EBS Volume volume-tester is protected by a backup plan." + ) + assert result[0].resource_id == "volume-tester" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:volume/volume-tester" + ) + assert result[0].resource_tags is None + + def test_ec2_instance_with_backup_plan_via_volume_wildcard(self): + ec2_client = mock.MagicMock() + from prowler.providers.aws.services.ec2.ec2_service import Volume + + ec2_client.volumes = [ + Volume( + id="volume-tester", + arn=f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:volume/volume-tester", + tags=None, + region=AWS_REGION_US_EAST_1, + encrypted=False, + ) + ] + + backup_client = mock.MagicMock() + backup_client.protected_resources = ["arn:aws:ec2:*:*:volume/*"] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.ec2_client", + new=ec2_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_client.ec2_client", + new=ec2_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.backup_client", + new=backup_client, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup_client, + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan import ( + ec2_ebs_volume_protected_by_backup_plan, + ) + + check = ec2_ebs_volume_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "EBS Volume volume-tester is protected by a backup plan." + ) + assert result[0].resource_id == "volume-tester" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:volume/volume-tester" + ) + assert result[0].resource_tags is None + + def test_ec2_instance_with_backup_plan_via_all_wildcard(self): + ec2_client = mock.MagicMock() + from prowler.providers.aws.services.ec2.ec2_service import Volume + + ec2_client.volumes = [ + Volume( + id="volume-tester", + arn=f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:volume/volume-tester", + tags=None, + region=AWS_REGION_US_EAST_1, + encrypted=False, + ) + ] + + backup_client = mock.MagicMock() + backup_client.protected_resources = ["*"] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.ec2_client", + new=ec2_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_client.ec2_client", + new=ec2_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan.backup_client", + new=backup_client, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup_client, + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_ebs_volume_protected_by_backup_plan.ec2_ebs_volume_protected_by_backup_plan import ( + ec2_ebs_volume_protected_by_backup_plan, + ) + + check = ec2_ebs_volume_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "EBS Volume volume-tester is protected by a backup plan." + ) + assert result[0].resource_id == "volume-tester" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:volume/volume-tester" + ) + assert result[0].resource_tags is None diff --git a/tests/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type_test.py b/tests/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type_test.py new file mode 100644 index 0000000000..0d71c1ebe8 --- /dev/null +++ b/tests/providers/aws/services/ec2/ec2_instance_paravirtual_type/ec2_instance_paravirtual_type_test.py @@ -0,0 +1,140 @@ +from unittest import mock + +from boto3 import resource +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +EXAMPLE_AMI_ID = "ami-12c6146b" +PARAVIRTUAL_AMI_ID = "ami-0efade68705c4bea5" + + +class Test_ec2_instance_paravirtual_type: + @mock_aws + def test_ec2_no_instances(self): + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_paravirtual_type.ec2_instance_paravirtual_type.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_instance_paravirtual_type.ec2_instance_paravirtual_type import ( + ec2_instance_paravirtual_type, + ) + + check = ec2_instance_paravirtual_type() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_one_compliant_ec2(self): + ec2 = resource("ec2", region_name=AWS_REGION_US_EAST_1) + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.0.0/18") + instance = ec2.create_instances( + ImageId=EXAMPLE_AMI_ID, + MinCount=1, + MaxCount=1, + NetworkInterfaces=[ + { + "DeviceIndex": 0, + "SubnetId": subnet.id, + "AssociatePublicIpAddress": False, + } + ], + )[0] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_paravirtual_type.ec2_instance_paravirtual_type.ec2_client", + new=EC2(aws_provider), + ): + from prowler.providers.aws.services.ec2.ec2_instance_paravirtual_type.ec2_instance_paravirtual_type import ( + ec2_instance_paravirtual_type, + ) + + check = ec2_instance_paravirtual_type() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags is None + assert ( + result[0].status_extended + == f"EC2 Instance {instance.id} virtualization type is set to HVM." + ) + assert result[0].resource_id == instance.id + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{aws_provider.identity.account}:instance/{instance.id}" + ) + + @mock_aws + def test_one_ec2_with_paravirtual_type(self): + ec2 = resource("ec2", region_name=AWS_REGION_US_EAST_1) + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + subnet = ec2.create_subnet(VpcId=vpc.id, CidrBlock="10.0.0.0/18") + instance = ec2.create_instances( + ImageId=PARAVIRTUAL_AMI_ID, + MinCount=1, + MaxCount=1, + NetworkInterfaces=[ + { + "DeviceIndex": 0, + "SubnetId": subnet.id, + "AssociatePublicIpAddress": False, + } + ], + )[0] + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_paravirtual_type.ec2_instance_paravirtual_type.ec2_client", + new=EC2(aws_provider), + ): + from prowler.providers.aws.services.ec2.ec2_instance_paravirtual_type.ec2_instance_paravirtual_type import ( + ec2_instance_paravirtual_type, + ) + + check = ec2_instance_paravirtual_type() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].status_extended + == f"EC2 Instance {instance.id} virtualization type is set to paravirtual." + ) + assert result[0].resource_id == instance.id + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{aws_provider.identity.account}:instance/{instance.id}" + ) diff --git a/tests/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni_test.py b/tests/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni_test.py new file mode 100644 index 0000000000..863067a294 --- /dev/null +++ b/tests/providers/aws/services/ec2/ec2_instance_uses_single_eni/ec2_instance_uses_single_eni_test.py @@ -0,0 +1,314 @@ +from unittest import mock + +import botocore +from boto3 import resource +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "DescribeInstances": + return { + "Reservations": [ + { + "Instances": [ + { + "InstanceId": "i-0123456789abcdef0", + "State": {"Name": "running"}, + "InstanceType": "t2.micro", + "ImageId": "ami-12345678", + "LaunchTime": "2023-09-01T12:34:56Z", + "PrivateDnsName": "ip-172-31-32-101.ec2.internal", + "Monitoring": {"State": "disabled"}, + "SecurityGroups": [ + {"GroupId": "sg-12345678", "GroupName": "default"} + ], + "SubnetId": "subnet-12345678", + "Tags": [{"Key": "Name", "Value": "MyInstance"}], + "NetworkInterfaces": [ + {"NetworkInterfaceId": "eni-1"}, + {"NetworkInterfaceId": "eni-2"}, + ], + } + ] + } + ] + } + elif operation_name == "DescribeNetworkInterfaces": + return { + "NetworkInterfaces": [ + { + "NetworkInterfaceId": "eni-1", + "SubnetId": "subnet-1234abcd", + "VpcId": "vpc-1234abcd", + "PrivateIpAddress": "192.168.1.1", + "InterfaceType": "interface", + }, + { + "NetworkInterfaceId": "eni-2", + "SubnetId": "subnet-1234abcd", + "VpcId": "vpc-1234abcd", + "PrivateIpAddress": "192.168.1.2", + "InterfaceType": "efa", + }, + ] + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_v2(self, operation_name, kwarg): + if operation_name == "DescribeInstances": + return { + "Reservations": [ + { + "Instances": [ + { + "InstanceId": "i-0123456789abcdef0", + "State": {"Name": "running"}, + "InstanceType": "t2.micro", + "ImageId": "ami-12345678", + "LaunchTime": "2023-09-01T12:34:56Z", + "PrivateDnsName": "ip-172-31-32-101.ec2.internal", + "Monitoring": {"State": "disabled"}, + "SecurityGroups": [ + {"GroupId": "sg-12345678", "GroupName": "default"} + ], + "SubnetId": "subnet-12345678", + "Tags": [{"Key": "Name", "Value": "MyInstance"}], + "NetworkInterfaces": [], + } + ] + } + ] + } + elif operation_name == "DescribeNetworkInterfaces": + return { + "NetworkInterfaces": [ + { + "NetworkInterfaceId": "eni-3", + "SubnetId": "subnet-5678abcd", + "VpcId": "vpc-5678abcd", + "PrivateIpAddress": "192.168.1.3", + "InterfaceType": "interface", + } + ] + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_v3(self, operation_name, kwarg): + if operation_name == "DescribeInstances": + return { + "Reservations": [ + { + "Instances": [ + { + "InstanceId": "i-0123456789abcdef0", + "State": {"Name": "running"}, + "InstanceType": "t2.micro", + "ImageId": "ami-12345678", + "LaunchTime": "2023-09-01T12:34:56Z", + "PrivateDnsName": "ip-172-31-32-101.ec2.internal", + "Monitoring": {"State": "disabled"}, + "SecurityGroups": [ + {"GroupId": "sg-12345678", "GroupName": "default"} + ], + "SubnetId": "subnet-12345678", + "Tags": [{"Key": "Name", "Value": "MyInstance"}], + "NetworkInterfaces": [ + {"NetworkInterfaceId": "eni-1"}, + {"NetworkInterfaceId": "eni-2"}, + {"NetworkInterfaceId": "eni-3"}, + ], + } + ] + } + ] + } + elif operation_name == "DescribeNetworkInterfaces": + return { + "NetworkInterfaces": [ + { + "NetworkInterfaceId": "eni-1", + "SubnetId": "subnet-1234abcd", + "VpcId": "vpc-1234abcd", + "PrivateIpAddress": "192.168.1.1", + "InterfaceType": "trunk", + }, + { + "NetworkInterfaceId": "eni-2", + "SubnetId": "subnet-1234abcd", + "VpcId": "vpc-1234abcd", + "PrivateIpAddress": "192.168.1.2", + "InterfaceType": "efa", + }, + { + "NetworkInterfaceId": "eni-3", + "SubnetId": "subnet-1234abcd", + "VpcId": "vpc-1234abcd", + "PrivateIpAddress": "192.168.1.3", + "InterfaceType": "trunk", + }, + ] + } + return make_api_call(self, operation_name, kwarg) + + +class Test_ec2_instance_uses_single_eni: + @mock_aws + def test_ec2_no_instances(self): + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_uses_single_eni.ec2_instance_uses_single_eni.ec2_client", + new=EC2(aws_provider), + ): + from prowler.providers.aws.services.ec2.ec2_instance_uses_single_eni.ec2_instance_uses_single_eni import ( + ec2_instance_uses_single_eni, + ) + + check = ec2_instance_uses_single_eni() + result = check.execute() + + assert len(result) == 0 + + @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call_v2) + def test_ec2_instance_no_eni(self): + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_uses_single_eni.ec2_instance_uses_single_eni.ec2_client", + new=EC2(aws_provider), + ): + from prowler.providers.aws.services.ec2.ec2_instance_uses_single_eni.ec2_instance_uses_single_eni import ( + ec2_instance_uses_single_eni, + ) + + check = ec2_instance_uses_single_eni() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "i-0123456789abcdef0" + assert ( + result[0].status_extended + == "EC2 Instance i-0123456789abcdef0 has no network interfaces attached." + ) + + @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + def test_instance_multiple_enis(self): + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_uses_single_eni.ec2_instance_uses_single_eni.ec2_client", + new=EC2(aws_provider), + ): + from prowler.providers.aws.services.ec2.ec2_instance_uses_single_eni.ec2_instance_uses_single_eni import ( + ec2_instance_uses_single_eni, + ) + + check = ec2_instance_uses_single_eni() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "i-0123456789abcdef0" + assert ( + result[0].status_extended + == "EC2 Instance i-0123456789abcdef0 uses multiple ENIs: ( EFAs: ['eni-2'] Interfaces: ['eni-1'] )." + ) + + @mock_aws + def test_instance_with_single_eni(self): + ec2 = resource("ec2", region_name=AWS_REGION_US_EAST_1) + instance = ec2.create_instances( + ImageId="ami-12c6146b", + MinCount=1, + MaxCount=1, + UserData="This is some user_data", + )[0] + network_interface = instance.network_interfaces[0] + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_uses_single_eni.ec2_instance_uses_single_eni.ec2_client", + new=EC2(aws_provider), + ): + from prowler.providers.aws.services.ec2.ec2_instance_uses_single_eni.ec2_instance_uses_single_eni import ( + ec2_instance_uses_single_eni, + ) + + check = ec2_instance_uses_single_eni() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == instance.id + assert ( + result[0].status_extended + == f"EC2 Instance {instance.id} uses only one ENI: ( Interfaces: ['{network_interface.id}'] )." + ) + + @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call_v3) + def test_instance_one_efa_multiple_trunks(self): + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_instance_uses_single_eni.ec2_instance_uses_single_eni.ec2_client", + new=EC2(aws_provider), + ): + from prowler.providers.aws.services.ec2.ec2_instance_uses_single_eni.ec2_instance_uses_single_eni import ( + ec2_instance_uses_single_eni, + ) + + check = ec2_instance_uses_single_eni() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "i-0123456789abcdef0" + assert ( + result[0].status_extended + == "EC2 Instance i-0123456789abcdef0 uses multiple ENIs: ( EFAs: ['eni-2'] Trunks: ['eni-1', 'eni-3'] )." + ) diff --git a/tests/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip_test.py b/tests/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip_test.py new file mode 100644 index 0000000000..d582b73698 --- /dev/null +++ b/tests/providers/aws/services/ec2/ec2_launch_template_no_public_ip/ec2_launch_template_no_public_ip_test.py @@ -0,0 +1,363 @@ +from base64 import b64encode +from ipaddress import IPv4Address, IPv6Address +from os import path +from pathlib import Path +from unittest import mock + +import botocore +from boto3 import client +from moto import mock_aws + +from prowler.config.config import encoding_format_utf_8 +from prowler.providers.aws.services.ec2.ec2_service import ( + Attachment, + LaunchTemplate, + LaunchTemplateVersion, + NetworkInterface, + TemplateData, +) +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +ACTUAL_DIRECTORY = Path(path.dirname(path.realpath(__file__))) +FIXTURES_DIR_NAME = "fixtures" + +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "DescribeLaunchTemplateVersions": + return { + "LaunchTemplateVersions": [ + { + "VersionNumber": 1, + "LaunchTemplateData": { + "NetworkInterfaces": [{"AssociatePublicIpAddress": False}], + }, + } + ] + } + return make_api_call(self, operation_name, kwarg) + + +class Test_ec2_launch_template_no_public_ip: + @mock_aws + def test_no_launch_templates(self): + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + ec2_client.launch_templates = [] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_launch_template_no_public_ip.ec2_launch_template_no_public_ip.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_no_public_ip.ec2_launch_template_no_public_ip import ( + ec2_launch_template_no_public_ip, + ) + + check = ec2_launch_template_no_public_ip() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + def test_launch_template_no_public_ip(self): + # Include launch_template to check + launch_template_name = "test-no-public-ip" + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + ec2_client.create_launch_template( + LaunchTemplateName=launch_template_name, + VersionDescription="Launch Template with no public IP", + LaunchTemplateData={ + "InstanceType": "t1.micro", + }, + ) + + launch_template_id = ec2_client.describe_launch_templates( + LaunchTemplateNames=[launch_template_name] + )["LaunchTemplates"][0]["LaunchTemplateId"] + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_launch_template_no_public_ip.ec2_launch_template_no_public_ip.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_no_public_ip.ec2_launch_template_no_public_ip import ( + ec2_launch_template_no_public_ip, + ) + + check = ec2_launch_template_no_public_ip() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"EC2 Launch Template {launch_template_name} is neither configured to assign a public IP address to network interfaces upon launch nor using a network interface with public IP addresses." + ) + assert result[0].resource_id == launch_template_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] + + def test_launch_template_public_ip_auto_assign(self): + ec2_client = mock.MagicMock() + launch_template_name = "tester" + launch_template_id = "lt-1234567890" + launch_template_arn = ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}" + ) + + launch_template_data = TemplateData( + user_data=b64encode("sinsecretos".encode(encoding_format_utf_8)).decode( + encoding_format_utf_8 + ), + associate_public_ip_address=True, + ) + + launch_template_versions = [ + LaunchTemplateVersion( + version_number=2, + template_data=launch_template_data, + ), + ] + + launch_template = LaunchTemplate( + name=launch_template_name, + id=launch_template_id, + arn=launch_template_arn, + region=AWS_REGION_US_EAST_1, + versions=launch_template_versions, + ) + + ec2_client.launch_templates = [launch_template] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=ec2_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_launch_template_no_public_ip.ec2_launch_template_no_public_ip.ec2_client", + new=ec2_client, + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_no_public_ip.ec2_launch_template_no_public_ip import ( + ec2_launch_template_no_public_ip, + ) + + check = ec2_launch_template_no_public_ip() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"EC2 Launch Template {launch_template_name} is configured to assign a public IP address to network interfaces upon launch in template versions: 2." + ) + assert result[0].resource_id == launch_template_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] + + def test_network_interface_with_public_ipv4_network_interface_autoassign_true_and_false( + self, + ): + ec2_client = mock.MagicMock() + launch_template_name = "tester" + launch_template_id = "lt-1234567890" + launch_template_arn = ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}" + ) + + network_interface = NetworkInterface( + id="eni-1234567890", + association={}, + attachment=Attachment(), + private_ip="", + public_ip_addresses=[IPv4Address("192.175.48.10")], + type="interface", + subnet_id="subnet-1234567890", + vpc_id="vpc-1234567890", + region=AWS_REGION_US_EAST_1, + ) + + launch_template_data = TemplateData( + user_data=b64encode("sinsecretos".encode(encoding_format_utf_8)).decode( + encoding_format_utf_8 + ), + associate_public_ip_address=False, + network_interfaces=[network_interface], + ) + launch_template_data2 = TemplateData( + user_data=b64encode("sinsecretos".encode(encoding_format_utf_8)).decode( + encoding_format_utf_8 + ), + associate_public_ip_address=True, + network_interfaces=[network_interface], + ) + launch_template_data3 = TemplateData( + user_data=b64encode("sinsecretos".encode(encoding_format_utf_8)).decode( + encoding_format_utf_8 + ), + associate_public_ip_address=False, + ) + + launch_template_versions = [ + LaunchTemplateVersion( + version_number=3, + template_data=launch_template_data, + ), + LaunchTemplateVersion( + version_number=4, + template_data=launch_template_data2, + ), + LaunchTemplateVersion( + version_number=5, + template_data=launch_template_data3, + ), + ] + + launch_template = LaunchTemplate( + name=launch_template_name, + id=launch_template_id, + arn=launch_template_arn, + region=AWS_REGION_US_EAST_1, + versions=launch_template_versions, + ) + + ec2_client.launch_templates = [launch_template] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=ec2_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_launch_template_no_public_ip.ec2_launch_template_no_public_ip.ec2_client", + new=ec2_client, + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_no_public_ip.ec2_launch_template_no_public_ip import ( + ec2_launch_template_no_public_ip, + ) + + check = ec2_launch_template_no_public_ip() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"EC2 Launch Template {launch_template_name} is configured to assign a public IP address to network interfaces upon launch in template versions: 4 and is using a network interface with public IP addresses in template versions: 3, 4." + ) + assert result[0].resource_id == launch_template_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] + + def test_network_interface_with_public_ipv6_network_interface_autoassign_true_and_false( + self, + ): + ec2_client = mock.MagicMock() + launch_template_name = "tester" + launch_template_id = "lt-1234567890" + launch_template_arn = ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}" + ) + + network_interface = NetworkInterface( + id="eni-1234567890", + association={}, + attachment=Attachment(), + private_ip="", + public_ip_addresses=[IPv6Address("::1234:5678")], + type="interface", + subnet_id="subnet-1234567890", + vpc_id="vpc-1234567890", + region=AWS_REGION_US_EAST_1, + ) + + launch_template_data = TemplateData( + user_data=b64encode("sinsecretos".encode(encoding_format_utf_8)).decode( + encoding_format_utf_8 + ), + associate_public_ip_address=True, + network_interfaces=[network_interface], + ) + launch_template_data2 = TemplateData( + user_data=b64encode("sinsecretos".encode(encoding_format_utf_8)).decode( + encoding_format_utf_8 + ), + associate_public_ip_address=False, + network_interfaces=[network_interface], + ) + + launch_template_versions = [ + LaunchTemplateVersion( + version_number=6, + template_data=launch_template_data, + ), + LaunchTemplateVersion( + version_number=7, + template_data=launch_template_data2, + ), + ] + + launch_template = LaunchTemplate( + name=launch_template_name, + id=launch_template_id, + arn=launch_template_arn, + region=AWS_REGION_US_EAST_1, + versions=launch_template_versions, + ) + + ec2_client.launch_templates = [launch_template] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=ec2_client, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_launch_template_no_public_ip.ec2_launch_template_no_public_ip.ec2_client", + new=ec2_client, + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_launch_template_no_public_ip.ec2_launch_template_no_public_ip import ( + ec2_launch_template_no_public_ip, + ) + + check = ec2_launch_template_no_public_ip() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"EC2 Launch Template {launch_template_name} is configured to assign a public IP address to network interfaces upon launch in template versions: 6 and is using a network interface with public IP addresses in template versions: 6, 7." + ) + assert result[0].resource_id == launch_template_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/ec2/ec2_launch_template_no_secrets/ec2_launch_template_no_secrets_test.py b/tests/providers/aws/services/ec2/ec2_launch_template_no_secrets/ec2_launch_template_no_secrets_test.py index 9f75048dd2..3e6f2ad6af 100644 --- a/tests/providers/aws/services/ec2/ec2_launch_template_no_secrets/ec2_launch_template_no_secrets_test.py +++ b/tests/providers/aws/services/ec2/ec2_launch_template_no_secrets/ec2_launch_template_no_secrets_test.py @@ -42,6 +42,9 @@ def mock_make_api_call(self, operation_name, kwarg): { "LaunchTemplateName": "tester1", "LaunchTemplateId": "lt-1234567890", + "Tags": [ + {"Key": "Name", "Value": "tester1"}, + ], } ] } @@ -122,10 +125,13 @@ class Test_ec2_launch_template_no_secrets: ) assert result[0].resource_id == launch_template_id assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_one_launch_template_with_secrets(self): - from prowler.providers.aws.services.ec2.ec2_service import EC2 aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) @@ -153,6 +159,10 @@ class Test_ec2_launch_template_no_secrets: ) assert result[0].resource_id == "lt-1234567890" assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == ( + "arn:aws:ec2:us-east-1:123456789012:launch-template/lt-1234567890" + ) + assert result[0].resource_tags == [{"Key": "Name", "Value": "tester1"}] def test_one_launch_template_with_secrets_in_multiple_versions(self): ec2_client = mock.MagicMock() @@ -217,6 +227,10 @@ class Test_ec2_launch_template_no_secrets: ) assert result[0].resource_id == launch_template_id assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] def test_one_launch_template_with_secrets_in_single_version(self): ec2_client = mock.MagicMock() @@ -287,6 +301,10 @@ class Test_ec2_launch_template_no_secrets: ) assert result[0].resource_id == launch_template_id assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] def test_one_launch_template_with_secrets_gzip(self): ec2_client = mock.MagicMock() @@ -347,6 +365,10 @@ class Test_ec2_launch_template_no_secrets: ) assert result[0].resource_id == launch_template_id assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] @mock_aws def test_one_launch_template_without_user_data(self): @@ -392,6 +414,10 @@ class Test_ec2_launch_template_no_secrets: ) assert result[0].resource_id == launch_template_id assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}" + ) + assert result[0].resource_tags == [] @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_two_launch_templates_one_template_with_secrets(self): @@ -480,6 +506,10 @@ class Test_ec2_launch_template_no_secrets: ) assert result[0].resource_id == launch_template_id1 assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id1}" + ) + assert result[0].resource_tags == [] assert result[1].status == "PASS" assert ( @@ -488,6 +518,10 @@ class Test_ec2_launch_template_no_secrets: ) assert result[1].resource_id == launch_template_id2 assert result[1].region == AWS_REGION_US_EAST_1 + assert result[1].resource_arn == ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id2}" + ) + assert result[1].resource_tags == [] @mock_aws def test_one_launch_template_with_unicode_error(self): @@ -535,8 +569,7 @@ class Test_ec2_launch_template_no_secrets: ) assert result[0].resource_id == launch_template_id assert result[0].region == AWS_REGION_US_EAST_1 - assert ( - result[0].resource_arn - == f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}" + assert result[0].resource_arn == ( + f"arn:aws:ec2:us-east-1:123456789012:launch-template/{launch_template_id}" ) assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused_test.py b/tests/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused_test.py new file mode 100644 index 0000000000..b141f51d0a --- /dev/null +++ b/tests/providers/aws/services/ec2/ec2_networkacl_unused/ec2_networkacl_unused_test.py @@ -0,0 +1,126 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_ec2_networkacl_unused: + @mock_aws + def test_ec2_default_nacls(self): + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_networkacl_unused.ec2_networkacl_unused.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_networkacl_unused.ec2_networkacl_unused import ( + ec2_networkacl_unused, + ) + + check = ec2_networkacl_unused() + result = check.execute() + + # One default nacl per region + assert len(result) == 0 + + @mock_aws + def test_ec2_unused_non_default_nacl(self): + # Create EC2 Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + nacl_id = ec2_client.create_network_acl(VpcId=vpc_id)["NetworkAcl"][ + "NetworkAclId" + ] + arn = f"arn:aws:ec2:eu-west-1:123456789012:network-acl/{nacl_id}" + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_networkacl_unused.ec2_networkacl_unused.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_networkacl_unused.ec2_networkacl_unused import ( + ec2_networkacl_unused, + ) + + check = ec2_networkacl_unused() + result = check.execute() + + assert len(result) == 1 + + assert result[0].status == "FAIL" + assert result[0].region == "eu-west-1" + assert result[0].resource_id == nacl_id + assert result[0].resource_arn == arn + assert ( + result[0].status_extended + == f"Network ACL {nacl_id} is not associated with any subnet and is not the default network ACL." + ) + + @mock_aws + def test_ec2_used_non_default_nacl(self): + # Create EC2 Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_EU_WEST_1) + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + nacl_id = ec2_client.create_network_acl(VpcId=vpc_id)["NetworkAcl"][ + "NetworkAclId" + ] + arn = f"arn:aws:ec2:eu-west-1:123456789012:network-acl/{nacl_id}" + + default_nacl_association_id = ec2_client.describe_network_acls()["NetworkAcls"][ + 0 + ]["Associations"][0]["NetworkAclAssociationId"] + # Replace the default association with the new Network ACL + ec2_client.replace_network_acl_association( + AssociationId=default_nacl_association_id, NetworkAclId=nacl_id + ) + + from prowler.providers.aws.services.ec2.ec2_service import EC2 + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ec2.ec2_networkacl_unused.ec2_networkacl_unused.ec2_client", + new=EC2(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.ec2.ec2_networkacl_unused.ec2_networkacl_unused import ( + ec2_networkacl_unused, + ) + + check = ec2_networkacl_unused() + result = check.execute() + + assert len(result) == 1 + # Search changed sg + assert result[0].status == "PASS" + assert result[0].region == "eu-west-1" + assert result[0].resource_id == nacl_id + assert result[0].resource_arn == arn + assert ( + result[0].status_extended + == f"Network ACL {nacl_id} is associated with a subnet." + ) diff --git a/tests/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port_test.py b/tests/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port_test.py index 4e3ea6b636..2c83564bc9 100644 --- a/tests/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port_test.py +++ b/tests/providers/aws/services/ec2/ec2_securitygroup_allow_ingress_from_internet_to_any_port/ec2_securitygroup_allow_ingress_from_internet_to_any_port_test.py @@ -3,6 +3,7 @@ from unittest import mock from boto3 import client, resource from moto import mock_aws +from prowler.providers.aws.services.ec2.ec2_service import Attachment from tests.providers.aws.utils import ( AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1, @@ -97,9 +98,7 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port: self, default_sg_id, default_sg_name, network_interface_response ): eni = network_interface_response.get("NetworkInterface", {}) - att = eni.get("Attachment", {}) eni_type = eni.get("InterfaceType", "") - eni_owner = att.get("InstanceOwnerId", "") from prowler.providers.aws.services.ec2.ec2_service import EC2 from prowler.providers.aws.services.vpc.vpc_service import VPC @@ -133,7 +132,7 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port: assert sg.region == AWS_REGION_US_EAST_1 assert ( sg.status_extended - == f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet and neither its network interface type ({eni_type}) nor its network interface instance owner ({eni_owner}) are part of the allowed network interfaces." + == f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet but its network interface type ({eni_type}) is not allowed." ) assert ( sg.resource_arn @@ -175,7 +174,7 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port: "eni_instance_owner": "NOT_ALLOWED", "report": { "status": "PASS", - "status_extended": "Security group SG_name (SG_id) has at least one port open to the Internet but is exclusively attached to an allowed network interface type (vpc_endpoint).", + "status_extended": "Security group SG_name (SG_id) has at least one port open to the Internet and it is attached to an allowed network interface type (vpc_endpoint).", }, }, { @@ -183,7 +182,7 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port: "eni_instance_owner": "amazon-elb", "report": { "status": "PASS", - "status_extended": "Security group SG_name (SG_id) has at least one port open to the Internet but is exclusively attached to an allowed network interface instance owner (amazon-elb).", + "status_extended": "Security group SG_name (SG_id) has at least one port open to the Internet and it is attached to an allowed network interface instance owner (amazon-elb).", }, }, { @@ -191,7 +190,7 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port: "eni_instance_owner": "NOT_ALLOWED_INSTANCE_OWNER", "report": { "status": "FAIL", - "status_extended": "Security group SG_name (SG_id) has at least one port open to the Internet and neither its network interface type (NOT_ALLOWED_ENI_TYPE) nor its network interface instance owner (NOT_ALLOWED_INSTANCE_OWNER) are part of the allowed network interfaces.", + "status_extended": "Security group SG_name (SG_id) has at least one port open to the Internet but its network interface type (NOT_ALLOWED_ENI_TYPE) is not allowed.", }, }, ] @@ -202,7 +201,7 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port: eni = NetworkInterface( id="1", association={}, - attachment={"InstanceOwnerId": test["eni_instance_owner"]}, + attachment=Attachment(instance_owner_id=test["eni_instance_owner"]), private_ip="1", public_ip_addresses=[], type=test["eni_interface_type"], @@ -303,7 +302,7 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port: assert sg.region == AWS_REGION_US_EAST_1 assert ( sg.status_extended - == f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet but is exclusively attached to an allowed network interface type ({eni_type})." + == f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet and it is attached to an allowed network interface type ({eni_type})." ) assert ( sg.resource_arn @@ -396,7 +395,7 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port: assert sg.region == AWS_REGION_US_EAST_1 assert ( sg.status_extended - == f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet but is exclusively attached to an allowed network interface instance owner ({eni_owner})." + == f"Security group {default_sg_name} ({default_sg_id}) has at least one port open to the Internet and it is attached to an allowed network interface instance owner ({eni_owner})." ) assert ( sg.resource_arn @@ -650,7 +649,6 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port: "SecurityGroups" ][0] default_sg_id = default_sg["GroupId"] - default_sg_name = default_sg["GroupName"] ec2_client.authorize_security_group_ingress( GroupId=default_sg_id, IpPermissions=[ @@ -721,15 +719,4 @@ class Test_ec2_securitygroup_allow_ingress_from_internet_to_any_port: result_specific_port = check_specific_port.execute() # One default sg per region - assert len(result_specific_port) == 3 - # Search changed sg - for sg in result_specific_port: - if sg.resource_id == default_sg_id: - assert sg.status == "PASS" - assert sg.region == AWS_REGION_US_EAST_1 - assert ( - sg.status_extended - == f"Security group {sg.resource_details} ({sg.resource_id}) has all ports open to the Internet and therefore was not checked against a specific port." - ) - assert sg.resource_tags == [] - assert sg.resource_details == default_sg_name + assert len(result_specific_port) == 2 diff --git a/tests/providers/aws/services/ec2/ec2_service_test.py b/tests/providers/aws/services/ec2/ec2_service_test.py index 46786b46f8..f4fd93b14a 100644 --- a/tests/providers/aws/services/ec2/ec2_service_test.py +++ b/tests/providers/aws/services/ec2/ec2_service_test.py @@ -145,6 +145,9 @@ class Test_EC2_Service: == f"ec2-{ec2.instances[0].public_ip.replace('.', '-')}.compute-1.amazonaws.com" ) + assert ec2.instances[0].network_interfaces is not None + assert ec2.instances[0].virtualization_type == "hvm" + # Test EC2 Describe Security Groups @mock_aws def test_describe_security_groups(self): @@ -240,14 +243,16 @@ class Test_EC2_Service: ec2 = EC2(aws_provider) assert nacl_id in str(ec2.network_acls) - for acl in ec2.network_acls: + for arn, acl in ec2.network_acls.items(): if acl.id == nacl_id: assert re.match(r"acl-[0-9a-z]{8}", acl.id) assert ( - acl.arn + arn == f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:network-acl/{acl.id}" ) assert acl.entries == [] + assert not acl.in_use + assert not acl.default assert acl.tags == [ {"Key": "test", "Value": "test"}, ] @@ -671,6 +676,14 @@ class Test_EC2_Service: KNOWN_SECRET_USER_DATA.encode(encoding_format_utf_8) ).decode(encoding_format_utf_8), }, + TagSpecifications=[ + { + "ResourceType": "launch-template", + "Tags": [ + {"Key": "test", "Value": "test"}, + ], + } + ], ) # EC2 client for this test class @@ -686,6 +699,9 @@ class Test_EC2_Service: ec2.launch_templates[0].arn == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:launch-template/{ec2.launch_templates[0].id}" ) + assert ec2.launch_templates[0].tags == [ + {"Key": "test", "Value": "test"}, + ] # Test EC2 Describe Launch Templates @mock_aws diff --git a/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py b/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py index e6fad5645b..92d6abef51 100644 --- a/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py +++ b/tests/providers/aws/services/ecr/ecr_repositories_not_publicly_accessible/ecr_repositories_not_publicly_accessible_test.py @@ -122,7 +122,50 @@ class Test_ecr_repositories_not_publicly_accessible: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Repository {repository_name} is not publicly accesible." + == f"Repository {repository_name} is not publicly accessible." + ) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + + def test_repository_no_policy(self): + ecr_client = mock.MagicMock + ecr_client.registries = {} + ecr_client.registries[AWS_REGION_EU_WEST_1] = Registry( + id=AWS_ACCOUNT_NUMBER, + region=AWS_REGION_EU_WEST_1, + scan_type="BASIC", + repositories=[ + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION_EU_WEST_1, + scan_on_push=True, + policy=None, + images_details=None, + lifecycle_policy=None, + ) + ], + rules=[], + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible.ecr_client", + ecr_client, + ): + from prowler.providers.aws.services.ecr.ecr_repositories_not_publicly_accessible.ecr_repositories_not_publicly_accessible import ( + ecr_repositories_not_publicly_accessible, + ) + + check = ecr_repositories_not_publicly_accessible() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Repository {repository_name} is not publicly accessible." ) assert result[0].resource_id == repository_name assert result[0].resource_arn == repository_arn @@ -165,7 +208,7 @@ class Test_ecr_repositories_not_publicly_accessible: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Repository {repository_name} policy may allow anonymous users to perform actions (Principal: '*')." + == f"Repository {repository_name} is publicly accessible." ) assert result[0].resource_id == repository_name assert result[0].resource_arn == repository_arn diff --git a/tests/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability_test.py b/tests/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability_test.py new file mode 100644 index 0000000000..2508cdd161 --- /dev/null +++ b/tests/providers/aws/services/ecr/ecr_repositories_tag_immutability/ecr_repositories_tag_immutability_test.py @@ -0,0 +1,163 @@ +from unittest import mock + +from prowler.providers.aws.services.ecr.ecr_service import Registry, Repository +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + +repository_name = "test_repo" +repository_arn = ( + f"arn:aws:ecr:eu-west-1:{AWS_ACCOUNT_NUMBER}:repository/{repository_name}" +) +repo_policy_public = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ECRRepositoryPolicy", + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:user/username"}, + "Action": ["ecr:DescribeImages", "ecr:DescribeRepositories"], + } + ], +} + + +class Test_ecr_repositories_tag_immutability: + # Mocked Audit Info + + def test_no_registries(self): + ecr_client = mock.MagicMock + ecr_client.registries = {} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_tag_immutability.ecr_repositories_tag_immutability.ecr_client", + ecr_client, + ): + from prowler.providers.aws.services.ecr.ecr_repositories_tag_immutability.ecr_repositories_tag_immutability import ( + ecr_repositories_tag_immutability, + ) + + check = ecr_repositories_tag_immutability() + result = check.execute() + assert len(result) == 0 + + def test_registry_no_repositories(self): + ecr_client = mock.MagicMock + ecr_client.registries = {} + ecr_client.registries[AWS_REGION_EU_WEST_1] = Registry( + id=AWS_ACCOUNT_NUMBER, + region=AWS_REGION_EU_WEST_1, + scan_type="BASIC", + repositories=[], + rules=[], + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_tag_immutability.ecr_repositories_tag_immutability.ecr_client", + ecr_client, + ): + from prowler.providers.aws.services.ecr.ecr_repositories_tag_immutability.ecr_repositories_tag_immutability import ( + ecr_repositories_tag_immutability, + ) + + check = ecr_repositories_tag_immutability() + result = check.execute() + assert len(result) == 0 + + def test_immutable(self): + ecr_client = mock.MagicMock + ecr_client.registries = {} + ecr_client.registries[AWS_REGION_EU_WEST_1] = Registry( + id=AWS_ACCOUNT_NUMBER, + region=AWS_REGION_EU_WEST_1, + scan_type="BASIC", + rules=[], + repositories=[ + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION_EU_WEST_1, + scan_on_push=True, + immutability="IMMUTABLE", + policy=repo_policy_public, + images_details=None, + lifecycle_policy="test-policy", + ) + ], + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_tag_immutability.ecr_repositories_tag_immutability.ecr_client", + ecr_client, + ): + from prowler.providers.aws.services.ecr.ecr_repositories_tag_immutability.ecr_repositories_tag_immutability import ( + ecr_repositories_tag_immutability, + ) + + check = ecr_repositories_tag_immutability() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Repository {repository_name} has immutability configured." + ) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + assert result[0].resource_tags == [] + + def test_mutable(self): + ecr_client = mock.MagicMock + ecr_client.registries = {} + ecr_client.registries[AWS_REGION_EU_WEST_1] = Registry( + id=AWS_ACCOUNT_NUMBER, + region=AWS_REGION_EU_WEST_1, + scan_type="BASIC", + rules=[], + repositories=[ + Repository( + name=repository_name, + arn=repository_arn, + region=AWS_REGION_EU_WEST_1, + scan_on_push=False, + immutability="MUTABLE", + policy=repo_policy_public, + images_details=None, + lifecycle_policy=None, + ) + ], + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider(), + ), mock.patch( + "prowler.providers.aws.services.ecr.ecr_repositories_tag_immutability.ecr_repositories_tag_immutability.ecr_client", + ecr_client, + ): + from prowler.providers.aws.services.ecr.ecr_repositories_tag_immutability.ecr_repositories_tag_immutability import ( + ecr_repositories_tag_immutability, + ) + + check = ecr_repositories_tag_immutability() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Repository {repository_name} does not have immutability configured." + ) + assert result[0].resource_id == repository_name + assert result[0].resource_arn == repository_arn + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/ecr/ecr_service_test.py b/tests/providers/aws/services/ecr/ecr_service_test.py index 50bc53e564..a8aec1b075 100644 --- a/tests/providers/aws/services/ecr/ecr_service_test.py +++ b/tests/providers/aws/services/ecr/ecr_service_test.py @@ -220,6 +220,7 @@ class Test_ECR_Service: ecr_client.create_repository( repositoryName=repo_name, imageScanningConfiguration={"scanOnPush": True}, + imageTagMutability="IMMUTABLE", ) aws_provider = set_mocked_aws_provider() ecr = ECR(aws_provider) @@ -248,6 +249,7 @@ class Test_ECR_Service: ecr_client.create_repository( repositoryName=repo_name, imageScanningConfiguration={"scanOnPush": True}, + imageTagMutability="IMMUTABLE", ) aws_provider = set_mocked_aws_provider() ecr = ECR(aws_provider) @@ -257,6 +259,7 @@ class Test_ECR_Service: assert ecr.registries[AWS_REGION_EU_WEST_1].repositories[0].arn == repo_arn assert ecr.registries[AWS_REGION_EU_WEST_1].repositories[0].scan_on_push assert ecr.registries[AWS_REGION_EU_WEST_1].repositories[0].lifecycle_policy + assert ecr.registries[AWS_REGION_EU_WEST_1].repositories[0].immutability # Test get image details @mock_aws diff --git a/tests/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip_test.py b/tests/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip_test.py new file mode 100644 index 0000000000..f2fc01aa4a --- /dev/null +++ b/tests/providers/aws/services/ecs/ecs_service_no_assign_public_ip/ecs_service_no_assign_public_ip_test.py @@ -0,0 +1,87 @@ +from unittest import mock + +from prowler.providers.aws.services.ecs.ecs_service import Service +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + +SERVICE_ARN = ( + f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:service/sample-service" +) +SERVICE_NAME = "sample-service" + + +class Test_ecs_service_no_assign_public_ip: + def test_no_services(self): + ecs_client = mock.MagicMock + ecs_client.services = {} + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_service_no_assign_public_ip.ecs_service_no_assign_public_ip import ( + ecs_service_no_assign_public_ip, + ) + + check = ecs_service_no_assign_public_ip() + result = check.execute() + assert len(result) == 0 + + def test_service_with_no_public_ip(self): + ecs_client = mock.MagicMock + ecs_client.services = {} + ecs_client.services[SERVICE_ARN] = Service( + name=SERVICE_NAME, + arn=SERVICE_ARN, + region=AWS_REGION_US_EAST_1, + assign_public_ip=False, + tags=[], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_service_no_assign_public_ip.ecs_service_no_assign_public_ip import ( + ecs_service_no_assign_public_ip, + ) + + check = ecs_service_no_assign_public_ip() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECS Service {SERVICE_NAME} does not have automatic public IP assignment." + ) + assert result[0].resource_id == SERVICE_NAME + assert result[0].resource_arn == SERVICE_ARN + + def test_task_definition_no_host_network_mode(self): + ecs_client = mock.MagicMock + ecs_client.services = {} + ecs_client.services[SERVICE_ARN] = Service( + name=SERVICE_NAME, + arn=SERVICE_ARN, + region=AWS_REGION_US_EAST_1, + assign_public_ip=True, + tags=[], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_service_no_assign_public_ip.ecs_service_no_assign_public_ip import ( + ecs_service_no_assign_public_ip, + ) + + check = ecs_service_no_assign_public_ip() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECS Service {SERVICE_NAME} has automatic public IP assignment." + ) + assert result[0].resource_id == SERVICE_NAME + assert result[0].resource_arn == SERVICE_ARN diff --git a/tests/providers/aws/services/ecs/ecs_service_test.py b/tests/providers/aws/services/ecs/ecs_service_test.py index f6a732dce9..26cbfbfc0b 100644 --- a/tests/providers/aws/services/ecs/ecs_service_test.py +++ b/tests/providers/aws/services/ecs/ecs_service_test.py @@ -1,11 +1,83 @@ from unittest.mock import patch -from boto3 import client -from moto import mock_aws +import botocore from prowler.providers.aws.services.ecs.ecs_service import ECS from tests.providers.aws.utils import AWS_REGION_EU_WEST_1, set_mocked_aws_provider +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "ListTaskDefinitions": + return { + "taskDefinitionArns": [ + "arn:aws:ecs:eu-west-1:123456789012:task-definition/test_ecs_task:1" + ] + } + if operation_name == "DescribeTaskDefinition": + return { + "taskDefinition": { + "containerDefinitions": [ + { + "name": "test-container", + "image": "test-image", + "environment": [ + {"name": "DB_PASSWORD", "value": "pass-12343"}, + ], + } + ], + "networkMode": "host", + "pidMode": "host", + "tags": [], + } + } + if operation_name == "ListServices": + return { + "serviceArns": [ + "arn:aws:ecs:eu-west-1:123456789012:service/test_cluster_1/test_ecs_service" + ] + } + if operation_name == "DescribeServices": + return { + "services": [ + { + "serviceArn": "arn:aws:ecs:eu-west-1:123456789012:service/test_cluster_1/test_ecs_service", + "serviceName": "test_ecs_service", + "launchType": "EC2", + "networkConfiguration": { + "awsvpcConfiguration": { + "subnets": ["subnet-12345678"], + "securityGroups": ["sg-12345678"], + "assignPublicIp": "ENABLED", + } + }, + } + ] + } + if operation_name == "ListClusters": + return { + "clusterArns": [ + "arn:aws:ecs:eu-west-1:123456789012:cluster/test_cluster_1", + ] + } + if operation_name == "DescribeClusters": + return { + "clusters": [ + { + "clusterArn": "arn:aws:ecs:eu-west-1:123456789012:cluster/test_cluster_1", + "clusterName": "test_cluster_1", + "status": "ACTIVE", + "tags": [{"key": "Name", "value": "test_cluster_1"}], + "registeredContainerInstancesCount": 5, + "runningTasksCount": 10, + "pendingTasksCount": 1, + "activeServicesCount": 2, + }, + ] + } + return make_api_call(self, operation_name, kwarg) + def mock_generate_regional_clients(provider, service): regional_client = provider._session.current_session.client( @@ -40,94 +112,103 @@ class Test_ECS_Service: assert ecs.session.__class__.__name__ == "Session" # Test list ECS task definitions - @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) def test_list_task_definitions(self): - ecs_client = client("ecs", region_name=AWS_REGION_EU_WEST_1) - - definition = dict( - family="test_ecs_task", - containerDefinitions=[ - { - "name": "hello_world", - "image": "hello-world:latest", - "memory": 400, - } - ], - ) - - task_definition = ecs_client.register_task_definition(**definition) aws_provider = set_mocked_aws_provider() ecs = ECS(aws_provider) - assert len(ecs.task_definitions) == 1 - assert ( - ecs.task_definitions[0].name == task_definition["taskDefinition"]["family"] - ) - assert ( - ecs.task_definitions[0].arn - == task_definition["taskDefinition"]["taskDefinitionArn"] - ) - assert ecs.task_definitions[0].environment_variables == [] + task_arn = "arn:aws:ecs:eu-west-1:123456789012:task-definition/test_ecs_task:1" - @mock_aws + assert len(ecs.task_definitions) == 1 + assert ecs.task_definitions[task_arn].name == "test_ecs_task" + assert ecs.task_definitions[task_arn].arn == task_arn + assert ecs.task_definitions[task_arn].revision == "1" + assert ecs.task_definitions[task_arn].region == AWS_REGION_EU_WEST_1 + + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) # Test describe ECS task definitions - def test__describe_task_definitions__(self): - ecs_client = client("ecs", region_name=AWS_REGION_EU_WEST_1) - - definition = dict( - family="test_ecs_task", - containerDefinitions=[ - { - "name": "hello_world", - "image": "hello-world:latest", - "memory": 400, - "environment": [ - {"name": "test-env", "value": "test-env-value"}, - {"name": "test-env2", "value": "test-env-value2"}, - ], - } - ], - tags=[ - {"key": "test", "value": "test"}, - ], - ) - - task_definition = ecs_client.register_task_definition(**definition) + def test_describe_task_definitions(self): aws_provider = set_mocked_aws_provider() ecs = ECS(aws_provider) + task_arn = "arn:aws:ecs:eu-west-1:123456789012:task-definition/test_ecs_task:1" + assert len(ecs.task_definitions) == 1 + assert ecs.task_definitions[task_arn].name == "test_ecs_task" + assert ecs.task_definitions[task_arn].arn == task_arn + assert ecs.task_definitions[task_arn].revision == "1" + assert ecs.task_definitions[task_arn].region == AWS_REGION_EU_WEST_1 + assert len(ecs.task_definitions[task_arn].container_definitions) == 1 assert ( - ecs.task_definitions[0].name == task_definition["taskDefinition"]["family"] + ecs.task_definitions[task_arn].container_definitions[0].name + == "test-container" ) - assert ecs.task_definitions[0].tags == [ - {"key": "test", "value": "test"}, + assert ( + len(ecs.task_definitions[task_arn].container_definitions[0].environment) + == 1 + ) + assert ( + ecs.task_definitions[task_arn].container_definitions[0].environment[0].name + == "DB_PASSWORD" + ) + assert ( + ecs.task_definitions[task_arn].container_definitions[0].environment[0].value + == "pass-12343" + ) + assert ecs.task_definitions[task_arn].network_mode == "host" + assert not ecs.task_definitions[task_arn].container_definitions[0].privileged + assert ecs.task_definitions[task_arn].container_definitions[0].user == "" + assert ecs.task_definitions[task_arn].container_definitions[0].log_driver == "" + assert ecs.task_definitions[task_arn].pid_mode == "host" + assert ( + not ecs.task_definitions[task_arn] + .container_definitions[0] + .readonly_rootfilesystem + ) + + # Test list ECS clusters + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + def test_list_clusters(self): + aws_provider = set_mocked_aws_provider() + ecs = ECS(aws_provider) + + cluster_arn1 = "arn:aws:ecs:eu-west-1:123456789012:cluster/test_cluster_1" + + assert len(ecs.clusters) == 1 + assert ecs.clusters[cluster_arn1].name == "test_cluster_1" + assert ecs.clusters[cluster_arn1].arn == cluster_arn1 + assert ecs.clusters[cluster_arn1].region == AWS_REGION_EU_WEST_1 + + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + # Test describe ECS clusters + def test_describe_clusters(self): + aws_provider = set_mocked_aws_provider() + ecs = ECS(aws_provider) + + cluster_arn1 = "arn:aws:ecs:eu-west-1:123456789012:cluster/test_cluster_1" + + assert len(ecs.clusters) == 1 + assert ecs.clusters[cluster_arn1].name == "test_cluster_1" + assert ecs.clusters[cluster_arn1].arn == cluster_arn1 + assert ecs.clusters[cluster_arn1].region == AWS_REGION_EU_WEST_1 + assert ecs.clusters[cluster_arn1].services + assert ecs.clusters[cluster_arn1].tags == [ + {"key": "Name", "value": "test_cluster_1"} ] - assert ( - ecs.task_definitions[0].arn - == task_definition["taskDefinition"]["taskDefinitionArn"] - ) - assert ( - ecs.task_definitions[0].environment_variables[0].name - == task_definition["taskDefinition"]["containerDefinitions"][0][ - "environment" - ][0]["name"] - ) - assert ( - ecs.task_definitions[0].environment_variables[0].value - == task_definition["taskDefinition"]["containerDefinitions"][0][ - "environment" - ][0]["value"] - ) - assert ( - ecs.task_definitions[0].environment_variables[1].name - == task_definition["taskDefinition"]["containerDefinitions"][0][ - "environment" - ][1]["name"] - ) - assert ( - ecs.task_definitions[0].environment_variables[1].value - == task_definition["taskDefinition"]["containerDefinitions"][0][ - "environment" - ][1]["value"] + + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + # Test describe ECS services + def test_describe_services(self): + aws_provider = set_mocked_aws_provider() + ecs = ECS(aws_provider) + + service_arn = ( + "arn:aws:ecs:eu-west-1:123456789012:service/test_cluster_1/test_ecs_service" ) + + assert len(ecs.services) == 1 + assert ecs.services[service_arn].name == "test_ecs_service" + assert ecs.services[service_arn].arn == service_arn + assert ecs.services[service_arn].region == AWS_REGION_EU_WEST_1 + assert ecs.services[service_arn].assign_public_ip + assert ecs.services[service_arn].tags == [] diff --git a/tests/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access_test.py b/tests/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access_test.py new file mode 100644 index 0000000000..13417518dd --- /dev/null +++ b/tests/providers/aws/services/ecs/ecs_task_definitions_containers_readonly_access/ecs_task_definitions_containers_readonly_access_test.py @@ -0,0 +1,149 @@ +from unittest import mock + +from prowler.providers.aws.services.ecs.ecs_service import ( + ContainerDefinition, + TaskDefinition, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + +TASK_NAME = "test-task-readonly" +TASK_REVISION = "1" +CONTAINER_NAME = "test-container" +TASK_ARN = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}" + + +class Test_ecs_task_definitions_containers_readonly_access: + def test_no_task_definitions(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import ( + ecs_task_definitions_containers_readonly_access, + ) + + check = ecs_task_definitions_containers_readonly_access() + result = check.execute() + assert len(result) == 0 + + def test_task_definition_all_containers_readonly(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="bridge", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + readonly_rootfilesystem=True, + privileged=False, + user="appuser", + environment=[], + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import ( + ecs_task_definitions_containers_readonly_access, + ) + + check = ecs_task_definitions_containers_readonly_access() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} does not have containers with write access to the root filesystems." + ) + + def test_task_definition_some_containers_not_readonly(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="bridge", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + readonly_rootfilesystem=False, + privileged=False, + user="appuser", + environment=[], + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import ( + ecs_task_definitions_containers_readonly_access, + ) + + check = ecs_task_definitions_containers_readonly_access() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} has containers with write access to the root filesystem: {CONTAINER_NAME}" + ) + + def test_task_definition_mixed_containers(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = { + TASK_ARN: TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="bridge", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + readonly_rootfilesystem=False, + privileged=False, + user="appuser", + environment=[], + ), + ContainerDefinition( + name="readonly-container", + readonly_rootfilesystem=True, + privileged=False, + user="appuser", + environment=[], + ), + ], + ) + } + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_containers_readonly_access.ecs_task_definitions_containers_readonly_access import ( + ecs_task_definitions_containers_readonly_access, + ) + + check = ecs_task_definitions_containers_readonly_access() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} has containers with write access to the root filesystem: {CONTAINER_NAME}" + ) diff --git a/tests/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared_test.py b/tests/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared_test.py new file mode 100644 index 0000000000..c88a9a6c7e --- /dev/null +++ b/tests/providers/aws/services/ecs/ecs_task_definitions_host_namespace_not_shared/ecs_task_definitions_host_namespace_not_shared_test.py @@ -0,0 +1,163 @@ +from unittest import mock + +from prowler.providers.aws.services.ecs.ecs_service import ( + ContainerDefinition, + ContainerEnvVariable, + TaskDefinition, +) +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +task_name = "test-task" +task_revision = "1" +container_name = "test-container" +task_arn = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}" + + +class Test_ecs_task_definitions_host_namespace_not_shared: + def test_no_task_definitions(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_host_namespace_not_shared.ecs_task_definitions_host_namespace_not_shared import ( + ecs_task_definitions_host_namespace_not_shared, + ) + + check = ecs_task_definitions_host_namespace_not_shared() + result = check.execute() + assert len(result) == 0 + + def test_task_definition_no_host_pid_mode(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[task_arn] = TaskDefinition( + name=task_name, + arn=task_arn, + revision=task_revision, + region=AWS_REGION_US_EAST_1, + network_mode="bridge", + pid_mode="task", + container_definitions=[ + ContainerDefinition( + name=container_name, + privileged=False, + user="", + environment=[ + ContainerEnvVariable( + name="env_var_name_no_secrets", + value="env_var_value_no_secrets", + ) + ], + ) + ], + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_host_namespace_not_shared.ecs_task_definitions_host_namespace_not_shared import ( + ecs_task_definitions_host_namespace_not_shared, + ) + + check = ecs_task_definitions_host_namespace_not_shared() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECS task definition {task_name} with revision {task_revision} does not share a host's process namespace with its containers." + ) + + def test_task_definition_host_pid_mode(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[task_arn] = TaskDefinition( + name=task_name, + arn=task_arn, + revision=task_revision, + region=AWS_REGION_US_EAST_1, + network_mode="host", + pid_mode="host", + container_definitions=[ + ContainerDefinition( + name=container_name, + privileged=False, + user="root", + environment=[], + ) + ], + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_host_namespace_not_shared.ecs_task_definitions_host_namespace_not_shared import ( + ecs_task_definitions_host_namespace_not_shared, + ) + + check = ecs_task_definitions_host_namespace_not_shared() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECS task definition {task_name} with revision {task_revision} is configured to share a host's process namespace with its containers." + ) + + def test_task_definition_no_pid_mode(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[task_arn] = TaskDefinition( + name=task_name, + arn=f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}", + revision=task_revision, + region=AWS_REGION_US_EAST_1, + network_mode="host", + pid_mode="", + container_definitions=[ + ContainerDefinition( + name=container_name, + privileged=True, + user="root", + environment=[], + ) + ], + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_host_namespace_not_shared.ecs_task_definitions_host_namespace_not_shared import ( + ecs_task_definitions_host_namespace_not_shared, + ) + + check = ecs_task_definitions_host_namespace_not_shared() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECS task definition {task_name} with revision {task_revision} does not share a host's process namespace with its containers." + ) diff --git a/tests/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users_test.py b/tests/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users_test.py new file mode 100644 index 0000000000..827d085de3 --- /dev/null +++ b/tests/providers/aws/services/ecs/ecs_task_definitions_host_networking_mode_users/ecs_task_definitions_host_networking_mode_users_test.py @@ -0,0 +1,180 @@ +from unittest import mock + +from prowler.providers.aws.services.ecs.ecs_service import ( + ContainerDefinition, + ContainerEnvVariable, + TaskDefinition, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + +TASK_NAME = "test-task-hostmode" +TASK_REVISION = "1" +CONTAINER_NAME = "test-container" +TASK_ARN = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}" + + +class Test_ecs_task_definitions_host_networking_mode_users: + def test_no_task_definitions(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_host_networking_mode_users.ecs_task_definitions_host_networking_mode_users import ( + ecs_task_definitions_host_networking_mode_users, + ) + + check = ecs_task_definitions_host_networking_mode_users() + result = check.execute() + assert len(result) == 0 + + def test_task_definition_no_host_network_mode(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="bridge", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + privileged=False, + user="", + environment=[ + ContainerEnvVariable( + name="env_var_name_no_secrets", + value="env_var_value_no_secrets", + ) + ], + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_host_networking_mode_users.ecs_task_definitions_host_networking_mode_users import ( + ecs_task_definitions_host_networking_mode_users, + ) + + check = ecs_task_definitions_host_networking_mode_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} does not have host network mode." + ) + + def test_task_definition_host_mode_container_root_non_privileged(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="host", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + privileged=False, + user="root", + environment=[], + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_host_networking_mode_users.ecs_task_definitions_host_networking_mode_users import ( + ecs_task_definitions_host_networking_mode_users, + ) + + check = ecs_task_definitions_host_networking_mode_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} has containers with host network mode and non-privileged containers running as root or with no user specified: {CONTAINER_NAME}" + ) + + def test_task_definition_host_mode_container_privileged(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}", + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="host", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + privileged=True, + user="root", + environment=[], + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_host_networking_mode_users.ecs_task_definitions_host_networking_mode_users import ( + ecs_task_definitions_host_networking_mode_users, + ) + + check = ecs_task_definitions_host_networking_mode_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} has host network mode but no containers running as root or with no user specified." + ) + + def test_task_definition_host_mode_container_not_root(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="host", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + privileged=False, + user="appuser", + environment=[], + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_host_networking_mode_users.ecs_task_definitions_host_networking_mode_users import ( + ecs_task_definitions_host_networking_mode_users, + ) + + check = ecs_task_definitions_host_networking_mode_users() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} has host network mode but no containers running as root or with no user specified." + ) diff --git a/tests/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled_test.py b/tests/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled_test.py new file mode 100644 index 0000000000..037ac54bd5 --- /dev/null +++ b/tests/providers/aws/services/ecs/ecs_task_definitions_logging_enabled/ecs_task_definitions_logging_enabled_test.py @@ -0,0 +1,166 @@ +from unittest import mock + +import botocore + +from prowler.providers.aws.services.ecs.ecs_service import ( + ContainerDefinition, + TaskDefinition, +) +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +TASK_NAME = "test-task" +TASK_REVISION = "1" +CONTAINER_NAME = "test-container" +TASK_ARN = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}" + + +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "ListTaskDefinitions": + return { + "taskDefinitionArns": [ + "arn:aws:ecs:eu-west-1:123456789012:task-definition/test-task:1" + ] + } + if operation_name == "DescribeTaskDefinition": + return { + "taskDefinition": { + "containerDefinitions": [ + { + "name": "test-container", + "image": "test-image", + "environment": [ + {"name": "DB_PASSWORD", "value": "pass-12343"}, + ], + } + ], + "networkMode": "host", + "tags": [], + } + } + return make_api_call(self, operation_name, kwarg) + + +class Test_ecs_task_definitions_logging_enabled: + def test_no_task_definitions(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_logging_enabled.ecs_task_definitions_logging_enabled import ( + ecs_task_definitions_logging_enabled, + ) + + check = ecs_task_definitions_logging_enabled() + result = check.execute() + assert len(result) == 0 + + @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + def test_task_definition_no_logconfiguration(self): + + from prowler.providers.aws.services.ecs.ecs_service import ECS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.ecs.ecs_task_definitions_logging_enabled.ecs_task_definitions_logging_enabled.ecs_client", + new=ECS(aws_provider), + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_logging_enabled.ecs_task_definitions_logging_enabled import ( + ecs_task_definitions_logging_enabled, + ) + + check = ecs_task_definitions_logging_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} has containers running with no logging configuration: {CONTAINER_NAME}" + ) + + def test_task_definition_no_logdriver(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="host", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + privileged=True, + user="root", + environment=[], + log_driver="", + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_logging_enabled.ecs_task_definitions_logging_enabled import ( + ecs_task_definitions_logging_enabled, + ) + + check = ecs_task_definitions_logging_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} has containers running with no logging configuration: {CONTAINER_NAME}" + ) + + def test_task_definition_privileged_container(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="host", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + privileged=True, + user="root", + environment=[], + log_driver="awslogs", + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_logging_enabled.ecs_task_definitions_logging_enabled import ( + ecs_task_definitions_logging_enabled, + ) + + check = ecs_task_definitions_logging_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} containers have logging configured." + ) diff --git a/tests/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets_test.py b/tests/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets_test.py index 94904924ca..7defe40eac 100644 --- a/tests/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets_test.py +++ b/tests/providers/aws/services/ecs/ecs_task_definitions_no_environment_secrets/ecs_task_definitions_no_environment_secrets_test.py @@ -1,6 +1,7 @@ from unittest import mock from prowler.providers.aws.services.ecs.ecs_service import ( + ContainerDefinition, ContainerEnvVariable, TaskDefinition, ) @@ -9,6 +10,7 @@ AWS_REGION = "eu-west-1" AWS_ACCOUNT_NUMBER = "123456789012" task_name = "test-task" task_revision = "1" +task_arn = f"arn:aws:ecs:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}" env_var_name_no_secrets = "host" env_var_value_no_secrets = "localhost:1234" env_var_name_with_secrets = "DB_PASSWORD" @@ -18,7 +20,7 @@ env_var_value_with_secrets = "pass-12343" class Test_ecs_task_definitions_no_environment_secrets: def test_no_task_definitions(self): ecs_client = mock.MagicMock - ecs_client.task_definitions = [] + ecs_client.task_definitions = {} with mock.patch( "prowler.providers.aws.services.ecs.ecs_service.ECS", @@ -34,19 +36,24 @@ class Test_ecs_task_definitions_no_environment_secrets: def test_container_env_var_no_secrets(self): ecs_client = mock.MagicMock - ecs_client.task_definitions = [] - ecs_client.task_definitions.append( - TaskDefinition( - name=task_name, - arn=f"arn:aws:ecs:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}", - revision="1", - region=AWS_REGION, - environment_variables=[ - ContainerEnvVariable( - name=env_var_name_no_secrets, value=env_var_value_no_secrets - ) - ], - ) + ecs_client.task_definitions = {} + ecs_client.task_definitions[task_arn] = TaskDefinition( + name=task_name, + arn=f"arn:aws:ecs:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}", + revision="1", + region=AWS_REGION, + container_definitions=[ + ContainerDefinition( + name="container1", + privileged=False, + user="", + environment=[ + ContainerEnvVariable( + name=env_var_name_no_secrets, value=env_var_value_no_secrets + ) + ], + ) + ], ) with mock.patch( @@ -73,19 +80,25 @@ class Test_ecs_task_definitions_no_environment_secrets: def test_container_env_var_with_secrets(self): ecs_client = mock.MagicMock - ecs_client.task_definitions = [] - ecs_client.task_definitions.append( - TaskDefinition( - name=task_name, - arn=f"arn:aws:ecs:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}", - revision="1", - region=AWS_REGION, - environment_variables=[ - ContainerEnvVariable( - name=env_var_name_with_secrets, value=env_var_value_with_secrets - ) - ], - ) + ecs_client.task_definitions = {} + ecs_client.task_definitions[task_arn] = TaskDefinition( + name=task_name, + arn=f"arn:aws:ecs:{AWS_REGION}:{AWS_ACCOUNT_NUMBER}:task-definition/{task_name}:{task_revision}", + revision="1", + region=AWS_REGION, + container_definitions=[ + ContainerDefinition( + name="container1", + privileged=False, + user="", + environment=[ + ContainerEnvVariable( + name=env_var_name_with_secrets, + value=env_var_value_with_secrets, + ) + ], + ) + ], ) with mock.patch( @@ -102,7 +115,7 @@ class Test_ecs_task_definitions_no_environment_secrets: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Potential secret found in variables of ECS task definition {task_name} with revision {task_revision} -> Secret Keyword on line 2." + == f"Potential secrets found in ECS task definition {task_name} with revision {task_revision}: Secrets in container container1 -> Secret Keyword on line 2." ) assert result[0].resource_id == f"{task_name}:1" assert ( diff --git a/tests/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers_test.py b/tests/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers_test.py new file mode 100644 index 0000000000..e167283ef8 --- /dev/null +++ b/tests/providers/aws/services/ecs/ecs_task_definitions_no_privileged_containers/ecs_task_definitions_no_privileged_containers_test.py @@ -0,0 +1,108 @@ +from unittest import mock + +from prowler.providers.aws.services.ecs.ecs_service import ( + ContainerDefinition, + ContainerEnvVariable, + TaskDefinition, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + +TASK_NAME = "test-task" +TASK_REVISION = "1" +CONTAINER_NAME = "test-container" +TASK_ARN = f"arn:aws:ecs:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:task-definition/{TASK_NAME}:{TASK_REVISION}" + + +class Test_ecs_task_definitions_no_privileged_containers: + def test_no_task_definitions(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_no_privileged_containers.ecs_task_definitions_no_privileged_containers import ( + ecs_task_definitions_no_privileged_containers, + ) + + check = ecs_task_definitions_no_privileged_containers() + result = check.execute() + assert len(result) == 0 + + def test_task_definition_no_priviled_container(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="bridge", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + privileged=False, + user="", + environment=[ + ContainerEnvVariable( + name="env_var_name_no_secrets", + value="env_var_value_no_secrets", + ) + ], + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_no_privileged_containers.ecs_task_definitions_no_privileged_containers import ( + ecs_task_definitions_no_privileged_containers, + ) + + check = ecs_task_definitions_no_privileged_containers() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} does not have privileged containers." + ) + + def test_task_definition_privileged_container(self): + ecs_client = mock.MagicMock + ecs_client.task_definitions = {} + ecs_client.task_definitions[TASK_ARN] = TaskDefinition( + name=TASK_NAME, + arn=TASK_ARN, + revision=TASK_REVISION, + region=AWS_REGION_US_EAST_1, + network_mode="host", + container_definitions=[ + ContainerDefinition( + name=CONTAINER_NAME, + privileged=True, + user="root", + environment=[], + ) + ], + ) + + with mock.patch( + "prowler.providers.aws.services.ecs.ecs_service.ECS", + ecs_client, + ): + from prowler.providers.aws.services.ecs.ecs_task_definitions_no_privileged_containers.ecs_task_definitions_no_privileged_containers import ( + ecs_task_definitions_no_privileged_containers, + ) + + check = ecs_task_definitions_no_privileged_containers() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"ECS task definition {TASK_NAME} with revision {TASK_REVISION} has privileged containers: {CONTAINER_NAME}" + ) diff --git a/tests/providers/aws/services/efs/lib/lib_test.py b/tests/providers/aws/services/efs/lib/lib_test.py deleted file mode 100644 index dc486ba914..0000000000 --- a/tests/providers/aws/services/efs/lib/lib_test.py +++ /dev/null @@ -1,102 +0,0 @@ -from prowler.providers.aws.services.efs.lib.lib import is_public_access_allowed - - -class Test_EFS_lib: - def test__is_public_access_allowed__(self): - statement = { - "Effect": "Allow", - "Principal": "*", - "Action": "elasticfilesystem:ClientMount", - "Resource": "*", - } - assert is_public_access_allowed(statement) - - def test__is_public_access_allowed__with_principal_dict(self): - statement = { - "Effect": "Allow", - "Principal": {"AWS": "*"}, - "Action": "elasticfilesystem:ClientMount", - "Resource": "*", - } - assert is_public_access_allowed(statement) - - def test__is_public_access_allowed__with_secure_conditions(self): - statement = { - "Effect": "Allow", - "Principal": "*", - "Action": "elasticfilesystem:ClientMount", - "Resource": "*", - "Condition": {"Bool": {"elasticfilesystem:AccessedViaMountTarget": "true"}}, - } - assert not is_public_access_allowed(statement) - - def test__is_public_access_allowed__with_secure_conditions_and_allowed_conditions( - self, - ): - statement = { - "Effect": "Allow", - "Principal": "*", - "Action": "elasticfilesystem:ClientMount", - "Resource": "*", - "Condition": { - "Bool": {"elasticfilesystem:AccessedViaMountTarget": "true"}, - "StringEquals": {"aws:SourceOwner": "123456789012"}, - }, - } - assert not is_public_access_allowed(statement) - - def test__is_public_access_allowed__with_secure_conditions_and_allowed_conditions_nested( - self, - ): - statement = { - "Effect": "Allow", - "Principal": "*", - "Action": "elasticfilesystem:ClientMount", - "Resource": "*", - "Condition": { - "Bool": {"elasticfilesystem:AccessedViaMountTarget": "true"}, - "StringEquals": {"aws:SourceOwner": "123456789012"}, - "StringEqualsIfExists": {"aws:SourceVpce": "vpce-1234567890abcdef0"}, - }, - } - assert not is_public_access_allowed(statement) - - def test__is_public_access_allowed__with_secure_conditions_and_allowed_conditions_nested_dict( - self, - ): - statement = { - "Effect": "Allow", - "Principal": "*", - "Action": "elasticfilesystem:ClientMount", - "Resource": "*", - "Condition": { - "Bool": {"elasticfilesystem:AccessedViaMountTarget": "true"}, - "StringEquals": {"aws:SourceOwner": "123456789012"}, - "StringEqualsIfExists": { - "aws:SourceVpce": { - "vpce-1234567890abcdef0": "vpce-1234567890abcdef0" - } - }, - }, - } - assert not is_public_access_allowed(statement) - - def test__is_public_access_allowed__with_secure_conditions_and_allowed_conditions_nested_dict_key( - self, - ): - statement = { - "Effect": "Allow", - "Principal": "*", - "Action": "elasticfilesystem:ClientMount", - "Resource": "*", - "Condition": { - "Bool": {"elasticfilesystem:AccessedViaMountTarget": "true"}, - "StringEquals": {"aws:SourceOwner": "123456789012"}, - "StringEqualsIfExists": { - "aws:SourceVpce": { - "vpce-1234567890abcdef0": "vpce-1234567890abcdef0" - } - }, - }, - } - assert not is_public_access_allowed(statement) diff --git a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades_test.py b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades_test.py index 5193d81aa2..553b378820 100644 --- a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades_test.py +++ b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_auto_minor_version_upgrades/elasticache_redis_cluster_auto_minor_version_upgrades_test.py @@ -7,6 +7,7 @@ from prowler.providers.aws.services.elasticache.elasticache_service import ( ) from tests.providers.aws.services.elasticache.elasticache_service_test import ( AUTO_MINOR_VERSION_UPGRADE, + AUTOMATIC_FAILOVER, REPLICATION_GROUP_ARN, REPLICATION_GROUP_ENCRYPTION, REPLICATION_GROUP_ID, @@ -14,7 +15,6 @@ from tests.providers.aws.services.elasticache.elasticache_service_test import ( REPLICATION_GROUP_SNAPSHOT_RETENTION, REPLICATION_GROUP_STATUS, REPLICATION_GROUP_TAGS, - REPLICATION_GROUP_TRANSIT_ENCRYPTION, ) from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider @@ -52,49 +52,6 @@ class Test_elasticache_redis_cluster_auto_minor_version_upgrades: result = check.execute() assert len(result) == 0 - def test_elasticache_clusters_auto_minor_version_upgrades_undefined(self): - # Mock ElastiCache Service - elasticache_service = MagicMock - elasticache_service.replication_groups = {} - - elasticache_service.replication_groups[REPLICATION_GROUP_ARN] = ( - ReplicationGroup( - arn=REPLICATION_GROUP_ARN, - id=REPLICATION_GROUP_ID, - region=AWS_REGION_US_EAST_1, - status=REPLICATION_GROUP_STATUS, - snapshot_retention=REPLICATION_GROUP_SNAPSHOT_RETENTION, - encrypted=REPLICATION_GROUP_ENCRYPTION, - transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION, - multi_az=REPLICATION_GROUP_MULTI_AZ, - tags=REPLICATION_GROUP_TAGS, - ) - ) - - with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), - ), mock.patch( - "prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache", - new=elasticache_service, - ): - from prowler.providers.aws.services.elasticache.elasticache_redis_cluster_auto_minor_version_upgrades.elasticache_redis_cluster_auto_minor_version_upgrades import ( - elasticache_redis_cluster_auto_minor_version_upgrades, - ) - - check = elasticache_redis_cluster_auto_minor_version_upgrades() - result = check.execute() - assert len(result) == 1 - assert result[0].status == "FAIL" - assert ( - result[0].status_extended - == f"Elasticache Redis cache cluster {REPLICATION_GROUP_ID} does not have automated minor version upgrades enabled." - ) - assert result[0].region == AWS_REGION_US_EAST_1 - assert result[0].resource_id == REPLICATION_GROUP_ID - assert result[0].resource_arn == REPLICATION_GROUP_ARN - assert result[0].resource_tags == REPLICATION_GROUP_TAGS - def test_elasticache_clusters_auto_minor_version_upgrades_disabled(self): # Mock ElastiCache Service elasticache_service = MagicMock @@ -112,6 +69,9 @@ class Test_elasticache_redis_cluster_auto_minor_version_upgrades: multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, auto_minor_version_upgrade=not AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) ) @@ -156,6 +116,9 @@ class Test_elasticache_redis_cluster_auto_minor_version_upgrades: multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) ) diff --git a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled_test.py b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled_test.py new file mode 100644 index 0000000000..a9863be907 --- /dev/null +++ b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_automatic_failover_enabled/elasticache_redis_cluster_automatic_failover_enabled_test.py @@ -0,0 +1,147 @@ +from unittest import mock + +from mock import MagicMock + +from prowler.providers.aws.services.elasticache.elasticache_service import ( + ReplicationGroup, +) +from tests.providers.aws.services.elasticache.elasticache_service_test import ( + AUTO_MINOR_VERSION_UPGRADE, + AUTOMATIC_FAILOVER, + REPLICATION_GROUP_ARN, + REPLICATION_GROUP_ENCRYPTION, + REPLICATION_GROUP_ID, + REPLICATION_GROUP_MULTI_AZ, + REPLICATION_GROUP_SNAPSHOT_RETENTION, + REPLICATION_GROUP_STATUS, + REPLICATION_GROUP_TAGS, +) +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +VPC_ID = "vpc-12345678901234567" + + +class Test_elasticache_redis_cluster_automatic_failover_enabled: + def test_elasticache_no_clusters(self): + # Mock VPC Service + vpc_client = MagicMock + vpc_client.vpc_subnets = {} + + # Mock ElastiCache Service + elasticache_service = MagicMock + elasticache_service.replication_groups = {} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache", + new=elasticache_service, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_service.VPC", + new=vpc_client, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_client.vpc_client", + new=vpc_client, + ): + from prowler.providers.aws.services.elasticache.elasticache_redis_cluster_automatic_failover_enabled.elasticache_redis_cluster_automatic_failover_enabled import ( + elasticache_redis_cluster_automatic_failover_enabled, + ) + + check = elasticache_redis_cluster_automatic_failover_enabled() + result = check.execute() + assert len(result) == 0 + + def test_elasticache_clusters_automatic_failover_disabled(self): + # Mock ElastiCache Service + elasticache_service = MagicMock + elasticache_service.replication_groups = {} + + elasticache_service.replication_groups[REPLICATION_GROUP_ARN] = ( + ReplicationGroup( + arn=REPLICATION_GROUP_ARN, + id=REPLICATION_GROUP_ID, + region=AWS_REGION_US_EAST_1, + status=REPLICATION_GROUP_STATUS, + snapshot_retention=REPLICATION_GROUP_SNAPSHOT_RETENTION, + encrypted=REPLICATION_GROUP_ENCRYPTION, + transit_encryption=False, + multi_az=REPLICATION_GROUP_MULTI_AZ, + tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=not AUTO_MINOR_VERSION_UPGRADE, + automatic_failover="disabled", + engine_version="6.0", + auth_token_enabled=False, + ) + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache", + new=elasticache_service, + ): + from prowler.providers.aws.services.elasticache.elasticache_redis_cluster_automatic_failover_enabled.elasticache_redis_cluster_automatic_failover_enabled import ( + elasticache_redis_cluster_automatic_failover_enabled, + ) + + check = elasticache_redis_cluster_automatic_failover_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Elasticache Redis cache cluster {REPLICATION_GROUP_ID} does not have automatic failover enabled." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == REPLICATION_GROUP_ID + assert result[0].resource_arn == REPLICATION_GROUP_ARN + assert result[0].resource_tags == REPLICATION_GROUP_TAGS + + def test_elasticache_clusters_automatic_failover_enabled(self): + # Mock ElastiCache Service + elasticache_service = MagicMock + elasticache_service.replication_groups = {} + + elasticache_service.replication_groups[REPLICATION_GROUP_ARN] = ( + ReplicationGroup( + arn=REPLICATION_GROUP_ARN, + id=REPLICATION_GROUP_ID, + region=AWS_REGION_US_EAST_1, + status=REPLICATION_GROUP_STATUS, + snapshot_retention=REPLICATION_GROUP_SNAPSHOT_RETENTION, + encrypted=REPLICATION_GROUP_ENCRYPTION, + transit_encryption=False, + multi_az=REPLICATION_GROUP_MULTI_AZ, + tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, + ) + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache", + new=elasticache_service, + ): + from prowler.providers.aws.services.elasticache.elasticache_redis_cluster_automatic_failover_enabled.elasticache_redis_cluster_automatic_failover_enabled import ( + elasticache_redis_cluster_automatic_failover_enabled, + ) + + check = elasticache_redis_cluster_automatic_failover_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Elasticache Redis cache cluster {REPLICATION_GROUP_ID} does have automatic failover enabled." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == REPLICATION_GROUP_ID + assert result[0].resource_arn == REPLICATION_GROUP_ARN + assert result[0].resource_tags == REPLICATION_GROUP_TAGS diff --git a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_backup_enabled/elasticache_redis_cluster_backup_enabled_test.py b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_backup_enabled/elasticache_redis_cluster_backup_enabled_test.py index ab69a9c844..a2806ad017 100644 --- a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_backup_enabled/elasticache_redis_cluster_backup_enabled_test.py +++ b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_backup_enabled/elasticache_redis_cluster_backup_enabled_test.py @@ -7,6 +7,10 @@ from moto import mock_aws from prowler.providers.aws.services.elasticache.elasticache_service import ( ReplicationGroup, ) +from tests.providers.aws.services.elasticache.elasticache_service_test import ( + AUTO_MINOR_VERSION_UPGRADE, + AUTOMATIC_FAILOVER, +) from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1, @@ -23,7 +27,6 @@ REPLICATION_GROUP_MULTI_AZ = "enabled" REPLICATION_GROUP_TAGS = [ {"Key": "environment", "Value": "test"}, ] - # Patch every AWS call using Boto3 make_api_call = botocore.client.BaseClient._make_api_call @@ -66,6 +69,10 @@ class Test_elasticache_redis_cluster_backup_enabled: transit_encryption=False, multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=not AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) elasticache_client.audit_config = {"minimum_snapshot_retention_period": 7} @@ -109,6 +116,10 @@ class Test_elasticache_redis_cluster_backup_enabled: transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION, multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=not AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) elasticache_client.audit_config = {"minimum_snapshot_retention_period": 7} @@ -153,6 +164,10 @@ class Test_elasticache_redis_cluster_backup_enabled: transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION, multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=not AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) elasticache_client.audit_config = {"minimum_snapshot_retention_period": 1} @@ -196,6 +211,10 @@ class Test_elasticache_redis_cluster_backup_enabled: transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION, multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=not AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) elasticache_client.audit_config = {"minimum_snapshot_retention_period": 3} diff --git a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_in_transit_encryption_enabled/elasticache_redis_cluster_in_transit_encryption_enabled_test.py b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_in_transit_encryption_enabled/elasticache_redis_cluster_in_transit_encryption_enabled_test.py index 25c025902a..e6db0f4997 100644 --- a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_in_transit_encryption_enabled/elasticache_redis_cluster_in_transit_encryption_enabled_test.py +++ b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_in_transit_encryption_enabled/elasticache_redis_cluster_in_transit_encryption_enabled_test.py @@ -7,6 +7,10 @@ from moto import mock_aws from prowler.providers.aws.services.elasticache.elasticache_service import ( ReplicationGroup, ) +from tests.providers.aws.services.elasticache.elasticache_service_test import ( + AUTO_MINOR_VERSION_UPGRADE, + AUTOMATIC_FAILOVER, +) from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1, @@ -68,6 +72,10 @@ class Test_elasticache_replication_group_in_transit_encryption_enabled: transit_encryption=False, multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) ) @@ -112,6 +120,10 @@ class Test_elasticache_replication_group_in_transit_encryption_enabled: transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION, multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) ) diff --git a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_multi_az_enabled/elasticache_redis_cluster_multi_az_enabled_test.py b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_multi_az_enabled/elasticache_redis_cluster_multi_az_enabled_test.py index b0f4bd4b42..c82679ccd0 100644 --- a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_multi_az_enabled/elasticache_redis_cluster_multi_az_enabled_test.py +++ b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_multi_az_enabled/elasticache_redis_cluster_multi_az_enabled_test.py @@ -7,6 +7,10 @@ from moto import mock_aws from prowler.providers.aws.services.elasticache.elasticache_service import ( ReplicationGroup, ) +from tests.providers.aws.services.elasticache.elasticache_service_test import ( + AUTO_MINOR_VERSION_UPGRADE, + AUTOMATIC_FAILOVER, +) from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1, @@ -68,6 +72,10 @@ class Test_elasticache_replication_group_multi_az_enabled: transit_encryption=False, multi_az="disabled", tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) ) @@ -112,6 +120,10 @@ class Test_elasticache_replication_group_multi_az_enabled: transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION, multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) ) diff --git a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_rest_encryption_enabled/elasticache_redis_cluster_rest_encryption_enabled_test.py b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_rest_encryption_enabled/elasticache_redis_cluster_rest_encryption_enabled_test.py index 9c0a371701..913f5f8948 100644 --- a/tests/providers/aws/services/elasticache/elasticache_redis_cluster_rest_encryption_enabled/elasticache_redis_cluster_rest_encryption_enabled_test.py +++ b/tests/providers/aws/services/elasticache/elasticache_redis_cluster_rest_encryption_enabled/elasticache_redis_cluster_rest_encryption_enabled_test.py @@ -7,6 +7,10 @@ from moto import mock_aws from prowler.providers.aws.services.elasticache.elasticache_service import ( ReplicationGroup, ) +from tests.providers.aws.services.elasticache.elasticache_service_test import ( + AUTO_MINOR_VERSION_UPGRADE, + AUTOMATIC_FAILOVER, +) from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1, @@ -67,6 +71,10 @@ class Test_elasticache_replication_group_at_rest_encryption_enabled: transit_encryption=False, multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) ) @@ -111,6 +119,10 @@ class Test_elasticache_replication_group_at_rest_encryption_enabled: transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION, multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, + auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE, + automatic_failover=AUTOMATIC_FAILOVER, + engine_version="6.0", + auth_token_enabled=False, ) ) diff --git a/tests/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled_test.py b/tests/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled_test.py new file mode 100644 index 0000000000..f7425d5398 --- /dev/null +++ b/tests/providers/aws/services/elasticache/elasticache_redis_replication_group_auth_enabled/elasticache_redis_replication_group_auth_enabled_test.py @@ -0,0 +1,227 @@ +from unittest import mock + +from mock import MagicMock, patch +from moto import mock_aws + +from prowler.providers.aws.services.elasticache.elasticache_service import ( + ReplicationGroup, +) +from tests.providers.aws.services.elasticache.elasticache_service_test import ( + REPLICATION_GROUP_ARN, + REPLICATION_GROUP_ENCRYPTION, + REPLICATION_GROUP_ID, + REPLICATION_GROUP_MULTI_AZ, + REPLICATION_GROUP_SNAPSHOT_RETENTION, + REPLICATION_GROUP_STATUS, + REPLICATION_GROUP_TAGS, + REPLICATION_GROUP_TRANSIT_ENCRYPTION, + mock_make_api_call, +) +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +VPC_ID = "vpc-12345678901234567" + + +# Patch every AWS call using Boto3 +@patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) +class Test_elasticache_redis_replication_group_auth_enabled: + @mock_aws + def test_elasticache_no_replication_groups(self): + # Mock VPC Service + vpc_client = MagicMock + vpc_client.vpc_subnets = {} + + # Mock ElastiCache Service + elasticache_service = MagicMock + elasticache_service.replication_groups = {} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache", + new=elasticache_service, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_service.VPC", + new=vpc_client, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_client.vpc_client", + new=vpc_client, + ): + from prowler.providers.aws.services.elasticache.elasticache_redis_replication_group_auth_enabled.elasticache_redis_replication_group_auth_enabled import ( + elasticache_redis_replication_group_auth_enabled, + ) + + check = elasticache_redis_replication_group_auth_enabled() + result = check.execute() + assert len(result) == 0 + + def test_elasticache_no_old_redis_replication_groups(self): + # Mock ElastiCache Service + elasticache_service = MagicMock + engine_version = "6.0" + elasticache_service.replication_groups = { + REPLICATION_GROUP_ARN: ReplicationGroup( + arn=REPLICATION_GROUP_ARN, + id=REPLICATION_GROUP_ID, + region=AWS_REGION_US_EAST_1, + status=REPLICATION_GROUP_STATUS, + snapshot_retention=REPLICATION_GROUP_SNAPSHOT_RETENTION, + encrypted=REPLICATION_GROUP_ENCRYPTION, + transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION, + multi_az=REPLICATION_GROUP_MULTI_AZ, + tags=REPLICATION_GROUP_TAGS, + automatic_failover="enabled", + auto_minor_version_upgrade=False, + engine_version=engine_version, + auth_token_enabled=False, + ) + } + + # Mock VPC Service + vpc_client = MagicMock + vpc_client.vpc_subnets = {} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache", + new=elasticache_service, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_service.VPC", + new=vpc_client, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_client.vpc_client", + new=vpc_client, + ): + from prowler.providers.aws.services.elasticache.elasticache_redis_replication_group_auth_enabled.elasticache_redis_replication_group_auth_enabled import ( + elasticache_redis_replication_group_auth_enabled, + ) + + check = elasticache_redis_replication_group_auth_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "MANUAL" + assert ( + result[0].status_extended + == f"Elasticache Redis replication group {REPLICATION_GROUP_ID} has version {engine_version} which supports Redis ACLs. Please review the ACL configuration." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == REPLICATION_GROUP_ID + assert result[0].resource_arn == REPLICATION_GROUP_ARN + assert result[0].resource_tags == REPLICATION_GROUP_TAGS + + def test_elasticache_redis_replication_group_auth_enabled(self): + # Mock ElastiCache Service + elasticache_service = MagicMock + engine_version = "5.0" + elasticache_service.replication_groups = { + REPLICATION_GROUP_ARN: ReplicationGroup( + arn=REPLICATION_GROUP_ARN, + id=REPLICATION_GROUP_ID, + region=AWS_REGION_US_EAST_1, + status=REPLICATION_GROUP_STATUS, + snapshot_retention=REPLICATION_GROUP_SNAPSHOT_RETENTION, + encrypted=REPLICATION_GROUP_ENCRYPTION, + transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION, + multi_az=REPLICATION_GROUP_MULTI_AZ, + tags=REPLICATION_GROUP_TAGS, + automatic_failover="enabled", + auto_minor_version_upgrade=False, + engine_version=engine_version, + auth_token_enabled=True, + ) + } + + # Mock VPC Service + vpc_client = MagicMock + vpc_client.vpc_subnets = {} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache", + new=elasticache_service, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_service.VPC", + new=vpc_client, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_client.vpc_client", + new=vpc_client, + ): + from prowler.providers.aws.services.elasticache.elasticache_redis_replication_group_auth_enabled.elasticache_redis_replication_group_auth_enabled import ( + elasticache_redis_replication_group_auth_enabled, + ) + + check = elasticache_redis_replication_group_auth_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Elasticache Redis replication group {REPLICATION_GROUP_ID}(v{engine_version}) does have AUTH enabled." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == REPLICATION_GROUP_ID + assert result[0].resource_arn == REPLICATION_GROUP_ARN + assert result[0].resource_tags == REPLICATION_GROUP_TAGS + + def test_elasticache_redis_cluster_auth_disabled(self): + # Mock ElastiCache Service + elasticache_service = MagicMock + engine_version = "4.2" + elasticache_service.replication_groups = { + REPLICATION_GROUP_ARN: ReplicationGroup( + arn=REPLICATION_GROUP_ARN, + id=REPLICATION_GROUP_ID, + region=AWS_REGION_US_EAST_1, + status=REPLICATION_GROUP_STATUS, + snapshot_retention=REPLICATION_GROUP_SNAPSHOT_RETENTION, + encrypted=REPLICATION_GROUP_ENCRYPTION, + transit_encryption=REPLICATION_GROUP_TRANSIT_ENCRYPTION, + multi_az=REPLICATION_GROUP_MULTI_AZ, + tags=REPLICATION_GROUP_TAGS, + automatic_failover="enabled", + auto_minor_version_upgrade=False, + engine_version=engine_version, + auth_token_enabled=False, + ) + } + + # Mock VPC Service + vpc_client = MagicMock + vpc_client.vpc_subnets = {} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.elasticache.elasticache_service.ElastiCache", + new=elasticache_service, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_service.VPC", + new=vpc_client, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_client.vpc_client", + new=vpc_client, + ): + from prowler.providers.aws.services.elasticache.elasticache_redis_replication_group_auth_enabled.elasticache_redis_replication_group_auth_enabled import ( + elasticache_redis_replication_group_auth_enabled, + ) + + check = elasticache_redis_replication_group_auth_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Elasticache Redis replication group {REPLICATION_GROUP_ID}(v{engine_version}) does not have AUTH enabled." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == REPLICATION_GROUP_ID + assert result[0].resource_arn == REPLICATION_GROUP_ARN + assert result[0].resource_tags == REPLICATION_GROUP_TAGS diff --git a/tests/providers/aws/services/elasticache/elasticache_service_test.py b/tests/providers/aws/services/elasticache/elasticache_service_test.py index bbebc7925b..1c5bbba661 100644 --- a/tests/providers/aws/services/elasticache/elasticache_service_test.py +++ b/tests/providers/aws/services/elasticache/elasticache_service_test.py @@ -19,7 +19,7 @@ SUBNET_1 = "subnet-1" SUBNET_2 = "subnet-2" ELASTICACHE_CLUSTER_NAME = "test-cluster" -ELASTICACHE_CLUSTER_ARN = f"arn:aws:elasticache:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:{ELASTICACHE_CLUSTER_NAME}" +ELASTICACHE_CLUSTER_ARN = f"arn:aws:elasticache:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:{ELASTICACHE_CLUSTER_NAME}" ELASTICACHE_ENGINE = "redis" ELASTICACHE_ENGINE_MEMCACHED = "memcached" @@ -38,6 +38,7 @@ REPLICATION_GROUP_TAGS = [ {"Key": "environment", "Value": "test"}, ] AUTO_MINOR_VERSION_UPGRADE = True +AUTOMATIC_FAILOVER = "enabled" # Mocking Access Analyzer Calls @@ -62,6 +63,8 @@ def mock_make_api_call(self, operation_name, kwargs): "Engine": ELASTICACHE_ENGINE, "SecurityGroups": [], "AutoMinorVersionUpgrade": AUTO_MINOR_VERSION_UPGRADE, + "EngineVersion": "6.0", + "AuthTokenEnabled": False, }, ] } @@ -107,6 +110,8 @@ def mock_make_api_call(self, operation_name, kwargs): "AtRestEncryptionEnabled": REPLICATION_GROUP_ENCRYPTION, "ARN": REPLICATION_GROUP_ARN, "AutoMinorVersionUpgrade": AUTO_MINOR_VERSION_UPGRADE, + "Memberclusters": [ELASTICACHE_CLUSTER_NAME], + "AuthTokenEnabled": False, }, ] } @@ -161,7 +166,6 @@ class Test_ElastiCache_Service: assert elasticache.clusters[ELASTICACHE_CLUSTER_ARN] assert elasticache.clusters[ELASTICACHE_CLUSTER_ARN] == Cluster( arn=ELASTICACHE_CLUSTER_ARN, - name=ELASTICACHE_CLUSTER_NAME, id=ELASTICACHE_CLUSTER_NAME, engine=ELASTICACHE_ENGINE, region=AWS_REGION_US_EAST_1, @@ -170,9 +174,11 @@ class Test_ElastiCache_Service: subnets=[SUBNET_1, SUBNET_2], tags=ELASTICACHE_CLUSTER_TAGS, auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE, + engine_version=6.0, + auth_token_enabled=False, ) - # Test Elasticache Redis cache clusters + # Test Elasticache Redis replication_groups def test_describe_replication_groups(self): aws_provider = set_mocked_aws_provider() elasticache = ElastiCache(aws_provider) @@ -192,4 +198,7 @@ class Test_ElastiCache_Service: multi_az=REPLICATION_GROUP_MULTI_AZ, tags=REPLICATION_GROUP_TAGS, auto_minor_version_upgrade=AUTO_MINOR_VERSION_UPGRADE, + auth_token_enabled=False, + automatic_failover="disabled", + engine_version="0.0", ) diff --git a/tests/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled_test.py b/tests/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled_test.py new file mode 100644 index 0000000000..f77c434622 --- /dev/null +++ b/tests/providers/aws/services/elb/elb_connection_draining_enabled/elb_connection_draining_enabled_test.py @@ -0,0 +1,145 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + + +class Test_elb_connection_draining_enabled: + + def test_elb_no_balancers(self): + from prowler.providers.aws.services.elb.elb_service import ELB + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_EU_WEST_1]), + ), mock.patch( + "prowler.providers.aws.services.elb.elb_connection_draining_enabled.elb_connection_draining_enabled.elb_client", + new=ELB(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])), + ): + # Test Check + from prowler.providers.aws.services.elb.elb_connection_draining_enabled.elb_connection_draining_enabled import ( + elb_connection_draining_enabled, + ) + + check = elb_connection_draining_enabled() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_elb_draining_connection_enabled(self): + elb_client = client("elb", region_name=AWS_REGION_EU_WEST_1) + # Create a compliant resource + + elb_client.create_load_balancer( + LoadBalancerName="my-lb", + Listeners=[ + {"Protocol": "tcp", "LoadBalancerPort": 80, "InstancePort": 8080}, + {"Protocol": "http", "LoadBalancerPort": 81, "InstancePort": 9000}, + ], + Scheme="internet-facing", + ) + + elb_client.modify_load_balancer_attributes( + LoadBalancerName="my-lb", + LoadBalancerAttributes={ + "ConnectionDraining": { + "Enabled": True, + "Timeout": 60, + } + }, + ) + + from prowler.providers.aws.services.elb.elb_service import ELB + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.elb.elb_connection_draining_enabled.elb_connection_draining_enabled.elb_client", + new=ELB(aws_provider), + ): + from prowler.providers.aws.services.elb.elb_connection_draining_enabled.elb_connection_draining_enabled import ( + elb_connection_draining_enabled, + ) + + check = elb_connection_draining_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "ELB my-lb has connection draining enabled." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == "my-lb" + assert ( + result[0].resource_arn + == f"arn:aws:elasticloadbalancing:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:loadbalancer/my-lb" + ) + assert result[0].resource_tags == [] + + @mock_aws + def test_elb_draining_connection_disabled(self): + elb_client = client("elb", region_name=AWS_REGION_EU_WEST_1) + # Create a compliant resource + + elb_client.create_load_balancer( + LoadBalancerName="my-lb", + Listeners=[ + {"Protocol": "tcp", "LoadBalancerPort": 80, "InstancePort": 8080}, + {"Protocol": "http", "LoadBalancerPort": 81, "InstancePort": 9000}, + ], + Scheme="internet-facing", + ) + + elb_client.modify_load_balancer_attributes( + LoadBalancerName="my-lb", + LoadBalancerAttributes={ + "ConnectionDraining": { + "Enabled": False, + "Timeout": 60, + } + }, + ) + + from prowler.providers.aws.services.elb.elb_service import ELB + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.elb.elb_connection_draining_enabled.elb_connection_draining_enabled.elb_client", + new=ELB(aws_provider), + ): + from prowler.providers.aws.services.elb.elb_connection_draining_enabled.elb_connection_draining_enabled import ( + elb_connection_draining_enabled, + ) + + check = elb_connection_draining_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "ELB my-lb does not have connection draining enabled." + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_id == "my-lb" + assert ( + result[0].resource_arn + == f"arn:aws:elasticloadbalancing:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:loadbalancer/my-lb" + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/elb/elb_service_test.py b/tests/providers/aws/services/elb/elb_service_test.py index b4eb77a418..0222767e5e 100644 --- a/tests/providers/aws/services/elb/elb_service_test.py +++ b/tests/providers/aws/services/elb/elb_service_test.py @@ -104,6 +104,7 @@ class Test_ELB_Service: "S3BucketPrefix": "s3bf", }, "CrossZoneLoadBalancing": {"Enabled": True}, + "ConnectionDraining": {"Enabled": True, "Timeout": 60}, }, ) elb_arn = f"arn:aws:elasticloadbalancing:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:loadbalancer/my-lb" @@ -115,6 +116,7 @@ class Test_ELB_Service: assert elb.loadbalancers[elb_arn].scheme == "internal" assert elb.loadbalancers[elb_arn].access_logs assert elb.loadbalancers[elb_arn].cross_zone_load_balancing + assert elb.loadbalancers[elb_arn].connection_draining # Test ELB Describe Tags @mock_aws diff --git a/tests/providers/aws/services/globalaccelerator/globalaccelerator_service_test.py b/tests/providers/aws/services/globalaccelerator/globalaccelerator_service_test.py index c73c5d85fe..19c35763fa 100644 --- a/tests/providers/aws/services/globalaccelerator/globalaccelerator_service_test.py +++ b/tests/providers/aws/services/globalaccelerator/globalaccelerator_service_test.py @@ -42,6 +42,8 @@ def mock_make_api_call(self, operation_name, kwarg): } if operation_name == "GetSubscriptionState": return {"SubscriptionState": "ACTIVE"} + if operation_name == "ListTagsForResource": + return {"Tags": [{"Key": "Name", "Value": "TestAccelerator"}]} return make_api_call(self, operation_name, kwarg) @@ -94,3 +96,18 @@ class Test_GlobalAccelerator_Service: == AWS_REGION_US_WEST_2 ) assert globalaccelerator.accelerators[TEST_ACCELERATOR_ARN].enabled + + def test_list_tags(self): + # GlobalAccelerator client for this test class + aws_provider = set_mocked_aws_provider() + globalaccelerator = GlobalAccelerator(aws_provider) + + assert len(globalaccelerator.accelerators) == 1 + assert ( + globalaccelerator.accelerators[TEST_ACCELERATOR_ARN].tags[0]["Key"] + == "Name" + ) + assert ( + globalaccelerator.accelerators[TEST_ACCELERATOR_ARN].tags[0]["Value"] + == "TestAccelerator" + ) diff --git a/tests/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled_test.py b/tests/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled_test.py index 43e0d7f882..b7cff5a676 100644 --- a/tests/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled_test.py +++ b/tests/providers/aws/services/glue/glue_database_connections_ssl_enabled/glue_database_connections_ssl_enabled_test.py @@ -38,6 +38,7 @@ class Test_glue_database_connections_ssl_enabled: }, region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"test": "test"}], ) ] @@ -61,6 +62,7 @@ class Test_glue_database_connections_ssl_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"test": "test"}] def test_glue_table_with_SSL(self): glue_client = mock.MagicMock @@ -77,6 +79,7 @@ class Test_glue_database_connections_ssl_enabled: }, region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"test": "test"}], ) ] @@ -100,3 +103,4 @@ class Test_glue_database_connections_ssl_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"test": "test"}] diff --git a/tests/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled_test.py b/tests/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled_test.py index 6107c216ee..49a21bd768 100644 --- a/tests/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled_test.py +++ b/tests/providers/aws/services/glue/glue_development_endpoints_cloudwatch_logs_encryption_enabled/glue_development_endpoints_cloudwatch_logs_encryption_enabled_test.py @@ -32,6 +32,7 @@ class Test_glue_development_endpoints_cloudwatch_logs_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [ @@ -65,6 +66,7 @@ class Test_glue_development_endpoints_cloudwatch_logs_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_unencrypted_endpoint(self): glue_client = mock.MagicMock @@ -74,6 +76,7 @@ class Test_glue_development_endpoints_cloudwatch_logs_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [ @@ -106,6 +109,7 @@ class Test_glue_development_endpoints_cloudwatch_logs_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_no_sec_configs(self): glue_client = mock.MagicMock @@ -115,6 +119,7 @@ class Test_glue_development_endpoints_cloudwatch_logs_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [] @@ -139,3 +144,4 @@ class Test_glue_development_endpoints_cloudwatch_logs_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] diff --git a/tests/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled_test.py b/tests/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled_test.py index 473b23dc26..4c3011faeb 100644 --- a/tests/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled_test.py +++ b/tests/providers/aws/services/glue/glue_development_endpoints_job_bookmark_encryption_enabled/glue_development_endpoints_job_bookmark_encryption_enabled_test.py @@ -32,6 +32,7 @@ class Test_glue_development_endpoints_job_bookmark_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"test": "value"}], ) ] glue_client.security_configs = [ @@ -65,6 +66,7 @@ class Test_glue_development_endpoints_job_bookmark_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"test": "value"}] def test_glue_unencrypted_endpoint(self): glue_client = mock.MagicMock @@ -74,6 +76,7 @@ class Test_glue_development_endpoints_job_bookmark_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"test": "value"}], ) ] glue_client.security_configs = [ @@ -106,6 +109,7 @@ class Test_glue_development_endpoints_job_bookmark_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"test": "value"}] def test_glue_no_sec_configs(self): glue_client = mock.MagicMock @@ -115,6 +119,7 @@ class Test_glue_development_endpoints_job_bookmark_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"test": "value"}], ) ] glue_client.security_configs = [] @@ -139,3 +144,4 @@ class Test_glue_development_endpoints_job_bookmark_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"test": "value"}] diff --git a/tests/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled_test.py b/tests/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled_test.py index 4b79f34805..c0b88ce77a 100644 --- a/tests/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled_test.py +++ b/tests/providers/aws/services/glue/glue_development_endpoints_s3_encryption_enabled/glue_development_endpoints_s3_encryption_enabled_test.py @@ -32,6 +32,7 @@ class Test_glue_development_endpoints_s3_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [ @@ -65,6 +66,7 @@ class Test_glue_development_endpoints_s3_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_unencrypted_endpoint(self): glue_client = mock.MagicMock @@ -74,6 +76,7 @@ class Test_glue_development_endpoints_s3_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [ @@ -106,6 +109,7 @@ class Test_glue_development_endpoints_s3_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_no_sec_configs(self): glue_client = mock.MagicMock @@ -115,6 +119,7 @@ class Test_glue_development_endpoints_s3_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [] @@ -139,3 +144,4 @@ class Test_glue_development_endpoints_s3_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] diff --git a/tests/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled_test.py b/tests/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled_test.py index 136e6334a7..e617fa81f9 100644 --- a/tests/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled_test.py +++ b/tests/providers/aws/services/glue/glue_etl_jobs_amazon_s3_encryption_enabled/glue_etl_jobs_amazon_s3_encryption_enabled_test.py @@ -33,6 +33,7 @@ class Test_glue_etl_jobs_amazon_s3_encryption_enabled: arguments=None, region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [ @@ -66,6 +67,7 @@ class Test_glue_etl_jobs_amazon_s3_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_unencrypted_job(self): glue_client = mock.MagicMock @@ -76,6 +78,7 @@ class Test_glue_etl_jobs_amazon_s3_encryption_enabled: arguments=None, region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [ @@ -108,6 +111,7 @@ class Test_glue_etl_jobs_amazon_s3_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_no_sec_configs(self): glue_client = mock.MagicMock @@ -117,6 +121,7 @@ class Test_glue_etl_jobs_amazon_s3_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [] @@ -141,6 +146,7 @@ class Test_glue_etl_jobs_amazon_s3_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_encrypted_job_with_argument(self): glue_client = mock.MagicMock @@ -154,6 +160,7 @@ class Test_glue_etl_jobs_amazon_s3_encryption_enabled: }, region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [] @@ -178,3 +185,4 @@ class Test_glue_etl_jobs_amazon_s3_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] diff --git a/tests/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled_test.py b/tests/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled_test.py index fbe3f8b34d..1841f93db4 100644 --- a/tests/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled_test.py +++ b/tests/providers/aws/services/glue/glue_etl_jobs_cloudwatch_logs_encryption_enabled/glue_etl_jobs_cloudwatch_logs_encryption_enabled_test.py @@ -33,6 +33,7 @@ class Test_glue_etl_jobs_cloudwatch_logs_encryption_enabled: arguments=None, region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [ @@ -66,6 +67,7 @@ class Test_glue_etl_jobs_cloudwatch_logs_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_unencrypted_job(self): glue_client = mock.MagicMock @@ -76,6 +78,7 @@ class Test_glue_etl_jobs_cloudwatch_logs_encryption_enabled: arguments=None, region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [ @@ -108,6 +111,7 @@ class Test_glue_etl_jobs_cloudwatch_logs_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_no_sec_configs(self): glue_client = mock.MagicMock @@ -117,6 +121,7 @@ class Test_glue_etl_jobs_cloudwatch_logs_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [] @@ -141,3 +146,4 @@ class Test_glue_etl_jobs_cloudwatch_logs_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] diff --git a/tests/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled_test.py b/tests/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled_test.py index b1936c9c4a..1431369fd6 100644 --- a/tests/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled_test.py +++ b/tests/providers/aws/services/glue/glue_etl_jobs_job_bookmark_encryption_enabled/glue_etl_jobs_job_bookmark_encryption_enabled_test.py @@ -33,6 +33,7 @@ class Test_glue_etl_jobs_job_bookmark_encryption_enabled: arguments=None, region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [ @@ -66,6 +67,7 @@ class Test_glue_etl_jobs_job_bookmark_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_unencrypted_job(self): glue_client = mock.MagicMock @@ -76,6 +78,7 @@ class Test_glue_etl_jobs_job_bookmark_encryption_enabled: arguments=None, region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [ @@ -108,6 +111,7 @@ class Test_glue_etl_jobs_job_bookmark_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] def test_glue_no_sec_configs(self): glue_client = mock.MagicMock @@ -117,6 +121,7 @@ class Test_glue_etl_jobs_job_bookmark_encryption_enabled: security="sec_config", region=AWS_REGION_US_EAST_1, arn="arn_test", + tags=[{"key_test": "value_test"}], ) ] glue_client.security_configs = [] @@ -141,3 +146,4 @@ class Test_glue_etl_jobs_job_bookmark_encryption_enabled: ) assert result[0].resource_id == "test" assert result[0].resource_arn == "arn_test" + assert result[0].resource_tags == [{"key_test": "value_test"}] diff --git a/tests/providers/aws/services/glue/glue_service_test.py b/tests/providers/aws/services/glue/glue_service_test.py index 6ac6367a9f..7a1a69ee50 100644 --- a/tests/providers/aws/services/glue/glue_service_test.py +++ b/tests/providers/aws/services/glue/glue_service_test.py @@ -100,6 +100,12 @@ def mock_make_api_call(self, operation_name, kwarg): }, ], } + elif operation_name == "GetTags": + return { + "Tags": { + "test_key": "test_value", + }, + } return make_api_call(self, operation_name, kwarg) @@ -227,3 +233,11 @@ class Test_Glue_Service: "--enable-job-insights": "false", } assert glue.jobs[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_get_tags(self): + aws_provider = set_mocked_aws_provider() + glue = Glue(aws_provider) + + assert glue.dev_endpoints[0].tags == [{"test_key": "test_value"}] + assert glue.jobs[0].tags == [{"test_key": "test_value"}] diff --git a/tests/providers/aws/services/guardduty/guardduty_is_enabled/guardduty_is_enabled_fixer_test.py b/tests/providers/aws/services/guardduty/guardduty_is_enabled/guardduty_is_enabled_fixer_test.py index bc920390ce..2765af1066 100644 --- a/tests/providers/aws/services/guardduty/guardduty_is_enabled/guardduty_is_enabled_fixer_test.py +++ b/tests/providers/aws/services/guardduty/guardduty_is_enabled/guardduty_is_enabled_fixer_test.py @@ -16,10 +16,14 @@ DETECTOR_ARN = f"arn:aws:guardduty:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:d class Test_guardduty_is_enabled_fixer: @mock_aws def test_guardduty_is_enabled_fixer(self): + regional_client = mock.MagicMock() guardduty_client = mock.MagicMock guardduty_client.region = AWS_REGION_EU_WEST_1 guardduty_client.detectors = [] guardduty_client.audited_account_arn = AWS_ACCOUNT_ARN + regional_client.create_detector.return_value = None + guardduty_client.regional_clients = {AWS_REGION_EU_WEST_1: regional_client} + with mock.patch( "prowler.providers.aws.services.guardduty.guardduty_service.GuardDuty", guardduty_client, diff --git a/tests/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled_test.py b/tests/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled_test.py new file mode 100644 index 0000000000..5587d9e0d1 --- /dev/null +++ b/tests/providers/aws/services/guardduty/guardduty_rds_protection_enabled/guardduty_rds_protection_enabled_test.py @@ -0,0 +1,139 @@ +from unittest import mock + +from prowler.providers.aws.services.guardduty.guardduty_service import Detector +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + + +class Test_guardduty_rds_protection_enabled: + def test_no_detectors(self): + guardduty_client = mock.MagicMock() + guardduty_client.detectors = [] + + with mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_service.GuardDuty", + new=guardduty_client, + ), mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_rds_protection_enabled.guardduty_rds_protection_enabled.guardduty_client", + new=guardduty_client, + ): + + from prowler.providers.aws.services.guardduty.guardduty_rds_protection_enabled.guardduty_rds_protection_enabled import ( + guardduty_rds_protection_enabled, + ) + + check = guardduty_rds_protection_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_detector_disabled(self): + guardduty_client = mock.MagicMock() + detector_arn = f"arn:aws:guardduty:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:detector/1234567890" + guardduty_client.detectors = [ + Detector( + id="1234567890", + arn=detector_arn, + region=AWS_REGION_US_EAST_1, + tags=[], + enabled_in_account=False, + rds_protection=False, + ) + ] + + with mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_service.GuardDuty", + new=guardduty_client, + ), mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_rds_protection_enabled.guardduty_rds_protection_enabled.guardduty_client", + new=guardduty_client, + ): + + from prowler.providers.aws.services.guardduty.guardduty_rds_protection_enabled.guardduty_rds_protection_enabled import ( + guardduty_rds_protection_enabled, + ) + + check = guardduty_rds_protection_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_detector_enabled_rds_protection_disabled(self): + guardduty_client = mock.MagicMock() + detector_arn = f"arn:aws:guardduty:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:detector/1234567890" + guardduty_client.detectors = [ + Detector( + id="1234567890", + arn=detector_arn, + region=AWS_REGION_US_EAST_1, + tags=[], + enabled_in_account=True, + rds_protection=False, + status=True, + ) + ] + + with mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_service.GuardDuty", + new=guardduty_client, + ), mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_rds_protection_enabled.guardduty_rds_protection_enabled.guardduty_client", + new=guardduty_client, + ): + + from prowler.providers.aws.services.guardduty.guardduty_rds_protection_enabled.guardduty_rds_protection_enabled import ( + guardduty_rds_protection_enabled, + ) + + check = guardduty_rds_protection_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "GuardDuty detector does not have RDS Protection enabled." + ) + assert result[0].resource_id == "1234567890" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == detector_arn + assert result[0].resource_tags == [] + + def test_detector_enabled_rds_protection_enabled(self): + guardduty_client = mock.MagicMock() + detector_arn = f"arn:aws:guardduty:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:detector/1234567890" + guardduty_client.detectors = [ + Detector( + id="1234567890", + arn=detector_arn, + region=AWS_REGION_US_EAST_1, + tags=[], + enabled_in_account=True, + rds_protection=True, + status=True, + ) + ] + + with mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_service.GuardDuty", + new=guardduty_client, + ), mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_rds_protection_enabled.guardduty_rds_protection_enabled.guardduty_client", + new=guardduty_client, + ): + from prowler.providers.aws.services.guardduty.guardduty_rds_protection_enabled.guardduty_rds_protection_enabled import ( + guardduty_rds_protection_enabled, + ) + + check = guardduty_rds_protection_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "GuardDuty detector has RDS Protection enabled." + ) + assert result[0].resource_id == "1234567890" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == detector_arn + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled_test.py b/tests/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled_test.py new file mode 100644 index 0000000000..731d075a15 --- /dev/null +++ b/tests/providers/aws/services/guardduty/guardduty_s3_protection_enabled/guardduty_s3_protection_enabled_test.py @@ -0,0 +1,139 @@ +from unittest import mock + +from prowler.providers.aws.services.guardduty.guardduty_service import Detector +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + + +class Test_guardduty_s3_protection_enabled: + def test_no_detectors(self): + guardduty_client = mock.MagicMock() + guardduty_client.detectors = [] + + with mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_service.GuardDuty", + new=guardduty_client, + ), mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_s3_protection_enabled.guardduty_s3_protection_enabled.guardduty_client", + new=guardduty_client, + ): + + from prowler.providers.aws.services.guardduty.guardduty_s3_protection_enabled.guardduty_s3_protection_enabled import ( + guardduty_s3_protection_enabled, + ) + + check = guardduty_s3_protection_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_detector_disabled(self): + guardduty_client = mock.MagicMock() + detector_arn = f"arn:aws:guardduty:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:detector/1234567890" + guardduty_client.detectors = [ + Detector( + id="1234567890", + arn=detector_arn, + region=AWS_REGION_US_EAST_1, + tags=[], + enabled_in_account=False, + s3_protection=False, + ) + ] + + with mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_service.GuardDuty", + new=guardduty_client, + ), mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_s3_protection_enabled.guardduty_s3_protection_enabled.guardduty_client", + new=guardduty_client, + ): + + from prowler.providers.aws.services.guardduty.guardduty_s3_protection_enabled.guardduty_s3_protection_enabled import ( + guardduty_s3_protection_enabled, + ) + + check = guardduty_s3_protection_enabled() + result = check.execute() + + assert len(result) == 0 + + def test_detector_enabled_s3_protection_disabled(self): + guardduty_client = mock.MagicMock() + detector_arn = f"arn:aws:guardduty:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:detector/1234567890" + guardduty_client.detectors = [ + Detector( + id="1234567890", + arn=detector_arn, + region=AWS_REGION_US_EAST_1, + tags=[], + enabled_in_account=True, + s3_protection=False, + status=True, + ) + ] + + with mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_service.GuardDuty", + new=guardduty_client, + ), mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_s3_protection_enabled.guardduty_s3_protection_enabled.guardduty_client", + new=guardduty_client, + ): + + from prowler.providers.aws.services.guardduty.guardduty_s3_protection_enabled.guardduty_s3_protection_enabled import ( + guardduty_s3_protection_enabled, + ) + + check = guardduty_s3_protection_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "GuardDuty detector does not have S3 Protection enabled." + ) + assert result[0].resource_id == "1234567890" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == detector_arn + assert result[0].resource_tags == [] + + def test_detector_enabled_s3_protection_enabled(self): + guardduty_client = mock.MagicMock() + detector_arn = f"arn:aws:guardduty:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:detector/1234567890" + guardduty_client.detectors = [ + Detector( + id="1234567890", + arn=detector_arn, + region=AWS_REGION_US_EAST_1, + tags=[], + enabled_in_account=True, + s3_protection=True, + status=True, + ) + ] + + with mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_service.GuardDuty", + new=guardduty_client, + ), mock.patch( + "prowler.providers.aws.services.guardduty.guardduty_s3_protection_enabled.guardduty_s3_protection_enabled.guardduty_client", + new=guardduty_client, + ): + from prowler.providers.aws.services.guardduty.guardduty_s3_protection_enabled.guardduty_s3_protection_enabled import ( + guardduty_s3_protection_enabled, + ) + + check = guardduty_s3_protection_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "GuardDuty detector has S3 Protection enabled." + ) + assert result[0].resource_id == "1234567890" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == detector_arn + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/guardduty/guardduty_service_test.py b/tests/providers/aws/services/guardduty/guardduty_service_test.py index 401cdf8dc0..9ed2ac0049 100644 --- a/tests/providers/aws/services/guardduty/guardduty_service_test.py +++ b/tests/providers/aws/services/guardduty/guardduty_service_test.py @@ -109,7 +109,10 @@ class Test_GuardDuty_Service: # Test GuardDuty session def test_get_detector(self): guardduty_client = client("guardduty", region_name=AWS_REGION_EU_WEST_1) - response = guardduty_client.create_detector(Enable=True) + response = guardduty_client.create_detector( + Enable=True, + DataSources={"S3Logs": {"Enable": True}}, + ) aws_provider = set_mocked_aws_provider() guardduty = GuardDuty(aws_provider) @@ -124,6 +127,8 @@ class Test_GuardDuty_Service: assert len(guardduty.detectors[0].findings) == 1 assert guardduty.detectors[0].member_accounts == ["123456789012"] assert guardduty.detectors[0].administrator_account == "123456789013" + assert guardduty.detectors[0].s3_protection + assert not guardduty.detectors[0].rds_protection assert guardduty.detectors[0].region == AWS_REGION_EU_WEST_1 assert guardduty.detectors[0].tags == [{"test": "test"}] diff --git a/tests/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation_test.py b/tests/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation_test.py index 00e4c57b1e..fe32dc63c8 100644 --- a/tests/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation_test.py +++ b/tests/providers/aws/services/iam/iam_inline_policy_allows_privilege_escalation/iam_inline_policy_allows_privilege_escalation_test.py @@ -106,9 +106,9 @@ class Test_iam_inline_policy_allows_privilege_escalation: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Inline Policy '{policy_name}' attached to role {role_arn} does not allow privilege escalation." + == f"Inline policy {policy_name} attached to role {role_name} does not allow privilege escalation." ) - assert result[0].resource_id == policy_name + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] @@ -162,9 +162,9 @@ class Test_iam_inline_policy_allows_privilege_escalation: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Inline Policy '{policy_name}' attached to user {user_arn} does not allow privilege escalation." + == f"Inline policy {policy_name} attached to user {user_name} does not allow privilege escalation." ) - assert result[0].resource_id == policy_name + assert result[0].resource_id == f"test_user/{policy_name}" assert result[0].resource_arn == user_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] @@ -228,9 +228,9 @@ class Test_iam_inline_policy_allows_privilege_escalation: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Inline Policy '{policy_name}' attached to group {group_arn} does not allow privilege escalation." + == f"Inline policy {policy_name} attached to group {group_name} does not allow privilege escalation." ) - assert result[0].resource_id == policy_name + assert result[0].resource_id == f"test_group/{policy_name}" assert result[0].resource_arn == group_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] @@ -289,13 +289,13 @@ class Test_iam_inline_policy_allows_privilege_escalation: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert result[0].resource_id == policy_name + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] assert search( - f"Inline Policy '{policy_name}' attached to role {role_arn} allows privilege escalation using the following actions: ", + f"Inline policy {policy_name} attached to role {role_name} allows privilege escalation using the following actions: ", result[0].status_extended, ) assert search("iam:PassRole", result[0].status_extended) @@ -348,13 +348,13 @@ class Test_iam_inline_policy_allows_privilege_escalation: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert result[0].resource_id == policy_name + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] assert search( - f"Inline Policy '{policy_name}' attached to role {role_arn} allows privilege escalation using the following actions: ", + f"Inline policy {policy_name} attached to role {role_name} allows privilege escalation using the following actions: ", result[0].status_extended, ) assert search("iam:PassRole", result[0].status_extended) @@ -425,13 +425,13 @@ class Test_iam_inline_policy_allows_privilege_escalation: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert result[0].resource_id == policy_name + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] assert search( - f"Inline Policy '{policy_name}' attached to role {role_arn} allows privilege escalation using the following actions: ", + f"Inline policy {policy_name} attached to role {role_name} allows privilege escalation using the following actions: ", result[0].status_extended, ) assert search("iam:PassRole", result[0].status_extended) @@ -491,13 +491,13 @@ class Test_iam_inline_policy_allows_privilege_escalation: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert result[0].resource_id == policy_name + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] assert search( - f"Inline Policy '{policy_name}' attached to role {role_arn} allows privilege escalation using the following actions: ", + f"Inline policy {policy_name} attached to role {role_name} allows privilege escalation using the following actions: ", result[0].status_extended, ) assert search("iam:PassRole", result[0].status_extended) @@ -551,13 +551,13 @@ class Test_iam_inline_policy_allows_privilege_escalation: result = check.execute() assert len(result) == 1 assert result[0].status == "FAIL" - assert result[0].resource_id == policy_name + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] assert search( - f"Inline Policy '{policy_name}' attached to role {role_arn} allows privilege escalation using the following actions: ", + f"Inline policy {policy_name} attached to role {role_name} allows privilege escalation using the following actions: ", result[0].status_extended, ) @@ -1115,3 +1115,60 @@ class Test_iam_inline_policy_allows_privilege_escalation: finding.status_extended == f"Inline Policy '{policy_name_1}' attached to role {role_arn} does not allow privilege escalation." ) + + @mock_aws + def test_iam_policy_random_not_action(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + role_name = "test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + policy_name = "privileged_policy_1" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "NotAction": "prowler:action", + "Resource": "*", + }, + ], + } + + _ = iam_client.put_role_policy( + RoleName=role_name, + PolicyName=policy_name, + PolicyDocument=dumps(policy_document), + ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + from prowler.providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.iam.iam_inline_policy_allows_privilege_escalation.iam_inline_policy_allows_privilege_escalation.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_inline_policy_allows_privilege_escalation.iam_inline_policy_allows_privilege_escalation import ( + iam_inline_policy_allows_privilege_escalation, + ) + + check = iam_inline_policy_allows_privilege_escalation() + result = check.execute() + assert len(result) == 1 + for finding in result: + if finding.resource_id == policy_name: + assert finding.status == "FAIL" + assert finding.resource_id == policy_name + assert finding.resource_arn == role_arn + assert finding.region == AWS_REGION_US_EAST_1 + assert finding.resource_tags == [] + assert search( + f"Inline Policy '{policy_name}' attached to role {role_arn} allows privilege escalation using the following actions:", + finding.status_extended, + ) diff --git a/tests/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges_test.py b/tests/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges_test.py index 18ad9f61c5..2a65ac0fe9 100644 --- a/tests/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges_test.py +++ b/tests/providers/aws/services/iam/iam_inline_policy_no_administrative_privileges/iam_inline_policy_no_administrative_privileges_test.py @@ -103,7 +103,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert results[0].status == "FAIL" assert ( results[0].status_extended - == f"Inline policy {policy_name} attached to group {group_arn} allows '*:*' administrative privileges." + == f"Inline policy {policy_name} attached to group {group_name} allows '*:*' administrative privileges." ) @mock_aws @@ -147,7 +147,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert results[0].status == "PASS" assert ( results[0].status_extended - == f"Inline policy {policy_name} attached to group {group_arn} does not allow '*:*' administrative privileges." + == f"Inline policy {policy_name} attached to group {group_name} does not allow '*:*' administrative privileges." ) @mock_aws @@ -201,7 +201,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert result.status == "FAIL" assert ( result.status_extended - == f"Inline policy {policy_name_admin} attached to group {group_arn} allows '*:*' administrative privileges." + == f"Inline policy {policy_name_admin} attached to group {group_name} allows '*:*' administrative privileges." ) elif result.resource_id == policy_name_not_admin: @@ -212,7 +212,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert result.status == "PASS" assert ( result.status_extended - == f"Inline policy {policy_name_not_admin} attached to group {group_arn} does not allow '*:*' administrative privileges." + == f"Inline policy {policy_name_not_admin} attached to group {group_name} does not allow '*:*' administrative privileges." ) # Roles @@ -291,7 +291,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert results[0].status == "FAIL" assert ( results[0].status_extended - == f"Inline policy {policy_name} attached to role {role_arn} allows '*:*' administrative privileges." + == f"Inline policy {policy_name} attached to role {role_name} allows '*:*' administrative privileges." ) @mock_aws @@ -338,7 +338,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert results[0].status == "PASS" assert ( results[0].status_extended - == f"Inline policy {policy_name} attached to role {role_arn} does not allow '*:*' administrative privileges." + == f"Inline policy {policy_name} attached to role {role_name} does not allow '*:*' administrative privileges." ) @mock_aws @@ -394,7 +394,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert result.status == "FAIL" assert ( result.status_extended - == f"Inline policy {policy_name_admin} attached to group {role_arn} allows '*:*' administrative privileges." + == f"Inline policy {policy_name_admin} attached to group {role_name} allows '*:*' administrative privileges." ) elif result.resource_id == policy_name_not_admin: @@ -405,7 +405,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert result.status == "PASS" assert ( result.status_extended - == f"Inline policy {policy_name_not_admin} attached to group {role_arn} does not allow '*:*' administrative privileges." + == f"Inline policy {policy_name_not_admin} attached to group {role_name} does not allow '*:*' administrative privileges." ) # Users @@ -484,7 +484,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert results[0].status == "FAIL" assert ( results[0].status_extended - == f"Inline policy {policy_name} attached to user {user_arn} allows '*:*' administrative privileges." + == f"Inline policy {policy_name} attached to user {user_name} allows '*:*' administrative privileges." ) @mock_aws @@ -532,7 +532,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert results[0].status == "PASS" assert ( results[0].status_extended - == f"Inline policy {policy_name} attached to user {user_arn} does not allow '*:*' administrative privileges." + == f"Inline policy {policy_name} attached to user {user_name} does not allow '*:*' administrative privileges." ) @mock_aws @@ -589,7 +589,7 @@ class Test_iam_inline_policy_no_administrative_privileges: assert result.status == "FAIL" assert ( result.status_extended - == f"Inline policy {policy_name_admin} attached to user {user_arn} allows '*:*' administrative privileges." + == f"Inline policy {policy_name_admin} attached to user {user_name} allows '*:*' administrative privileges." ) elif result.resource_id == policy_name_not_admin: @@ -600,5 +600,5 @@ class Test_iam_inline_policy_no_administrative_privileges: assert result.status == "PASS" assert ( result.status_extended - == f"Inline policy {policy_name_not_admin} attached to user {user_arn} does not allow '*:*' administrative privileges." + == f"Inline policy {policy_name_not_admin} attached to user {user_name} does not allow '*:*' administrative privileges." ) diff --git a/tests/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail_test.py b/tests/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail_test.py index d0b1596e10..a5e9da2496 100644 --- a/tests/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail_test.py +++ b/tests/providers/aws/services/iam/iam_inline_policy_no_full_access_to_cloudtrail/iam_inline_policy_no_full_access_to_cloudtrail_test.py @@ -54,9 +54,9 @@ class Test_iam_inline_policy_no_full_access_to_cloudtrail: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Inline Policy {policy_name} allows 'cloudtrail:*' privileges to all resources." + == f"Inline policy {policy_name} attached to role {role_name} allows 'cloudtrail:*' privileges to all resources." ) - assert result[0].resource_id == "policy_cloudtrail_full" + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == "eu-west-1" @@ -101,9 +101,9 @@ class Test_iam_inline_policy_no_full_access_to_cloudtrail: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Inline Policy {policy_name} does not allow 'cloudtrail:*' privileges." + == f"Inline policy {policy_name} attached to role {role_name} does not allow 'cloudtrail:*' privileges." ) - assert result[0].resource_id == "policy_no_cloudtrail_full" + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == "eu-west-1" @@ -148,9 +148,9 @@ class Test_iam_inline_policy_no_full_access_to_cloudtrail: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Inline Policy {policy_name} allows 'cloudtrail:*' privileges to all resources." + == f"Inline policy {policy_name} attached to role {role_name} allows 'cloudtrail:*' privileges to all resources." ) - assert result[0].resource_id == "policy_cloudtrail_full" + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == "eu-west-1" @@ -199,9 +199,9 @@ class Test_iam_inline_policy_no_full_access_to_cloudtrail: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Inline Policy {policy_name} does not allow 'cloudtrail:*' privileges." + == f"Inline policy {policy_name} attached to role {role_name} does not allow 'cloudtrail:*' privileges." ) - assert result[0].resource_id == "policy_no_cloudtrail_full" + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == "eu-west-1" @@ -250,8 +250,8 @@ class Test_iam_inline_policy_no_full_access_to_cloudtrail: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Inline Policy {policy_name} allows 'cloudtrail:*' privileges to all resources." + == f"Inline policy {policy_name} attached to role {role_name} allows 'cloudtrail:*' privileges to all resources." ) - assert result[0].resource_id == "policy_cloudtrail_full" + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == "eu-west-1" diff --git a/tests/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms_test.py b/tests/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms_test.py index b324512b65..67d7e27a94 100644 --- a/tests/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms_test.py +++ b/tests/providers/aws/services/iam/iam_inline_policy_no_full_access_to_kms/iam_inline_policy_no_full_access_to_kms_test.py @@ -54,9 +54,9 @@ class Test_iam_inline_policy_no_full_access_to_kms: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Inline Policy {policy_name} allows 'kms:*' privileges." + == f"Inline policy {policy_name} attached to role {role_name} allows 'kms:*' privileges." ) - assert result[0].resource_id == "policy_kms_full" + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == "eu-west-1" @@ -101,9 +101,9 @@ class Test_iam_inline_policy_no_full_access_to_kms: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Inline Policy {policy_name} does not allow 'kms:*' privileges." + == f"Inline policy {policy_name} attached to role {role_name} does not allow 'kms:*' privileges." ) - assert result[0].resource_id == "policy_no_kms_full" + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == "eu-west-1" @@ -148,9 +148,9 @@ class Test_iam_inline_policy_no_full_access_to_kms: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Inline Policy {policy_name} allows 'kms:*' privileges." + == f"Inline policy {policy_name} attached to role {role_name} allows 'kms:*' privileges." ) - assert result[0].resource_id == "policy_kms_full" + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == "eu-west-1" @@ -199,9 +199,9 @@ class Test_iam_inline_policy_no_full_access_to_kms: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Inline Policy {policy_name} does not allow 'kms:*' privileges." + == f"Inline policy {policy_name} attached to role {role_name} does not allow 'kms:*' privileges." ) - assert result[0].resource_id == "policy_no_kms_full" + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == "eu-west-1" @@ -250,8 +250,8 @@ class Test_iam_inline_policy_no_full_access_to_kms: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Inline Policy {policy_name} allows 'kms:*' privileges." + == f"Inline policy {policy_name} attached to role {role_name} allows 'kms:*' privileges." ) - assert result[0].resource_id == "policy_kms_full" + assert result[0].resource_id == f"test_role/{policy_name}" assert result[0].resource_arn == role_arn assert result[0].region == "eu-west-1" diff --git a/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py b/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py index 41fe22aaca..5b0e747f41 100644 --- a/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py +++ b/tests/providers/aws/services/iam/iam_policy_allows_privilege_escalation/iam_policy_allows_privilege_escalation_test.py @@ -989,3 +989,56 @@ class Test_iam_policy_allows_privilege_escalation: finding.status_extended == f"Custom Policy {policy_arn_1} does not allow privilege escalation." ) + + @mock_aws + def test_iam_policy_random_not_action(self): + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + policy_name = "policy1" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "NotAction": "prowler:action", + "Resource": "*", + }, + ], + } + policy_arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + from prowler.providers.aws.services.iam.iam_service import IAM + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation.iam_client", + new=IAM(aws_provider), + ): + from prowler.providers.aws.services.iam.iam_policy_allows_privilege_escalation.iam_policy_allows_privilege_escalation import ( + iam_policy_allows_privilege_escalation, + ) + + check = iam_policy_allows_privilege_escalation() + result = check.execute() + assert len(result) == 1 + for finding in result: + if finding.resource_id == policy_name: + assert finding.status == "FAIL" + assert finding.resource_id == policy_name + assert finding.resource_arn == policy_arn + assert finding.region == AWS_REGION_US_EAST_1 + assert finding.resource_tags == [] + assert search( + f"Custom Policy {policy_arn} allows privilege escalation using the following actions:", + finding.status_extended, + ) + # Since the policy is admin all the possible privilege escalation paths should be present + for permissions in privilege_escalation_policies_combination: + for permission in privilege_escalation_policies_combination[ + permissions + ]: + assert search(permission, finding.status_extended) diff --git a/tests/providers/aws/services/iam/lib/policy_test.py b/tests/providers/aws/services/iam/lib/policy_test.py index a9ce8312dd..5b62dc61e3 100644 --- a/tests/providers/aws/services/iam/lib/policy_test.py +++ b/tests/providers/aws/services/iam/lib/policy_test.py @@ -1,13 +1,1449 @@ from prowler.providers.aws.services.iam.lib.policy import ( + check_admin_access, check_full_service_access, + is_condition_block_restrictive, + is_condition_block_restrictive_organization, is_condition_restricting_from_private_ip, is_policy_cross_account, is_policy_public, ) from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER +TRUSTED_AWS_ACCOUNT_NUMBER = "123456789012" +NON_TRUSTED_AWS_ACCOUNT_NUMBER = "111222333444" + +TRUSTED_ORGANIZATION_ID = "o-123456789012" +NON_TRUSTED_ORGANIZATION_ID = "o-111222333444" + +ALL_ORGS = "*" + class Test_Policy: + # Test lowercase context key name --> aws + def test_condition_parser_string_equals_aws_SourceAccount_list(self): + condition_statement = { + "StringEquals": {"aws:SourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_SourceAccount_list_not_valid(self): + condition_statement = { + "StringEquals": { + "aws:SourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_SourceAccount_str(self): + condition_statement = { + "StringEquals": {"aws:SourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_SourceAccount_str_not_valid(self): + condition_statement = { + "StringEquals": {"aws:SourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceAccount_list(self): + condition_statement = { + "StringLike": {"aws:SourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceAccount_list_not_valid(self): + condition_statement = { + "StringLike": { + "aws:SourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceAccount_str(self): + condition_statement = { + "StringLike": {"aws:SourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceAccount_str_not_valid(self): + condition_statement = { + "StringLike": {"aws:SourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_SourceOwner_str(self): + condition_statement = { + "StringEquals": {"aws:SourceOwner": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_SourceOwner_str_not_valid(self): + condition_statement = { + "StringEquals": {"aws:SourceOwner": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_SourceOwner_list(self): + condition_statement = { + "StringEquals": {"aws:SourceOwner": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_SourceOwner_list_not_valid(self): + condition_statement = { + "StringEquals": { + "aws:SourceOwner": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceOwner_list(self): + condition_statement = { + "StringLike": {"aws:SourceOwner": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceOwner_list_not_valid(self): + condition_statement = { + "StringLike": { + "aws:SourceOwner": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceOwner_str(self): + condition_statement = { + "StringLike": {"aws:SourceOwner": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceOwner_str_not_valid(self): + condition_statement = { + "StringLike": {"aws:SourceOwner": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_s3_ResourceAccount_list(self): + condition_statement = { + "StringEquals": {"s3:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_s3_ResourceAccount_list_not_valid(self): + condition_statement = { + "StringEquals": { + "s3:ResourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_s3_ResourceAccount_str(self): + condition_statement = { + "StringEquals": {"s3:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_s3_ResourceAccount_str_not_valid(self): + condition_statement = { + "StringEquals": {"s3:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_PrincipalAccount_list(self): + condition_statement = { + "StringEquals": {"aws:PrincipalAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_PrincipalAccount_list_not_valid(self): + condition_statement = { + "StringEquals": { + "aws:PrincipalAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_PrincipalAccount_str(self): + condition_statement = { + "StringEquals": {"aws:PrincipalAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_PrincipalAccount_str_not_valid(self): + condition_statement = { + "StringEquals": {"aws:PrincipalAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_SourceArn_str(self): + condition_statement = { + "StringEquals": { + "aws:SourceArn": f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + } + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_SourceArn_str_not_valid(self): + condition_statement = { + "StringEquals": { + "aws:SourceArn": f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_PrincipalAccount_list(self): + condition_statement = { + "StringLike": {"aws:PrincipalAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_PrincipalAccount_list_not_valid(self): + condition_statement = { + "StringLike": { + "aws:PrincipalAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_PrincipalAccount_str(self): + condition_statement = { + "StringLike": {"aws:PrincipalAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_PrincipalAccount_str_not_valid(self): + condition_statement = { + "StringLike": {"aws:PrincipalAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_aws_SourceArn_list(self): + condition_statement = { + "ArnLike": { + "aws:SourceArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_aws_SourceArn_list_not_valid(self): + condition_statement = { + "ArnLike": { + "aws:SourceArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_aws_SourceArn_str(self): + condition_statement = { + "ArnLike": { + "aws:SourceArn": f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_aws_SourceArn_str_not_valid(self): + condition_statement = { + "ArnLike": { + "aws:SourceArn": f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_aws_PrincipalArn_list(self): + condition_statement = { + "ArnLike": { + "aws:PrincipalArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_aws_PrincipalArn_list_not_valid(self): + condition_statement = { + "ArnLike": { + "aws:PrincipalArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_aws_PrincipalArn_str(self): + condition_statement = { + "ArnLike": { + "aws:PrincipalArn": f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_aws_PrincipalArn_str_not_valid(self): + condition_statement = { + "ArnLike": { + "aws:PrincipalArn": f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_aws_SourceArn_list(self): + condition_statement = { + "ArnEquals": { + "aws:SourceArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_aws_SourceArn_list_not_valid(self): + condition_statement = { + "ArnEquals": { + "aws:SourceArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_aws_SourceArn_str(self): + condition_statement = { + "ArnEquals": { + "aws:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_aws_SourceArn_str_not_valid(self): + condition_statement = { + "ArnEquals": { + "aws:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_aws_PrincipalArn_list(self): + condition_statement = { + "ArnEquals": { + "aws:PrincipalArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_aws_PrincipalArn_list_not_valid(self): + condition_statement = { + "ArnEquals": { + "aws:PrincipalArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_aws_PrincipalArn_str(self): + condition_statement = { + "ArnEquals": { + "aws:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_aws_PrincipalArn_str_not_valid(self): + condition_statement = { + "ArnEquals": { + "aws:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceArn_list(self): + condition_statement = { + "StringLike": { + "aws:SourceArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceArn_list_not_valid(self): + condition_statement = { + "StringLike": { + "aws:SourceArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceArn_str(self): + condition_statement = { + "StringLike": { + "aws:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_SourceArn_str_not_valid(self): + condition_statement = { + "StringLike": { + "aws:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_PrincipalArn_list(self): + condition_statement = { + "StringLike": { + "aws:PrincipalArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_PrincipalArn_list_not_valid(self): + condition_statement = { + "StringLike": { + "aws:PrincipalArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_PrincipalArn_str(self): + condition_statement = { + "StringLike": { + "aws:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_PrincipalArn_str_not_valid(self): + condition_statement = { + "StringLike": { + "aws:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_ResourceAccount_list(self): + condition_statement = { + "StringEquals": {"aws:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_ResourceAccount_list_not_valid(self): + condition_statement = { + "StringEquals": { + "aws:ResourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_ResourceAccount_str(self): + condition_statement = { + "StringEquals": {"aws:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_aws_ResourceAccount_str_not_valid(self): + condition_statement = { + "StringEquals": {"aws:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_ResourceAccount_list(self): + condition_statement = { + "StringLike": {"aws:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_ResourceAccount_list_not_valid(self): + condition_statement = { + "StringLike": { + "aws:ResourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_ResourceAccount_str(self): + condition_statement = { + "StringLike": {"aws:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_aws_ResourceAccount_str_not_valid(self): + condition_statement = { + "StringLike": {"aws:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + # Test uppercase context key name --> AWS + def test_condition_parser_string_equals_AWS_SourceAccount_list(self): + condition_statement = { + "StringEquals": {"AWS:SourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_SourceAccount_list_not_valid(self): + condition_statement = { + "StringEquals": { + "AWS:SourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_SourceAccount_str(self): + condition_statement = { + "StringEquals": {"AWS:SourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_SourceAccount_str_not_valid(self): + condition_statement = { + "StringEquals": {"AWS:SourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceAccount_list(self): + condition_statement = { + "StringLike": {"AWS:SourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceAccount_list_not_valid(self): + condition_statement = { + "StringLike": { + "AWS:SourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceAccount_str(self): + condition_statement = { + "StringLike": {"AWS:SourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceAccount_str_not_valid(self): + condition_statement = { + "StringLike": {"AWS:SourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_SourceOwner_str(self): + condition_statement = { + "StringEquals": {"AWS:SourceOwner": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_SourceOwner_str_not_valid(self): + condition_statement = { + "StringEquals": {"AWS:SourceOwner": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_SourceOwner_list(self): + condition_statement = { + "StringEquals": {"AWS:SourceOwner": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_SourceOwner_list_not_valid(self): + condition_statement = { + "StringEquals": { + "AWS:SourceOwner": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceOwner_list(self): + condition_statement = { + "StringLike": {"AWS:SourceOwner": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceOwner_list_not_valid(self): + condition_statement = { + "StringLike": { + "AWS:SourceOwner": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceOwner_str(self): + condition_statement = { + "StringLike": {"AWS:SourceOwner": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceOwner_str_not_valid(self): + condition_statement = { + "StringLike": {"AWS:SourceOwner": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_S3_ResourceAccount_list(self): + condition_statement = { + "StringEquals": {"S3:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_S3_ResourceAccount_list_not_valid(self): + condition_statement = { + "StringEquals": { + "S3:ResourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_S3_ResourceAccount_str(self): + condition_statement = { + "StringEquals": {"S3:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_S3_ResourceAccount_str_not_valid(self): + condition_statement = { + "StringEquals": {"S3:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_PrincipalAccount_list(self): + condition_statement = { + "StringEquals": {"AWS:PrincipalAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_PrincipalAccount_list_not_valid(self): + condition_statement = { + "StringEquals": { + "AWS:PrincipalAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_PrincipalAccount_str(self): + condition_statement = { + "StringEquals": {"AWS:PrincipalAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_PrincipalAccount_str_not_valid(self): + condition_statement = { + "StringEquals": {"AWS:PrincipalAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_PrincipalAccount_list(self): + condition_statement = { + "StringLike": {"AWS:PrincipalAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_PrincipalAccount_list_not_valid(self): + condition_statement = { + "StringLike": { + "AWS:PrincipalAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_PrincipalAccount_str(self): + condition_statement = { + "StringLike": {"AWS:PrincipalAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_PrincipalAccount_str_not_valid(self): + condition_statement = { + "StringLike": {"AWS:PrincipalAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_AWS_SourceArn_list(self): + condition_statement = { + "ArnLike": { + "AWS:SourceArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_AWS_SourceArn_list_not_valid(self): + condition_statement = { + "ArnLike": { + "AWS:SourceArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_AWS_SourceArn_str(self): + condition_statement = { + "ArnLike": { + "AWS:SourceArn": f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_AWS_SourceArn_str_not_valid(self): + condition_statement = { + "ArnLike": { + "AWS:SourceArn": f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_AWS_PrincipalArn_list(self): + condition_statement = { + "ArnLike": { + "AWS:PrincipalArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_AWS_PrincipalArn_list_not_valid(self): + condition_statement = { + "ArnLike": { + "AWS:PrincipalArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_AWS_PrincipalArn_str(self): + condition_statement = { + "ArnLike": { + "AWS:PrincipalArn": f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_like_AWS_PrincipalArn_str_not_valid(self): + condition_statement = { + "ArnLike": { + "AWS:PrincipalArn": f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_AWS_SourceArn_list(self): + condition_statement = { + "ArnEquals": { + "AWS:SourceArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_AWS_SourceArn_list_not_valid(self): + condition_statement = { + "ArnEquals": { + "AWS:SourceArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_AWS_SourceArn_str(self): + condition_statement = { + "ArnEquals": { + "AWS:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_AWS_SourceArn_str_not_valid(self): + condition_statement = { + "ArnEquals": { + "AWS:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_AWS_PrincipalArn_list(self): + condition_statement = { + "ArnEquals": { + "AWS:PrincipalArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_AWS_PrincipalArn_list_not_valid(self): + condition_statement = { + "ArnEquals": { + "AWS:PrincipalArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_AWS_PrincipalArn_str(self): + condition_statement = { + "ArnEquals": { + "AWS:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_arn_equals_AWS_PrincipalArn_str_not_valid(self): + condition_statement = { + "ArnEquals": { + "AWS:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceArn_list(self): + condition_statement = { + "StringLike": { + "AWS:SourceArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceArn_list_not_valid(self): + condition_statement = { + "StringLike": { + "AWS:SourceArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceArn_str(self): + condition_statement = { + "StringLike": { + "AWS:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_SourceArn_str_not_valid(self): + condition_statement = { + "StringLike": { + "AWS:SourceArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_PrincipalArn_list(self): + condition_statement = { + "StringLike": { + "AWS:PrincipalArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + ] + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_PrincipalArn_list_not_valid(self): + condition_statement = { + "StringLike": { + "AWS:PrincipalArn": [ + f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test", + ] + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_PrincipalArn_str(self): + condition_statement = { + "StringLike": { + "AWS:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_PrincipalArn_str_not_valid(self): + condition_statement = { + "StringLike": { + "AWS:PrincipalArn": f"arn:aws:cloudtrail:eu-west-1:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/test" + } + } + + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_ResourceAccount_list(self): + condition_statement = { + "StringEquals": {"AWS:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_ResourceAccount_list_not_valid(self): + condition_statement = { + "StringEquals": { + "AWS:ResourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_ResourceAccount_str(self): + condition_statement = { + "StringEquals": {"AWS:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_equals_AWS_ResourceAccount_str_not_valid(self): + condition_statement = { + "StringEquals": {"AWS:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_ResourceAccount_list(self): + condition_statement = { + "StringLike": {"AWS:ResourceAccount": [TRUSTED_AWS_ACCOUNT_NUMBER]} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_ResourceAccount_list_not_valid(self): + condition_statement = { + "StringLike": { + "AWS:ResourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + } + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_ResourceAccount_str(self): + condition_statement = { + "StringLike": {"AWS:ResourceAccount": TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_string_like_AWS_ResourceAccount_str_not_valid(self): + condition_statement = { + "StringLike": {"AWS:ResourceAccount": NON_TRUSTED_AWS_ACCOUNT_NUMBER} + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_two_lists_unrestrictive(self): + condition_statement = { + "StringLike": { + "AWS:ResourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + }, + "ArnLike": { + "AWS:SourceArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + ] + }, + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_two_lists_both_restrictive(self): + condition_statement = { + "StringLike": { + "AWS:ResourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + ] + }, + "ArnLike": { + "AWS:SourceArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + ] + }, + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_two_lists_first_restrictive(self): + condition_statement = { + "StringLike": { + "AWS:ResourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + ] + }, + "ArnLike": { + "AWS:SourceArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + f"arn:aws:cloudtrail:*:{NON_TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + ] + }, + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_two_lists_second_restrictive(self): + condition_statement = { + "StringLike": { + "AWS:ResourceAccount": [ + TRUSTED_AWS_ACCOUNT_NUMBER, + NON_TRUSTED_AWS_ACCOUNT_NUMBER, + ] + }, + "ArnLike": { + "AWS:SourceArn": [ + f"arn:aws:cloudtrail:*:{TRUSTED_AWS_ACCOUNT_NUMBER}:trail/*", + ] + }, + } + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER + ) + + def test_condition_parser_allowing_cross_account_with_invalid_block(self): + condition_statement = { + "StringLike": { + "s3:prefix": [ + "home/", + ] + }, + } + assert not is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True + ) + + def test_condition_parser_string_equals_vpc(self): + condition_statement = {"StringEquals": {"aws:SourceVpc": "vpc-123456"}} + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True + ) + + def test_condition_parser_string_equals_vpc_list(self): + condition_statement = {"StringEquals": {"aws:sourcevpc": ["vpc-123456"]}} + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True + ) + + def test_condition_parser_string_equals_vpc_list_not_valid(self): + condition_statement = { + "StringEquals": {"aws:SourceVpc": ["vpc-123456", "vpc-654321"]} + } + + assert is_condition_block_restrictive( + condition_statement, TRUSTED_AWS_ACCOUNT_NUMBER, True + ) + + def test_condition_parser_string_equals_aws_PrincipalOrgID_list(self): + condition_statement = { + "StringEquals": {"aws:PrincipalOrgID": [TRUSTED_ORGANIZATION_ID]} + } + assert is_condition_block_restrictive_organization(condition_statement) + + def test_condition_parser_string_equals_aws_PrincipalOrgID_list_multiple_items( + self, + ): + condition_statement = { + "StringEquals": { + "aws:PrincipalOrgID": [ + TRUSTED_ORGANIZATION_ID, + NON_TRUSTED_ORGANIZATION_ID, + ] + } + } + assert is_condition_block_restrictive_organization(condition_statement) + + def test_condition_parser_string_equals_aws_PrincipalOrgID_str(self): + condition_statement = { + "StringEquals": {"aws:PrincipalOrgID": TRUSTED_ORGANIZATION_ID} + } + assert is_condition_block_restrictive_organization(condition_statement) + + def test_condition_parser_string_equals_aws_All_Orgs_list_multiple_items( + self, + ): + condition_statement = { + "StringEquals": { + "aws:PrincipalOrgID": [ + TRUSTED_ORGANIZATION_ID, + ALL_ORGS, + ] + } + } + assert not is_condition_block_restrictive_organization(condition_statement) + + def test_condition_parser_string_equals_aws_All_Orgs_str(self): + condition_statement = {"StringEquals": {"aws:PrincipalOrgID": ALL_ORGS}} + assert not is_condition_block_restrictive_organization(condition_statement) + def test_policy_allows_cross_account_access_with_root_and_wildcard_principal(self): policy_allow_root_and_wildcard_principal = { "Statement": [ @@ -66,7 +1502,9 @@ class Test_Policy: } ] } - assert is_policy_public(policy_allow_wildcard_principal) + assert is_policy_public( + policy_allow_wildcard_principal, not_allowed_actions=["s3:*"] + ) def test_policy_allows_public_access_with_aws_wildcard_principal(self): policy_allow_aws_wildcard_principal = { @@ -79,7 +1517,9 @@ class Test_Policy: } ] } - assert is_policy_public(policy_allow_aws_wildcard_principal) + assert is_policy_public( + policy_allow_aws_wildcard_principal, not_allowed_actions=["s3:*"] + ) def test_policy_does_not_allow_public_access_with_specific_aws_principal(self): policy_allow_specific_aws_principal = { @@ -277,3 +1717,198 @@ class Test_Policy: "IpAddress": {"aws:SourceIp": "256.256.256.256"}, } assert not is_condition_restricting_from_private_ip(condition_from_invalid_ip) + + def test__is_policy_public__(self): + policy = { + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "elasticfilesystem:ClientMount", + "Resource": "*", + } + ] + } + assert is_policy_public( + policy, not_allowed_actions=["elasticfilesystem:ClientMount"] + ) + + def test__is_policy_public__with_principal_dict(self): + policy = { + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": "elasticfilesystem:ClientMount", + "Resource": "*", + } + ] + } + assert is_policy_public( + policy, not_allowed_actions=["elasticfilesystem:ClientMount"] + ) + + def test__is_policy_public__with_secure_conditions_and_allowed_conditions( + self, + ): + policy = { + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "elasticfilesystem:ClientMount", + "Resource": "*", + "Condition": { + "Bool": {"elasticfilesystem:AccessedViaMountTarget": "true"}, + "StringEquals": {"aws:SourceOwner": "123456789012"}, + }, + } + ] + } + assert not is_policy_public(policy) + + def test__is_policy_public__with_secure_conditions_and_allowed_conditions_nested( + self, + ): + policy = { + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "elasticfilesystem:ClientMount", + "Resource": "*", + "Condition": { + "Bool": {"elasticfilesystem:AccessedViaMountTarget": "true"}, + "StringEquals": {"aws:SourceOwner": "123456789012"}, + "StringEqualsIfExists": { + "aws:SourceVpce": "vpce-1234567890abcdef0" + }, + }, + } + ] + } + assert not is_policy_public(policy) + + def test__is_policy_public__with_secure_conditions_and_allowed_conditions_nested_dict( + self, + ): + policy = { + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "elasticfilesystem:ClientMount", + "Resource": "*", + "Condition": { + "Bool": {"elasticfilesystem:AccessedViaMountTarget": "true"}, + "StringEquals": {"aws:SourceOwner": "123456789012"}, + "StringEqualsIfExists": { + "aws:SourceVpce": { + "vpce-1234567890abcdef0": "vpce-1234567890abcdef0" + } + }, + }, + } + ] + } + assert not is_policy_public(policy) + + def test__is_policy_public__with_secure_conditions_and_allowed_conditions_nested_dict_key( + self, + ): + policy = { + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "elasticfilesystem:ClientMount", + "Resource": "*", + "Condition": { + "Bool": {"elasticfilesystem:AccessedViaMountTarget": "true"}, + "StringEquals": {"aws:SourceOwner": "123456789012"}, + "StringEqualsIfExists": { + "aws:SourceVpce": { + "vpce-1234567890abcdef0": "vpce-1234567890abcdef0" + } + }, + }, + } + ] + } + assert not is_policy_public(policy) + + def test__is_policy_public_with_action_wildcard( + self, + ): + policy = { + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "elasticfilesystem:*", + "Resource": "*", + } + ] + } + assert is_policy_public(policy) + + def test__is_policy_public_allowing_all_actions( + self, + ): + policy = { + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "*", + "Resource": "*", + } + ] + } + assert is_policy_public(policy) + + def test_check_admin_access(self): + policy = { + "Version": "2012-10-17", + "Statement": [{"Effect": "Allow", "Action": ["*"], "Resource": "*"}], + } + assert check_admin_access(policy) + + def test_check_admin_access_false(self): + policy = { + "Version": "2012-10-17", + "Statement": [{"Effect": "Allow", "Action": ["s3:*"], "Resource": "*"}], + } + assert not check_admin_access(policy) + + def test_check_admin_access_not_action(self): + policy = { + "Version": "2012-10-17", + "Statement": [{"Effect": "Allow", "NotAction": "s3:*", "Resource": "*"}], + } + assert check_admin_access(policy) + + def test_check_admin_access_not_action_with_random_action(self): + policy = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "NotAction": "prowler:action", "Resource": "*"} + ], + } + assert check_admin_access(policy) + + def test_check_admin_access_not_resource(self): + policy = { + "Version": "2012-10-17", + "Statement": [{"Effect": "Allow", "Action": "*", "NotResource": "*"}], + } + assert not check_admin_access(policy) + + def test_check_admin_access_not_resource_with_random_resource(self): + policy = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Action": "*", "NotResource": "prowler:resource"} + ], + } + assert check_admin_access(policy) diff --git a/tests/providers/aws/services/iam/lib/privilege_escalation_test.py b/tests/providers/aws/services/iam/lib/privilege_escalation_test.py index 7349b5dda1..fbd55742d5 100644 --- a/tests/providers/aws/services/iam/lib/privilege_escalation_test.py +++ b/tests/providers/aws/services/iam/lib/privilege_escalation_test.py @@ -8,6 +8,7 @@ class Test_PrivilegeEscalation: def test_find_privilege_escalation_combinations_no_priv_escalation(self): allowed_actions = set() denied_actions = set() + allowed_not_actions = set() denied_not_actions = set() allowed_actions.add("s3:GetObject") @@ -16,7 +17,7 @@ class Test_PrivilegeEscalation: assert ( find_privilege_escalation_combinations( - allowed_actions, denied_actions, denied_not_actions + allowed_actions, denied_actions, allowed_not_actions, denied_not_actions ) == set() ) @@ -26,13 +27,14 @@ class Test_PrivilegeEscalation: ): allowed_actions = set() denied_actions = set() + allowed_not_actions = set() denied_not_actions = set() allowed_actions.add("iam:*") denied_actions.add("ec2:RunInstances") assert find_privilege_escalation_combinations( - allowed_actions, denied_actions, denied_not_actions + allowed_actions, denied_actions, allowed_not_actions, denied_not_actions ) == { "iam:Put*", "iam:AddUserToGroup", @@ -54,13 +56,14 @@ class Test_PrivilegeEscalation: def test_find_privilege_escalation_combinations_priv_escalation_iam_PassRole(self): allowed_actions = set() + allowed_not_actions = set() denied_actions = set() denied_not_actions = set() allowed_actions.add("iam:PassRole") assert find_privilege_escalation_combinations( - allowed_actions, denied_actions, denied_not_actions + allowed_actions, denied_actions, allowed_not_actions, denied_not_actions ) == {"iam:PassRole"} def test_check_privilege_escalation_no_priv_escalation(self): @@ -127,3 +130,70 @@ class Test_PrivilegeEscalation: result = check_privilege_escalation(policy) assert "iam:PassRole" in result + + def test_check_privilege_escalation_priv_escalation_not_action( + self, + ): + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Statement1", + "Effect": "Allow", + "NotAction": "iam:Put*", + "Resource": "*", + } + ], + } + + result = check_privilege_escalation(policy) + assert "iam:Put*" not in result + assert "iam:AddUserToGroup" in result + assert "iam:AttachRolePolicy" in result + assert "iam:PassRole" in result + assert "iam:CreateLoginProfile" in result + assert "iam:CreateAccessKey" in result + assert "iam:AttachGroupPolicy" in result + assert "iam:SetDefaultPolicyVersion" in result + assert "iam:PutRolePolicy" in result + assert "iam:UpdateAssumeRolePolicy" in result + assert "iam:*" in result + assert "iam:PutGroupPolicy" in result + assert "iam:PutUserPolicy" in result + assert "iam:CreatePolicyVersion" in result + assert "iam:AttachUserPolicy" in result + assert "iam:UpdateLoginProfile" in result + + def test_check_privilege_escalation_priv_escalation_with_invalid_not_action( + self, + ): + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "Statement1", + "Effect": "Allow", + "NotAction": "prowler:action", + "Resource": "*", + } + ], + } + + result = check_privilege_escalation(policy) + assert "proler:action" not in result + assert "iam:Put*" in result + assert "iam:AddUserToGroup" in result + assert "iam:AttachRolePolicy" in result + assert "iam:PassRole" in result + assert "iam:CreateLoginProfile" in result + assert "iam:CreateAccessKey" in result + assert "iam:AttachGroupPolicy" in result + assert "iam:SetDefaultPolicyVersion" in result + assert "iam:PutRolePolicy" in result + assert "iam:UpdateAssumeRolePolicy" in result + assert "iam:*" in result + assert "iam:PutGroupPolicy" in result + assert "iam:PutUserPolicy" in result + assert "iam:CreatePolicyVersion" in result + assert "iam:AttachUserPolicy" in result + assert "iam:UpdateLoginProfile" in result diff --git a/tests/providers/aws/services/inspector2/inspector2_active_findings_exist/inspector2_active_findings_exist_test.py b/tests/providers/aws/services/inspector2/inspector2_active_findings_exist/inspector2_active_findings_exist_test.py index 093f29d7a2..ef0abf9f55 100644 --- a/tests/providers/aws/services/inspector2/inspector2_active_findings_exist/inspector2_active_findings_exist_test.py +++ b/tests/providers/aws/services/inspector2/inspector2_active_findings_exist/inspector2_active_findings_exist_test.py @@ -28,6 +28,10 @@ class Test_inspector2_active_findings_exist: id=AWS_ACCOUNT_NUMBER, arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", status="ENABLED", + ec2_status="ENABLED", + ecr_status="DISABLED", + lambda_status="DISABLED", + lambda_code_status="ENABLED", region=AWS_REGION_EU_WEST_1, active_findings=False, ) @@ -80,6 +84,10 @@ class Test_inspector2_active_findings_exist: arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", region=AWS_REGION_EU_WEST_1, status="ENABLED", + ec2_status="ENABLED", + ecr_status="DISABLED", + lambda_status="DISABLED", + lambda_code_status="ENABLED", active_findings=False, ) ] @@ -131,6 +139,10 @@ class Test_inspector2_active_findings_exist: arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", region=AWS_REGION_EU_WEST_1, status="ENABLED", + ec2_status="ENABLED", + ecr_status="DISABLED", + lambda_status="DISABLED", + lambda_code_status="ENABLED", active_findings=True, ) ] @@ -191,6 +203,10 @@ class Test_inspector2_active_findings_exist: id=AWS_ACCOUNT_NUMBER, arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", status="DISABLED", + ec2_status="ENABLED", + ecr_status="DISABLED", + lambda_status="DISABLED", + lambda_code_status="ENABLED", region=AWS_REGION_EU_WEST_1, active_findings=False, ) diff --git a/tests/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled_test.py b/tests/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled_test.py index 34a4fc1922..2ea7702cd3 100644 --- a/tests/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled_test.py +++ b/tests/providers/aws/services/inspector2/inspector2_is_enabled/inspector2_is_enabled_test.py @@ -33,8 +33,11 @@ class Test_inspector2_is_enabled: id=AWS_ACCOUNT_NUMBER, arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", status="DISABLED", + ec2_status="DISABLED", + ecr_status="DISABLED", + lambda_status="DISABLED", + lambda_code_status="DISABLED", region=AWS_REGION_EU_WEST_1, - findings=[], ) ] aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) @@ -57,7 +60,10 @@ class Test_inspector2_is_enabled: assert len(result) == 1 assert result[0].status == "FAIL" - assert result[0].status_extended == "Inspector2 is not enabled." + assert ( + result[0].status_extended + == "Inspector2 is not enabled in this account." + ) assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert ( result[0].resource_arn @@ -65,7 +71,7 @@ class Test_inspector2_is_enabled: ) assert result[0].region == AWS_REGION_EU_WEST_1 - def test_enabled_no_finding(self): + def test_all_enabled(self): # Mock the inspector2 client inspector2_client = mock.MagicMock inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) @@ -79,8 +85,11 @@ class Test_inspector2_is_enabled: id=AWS_ACCOUNT_NUMBER, arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", status="ENABLED", + ec2_status="ENABLED", + ecr_status="ENABLED", + lambda_status="ENABLED", + lambda_code_status="ENABLED", region=AWS_REGION_EU_WEST_1, - findings=[], ) ] aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) @@ -103,7 +112,775 @@ class Test_inspector2_is_enabled: assert len(result) == 1 assert result[0].status == "PASS" - assert result[0].status_extended == "Inspector2 is enabled." + assert ( + result[0].status_extended + == "Inspector2 is enabled for EC2 instances, ECR container images, Lambda functions and code." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_all_services_disabled(self): + inspector2_client = mock.MagicMock() + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="DISABLED", + ecr_status="DISABLED", + lambda_status="DISABLED", + lambda_code_status="DISABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: EC2, ECR, Lambda, Lambda Code." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ec2_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="DISABLED", + ecr_status="ENABLED", + lambda_status="ENABLED", + lambda_code_status="ENABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: EC2." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ecr_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="ENABLED", + ecr_status="DISABLED", + lambda_status="ENABLED", + lambda_code_status="ENABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: ECR." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_lambda_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="ENABLED", + ecr_status="ENABLED", + lambda_status="DISABLED", + lambda_code_status="ENABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: Lambda." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_lambda_code_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="ENABLED", + ecr_status="ENABLED", + lambda_status="ENABLED", + lambda_code_status="DISABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: Lambda Code." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ec2_ecr_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="DISABLED", + ecr_status="DISABLED", + lambda_status="ENABLED", + lambda_code_status="ENABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: EC2, ECR." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ec2_lambda_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="DISABLED", + ecr_status="ENABLED", + lambda_status="DISABLED", + lambda_code_status="ENABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: EC2, Lambda." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ec2_lambda_code_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="DISABLED", + ecr_status="ENABLED", + lambda_status="ENABLED", + lambda_code_status="DISABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: EC2, Lambda Code." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ecr_lambda_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="ENABLED", + ecr_status="DISABLED", + lambda_status="DISABLED", + lambda_code_status="ENABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: ECR, Lambda." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ecr_lambda_code_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="ENABLED", + ecr_status="DISABLED", + lambda_status="ENABLED", + lambda_code_status="DISABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: ECR, Lambda Code." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_lambda_lambda_code_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="ENABLED", + ecr_status="ENABLED", + lambda_status="DISABLED", + lambda_code_status="DISABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: Lambda, Lambda Code." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ec2_ecr_lambda_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="DISABLED", + ecr_status="DISABLED", + lambda_status="DISABLED", + lambda_code_status="ENABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: EC2, ECR, Lambda." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ec2_ecr_lambda_code_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="DISABLED", + ecr_status="DISABLED", + lambda_status="ENABLED", + lambda_code_status="DISABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: EC2, ECR, Lambda Code." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ec2_lambda_lambda_code_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="DISABLED", + ecr_status="ENABLED", + lambda_status="DISABLED", + lambda_code_status="DISABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: EC2, Lambda, Lambda Code." + ) + assert result[0].resource_id == AWS_ACCOUNT_NUMBER + assert ( + result[0].resource_arn + == f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_ecr_lambda_lambda_code_disabled(self): + inspector2_client = mock.MagicMock + inspector2_client.provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + inspector2_client.audited_account = AWS_ACCOUNT_NUMBER + inspector2_client.audited_account_arn = ( + f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root" + ) + inspector2_client.region = AWS_REGION_EU_WEST_1 + inspector2_client.inspectors = [ + Inspector( + id=AWS_ACCOUNT_NUMBER, + arn=f"arn:aws:inspector2:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:inspector2", + status="ENABLED", + ec2_status="ENABLED", + ecr_status="DISABLED", + lambda_status="DISABLED", + lambda_code_status="DISABLED", + region=AWS_REGION_EU_WEST_1, + ) + ] + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled.inspector2_client", + new=inspector2_client, + ): + # Test Check + from prowler.providers.aws.services.inspector2.inspector2_is_enabled.inspector2_is_enabled import ( + inspector2_is_enabled, + ) + + check = inspector2_is_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Inspector2 is not enabled for the following services: ECR, Lambda, Lambda Code." + ) assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert ( result[0].resource_arn diff --git a/tests/providers/aws/services/inspector2/inspector2_service_test.py b/tests/providers/aws/services/inspector2/inspector2_service_test.py index 8f865ac898..c84797eacd 100644 --- a/tests/providers/aws/services/inspector2/inspector2_service_test.py +++ b/tests/providers/aws/services/inspector2/inspector2_service_test.py @@ -41,6 +41,11 @@ def mock_make_api_call(self, operation_name, kwargs): "errorMessage": "string", "status": "ENABLED", }, + "lambdaCode": { + "errorCode": "ALREADY_ENABLED", + "errorMessage": "string", + "status": "ENABLED", + }, }, "state": { "errorCode": "ALREADY_ENABLED", @@ -104,6 +109,10 @@ class Test_Inspector2_Service: assert inspector2.inspectors[0].id == "Inspector2" assert inspector2.inspectors[0].region == AWS_REGION_EU_WEST_1 assert inspector2.inspectors[0].status == "ENABLED" + assert inspector2.inspectors[0].ec2_status == "ENABLED" + assert inspector2.inspectors[0].ecr_status == "ENABLED" + assert inspector2.inspectors[0].lambda_status == "ENABLED" + assert inspector2.inspectors[0].lambda_code_status == "ENABLED" def test_list_active_findings(self): aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) diff --git a/tests/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots_test.py b/tests/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots_test.py new file mode 100644 index 0000000000..98ce7bc49b --- /dev/null +++ b/tests/providers/aws/services/neptune/neptune_cluster_copy_tags_to_snapshots/neptune_cluster_copy_tags_to_snapshots_test.py @@ -0,0 +1,121 @@ +from unittest import mock + +from prowler.providers.aws.services.neptune.neptune_service import Cluster +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + + +class Test_neptune_cluster_copy_tags_to_snapshots: + def test_neptune_no_clusters(self): + neptune_client = mock.MagicMock + neptune_client.clusters = {} + + with mock.patch( + "prowler.providers.aws.services.neptune.neptune_service.Neptune", + new=neptune_client, + ), mock.patch( + "prowler.providers.aws.services.neptune.neptune_cluster_copy_tags_to_snapshots.neptune_cluster_copy_tags_to_snapshots.neptune_client", + new=neptune_client, + ): + # Test Check + from prowler.providers.aws.services.neptune.neptune_cluster_copy_tags_to_snapshots.neptune_cluster_copy_tags_to_snapshots import ( + neptune_cluster_copy_tags_to_snapshots, + ) + + check = neptune_cluster_copy_tags_to_snapshots() + result = check.execute() + + assert len(result) == 0 + + def test_neptune_cluster_copy_tags_disabled(self): + neptune_client = mock.MagicMock + cluster_arn = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + neptune_client.clusters = { + cluster_arn: Cluster( + arn=cluster_arn, + name="db-cluster-1", + id="db-cluster-1", + region=AWS_REGION_US_EAST_1, + tags=[], + copy_tags_to_snapshot=False, + backup_retention_period=7, + encrypted=True, + kms_key="arn:aws:kms:us-east-1:123456789012:key/abcd1234-a123-456a-a12b-a123b4cd56ef", + multi_az=False, + iam_auth=False, + deletion_protection=False, + db_subnet_group_id="subnet-1234abcd", + ) + } + + with mock.patch( + "prowler.providers.aws.services.neptune.neptune_service.Neptune", + new=neptune_client, + ), mock.patch( + "prowler.providers.aws.services.neptune.neptune_cluster_copy_tags_to_snapshots.neptune_cluster_copy_tags_to_snapshots.neptune_client", + new=neptune_client, + ): + # Test Check + from prowler.providers.aws.services.neptune.neptune_cluster_copy_tags_to_snapshots.neptune_cluster_copy_tags_to_snapshots import ( + neptune_cluster_copy_tags_to_snapshots, + ) + + check = neptune_cluster_copy_tags_to_snapshots() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Neptune DB Cluster db-cluster-1 is not configured to copy tags to snapshots." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == cluster_arn + assert result[0].resource_tags == [] + + def test_neptune_cluster_copy_tags_enabled(self): + neptune_client = mock.MagicMock + cluster_arn = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-2" + neptune_client.clusters = { + cluster_arn: Cluster( + arn=cluster_arn, + name="db-cluster-2", + id="db-cluster-2", + region=AWS_REGION_US_EAST_1, + tags=[], + copy_tags_to_snapshot=True, + backup_retention_period=7, + encrypted=True, + kms_key="arn:aws:kms:us-east-1:123456789012:key/abcd1234-a123-456a-a12b-a123b4cd56ef", + multi_az=False, + iam_auth=False, + deletion_protection=False, + db_subnet_group_id="subnet-1234abcd", + ) + } + + with mock.patch( + "prowler.providers.aws.services.neptune.neptune_service.Neptune", + new=neptune_client, + ), mock.patch( + "prowler.providers.aws.services.neptune.neptune_cluster_copy_tags_to_snapshots.neptune_cluster_copy_tags_to_snapshots.neptune_client", + new=neptune_client, + ): + # Test Check + from prowler.providers.aws.services.neptune.neptune_cluster_copy_tags_to_snapshots.neptune_cluster_copy_tags_to_snapshots import ( + neptune_cluster_copy_tags_to_snapshots, + ) + + check = neptune_cluster_copy_tags_to_snapshots() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Neptune DB Cluster db-cluster-2 is configured to copy tags to snapshots." + ) + assert result[0].resource_id == "db-cluster-2" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == cluster_arn + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs_test.py b/tests/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs_test.py new file mode 100644 index 0000000000..1d3dc746f5 --- /dev/null +++ b/tests/providers/aws/services/neptune/neptune_cluster_integration_cloudwatch_logs/neptune_cluster_integration_cloudwatch_logs_test.py @@ -0,0 +1,205 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.neptune.neptune_service import Cluster +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_neptune_cluster_integration_cloudwatch_logs: + @mock_aws + def test_neptune_no_instances(self): + from prowler.providers.aws.services.neptune.neptune_service import Neptune + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.neptune.neptune_cluster_integration_cloudwatch_logs.neptune_cluster_integration_cloudwatch_logs.neptune_client", + new=Neptune(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.neptune.neptune_cluster_integration_cloudwatch_logs.neptune_cluster_integration_cloudwatch_logs import ( + neptune_cluster_integration_cloudwatch_logs, + ) + + check = neptune_cluster_integration_cloudwatch_logs() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_neptune_cluster_without_integration_cloudwatch_logs(self): + conn = client("neptune", region_name=AWS_REGION_US_EAST_1) + conn.create_db_parameter_group( + DBParameterGroupName="test", + DBParameterGroupFamily="default.neptune", + Description="test parameter group", + ) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-1", + Engine="neptune", + DatabaseName="test-1", + DeletionProtection=False, + DBClusterParameterGroupName="test", + MasterUsername="test", + MasterUserPassword="password", + EnableIAMDatabaseAuthentication=False, + BackupRetentionPeriod=0, + StorageEncrypted=False, + Tags=[], + EnableCloudwatchLogsExports=[], + ) + from prowler.providers.aws.services.neptune.neptune_service import Neptune + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.neptune.neptune_cluster_integration_cloudwatch_logs.neptune_cluster_integration_cloudwatch_logs.neptune_client", + new=Neptune(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.neptune.neptune_cluster_integration_cloudwatch_logs.neptune_cluster_integration_cloudwatch_logs import ( + neptune_cluster_integration_cloudwatch_logs, + ) + + check = neptune_cluster_integration_cloudwatch_logs() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Neptune Cluster db-cluster-1 does not have cloudwatch audit logs enabled." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + ) + assert result[0].resource_tags == [] + + @mock_aws + def test_neptune_cluster_with_integration_cloudwatch_logs_not_audit(self): + conn = client("neptune", region_name=AWS_REGION_US_EAST_1) + conn.create_db_parameter_group( + DBParameterGroupName="test", + DBParameterGroupFamily="default.neptune", + Description="test parameter group", + ) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-1", + Engine="neptune", + DatabaseName="test-1", + DeletionProtection=True, + DBClusterParameterGroupName="test", + MasterUsername="test", + MasterUserPassword="password", + BackupRetentionPeriod=0, + StorageEncrypted=True, + Tags=[], + EnableCloudwatchLogsExports=["slowquery"], + ) + from prowler.providers.aws.services.neptune.neptune_service import Neptune + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.neptune.neptune_cluster_integration_cloudwatch_logs.neptune_cluster_integration_cloudwatch_logs.neptune_client", + new=Neptune(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.neptune.neptune_cluster_integration_cloudwatch_logs.neptune_cluster_integration_cloudwatch_logs import ( + neptune_cluster_integration_cloudwatch_logs, + ) + + check = neptune_cluster_integration_cloudwatch_logs() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Neptune Cluster db-cluster-1 does not have cloudwatch audit logs enabled." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + ) + assert result[0].resource_tags == [] + + def test_neptune_cluster_with_integration_cloudwatch_logs_audit(self): + neptune_client = mock.MagicMock + cluster_arn = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + neptune_client.clusters = { + cluster_arn: Cluster( + arn=cluster_arn, + name="db-cluster-1", + id="db-cluster-1", + backup_retention_period=7, + encrypted=True, + kms_key="clave-kms", + multi_az=False, + iam_auth=True, + deletion_protection=False, + region="us-east-1", + db_subnet_group_id="subnet-grupo-id", + subnets=[ + { + "SubnetIdentifier": "subnet-123", + "SubnetAvailabilityZone": {"Name": "us-east-1a"}, + "SubnetStatus": "Active", + } + ], + tags=[], + cloudwatch_logs=["audit"], + ) + } + + with mock.patch( + "prowler.providers.aws.services.neptune.neptune_service.Neptune", + new=neptune_client, + ), mock.patch( + "prowler.providers.aws.services.neptune.neptune_cluster_integration_cloudwatch_logs.neptune_cluster_integration_cloudwatch_logs.neptune_client", + new=neptune_client, + ): + # Test Check + from prowler.providers.aws.services.neptune.neptune_cluster_integration_cloudwatch_logs.neptune_cluster_integration_cloudwatch_logs import ( + neptune_cluster_integration_cloudwatch_logs, + ) + + check = neptune_cluster_integration_cloudwatch_logs() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Neptune Cluster db-cluster-1 has cloudwatch audit logs enabled." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted_test.py b/tests/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted_test.py new file mode 100644 index 0000000000..0bf8747fd0 --- /dev/null +++ b/tests/providers/aws/services/neptune/neptune_cluster_snapshot_encrypted/neptune_cluster_snapshot_encrypted_test.py @@ -0,0 +1,107 @@ +from unittest import mock + +from prowler.providers.aws.services.neptune.neptune_service import ClusterSnapshot +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 + + +class Test_neptune_cluster_snapshot_encrypted: + def test_neptune_no_snapshots(self): + neptune_client = mock.MagicMock() + neptune_client.db_cluster_snapshots = [] + + with mock.patch( + "prowler.providers.aws.services.neptune.neptune_service.Neptune", + new=neptune_client, + ), mock.patch( + "prowler.providers.aws.services.neptune.neptune_cluster_snapshot_encrypted.neptune_cluster_snapshot_encrypted.neptune_client", + new=neptune_client, + ): + # Test Check + from prowler.providers.aws.services.neptune.neptune_cluster_snapshot_encrypted.neptune_cluster_snapshot_encrypted import ( + neptune_cluster_snapshot_encrypted, + ) + + check = neptune_cluster_snapshot_encrypted() + result = check.execute() + + assert len(result) == 0 + + def test_neptune_snapshot_not_encrypted(self): + neptune_client = mock.MagicMock + snapshot_arn = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster-snapshot:snapshot-1" + neptune_client.db_cluster_snapshots = [ + ClusterSnapshot( + arn=snapshot_arn, + id="snapshot-1", + cluster_id="cluster-1", + region=AWS_REGION_US_EAST_1, + encrypted=False, + tags=[], + ) + ] + + with mock.patch( + "prowler.providers.aws.services.neptune.neptune_service.Neptune", + new=neptune_client, + ), mock.patch( + "prowler.providers.aws.services.neptune.neptune_cluster_snapshot_encrypted.neptune_cluster_snapshot_encrypted.neptune_client", + new=neptune_client, + ): + # Test Check + from prowler.providers.aws.services.neptune.neptune_cluster_snapshot_encrypted.neptune_cluster_snapshot_encrypted import ( + neptune_cluster_snapshot_encrypted, + ) + + check = neptune_cluster_snapshot_encrypted() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "Neptune Cluster Snapshot snapshot-1 is not encrypted at rest." + ) + assert result[0].resource_id == "snapshot-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == snapshot_arn + assert result[0].resource_tags == [] + + def test_neptune_snapshot_encrypted(self): + neptune_client = mock.MagicMock + snapshot_arn = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster-snapshot:snapshot-1" + neptune_client.db_cluster_snapshots = [ + ClusterSnapshot( + arn=snapshot_arn, + id="snapshot-1", + cluster_id="cluster-1", + region=AWS_REGION_US_EAST_1, + encrypted=True, + tags=[], + ) + ] + + with mock.patch( + "prowler.providers.aws.services.neptune.neptune_service.Neptune", + new=neptune_client, + ), mock.patch( + "prowler.providers.aws.services.neptune.neptune_cluster_snapshot_encrypted.neptune_cluster_snapshot_encrypted.neptune_client", + new=neptune_client, + ): + # Test Check + from prowler.providers.aws.services.neptune.neptune_cluster_snapshot_encrypted.neptune_cluster_snapshot_encrypted import ( + neptune_cluster_snapshot_encrypted, + ) + + check = neptune_cluster_snapshot_encrypted() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "Neptune Cluster Snapshot snapshot-1 is encrypted at rest." + ) + assert result[0].resource_id == "snapshot-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_arn == snapshot_arn + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/neptune/neptune_service_test.py b/tests/providers/aws/services/neptune/neptune_service_test.py index c804c6853e..7f893aa6fd 100644 --- a/tests/providers/aws/services/neptune/neptune_service_test.py +++ b/tests/providers/aws/services/neptune/neptune_service_test.py @@ -149,7 +149,7 @@ class Test_Neptune_Service: cluster = neptune_client.create_db_cluster( AvailabilityZones=[AWS_REGION_US_EAST_1_AZA, AWS_REGION_US_EAST_1_AZB], BackupRetentionPeriod=1, - CopyTagsToSnapshot=True, + CopyTagsToSnapshot=False, Engine=NEPTUNE_ENGINE, DatabaseName=NEPTUNE_CLUSTER_NAME, DBClusterIdentifier=NEPTUNE_CLUSTER_NAME, @@ -183,6 +183,8 @@ class Test_Neptune_Service: db_subnet_group_id=SUBNET_GROUP_NAME, subnets=[SUBNET_1, SUBNET_2], tags=NEPTUNE_CLUSTER_TAGS, + copy_tags_to_snapshot=False, + cloudwatch_logs=[], ) def test_describe_db_cluster_snapshots(self): diff --git a/tests/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection_test.py b/tests/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection_test.py index 984834761b..1ca85fe987 100644 --- a/tests/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection_test.py +++ b/tests/providers/aws/services/networkfirewall/networkfirewall_deletion_protection/networkfirewall_deletion_protection_test.py @@ -19,7 +19,7 @@ class Test_networkfirewall_deletion_protection: [AWS_REGION_US_EAST_1] ) networkfirewall_client.region = AWS_REGION_US_EAST_1 - networkfirewall_client.network_firewalls = [] + networkfirewall_client.network_firewalls = {} aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) @@ -41,14 +41,14 @@ class Test_networkfirewall_deletion_protection: assert len(result) == 0 - def Test_networkfirewall_deletion_protection_disabled(self): + def test_networkfirewall_deletion_protection_disabled(self): networkfirewall_client = mock.MagicMock networkfirewall_client.provider = set_mocked_aws_provider( [AWS_REGION_US_EAST_1] ) networkfirewall_client.region = AWS_REGION_US_EAST_1 - networkfirewall_client.network_firewalls = [ - Firewall( + networkfirewall_client.network_firewalls = { + FIREWALL_ARN: Firewall( arn=FIREWALL_ARN, name=FIREWALL_NAME, region=AWS_REGION_US_EAST_1, @@ -58,8 +58,7 @@ class Test_networkfirewall_deletion_protection: encryption_type="CUSTOMER_KMS", deletion_protection=False, ) - ] - + } aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) with mock.patch( @@ -89,14 +88,14 @@ class Test_networkfirewall_deletion_protection: assert result[0].resource_tags == [] assert result[0].resource_arn == FIREWALL_ARN - def Test_networkfirewall_deletion_protection_enabled(self): + def test_networkfirewall_deletion_protection_enabled(self): networkfirewall_client = mock.MagicMock networkfirewall_client.provider = set_mocked_aws_provider( [AWS_REGION_US_EAST_1] ) networkfirewall_client.region = AWS_REGION_US_EAST_1 - networkfirewall_client.network_firewalls = [ - Firewall( + networkfirewall_client.network_firewalls = { + FIREWALL_ARN: Firewall( arn=FIREWALL_ARN, name=FIREWALL_NAME, region=AWS_REGION_US_EAST_1, @@ -106,7 +105,7 @@ class Test_networkfirewall_deletion_protection: encryption_type="CUSTOMER_KMS", deletion_protection=True, ) - ] + } aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) diff --git a/tests/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc_test.py b/tests/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc_test.py index d995975f0f..18193b4f17 100644 --- a/tests/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc_test.py +++ b/tests/providers/aws/services/networkfirewall/networkfirewall_in_all_vpc/networkfirewall_in_all_vpc_test.py @@ -20,7 +20,7 @@ class Test_networkfirewall_in_all_vpc: [AWS_REGION_US_EAST_1] ) networkfirewall_client.region = AWS_REGION_US_EAST_1 - networkfirewall_client.network_firewalls = [] + networkfirewall_client.network_firewalls = {} vpc_client = mock.MagicMock vpc_client.provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) vpc_client.region = AWS_REGION_US_EAST_1 @@ -56,8 +56,8 @@ class Test_networkfirewall_in_all_vpc: [AWS_REGION_US_EAST_1] ) networkfirewall_client.region = AWS_REGION_US_EAST_1 - networkfirewall_client.network_firewalls = [ - Firewall( + networkfirewall_client.network_firewalls = { + FIREWALL_ARN: Firewall( arn=FIREWALL_ARN, name=FIREWALL_NAME, region=AWS_REGION_US_EAST_1, @@ -67,7 +67,7 @@ class Test_networkfirewall_in_all_vpc: encryption_type="CUSTOMER_KMS", deletion_protection=True, ) - ] + } vpc_client = mock.MagicMock vpc_client.provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) vpc_client.region = AWS_REGION_US_EAST_1 @@ -139,7 +139,7 @@ class Test_networkfirewall_in_all_vpc: [AWS_REGION_US_EAST_1] ) networkfirewall_client.region = AWS_REGION_US_EAST_1 - networkfirewall_client.network_firewalls = [] + networkfirewall_client.network_firewalls = {} vpc_client = mock.MagicMock vpc_client.provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) vpc_client.region = AWS_REGION_US_EAST_1 @@ -211,7 +211,7 @@ class Test_networkfirewall_in_all_vpc: [AWS_REGION_US_EAST_1] ) networkfirewall_client.region = AWS_REGION_US_EAST_1 - networkfirewall_client.network_firewalls = [] + networkfirewall_client.network_firewalls = {} vpc_client = mock.MagicMock vpc_client.provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) @@ -284,8 +284,8 @@ class Test_networkfirewall_in_all_vpc: [AWS_REGION_US_EAST_1] ) networkfirewall_client.region = AWS_REGION_US_EAST_1 - networkfirewall_client.network_firewalls = [ - Firewall( + networkfirewall_client.network_firewalls = { + FIREWALL_ARN: Firewall( arn=FIREWALL_ARN, name=FIREWALL_NAME, region=AWS_REGION_US_EAST_1, @@ -295,7 +295,7 @@ class Test_networkfirewall_in_all_vpc: encryption_type="CUSTOMER_KMS", deletion_protection=True, ) - ] + } vpc_client = mock.MagicMock vpc_client.provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) vpc_client.region = AWS_REGION_US_EAST_1 @@ -405,7 +405,7 @@ class Test_networkfirewall_in_all_vpc: [AWS_REGION_US_EAST_1] ) networkfirewall_client.region = AWS_REGION_US_EAST_1 - networkfirewall_client.network_firewalls = [] + networkfirewall_client.network_firewalls = {} vpc_client = mock.MagicMock vpc_client.provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) vpc_client.region = AWS_REGION_US_EAST_1 @@ -469,7 +469,7 @@ class Test_networkfirewall_in_all_vpc: [AWS_REGION_US_EAST_1] ) networkfirewall_client.region = AWS_REGION_US_EAST_1 - networkfirewall_client.network_firewalls = [] + networkfirewall_client.network_firewalls = {} vpc_client = mock.MagicMock vpc_client.provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) vpc_client.region = AWS_REGION_US_EAST_1 diff --git a/tests/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated_test.py b/tests/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated_test.py new file mode 100644 index 0000000000..1340e4b938 --- /dev/null +++ b/tests/providers/aws/services/networkfirewall/networkfirewall_policy_rule_group_associated/networkfirewall_policy_rule_group_associated_test.py @@ -0,0 +1,244 @@ +from unittest import mock + +from prowler.providers.aws.services.networkfirewall.networkfirewall_service import ( + Firewall, +) +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +FIREWALL_ARN = "arn:aws:network-firewall:us-east-1:123456789012:firewall/my-firewall" +FIREWALL_NAME = "my-firewall" +VPC_ID_PROTECTED = "vpc-12345678901234567" +VPC_ID_UNPROTECTED = "vpc-12345678901234568" +POLICY_ARN = "arn:aws:network-firewall:us-east-1:123456789012:firewall-policy/my-policy" + + +class Test_networkfirewall_policy_rule_group_associated: + def test_no_networkfirewall(self): + networkfirewall_client = mock.MagicMock + networkfirewall_client.provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1] + ) + networkfirewall_client.region = AWS_REGION_US_EAST_1 + networkfirewall_client.network_firewalls = {} + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.networkfirewall.networkfirewall_policy_rule_group_associated.networkfirewall_policy_rule_group_associated.networkfirewall_client", + new=networkfirewall_client, + ): + # Test Check + from prowler.providers.aws.services.networkfirewall.networkfirewall_policy_rule_group_associated.networkfirewall_policy_rule_group_associated import ( + networkfirewall_policy_rule_group_associated, + ) + + check = networkfirewall_policy_rule_group_associated() + result = check.execute() + + assert len(result) == 0 + + def test_networkfirewall_policy_stateless_rule_group_associated(self): + networkfirewall_client = mock.MagicMock + networkfirewall_client.provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1] + ) + networkfirewall_client.region = AWS_REGION_US_EAST_1 + networkfirewall_client.network_firewalls = { + FIREWALL_ARN: Firewall( + arn=FIREWALL_ARN, + name=FIREWALL_NAME, + region=AWS_REGION_US_EAST_1, + policy_arn=POLICY_ARN, + vpc_id=VPC_ID_PROTECTED, + tags=[], + encryption_type="CUSTOMER_KMS", + deletion_protection=False, + stateless_rule_groups=[ + "arn:aws:network-firewall:us-east-1:123456789012:stateful-rule-group/my-stateless-rule-group" + ], + ) + } + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.networkfirewall.networkfirewall_policy_rule_group_associated.networkfirewall_policy_rule_group_associated.networkfirewall_client", + new=networkfirewall_client, + ): + # Test Check + from prowler.providers.aws.services.networkfirewall.networkfirewall_policy_rule_group_associated.networkfirewall_policy_rule_group_associated import ( + networkfirewall_policy_rule_group_associated, + ) + + check = networkfirewall_policy_rule_group_associated() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Network Firewall {FIREWALL_NAME} policy has at least one rule group associated." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == FIREWALL_NAME + assert result[0].resource_tags == [] + assert result[0].resource_arn == FIREWALL_ARN + + def test_networkfirewall_policy_stateful_rule_group_associated(self): + networkfirewall_client = mock.MagicMock + networkfirewall_client.provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1] + ) + networkfirewall_client.region = AWS_REGION_US_EAST_1 + networkfirewall_client.network_firewalls = { + FIREWALL_ARN: Firewall( + arn=FIREWALL_ARN, + name=FIREWALL_NAME, + region=AWS_REGION_US_EAST_1, + policy_arn=POLICY_ARN, + vpc_id=VPC_ID_PROTECTED, + tags=[], + encryption_type="CUSTOMER_KMS", + deletion_protection=False, + stateful_rule_groups=[ + "arn:aws:network-firewall:us-east-1:123456789012:stateful-rule-group/my-stateful-rule-group" + ], + ) + } + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.networkfirewall.networkfirewall_policy_rule_group_associated.networkfirewall_policy_rule_group_associated.networkfirewall_client", + new=networkfirewall_client, + ): + # Test Check + from prowler.providers.aws.services.networkfirewall.networkfirewall_policy_rule_group_associated.networkfirewall_policy_rule_group_associated import ( + networkfirewall_policy_rule_group_associated, + ) + + check = networkfirewall_policy_rule_group_associated() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Network Firewall {FIREWALL_NAME} policy has at least one rule group associated." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == FIREWALL_NAME + assert result[0].resource_tags == [] + assert result[0].resource_arn == FIREWALL_ARN + + def test_networkfirewall_policy_both_rule_groups_associated(self): + networkfirewall_client = mock.MagicMock + networkfirewall_client.provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1] + ) + networkfirewall_client.region = AWS_REGION_US_EAST_1 + networkfirewall_client.network_firewalls = { + FIREWALL_ARN: Firewall( + arn=FIREWALL_ARN, + name=FIREWALL_NAME, + region=AWS_REGION_US_EAST_1, + policy_arn=POLICY_ARN, + vpc_id=VPC_ID_PROTECTED, + tags=[], + encryption_type="CUSTOMER_KMS", + deletion_protection=True, + stateless_rule_groups=[ + "arn:aws:network-firewall:us-east-1:123456789012:stateful-rule-group/my-stateless-rule-group" + ], + stateful_rule_groups=[ + "arn:aws:network-firewall:us-east-1:123456789012:stateful-rule-group/my-stateful-rule-group" + ], + ) + } + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.networkfirewall.networkfirewall_policy_rule_group_associated.networkfirewall_policy_rule_group_associated.networkfirewall_client", + new=networkfirewall_client, + ): + # Test Check + from prowler.providers.aws.services.networkfirewall.networkfirewall_policy_rule_group_associated.networkfirewall_policy_rule_group_associated import ( + networkfirewall_policy_rule_group_associated, + ) + + check = networkfirewall_policy_rule_group_associated() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Network Firewall {FIREWALL_NAME} policy has at least one rule group associated." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == FIREWALL_NAME + assert result[0].resource_tags == [] + assert result[0].resource_arn == FIREWALL_ARN + + def test_networkfirewall_policy_no_rule_groups_associated(self): + networkfirewall_client = mock.MagicMock + networkfirewall_client.provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1] + ) + networkfirewall_client.region = AWS_REGION_US_EAST_1 + networkfirewall_client.network_firewalls = { + FIREWALL_ARN: Firewall( + arn=FIREWALL_ARN, + name=FIREWALL_NAME, + region=AWS_REGION_US_EAST_1, + policy_arn=POLICY_ARN, + vpc_id=VPC_ID_PROTECTED, + tags=[], + encryption_type="CUSTOMER_KMS", + deletion_protection=True, + ) + } + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.networkfirewall.networkfirewall_policy_rule_group_associated.networkfirewall_policy_rule_group_associated.networkfirewall_client", + new=networkfirewall_client, + ): + # Test Check + from prowler.providers.aws.services.networkfirewall.networkfirewall_policy_rule_group_associated.networkfirewall_policy_rule_group_associated import ( + networkfirewall_policy_rule_group_associated, + ) + + check = networkfirewall_policy_rule_group_associated() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Network Firewall {FIREWALL_NAME} policy does not have rule groups associated." + ) + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == FIREWALL_NAME + assert result[0].resource_tags == [] + assert result[0].resource_arn == FIREWALL_ARN diff --git a/tests/providers/aws/services/networkfirewall/networkfirewall_service_test.py b/tests/providers/aws/services/networkfirewall/networkfirewall_service_test.py index b65f39a7f2..e30de7ed69 100644 --- a/tests/providers/aws/services/networkfirewall/networkfirewall_service_test.py +++ b/tests/providers/aws/services/networkfirewall/networkfirewall_service_test.py @@ -80,21 +80,28 @@ class Test_NetworkFirewall_Service: aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) networkfirewall = NetworkFirewall(aws_provider) assert len(networkfirewall.network_firewalls) == 1 - assert networkfirewall.network_firewalls[0].arn == FIREWALL_ARN - assert networkfirewall.network_firewalls[0].region == AWS_REGION_US_EAST_1 - assert networkfirewall.network_firewalls[0].name == FIREWALL_NAME + assert ( + networkfirewall.network_firewalls[FIREWALL_ARN].region + == AWS_REGION_US_EAST_1 + ) + assert networkfirewall.network_firewalls[FIREWALL_ARN].name == FIREWALL_NAME def test_describe_firewall(self): aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) networkfirewall = NetworkFirewall(aws_provider) assert len(networkfirewall.network_firewalls) == 1 - assert networkfirewall.network_firewalls[0].arn == FIREWALL_ARN - assert networkfirewall.network_firewalls[0].region == AWS_REGION_US_EAST_1 - assert networkfirewall.network_firewalls[0].name == FIREWALL_NAME - assert networkfirewall.network_firewalls[0].policy_arn == POLICY_ARN - assert networkfirewall.network_firewalls[0].vpc_id == VPC_ID - assert networkfirewall.network_firewalls[0].tags == [ + assert ( + networkfirewall.network_firewalls[FIREWALL_ARN].region + == AWS_REGION_US_EAST_1 + ) + assert networkfirewall.network_firewalls[FIREWALL_ARN].name == FIREWALL_NAME + assert networkfirewall.network_firewalls[FIREWALL_ARN].policy_arn == POLICY_ARN + assert networkfirewall.network_firewalls[FIREWALL_ARN].vpc_id == VPC_ID + assert networkfirewall.network_firewalls[FIREWALL_ARN].tags == [ {"Key": "test_tag", "Value": "test_value"} ] - assert networkfirewall.network_firewalls[0].encryption_type == "CUSTOMER_KMS" - assert networkfirewall.network_firewalls[0].deletion_protection + assert ( + networkfirewall.network_firewalls[FIREWALL_ARN].encryption_type + == "CUSTOMER_KMS" + ) + assert networkfirewall.network_firewalls[FIREWALL_ARN].deletion_protection diff --git a/tests/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled_test.py b/tests/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled_test.py new file mode 100644 index 0000000000..c679dd1965 --- /dev/null +++ b/tests/providers/aws/services/opensearch/opensearch_service_domains_access_control_enabled/opensearch_service_domains_access_control_enabled_test.py @@ -0,0 +1,94 @@ +from re import search +from unittest import mock + +from prowler.providers.aws.services.opensearch.opensearch_service import ( + OpenSearchDomain, +) +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 + +domain_name = "test-domain" +domain_arn = f"arn:aws:es:us-west-2:{AWS_ACCOUNT_NUMBER}:domain/{domain_name}" + + +class Test_opensearch_service_domains_access_control_enabled: + def test_no_domains(self): + opensearch_client = mock.MagicMock + opensearch_client.opensearch_domains = [] + + with mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service.OpenSearchService", + new=opensearch_client, + ), mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service_domains_access_control_enabled.opensearch_service_domains_access_control_enabled.opensearch_client", + new=opensearch_client, + ): + from prowler.providers.aws.services.opensearch.opensearch_service_domains_access_control_enabled.opensearch_service_domains_access_control_enabled import ( + opensearch_service_domains_access_control_enabled, + ) + + check = opensearch_service_domains_access_control_enabled() + result = check.execute() + assert len(result) == 0 + + def test_no_fine_grained_access_enabled(self): + opensearch_client = mock.MagicMock + opensearch_client.opensearch_domains = [] + opensearch_client.opensearch_domains.append( + OpenSearchDomain( + name=domain_name, region=AWS_REGION_EU_WEST_1, arn=domain_arn + ) + ) + opensearch_client.opensearch_domains[0].advanced_settings_enabled = False + + with mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service.OpenSearchService", + new=opensearch_client, + ), mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service_domains_access_control_enabled.opensearch_service_domains_access_control_enabled.opensearch_client", + new=opensearch_client, + ): + from prowler.providers.aws.services.opensearch.opensearch_service_domains_access_control_enabled.opensearch_service_domains_access_control_enabled import ( + opensearch_service_domains_access_control_enabled, + ) + + check = opensearch_service_domains_access_control_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert search( + "does not have fine grained access control enabled.", + result[0].status_extended, + ) + assert result[0].resource_id == domain_name + assert result[0].resource_arn == domain_arn + + def test_logging_AUDIT_LOGS_enabled(self): + opensearch_client = mock.MagicMock + opensearch_client.opensearch_domains = [] + opensearch_client.opensearch_domains.append( + OpenSearchDomain( + name=domain_name, region=AWS_REGION_EU_WEST_1, arn=domain_arn + ) + ) + opensearch_client.opensearch_domains[0].advanced_settings_enabled = True + + with mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service.OpenSearchService", + new=opensearch_client, + ), mock.patch( + "prowler.providers.aws.services.opensearch.opensearch_service_domains_access_control_enabled.opensearch_service_domains_access_control_enabled.opensearch_client", + new=opensearch_client, + ): + from prowler.providers.aws.services.opensearch.opensearch_service_domains_access_control_enabled.opensearch_service_domains_access_control_enabled import ( + opensearch_service_domains_access_control_enabled, + ) + + check = opensearch_service_domains_access_control_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + "has fine grained access control enabled.", result[0].status_extended + ) + assert result[0].resource_id == domain_name + assert result[0].resource_arn == domain_arn diff --git a/tests/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy_test.py b/tests/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy_test.py new file mode 100644 index 0000000000..5d94c36300 --- /dev/null +++ b/tests/providers/aws/services/organizations/organizations_opt_out_ai_services_policy/organizations_opt_out_ai_services_policy_test.py @@ -0,0 +1,216 @@ +from unittest import mock + +from prowler.providers.aws.services.organizations.organizations_service import ( + Organization, + Policy, +) +from tests.providers.aws.utils import ( + AWS_ACCOUNT_ARN, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) + + +class Test_organizations_tags_policies_enabled_and_attached: + def test_organization_no_organization(self): + organizations_client = mock.MagicMock + organizations_client.region = AWS_REGION_EU_WEST_1 + organizations_client.organizations = [ + Organization( + arn=AWS_ACCOUNT_ARN, + id="AWS Organization", + status="NOT_AVAILABLE", + master_id="", + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.organizations.organizations_opt_out_ai_services_policy.organizations_opt_out_ai_services_policy.organizations_client", + new=organizations_client, + ): + # Test Check + from prowler.providers.aws.services.organizations.organizations_opt_out_ai_services_policy.organizations_opt_out_ai_services_policy import ( + organizations_opt_out_ai_services_policy, + ) + + check = organizations_opt_out_ai_services_policy() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "AWS Organizations is not in-use for this AWS Account." + ) + assert result[0].resource_id == "AWS Organization" + assert result[0].resource_arn == AWS_ACCOUNT_ARN + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_organization_with_AI_optout_no_policies(self): + organizations_client = mock.MagicMock + organizations_client.region = AWS_REGION_EU_WEST_1 + organizations_client.organizations = [ + Organization( + id="o-1234567890", + arn="arn:aws:organizations::1234567890:organization/o-1234567890", + status="ACTIVE", + master_id="1234567890", + policies={"AISERVICES_OPT_OUT_POLICY": []}, + delegated_administrators=None, + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.organizations.organizations_opt_out_ai_services_policy.organizations_opt_out_ai_services_policy.organizations_client", + new=organizations_client, + ): + # Test Check + from prowler.providers.aws.services.organizations.organizations_opt_out_ai_services_policy.organizations_opt_out_ai_services_policy import ( + organizations_opt_out_ai_services_policy, + ) + + check = organizations_opt_out_ai_services_policy() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "AWS Organization o-1234567890 has not opted out of all AI services, granting consent for AWS to access its data." + ) + assert result[0].resource_id == "o-1234567890" + assert ( + result[0].resource_arn + == "arn:aws:organizations::1234567890:organization/o-1234567890" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_organization_with_AI_optout_policy(self): + organizations_client = mock.MagicMock + organizations_client.region = AWS_REGION_EU_WEST_1 + organizations_client.organizations = [ + Organization( + id="o-1234567890", + arn="arn:aws:organizations::1234567890:organization/o-1234567890", + status="ACTIVE", + master_id="1234567890", + policies={ + "AISERVICES_OPT_OUT_POLICY": [ + Policy( + id="p-1234567890", + arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890", + type="AISERVICES_OPT_OUT_POLICY", + aws_managed=False, + content={ + "services": { + "default": { + "opt_out_policy": {"@@assign": "optOut"} + } + } + }, + targets=[], + ) + ] + }, + delegated_administrators=None, + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.organizations.organizations_opt_out_ai_services_policy.organizations_opt_out_ai_services_policy.organizations_client", + new=organizations_client, + ): + # Test Check + from prowler.providers.aws.services.organizations.organizations_opt_out_ai_services_policy.organizations_opt_out_ai_services_policy import ( + organizations_opt_out_ai_services_policy, + ) + + check = organizations_opt_out_ai_services_policy() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "AWS Organization o-1234567890 has opted out of all AI services, not granting consent for AWS to access its data." + ) + assert result[0].resource_id == "o-1234567890" + assert ( + result[0].resource_arn + == "arn:aws:organizations::1234567890:organization/o-1234567890" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_organization_with_AI_optout_policy_no_content(self): + organizations_client = mock.MagicMock + organizations_client.region = AWS_REGION_EU_WEST_1 + organizations_client.organizations = [ + Organization( + id="o-1234567890", + arn="arn:aws:organizations::1234567890:organization/o-1234567890", + status="ACTIVE", + master_id="1234567890", + policies={ + "AISERVICES_OPT_OUT_POLICY": [ + Policy( + id="p-1234567890", + arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890", + type="AISERVICES_OPT_OUT_POLICY", + aws_managed=False, + content={}, + targets=[], + ) + ] + }, + delegated_administrators=None, + ) + ] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.organizations.organizations_opt_out_ai_services_policy.organizations_opt_out_ai_services_policy.organizations_client", + new=organizations_client, + ): + # Test Check + from prowler.providers.aws.services.organizations.organizations_opt_out_ai_services_policy.organizations_opt_out_ai_services_policy import ( + organizations_opt_out_ai_services_policy, + ) + + check = organizations_opt_out_ai_services_policy() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "AWS Organization o-1234567890 has not opted out of all AI services, granting consent for AWS to access its data." + ) + assert result[0].resource_id == "o-1234567890" + assert ( + result[0].resource_arn + == "arn:aws:organizations::1234567890:organization/o-1234567890" + ) + assert result[0].region == AWS_REGION_EU_WEST_1 diff --git a/tests/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions_test.py b/tests/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions_test.py index 455cb1d676..645c0c9170 100644 --- a/tests/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions_test.py +++ b/tests/providers/aws/services/organizations/organizations_scp_check_deny_regions/organizations_scp_check_deny_regions_test.py @@ -233,3 +233,40 @@ class Test_organizations_scp_check_deny_regions: == f"AWS Organization {org_id} has SCP policy {policy_id} restricting all configured regions found." ) assert result[0].region == AWS_REGION_EU_WEST_1 + + @mock_aws + def test_access_denied(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + aws_provider._audit_config = { + "organizations_enabled_regions": [ + AWS_REGION_EU_WEST_1, + AWS_REGION_EU_CENTRAL_1, + ] + } + + # Create Organization + conn = client("organizations", region_name=AWS_REGION_EU_WEST_1) + response = conn.create_organization() + org_arn = response["Organization"]["Arn"] + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.organizations.organizations_scp_check_deny_regions.organizations_scp_check_deny_regions.organizations_client", + new=Organizations(aws_provider), + ) as organizations_client: + # Test Check + from prowler.providers.aws.services.organizations.organizations_scp_check_deny_regions.organizations_scp_check_deny_regions import ( + organizations_scp_check_deny_regions, + ) + + for org in organizations_client.organizations: + if org.arn == org_arn: + org.policies = None + + check = organizations_scp_check_deny_regions() + result = check.execute() + + assert len(result) == 0 diff --git a/tests/providers/aws/services/organizations/organizations_service_test.py b/tests/providers/aws/services/organizations/organizations_service_test.py index 724f00146c..dfae12d402 100644 --- a/tests/providers/aws/services/organizations/organizations_service_test.py +++ b/tests/providers/aws/services/organizations/organizations_service_test.py @@ -22,15 +22,12 @@ class Test_Organizations_Service: @mock_aws def test_describe_organization(self): - # Create Organization conn = client("organizations", region_name=AWS_REGION_EU_WEST_1) response = conn.create_organization() - # Mock aws_provider = set_mocked_aws_provider( [AWS_REGION_EU_WEST_1], create_default_organization=False ) organizations = Organizations(aws_provider) - # Tests assert len(organizations.organizations) == 1 assert organizations.organizations[0].arn == response["Organization"]["Arn"] assert organizations.organizations[0].id == response["Organization"]["Id"] @@ -43,7 +40,6 @@ class Test_Organizations_Service: @mock_aws def test_list_policies(self): - # Create Policy conn = client("organizations", region_name=AWS_REGION_EU_WEST_1) conn.create_organization() response = conn.create_policy( @@ -52,13 +48,28 @@ class Test_Organizations_Service: Name="Test", Type="SERVICE_CONTROL_POLICY", ) - # Mock aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) organizations = Organizations(aws_provider) - # Tests for policy in organizations.policies: if policy.arn == response["Policy"]["PolicySummary"]["Arn"]: assert policy.type == "SERVICE_CONTROL_POLICY" assert policy.aws_managed is False assert policy.content == json.loads(response["Policy"]["Content"]) assert policy.targets == [] + + @mock_aws + def test_describe_policy(self): + conn = client("organizations", region_name=AWS_REGION_EU_WEST_1) + conn.create_organization() + response = conn.create_policy( + Content=scp_restrict_regions_with_deny(), + Description="Test", + Name="Test", + Type="SERVICE_CONTROL_POLICY", + ) + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + organizations = Organizations(aws_provider) + policy = organizations._describe_policy( + response["Policy"]["PolicySummary"]["Id"] + ) + assert policy == json.loads(response["Policy"]["Content"]) diff --git a/tests/providers/aws/services/organizations/organizations_tags_policies_enabled_and_attached/organizations_tags_policies_enabled_and_attached_test.py b/tests/providers/aws/services/organizations/organizations_tags_policies_enabled_and_attached/organizations_tags_policies_enabled_and_attached_test.py index 966e5fd14d..9a90c9f61c 100644 --- a/tests/providers/aws/services/organizations/organizations_tags_policies_enabled_and_attached/organizations_tags_policies_enabled_and_attached_test.py +++ b/tests/providers/aws/services/organizations/organizations_tags_policies_enabled_and_attached/organizations_tags_policies_enabled_and_attached_test.py @@ -10,9 +10,6 @@ from tests.providers.aws.utils import ( set_mocked_aws_provider, ) -# Moto: NotImplementedError: The TAG_POLICY policy type has not been implemented -# Needs to Mock manually - class Test_organizations_tags_policies_enabled_and_attached: def test_organization_no_organization(self): @@ -64,16 +61,18 @@ class Test_organizations_tags_policies_enabled_and_attached: arn="arn:aws:organizations::1234567890:organization/o-1234567890", status="ACTIVE", master_id="1234567890", - policies=[ - Policy( - id="p-1234567890", - arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890", - type="TAG_POLICY", - aws_managed=False, - content={"tags": {"Owner": {}}}, - targets=[], - ) - ], + policies={ + "TAG_POLICY": [ + Policy( + id="p-1234567890", + arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890", + type="TAG_POLICY", + aws_managed=False, + content={"tags": {"Owner": {}}}, + targets=[], + ) + ] + }, delegated_administrators=None, ) ] @@ -118,16 +117,18 @@ class Test_organizations_tags_policies_enabled_and_attached: arn="arn:aws:organizations::1234567890:organization/o-1234567890", status="ACTIVE", master_id="1234567890", - policies=[ - Policy( - id="p-1234567890", - arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890", - type="TAG_POLICY", - aws_managed=False, - content={"tags": {"Owner": {}}}, - targets=["1234567890"], - ) - ], + policies={ + "TAG_POLICY": [ + Policy( + id="p-1234567890", + arn="arn:aws:organizations::1234567890:policy/o-1234567890/p-1234567890", + type="TAG_POLICY", + aws_managed=False, + content={"tags": {"Owner": {}}}, + targets=["1234567890"], + ) + ] + }, delegated_administrators=None, ) ] diff --git a/tests/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription_test.py b/tests/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription_test.py index 7fdf7e7e08..cd0410212f 100644 --- a/tests/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription_test.py +++ b/tests/providers/aws/services/rds/rds_cluster_critical_event_subscription/rds_cluster_critical_event_subscription_test.py @@ -44,6 +44,7 @@ class Test_rds_cluster_critical_event_subscription: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert result[0].resource_arn == RDS_ACCOUNT_ARN + assert result[0].resource_tags == [] @mock_aws def test_rds_cluster_event_subscription_enabled(self): @@ -93,6 +94,7 @@ class Test_rds_cluster_critical_event_subscription: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [{"Key": "test", "Value": "testing"}] @mock_aws def test_rds_cluster_event_failure_only_subscription(self): @@ -145,6 +147,7 @@ class Test_rds_cluster_critical_event_subscription: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [{"Key": "test", "Value": "testing"}] @mock_aws def test_rds_cluster_event_maintenance_only_subscription(self): @@ -194,3 +197,4 @@ class Test_rds_cluster_critical_event_subscription: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs_test.py b/tests/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs_test.py new file mode 100644 index 0000000000..ba01c0f482 --- /dev/null +++ b/tests/providers/aws/services/rds/rds_cluster_integration_cloudwatch_logs/rds_cluster_integration_cloudwatch_logs_test.py @@ -0,0 +1,175 @@ +from unittest import mock + +import botocore +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "CreateDBCluster": + return { + "DBClusterIdentifier": "cluster-1", + "Engine": "aurora", + "MasterUsername": "admin", + "MasterUserPassword": "password", + } + + return make_api_call(self, operation_name, kwarg) + + +class Test_rds_cluster_integration_cloudwatch_logs: + @mock_aws + def test_rds_no_clusters(self): + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_integration_cloudwatch_logs.rds_cluster_integration_cloudwatch_logs.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_cluster_integration_cloudwatch_logs.rds_cluster_integration_cloudwatch_logs import ( + rds_cluster_integration_cloudwatch_logs, + ) + + check = rds_cluster_integration_cloudwatch_logs() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_rds_no_valid_cluster(self): + with mock.patch( + "botocore.client.BaseClient._make_api_call", new=mock_make_api_call + ): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="cluster-1", + Engine="aurora", + MasterUsername="admin", + MasterUserPassword="password", + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_integration_cloudwatch_logs.rds_cluster_integration_cloudwatch_logs.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_cluster_integration_cloudwatch_logs.rds_cluster_integration_cloudwatch_logs import ( + rds_cluster_integration_cloudwatch_logs, + ) + + check = rds_cluster_integration_cloudwatch_logs() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_rds_cluster_no_logs(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="aurora-cluster-1", + Engine="aurora-mysql", + MasterUsername="admin", + MasterUserPassword="password", + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_integration_cloudwatch_logs.rds_cluster_integration_cloudwatch_logs.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_cluster_integration_cloudwatch_logs.rds_cluster_integration_cloudwatch_logs import ( + rds_cluster_integration_cloudwatch_logs, + ) + + check = rds_cluster_integration_cloudwatch_logs() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Cluster aurora-cluster-1 does not have CloudWatch Logs enabled." + ) + assert result[0].resource_id == "aurora-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:aurora-cluster-1" + ) + assert result[0].resource_tags == [] + + @mock_aws + def test_rds_cluster_with_logs(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="aurora-cluster-1", + Engine="aurora-mysql", + MasterUsername="admin", + MasterUserPassword="password", + EnableCloudwatchLogsExports=["audit", "error"], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_integration_cloudwatch_logs.rds_cluster_integration_cloudwatch_logs.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_cluster_integration_cloudwatch_logs.rds_cluster_integration_cloudwatch_logs import ( + rds_cluster_integration_cloudwatch_logs, + ) + + check = rds_cluster_integration_cloudwatch_logs() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Cluster aurora-cluster-1 is shipping audit, error logs to CloudWatch Logs." + ) + assert result[0].resource_id == "aurora-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:aurora-cluster-1" + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port_test.py b/tests/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port_test.py new file mode 100644 index 0000000000..5315b16965 --- /dev/null +++ b/tests/providers/aws/services/rds/rds_cluster_non_default_port/rds_cluster_non_default_port_test.py @@ -0,0 +1,227 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_rds_cluster_non_default_port: + @mock_aws + def test_rds_no_clusters(self): + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port import ( + rds_cluster_non_default_port, + ) + + check = rds_cluster_non_default_port() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_rds_cluster_using_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-1", + Engine="aurora-postgresql", + StorageEncrypted=True, + DeletionProtection=True, + MasterUsername="cluster", + MasterUserPassword="password", + Port=5432, + Tags=[{"Key": "test", "Value": "test"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port import ( + rds_cluster_non_default_port, + ) + + check = rds_cluster_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-1 is using the default port 5432 for aurora-postgresql." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + ) + assert result[0].resource_tags == [{"Key": "test", "Value": "test"}] + + @mock_aws + def test_rds_cluster_using_non_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-1", + Engine="aurora-postgresql", + StorageEncrypted=True, + DeletionProtection=True, + MasterUsername="cluster", + MasterUserPassword="password", + Port=5433, + Tags=[{"Key": "env", "Value": "production"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port import ( + rds_cluster_non_default_port, + ) + + check = rds_cluster_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-1 is not using the default port 5433 for aurora-postgresql." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + ) + assert result[0].resource_tags == [ + {"Key": "env", "Value": "production"} + ] + + @mock_aws + def test_rds_cluster_mysql_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-1", + Engine="mysql", + StorageEncrypted=True, + DeletionProtection=True, + MasterUsername="cluster", + MasterUserPassword="password", + Port=3306, + Tags=[{"Key": "env", "Value": "staging"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port import ( + rds_cluster_non_default_port, + ) + + check = rds_cluster_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-1 is using the default port 3306 for mysql." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + ) + assert result[0].resource_tags == [{"Key": "env", "Value": "staging"}] + + @mock_aws + def test_rds_cluster_mysql_non_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_cluster( + DBClusterIdentifier="db-cluster-1", + Engine="mysql", + StorageEncrypted=True, + DeletionProtection=True, + MasterUsername="cluster", + MasterUserPassword="password", + Port=3307, + Tags=[{"Key": "env", "Value": "production"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_cluster_non_default_port.rds_cluster_non_default_port import ( + rds_cluster_non_default_port, + ) + + check = rds_cluster_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Cluster db-cluster-1 is not using the default port 3307 for mysql." + ) + assert result[0].resource_id == "db-cluster-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:cluster:db-cluster-1" + ) + assert result[0].resource_tags == [ + {"Key": "env", "Value": "production"} + ] diff --git a/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py b/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py index 290c3b9dfb..d72df2303a 100644 --- a/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py +++ b/tests/providers/aws/services/rds/rds_instance_critical_event_subscription/rds_instance_critical_event_subscription_test.py @@ -46,6 +46,7 @@ class Test_rds_instance_critical_event_subscription: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert result[0].resource_arn == RDS_ACCOUNT_ARN + assert result[0].resource_tags == [] @mock_aws def test_rds_no_events_ignoring(self): @@ -130,6 +131,7 @@ class Test_rds_instance_critical_event_subscription: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [{"Key": "test", "Value": "testing"}] @mock_aws def test_rds_instance_event_failure_only_subscription(self): @@ -190,6 +192,7 @@ class Test_rds_instance_critical_event_subscription: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [{"Key": "test", "Value": "testing"}] @mock_aws def test_rds_instance_event_maintenance_only_subscription(self): @@ -247,6 +250,7 @@ class Test_rds_instance_critical_event_subscription: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [] @mock_aws def test_rds_instance_event_configuration_change_only_subscription(self): @@ -304,6 +308,7 @@ class Test_rds_instance_critical_event_subscription: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [] @mock_aws def test_rds_instance_event_configuration_change_and_maintenance(self): @@ -361,6 +366,7 @@ class Test_rds_instance_critical_event_subscription: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [] @mock_aws def test_rds_instance_event_configuration_change_and_failure(self): @@ -418,6 +424,7 @@ class Test_rds_instance_critical_event_subscription: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [] @mock_aws def test_rds_instance_event_failure_and_maintenance(self): @@ -475,3 +482,4 @@ class Test_rds_instance_critical_event_subscription: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups_test.py b/tests/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups_test.py index de5b755b71..81c2ead211 100644 --- a/tests/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups_test.py +++ b/tests/providers/aws/services/rds/rds_instance_event_subscription_parameter_groups/rds_instance_event_subscription_parameter_groups_test.py @@ -46,6 +46,7 @@ class Test_rds_instance__no_event_subscriptions: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert result[0].resource_arn == RDS_ACCOUNT_ARN + assert result[0].resource_tags == [] @mock_aws def test_rds_no_events_ignoring(self): @@ -130,6 +131,7 @@ class Test_rds_instance__no_event_subscriptions: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [{"Key": "test", "Value": "testing"}] @mock_aws def test_rds_parameter_event_configuration_change_only_subscription(self): @@ -187,3 +189,4 @@ class Test_rds_instance__no_event_subscriptions: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups_test.py b/tests/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups_test.py index f907a324bd..fdefbc38d0 100644 --- a/tests/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups_test.py +++ b/tests/providers/aws/services/rds/rds_instance_event_subscription_security_groups/rds_instance_event_subscription_security_groups_test.py @@ -14,7 +14,7 @@ make_api_call = botocore.client.BaseClient._make_api_call RDS_ACCOUNT_ARN = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:account" -class Test_rds_instance__no_event_subscriptions: +class Test_rds_instance_no_event_subscriptions: @mock_aws def test_rds_no_events(self): from prowler.providers.aws.services.rds.rds_service import RDS @@ -46,6 +46,7 @@ class Test_rds_instance__no_event_subscriptions: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert result[0].resource_arn == RDS_ACCOUNT_ARN + assert result[0].resource_tags == [] @mock_aws def test_rds_no_events_ignoring(self): @@ -130,6 +131,7 @@ class Test_rds_instance__no_event_subscriptions: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [{"Key": "test", "Value": "testing"}] @mock_aws def test_rds_security_event_failure_only_subscription(self): @@ -190,7 +192,7 @@ class Test_rds_instance__no_event_subscriptions: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) - assert result[0].resource_tags == [] + assert result[0].resource_tags == [{"Key": "test", "Value": "testing"}] @mock_aws def test_rds_security_event_configuration_change_only_subscription(self): @@ -215,6 +217,9 @@ class Test_rds_instance__no_event_subscriptions: SourceType="db-security-group", EventCategories=["configuration change"], Enabled=True, + Tags=[ + {"Key": "test", "Value": "testing"}, + ], ) from prowler.providers.aws.services.rds.rds_service import RDS @@ -248,6 +253,7 @@ class Test_rds_instance__no_event_subscriptions: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [{"Key": "test", "Value": "testing"}] @mock_aws def test_rds_no_security_group_event_subscription(self): @@ -272,6 +278,9 @@ class Test_rds_instance__no_event_subscriptions: SourceType="db-instance", EventCategories=["configuration change"], Enabled=True, + Tags=[ + {"Key": "test", "Value": "testing"}, + ], ) from prowler.providers.aws.services.rds.rds_service import RDS @@ -302,6 +311,7 @@ class Test_rds_instance__no_event_subscriptions: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert result[0].resource_arn == RDS_ACCOUNT_ARN + assert result[0].resource_tags == [] @mock_aws def test_rds_no_event_subscription(self): @@ -349,6 +359,7 @@ class Test_rds_instance__no_event_subscriptions: assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == AWS_ACCOUNT_NUMBER assert result[0].resource_arn == RDS_ACCOUNT_ARN + assert result[0].resource_tags == [] @mock_aws def test_rds_security_event_subscription_both_enabled(self): @@ -409,3 +420,4 @@ class Test_rds_instance__no_event_subscriptions: result[0].resource_arn == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:es:TestSub" ) + assert result[0].resource_tags == [{"Key": "test", "Value": "testing"}] diff --git a/tests/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc_test.py b/tests/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc_test.py new file mode 100644 index 0000000000..d9751f4cac --- /dev/null +++ b/tests/providers/aws/services/rds/rds_instance_inside_vpc/rds_instance_inside_vpc_test.py @@ -0,0 +1,169 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from prowler.providers.aws.services.rds.rds_service import DBInstance +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_rds_instance_inside_vpc: + @mock_aws + def test_rds_no_instances(self): + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_inside_vpc.rds_instance_inside_vpc.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_inside_vpc.rds_instance_inside_vpc import ( + rds_instance_inside_vpc, + ) + + check = rds_instance_inside_vpc() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_rds_instance_inside_vpc(self): + rds_conn = client("rds", region_name=AWS_REGION_US_EAST_1) + ec2_conn = client("ec2") + + # Step 1: Create the VPC + vpc_id = ec2_conn.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + subnet_1_id = ec2_conn.create_subnet(CidrBlock="10.0.1.0/24", VpcId=vpc_id)[ + "Subnet" + ]["SubnetId"] + subnet_2_id = ec2_conn.create_subnet(CidrBlock="10.0.2.0/24", VpcId=vpc_id)[ + "Subnet" + ]["SubnetId"] + subnet_group_name = "my-rds-subnet-group" + rds_conn.create_db_subnet_group( + DBSubnetGroupName=subnet_group_name, + DBSubnetGroupDescription="Subnet group for RDS instance in VPC", + SubnetIds=[ + subnet_1_id, + subnet_2_id, + ], + ) + rds_conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=5432, + DBSubnetGroupName=subnet_group_name, + Tags=[{"Key": "test", "Value": "test"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_inside_vpc.rds_instance_inside_vpc.rds_client", + new=RDS(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_inside_vpc.rds_instance_inside_vpc import ( + rds_instance_inside_vpc, + ) + + check = rds_instance_inside_vpc() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"RDS Instance db-master-1 is deployed in a VPC {vpc_id}." + ) + assert result[0].resource_id == "db-master-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + ) + assert result[0].resource_tags == [{"Key": "test", "Value": "test"}] + + def test_rds_instance_not_in_vpc(self): + rds_client = mock.MagicMock + instance_arn = ( + f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + ) + rds_client.db_instances = { + instance_arn: DBInstance( + id="db-master-1", + arn=instance_arn, + engine="postgres", + cloudwatch_logs=None, + deletion_protection=True, + auto_minor_version_upgrade=True, + enhanced_monitoring_arn=None, + endpoint={ + "Address": "db-master-1.us-east-1.rds.amazonaws.com", + "Port": 5432, + }, + engine_version="12.3", + status="available", + public=False, + encrypted=False, + iam_auth=False, + region=AWS_REGION_US_EAST_1, + multi_az=False, + username="admin", + tags=[{"Key": "test", "Value": "test"}], + copy_tags_to_snapshot=None, + ) + } + + with mock.patch( + "prowler.providers.aws.services.rds.rds_service.RDS", + new=rds_client, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_instance_inside_vpc.rds_instance_inside_vpc.rds_client", + new=rds_client, + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_inside_vpc.rds_instance_inside_vpc import ( + rds_instance_inside_vpc, + ) + + check = rds_instance_inside_vpc() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Instance db-master-1 is not deployed in a VPC." + ) + assert result[0].resource_id == "db-master-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + ) + assert result[0].resource_tags == [{"Key": "test", "Value": "test"}] diff --git a/tests/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs_test.py b/tests/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs_test.py index d6abac7c78..1077bdfd47 100644 --- a/tests/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs_test.py +++ b/tests/providers/aws/services/rds/rds_instance_integration_cloudwatch_logs/rds_instance_integration_cloudwatch_logs_test.py @@ -135,7 +135,7 @@ class Test_rds_instance_integration_cloudwatch_logs: assert result[0].status == "PASS" assert ( result[0].status_extended - == "RDS Instance db-master-1 is shipping audit error to CloudWatch Logs." + == "RDS Instance db-master-1 is shipping audit, error logs to CloudWatch Logs." ) assert result[0].resource_id == "db-master-1" assert result[0].region == AWS_REGION_US_EAST_1 diff --git a/tests/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port_test.py b/tests/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port_test.py new file mode 100644 index 0000000000..97775dc847 --- /dev/null +++ b/tests/providers/aws/services/rds/rds_instance_non_default_port/rds_instance_non_default_port_test.py @@ -0,0 +1,243 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_rds_instance_non_default_port: + @mock_aws + def test_rds_no_instances(self): + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_rds_instance_using_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-master-1", + AllocatedStorage=10, + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=5432, + Tags=[{"Key": "test", "Value": "test"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Instance db-master-1 is using the default port 5432 for postgres." + ) + assert result[0].resource_id == "db-master-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + ) + assert result[0].resource_tags == [{"Key": "test", "Value": "test"}] + + @mock_aws + def test_rds_instance_using_non_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-master-2", + AllocatedStorage=10, + Engine="postgres", + DBName="production-postgres", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=5433, + Tags=[{"Key": "env", "Value": "production"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Instance db-master-2 is not using the default port 5433 for postgres." + ) + assert result[0].resource_id == "db-master-2" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-2" + ) + assert result[0].resource_tags == [ + {"Key": "env", "Value": "production"} + ] + + @mock_aws + def test_rds_instance_mariadb_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-master-3", + AllocatedStorage=10, + Engine="mariadb", + DBName="staging-mariadb", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=3306, + Tags=[{"Key": "env", "Value": "staging"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == "RDS Instance db-master-3 is using the default port 3306 for mariadb." + ) + assert result[0].resource_id == "db-master-3" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-3" + ) + assert result[0].resource_tags == [{"Key": "env", "Value": "staging"}] + + @mock_aws + def test_rds_instance_mariadb_non_default_port(self): + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-master-4", + AllocatedStorage=10, + Engine="mariadb", + DBName="production-mariadb", + DBInstanceClass="db.m1.small", + StorageEncrypted=True, + DeletionProtection=True, + PubliclyAccessible=True, + AutoMinorVersionUpgrade=True, + BackupRetentionPeriod=10, + Port=3307, + Tags=[{"Key": "env", "Value": "production"}], + ) + + from prowler.providers.aws.services.rds.rds_service import RDS + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port.rds_client", + new=RDS(aws_provider), + ): + from prowler.providers.aws.services.rds.rds_instance_non_default_port.rds_instance_non_default_port import ( + rds_instance_non_default_port, + ) + + check = rds_instance_non_default_port() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Instance db-master-4 is not using the default port 3307 for mariadb." + ) + assert result[0].resource_id == "db-master-4" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-4" + ) + assert result[0].resource_tags == [ + {"Key": "env", "Value": "production"} + ] diff --git a/tests/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan_test.py b/tests/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan_test.py index d3c451077f..5686480fd7 100644 --- a/tests/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan_test.py +++ b/tests/providers/aws/services/rds/rds_instance_protected_by_backup_plan/rds_instance_protected_by_backup_plan_test.py @@ -1,7 +1,5 @@ from unittest import mock -from unittest.mock import patch -import botocore from boto3 import client from moto import mock_aws @@ -11,39 +9,6 @@ from tests.providers.aws.utils import ( set_mocked_aws_provider, ) -make_api_call = botocore.client.BaseClient._make_api_call - - -def mock_make_api_call(self, operation_name, kwarg): - if operation_name == "CreateBackupSelection": - return { - "SelectionName": "test-backup-selection", - "IamRoleArn": "arn:aws:iam::123456789012:role/backup-role", - "Resources": [ - f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1", - ], - } - elif operation_name == "ListProtectedResources": - return { - "Results": [ - { - "ResourceArn": f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1", - "ResourceType": "RDS", - "LastBackupTime": "2023-08-23T00:00:00Z", - } - ] - } - elif operation_name == "ListBackupPlans": - return { - "BackupPlans": [ - { - "BackupPlanId": "test-backup-plan-id", - "BackupPlanName": "test-backup-plan", - } - ] - } - return make_api_call(self, operation_name, kwarg) - class Test_rds_instance_protected_by_backup_plan: @mock_aws @@ -123,36 +88,43 @@ class Test_rds_instance_protected_by_backup_plan: ) assert result[0].resource_tags == [] - @mock_aws def test_rds_instance_without_backup_plan(self): - instance = client("rds", region_name=AWS_REGION_US_EAST_1) - backup = client("backup", region_name=AWS_REGION_US_EAST_1) - instance.create_db_instance( - DBInstanceIdentifier="db-master-1", - AllocatedStorage=10, - Engine="postgres", - DBName="staging-postgres", - DBInstanceClass="db.m1.small", - ) - backup.create_backup_plan( - BackupPlan={ - "BackupPlanName": "test-backup-plan", - "Rules": [ - { - "RuleName": "DailyBackup", - "TargetBackupVaultName": "test-vault", - "ScheduleExpression": "cron(0 12 * * ? *)", - "Lifecycle": {"DeleteAfterDays": 30}, - "RecoveryPointTags": { - "Type": "Daily", - }, - }, - ], - } - ) + instance = mock.MagicMock() + backup = mock.MagicMock() - from prowler.providers.aws.services.backup.backup_service import Backup - from prowler.providers.aws.services.rds.rds_service import RDS + from prowler.providers.aws.services.rds.rds_service import DBInstance + + arn = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + instance.db_instances = { + arn: DBInstance( + id="db-master-1", + arn=f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1", + endpoint={ + "Address": "db-master-1.c9akciq32.rds.amazonaws.com", + "Port": 5432, + }, + engine_version="13.3", + status="available", + public=False, + encrypted=True, + deletion_protection=False, + auto_minor_version_upgrade=True, + multi_az=False, + username="admin", + iam_auth=False, + name="db-master-1", + region="us-east-1", + instance_class="db.m1.small", + engine="postgres", + db_name="staging-postgres", + allocated_storage=10, + tags=[], + ) + } + + backup.protected_resources = [ + f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-2" + ] aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) @@ -162,10 +134,16 @@ class Test_rds_instance_protected_by_backup_plan: ): with mock.patch( "prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan.rds_client", - new=RDS(aws_provider), + new=instance, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_client.rds_client", + new=instance, ), mock.patch( "prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan.backup_client", - new=Backup(aws_provider), + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, ): # Test Check from prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan import ( @@ -189,81 +167,233 @@ class Test_rds_instance_protected_by_backup_plan: ) assert result[0].resource_tags == [] - @mock_aws def test_rds_instance_with_backup_plan(self): - with patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call): - instance = client("rds", region_name=AWS_REGION_US_EAST_1) - backup = client("backup", region_name=AWS_REGION_US_EAST_1) - instance.create_db_instance( - DBInstanceIdentifier="db-master-1", - AllocatedStorage=10, - Engine="postgres", - DBName="staging-postgres", - DBInstanceClass="db.m1.small", - ) - backup.create_backup_plan( - BackupPlan={ - "BackupPlanName": "test-backup-plan", - "Rules": [ - { - "RuleName": "DailyBackup", - "TargetBackupVaultName": "test-vault", - "ScheduleExpression": "cron(0 12 * * ? *)", - "Lifecycle": {"DeleteAfterDays": 30}, - "RecoveryPointTags": { - "Type": "Daily", - }, - }, - ], - } - ) - backup.create_backup_selection( - BackupPlanID={ - backup.list_backup_plans()["BackupPlans"][0]["BackupPlanId"] - }, - BackupPlanSelection={ - "SelectionName": "test-backup-selection", - "IamRoleArn": "arn:aws:iam::123456789012:role/backup-role", - "Resources": [ - f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1", - ], + instance = mock.MagicMock() + + from prowler.providers.aws.services.rds.rds_service import DBInstance + + arn = f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + instance.db_instances = { + arn: DBInstance( + id="db-master-1", + arn=f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1", + endpoint={ + "Address": "db-master-1.c9akciq32.rds.amazonaws.com", + "Port": 5432, }, + engine_version="13.3", + status="available", + public=False, + encrypted=True, + deletion_protection=False, + auto_minor_version_upgrade=True, + multi_az=False, + username="admin", + iam_auth=False, + name="db-master-1", + region="us-east-1", + instance_class="db.m1.small", + engine="postgres", + db_name="staging-postgres", + allocated_storage=10, + tags=[], ) + } - from prowler.providers.aws.services.backup.backup_service import Backup - from prowler.providers.aws.services.rds.rds_service import RDS + backup = mock.MagicMock() + backup.protected_resources = [arn] - aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): with mock.patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=aws_provider, + "prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan.rds_client", + new=instance, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_client.rds_client", + new=instance, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, ): - with mock.patch( - "prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan.rds_client", - new=RDS(aws_provider), - ), mock.patch( - "prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan.backup_client", - new=Backup(aws_provider), - ): - # Test Check - from prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan import ( - rds_instance_protected_by_backup_plan, - ) + # Test Check + from prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan import ( + rds_instance_protected_by_backup_plan, + ) - check = rds_instance_protected_by_backup_plan() - result = check.execute() + check = rds_instance_protected_by_backup_plan() + result = check.execute() - assert len(result) == 1 - assert result[0].status == "PASS" - assert ( - result[0].status_extended - == "RDS Instance db-master-1 is protected by a backup plan." - ) - assert result[0].resource_id == "db-master-1" - assert result[0].region == AWS_REGION_US_EAST_1 - assert ( - result[0].resource_arn - == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" - ) - assert result[0].resource_tags == [] + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Instance db-master-1 is protected by a backup plan." + ) + assert result[0].resource_id == "db-master-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + ) + assert result[0].resource_tags == [] + + def test_rds_instance_with_backup_plan_via_instance_wildcard(self): + instance = mock.MagicMock() + + from prowler.providers.aws.services.rds.rds_service import DBInstance + + arn = "arn:aws:dynamodb:*:*:instance:*" + instance.db_instances = { + f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1": DBInstance( + id="db-master-1", + arn=f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1", + endpoint={ + "Address": "db-master-1.c9akciq32.rds.amazonaws.com", + "Port": 5432, + }, + engine_version="13.3", + status="available", + public=False, + encrypted=True, + deletion_protection=False, + auto_minor_version_upgrade=True, + multi_az=False, + username="admin", + iam_auth=False, + name="db-master-1", + region="us-east-1", + instance_class="db.m1.small", + engine="postgres", + db_name="staging-postgres", + allocated_storage=10, + tags=[], + ) + } + + backup = mock.MagicMock() + backup.protected_resources = [arn] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan.rds_client", + new=instance, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_client.rds_client", + new=instance, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan import ( + rds_instance_protected_by_backup_plan, + ) + + check = rds_instance_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Instance db-master-1 is protected by a backup plan." + ) + assert result[0].resource_id == "db-master-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + ) + assert result[0].resource_tags == [] + + def test_rds_instance_with_backup_plan_via_all_wildcard(self): + instance = mock.MagicMock() + + from prowler.providers.aws.services.rds.rds_service import DBInstance + + arn = "*" + instance.db_instances = { + f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1": DBInstance( + id="db-master-1", + arn=f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1", + endpoint={ + "Address": "db-master-1.c9akciq32.rds.amazonaws.com", + "Port": 5432, + }, + engine_version="13.3", + status="available", + public=False, + encrypted=True, + deletion_protection=False, + auto_minor_version_upgrade=True, + multi_az=False, + username="admin", + iam_auth=False, + name="db-master-1", + region="us-east-1", + instance_class="db.m1.small", + engine="postgres", + db_name="staging-postgres", + allocated_storage=10, + tags=[], + ) + } + + backup = mock.MagicMock() + backup.protected_resources = [arn] + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan.rds_client", + new=instance, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_client.rds_client", + new=instance, + ), mock.patch( + "prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan.backup_client", + new=backup, + ), mock.patch( + "prowler.providers.aws.services.backup.backup_client.backup_client", + new=backup, + ): + # Test Check + from prowler.providers.aws.services.rds.rds_instance_protected_by_backup_plan.rds_instance_protected_by_backup_plan import ( + rds_instance_protected_by_backup_plan, + ) + + check = rds_instance_protected_by_backup_plan() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == "RDS Instance db-master-1 is protected by a backup plan." + ) + assert result[0].resource_id == "db-master-1" + assert result[0].region == AWS_REGION_US_EAST_1 + assert ( + result[0].resource_arn + == f"arn:aws:rds:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:db:db-master-1" + ) + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/rds/rds_service_test.py b/tests/providers/aws/services/rds/rds_service_test.py index 5008a1fc1c..4145f5e2c4 100644 --- a/tests/providers/aws/services/rds/rds_service_test.py +++ b/tests/providers/aws/services/rds/rds_service_test.py @@ -33,7 +33,6 @@ def mock_make_api_call(self, operation_name, kwarg): @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) class Test_RDS_Service: - # Test Dynamo Service @mock_aws def test_service(self): @@ -95,6 +94,7 @@ class Test_RDS_Service: {"Key": "test", "Value": "test"}, ], CopyTagsToSnapshot=True, + Port=5432, ) # RDS client for this test class aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) @@ -121,6 +121,7 @@ class Test_RDS_Service: ] assert "test" in db_instance.parameter_groups assert db_instance.copy_tags_to_snapshot + assert db_instance.port == 5432 @mock_aws def test_describe_db_parameters(self): @@ -276,6 +277,7 @@ class Test_RDS_Service: {"Key": "test", "Value": "test"}, ], CopyTagsToSnapshot=True, + Port=5432, ) # RDS client for this test class aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) @@ -306,6 +308,7 @@ class Test_RDS_Service: assert rds.db_clusters[db_cluster_arn].force_ssl == "0" assert rds.db_clusters[db_cluster_arn].require_secure_transport == "OFF" assert rds.db_clusters[db_cluster_arn].copy_tags_to_snapshot + assert rds.db_clusters[db_cluster_arn].port == 5432 # Test RDS Describe DB Cluster Snapshots @mock_aws @@ -378,3 +381,37 @@ class Test_RDS_Service: rds.db_engines[AWS_REGION_US_EAST_1]["mysql"].engine_description == "description" ) + + @mock_aws + def test_list_tags(self): + # RDS client for this test class + conn = client("rds", region_name=AWS_REGION_US_EAST_1) + conn.create_db_instance( + DBInstanceIdentifier="db-primary-1", + AllocatedStorage=10, + Engine="postgres", + DBName="staging-postgres", + DBInstanceClass="db.m1.small", + ) + event_sub = conn.create_event_subscription( + SubscriptionName="TestSub", + SnsTopicArn=f"arn:aws:sns:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:test", + SourceType="db-security-group", + Enabled=True, + Tags=[ + {"Key": "test", "Value": "testing"}, + ], + ) + # Tag event subscription + conn.add_tags_to_resource( + ResourceName=event_sub["EventSubscription"]["EventSubscriptionArn"], + Tags=[ + {"Key": "test", "Value": "testing"}, + ], + ) + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + rds = RDS(aws_provider) + assert len(rds.db_event_subscriptions) == 1 + assert rds.db_event_subscriptions[0].tags == [ + {"Key": "test", "Value": "testing"}, + ] diff --git a/tests/providers/aws/services/s3/s3_access_point_public_access_block/s3_access_point_public_access_block_test.py b/tests/providers/aws/services/s3/s3_access_point_public_access_block/s3_access_point_public_access_block_test.py index d4fb68ee67..4ab9c359d1 100644 --- a/tests/providers/aws/services/s3/s3_access_point_public_access_block/s3_access_point_public_access_block_test.py +++ b/tests/providers/aws/services/s3/s3_access_point_public_access_block/s3_access_point_public_access_block_test.py @@ -316,3 +316,112 @@ class Test_s3_access_point_public_access_block: == f"arn:aws:s3:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:accesspoint/{ap_name_eu}" ) assert result[1].region == AWS_REGION_EU_WEST_1 + + def test_access_points_mixed_regions(self): + aws_provider = set_mocked_aws_provider( + [AWS_REGION_US_EAST_1, AWS_REGION_EU_WEST_1, "ap-southeast-2"] + ) + + ap_name_us = "test-access-point-us-east-1" + ap_name_eu = "test-access-point-eu-west-1" + ap_name_ap = "test-access-point-ap-southeast-2" + + arn_us = f"arn:aws:s3:us-east-1:{AWS_ACCOUNT_NUMBER}:accesspoint/{ap_name_us}" + arn_eu = f"arn:aws:s3:eu-west-1:{AWS_ACCOUNT_NUMBER}:accesspoint/{ap_name_eu}" + arn_ap = ( + f"arn:aws:s3:ap-southeast-2:{AWS_ACCOUNT_NUMBER}:accesspoint/{ap_name_ap}" + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.s3.s3_access_point_public_access_block.s3_access_point_public_access_block.s3control_client", + new=S3Control(aws_provider), + ): + from prowler.providers.aws.services.s3.s3_access_point_public_access_block.s3_access_point_public_access_block import ( + s3_access_point_public_access_block, + ) + + s3control_client = mock.MagicMock() + s3control_client.access_points = { + arn_us: AccessPoint( + account_id=AWS_ACCOUNT_NUMBER, + name=ap_name_us, + bucket="bucket-us", + region="us-east-1", + public_access_block=PublicAccessBlock( + block_public_acls=True, + ignore_public_acls=True, + block_public_policy=True, + restrict_public_buckets=True, + ), + ), + arn_eu: AccessPoint( + account_id=AWS_ACCOUNT_NUMBER, + name=ap_name_eu, + bucket="bucket-eu", + region="eu-west-1", + public_access_block=PublicAccessBlock( + block_public_acls=False, + ignore_public_acls=False, + block_public_policy=False, + restrict_public_buckets=False, + ), + ), + arn_ap: AccessPoint( + account_id=AWS_ACCOUNT_NUMBER, + name=ap_name_ap, + bucket="bucket-ap", + region="ap-southeast-2", + public_access_block=PublicAccessBlock( + block_public_acls=True, + ignore_public_acls=False, + block_public_policy=True, + restrict_public_buckets=True, + ), + ), + } + + with patch( + "prowler.providers.aws.services.s3.s3_access_point_public_access_block.s3_access_point_public_access_block.s3control_client", + s3control_client, + ): + check = s3_access_point_public_access_block() + results = check.execute() + + assert len(results) == 3 + for result in results: + if result.resource_id == ap_name_us: + assert result.region == AWS_REGION_US_EAST_1 + assert result.status == "PASS" + assert ( + result.status_extended + == f"Access Point {ap_name_us} of bucket bucket-us does have Public Access Block enabled." + ) + assert ( + result.resource_arn + == f"arn:aws:s3:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:accesspoint/{ap_name_us}" + ) + elif result.resource_id == ap_name_eu: + assert result.region == AWS_REGION_EU_WEST_1 + assert result.status == "FAIL" + assert ( + result.status_extended + == f"Access Point {ap_name_eu} of bucket bucket-eu does not have Public Access Block enabled." + ) + assert ( + result.resource_arn + == f"arn:aws:s3:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:accesspoint/{ap_name_eu}" + ) + elif result.resource_id == ap_name_ap: + assert result.region == "ap-southeast-2" + assert result.status == "FAIL" + assert ( + result.status_extended + == f"Access Point {ap_name_ap} of bucket bucket-ap does not have Public Access Block enabled." + ) + assert ( + result.resource_arn + == f"arn:aws:s3:ap-southeast-2:{AWS_ACCOUNT_NUMBER}:accesspoint/{ap_name_ap}" + ) diff --git a/tests/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access_test.py b/tests/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access_test.py new file mode 100644 index 0000000000..176e013561 --- /dev/null +++ b/tests/providers/aws/services/s3/s3_bucket_cross_account_access/s3_bucket_cross_account_access_test.py @@ -0,0 +1,245 @@ +import json +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_s3_bucket_cross_account_access: + @mock_aws + def test_no_buckets(self): + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access import ( + s3_bucket_cross_account_access, + ) + + check = s3_bucket_cross_account_access() + result = check.execute() + + assert len(result) == 0 + + @mock_aws + def test_bucket_no_policy(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + bucket_name_us = "bucket_test_us" + s3_client.create_bucket(Bucket=bucket_name_us) + + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access import ( + s3_bucket_cross_account_access, + ) + + check = s3_bucket_cross_account_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} does not have a bucket policy." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bucket_policy_allow_delete(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + delete_bucket_policy = "s3:DeleteBucketPolicy" + bucket_name_us = "bucket_test_us" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "*"}, + "Action": delete_bucket_policy, + "Resource": "arn:aws:s3:::*", + } + ], + } + s3_client.create_bucket(Bucket=bucket_name_us) + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=json.dumps(policy), + ) + + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access import ( + s3_bucket_cross_account_access, + ) + + check = s3_bucket_cross_account_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} has a bucket policy allowing cross account access." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bucket_policy_allow_multiple_other_accounts(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + put_encryption_configuration = "s3:PutEncryptionConfiguration" + put_bucket_policy = "s3:PutBucketPolicy" + bucket_name_us = "bucket_test_us" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::*:root"}, + "Action": [ + put_encryption_configuration, + put_bucket_policy, + ], + "Resource": "arn:aws:s3:::*", + } + ], + } + s3_client.create_bucket(Bucket=bucket_name_us) + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=json.dumps(policy), + ) + + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access import ( + s3_bucket_cross_account_access, + ) + + check = s3_bucket_cross_account_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} has a bucket policy allowing cross account access." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 + + @mock_aws + def test_bucket_policy_allow_same_account(self): + s3_client = client("s3", region_name=AWS_REGION_US_EAST_1) + delete_bucket_policy = "s3:DeleteBucketPolicy" + bucket_name_us = "bucket_test_us" + policy = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": {"AWS": f"arn:aws:iam::{AWS_ACCOUNT_NUMBER}:root"}, + "Action": delete_bucket_policy, + "Resource": "arn:aws:s3:::*", + } + ], + } + s3_client.create_bucket(Bucket=bucket_name_us) + s3_client.put_bucket_policy( + Bucket=bucket_name_us, + Policy=json.dumps(policy), + ) + + from prowler.providers.aws.services.s3.s3_service import S3 + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access.s3_client", + new=S3(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.s3.s3_bucket_cross_account_access.s3_bucket_cross_account_access import ( + s3_bucket_cross_account_access, + ) + + check = s3_bucket_cross_account_access() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"S3 Bucket {bucket_name_us} has a bucket policy but it does not allow cross account access." + ) + assert result[0].resource_id == bucket_name_us + assert ( + result[0].resource_arn + == f"arn:{aws_provider.identity.partition}:s3:::{bucket_name_us}" + ) + assert result[0].region == AWS_REGION_US_EAST_1 diff --git a/tests/providers/aws/services/s3/s3_service_test.py b/tests/providers/aws/services/s3/s3_service_test.py index 1e02f7cc3b..dc902b3437 100644 --- a/tests/providers/aws/services/s3/s3_service_test.py +++ b/tests/providers/aws/services/s3/s3_service_test.py @@ -2,6 +2,7 @@ import json from unittest.mock import patch import botocore +import botocore.exceptions from boto3 import client from moto import mock_aws @@ -478,6 +479,32 @@ class Test_S3_Service: assert s3.buckets[bucket_arn].lifecycle[0].id == "test" assert s3.buckets[bucket_arn].lifecycle[0].status == "Enabled" + # Test S3 Head Bucket + @mock_aws + @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + def test_head_bucket(self): + # Generate S3 Client + s3_client = client("s3") + + # Create S3 Bucket + bucket_name = "test-bucket" + bucket_arn = f"arn:aws:s3:::{bucket_name}" + s3_client.create_bucket( + Bucket=bucket_name, + ObjectOwnership="BucketOwnerEnforced", + ObjectLockEnabledForBucket=True, + ) + + # S3 client for this test class + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + s3 = S3(aws_provider) + assert len(s3.buckets) == 1 + assert s3.buckets[bucket_arn].name == bucket_name + assert s3._head_bucket( + bucket_name=bucket_name, + ) + assert s3.buckets[bucket_arn].region == AWS_REGION_US_EAST_1 + # Test S3 List Access Points @patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) @mock_aws diff --git a/tests/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances_test.py b/tests/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances_test.py new file mode 100644 index 0000000000..714726b50e --- /dev/null +++ b/tests/providers/aws/services/sagemaker/sagemaker_endpoint_config_prod_variant_instances/sagemaker_endpoint_config_prod_variant_instances_test.py @@ -0,0 +1,148 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import ( + AWS_REGION_EU_WEST_1, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) + + +class Test_sagemaker_endpoint_config_prod_variant_instances: + @mock_aws + def test_no_endpoint_configs(self): + + from prowler.providers.aws.services.sagemaker.sagemaker_service import SageMaker + + aws_provider = set_mocked_aws_provider( + [AWS_REGION_EU_WEST_1, AWS_REGION_US_EAST_1] + ) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_endpoint_config_prod_variant_instances.sagemaker_endpoint_config_prod_variant_instances.sagemaker_client", + new=SageMaker(aws_provider), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_endpoint_config_prod_variant_instances.sagemaker_endpoint_config_prod_variant_instances import ( + sagemaker_endpoint_config_prod_variant_instances, + ) + + check = sagemaker_endpoint_config_prod_variant_instances() + result = check.execute() + assert len(result) == 0 + + @mock_aws + def test_endpoint_config_non_compliant_prod_variant(self): + sagemaker_client = client("sagemaker", region_name=AWS_REGION_EU_WEST_1) + endpoint_config_name = "endpoint-config-test" + prod_variant_name = "Variant1" + prod_variant_name2 = "Variant2" + model_name = "mi-modelo-v1" + model_name2 = "mi-modelo-v2" + sagemaker_client.create_model(ModelName=model_name) + sagemaker_client.create_model(ModelName=model_name2) + endpoint_config = sagemaker_client.create_endpoint_config( + EndpointConfigName=endpoint_config_name, + ProductionVariants=[ + { + "VariantName": prod_variant_name, + "ModelName": "mi-modelo-v1", + "InitialInstanceCount": 1, + "InstanceType": "ml.m5.large", + "InitialVariantWeight": 0.6, + }, + { + "VariantName": prod_variant_name2, + "ModelName": "mi-modelo-v2", + "InitialInstanceCount": 2, + "InstanceType": "ml.m5.large", + "InitialVariantWeight": 0.4, + }, + ], + ) + + from prowler.providers.aws.services.sagemaker.sagemaker_service import SageMaker + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_endpoint_config_prod_variant_instances.sagemaker_endpoint_config_prod_variant_instances.sagemaker_client", + new=SageMaker(aws_provider), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_endpoint_config_prod_variant_instances.sagemaker_endpoint_config_prod_variant_instances import ( + sagemaker_endpoint_config_prod_variant_instances, + ) + + check = sagemaker_endpoint_config_prod_variant_instances() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Sagemaker Endpoint Config {endpoint_config_name}'s production variants {prod_variant_name} with less than two initial instance." + ) + assert result[0].resource_id == endpoint_config_name + assert result[0].resource_arn == endpoint_config["EndpointConfigArn"] + + @mock_aws + def test_endpoint_config_compliant_prod_variants(self): + sagemaker_client = client("sagemaker", region_name=AWS_REGION_EU_WEST_1) + endpoint_config_name = "endpoint-config-test" + prod_variant_name = "Variant1" + prod_variant_name2 = "Variant2" + model_name = "mi-modelo-v1" + model_name2 = "mi-modelo-v2" + sagemaker_client.create_model(ModelName=model_name) + sagemaker_client.create_model(ModelName=model_name2) + endpoint_config = sagemaker_client.create_endpoint_config( + EndpointConfigName=endpoint_config_name, + ProductionVariants=[ + { + "VariantName": prod_variant_name, + "ModelName": "mi-modelo-v1", + "InitialInstanceCount": 2, + "InstanceType": "ml.m5.large", + "InitialVariantWeight": 0.6, + }, + { + "VariantName": prod_variant_name2, + "ModelName": "mi-modelo-v2", + "InitialInstanceCount": 2, + "InstanceType": "ml.m5.large", + "InitialVariantWeight": 0.4, + }, + ], + ) + + from prowler.providers.aws.services.sagemaker.sagemaker_service import SageMaker + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_endpoint_config_prod_variant_instances.sagemaker_endpoint_config_prod_variant_instances.sagemaker_client", + new=SageMaker(aws_provider), + ): + from prowler.providers.aws.services.sagemaker.sagemaker_endpoint_config_prod_variant_instances.sagemaker_endpoint_config_prod_variant_instances import ( + sagemaker_endpoint_config_prod_variant_instances, + ) + + check = sagemaker_endpoint_config_prod_variant_instances() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Sagemaker Endpoint Config {endpoint_config_name} has all production variants with more than one initial instance." + ) + assert result[0].resource_id == endpoint_config_name + assert result[0].resource_arn == endpoint_config["EndpointConfigArn"] diff --git a/tests/providers/aws/services/sagemaker/sagemaker_models_network_isolation_enabled/sagemaker_models_network_isolation_enabled_test.py b/tests/providers/aws/services/sagemaker/sagemaker_models_network_isolation_enabled/sagemaker_models_network_isolation_enabled_test.py index bf51690923..b0a6db43be 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_models_network_isolation_enabled/sagemaker_models_network_isolation_enabled_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_models_network_isolation_enabled/sagemaker_models_network_isolation_enabled_test.py @@ -2,7 +2,11 @@ from unittest import mock from uuid import uuid4 from prowler.providers.aws.services.sagemaker.sagemaker_service import Model -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) test_notebook_instance = "test-notebook-instance" notebook_instance_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:notebook-instance/{test_notebook_instance}" @@ -13,8 +17,14 @@ class Test_sagemaker_models_network_isolation_enabled: def test_no_models(self): sagemaker_client = mock.MagicMock sagemaker_client.sagemaker_models = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_models_network_isolation_enabled.sagemaker_models_network_isolation_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_models_network_isolation_enabled.sagemaker_models_network_isolation_enabled import ( @@ -36,8 +46,14 @@ class Test_sagemaker_models_network_isolation_enabled: network_isolation=True, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_models_network_isolation_enabled.sagemaker_models_network_isolation_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_models_network_isolation_enabled.sagemaker_models_network_isolation_enabled import ( @@ -66,8 +82,14 @@ class Test_sagemaker_models_network_isolation_enabled: network_isolation=False, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_models_network_isolation_enabled.sagemaker_models_network_isolation_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_models_network_isolation_enabled.sagemaker_models_network_isolation_enabled import ( diff --git a/tests/providers/aws/services/sagemaker/sagemaker_models_vpc_settings_configured/sagemaker_models_vpc_settings_configured_test.py b/tests/providers/aws/services/sagemaker/sagemaker_models_vpc_settings_configured/sagemaker_models_vpc_settings_configured_test.py index 65ddfaea2e..42df229c0a 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_models_vpc_settings_configured/sagemaker_models_vpc_settings_configured_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_models_vpc_settings_configured/sagemaker_models_vpc_settings_configured_test.py @@ -2,7 +2,11 @@ from unittest import mock from uuid import uuid4 from prowler.providers.aws.services.sagemaker.sagemaker_service import Model -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) test_notebook_instance = "test-notebook-instance" notebook_instance_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:notebook-instance/{test_notebook_instance}" @@ -13,8 +17,14 @@ class Test_sagemaker_models_vpc_settings_configured: def test_no_models(self): sagemaker_client = mock.MagicMock sagemaker_client.sagemaker_models = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_models_vpc_settings_configured.sagemaker_models_vpc_settings_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_models_vpc_settings_configured.sagemaker_models_vpc_settings_configured import ( @@ -36,8 +46,14 @@ class Test_sagemaker_models_vpc_settings_configured: vpc_config_subnets=[subnet_id], ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_models_vpc_settings_configured.sagemaker_models_vpc_settings_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_models_vpc_settings_configured.sagemaker_models_vpc_settings_configured import ( @@ -65,8 +81,14 @@ class Test_sagemaker_models_vpc_settings_configured: region=AWS_REGION_EU_WEST_1, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_models_vpc_settings_configured.sagemaker_models_vpc_settings_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_models_vpc_settings_configured.sagemaker_models_vpc_settings_configured import ( diff --git a/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_encryption_enabled/sagemaker_notebook_instance_encryption_enabled_test.py b/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_encryption_enabled/sagemaker_notebook_instance_encryption_enabled_test.py index fe829c5ad9..5227206736 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_encryption_enabled/sagemaker_notebook_instance_encryption_enabled_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_encryption_enabled/sagemaker_notebook_instance_encryption_enabled_test.py @@ -2,7 +2,11 @@ from unittest import mock from uuid import uuid4 from prowler.providers.aws.services.sagemaker.sagemaker_service import NotebookInstance -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) test_notebook_instance = "test-notebook-instance" notebook_instance_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:notebook-instance/{test_notebook_instance}" @@ -13,8 +17,14 @@ class Test_sagemaker_notebook_instance_encryption_enabled: def test_no_instances(self): sagemaker_client = mock.MagicMock sagemaker_client.sagemaker_notebook_instances = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_encryption_enabled.sagemaker_notebook_instance_encryption_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_encryption_enabled.sagemaker_notebook_instance_encryption_enabled import ( @@ -36,8 +46,14 @@ class Test_sagemaker_notebook_instance_encryption_enabled: kms_key_id=kms_key, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_encryption_enabled.sagemaker_notebook_instance_encryption_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_encryption_enabled.sagemaker_notebook_instance_encryption_enabled import ( @@ -65,8 +81,14 @@ class Test_sagemaker_notebook_instance_encryption_enabled: region=AWS_REGION_EU_WEST_1, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_encryption_enabled.sagemaker_notebook_instance_encryption_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_encryption_enabled.sagemaker_notebook_instance_encryption_enabled import ( diff --git a/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_root_access_disabled/sagemaker_notebook_instance_root_access_disabled_test.py b/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_root_access_disabled/sagemaker_notebook_instance_root_access_disabled_test.py index be033d12e6..3b0a38382a 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_root_access_disabled/sagemaker_notebook_instance_root_access_disabled_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_root_access_disabled/sagemaker_notebook_instance_root_access_disabled_test.py @@ -1,7 +1,11 @@ from unittest import mock from prowler.providers.aws.services.sagemaker.sagemaker_service import NotebookInstance -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) test_notebook_instance = "test-notebook-instance" notebook_instance_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:notebook-instance/{test_notebook_instance}" @@ -11,8 +15,14 @@ class Test_sagemaker_notebook_instance_root_access_disabled: def test_no_instances(self): sagemaker_client = mock.MagicMock sagemaker_client.sagemaker_notebook_instances = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_root_access_disabled.sagemaker_notebook_instance_root_access_disabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_root_access_disabled.sagemaker_notebook_instance_root_access_disabled import ( @@ -34,8 +44,14 @@ class Test_sagemaker_notebook_instance_root_access_disabled: root_access=False, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_root_access_disabled.sagemaker_notebook_instance_root_access_disabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_root_access_disabled.sagemaker_notebook_instance_root_access_disabled import ( @@ -64,8 +80,14 @@ class Test_sagemaker_notebook_instance_root_access_disabled: root_access=True, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_root_access_disabled.sagemaker_notebook_instance_root_access_disabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_root_access_disabled.sagemaker_notebook_instance_root_access_disabled import ( diff --git a/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_vpc_settings_configured/sagemaker_notebook_instance_vpc_settings_configured_test.py b/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_vpc_settings_configured/sagemaker_notebook_instance_vpc_settings_configured_test.py index 4d0e70ff10..b613cd4c74 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_vpc_settings_configured/sagemaker_notebook_instance_vpc_settings_configured_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_vpc_settings_configured/sagemaker_notebook_instance_vpc_settings_configured_test.py @@ -2,7 +2,11 @@ from unittest import mock from uuid import uuid4 from prowler.providers.aws.services.sagemaker.sagemaker_service import NotebookInstance -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) test_notebook_instance = "test-notebook-instance" notebook_instance_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:notebook-instance/{test_notebook_instance}" @@ -13,8 +17,14 @@ class Test_sagemaker_notebook_instance_vpc_settings_configured: def test_no_instances(self): sagemaker_client = mock.MagicMock sagemaker_client.sagemaker_notebook_instances = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_vpc_settings_configured.sagemaker_notebook_instance_vpc_settings_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_vpc_settings_configured.sagemaker_notebook_instance_vpc_settings_configured import ( @@ -36,8 +46,14 @@ class Test_sagemaker_notebook_instance_vpc_settings_configured: subnet_id=subnet_id, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_vpc_settings_configured.sagemaker_notebook_instance_vpc_settings_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_vpc_settings_configured.sagemaker_notebook_instance_vpc_settings_configured import ( @@ -66,8 +82,14 @@ class Test_sagemaker_notebook_instance_vpc_settings_configured: root_access=True, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_vpc_settings_configured.sagemaker_notebook_instance_vpc_settings_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_vpc_settings_configured.sagemaker_notebook_instance_vpc_settings_configured import ( diff --git a/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_without_direct_internet_access_configured/sagemaker_notebook_instance_without_direct_internet_access_configured_test.py b/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_without_direct_internet_access_configured/sagemaker_notebook_instance_without_direct_internet_access_configured_test.py index cbc08f5f74..8c80572443 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_without_direct_internet_access_configured/sagemaker_notebook_instance_without_direct_internet_access_configured_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_notebook_instance_without_direct_internet_access_configured/sagemaker_notebook_instance_without_direct_internet_access_configured_test.py @@ -1,7 +1,11 @@ from unittest import mock from prowler.providers.aws.services.sagemaker.sagemaker_service import NotebookInstance -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) test_notebook_instance = "test-notebook-instance" notebook_instance_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:notebook-instance/{test_notebook_instance}" @@ -11,8 +15,14 @@ class Test_sagemaker_notebook_instance_without_direct_internet_access_configured def test_no_instances(self): sagemaker_client = mock.MagicMock sagemaker_client.sagemaker_notebook_instances = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_without_direct_internet_access_configured.sagemaker_notebook_instance_without_direct_internet_access_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_without_direct_internet_access_configured.sagemaker_notebook_instance_without_direct_internet_access_configured import ( @@ -36,8 +46,14 @@ class Test_sagemaker_notebook_instance_without_direct_internet_access_configured direct_internet_access=False, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_without_direct_internet_access_configured.sagemaker_notebook_instance_without_direct_internet_access_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_without_direct_internet_access_configured.sagemaker_notebook_instance_without_direct_internet_access_configured import ( @@ -68,8 +84,14 @@ class Test_sagemaker_notebook_instance_without_direct_internet_access_configured direct_internet_access=True, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_without_direct_internet_access_configured.sagemaker_notebook_instance_without_direct_internet_access_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_notebook_instance_without_direct_internet_access_configured.sagemaker_notebook_instance_without_direct_internet_access_configured import ( diff --git a/tests/providers/aws/services/sagemaker/sagemaker_service_test.py b/tests/providers/aws/services/sagemaker/sagemaker_service_test.py index d315128ae2..40fb047e5a 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_service_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_service_test.py @@ -20,6 +20,9 @@ test_training_job = "test-training-job" test_arn_training_job = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:training-job/{test_model}" subnet_id = "subnet-" + str(uuid4()) kms_key_id = str(uuid4()) +endpoint_config_name = "endpoint-config-test" +endpoint_config_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:endpoint-config/{endpoint_config_name}" +prod_variant_name = "Variant1" make_api_call = botocore.client.BaseClient._make_api_call @@ -87,6 +90,29 @@ def mock_make_api_call(self, operation_name, kwarg): {"Key": "test", "Value": "test"}, ], } + if operation_name == "ListEndpointConfigs": + return { + "EndpointConfigs": [ + { + "EndpointConfigName": endpoint_config_name, + "EndpointConfigArn": endpoint_config_arn, + }, + ], + } + if operation_name == "DescribeEndpointConfig": + return { + "ProductionVariants": [ + { + "VariantName": prod_variant_name, + "InitialInstanceCount": 5, + }, + { + "VariantName": "Variant2", + "InitialInstanceCount": 2, + }, + ] + } + return make_api_call(self, operation_name, kwarg) @@ -186,3 +212,36 @@ class Test_SageMaker_Service: assert sagemaker.sagemaker_training_jobs[0].network_isolation assert sagemaker.sagemaker_training_jobs[0].volume_kms_key_id == kms_key_id assert sagemaker.sagemaker_training_jobs[0].vpc_config_subnets == [subnet_id] + + # Test SageMaker list endpoint configs + def test_list_endpoint_configs(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + sagemaker = SageMaker(aws_provider) + assert len(sagemaker.endpoint_configs) == 1 + assert ( + sagemaker.endpoint_configs[endpoint_config_arn].name == endpoint_config_name + ) + assert ( + sagemaker.endpoint_configs[endpoint_config_arn].arn == endpoint_config_arn + ) + assert ( + sagemaker.endpoint_configs[endpoint_config_arn].region + == AWS_REGION_EU_WEST_1 + ) + assert sagemaker.sagemaker_notebook_instances[0].tags == [ + {"Key": "test", "Value": "test"}, + ] + + # Test SageMaker describe training jobs + def test_describe_endpoint_configs(self): + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + sagemaker = SageMaker(aws_provider) + assert len(sagemaker.endpoint_configs) == 1 + assert sagemaker.endpoint_configs[endpoint_config_arn].production_variants + for prod_variant in sagemaker.endpoint_configs[ + endpoint_config_arn + ].production_variants: + if prod_variant.name == prod_variant_name: + assert prod_variant.initial_instance_count == 5 + else: + assert prod_variant.initial_instance_count == 2 diff --git a/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled_test.py b/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled_test.py index 0e239540bf..80814733f9 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_intercontainer_encryption_enabled/sagemaker_training_jobs_intercontainer_encryption_enabled_test.py @@ -1,7 +1,11 @@ from unittest import mock from prowler.providers.aws.services.sagemaker.sagemaker_service import TrainingJob -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) test_training_job = "test-training-job" training_job_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:training-job/{test_training_job}" @@ -11,8 +15,14 @@ class Test_sagemaker_training_jobs_intercontainer_encryption_enabled: def test_no_training_jobs(self): sagemaker_client = mock.MagicMock sagemaker_client.sagemaker_training_jobs = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_intercontainer_encryption_enabled.sagemaker_training_jobs_intercontainer_encryption_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_intercontainer_encryption_enabled.sagemaker_training_jobs_intercontainer_encryption_enabled import ( @@ -34,8 +44,14 @@ class Test_sagemaker_training_jobs_intercontainer_encryption_enabled: container_traffic_encryption=True, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_intercontainer_encryption_enabled.sagemaker_training_jobs_intercontainer_encryption_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_intercontainer_encryption_enabled.sagemaker_training_jobs_intercontainer_encryption_enabled import ( @@ -63,8 +79,14 @@ class Test_sagemaker_training_jobs_intercontainer_encryption_enabled: region=AWS_REGION_EU_WEST_1, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_intercontainer_encryption_enabled.sagemaker_training_jobs_intercontainer_encryption_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_intercontainer_encryption_enabled.sagemaker_training_jobs_intercontainer_encryption_enabled import ( diff --git a/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_network_isolation_enabled/sagemaker_training_jobs_network_isolation_enabled_test.py b/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_network_isolation_enabled/sagemaker_training_jobs_network_isolation_enabled_test.py index f23aab5359..c0b9409347 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_network_isolation_enabled/sagemaker_training_jobs_network_isolation_enabled_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_network_isolation_enabled/sagemaker_training_jobs_network_isolation_enabled_test.py @@ -2,7 +2,11 @@ from unittest import mock from uuid import uuid4 from prowler.providers.aws.services.sagemaker.sagemaker_service import TrainingJob -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) test_training_job = "test-training-job" training_job_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:training-job/{test_training_job}" @@ -13,8 +17,14 @@ class Test_sagemaker_training_jobs_network_isolation_enabled: def test_no_training_jobs(self): sagemaker_client = mock.MagicMock sagemaker_client.sagemaker_training_jobs = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_network_isolation_enabled.sagemaker_training_jobs_network_isolation_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_network_isolation_enabled.sagemaker_training_jobs_network_isolation_enabled import ( @@ -36,8 +46,14 @@ class Test_sagemaker_training_jobs_network_isolation_enabled: network_isolation=True, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_network_isolation_enabled.sagemaker_training_jobs_network_isolation_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_network_isolation_enabled.sagemaker_training_jobs_network_isolation_enabled import ( @@ -65,8 +81,14 @@ class Test_sagemaker_training_jobs_network_isolation_enabled: region=AWS_REGION_EU_WEST_1, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_network_isolation_enabled.sagemaker_training_jobs_network_isolation_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_network_isolation_enabled.sagemaker_training_jobs_network_isolation_enabled import ( diff --git a/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_volume_and_output_encryption_enabled/sagemaker_training_jobs_volume_and_output_encryption_enabled_test.py b/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_volume_and_output_encryption_enabled/sagemaker_training_jobs_volume_and_output_encryption_enabled_test.py index 33fca077e0..365efa0960 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_volume_and_output_encryption_enabled/sagemaker_training_jobs_volume_and_output_encryption_enabled_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_volume_and_output_encryption_enabled/sagemaker_training_jobs_volume_and_output_encryption_enabled_test.py @@ -2,7 +2,11 @@ from unittest import mock from uuid import uuid4 from prowler.providers.aws.services.sagemaker.sagemaker_service import TrainingJob -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) test_training_job = "test-training-job" training_job_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:training-job/{test_training_job}" @@ -13,8 +17,14 @@ class Test_sagemaker_training_jobs_volume_and_output_encryption_enabled: def test_no_training_jobs(self): sagemaker_client = mock.MagicMock sagemaker_client.sagemaker_training_jobs = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_volume_and_output_encryption_enabled.sagemaker_training_jobs_volume_and_output_encryption_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_volume_and_output_encryption_enabled.sagemaker_training_jobs_volume_and_output_encryption_enabled import ( @@ -36,8 +46,14 @@ class Test_sagemaker_training_jobs_volume_and_output_encryption_enabled: volume_kms_key_id=kms_key_id, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_volume_and_output_encryption_enabled.sagemaker_training_jobs_volume_and_output_encryption_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_volume_and_output_encryption_enabled.sagemaker_training_jobs_volume_and_output_encryption_enabled import ( @@ -65,8 +81,14 @@ class Test_sagemaker_training_jobs_volume_and_output_encryption_enabled: region=AWS_REGION_EU_WEST_1, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_volume_and_output_encryption_enabled.sagemaker_training_jobs_volume_and_output_encryption_enabled.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_volume_and_output_encryption_enabled.sagemaker_training_jobs_volume_and_output_encryption_enabled import ( diff --git a/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_vpc_settings_configured/sagemaker_training_jobs_vpc_settings_configured_test.py b/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_vpc_settings_configured/sagemaker_training_jobs_vpc_settings_configured_test.py index 0e4c5a674b..c4a75a3f5b 100644 --- a/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_vpc_settings_configured/sagemaker_training_jobs_vpc_settings_configured_test.py +++ b/tests/providers/aws/services/sagemaker/sagemaker_training_jobs_vpc_settings_configured/sagemaker_training_jobs_vpc_settings_configured_test.py @@ -2,7 +2,11 @@ from unittest import mock from uuid import uuid4 from prowler.providers.aws.services.sagemaker.sagemaker_service import TrainingJob -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_EU_WEST_1, + set_mocked_aws_provider, +) test_training_job = "test-training-job" training_job_arn = f"arn:aws:sagemaker:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:training-job/{test_training_job}" @@ -13,8 +17,14 @@ class Test_sagemaker_training_jobs_vpc_settings_configured: def test_no_training_jobs(self): sagemaker_client = mock.MagicMock sagemaker_client.sagemaker_training_jobs = [] + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_vpc_settings_configured.sagemaker_training_jobs_vpc_settings_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_vpc_settings_configured.sagemaker_training_jobs_vpc_settings_configured import ( @@ -36,8 +46,14 @@ class Test_sagemaker_training_jobs_vpc_settings_configured: vpc_config_subnets=[subnet_id], ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_vpc_settings_configured.sagemaker_training_jobs_vpc_settings_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_vpc_settings_configured.sagemaker_training_jobs_vpc_settings_configured import ( @@ -65,8 +81,14 @@ class Test_sagemaker_training_jobs_vpc_settings_configured: region=AWS_REGION_EU_WEST_1, ) ) + + aws_provider = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + with mock.patch( - "prowler.providers.aws.services.sagemaker.sagemaker_service.SageMaker", + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_vpc_settings_configured.sagemaker_training_jobs_vpc_settings_configured.sagemaker_client", sagemaker_client, ): from prowler.providers.aws.services.sagemaker.sagemaker_training_jobs_vpc_settings_configured.sagemaker_training_jobs_vpc_settings_configured import ( diff --git a/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py b/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py index 1438b048e0..8d601ae7cc 100644 --- a/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py +++ b/tests/providers/aws/services/securityhub/securityhub_enabled/securityhub_enabled_test.py @@ -18,6 +18,7 @@ class Test_securityhub_enabled: standards="", integrations="", region=AWS_REGION_EU_WEST_1, + tags=[{"test_key": "test_value"}], ) ] with mock.patch( @@ -37,6 +38,7 @@ class Test_securityhub_enabled: assert result[0].resource_id == "Security Hub" assert result[0].resource_arn == AWS_ACCOUNT_ARN assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [{"test_key": "test_value"}] def test_securityhub_hub_active_with_standards(self): securityhub_client = mock.MagicMock @@ -47,7 +49,8 @@ class Test_securityhub_enabled: status="ACTIVE", standards="cis-aws-foundations-benchmark/v/1.2.0", integrations="", - region="eu-west-1", + region=AWS_REGION_EU_WEST_1, + tags=[{"test_key": "test_value"}], ) ] with mock.patch( @@ -73,6 +76,7 @@ class Test_securityhub_enabled: == "arn:aws:securityhub:us-east-1:0123456789012:hub/default" ) assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [{"test_key": "test_value"}] def test_securityhub_hub_active_with_integrations(self): securityhub_client = mock.MagicMock @@ -83,7 +87,8 @@ class Test_securityhub_enabled: status="ACTIVE", standards="", integrations="prowler", - region="eu-west-1", + region=AWS_REGION_EU_WEST_1, + tags=[{"test_key": "test_value"}], ) ] with mock.patch( @@ -109,6 +114,7 @@ class Test_securityhub_enabled: == "arn:aws:securityhub:us-east-1:0123456789012:hub/default" ) assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [{"test_key": "test_value"}] def test_securityhub_hub_active_without_integrations_or_standards(self): securityhub_client = mock.MagicMock @@ -120,7 +126,8 @@ class Test_securityhub_enabled: status="ACTIVE", standards="", integrations="", - region="eu-west-1", + region=AWS_REGION_EU_WEST_1, + tags=[{"test_key": "test_value"}], ) ] with mock.patch( @@ -146,6 +153,7 @@ class Test_securityhub_enabled: == "arn:aws:securityhub:us-east-1:0123456789012:hub/default" ) assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [{"test_key": "test_value"}] def test_securityhub_hub_active_without_integrations_or_standards_muted(self): securityhub_client = mock.MagicMock @@ -159,6 +167,7 @@ class Test_securityhub_enabled: standards="", integrations="", region="eu-south-2", + tags=[], ) ] with mock.patch( @@ -185,3 +194,4 @@ class Test_securityhub_enabled: == "arn:aws:securityhub:us-east-1:0123456789012:hub/default" ) assert result[0].region == "eu-south-2" + assert result[0].resource_tags == [] diff --git a/tests/providers/aws/services/securityhub/securityhub_service_test.py b/tests/providers/aws/services/securityhub/securityhub_service_test.py index 1799262440..bdfabe65ef 100644 --- a/tests/providers/aws/services/securityhub/securityhub_service_test.py +++ b/tests/providers/aws/services/securityhub/securityhub_service_test.py @@ -37,6 +37,11 @@ def mock_make_api_call(self, operation_name, kwarg): return { "HubArn": "arn:aws:securityhub:us-east-1:0123456789012:hub/default", } + if operation_name == "ListTagsForResource": + return { + "Tags": {"test_key": "test_value"}, + } + return make_api_call(self, operation_name, kwarg) @@ -80,3 +85,9 @@ class Test_SecurityHub_Service: assert securityhub.securityhubs[0].id == "default" assert securityhub.securityhubs[0].standards == "cis-aws-foundations-benchmark " assert securityhub.securityhubs[0].integrations == "prowler " + + def test_list_tags(self): + # Set partition for the service + securityhub = SecurityHub(set_mocked_aws_provider([AWS_REGION_EU_WEST_1])) + assert len(securityhub.securityhubs) == 1 + assert securityhub.securityhubs[0].tags == [{"test_key": "test_value"}] diff --git a/tests/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators_test.py b/tests/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators_test.py index 7b435287c8..eb1a04bbf1 100644 --- a/tests/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators_test.py +++ b/tests/providers/aws/services/shield/shield_advanced_protection_in_global_accelerators/shield_advanced_protection_in_global_accelerators_test.py @@ -43,6 +43,7 @@ class Test_shield_advanced_protection_in_global_accelerators: name=accelerator_name, region=AWS_REGION_EU_WEST_1, enabled=True, + tags=[{"Key": "Name", "Value": "TestAccelerator"}], ) } @@ -85,6 +86,9 @@ class Test_shield_advanced_protection_in_global_accelerators: result[0].status_extended == f"Global Accelerator {accelerator_id} is protected by AWS Shield Advanced." ) + assert result[0].resource_tags == [ + {"Key": "Name", "Value": "TestAccelerator"} + ] def test_shield_enabled_globalaccelerator_not_protected(self): # GlobalAccelerator Client @@ -98,6 +102,7 @@ class Test_shield_advanced_protection_in_global_accelerators: name=accelerator_name, region=AWS_REGION_EU_WEST_1, enabled=True, + tags=[{"Key": "Name", "Value": "TestAccelerator"}], ) } @@ -131,6 +136,9 @@ class Test_shield_advanced_protection_in_global_accelerators: result[0].status_extended == f"Global Accelerator {accelerator_id} is not protected by AWS Shield Advanced." ) + assert result[0].resource_tags == [ + {"Key": "Name", "Value": "TestAccelerator"} + ] def test_shield_disabled_globalaccelerator_not_protected(self): # GlobalAccelerator Client diff --git a/tests/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible_test.py b/tests/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible_test.py index 30fc8dee7d..81f2cc8bad 100644 --- a/tests/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible_test.py +++ b/tests/providers/aws/services/sns/sns_topics_not_publicly_accessible/sns_topics_not_publicly_accessible_test.py @@ -139,7 +139,7 @@ class Test_sns_topics_not_publicly_accessible: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SNS topic {topic_name} is not publicly accesible." + == f"SNS topic {topic_name} is not publicly accessible." ) assert result[0].resource_id == topic_name assert result[0].resource_arn == topic_arn @@ -167,7 +167,7 @@ class Test_sns_topics_not_publicly_accessible: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"SNS topic {topic_name} is not publicly accesible." + == f"SNS topic {topic_name} is not publicly accessible." ) assert result[0].resource_id == topic_name assert result[0].resource_arn == topic_arn diff --git a/tests/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public_test.py b/tests/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public_test.py index 9b1988f97f..9c218bb241 100644 --- a/tests/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public_test.py +++ b/tests/providers/aws/services/ssm/ssm_documents_set_as_public/ssm_documents_set_as_public_test.py @@ -22,7 +22,7 @@ class Test_ssm_documents_set_as_public: assert len(result) == 0 - def test_document_public(self): + def test_document_public_account_owners(self): ssm_client = mock.MagicMock document_name = "test-document" document_arn = f"arn:aws:ssm:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:document/{document_name}" @@ -48,6 +48,42 @@ class Test_ssm_documents_set_as_public: check = ssm_documents_set_as_public() result = check.execute() + assert len(result) == 1 + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == document_name + assert result[0].resource_arn == document_arn + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"SSM Document {document_name} is shared to non-trusted AWS accounts: 111111111111, 111111222222." + ) + + def test_document_public_all_account_owners(self): + ssm_client = mock.MagicMock + document_name = "test-document" + document_arn = f"arn:aws:ssm:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:document/{document_name}" + ssm_client.audited_account = AWS_ACCOUNT_NUMBER + ssm_client.documents = { + document_name: Document( + arn=document_arn, + name=document_name, + region=AWS_REGION_US_EAST_1, + content="", + account_owners=["all"], + ) + } + with mock.patch( + "prowler.providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from prowler.providers.aws.services.ssm.ssm_documents_set_as_public.ssm_documents_set_as_public import ( + ssm_documents_set_as_public, + ) + + check = ssm_documents_set_as_public() + result = check.execute() + assert len(result) == 1 assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_id == document_name @@ -57,6 +93,81 @@ class Test_ssm_documents_set_as_public: result[0].status_extended == f"SSM Document {document_name} is public." ) + def test_document_public_to_other_trusted_AWS_accounts(self): + ssm_client = mock.MagicMock + document_name = "test-document" + document_arn = f"arn:aws:ssm:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:document/{document_name}" + ssm_client.audited_account = AWS_ACCOUNT_NUMBER + ssm_client.documents = { + document_name: Document( + arn=document_arn, + name=document_name, + region=AWS_REGION_US_EAST_1, + content="", + account_owners=["111111111333", "111111222444"], + ) + } + ssm_client.audit_config = { + "trusted_account_ids": ["111111111333", "111111222444"] + } + with mock.patch( + "prowler.providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from prowler.providers.aws.services.ssm.ssm_documents_set_as_public.ssm_documents_set_as_public import ( + ssm_documents_set_as_public, + ) + + check = ssm_documents_set_as_public() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == document_name + assert result[0].resource_arn == document_arn + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SSM Document {document_name} is shared to trusted AWS accounts: 111111111333, 111111222444." + ) + + def test_document_public_to_self_account(self): + ssm_client = mock.MagicMock + document_name = "test-document" + document_arn = f"arn:aws:ssm:{AWS_REGION_US_EAST_1}:{AWS_ACCOUNT_NUMBER}:document/{document_name}" + ssm_client.audited_account = AWS_ACCOUNT_NUMBER + ssm_client.documents = { + document_name: Document( + arn=document_arn, + name=document_name, + region=AWS_REGION_US_EAST_1, + content="", + account_owners=[AWS_ACCOUNT_NUMBER], + ) + } + with mock.patch( + "prowler.providers.aws.services.ssm.ssm_service.SSM", + new=ssm_client, + ): + # Test Check + from prowler.providers.aws.services.ssm.ssm_documents_set_as_public.ssm_documents_set_as_public import ( + ssm_documents_set_as_public, + ) + + check = ssm_documents_set_as_public() + result = check.execute() + + assert len(result) == 1 + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == document_name + assert result[0].resource_arn == document_arn + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"SSM Document {document_name} is not public." + ) + def test_document_not_public(self): ssm_client = mock.MagicMock document_name = "test-document" diff --git a/tests/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching_test.py b/tests/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching_test.py index 56620a86f4..9c9ec9ff52 100644 --- a/tests/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching_test.py +++ b/tests/providers/aws/services/ssm/ssm_managed_compliant_patching/ssm_managed_compliant_patching_test.py @@ -4,7 +4,11 @@ from prowler.providers.aws.services.ssm.ssm_service import ( ComplianceResource, ResourceStatus, ) -from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_US_EAST_1 +from tests.providers.aws.utils import ( + AWS_ACCOUNT_NUMBER, + AWS_REGION_US_EAST_1, + set_mocked_aws_provider, +) class Test_ssm_managed_compliant_patching: @@ -37,10 +41,26 @@ class Test_ssm_managed_compliant_patching: status=ResourceStatus.COMPLIANT, ) } + ec2_client = mock.MagicMock + ec2_client.instances = [ + mock.MagicMock( + id=instance_id, + tags=[ + {"Key": "Name", "Value": "test_instance"}, + {"Key": "Environment", "Value": "development"}, + ], + ) + ] with mock.patch( "prowler.providers.aws.services.ssm.ssm_service.SSM", new=ssm_client, + ), mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.ssm.ssm_managed_compliant_patching.ssm_managed_compliant_patching.ec2_client", + new=ec2_client, ): # Test Check from prowler.providers.aws.services.ssm.ssm_managed_compliant_patching.ssm_managed_compliant_patching import ( @@ -62,6 +82,10 @@ class Test_ssm_managed_compliant_patching: result[0].status_extended == f"EC2 managed instance {instance_id} is compliant." ) + assert result[0].resource_tags == [ + {"Key": "Name", "Value": "test_instance"}, + {"Key": "Environment", "Value": "development"}, + ] def test_compliance_resources_non_compliant(self): ssm_client = mock.MagicMock @@ -75,10 +99,26 @@ class Test_ssm_managed_compliant_patching: status=ResourceStatus.NON_COMPLIANT, ) } + ec2_client = mock.MagicMock + ec2_client.instances = [ + mock.MagicMock( + id=instance_id, + tags=[ + {"Key": "Name", "Value": "test_instance"}, + {"Key": "Environment", "Value": "development"}, + ], + ) + ] with mock.patch( "prowler.providers.aws.services.ssm.ssm_service.SSM", new=ssm_client, + ), mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_aws_provider([AWS_REGION_US_EAST_1]), + ), mock.patch( + "prowler.providers.aws.services.ssm.ssm_managed_compliant_patching.ssm_managed_compliant_patching.ec2_client", + new=ec2_client, ): # Test Check from prowler.providers.aws.services.ssm.ssm_managed_compliant_patching.ssm_managed_compliant_patching import ( @@ -100,3 +140,7 @@ class Test_ssm_managed_compliant_patching: result[0].status_extended == f"EC2 managed instance {instance_id} is non-compliant." ) + assert result[0].resource_tags == [ + {"Key": "Name", "Value": "test_instance"}, + {"Key": "Environment", "Value": "development"}, + ] diff --git a/tests/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled_test.py b/tests/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled_test.py new file mode 100644 index 0000000000..0af3b24ebb --- /dev/null +++ b/tests/providers/aws/services/vpc/vpc_endpoint_for_ec2_enabled/vpc_endpoint_for_ec2_enabled_test.py @@ -0,0 +1,156 @@ +from unittest import mock + +from boto3 import client +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + + +class Test_vpc_endpoint_for_ec2_enabled: + @mock_aws + def test_vpc_no_endpoints(self): + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + vpc_id = ec2_client.describe_vpcs()["Vpcs"][0]["VpcId"] + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.vpc.vpc_endpoint_for_ec2_enabled.vpc_endpoint_for_ec2_enabled.vpc_client", + new=VPC(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.vpc.vpc_endpoint_for_ec2_enabled.vpc_endpoint_for_ec2_enabled import ( + vpc_endpoint_for_ec2_enabled, + ) + + check = vpc_endpoint_for_ec2_enabled() + result = check.execute() + + assert len(result) == 1 + assert result[0].resource_id == vpc_id + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].status == "FAIL" + assert result[0].status_extended == f"VPC {vpc_id} has no EC2 endpoint." + + @mock_aws + def test_vpc_no_ec2_endpoint(self): + # Create VPC Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + + vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"] + vpc_default_id = ec2_client.describe_vpcs()["Vpcs"][0]["VpcId"] + + route_table = ec2_client.create_route_table(VpcId=vpc["VpcId"])["RouteTable"] + ec2_client.create_vpc_endpoint( + VpcId=vpc["VpcId"], + ServiceName="com.amazonaws.vpce.us-east-1.s3", + RouteTableIds=[route_table["RouteTableId"]], + VpcEndpointType="Interface", + ) + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.vpc.vpc_endpoint_for_ec2_enabled.vpc_endpoint_for_ec2_enabled.vpc_client", + new=VPC(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.vpc.vpc_endpoint_for_ec2_enabled.vpc_endpoint_for_ec2_enabled import ( + vpc_endpoint_for_ec2_enabled, + ) + + check = vpc_endpoint_for_ec2_enabled() + result = check.execute() + + # One VPC is created by default, so we should have 2 results + assert len(result) == 2 + for result_vpc in result: + if result_vpc.resource_id == vpc["VpcId"]: + assert result_vpc.region == AWS_REGION_US_EAST_1 + assert result_vpc.status == "FAIL" + assert ( + result_vpc.status_extended + == f"VPC {vpc['VpcId']} has no EC2 endpoint." + ) + assert ( + result_vpc.resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:vpc/{vpc['VpcId']}" + ) + else: + assert result_vpc.region == AWS_REGION_US_EAST_1 + assert result_vpc.status == "FAIL" + assert ( + result_vpc.status_extended + == f"VPC {vpc_default_id} has no EC2 endpoint." + ) + assert result_vpc.resource_id == vpc_default_id + + @mock_aws + def test_vpc_ec2_endpoint_enabled(self): + # Create VPC Mocked Resources + ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) + + vpc = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"] + vpc_default_id = ec2_client.describe_vpcs()["Vpcs"][0]["VpcId"] + + route_table = ec2_client.create_route_table(VpcId=vpc["VpcId"])["RouteTable"] + ec2_client.create_vpc_endpoint( + VpcId=vpc["VpcId"], + ServiceName="com.amazonaws.vpce.us-east-1.ec2", + RouteTableIds=[route_table["RouteTableId"]], + VpcEndpointType="Gateway", + ) + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ): + with mock.patch( + "prowler.providers.aws.services.vpc.vpc_endpoint_for_ec2_enabled.vpc_endpoint_for_ec2_enabled.vpc_client", + new=VPC(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.vpc.vpc_endpoint_for_ec2_enabled.vpc_endpoint_for_ec2_enabled import ( + vpc_endpoint_for_ec2_enabled, + ) + + check = vpc_endpoint_for_ec2_enabled() + result = check.execute() + + # One VPC is created by default, so we should have 2 results + assert len(result) == 2 + for result_vpc in result: + if result_vpc.resource_id == vpc["VpcId"]: + assert result_vpc.region == AWS_REGION_US_EAST_1 + assert result_vpc.status == "PASS" + assert ( + result_vpc.status_extended + == f"VPC {vpc['VpcId']} has an EC2 Gateway endpoint." + ) + assert ( + result_vpc.resource_arn + == f"arn:aws:ec2:{AWS_REGION_US_EAST_1}:123456789012:vpc/{vpc['VpcId']}" + ) + else: + assert result_vpc.region == AWS_REGION_US_EAST_1 + assert result_vpc.status == "FAIL" + assert ( + result_vpc.status_extended + == f"VPC {vpc_default_id} has no EC2 endpoint." + ) + assert result_vpc.resource_id == vpc_default_id diff --git a/tests/providers/aws/services/vpc/vpc_service_test.py b/tests/providers/aws/services/vpc/vpc_service_test.py index 8498496ca3..d8fe736ed0 100644 --- a/tests/providers/aws/services/vpc/vpc_service_test.py +++ b/tests/providers/aws/services/vpc/vpc_service_test.py @@ -1,9 +1,11 @@ import json +import botocore +import mock from boto3 import client, resource from moto import mock_aws -from prowler.providers.aws.services.vpc.vpc_service import Route +from prowler.providers.aws.services.vpc.vpc_service import VPC, Route from tests.providers.aws.utils import ( AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1, @@ -11,6 +13,37 @@ from tests.providers.aws.utils import ( set_mocked_aws_provider, ) +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "DescribeVpnConnections": + return { + "VpnConnections": [ + { + "VpnConnectionId": "vpn-1234567890abcdef0", + "CustomerGatewayId": "cgw-0123456789abcdef0", + "VpnGatewayId": "vgw-0123456789abcdef0", + "State": "available", + "Type": "ipsec.1", + "VgwTelemetry": [ + { + "OutsideIpAddress": "192.168.1.1", + "Status": "UP", + "AcceptedRouteCount": 10, + }, + { + "OutsideIpAddress": "192.168.1.2", + "Status": "UP", + "AcceptedRouteCount": 5, + }, + ], + "Tags": [{"Key": "Name", "Value": "MyVPNConnection"}], + } + ] + } + return make_api_call(self, operation_name, kwarg) + class Test_VPC_Service: @@ -256,6 +289,7 @@ class Test_VPC_Service: assert vpc.vpc_endpoints[0].tags == [ {"Key": "test", "Value": "test"}, ] + assert vpc.vpc_endpoints[0].type == "Gateway" # Test VPC Describe VPC Endpoint Services @mock_aws @@ -318,13 +352,46 @@ class Test_VPC_Service: # Generate VPC Client ec2_client = client("ec2", region_name=AWS_REGION_US_EAST_1) # Create VPC - vpc = ec2_client.create_vpc( - CidrBlock="172.28.7.0/24", InstanceTenancy="default" + vpc_id = ec2_client.create_vpc(CidrBlock="10.0.0.0/16")["Vpc"]["VpcId"] + default_sg = ec2_client.describe_security_groups(GroupNames=["default"])[ + "SecurityGroups" + ][0] + default_sg_id = default_sg["GroupId"] + ec2_client.authorize_security_group_ingress( + GroupId=default_sg_id, + IpPermissions=[ + { + "IpProtocol": "tcp", + "FromPort": 389, + "ToPort": 389, + "IpRanges": [{"CidrIp": "0.0.0.0/0"}], + } + ], ) - subnet = ec2_client.create_subnet( - VpcId=vpc["Vpc"]["VpcId"], - CidrBlock="172.28.7.192/26", + subnet_id = ec2_client.create_subnet( + VpcId=vpc_id, + CidrBlock="10.0.0.0/16", AvailabilityZone=f"{AWS_REGION_US_EAST_1}a", + )["Subnet"]["SubnetId"] + # add default route of subnet to an internet gateway to make it public + igw_id = ec2_client.create_internet_gateway()["InternetGateway"][ + "InternetGatewayId" + ] + # attach internet gateway to subnet + ec2_client.attach_internet_gateway(InternetGatewayId=igw_id, VpcId=vpc_id) + # create route table + route_table_id = ec2_client.create_route_table(VpcId=vpc_id)["RouteTable"][ + "RouteTableId" + ] + # associate route table with subnet + ec2_client.associate_route_table( + RouteTableId=route_table_id, SubnetId=subnet_id + ) + # add route to route table + ec2_client.create_route( + RouteTableId=route_table_id, + DestinationCidrBlock="0.0.0.0/0", + GatewayId=igw_id, ) # VPC client for this test class aws_provider = set_mocked_aws_provider( @@ -337,13 +404,29 @@ class Test_VPC_Service: len(vpc.vpcs) == 3 ) # Number of AWS regions + created VPC, one default VPC per region for vpc in vpc.vpcs.values(): - if vpc.cidr_block == "172.28.7.0/24": - assert vpc.subnets[0].id == subnet["Subnet"]["SubnetId"] + if vpc.cidr_block == "10.0.0.0/16": + assert vpc.subnets[0].id == subnet_id assert vpc.subnets[0].default is False - assert vpc.subnets[0].vpc_id == vpc.id - assert vpc.subnets[0].cidr_block == "172.28.7.192/26" + assert vpc.subnets[0].vpc_id == vpc_id + assert vpc.subnets[0].cidr_block == "10.0.0.0/16" assert vpc.subnets[0].availability_zone == f"{AWS_REGION_US_EAST_1}a" - assert vpc.subnets[0].public is False + assert vpc.subnets[0].public assert vpc.subnets[0].nat_gateway is False assert vpc.subnets[0].region == AWS_REGION_US_EAST_1 assert vpc.subnets[0].tags is None + + # Test VPC Describe VPN Connections + @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + def test_describe_vpn_connections(self): + # Generate VPC Client + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + vpc = VPC(aws_provider) + + vpn_arn = f"arn:{aws_provider.identity.partition}:ec2:{AWS_REGION_US_EAST_1}:{aws_provider.identity.account}:vpn-connection/vpn-1234567890abcdef0" + assert len(vpc.vpn_connections) == 1 + assert vpn_arn in vpc.vpn_connections + vpn_conn = vpc.vpn_connections[vpn_arn] + assert vpn_conn.id == "vpn-1234567890abcdef0" + assert vpn_conn.region == AWS_REGION_US_EAST_1 + assert len(vpn_conn.tunnels) == 2 diff --git a/tests/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up_test.py b/tests/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up_test.py new file mode 100644 index 0000000000..2248242ed7 --- /dev/null +++ b/tests/providers/aws/services/vpc/vpc_vpn_connection_tunnels_up/vpc_vpn_connection_tunnels_up_test.py @@ -0,0 +1,160 @@ +from unittest import mock + +import botocore +from moto import mock_aws + +from tests.providers.aws.utils import AWS_REGION_US_EAST_1, set_mocked_aws_provider + +make_api_call = botocore.client.BaseClient._make_api_call + + +def mock_make_api_call(self, operation_name, kwarg): + if operation_name == "DescribeVpnConnections": + return { + "VpnConnections": [ + { + "VpnConnectionId": "vpn-1234567890abcdef0", + "CustomerGatewayId": "cgw-0123456789abcdef0", + "VpnGatewayId": "vgw-0123456789abcdef0", + "State": "available", + "Type": "ipsec.1", + "VgwTelemetry": [ + { + "OutsideIpAddress": "192.168.1.1", + "Status": "UP", + "AcceptedRouteCount": 10, + }, + { + "OutsideIpAddress": "192.168.1.2", + "Status": "UP", + "AcceptedRouteCount": 5, + }, + ], + "Tags": [{"Key": "Name", "Value": "MyVPNConnection"}], + } + ] + } + return make_api_call(self, operation_name, kwarg) + + +def mock_make_api_call_v2(self, operation_name, kwarg): + if operation_name == "DescribeVpnConnections": + return { + "VpnConnections": [ + { + "VpnConnectionId": "vpn-1234567890abcdef0", + "CustomerGatewayId": "cgw-0123456789abcdef0", + "VpnGatewayId": "vgw-0123456789abcdef0", + "State": "available", + "Type": "ipsec.1", + "VgwTelemetry": [ + { + "OutsideIpAddress": "192.168.1.1", + "Status": "UP", + "AcceptedRouteCount": 10, + }, + { + "OutsideIpAddress": "192.168.1.2", + "Status": "DOWN", + "AcceptedRouteCount": 5, + }, + ], + "Tags": [{"Key": "Name", "Value": "MyVPNConnection"}], + } + ] + } + return make_api_call(self, operation_name, kwarg) + + +class Test_vpc_vpn_connection_tunnels_up: + @mock_aws + def test_no_vpn_connections(self): + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_vpn_connection_tunnels_up.vpc_vpn_connection_tunnels_up.vpc_client", + new=VPC(aws_provider), + ): + # Test Check + from prowler.providers.aws.services.vpc.vpc_vpn_connection_tunnels_up.vpc_vpn_connection_tunnels_up import ( + vpc_vpn_connection_tunnels_up, + ) + + check = vpc_vpn_connection_tunnels_up() + result = check.execute() + + assert len(result) == 0 + + @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call) + def test_vpn_both_tunnels_up(self): + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_vpn_connection_tunnels_up.vpc_vpn_connection_tunnels_up.vpc_client", + new=VPC(aws_provider), + ): + from prowler.providers.aws.services.vpc.vpc_vpn_connection_tunnels_up.vpc_vpn_connection_tunnels_up import ( + vpc_vpn_connection_tunnels_up, + ) + + check = vpc_vpn_connection_tunnels_up() + result = check.execute() + + # Se espera que el resultado sea PASS ya que ambos túneles están "UP" + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "vpn-1234567890abcdef0" + assert ( + result[0].resource_arn + == "arn:aws:ec2:us-east-1:123456789012:vpn-connection/vpn-1234567890abcdef0" + ) + assert ( + result[0].status_extended + == "VPN Connection vpn-1234567890abcdef0 has both tunnels UP. " + ) + + @mock.patch("botocore.client.BaseClient._make_api_call", new=mock_make_api_call_v2) + def test_vpn_one_tunnel_down(self): + + from prowler.providers.aws.services.vpc.vpc_service import VPC + + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=aws_provider, + ), mock.patch( + "prowler.providers.aws.services.vpc.vpc_vpn_connection_tunnels_up.vpc_vpn_connection_tunnels_up.vpc_client", + new=VPC(aws_provider), + ): + from prowler.providers.aws.services.vpc.vpc_vpn_connection_tunnels_up.vpc_vpn_connection_tunnels_up import ( + vpc_vpn_connection_tunnels_up, + ) + + check = vpc_vpn_connection_tunnels_up() + result = check.execute() + + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_id == "vpn-1234567890abcdef0" + assert ( + result[0].resource_arn + == "arn:aws:ec2:us-east-1:123456789012:vpn-connection/vpn-1234567890abcdef0" + ) + assert ( + result[0].status_extended + == "VPN Connection vpn-1234567890abcdef0 has at least one tunnel DOWN. " + ) diff --git a/tests/providers/aws/services/wafv2/wafv2_service_test.py b/tests/providers/aws/services/wafv2/wafv2_service_test.py index 2cfb12e955..c649ac3ec9 100644 --- a/tests/providers/aws/services/wafv2/wafv2_service_test.py +++ b/tests/providers/aws/services/wafv2/wafv2_service_test.py @@ -104,7 +104,7 @@ class Test_WAFv2_Service: # Test WAFv2 describe Web user pools @mock_aws - def test__list_resources_for_web_user_pools__(self): + def test_list_resources_for_web_user_pools(self): wafv2 = client("wafv2", region_name=AWS_REGION_EU_WEST_1) cognito = client("cognito-idp", region_name=AWS_REGION_EU_WEST_1) waf = wafv2.create_web_acl( @@ -126,3 +126,27 @@ class Test_WAFv2_Service: assert len(wafv2.web_acls) == 1 assert len(wafv2.web_acls[0].user_pools) == 1 assert user_pool["Arn"] in wafv2.web_acls[0].user_pools + + @mock_aws + def test_list_tags(self): + wafv2 = client("wafv2", region_name=AWS_REGION_EU_WEST_1) + waf = wafv2.create_web_acl( + Scope="REGIONAL", + Name="my-web-acl", + DefaultAction={"Allow": {}}, + VisibilityConfig={ + "SampledRequestsEnabled": False, + "CloudWatchMetricsEnabled": False, + "MetricName": "idk", + }, + )["Summary"] + wafv2.tag_resource( + ResourceARN=waf["ARN"], Tags=[{"Key": "Name", "Value": "my-web-acl"}] + ) + # WAFv2 client for this test class + aws = set_mocked_aws_provider([AWS_REGION_EU_WEST_1]) + wafv2 = WAFv2(aws) + assert len(wafv2.web_acls) == 1 + assert len(wafv2.web_acls[0].tags) == 1 + assert wafv2.web_acls[0].tags[0]["Key"] == "Name" + assert wafv2.web_acls[0].tags[0]["Value"] == "my-web-acl" diff --git a/tests/providers/aws/services/wafv2/wafv2_webacl_logging_enabled/wafv2_webacl_logging_enabled_test.py b/tests/providers/aws/services/wafv2/wafv2_webacl_logging_enabled/wafv2_webacl_logging_enabled_test.py index eaf0bb086c..8e0f02b7fa 100644 --- a/tests/providers/aws/services/wafv2/wafv2_webacl_logging_enabled/wafv2_webacl_logging_enabled_test.py +++ b/tests/providers/aws/services/wafv2/wafv2_webacl_logging_enabled/wafv2_webacl_logging_enabled_test.py @@ -41,6 +41,7 @@ class Test_wafv2_webacl_logging_enabled: user_pools=[], region=AWS_REGION_EU_WEST_1, logging_enabled=True, + tags=[{"Key": "Name", "Value": waf_name}], ) ) with mock.patch( @@ -65,6 +66,7 @@ class Test_wafv2_webacl_logging_enabled: assert result[0].resource_id == waf_id assert result[0].resource_arn == waf_arn assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [{"Key": "Name", "Value": waf_name}] def test_wafv2_wb_acl_without_logging(self): wafv2_client = mock.MagicMock @@ -79,6 +81,7 @@ class Test_wafv2_webacl_logging_enabled: user_pools=[], region=AWS_REGION_EU_WEST_1, logging_enabled=False, + tags=[{"Key": "Name", "Value": waf_name}], ) ) with mock.patch( @@ -103,3 +106,4 @@ class Test_wafv2_webacl_logging_enabled: assert result[0].resource_id == waf_id assert result[0].resource_arn == waf_arn assert result[0].region == AWS_REGION_EU_WEST_1 + assert result[0].resource_tags == [{"Key": "Name", "Value": waf_name}] diff --git a/tests/providers/aws/utils.py b/tests/providers/aws/utils.py index 738fce0940..7104bef9dd 100644 --- a/tests/providers/aws/utils.py +++ b/tests/providers/aws/utils.py @@ -120,13 +120,6 @@ def set_mocked_aws_provider( # AWS Provider provider = AwsProvider() - # Set output options - provider.output_options = arguments, {} - - # Output options - - provider.output_options = arguments, {} - # Mock Session provider._session.session_config = None provider._session.original_session = original_session diff --git a/tests/providers/azure/azure_provider_test.py b/tests/providers/azure/azure_provider_test.py index 3a4a6c1797..334c83bf42 100644 --- a/tests/providers/azure/azure_provider_test.py +++ b/tests/providers/azure/azure_provider_test.py @@ -1,13 +1,9 @@ -from argparse import Namespace -from datetime import datetime -from os import rmdir from unittest.mock import patch from uuid import uuid4 import pytest from azure.core.credentials import AccessToken from azure.identity import DefaultAzureCredential -from freezegun import freeze_time from mock import MagicMock from prowler.config.config import ( @@ -22,11 +18,7 @@ from prowler.providers.azure.exceptions.exceptions import ( AzureNoAuthenticationMethodError, AzureTenantIDNoBrowserAuthError, ) -from prowler.providers.azure.models import ( - AzureIdentityInfo, - AzureOutputOptions, - AzureRegionConfig, -) +from prowler.providers.azure.models import AzureIdentityInfo, AzureRegionConfig from prowler.providers.common.models import Connection @@ -203,87 +195,6 @@ class TestAzureProvider: == "[1919] Azure Tenant ID (--tenant-id) is required for browser authentication mode" ) - @freeze_time(datetime.today()) - def test_azure_provider_output_options_with_domain(self): - arguments = Namespace() - subscription_id = None - tenant_id = None - - # We need to set exactly one auth method - az_cli_auth = None - sp_env_auth = True - browser_auth = None - managed_identity_auth = None - - config_file = default_config_file_path - fixer_config = default_fixer_config_file_path - azure_region = "AzureCloud" - - # Output Options - arguments.output_formats = ["csv"] - arguments.output_directory = "output_test_directory" - output_directory = arguments.output_directory - arguments.status = [] - arguments.verbose = True - arguments.only_logs = False - arguments.unix_timestamp = False - arguments.shodan = "test-api-key" - - tenant_domain = "test-domain" - with patch( - "prowler.providers.azure.azure_provider.AzureProvider.setup_identity", - return_value=AzureIdentityInfo(tenant_domain=tenant_domain), - ), patch( - "prowler.providers.azure.azure_provider.AzureProvider.get_locations", - return_value={}, - ), patch( - "prowler.providers.azure.azure_provider.AzureProvider.setup_session", - return_value=DefaultAzureCredential(), - ): - azure_provider = AzureProvider( - az_cli_auth, - sp_env_auth, - browser_auth, - managed_identity_auth, - tenant_id, - azure_region, - subscription_id, - config_file, - fixer_config, - ) - # This is needed since the output_options requires to get the global provider to get the audit config - with patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=azure_provider, - ): - - azure_provider.output_options = arguments, {} - - assert isinstance(azure_provider.output_options, AzureOutputOptions) - assert azure_provider.output_options.status == [] - assert azure_provider.output_options.output_modes == [ - "csv", - ] - assert ( - azure_provider.output_options.output_directory == output_directory - ) - assert azure_provider.output_options.bulk_checks_metadata == {} - assert azure_provider.output_options.verbose - # Flaky due to the millisecond part of the timestamp - # assert ( - # azure_provider.output_options.output_filename - # == f"prowler-output-{azure_provider.identity.tenant_domain}-{datetime.today().strftime('%Y%m%d%H%M%S')}" - # ) - assert ( - f"prowler-output-{azure_provider.identity.tenant_domain}" - in azure_provider.output_options.output_filename - ) - - # Delete testing directory - # TODO: move this to a fixtures file - rmdir(f"{arguments.output_directory}/compliance") - rmdir(arguments.output_directory) - def test_test_connection_browser_auth(self): with patch( "prowler.providers.azure.azure_provider.DefaultAzureCredential" diff --git a/tests/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled_test.py b/tests/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled_test.py new file mode 100644 index 0000000000..1d92841844 --- /dev/null +++ b/tests/providers/azure/services/containerregistry/containerregistry_admin_user_disabled/containerregistry_admin_user_disabled_test.py @@ -0,0 +1,134 @@ +from unittest import mock +from unittest.mock import MagicMock +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class TestContainerRegistryAdminUserDisabled: + def test_no_container_registries(self): + containerregistry_client = MagicMock() + containerregistry_client.registries = {} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( + "prowler.providers.azure.services.containerregistry.containerregistry_admin_user_disabled.containerregistry_admin_user_disabled.containerregistry_client", + new=containerregistry_client, + ): + from prowler.providers.azure.services.containerregistry.containerregistry_admin_user_disabled.containerregistry_admin_user_disabled import ( + containerregistry_admin_user_disabled, + ) + + check = containerregistry_admin_user_disabled() + result = check.execute() + assert len(result) == 0 + + def test_container_registry_admin_user_enabled(self): + containerregistry_client = MagicMock() + registry_id = str(uuid4()) + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( + "prowler.providers.azure.services.containerregistry.containerregistry_admin_user_disabled.containerregistry_admin_user_disabled.containerregistry_client", + new=containerregistry_client, + ): + from prowler.providers.azure.services.containerregistry.containerregistry_admin_user_disabled.containerregistry_admin_user_disabled import ( + containerregistry_admin_user_disabled, + ) + from prowler.providers.azure.services.containerregistry.containerregistry_service import ( + ContainerRegistryInfo, + ) + + containerregistry_client.registries = { + AZURE_SUBSCRIPTION_ID: { + registry_id: ContainerRegistryInfo( + id=registry_id, + name="mock_registry", + location="westeurope", + resource_group="mock_resource_group", + sku="Basic", + login_server="mock_login_server.azurecr.io", + public_network_access="Enabled", + admin_user_enabled=True, + ) + } + } + + check = containerregistry_admin_user_disabled() + + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_ID} has its admin user enabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "mock_registry" + assert ( + result[0].resource_id + == containerregistry_client.registries[AZURE_SUBSCRIPTION_ID][ + registry_id + ].id + ) + assert result[0].location == "westeurope" + + def test_container_registry_admin_user_disabled(self): + containerregistry_client = mock.MagicMock() + containerregistry_client.registries = {} + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), mock.patch( + "prowler.providers.azure.services.containerregistry.containerregistry_admin_user_disabled.containerregistry_admin_user_disabled.containerregistry_client", + new=containerregistry_client, + ): + from prowler.providers.azure.services.containerregistry.containerregistry_admin_user_disabled.containerregistry_admin_user_disabled import ( + containerregistry_admin_user_disabled, + ) + from prowler.providers.azure.services.containerregistry.containerregistry_service import ( + ContainerRegistryInfo, + ) + + registry_id = "mock_registry_id" + + containerregistry_client.registries = { + AZURE_SUBSCRIPTION_ID: { + registry_id: ContainerRegistryInfo( + id=registry_id, + name="mock_registry", + location="westeurope", + resource_group="mock_resource_group", + sku="Basic", + login_server="mock_login_server.azurecr.io", + public_network_access="Enabled", + admin_user_enabled=False, + ) + } + } + + check = containerregistry_admin_user_disabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Container Registry mock_registry from subscription {AZURE_SUBSCRIPTION_ID} has its admin user disabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "mock_registry" + assert ( + result[0].resource_id + == containerregistry_client.registries[AZURE_SUBSCRIPTION_ID][ + registry_id + ].id + ) + assert result[0].location == "westeurope" diff --git a/tests/providers/azure/services/containerregistry/containerregistry_service_test.py b/tests/providers/azure/services/containerregistry/containerregistry_service_test.py new file mode 100644 index 0000000000..d4d2a8cb28 --- /dev/null +++ b/tests/providers/azure/services/containerregistry/containerregistry_service_test.py @@ -0,0 +1,87 @@ +from unittest.mock import MagicMock, patch +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class TestContainerRegistryService: + def test_get_container_registry(self): + with patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), patch( + "prowler.providers.azure.services.monitor.monitor_service.Monitor", + new=MagicMock(), + ): + from prowler.providers.azure.services.containerregistry.containerregistry_service import ( + ContainerRegistryInfo, + ) + + # Initialize ContainerRegistry with the mocked provider + containerregistry_service = MagicMock() + registry_id = str(uuid4()) + containerregistry_service.registries = { + AZURE_SUBSCRIPTION_ID: { + registry_id: ContainerRegistryInfo( + id=registry_id, + name="mock_registry", + location="westeurope", + resource_group="mock_resource_group", + sku="Basic", + login_server="mock_login_server.azurecr.io", + public_network_access="Enabled", + admin_user_enabled=True, + monitor_diagnostic_settings=[ + { + "id": "id1/id1", + "logs": [ + { + "category": "ContainerLogs", + "enabled": True, + }, + { + "category": "AdminLogs", + "enabled": False, + }, + ], + "storage_account_name": "mock_storage_account", + "storage_account_id": "mock_storage_account_id", + "name": "mock_diagnostic_setting", + } + ], + ) + } + } + + # Assertions to check the populated data in the registries + assert len(containerregistry_service.registries[AZURE_SUBSCRIPTION_ID]) == 1 + + registry_info = containerregistry_service.registries[AZURE_SUBSCRIPTION_ID][ + registry_id + ] + + assert registry_info.id == registry_id + assert registry_info.name == "mock_registry" + assert registry_info.location == "westeurope" + assert registry_info.resource_group == "mock_resource_group" + assert registry_info.sku == "Basic" + assert registry_info.login_server == "mock_login_server.azurecr.io" + assert registry_info.public_network_access == "Enabled" + assert registry_info.admin_user_enabled is True + assert isinstance(registry_info.monitor_diagnostic_settings, list) + + # Check the properties of monitor diagnostic settings + monitor_setting = registry_info.monitor_diagnostic_settings[0] + assert monitor_setting["id"] == "id1/id1" # Use dictionary access here + assert monitor_setting["storage_account_name"] == "mock_storage_account" + assert monitor_setting["storage_account_id"] == "mock_storage_account_id" + assert monitor_setting["name"] == "mock_diagnostic_setting" + assert len(monitor_setting["logs"]) == 2 + + assert monitor_setting["logs"][0]["category"] == "ContainerLogs" + assert monitor_setting["logs"][0]["enabled"] is True + assert monitor_setting["logs"][1]["category"] == "AdminLogs" + assert monitor_setting["logs"][1]["enabled"] is False diff --git a/tests/providers/gcp/gcp_fixtures.py b/tests/providers/gcp/gcp_fixtures.py index e1c1f6ce89..b75db5f84b 100644 --- a/tests/providers/gcp/gcp_fixtures.py +++ b/tests/providers/gcp/gcp_fixtures.py @@ -12,13 +12,14 @@ GCP_US_CENTER1_LOCATION = "us-central1" def set_mocked_gcp_provider( - project_ids: list[str] = [], profile: str = "" + project_ids: list[str] = [GCP_PROJECT_ID], profile: str = "" ) -> GcpProvider: provider = MagicMock() provider.type = "gcp" provider.session = MagicMock() provider.session._service_account_email = "test@test.com" provider.project_ids = project_ids + provider.default_project_id = GCP_PROJECT_ID provider.identity = GCPIdentityInfo( profile=profile, ) diff --git a/tests/providers/gcp/gcp_provider_test.py b/tests/providers/gcp/gcp_provider_test.py index 55ecfe2887..99d8e61c4e 100644 --- a/tests/providers/gcp/gcp_provider_test.py +++ b/tests/providers/gcp/gcp_provider_test.py @@ -1,6 +1,6 @@ from argparse import Namespace from datetime import datetime -from os import environ, rmdir +from os import environ from freezegun import freeze_time from mock import MagicMock, patch @@ -11,8 +11,7 @@ from prowler.config.config import ( load_and_validate_config_file, ) from prowler.providers.gcp.gcp_provider import GcpProvider -from prowler.providers.gcp.models import GCPIdentityInfo, GCPOutputOptions, GCPProject -from tests.providers.gcp.gcp_fixtures import mock_api_client +from prowler.providers.gcp.models import GCPIdentityInfo, GCPProject class TestGCPProvider: @@ -45,7 +44,7 @@ class TestGCPProvider: with patch( "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", - return_value=None, + return_value=(None, "test-project"), ), patch( "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", return_value=projects, @@ -68,102 +67,10 @@ class TestGCPProvider: assert gcp_provider.session is None assert gcp_provider.project_ids == ["test-project"] assert gcp_provider.projects == projects + assert gcp_provider.default_project_id == "test-project" assert gcp_provider.identity == GCPIdentityInfo(profile="default") assert gcp_provider.audit_config == {"shodan_api_key": None} - @freeze_time(datetime.today()) - def test_gcp_provider_output_options(self): - arguments = Namespace() - arguments.project_id = [] - arguments.excluded_project_id = [] - arguments.list_project_id = False - arguments.credentials_file = "" - arguments.impersonate_service_account = "" - arguments.config_file = default_config_file_path - arguments.fixer_config = default_fixer_config_file_path - - # Output options - arguments.status = [] - arguments.output_formats = ["csv"] - arguments.output_directory = "output_test_directory" - arguments.verbose = True - arguments.only_logs = False - arguments.unix_timestamp = False - arguments.shodan = "test-api-key" - - projects = { - "test-project": GCPProject( - number="55555555", - id="project/55555555", - name="test-project", - labels={"test": "value"}, - lifecycle_state="", - ) - } - - mocked_service = MagicMock() - - mocked_service.projects.list.return_value = MagicMock( - execute=MagicMock(return_value={"projects": projects}) - ) - with patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", - return_value=None, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", - return_value=projects, - ), patch( - "prowler.providers.gcp.gcp_provider.GcpProvider.update_projects_with_organizations", - return_value=None, - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - new=mock_api_client, - ), patch( - "prowler.providers.gcp.gcp_provider.discovery.build", - return_value=mocked_service, - ): - gcp_provider = GcpProvider( - arguments.project_id, - arguments.excluded_project_id, - arguments.credentials_file, - arguments.impersonate_service_account, - arguments.list_project_id, - arguments.config_file, - arguments.fixer_config, - ) - # This is needed since the output_options requires to get the global provider to get the audit config - with patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=gcp_provider, - ): - gcp_provider.output_options = arguments, {} - - assert isinstance(gcp_provider.output_options, GCPOutputOptions) - assert gcp_provider.output_options.status == [] - assert gcp_provider.output_options.output_modes == [ - "csv", - ] - assert ( - gcp_provider.output_options.output_directory - == arguments.output_directory - ) - assert gcp_provider.output_options.bulk_checks_metadata == {} - assert gcp_provider.output_options.verbose - assert ( - f"prowler-output-{gcp_provider.identity.profile}" - in gcp_provider.output_options.output_filename - ) - # Flaky due to the millisecond part of the timestamp - # assert ( - # gcp_provider.output_options.output_filename - # == f"prowler-output-{gcp_provider.identity.profile}-{datetime.today().strftime('%Y%m%d%H%M%S')}" - # ) - - # Delete testing directory - # TODO: move this to a fixtures file - rmdir(f"{arguments.output_directory}/compliance") - rmdir(arguments.output_directory) - @freeze_time(datetime.today()) def test_is_project_matching(self): arguments = Namespace() @@ -201,7 +108,7 @@ class TestGCPProvider: ) with patch( "prowler.providers.gcp.gcp_provider.GcpProvider.setup_session", - return_value=None, + return_value=(None, None), ), patch( "prowler.providers.gcp.gcp_provider.GcpProvider.get_projects", return_value=projects, diff --git a/tests/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured_test.py b/tests/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured_test.py index 61a209728e..578f04bc75 100644 --- a/tests/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured_test.py +++ b/tests/providers/gcp/services/apikeys/apikeys_api_restrictions_configured/apikeys_api_restrictions_configured_test.py @@ -100,7 +100,7 @@ class Test_apikeys_api_restrictions_configured: assert len(result) == 1 assert result[0].status == "FAIL" assert search( - f"API key {key.name} doens't have restrictions configured.", + f"API key {key.name} does not have restrictions configured.", result[0].status_extended, ) assert result[0].resource_id == key.id @@ -144,7 +144,7 @@ class Test_apikeys_api_restrictions_configured: assert len(result) == 1 assert result[0].status == "FAIL" assert search( - f"API key {key.name} doens't have restrictions configured.", + f"API key {key.name} does not have restrictions configured.", result[0].status_extended, ) assert result[0].resource_id == key.id diff --git a/tests/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured_test.py b/tests/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured_test.py index d002eaf550..c9ea843b20 100644 --- a/tests/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured_test.py +++ b/tests/providers/gcp/services/iam/iam_organization_essential_contacts_configured/iam_organization_essential_contacts_configured_test.py @@ -40,6 +40,7 @@ class Test_iam_organization_essential_contacts_configured: essentialcontacts_client.organizations = [ Organization(id="test_id", name="test", contacts=True) ] + essentialcontacts_client.default_project_id = "test_id" from prowler.providers.gcp.services.iam.iam_organization_essential_contacts_configured.iam_organization_essential_contacts_configured import ( iam_organization_essential_contacts_configured, ) @@ -73,6 +74,7 @@ class Test_iam_organization_essential_contacts_configured: essentialcontacts_client.organizations = [ Organization(id="test_id", name="test", contacts=False) ] + essentialcontacts_client.default_project_id = "test_id" from prowler.providers.gcp.services.iam.iam_organization_essential_contacts_configured.iam_organization_essential_contacts_configured import ( iam_organization_essential_contacts_configured, diff --git a/tests/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled_test.py b/tests/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled_test.py index 0cf3fbd2d9..0f1311cf09 100644 --- a/tests/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled_test.py +++ b/tests/providers/gcp/services/kms/kms_key_rotation_enabled/kms_key_rotation_enabled_test.py @@ -549,3 +549,61 @@ class Test_kms_key_rotation_enabled: assert result[0].resource_name == kms_client.crypto_keys[0].name assert result[0].location == kms_client.crypto_keys[0].location assert result[0].project_id == kms_client.crypto_keys[0].project_id + + def test_kms_key_rotation_with_fractional_seconds(self): + kms_client = mock.MagicMock + + with mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), mock.patch( + "prowler.providers.gcp.services.kms.kms_key_rotation_enabled.kms_key_rotation_enabled.kms_client", + new=kms_client, + ): + from prowler.providers.gcp.services.kms.kms_key_rotation_enabled.kms_key_rotation_enabled import ( + kms_key_rotation_enabled, + ) + from prowler.providers.gcp.services.kms.kms_service import ( + CriptoKey, + KeyLocation, + KeyRing, + ) + + kms_client.project_ids = [GCP_PROJECT_ID] + kms_client.region = GCP_US_CENTER1_LOCATION + + keyring = KeyRing( + name="projects/123/locations/us-central1/keyRings/keyring1", + project_id=GCP_PROJECT_ID, + ) + + keylocation = KeyLocation( + name=GCP_US_CENTER1_LOCATION, + project_id=GCP_PROJECT_ID, + ) + + kms_client.crypto_keys = [ + CriptoKey( + name="key1", + id="projects/123/locations/us-central1/keyRings/keyring1/cryptoKeys/key1", + project_id=GCP_PROJECT_ID, + rotation_period="7776000s", + next_rotation_time="2025-07-06T22:00:00.561275Z", + key_ring=keyring.name, + location=keylocation.name, + members=["user:jane@example.com"], + ) + ] + + check = kms_key_rotation_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Key {kms_client.crypto_keys[0].name} is rotated every 90 days or less but the next rotation time is in more than 90 days." + ) + assert result[0].resource_id == kms_client.crypto_keys[0].id + assert result[0].resource_name == kms_client.crypto_keys[0].name + assert result[0].location == kms_client.crypto_keys[0].location + assert result[0].project_id == kms_client.crypto_keys[0].project_id diff --git a/tests/providers/kubernetes/kubernetes_provider_test.py b/tests/providers/kubernetes/kubernetes_provider_test.py index 02e99f6ad1..5edafc1e3f 100644 --- a/tests/providers/kubernetes/kubernetes_provider_test.py +++ b/tests/providers/kubernetes/kubernetes_provider_test.py @@ -1,5 +1,4 @@ from argparse import Namespace -from os import rmdir from unittest.mock import patch from kubernetes import client @@ -11,7 +10,6 @@ from prowler.config.config import ( from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider from prowler.providers.kubernetes.models import ( KubernetesIdentityInfo, - KubernetesOutputOptions, KubernetesSession, ) from tests.providers.kubernetes.kubernetes_fixtures import KUBERNETES_CONFIG @@ -80,76 +78,6 @@ class TestKubernetesProvider: assert kubernetes_provider.audit_config == KUBERNETES_CONFIG - def test_set_provider_output_options_kubernetes(self): - arguments = Namespace() - arguments.kubeconfig_file = "dummy_path" - arguments.context = None - arguments.only_logs = False - arguments.namespace = None - arguments.config_file = default_config_file_path - arguments.fixer_config = default_fixer_config_file_path - arguments.status = [] - arguments.output_formats = ["csv"] - arguments.output_directory = "output_test_directory" - arguments.verbose = True - arguments.output_filename = "output_test_filename" - arguments.only_logs = False - arguments.unix_timestamp = False - arguments.shodan = "test-api-key" - - context = { - "name": "test-context", - "context": { - "user": "test-user", - "cluster": "test-cluster", - }, - } - with patch( - "prowler.providers.kubernetes.kubernetes_provider.KubernetesProvider.setup_session", - return_value=KubernetesSession( - api_client=client.ApiClient, - context=context, - ), - ), patch( - "prowler.providers.kubernetes.kubernetes_provider.KubernetesProvider.get_all_namespaces", - return_value=["namespace-1"], - ): - - kubernetes_provider = KubernetesProvider( - arguments.kubeconfig_file, - arguments.context, - arguments.namespace, - arguments.config_file, - arguments.fixer_config, - ) - # This is needed since the output_options requires to get the global provider to get the audit config - with patch( - "prowler.providers.common.provider.Provider.get_global_provider", - return_value=kubernetes_provider, - ): - - kubernetes_provider.output_options = arguments, {} - - assert isinstance( - kubernetes_provider.output_options, KubernetesOutputOptions - ) - assert kubernetes_provider.output_options.status == [] - assert kubernetes_provider.output_options.output_modes == ["csv"] - assert ( - kubernetes_provider.output_options.output_directory - == arguments.output_directory - ) - assert kubernetes_provider.output_options.bulk_checks_metadata == {} - assert kubernetes_provider.output_options.verbose - assert ( - kubernetes_provider.output_options.output_filename - == arguments.output_filename - ) - - # Delete testing directory - rmdir(f"{arguments.output_directory}/compliance") - rmdir(arguments.output_directory) - # @patch("kubernetes.client.RbacAuthorizationV1Api") # @patch("kubernetes.config.list_kube_config_contexts") # @patch("kubernetes.config.load_incluster_config")