diff --git a/.github/workflows/ui-e2e-tests.yml b/.github/workflows/ui-e2e-tests.yml index e13ec2482a..543857a511 100644 --- a/.github/workflows/ui-e2e-tests.yml +++ b/.github/workflows/ui-e2e-tests.yml @@ -84,7 +84,7 @@ jobs: working-directory: ./ui run: npm run test:e2e - name: Upload test reports - uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: failure() with: name: playwright-report diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index 61b9ed4773..14cb1d027c 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to the **Prowler API** are documented in this file. +## [1.12.0] (Prowler 5.11.0 - UNRELEASED) + +### Added +- Lighthouse support for OpenAI GPT-5 [(#8527)](https://github.com/prowler-cloud/prowler/pull/8527) + ## [1.11.0] (Prowler 5.10.0) ### Added diff --git a/api/src/backend/api/models.py b/api/src/backend/api/models.py index 97d10efc57..e08f9919a8 100644 --- a/api/src/backend/api/models.py +++ b/api/src/backend/api/models.py @@ -1752,6 +1752,10 @@ class LighthouseConfiguration(RowLevelSecurityProtectedModel): GPT_4O = "gpt-4o", _("GPT-4o Default") GPT_4O_MINI_2024_07_18 = "gpt-4o-mini-2024-07-18", _("GPT-4o Mini v2024-07-18") GPT_4O_MINI = "gpt-4o-mini", _("GPT-4o Mini Default") + GPT_5_2025_08_07 = "gpt-5-2025-08-07", _("GPT-5 v2025-08-07") + GPT_5 = "gpt-5", _("GPT-5 Default") + GPT_5_MINI_2025_08_07 = "gpt-5-mini-2025-08-07", _("GPT-5 Mini v2025-08-07") + GPT_5_MINI = "gpt-5-mini", _("GPT-5 Mini Default") id = models.UUIDField(primary_key=True, default=uuid4, editable=False) inserted_at = models.DateTimeField(auto_now_add=True, editable=False) diff --git a/docs/tutorials/configuration_file.md b/docs/tutorials/configuration_file.md index c4f48f395f..b99e5064d0 100644 --- a/docs/tutorials/configuration_file.md +++ b/docs/tutorials/configuration_file.md @@ -80,6 +80,7 @@ The following list includes all the Azure checks with configurable variables tha | `app_ensure_python_version_is_latest` | `python_latest_version` | String | | `app_ensure_java_version_is_latest` | `java_latest_version` | String | | `sqlserver_recommended_minimal_tls_version` | `recommended_minimal_tls_versions` | List of Strings | +| `vm_sufficient_daily_backup_retention_period` | `vm_backup_min_daily_retention_days` | Integer | | `vm_desired_sku_size` | `desired_vm_sku_sizes` | List of Strings | | `defender_attack_path_notifications_properly_configured` | `defender_attack_path_minimal_risk_level` | String | diff --git a/docs/tutorials/github/getting-started-github.md b/docs/tutorials/github/getting-started-github.md index 3515b2ca23..b4f22d0366 100644 --- a/docs/tutorials/github/getting-started-github.md +++ b/docs/tutorials/github/getting-started-github.md @@ -13,7 +13,10 @@ This guide explains how to set up authentication with GitHub for Prowler. The do Personal Access Tokens provide the simplest GitHub authentication method and support individual user authentication or testing scenarios. -#### How to Create a Personal Access Token +???+ warning "Classic Tokens Deprecated" + GitHub has deprecated Personal Access Tokens (classic) in favor of fine-grained Personal Access Tokens. We recommend using fine-grained tokens as they provide better security through more granular permissions and resource-specific access control. + +#### **Option 1: Create a Fine-Grained Personal Access Token (Recommended)** 1. **Navigate to GitHub Settings** - Open [GitHub](https://github.com) and sign in @@ -24,18 +27,67 @@ Personal Access Tokens provide the simplest GitHub authentication method and sup - Scroll down the left sidebar - Click "Developer settings" -3. **Generate New Token** +3. **Generate Fine-Grained Token** + - Click "Personal access tokens" + - Select "Fine-grained tokens" + - Click "Generate new token" + +4. **Configure Token Settings** + - **Token name**: Give your token a descriptive name (e.g., "Prowler Security Scanner") + - **Expiration**: Set an appropriate expiration date (recommended: 90 days or less) + - **Repository access**: Choose "All repositories" or "Only select repositories" based on your needs + + ???+ note "Public repositories" + Even if you select 'Only select repositories', the token will have access to the public repositories that you own or are a member of. + +5. **Configure Token Permissions** + To enable Prowler functionality, configure the following permissions: + + - **Repository permissions:** + - **Contents**: Read-only access + - **Metadata**: Read-only access + - **Pull requests**: Read-only access + - **Security advisories**: Read-only access + - **Statuses**: Read-only access + + - **Organization permissions:** + - **Members**: Read-only access + + - **Account permissions:** + - **Email addresses**: Read-only access + +6. **Copy and Store the Token** + - Copy the generated token immediately (GitHub displays tokens only once) + - Store tokens securely using environment variables + +![GitHub Personal Access Token Permissions](./img/github-pat-permissions.png) + +#### **Option 2: Create a Classic Personal Access Token (Not Recommended)** + +???+ warning "Security Risk" + Classic tokens provide broad permissions that may exceed what Prowler actually needs. Use fine-grained tokens instead for better security. + +1. **Navigate to GitHub Settings** + - Open [GitHub](https://github.com) and sign in + - Click the profile picture in the top right corner + - Select "Settings" from the dropdown menu + +2. **Access Developer Settings** + - Scroll down the left sidebar + - Click "Developer settings" + +3. **Generate Classic Token** - Click "Personal access tokens" - Select "Tokens (classic)" - Click "Generate new token" 4. **Configure Token Permissions** To enable Prowler functionality, configure the following scopes: - - `repo`: Full control of private repositories + - `repo`: Full control of private repositories (includes `repo:status` and `repo:contents`) - `read:org`: Read organization and team membership - `read:user`: Read user profile data - - `read:discussion`: Read discussions - - `read:enterprise`: Read enterprise data (if applicable) + - `security_events`: Access security events (secret scanning and Dependabot alerts) + - `read:enterprise`: Read enterprise data (if using GitHub Enterprise) 5. **Copy and Store the Token** - Copy the generated token immediately (GitHub displays tokens only once) diff --git a/docs/tutorials/github/img/github-pat-permissions.png b/docs/tutorials/github/img/github-pat-permissions.png new file mode 100644 index 0000000000..d666c70baa Binary files /dev/null and b/docs/tutorials/github/img/github-pat-permissions.png differ diff --git a/docs/tutorials/img/github-app-credentials.png b/docs/tutorials/img/github-app-credentials.png new file mode 100644 index 0000000000..1a71db4fbd Binary files /dev/null and b/docs/tutorials/img/github-app-credentials.png differ diff --git a/docs/tutorials/img/github-auth-methods.png b/docs/tutorials/img/github-auth-methods.png new file mode 100644 index 0000000000..17414054aa Binary files /dev/null and b/docs/tutorials/img/github-auth-methods.png differ diff --git a/docs/tutorials/img/github-oauth-credentials.png b/docs/tutorials/img/github-oauth-credentials.png new file mode 100644 index 0000000000..5d84c4bf75 Binary files /dev/null and b/docs/tutorials/img/github-oauth-credentials.png differ diff --git a/docs/tutorials/img/github-pat-credentials.png b/docs/tutorials/img/github-pat-credentials.png new file mode 100644 index 0000000000..044cbb7996 Binary files /dev/null and b/docs/tutorials/img/github-pat-credentials.png differ diff --git a/docs/tutorials/prowler-app.md b/docs/tutorials/prowler-app.md index b7eb18e183..85e498aba5 100644 --- a/docs/tutorials/prowler-app.md +++ b/docs/tutorials/prowler-app.md @@ -201,6 +201,51 @@ For full setup instructions and requirements, check the [Microsoft 365 provider Prowler Cloud M365 Credentials +### **Step 4.6: GitHub Credentials** +For GitHub, you must enter your Provider ID (username or organization name) and choose the authentication method you want to use: + +- **Personal Access Token** (Recommended for individual users) +- **OAuth App Token** (For applications requiring user consent) +- **GitHub App** (Recommended for organizations and production use) + +???+ note + For full setup instructions and requirements, check the [GitHub provider requirements](./github/getting-started-github.md). + +GitHub Authentication Methods + +#### **Step 4.6.1: Personal Access Token** + +Personal Access Tokens provide the simplest GitHub authentication method and support individual user authentication or testing scenarios. + +- Select `Personal Access Token` and enter your `Personal Access Token`: + +GitHub Personal Access Token Credentials + +???+ note + For detailed instructions on creating a Personal Access Token and the exact permissions required, check the [GitHub Personal Access Token tutorial](./github/getting-started-github.md#1-personal-access-token-pat). + +#### **Step 4.6.2: OAuth App Token** + +OAuth Apps enable applications to act on behalf of users with explicit consent. + +- Select `OAuth App Token` and enter your `OAuth App Token`: + +GitHub OAuth App Credentials + +???+ note + To create an OAuth App, go to GitHub Settings → Developer settings → OAuth Apps → New OAuth App. You'll need to exchange an authorization code for an access token using the OAuth flow. + +#### **Step 4.6.3: GitHub App** + +GitHub Apps provide the recommended integration method for accessing multiple repositories or organizations. + +- Select `GitHub App` and enter your `GitHub App ID` and `GitHub App Private Key`: + + GitHub App Credentials + +???+ note + To create a GitHub App, go to GitHub Settings → Developer settings → GitHub Apps → New GitHub App. Configure the necessary permissions and generate a private key. Install the app to your account or organization and provide the App ID and private key content. + ## **Step 5: Test Connection** After adding your credentials of your cloud account, click the `Launch` button to verify that Prowler App can successfully connect to your provider: diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index a6ec20d3c5..3ee7fe8df6 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -2,7 +2,46 @@ All notable changes to the **Prowler SDK** are documented in this file. -## [v5.10.1] (Prowler UNRELEASED) +## [v5.11.0] (Prowler UNRELEASED) + +### Added +- Certificate authentication for M365 provider [(#8404)](https://github.com/prowler-cloud/prowler/pull/8404) +- `vm_sufficient_daily_backup_retention_period` check for Azure provider [(#8200)](https://github.com/prowler-cloud/prowler/pull/8200) +- `vm_jit_access_enabled` check for Azure provider [(#8202)](https://github.com/prowler-cloud/prowler/pull/8202) +- Bedrock AgentCore privilege escalation combination for AWS provider [(#8526)](https://github.com/prowler-cloud/prowler/pull/8526) +- Remove standalone iam:PassRole from privesc detection and add missing patterns [(#8530)](https://github.com/prowler-cloud/prowler/pull/8530) +- `eks_cluster_deletion_protection_enabled` check for AWS provider [(#8536)](https://github.com/prowler-cloud/prowler/pull/8536) + +### Changed +- Refine kisa isms-p compliance mapping [(#8479)](https://github.com/prowler-cloud/prowler/pull/8479) + +### Fixed + +--- + +## [v5.10.3] (Prowler UNRELEASED) + +### Fixed +- AWS resource-arn filtering [(#8533)](https://github.com/prowler-cloud/prowler/pull/8533) +- GitHub App authentication for GitHub provider [(#8529)](https://github.com/prowler-cloud/prowler/pull/8529) + +--- + +## [v5.10.2] (Prowler v5.10.2) + +### Fixed +- Order requirements by ID in Prowler ThreatScore AWS compliance framework [(#8495)](https://github.com/prowler-cloud/prowler/pull/8495) +- Add explicit resource name to GCP and Azure Defender checks [(#8352)](https://github.com/prowler-cloud/prowler/pull/8352) +- Validation errors in Azure and M365 providers [(#8353)](https://github.com/prowler-cloud/prowler/pull/8353) +- Azure `app_http_logs_enabled` check false positives [(#8507)](https://github.com/prowler-cloud/prowler/pull/8507) +- Azure `storage_geo_redundant_enabled` check false positives [(#8504)](https://github.com/prowler-cloud/prowler/pull/8504) +- AWS `kafka_cluster_is_public` check false positives [(#8514)](https://github.com/prowler-cloud/prowler/pull/8514) +- List all accessible repositories in GitHub [(#8522)](https://github.com/prowler-cloud/prowler/pull/8522) +- GitHub CIS 1.0 Compliance Reports [(#8519)](https://github.com/prowler-cloud/prowler/pull/8519) + +--- + +## [v5.10.1] (Prowler v5.10.1) ### Changed - Update AWS Neptune service metadata to new format [(#8494)](https://github.com/prowler-cloud/prowler/pull/8494) diff --git a/prowler/compliance/aws/kisa_isms_p_2023_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_aws.json index 03bd5acaef..bd9b256c37 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_aws.json @@ -206,11 +206,7 @@ "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": [ - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered" - ], + "Checks": [], "Attributes": [ { "Domain": "1. Establishment and Operation of the Management System", @@ -403,10 +399,7 @@ "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": [ - "cloudwatch_log_metric_filter_aws_organizations_changes", - "codebuild_project_older_90_days" - ], + "Checks": [], "Attributes": [ { "Domain": "1. Establishment and Operation of the Management System", @@ -574,11 +567,7 @@ "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": [ - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered" - ], + "Checks": [], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -617,18 +606,13 @@ "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": [ - "ec2_elastic_ip_unassigned", - "ec2_instance_older_than_specific_days", - "ecr_repositories_lifecycle_policy_enabled", - "neptune_cluster_copy_tags_to_snapshots", + "fsx_file_system_copy_tags_to_backups_enabled", "fsx_file_system_copy_tags_to_volumes_enabled", - "organizations_account_part_of_organizations", - "organizations_delegated_administrators", - "organizations_scp_check_deny_regions", + "neptune_cluster_copy_tags_to_snapshots", "organizations_tags_policies_enabled_and_attached", "rds_cluster_copy_tags_to_snapshots", "rds_instance_copy_tags_to_snapshots", - "resourceexplorer2_indexes_found" + "redshift_cluster_non_default_username" ], "Attributes": [ { @@ -658,10 +642,7 @@ "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": [ - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered" - ], + "Checks": [], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -697,9 +678,7 @@ "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": [ - "account_maintain_different_contact_details_to_security_billing_and_operations" - ], + "Checks": [], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -1201,17 +1180,12 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings", - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered", "account_security_questions_are_registered_in_the_aws_account", - "apigateway_restapi_authorizers_enabled", - "autoscaling_group_launch_configuration_requires_imdsv2", + "cognito_identity_pool_guest_access_disabled", "cognito_user_pool_deletion_protection_enabled", "cognito_user_pool_self_registration_disabled", - "config_recorder_using_aws_service_role", "ec2_instance_profile_attached", - "eventbridge_schema_registry_cross_account_access", + "iam_avoid_root_usage", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_customer_unattached_policy_no_administrative_privileges", @@ -1221,7 +1195,6 @@ "iam_inline_policy_no_full_access_to_cloudtrail", "iam_inline_policy_no_full_access_to_kms", "iam_no_custom_policy_permissive_role_assumption", - "iam_no_root_access_key", "iam_policy_allows_privilege_escalation", "iam_policy_attached_only_to_group_or_roles", "iam_policy_cloudshell_admin_not_attached", @@ -1232,7 +1205,10 @@ "iam_role_cross_service_confused_deputy_prevention", "iam_securityaudit_role_created", "iam_support_role_created", - "iam_user_administrator_access_policy" + "iam_user_administrator_access_policy", + "rds_cluster_default_admin", + "rds_instance_default_admin", + "redshift_cluster_non_default_database_name" ], "Attributes": [ { @@ -1267,8 +1243,6 @@ "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": [ - "cognito_user_pool_advanced_security_enabled", - "ec2_instance_profile_attached", "efs_access_point_enforce_user_identity" ], "Attributes": [ @@ -1303,15 +1277,14 @@ "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": [ "account_security_questions_are_registered_in_the_aws_account", - "apigateway_restapi_client_certificate_enabled", + "apigateway_restapi_authorizers_enabled", "apigateway_restapi_public_with_authorizer", "apigatewayv2_api_authorizers_enabled", "appstream_fleet_maximum_session_duration", "appstream_fleet_session_disconnect_timeout", "appstream_fleet_session_idle_disconnect_timeout", + "autoscaling_group_launch_configuration_requires_imdsv2", "cloudtrail_bucket_requires_mfa_delete", - "cloudwatch_log_metric_filter_authentication_failures", - "cloudwatch_log_metric_filter_sign_in_without_mfa", "cognito_user_pool_advanced_security_enabled", "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", @@ -1325,24 +1298,16 @@ "elasticache_redis_replication_group_auth_enabled", "iam_administrator_access_with_mfa", "iam_check_saml_providers_sts", - "iam_no_root_access_key", "iam_root_hardware_mfa_enabled", "iam_root_mfa_enabled", - "iam_rotate_access_key_90_days", - "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_user_hardware_mfa_enabled", "iam_user_mfa_enabled_console_access", - "iam_user_no_setup_initial_access_key", - "iam_user_two_active_access_key", "iam_user_with_temporary_credentials", "neptune_cluster_iam_authentication_enabled", - "opensearch_service_domains_internal_user_database_enabled", "opensearch_service_domains_use_cognito_authentication_for_kibana", - "organizations_account_part_of_organizations", "rds_cluster_iam_authentication_enabled", "rds_instance_iam_authentication_enabled", - "s3_bucket_cross_account_access", "s3_bucket_no_mfa_delete" ], "Attributes": [ @@ -1423,11 +1388,6 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings", - "apigateway_restapi_authorizers_enabled", - "cloudwatch_log_metric_filter_root_usage", - "cognito_user_pool_self_registration_disabled", - "config_recorder_using_aws_service_role", - "ecs_task_definitions_no_privileged_containers", "iam_administrator_access_with_mfa", "iam_avoid_root_usage", "iam_aws_attached_policy_no_administrative_privileges", @@ -1445,17 +1405,12 @@ "iam_policy_no_full_access_to_cloudtrail", "iam_policy_no_full_access_to_kms", "iam_role_administratoraccess_policy", - "iam_role_cross_account_readonlyaccess_policy", "iam_root_hardware_mfa_enabled", "iam_root_mfa_enabled", "iam_securityaudit_role_created", "iam_support_role_created", "iam_user_administrator_access_policy", - "organizations_delegated_administrators", - "rds_cluster_default_admin", - "rds_instance_default_admin", - "sagemaker_notebook_instance_root_access_disabled", - "ses_identity_not_publicly_accessible" + "organizations_delegated_administrators" ], "Attributes": [ { @@ -1492,25 +1447,9 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings", - "apigateway_restapi_authorizers_enabled", - "appstream_fleet_maximum_session_duration", - "appstream_fleet_session_disconnect_timeout", - "appstream_fleet_session_idle_disconnect_timeout", - "autoscaling_group_launch_configuration_requires_imdsv2", - "cloudwatch_cross_account_sharing_disabled", - "cloudwatch_log_group_not_publicly_accessible", - "cloudwatch_log_metric_filter_policy_changes", - "cognito_identity_pool_guest_access_disabled", - "cognito_user_pool_advanced_security_enabled", - "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", - "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", - "cognito_user_pool_client_prevent_user_existence_errors", - "cognito_user_pool_client_token_revocation_enabled", - "cognito_user_pool_self_registration_disabled", "config_recorder_using_aws_service_role", "dynamodb_table_cross_account_access", "ec2_instance_internet_facing_with_instance_profile", - "ec2_instance_managed_by_ssm", "ec2_instance_profile_attached", "eventbridge_bus_cross_account_access", "eventbridge_schema_registry_cross_account_access", @@ -1538,7 +1477,6 @@ "s3_bucket_policy_public_write_access", "s3_bucket_public_list_acl", "s3_bucket_public_write_acl", - "ses_identity_not_publicly_accessible", "sns_topics_not_publicly_accessible", "sqs_queues_not_publicly_accessible", "ssm_documents_set_as_public", @@ -1576,27 +1514,22 @@ "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": [ - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_client_certificate_enabled", "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", "apigateway_restapi_waf_acl_attached", "appstream_fleet_default_internet_access_disabled", "autoscaling_group_launch_configuration_no_public_ip", "awslambda_function_inside_vpc", "awslambda_function_not_publicly_accessible", - "cloudfront_distributions_default_root_object", "cloudfront_distributions_geo_restrictions_enabled", "cloudfront_distributions_s3_origin_access_control", "cloudfront_distributions_using_waf", "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudwatch_cross_account_sharing_disabled", - "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", - "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_log_group_not_publicly_accessible", "cognito_user_pool_waf_acl_attached", + "dms_endpoint_ssl_enabled", "dms_instance_no_public_access", "documentdb_cluster_public_snapshot", - "dynamodb_table_cross_account_access", + "ec2_elastic_ip_shodan", "ec2_instance_internet_facing_with_instance_profile", "ec2_instance_port_cassandra_exposed_to_internet", "ec2_instance_port_cifs_exposed_to_internet", @@ -1641,68 +1574,50 @@ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", "ec2_securitygroup_allow_wide_open_public_ipv4", "ec2_securitygroup_default_restrict_traffic", - "ec2_securitygroup_from_launch_wizard", - "ec2_securitygroup_not_used", - "ec2_securitygroup_with_many_ingress_egress_rules", "ec2_transitgateway_auto_accept_vpc_attachments", "ecr_repositories_not_publicly_accessible", "ecs_service_no_assign_public_ip", "ecs_task_set_no_assign_public_ip", "efs_mount_target_not_publicly_accessible", - "efs_not_publicly_accessible", "eks_cluster_network_policy_enabled", "eks_cluster_not_publicly_accessible", "eks_cluster_private_nodes_enabled", "elasticache_cluster_uses_public_subnet", "elb_internet_facing", "elbv2_internet_facing", - "elbv2_listeners_underneath", "elbv2_waf_acl_attached", "emr_cluster_account_public_block_enabled", "emr_cluster_master_nodes_no_public_ip", "emr_cluster_publicly_accesible", - "eventbridge_bus_exposed", - "glue_data_catalogs_not_publicly_accessible", + "glacier_vaults_policy_public_access", "kafka_cluster_is_public", - "kafka_cluster_unrestricted_access_disabled", "lightsail_database_public", "lightsail_instance_public", - "lightsail_static_ip_unused", - "neptune_cluster_public_snapshot", "neptune_cluster_uses_public_subnet", - "networkfirewall_deletion_protection", - "networkfirewall_in_all_vpc", - "networkfirewall_multi_az", - "networkfirewall_policy_default_action_fragmented_packets", - "networkfirewall_policy_default_action_full_packets", - "networkfirewall_policy_rule_group_associated", "opensearch_service_domains_not_publicly_accessible", - "rds_snapshots_public_access", + "rds_cluster_non_default_port", "rds_instance_inside_vpc", + "rds_instance_no_public_access", + "rds_instance_non_default_port", "redshift_cluster_enhanced_vpc_routing", "redshift_cluster_public_access", "route53_dangling_ip_subdomain_takeover", "s3_access_point_public_access_block", + "s3_account_level_public_access_blocks", + "s3_bucket_level_public_access_block", + "s3_bucket_public_access", + "s3_multi_region_access_point_public_access_block", "sagemaker_models_network_isolation_enabled", "sagemaker_models_vpc_settings_configured", "sagemaker_notebook_instance_vpc_settings_configured", "sagemaker_notebook_instance_without_direct_internet_access_configured", "sagemaker_training_jobs_network_isolation_enabled", "sagemaker_training_jobs_vpc_settings_configured", - "sqs_queues_server_side_encryption_enabled", "vpc_endpoint_connections_trust_boundaries", "vpc_endpoint_for_ec2_enabled", "vpc_peering_routing_tables_with_least_privilege", "vpc_subnet_no_public_ip_by_default", "vpc_subnet_separate_private_public", - "vpc_vpn_connection_tunnels_up", - "waf_global_rule_with_conditions", - "waf_global_rulegroup_not_empty", - "waf_global_webacl_with_rules", - "waf_regional_rule_with_conditions", - "waf_regional_rulegroup_not_empty", - "waf_regional_webacl_with_rules", - "wafv2_webacl_with_rules", "workspaces_vpc_2private_1public_subnets_nat" ], "Attributes": [ @@ -1722,7 +1637,6 @@ ], "AuditEvidence": [ "Network diagram", - "IP management ledger", "Information asset list", "Firewall rules" ], @@ -1741,24 +1655,14 @@ "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": [ - "apigateway_restapi_client_certificate_enabled", - "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", "apigateway_restapi_waf_acl_attached", - "apigatewayv2_api_authorizers_enabled", "athena_workgroup_enforce_configuration", - "autoscaling_group_launch_configuration_requires_imdsv2", - "awslambda_function_inside_vpc", - "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", - "awslambda_function_not_publicly_accessible", - "cloudfront_distributions_s3_origin_access_control", + "awslambda_function_url_cors_policy", + "awslambda_function_url_public", + "cloudfront_distributions_default_root_object", + "cloudfront_distributions_using_waf", "cloudwatch_cross_account_sharing_disabled", - "cloudwatch_log_group_not_publicly_accessible", - "cognito_identity_pool_guest_access_disabled", - "cognito_user_pool_client_token_revocation_enabled", - "dms_instance_no_public_access", - "documentdb_cluster_public_snapshot", - "dynamodb_table_cross_account_access", + "cognito_user_pool_waf_acl_attached", "ec2_ami_public", "ec2_ebs_public_snapshot", "ec2_ebs_snapshot_account_block_public_access", @@ -1766,70 +1670,39 @@ "ec2_instance_imdsv2_enabled", "ec2_instance_internet_facing_with_instance_profile", "ec2_instance_managed_by_ssm", - "ec2_instance_port_cassandra_exposed_to_internet", "ec2_instance_port_cifs_exposed_to_internet", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", "ec2_instance_port_ftp_exposed_to_internet", - "ec2_instance_port_memcached_exposed_to_internet", - "ec2_instance_port_mongodb_exposed_to_internet", - "ec2_instance_port_mysql_exposed_to_internet", - "ec2_instance_port_oracle_exposed_to_internet", - "ec2_instance_port_postgresql_exposed_to_internet", + "ec2_instance_port_kafka_exposed_to_internet", + "ec2_instance_port_kerberos_exposed_to_internet", + "ec2_instance_port_ldap_exposed_to_internet", "ec2_instance_port_rdp_exposed_to_internet", - "ec2_instance_port_redis_exposed_to_internet", - "ec2_instance_port_sqlserver_exposed_to_internet", "ec2_instance_port_ssh_exposed_to_internet", "ec2_instance_port_telnet_exposed_to_internet", - "ec2_networkacl_allow_ingress_any_port", - "ec2_networkacl_allow_ingress_tcp_port_22", - "ec2_networkacl_allow_ingress_tcp_port_3389", - "ec2_networkacl_unused", "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", "ec2_securitygroup_allow_ingress_from_internet_to_any_port", "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", - "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", - "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_ingress_from_internet_to_tcp_port_sql_server_1433_1434", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", - "ecr_repositories_not_publicly_accessible", - "ecs_service_no_assign_public_ip", - "ecs_task_definitions_host_namespace_not_shared", - "ecs_task_definitions_host_networking_mode_users", "ecs_task_definitions_no_privileged_containers", - "ecs_task_set_no_assign_public_ip", "efs_access_point_enforce_root_directory", "efs_access_point_enforce_user_identity", - "efs_mount_target_not_publicly_accessible", "efs_not_publicly_accessible", - "eks_cluster_network_policy_enabled", - "eks_cluster_not_publicly_accessible", - "eks_cluster_private_nodes_enabled", - "elasticache_cluster_uses_public_subnet", - "elasticache_redis_replication_group_auth_enabled", - "emr_cluster_account_public_block_enabled", - "emr_cluster_master_nodes_no_public_ip", - "emr_cluster_publicly_accesible", + "elbv2_waf_acl_attached", + "eventbridge_bus_cross_account_access", "eventbridge_bus_exposed", - "glacier_vaults_policy_public_access", + "eventbridge_schema_registry_cross_account_access", "glue_data_catalogs_not_publicly_accessible", - "guardduty_lambda_protection_enabled", - "guardduty_s3_protection_enabled", "kafka_cluster_is_public", "kafka_cluster_unrestricted_access_disabled", - "lightsail_instance_public", - "lightsail_static_ip_unused", "neptune_cluster_public_snapshot", "opensearch_service_domains_not_publicly_accessible", "opensearch_service_domains_use_cognito_authentication_for_kibana", + "rds_snapshots_public_access", "s3_access_point_public_access_block", "s3_account_level_public_access_blocks", "s3_bucket_acl_prohibited", @@ -1840,7 +1713,10 @@ "s3_bucket_public_list_acl", "s3_bucket_public_write_acl", "s3_multi_region_access_point_public_access_block", - "ssm_documents_set_as_public" + "sagemaker_notebook_instance_root_access_disabled", + "ses_identity_not_publicly_accessible", + "ssm_documents_set_as_public", + "vpc_endpoint_connections_trust_boundaries" ], "Attributes": [ { @@ -1878,24 +1754,8 @@ "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": [ - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", - "apigatewayv2_api_authorizers_enabled", - "awslambda_function_url_cors_policy", - "awslambda_function_url_public", - "cloudfront_distributions_default_root_object", - "codeartifact_packages_external_public_publishing_disabled", - "cognito_user_pool_waf_acl_attached", - "ec2_ami_public", "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", - "ec2_instance_port_kafka_exposed_to_internet", - "ec2_instance_port_kerberos_exposed_to_internet", - "ec2_instance_port_ldap_exposed_to_internet", - "ecr_repositories_not_publicly_accessible", - "ecs_task_definitions_host_namespace_not_shared", - "ecs_task_definitions_host_networking_mode_users", - "eks_cluster_network_policy_enabled" + "opensearch_service_domains_use_cognito_authentication_for_kibana" ], "Attributes": [ { @@ -1920,9 +1780,7 @@ "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" + "Personal information processing system's personal information viewing and search screens" ], "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.", @@ -1944,7 +1802,6 @@ "dms_endpoint_mongodb_authentication_enabled", "dms_endpoint_neptune_iam_authorization_enabled", "dms_endpoint_ssl_enabled", - "documentdb_cluster_public_snapshot", "dynamodb_table_cross_account_access", "ec2_instance_port_cassandra_exposed_to_internet", "ec2_instance_port_memcached_exposed_to_internet", @@ -1954,6 +1811,10 @@ "ec2_instance_port_postgresql_exposed_to_internet", "ec2_instance_port_redis_exposed_to_internet", "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", @@ -1961,25 +1822,17 @@ "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_ingress_from_internet_to_tcp_port_sql_server_1433_1434", - "guardduty_rds_protection_enabled", + "elasticache_redis_replication_group_auth_enabled", "lightsail_database_public", "neptune_cluster_iam_authentication_enabled", - "neptune_cluster_uses_public_subnet", + "neptune_cluster_public_snapshot", "opensearch_service_domains_internal_user_database_enabled", "rds_cluster_iam_authentication_enabled", "rds_cluster_non_default_port", - "rds_cluster_storage_encrypted", "rds_instance_iam_authentication_enabled", "rds_instance_no_public_access", "rds_instance_non_default_port", - "rds_instance_storage_encrypted", - "rds_instance_transport_encrypted", - "rds_snapshots_public_access", - "redshift_cluster_encrypted_at_rest", "redshift_cluster_enhanced_vpc_routing", - "redshift_cluster_in_transit_encryption_enabled", - "redshift_cluster_non_default_database_name", - "redshift_cluster_non_default_username", "redshift_cluster_public_access" ], "Attributes": [ @@ -2054,7 +1907,6 @@ "appstream_fleet_maximum_session_duration", "appstream_fleet_session_disconnect_timeout", "appstream_fleet_session_idle_disconnect_timeout", - "autoscaling_group_launch_configuration_requires_imdsv2", "ec2_instance_port_cifs_exposed_to_internet", "ec2_instance_port_ftp_exposed_to_internet", "ec2_instance_port_rdp_exposed_to_internet", @@ -2062,12 +1914,13 @@ "ec2_instance_port_telnet_exposed_to_internet", "ec2_networkacl_allow_ingress_tcp_port_22", "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", - "vpc_vpn_connection_tunnels_up", - "workspaces_volume_encryption_enabled", "workspaces_vpc_2private_1public_subnets_nat" ], "Attributes": [ @@ -2082,8 +1935,8 @@ "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)" + "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)", @@ -2107,59 +1960,7 @@ "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": [ - "apigateway_restapi_waf_acl_attached", - "appstream_fleet_default_internet_access_disabled", - "autoscaling_group_launch_configuration_no_public_ip", - "awslambda_function_not_publicly_accessible", - "awslambda_function_url_cors_policy", - "awslambda_function_url_public", - "cloudfront_distributions_using_waf", - "dms_instance_no_public_access", - "ec2_elastic_ip_shodan", - "ec2_instance_port_cassandra_exposed_to_internet", - "ec2_instance_port_cifs_exposed_to_internet", - "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", - "ec2_instance_port_ftp_exposed_to_internet", - "ec2_instance_port_kafka_exposed_to_internet", - "ec2_instance_port_kerberos_exposed_to_internet", - "ec2_instance_port_ldap_exposed_to_internet", - "ec2_instance_port_memcached_exposed_to_internet", - "ec2_instance_port_mongodb_exposed_to_internet", - "ec2_instance_port_mysql_exposed_to_internet", - "ec2_instance_port_oracle_exposed_to_internet", - "ec2_instance_port_postgresql_exposed_to_internet", - "ec2_instance_port_rdp_exposed_to_internet", - "ec2_instance_port_redis_exposed_to_internet", - "ec2_instance_port_sqlserver_exposed_to_internet", - "ec2_instance_port_ssh_exposed_to_internet", - "ec2_instance_port_telnet_exposed_to_internet", - "ec2_instance_public_ip", - "ecs_service_no_assign_public_ip", - "elb_internet_facing", - "elbv2_internet_facing", - "emr_cluster_account_public_block_enabled", - "emr_cluster_master_nodes_no_public_ip", - "emr_cluster_publicly_accesible", - "eventbridge_bus_exposed", - "glue_data_catalogs_not_publicly_accessible", - "kafka_cluster_is_public", - "kafka_cluster_unrestricted_access_disabled", - "neptune_cluster_public_snapshot", - "networkfirewall_deletion_protection", - "networkfirewall_in_all_vpc", - "networkfirewall_multi_az", - "opensearch_service_domains_not_publicly_accessible", - "route53_dangling_ip_subdomain_takeover", - "sagemaker_notebook_instance_without_direct_internet_access_configured", - "vpc_endpoint_connections_trust_boundaries", - "vpc_endpoint_for_ec2_enabled", - "vpc_peering_routing_tables_with_least_privilege", - "vpc_subnet_no_public_ip_by_default", - "vpc_subnet_separate_private_public", - "vpc_vpn_connection_tunnels_up", - "workspaces_vpc_2private_1public_subnets_nat" - ], + "Checks": [], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -2171,8 +1972,8 @@ "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)" + "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)", @@ -2198,8 +1999,8 @@ "Checks": [ "acm_certificates_expiration_check", "acm_certificates_with_secure_key_algorithms", - "apigateway_restapi_authorizers_enabled", "apigateway_restapi_cache_encrypted", + "apigateway_restapi_client_certificate_enabled", "athena_workgroup_encryption", "awslambda_function_no_secrets_in_code", "awslambda_function_no_secrets_in_variables", @@ -2211,7 +2012,6 @@ "cloudfront_distributions_https_sni_enabled", "cloudfront_distributions_origin_traffic_encrypted", "cloudfront_distributions_using_deprecated_ssl_protocols", - "cloudwatch_log_group_no_secrets_in_logs", "codebuild_project_no_secrets_in_variables", "codebuild_project_s3_logs_encrypted", "codebuild_project_source_repo_url_no_sensitive_credentials", @@ -2229,7 +2029,6 @@ "ec2_launch_template_no_secrets", "ecs_task_definitions_no_environment_secrets", "efs_encryption_at_rest_enabled", - "eks_cluster_kms_cmk_encryption_in_secrets_enabled", "elasticache_redis_cluster_in_transit_encryption_enabled", "elasticache_redis_cluster_rest_encryption_enabled", "elb_insecure_ssl_ciphers", @@ -2248,7 +2047,7 @@ "glue_etl_jobs_cloudwatch_logs_encryption_enabled", "glue_etl_jobs_job_bookmark_encryption_enabled", "glue_ml_transform_encrypted_at_rest", - "kafka_cluster_encryption_at_rest_uses_cmk", + "iam_no_expired_server_certificates_stored", "kafka_cluster_in_transit_encryption_enabled", "kafka_cluster_mutual_tls_authentication_enabled", "kafka_connector_in_transit_encryption_enabled", @@ -2259,6 +2058,7 @@ "opensearch_service_domains_https_communications_enforced", "opensearch_service_domains_node_to_node_encryption_enabled", "rds_cluster_storage_encrypted", + "rds_instance_certificate_expiration", "rds_instance_storage_encrypted", "rds_instance_transport_encrypted", "rds_snapshots_encrypted", @@ -2268,11 +2068,11 @@ "s3_bucket_secure_transport_policy", "sagemaker_notebook_instance_encryption_enabled", "sagemaker_training_jobs_intercontainer_encryption_enabled", - "sagemaker_training_jobs_volume_and_output_encryption_enabled", "sns_subscription_not_using_http_endpoints", "sqs_queues_server_side_encryption_enabled", "ssm_document_secrets", - "transfer_server_in_transit_encryption_enabled" + "transfer_server_in_transit_encryption_enabled", + "workspaces_volume_encryption_enabled" ], "Attributes": [ { @@ -2308,40 +2108,30 @@ "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": [ - "acm_certificates_expiration_check", - "acm_certificates_transparency_logs_enabled", - "athena_workgroup_encryption", "backup_vaults_encrypted", "bedrock_model_invocation_logs_encryption_enabled", - "cloudfront_distributions_custom_ssl_certificate", - "cloudfront_distributions_https_enabled", - "cloudfront_distributions_https_sni_enabled", - "cloudfront_distributions_origin_traffic_encrypted", - "cloudfront_distributions_using_deprecated_ssl_protocols", "cloudtrail_kms_encryption_enabled", "cloudwatch_log_group_kms_encryption_enabled", - "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", - "dms_endpoint_ssl_enabled", "dynamodb_tables_kms_cmk_encryption_enabled", "eks_cluster_kms_cmk_encryption_in_secrets_enabled", - "iam_no_expired_server_certificates_stored", + "iam_no_root_access_key", "iam_rotate_access_key_90_days", "iam_user_accesskey_unused", "iam_user_no_setup_initial_access_key", "iam_user_two_active_access_key", + "kafka_cluster_encryption_at_rest_uses_cmk", "kms_cmk_are_used", "kms_cmk_not_deleted_unintentionally", "kms_cmk_rotation_enabled", "kms_key_not_publicly_accessible", - "rds_instance_certificate_expiration", "s3_bucket_kms_encryption", + "sagemaker_training_jobs_volume_and_output_encryption_enabled", "secretsmanager_automatic_rotation_enabled", "secretsmanager_not_publicly_accessible", "secretsmanager_secret_rotated_periodically", "secretsmanager_secret_unused", "sns_topics_kms_encryption_at_rest_enabled", - "storagegateway_fileshare_encryption_enabled", - "workspaces_volume_encryption_enabled" + "storagegateway_fileshare_encryption_enabled" ], "Attributes": [ { @@ -2498,10 +2288,7 @@ "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": [ - "codeartifact_packages_external_public_publishing_disabled", - "ecr_repositories_tag_immutability" - ], + "Checks": [], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -2587,7 +2374,6 @@ "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": [ - "acm_certificates_expiration_check", "apigateway_restapi_tracing_enabled", "autoscaling_group_capacity_rebalance_enabled", "autoscaling_group_elb_health_check_enabled", @@ -2598,14 +2384,18 @@ "cloudfront_distributions_s3_origin_non_existent_bucket", "directconnect_connection_redundancy", "directconnect_virtual_interface_redundancy", - "dlm_ebs_snapshot_lifecycle_policy_exists", + "directoryservice_directory_snapshots_limit", "dms_instance_multi_az_enabled", "documentdb_cluster_deletion_protection", "documentdb_cluster_multi_az_enabled", "dynamodb_accelerator_cluster_multi_az", "dynamodb_table_autoscaling_enabled", "dynamodb_table_deletion_protection_enabled", + "ec2_elastic_ip_unassigned", "ec2_instance_detailed_monitoring_enabled", + "ec2_instance_older_than_specific_days", + "ec2_instance_paravirtual_type", + "ecr_repositories_lifecycle_policy_enabled", "ecs_cluster_container_insights_enabled", "ecs_task_definitions_containers_readonly_access", "ecs_task_definitions_logging_block_mode", @@ -2618,10 +2408,11 @@ "elbv2_cross_zone_load_balancing_enabled", "elbv2_deletion_protection", "elbv2_is_in_multiple_az", + "eventbridge_global_endpoint_event_replication_enabled", "fsx_windows_file_system_multi_az_enabled", - "iam_no_expired_server_certificates_stored", "kafka_cluster_enhanced_monitoring_enabled", "kms_cmk_not_deleted_unintentionally", + "lightsail_static_ip_unused", "mq_broker_active_deployment_mode", "mq_broker_cluster_deployment_mode", "neptune_cluster_deletion_protection", @@ -2632,21 +2423,13 @@ "opensearch_service_domains_fault_tolerant_master_nodes", "rds_cluster_deletion_protection", "rds_cluster_multi_az", - "rds_instance_certificate_expiration", "rds_instance_deletion_protection", - "rds_instance_deprecated_engine_version", + "rds_instance_enhanced_monitoring_enabled", "rds_instance_multi_az", "redshift_cluster_multi_az_enabled", + "resourceexplorer2_indexes_found", "s3_bucket_cross_region_replication", - "s3_bucket_no_mfa_delete", - "s3_bucket_object_lock", - "s3_bucket_object_versioning", "sagemaker_endpoint_config_prod_variant_instances", - "shield_advanced_protection_in_classic_load_balancers", - "shield_advanced_protection_in_global_accelerators", - "shield_advanced_protection_in_internet_facing_load_balancers", - "trustedadvisor_errors_and_warnings", - "trustedadvisor_premium_support_plan_subscribed", "vpc_different_regions", "vpc_endpoint_multi_az_enabled", "vpc_subnet_different_az", @@ -2688,9 +2471,9 @@ "backup_plans_exist", "backup_recovery_point_encrypted", "backup_reportplans_exist", + "backup_vaults_encrypted", "backup_vaults_exist", "cloudfront_distributions_multiple_origin_failover_configured", - "directoryservice_directory_snapshots_limit", "dlm_ebs_snapshot_lifecycle_policy_exists", "documentdb_cluster_backup_enabled", "dynamodb_table_protected_by_backup_plan", @@ -2699,22 +2482,16 @@ "ec2_ebs_volume_snapshots_exists", "efs_have_backup_enabled", "elasticache_redis_cluster_backup_enabled", - "fsx_file_system_copy_tags_to_backups_enabled", "lightsail_instance_automated_snapshots", "neptune_cluster_backup_enabled", - "neptune_cluster_copy_tags_to_snapshots", - "neptune_cluster_public_snapshot", - "neptune_cluster_snapshot_encrypted", "rds_cluster_backtrack_enabled", - "rds_cluster_copy_tags_to_snapshots", "rds_cluster_protected_by_backup_plan", "rds_instance_backup_enabled", - "rds_instance_copy_tags_to_snapshots", "rds_instance_protected_by_backup_plan", - "rds_snapshots_encrypted", - "rds_snapshots_public_access", "redshift_cluster_automated_snapshot", - "s3_bucket_lifecycle_enabled" + "s3_bucket_lifecycle_enabled", + "s3_bucket_object_lock", + "s3_bucket_object_versioning" ], "Attributes": [ { @@ -2756,7 +2533,6 @@ "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", "bedrock_model_invocation_logging_enabled", "bedrock_model_invocation_logs_encryption_enabled", - "cloudformation_stack_outputs_find_secrets", "cloudfront_distributions_logging_enabled", "cloudtrail_bucket_requires_mfa_delete", "cloudtrail_cloudwatch_logging_enabled", @@ -2776,14 +2552,11 @@ "cloudwatch_changes_to_network_gateways_alarm_configured", "cloudwatch_changes_to_network_route_tables_alarm_configured", "cloudwatch_changes_to_vpcs_alarm_configured", - "cloudwatch_log_group_kms_encryption_enabled", "cloudwatch_log_group_no_critical_pii_in_logs", "cloudwatch_log_group_no_secrets_in_logs", - "cloudwatch_log_group_not_publicly_accessible", "cloudwatch_log_group_retention_policy_specific_days_enabled", "codebuild_project_logging_enabled", "codebuild_project_s3_logs_encrypted", - "config_recorder_all_regions_enabled", "datasync_task_logging_enabled", "directoryservice_directory_log_forwarding_enabled", "directoryservice_directory_monitor_notifications", @@ -2795,22 +2568,14 @@ "elasticbeanstalk_environment_cloudwatch_logging_enabled", "elb_logging_enabled", "elbv2_logging_enabled", - "eventbridge_global_endpoint_event_replication_enabled", - "glue_development_endpoints_cloudwatch_logs_encryption_enabled", - "glue_etl_jobs_cloudwatch_logs_encryption_enabled", "glue_etl_jobs_logging_enabled", - "kafka_cluster_enhanced_monitoring_enabled", + "guardduty_eks_audit_log_enabled", "mq_broker_logging_enabled", "neptune_cluster_integration_cloudwatch_logs", "networkfirewall_logging_enabled", "opensearch_service_domains_audit_logging_enabled", "opensearch_service_domains_cloudwatch_logging_enabled", - "rds_cluster_critical_event_subscription", "rds_cluster_integration_cloudwatch_logs", - "rds_instance_critical_event_subscription", - "rds_instance_enhanced_monitoring_enabled", - "rds_instance_event_subscription_parameter_groups", - "rds_instance_event_subscription_security_groups", "rds_instance_integration_cloudwatch_logs", "redshift_cluster_audit_logging", "route53_public_hosted_zones_cloudwatch_logging_enabled", @@ -2898,7 +2663,7 @@ "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?'" + "Is regular inspection conducted to ensure that time synchronization is functioning properly?" ], "RelatedRegulations": [], "AuditEvidence": [ @@ -2954,8 +2719,6 @@ "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": [ "apigateway_restapi_waf_acl_attached", - "autoscaling_group_multiple_az", - "autoscaling_group_multiple_instance_types", "cloudfront_distributions_using_waf", "cloudtrail_bucket_requires_mfa_delete", "cloudtrail_cloudwatch_logging_enabled", @@ -3001,8 +2764,6 @@ "dms_instance_minor_version_upgrade_enabled", "dms_instance_multi_az_enabled", "dms_instance_no_public_access", - "ec2_client_vpn_endpoint_connection_logging_enabled", - "ec2_instance_managed_by_ssm", "elbv2_waf_acl_attached", "fms_policy_compliant", "guardduty_centrally_managed", @@ -3037,7 +2798,6 @@ "shield_advanced_protection_in_internet_facing_load_balancers", "shield_advanced_protection_in_route53_hosted_zones", "trustedadvisor_errors_and_warnings", - "trustedadvisor_premium_support_plan_subscribed", "waf_global_rule_with_conditions", "waf_global_rulegroup_not_empty", "waf_global_webacl_logging_enabled", @@ -3105,10 +2865,8 @@ "apigateway_restapi_logging_enabled", "apigateway_restapi_public", "apigateway_restapi_public_with_authorizer", - "apigateway_restapi_tracing_enabled", "apigateway_restapi_waf_acl_attached", "apigatewayv2_api_access_logging_enabled", - "apigatewayv2_api_authorizers_enabled", "appstream_fleet_default_internet_access_disabled", "appstream_fleet_maximum_session_duration", "appstream_fleet_session_disconnect_timeout", @@ -3116,13 +2874,8 @@ "athena_workgroup_encryption", "athena_workgroup_enforce_configuration", "athena_workgroup_logging_enabled", - "autoscaling_group_capacity_rebalance_enabled", - "autoscaling_group_elb_health_check_enabled", "autoscaling_group_launch_configuration_no_public_ip", "autoscaling_group_launch_configuration_requires_imdsv2", - "autoscaling_group_multiple_az", - "autoscaling_group_multiple_instance_types", - "autoscaling_group_using_ec2_launch_template", "awslambda_function_inside_vpc", "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", "awslambda_function_no_secrets_in_code", @@ -3131,19 +2884,14 @@ "awslambda_function_url_cors_policy", "awslambda_function_url_public", "awslambda_function_using_supported_runtimes", - "awslambda_function_vpc_multi_az", - "backup_plans_exist", "backup_recovery_point_encrypted", - "backup_reportplans_exist", "backup_vaults_encrypted", - "backup_vaults_exist", "bedrock_agent_guardrail_enabled", "bedrock_guardrail_prompt_attack_filter_enabled", "bedrock_guardrail_sensitive_information_filter_enabled", "bedrock_model_invocation_logging_enabled", "bedrock_model_invocation_logs_encryption_enabled", "cloudformation_stack_outputs_find_secrets", - "cloudformation_stacks_termination_protection_enabled", "cloudfront_distributions_custom_ssl_certificate", "cloudfront_distributions_default_root_object", "cloudfront_distributions_field_level_encryption_enabled", @@ -3151,10 +2899,8 @@ "cloudfront_distributions_https_enabled", "cloudfront_distributions_https_sni_enabled", "cloudfront_distributions_logging_enabled", - "cloudfront_distributions_multiple_origin_failover_configured", "cloudfront_distributions_origin_traffic_encrypted", "cloudfront_distributions_s3_origin_access_control", - "cloudfront_distributions_s3_origin_non_existent_bucket", "cloudfront_distributions_using_deprecated_ssl_protocols", "cloudfront_distributions_using_waf", "cloudtrail_bucket_requires_mfa_delete", @@ -3163,7 +2909,6 @@ "cloudtrail_kms_encryption_enabled", "cloudtrail_log_file_validation_enabled", "cloudtrail_logs_s3_bucket_access_logging_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", "cloudtrail_multi_region_enabled", "cloudtrail_multi_region_enabled_logging_management_events", "cloudtrail_s3_dataevents_read_enabled", @@ -3197,10 +2942,8 @@ "codeartifact_packages_external_public_publishing_disabled", "codebuild_project_logging_enabled", "codebuild_project_no_secrets_in_variables", - "codebuild_project_older_90_days", "codebuild_project_s3_logs_encrypted", "codebuild_project_source_repo_url_no_sensitive_credentials", - "codebuild_project_user_controlled_buildspec", "codebuild_report_group_export_encrypted", "cognito_identity_pool_guest_access_disabled", "cognito_user_pool_advanced_security_enabled", @@ -3221,37 +2964,24 @@ "config_recorder_all_regions_enabled", "config_recorder_using_aws_service_role", "datasync_task_logging_enabled", - "directconnect_connection_redundancy", - "directconnect_virtual_interface_redundancy", "directoryservice_directory_log_forwarding_enabled", "directoryservice_directory_monitor_notifications", - "directoryservice_directory_snapshots_limit", "directoryservice_ldap_certificate_expiration", "directoryservice_radius_server_security_protocol", "directoryservice_supported_mfa_radius_enabled", - "dlm_ebs_snapshot_lifecycle_policy_exists", "dms_endpoint_mongodb_authentication_enabled", "dms_endpoint_neptune_iam_authorization_enabled", "dms_endpoint_ssl_enabled", "dms_instance_minor_version_upgrade_enabled", - "dms_instance_multi_az_enabled", "dms_instance_no_public_access", - "documentdb_cluster_backup_enabled", "documentdb_cluster_cloudwatch_log_export", - "documentdb_cluster_deletion_protection", - "documentdb_cluster_multi_az_enabled", "documentdb_cluster_public_snapshot", "documentdb_cluster_storage_encrypted", "drs_job_exist", "dynamodb_accelerator_cluster_encryption_enabled", "dynamodb_accelerator_cluster_in_transit_encryption_enabled", - "dynamodb_accelerator_cluster_multi_az", - "dynamodb_table_autoscaling_enabled", "dynamodb_table_cross_account_access", - "dynamodb_table_deletion_protection_enabled", - "dynamodb_table_protected_by_backup_plan", "dynamodb_tables_kms_cmk_encryption_enabled", - "dynamodb_tables_pitr_enabled", "ec2_ami_public", "ec2_client_vpn_endpoint_connection_logging_enabled", "ec2_ebs_default_encryption", @@ -3259,17 +2989,11 @@ "ec2_ebs_snapshot_account_block_public_access", "ec2_ebs_snapshots_encrypted", "ec2_ebs_volume_encryption", - "ec2_ebs_volume_protected_by_backup_plan", - "ec2_ebs_volume_snapshots_exists", "ec2_elastic_ip_shodan", - "ec2_elastic_ip_unassigned", "ec2_instance_account_imdsv2_enabled", - "ec2_instance_detailed_monitoring_enabled", "ec2_instance_imdsv2_enabled", "ec2_instance_internet_facing_with_instance_profile", "ec2_instance_managed_by_ssm", - "ec2_instance_older_than_specific_days", - "ec2_instance_paravirtual_type", "ec2_instance_port_cassandra_exposed_to_internet", "ec2_instance_port_cifs_exposed_to_internet", "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", @@ -3321,17 +3045,12 @@ "ec2_securitygroup_with_many_ingress_egress_rules", "ec2_transitgateway_auto_accept_vpc_attachments", "ecr_registry_scan_images_on_push_enabled", - "ecr_repositories_lifecycle_policy_enabled", "ecr_repositories_not_publicly_accessible", "ecr_repositories_scan_vulnerabilities_in_latest_image", - "ecr_repositories_tag_immutability", - "ecs_cluster_container_insights_enabled", "ecs_service_fargate_latest_platform_version", "ecs_service_no_assign_public_ip", - "ecs_task_definitions_containers_readonly_access", "ecs_task_definitions_host_namespace_not_shared", "ecs_task_definitions_host_networking_mode_users", - "ecs_task_definitions_logging_block_mode", "ecs_task_definitions_logging_enabled", "ecs_task_definitions_no_environment_secrets", "ecs_task_definitions_no_privileged_containers", @@ -3339,7 +3058,6 @@ "efs_access_point_enforce_root_directory", "efs_access_point_enforce_user_identity", "efs_encryption_at_rest_enabled", - "efs_have_backup_enabled", "efs_mount_target_not_publicly_accessible", "efs_not_publicly_accessible", "eks_cluster_kms_cmk_encryption_in_secrets_enabled", @@ -3350,30 +3068,20 @@ "eks_control_plane_logging_all_types_enabled", "elasticache_cluster_uses_public_subnet", "elasticache_redis_cluster_auto_minor_version_upgrades", - "elasticache_redis_cluster_automatic_failover_enabled", - "elasticache_redis_cluster_backup_enabled", "elasticache_redis_cluster_in_transit_encryption_enabled", - "elasticache_redis_cluster_multi_az_enabled", "elasticache_redis_cluster_rest_encryption_enabled", "elasticache_redis_replication_group_auth_enabled", "elasticbeanstalk_environment_cloudwatch_logging_enabled", - "elasticbeanstalk_environment_enhanced_health_reporting", "elasticbeanstalk_environment_managed_updates_enabled", - "elb_connection_draining_enabled", - "elb_cross_zone_load_balancing_enabled", "elb_desync_mitigation_mode", "elb_insecure_ssl_ciphers", "elb_internet_facing", - "elb_is_in_multiple_az", "elb_logging_enabled", "elb_ssl_listeners", "elb_ssl_listeners_use_acm_certificate", - "elbv2_cross_zone_load_balancing_enabled", - "elbv2_deletion_protection", "elbv2_desync_mitigation_mode", "elbv2_insecure_ssl_ciphers", "elbv2_internet_facing", - "elbv2_is_in_multiple_az", "elbv2_listeners_underneath", "elbv2_logging_enabled", "elbv2_nlb_tls_termination_enabled", @@ -3384,12 +3092,8 @@ "emr_cluster_publicly_accesible", "eventbridge_bus_cross_account_access", "eventbridge_bus_exposed", - "eventbridge_global_endpoint_event_replication_enabled", "eventbridge_schema_registry_cross_account_access", "fms_policy_compliant", - "fsx_file_system_copy_tags_to_backups_enabled", - "fsx_file_system_copy_tags_to_volumes_enabled", - "fsx_windows_file_system_multi_az_enabled", "glacier_vaults_policy_public_access", "glue_data_catalogs_connection_passwords_encryption_enabled", "glue_data_catalogs_metadata_encryption_enabled", @@ -3457,7 +3161,6 @@ "inspector2_active_findings_exist", "inspector2_is_enabled", "kafka_cluster_encryption_at_rest_uses_cmk", - "kafka_cluster_enhanced_monitoring_enabled", "kafka_cluster_in_transit_encryption_enabled", "kafka_cluster_is_public", "kafka_cluster_mutual_tls_authentication_enabled", @@ -3466,25 +3169,16 @@ "kafka_connector_in_transit_encryption_enabled", "kinesis_stream_encrypted_at_rest", "kms_cmk_are_used", - "kms_cmk_not_deleted_unintentionally", "kms_cmk_rotation_enabled", "kms_key_not_publicly_accessible", "lightsail_database_public", - "lightsail_instance_automated_snapshots", "lightsail_instance_public", - "lightsail_static_ip_unused", "macie_automated_sensitive_data_discovery_enabled", "macie_is_enabled", - "mq_broker_active_deployment_mode", "mq_broker_auto_minor_version_upgrades", - "mq_broker_cluster_deployment_mode", "mq_broker_logging_enabled", - "neptune_cluster_backup_enabled", - "neptune_cluster_copy_tags_to_snapshots", - "neptune_cluster_deletion_protection", "neptune_cluster_iam_authentication_enabled", "neptune_cluster_integration_cloudwatch_logs", - "neptune_cluster_multi_az", "neptune_cluster_public_snapshot", "neptune_cluster_snapshot_encrypted", "neptune_cluster_storage_encrypted", @@ -3492,15 +3186,12 @@ "networkfirewall_deletion_protection", "networkfirewall_in_all_vpc", "networkfirewall_logging_enabled", - "networkfirewall_multi_az", "networkfirewall_policy_default_action_fragmented_packets", "networkfirewall_policy_default_action_full_packets", "networkfirewall_policy_rule_group_associated", "opensearch_service_domains_audit_logging_enabled", "opensearch_service_domains_cloudwatch_logging_enabled", "opensearch_service_domains_encryption_at_rest_enabled", - "opensearch_service_domains_fault_tolerant_data_nodes", - "opensearch_service_domains_fault_tolerant_master_nodes", "opensearch_service_domains_https_communications_enforced", "opensearch_service_domains_internal_user_database_enabled", "opensearch_service_domains_node_to_node_encryption_enabled", @@ -3511,52 +3202,33 @@ "organizations_delegated_administrators", "organizations_opt_out_ai_services_policy", "organizations_scp_check_deny_regions", - "organizations_tags_policies_enabled_and_attached", - "rds_cluster_backtrack_enabled", - "rds_cluster_copy_tags_to_snapshots", "rds_cluster_critical_event_subscription", - "rds_cluster_default_admin", - "rds_cluster_deletion_protection", "rds_cluster_iam_authentication_enabled", "rds_cluster_integration_cloudwatch_logs", "rds_cluster_minor_version_upgrade_enabled", - "rds_cluster_multi_az", "rds_cluster_non_default_port", - "rds_cluster_protected_by_backup_plan", "rds_cluster_storage_encrypted", - "rds_instance_backup_enabled", "rds_instance_certificate_expiration", - "rds_instance_copy_tags_to_snapshots", "rds_instance_critical_event_subscription", - "rds_instance_default_admin", - "rds_instance_deletion_protection", "rds_instance_deprecated_engine_version", - "rds_instance_enhanced_monitoring_enabled", "rds_instance_event_subscription_parameter_groups", "rds_instance_event_subscription_security_groups", "rds_instance_iam_authentication_enabled", "rds_instance_inside_vpc", "rds_instance_integration_cloudwatch_logs", "rds_instance_minor_version_upgrade_enabled", - "rds_instance_multi_az", "rds_instance_no_public_access", "rds_instance_non_default_port", - "rds_instance_protected_by_backup_plan", "rds_instance_storage_encrypted", "rds_instance_transport_encrypted", "rds_snapshots_encrypted", "rds_snapshots_public_access", "redshift_cluster_audit_logging", - "redshift_cluster_automated_snapshot", "redshift_cluster_automatic_upgrades", "redshift_cluster_encrypted_at_rest", "redshift_cluster_enhanced_vpc_routing", "redshift_cluster_in_transit_encryption_enabled", - "redshift_cluster_multi_az_enabled", - "redshift_cluster_non_default_database_name", - "redshift_cluster_non_default_username", "redshift_cluster_public_access", - "resourceexplorer2_indexes_found", "route53_dangling_ip_subdomain_takeover", "route53_domains_privacy_protection_enabled", "route53_domains_transferlock_enabled", @@ -3565,15 +3237,11 @@ "s3_account_level_public_access_blocks", "s3_bucket_acl_prohibited", "s3_bucket_cross_account_access", - "s3_bucket_cross_region_replication", "s3_bucket_default_encryption", "s3_bucket_event_notifications_enabled", "s3_bucket_kms_encryption", "s3_bucket_level_public_access_block", - "s3_bucket_lifecycle_enabled", "s3_bucket_no_mfa_delete", - "s3_bucket_object_lock", - "s3_bucket_object_versioning", "s3_bucket_policy_public_write_access", "s3_bucket_public_access", "s3_bucket_public_list_acl", @@ -3581,7 +3249,6 @@ "s3_bucket_secure_transport_policy", "s3_bucket_server_access_logging_enabled", "s3_multi_region_access_point_public_access_block", - "sagemaker_endpoint_config_prod_variant_instances", "sagemaker_models_network_isolation_enabled", "sagemaker_models_vpc_settings_configured", "sagemaker_notebook_instance_encryption_enabled", @@ -3616,18 +3283,13 @@ "storagegateway_fileshare_encryption_enabled", "transfer_server_in_transit_encryption_enabled", "trustedadvisor_errors_and_warnings", - "trustedadvisor_premium_support_plan_subscribed", - "vpc_different_regions", "vpc_endpoint_connections_trust_boundaries", "vpc_endpoint_for_ec2_enabled", - "vpc_endpoint_multi_az_enabled", "vpc_endpoint_services_allowed_principals_trust_boundaries", "vpc_flow_logs_enabled", "vpc_peering_routing_tables_with_least_privilege", - "vpc_subnet_different_az", "vpc_subnet_no_public_ip_by_default", "vpc_subnet_separate_private_public", - "vpc_vpn_connection_tunnels_up", "waf_global_rule_with_conditions", "waf_global_rulegroup_not_empty", "waf_global_webacl_logging_enabled", @@ -3676,41 +3338,7 @@ "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": [ - "acm_certificates_expiration_check", - "acm_certificates_transparency_logs_enabled", - "acm_certificates_with_secure_key_algorithms", - "apigateway_restapi_client_certificate_enabled", - "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", - "apigateway_restapi_waf_acl_attached", - "cloudfront_distributions_s3_origin_non_existent_bucket", - "cloudfront_distributions_using_waf", - "cognito_user_pool_waf_acl_attached", - "elb_desync_mitigation_mode", - "elb_insecure_ssl_ciphers", - "elb_internet_facing", - "elb_ssl_listeners", - "elb_ssl_listeners_use_acm_certificate", - "elbv2_desync_mitigation_mode", - "elbv2_insecure_ssl_ciphers", - "elbv2_internet_facing", - "elbv2_listeners_underneath", - "elbv2_nlb_tls_termination_enabled", - "elbv2_ssl_listeners", - "elbv2_waf_acl_attached", - "lightsail_database_public", - "lightsail_instance_public", - "lightsail_static_ip_unused", - "route53_dangling_ip_subdomain_takeover", - "route53_domains_privacy_protection_enabled", - "shield_advanced_protection_in_associated_elastic_ips", - "shield_advanced_protection_in_classic_load_balancers", - "shield_advanced_protection_in_cloudfront_distributions", - "shield_advanced_protection_in_global_accelerators", - "shield_advanced_protection_in_internet_facing_load_balancers", - "shield_advanced_protection_in_route53_hosted_zones" - ], + "Checks": [], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -3952,12 +3580,6 @@ "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": [ - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered", - "account_security_questions_are_registered_in_the_aws_account", - "acm_certificates_with_secure_key_algorithms", - "guardduty_centrally_managed", "iam_support_role_created", "ssmincidents_enabled_with_plans" ], @@ -3996,557 +3618,9 @@ "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": [ - "accessanalyzer_enabled", - "accessanalyzer_enabled_without_findings", - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered", - "account_security_questions_are_registered_in_the_aws_account", - "acm_certificates_expiration_check", - "acm_certificates_transparency_logs_enabled", - "acm_certificates_with_secure_key_algorithms", - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_cache_encrypted", - "apigateway_restapi_client_certificate_enabled", - "apigateway_restapi_logging_enabled", - "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", - "apigateway_restapi_tracing_enabled", - "apigateway_restapi_waf_acl_attached", - "apigatewayv2_api_access_logging_enabled", - "apigatewayv2_api_authorizers_enabled", - "appstream_fleet_default_internet_access_disabled", - "appstream_fleet_maximum_session_duration", - "appstream_fleet_session_disconnect_timeout", - "appstream_fleet_session_idle_disconnect_timeout", - "athena_workgroup_encryption", - "athena_workgroup_enforce_configuration", - "athena_workgroup_logging_enabled", - "autoscaling_group_capacity_rebalance_enabled", - "autoscaling_group_elb_health_check_enabled", - "autoscaling_group_launch_configuration_no_public_ip", - "autoscaling_group_launch_configuration_requires_imdsv2", - "autoscaling_group_multiple_az", - "autoscaling_group_multiple_instance_types", - "autoscaling_group_using_ec2_launch_template", - "awslambda_function_inside_vpc", - "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", - "awslambda_function_no_secrets_in_code", - "awslambda_function_no_secrets_in_variables", - "awslambda_function_not_publicly_accessible", - "awslambda_function_url_cors_policy", - "awslambda_function_url_public", - "awslambda_function_using_supported_runtimes", - "awslambda_function_vpc_multi_az", - "backup_plans_exist", - "backup_recovery_point_encrypted", - "backup_reportplans_exist", - "backup_vaults_encrypted", - "backup_vaults_exist", - "bedrock_agent_guardrail_enabled", - "bedrock_guardrail_prompt_attack_filter_enabled", - "bedrock_guardrail_sensitive_information_filter_enabled", - "bedrock_model_invocation_logging_enabled", - "bedrock_model_invocation_logs_encryption_enabled", - "cloudformation_stack_outputs_find_secrets", - "cloudformation_stacks_termination_protection_enabled", - "cloudfront_distributions_custom_ssl_certificate", - "cloudfront_distributions_default_root_object", - "cloudfront_distributions_field_level_encryption_enabled", - "cloudfront_distributions_geo_restrictions_enabled", - "cloudfront_distributions_https_enabled", - "cloudfront_distributions_https_sni_enabled", - "cloudfront_distributions_logging_enabled", - "cloudfront_distributions_multiple_origin_failover_configured", - "cloudfront_distributions_origin_traffic_encrypted", - "cloudfront_distributions_s3_origin_access_control", - "cloudfront_distributions_s3_origin_non_existent_bucket", - "cloudfront_distributions_using_deprecated_ssl_protocols", - "cloudfront_distributions_using_waf", - "cloudtrail_bucket_requires_mfa_delete", - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_insights_exist", - "cloudtrail_kms_encryption_enabled", - "cloudtrail_log_file_validation_enabled", - "cloudtrail_logs_s3_bucket_access_logging_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_multi_region_enabled", - "cloudtrail_multi_region_enabled_logging_management_events", - "cloudtrail_s3_dataevents_read_enabled", - "cloudtrail_s3_dataevents_write_enabled", - "cloudtrail_threat_detection_enumeration", - "cloudtrail_threat_detection_llm_jacking", - "cloudtrail_threat_detection_privilege_escalation", - "cloudwatch_alarm_actions_alarm_state_configured", - "cloudwatch_alarm_actions_enabled", - "cloudwatch_changes_to_network_acls_alarm_configured", - "cloudwatch_changes_to_network_gateways_alarm_configured", - "cloudwatch_changes_to_network_route_tables_alarm_configured", - "cloudwatch_changes_to_vpcs_alarm_configured", - "cloudwatch_cross_account_sharing_disabled", - "cloudwatch_log_group_kms_encryption_enabled", - "cloudwatch_log_group_no_critical_pii_in_logs", - "cloudwatch_log_group_no_secrets_in_logs", - "cloudwatch_log_group_not_publicly_accessible", - "cloudwatch_log_group_retention_policy_specific_days_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudwatch_log_metric_filter_authentication_failures", - "cloudwatch_log_metric_filter_aws_organizations_changes", - "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", - "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", - "cloudwatch_log_metric_filter_policy_changes", - "cloudwatch_log_metric_filter_root_usage", - "cloudwatch_log_metric_filter_security_group_changes", - "cloudwatch_log_metric_filter_sign_in_without_mfa", - "cloudwatch_log_metric_filter_unauthorized_api_calls", - "codeartifact_packages_external_public_publishing_disabled", - "codebuild_project_logging_enabled", - "codebuild_project_no_secrets_in_variables", - "codebuild_project_older_90_days", - "codebuild_project_s3_logs_encrypted", - "codebuild_project_source_repo_url_no_sensitive_credentials", - "codebuild_project_user_controlled_buildspec", - "codebuild_report_group_export_encrypted", - "cognito_identity_pool_guest_access_disabled", - "cognito_user_pool_advanced_security_enabled", - "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", - "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", - "cognito_user_pool_client_prevent_user_existence_errors", - "cognito_user_pool_client_token_revocation_enabled", - "cognito_user_pool_deletion_protection_enabled", - "cognito_user_pool_mfa_enabled", - "cognito_user_pool_password_policy_lowercase", - "cognito_user_pool_password_policy_minimum_length_14", - "cognito_user_pool_password_policy_number", - "cognito_user_pool_password_policy_symbol", - "cognito_user_pool_password_policy_uppercase", - "cognito_user_pool_self_registration_disabled", - "cognito_user_pool_temporary_password_expiration", - "cognito_user_pool_waf_acl_attached", - "config_recorder_all_regions_enabled", - "config_recorder_using_aws_service_role", - "datasync_task_logging_enabled", - "directconnect_connection_redundancy", - "directconnect_virtual_interface_redundancy", - "directoryservice_directory_log_forwarding_enabled", - "directoryservice_directory_monitor_notifications", - "directoryservice_directory_snapshots_limit", - "directoryservice_ldap_certificate_expiration", - "directoryservice_radius_server_security_protocol", - "directoryservice_supported_mfa_radius_enabled", - "dlm_ebs_snapshot_lifecycle_policy_exists", - "dms_endpoint_mongodb_authentication_enabled", - "dms_endpoint_neptune_iam_authorization_enabled", - "dms_endpoint_ssl_enabled", - "dms_instance_minor_version_upgrade_enabled", - "dms_instance_multi_az_enabled", - "dms_instance_no_public_access", - "documentdb_cluster_backup_enabled", - "documentdb_cluster_cloudwatch_log_export", - "documentdb_cluster_deletion_protection", - "documentdb_cluster_multi_az_enabled", - "documentdb_cluster_public_snapshot", - "documentdb_cluster_storage_encrypted", - "drs_job_exist", - "dynamodb_accelerator_cluster_encryption_enabled", - "dynamodb_accelerator_cluster_in_transit_encryption_enabled", - "dynamodb_accelerator_cluster_multi_az", - "dynamodb_table_autoscaling_enabled", - "dynamodb_table_cross_account_access", - "dynamodb_table_deletion_protection_enabled", - "dynamodb_table_protected_by_backup_plan", - "dynamodb_tables_kms_cmk_encryption_enabled", - "dynamodb_tables_pitr_enabled", - "ec2_ami_public", - "ec2_client_vpn_endpoint_connection_logging_enabled", - "ec2_ebs_default_encryption", - "ec2_ebs_public_snapshot", - "ec2_ebs_snapshot_account_block_public_access", - "ec2_ebs_snapshots_encrypted", - "ec2_ebs_volume_encryption", - "ec2_ebs_volume_protected_by_backup_plan", - "ec2_ebs_volume_snapshots_exists", - "ec2_elastic_ip_shodan", - "ec2_elastic_ip_unassigned", - "ec2_instance_account_imdsv2_enabled", - "ec2_instance_detailed_monitoring_enabled", - "ec2_instance_imdsv2_enabled", - "ec2_instance_internet_facing_with_instance_profile", - "ec2_instance_managed_by_ssm", - "ec2_instance_older_than_specific_days", - "ec2_instance_paravirtual_type", - "ec2_instance_port_cassandra_exposed_to_internet", - "ec2_instance_port_cifs_exposed_to_internet", - "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", - "ec2_instance_port_ftp_exposed_to_internet", - "ec2_instance_port_kafka_exposed_to_internet", - "ec2_instance_port_kerberos_exposed_to_internet", - "ec2_instance_port_ldap_exposed_to_internet", - "ec2_instance_port_memcached_exposed_to_internet", - "ec2_instance_port_mongodb_exposed_to_internet", - "ec2_instance_port_mysql_exposed_to_internet", - "ec2_instance_port_oracle_exposed_to_internet", - "ec2_instance_port_postgresql_exposed_to_internet", - "ec2_instance_port_rdp_exposed_to_internet", - "ec2_instance_port_redis_exposed_to_internet", - "ec2_instance_port_sqlserver_exposed_to_internet", - "ec2_instance_port_ssh_exposed_to_internet", - "ec2_instance_port_telnet_exposed_to_internet", - "ec2_instance_profile_attached", - "ec2_instance_public_ip", - "ec2_instance_secrets_user_data", - "ec2_instance_uses_single_eni", - "ec2_launch_template_no_public_ip", - "ec2_launch_template_no_secrets", - "ec2_networkacl_allow_ingress_any_port", - "ec2_networkacl_allow_ingress_tcp_port_22", - "ec2_networkacl_allow_ingress_tcp_port_3389", - "ec2_networkacl_unused", - "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", - "ec2_securitygroup_allow_ingress_from_internet_to_any_port", - "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", - "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", - "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_ingress_from_internet_to_tcp_port_sql_server_1433_1434", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", - "ec2_securitygroup_allow_wide_open_public_ipv4", - "ec2_securitygroup_default_restrict_traffic", - "ec2_securitygroup_from_launch_wizard", - "ec2_securitygroup_not_used", - "ec2_securitygroup_with_many_ingress_egress_rules", - "ec2_transitgateway_auto_accept_vpc_attachments", "ecr_registry_scan_images_on_push_enabled", - "ecr_repositories_lifecycle_policy_enabled", - "ecr_repositories_not_publicly_accessible", "ecr_repositories_scan_vulnerabilities_in_latest_image", - "ecr_repositories_tag_immutability", - "ecs_cluster_container_insights_enabled", - "ecs_service_fargate_latest_platform_version", - "ecs_service_no_assign_public_ip", - "ecs_task_definitions_containers_readonly_access", - "ecs_task_definitions_host_namespace_not_shared", - "ecs_task_definitions_host_networking_mode_users", - "ecs_task_definitions_logging_block_mode", - "ecs_task_definitions_logging_enabled", - "ecs_task_definitions_no_environment_secrets", - "ecs_task_definitions_no_privileged_containers", - "ecs_task_set_no_assign_public_ip", - "efs_access_point_enforce_root_directory", - "efs_access_point_enforce_user_identity", - "efs_encryption_at_rest_enabled", - "efs_have_backup_enabled", - "efs_mount_target_not_publicly_accessible", - "efs_not_publicly_accessible", - "eks_cluster_kms_cmk_encryption_in_secrets_enabled", - "eks_cluster_network_policy_enabled", - "eks_cluster_not_publicly_accessible", - "eks_cluster_private_nodes_enabled", - "eks_cluster_uses_a_supported_version", - "eks_control_plane_logging_all_types_enabled", - "elasticache_cluster_uses_public_subnet", - "elasticache_redis_cluster_auto_minor_version_upgrades", - "elasticache_redis_cluster_automatic_failover_enabled", - "elasticache_redis_cluster_backup_enabled", - "elasticache_redis_cluster_in_transit_encryption_enabled", - "elasticache_redis_cluster_multi_az_enabled", - "elasticache_redis_cluster_rest_encryption_enabled", - "elasticache_redis_replication_group_auth_enabled", - "elasticbeanstalk_environment_cloudwatch_logging_enabled", - "elasticbeanstalk_environment_enhanced_health_reporting", - "elasticbeanstalk_environment_managed_updates_enabled", - "elb_connection_draining_enabled", - "elb_cross_zone_load_balancing_enabled", - "elb_desync_mitigation_mode", - "elb_insecure_ssl_ciphers", - "elb_internet_facing", - "elb_is_in_multiple_az", - "elb_logging_enabled", - "elb_ssl_listeners", - "elb_ssl_listeners_use_acm_certificate", - "elbv2_cross_zone_load_balancing_enabled", - "elbv2_deletion_protection", - "elbv2_desync_mitigation_mode", - "elbv2_insecure_ssl_ciphers", - "elbv2_internet_facing", - "elbv2_is_in_multiple_az", - "elbv2_listeners_underneath", - "elbv2_logging_enabled", - "elbv2_nlb_tls_termination_enabled", - "elbv2_ssl_listeners", - "elbv2_waf_acl_attached", - "emr_cluster_account_public_block_enabled", - "emr_cluster_master_nodes_no_public_ip", - "emr_cluster_publicly_accesible", - "eventbridge_bus_cross_account_access", - "eventbridge_bus_exposed", - "eventbridge_global_endpoint_event_replication_enabled", - "eventbridge_schema_registry_cross_account_access", - "fms_policy_compliant", - "fsx_file_system_copy_tags_to_backups_enabled", - "fsx_file_system_copy_tags_to_volumes_enabled", - "fsx_windows_file_system_multi_az_enabled", - "glacier_vaults_policy_public_access", - "glue_data_catalogs_connection_passwords_encryption_enabled", - "glue_data_catalogs_metadata_encryption_enabled", - "glue_data_catalogs_not_publicly_accessible", - "glue_database_connections_ssl_enabled", - "glue_development_endpoints_cloudwatch_logs_encryption_enabled", - "glue_development_endpoints_job_bookmark_encryption_enabled", - "glue_development_endpoints_s3_encryption_enabled", - "glue_etl_jobs_amazon_s3_encryption_enabled", - "glue_etl_jobs_cloudwatch_logs_encryption_enabled", - "glue_etl_jobs_job_bookmark_encryption_enabled", - "glue_etl_jobs_logging_enabled", - "glue_ml_transform_encrypted_at_rest", - "guardduty_centrally_managed", - "guardduty_ec2_malware_protection_enabled", - "guardduty_eks_audit_log_enabled", - "guardduty_eks_runtime_monitoring_enabled", - "guardduty_is_enabled", - "guardduty_lambda_protection_enabled", - "guardduty_no_high_severity_findings", - "guardduty_rds_protection_enabled", - "guardduty_s3_protection_enabled", - "iam_administrator_access_with_mfa", - "iam_avoid_root_usage", - "iam_aws_attached_policy_no_administrative_privileges", - "iam_check_saml_providers_sts", - "iam_customer_attached_policy_no_administrative_privileges", - "iam_customer_unattached_policy_no_administrative_privileges", - "iam_group_administrator_access_policy", - "iam_inline_policy_allows_privilege_escalation", - "iam_inline_policy_no_administrative_privileges", - "iam_inline_policy_no_full_access_to_cloudtrail", - "iam_inline_policy_no_full_access_to_kms", - "iam_no_custom_policy_permissive_role_assumption", - "iam_no_expired_server_certificates_stored", - "iam_no_root_access_key", - "iam_password_policy_expires_passwords_within_90_days_or_less", - "iam_password_policy_lowercase", - "iam_password_policy_minimum_length_14", - "iam_password_policy_number", - "iam_password_policy_reuse_24", - "iam_password_policy_symbol", - "iam_password_policy_uppercase", - "iam_policy_allows_privilege_escalation", - "iam_policy_attached_only_to_group_or_roles", - "iam_policy_cloudshell_admin_not_attached", - "iam_policy_no_full_access_to_cloudtrail", - "iam_policy_no_full_access_to_kms", - "iam_role_administratoraccess_policy", - "iam_role_cross_account_readonlyaccess_policy", - "iam_role_cross_service_confused_deputy_prevention", - "iam_root_hardware_mfa_enabled", - "iam_root_mfa_enabled", - "iam_rotate_access_key_90_days", - "iam_securityaudit_role_created", - "iam_support_role_created", - "iam_user_accesskey_unused", - "iam_user_administrator_access_policy", - "iam_user_console_access_unused", - "iam_user_hardware_mfa_enabled", - "iam_user_mfa_enabled_console_access", - "iam_user_no_setup_initial_access_key", - "iam_user_two_active_access_key", - "iam_user_with_temporary_credentials", - "inspector2_active_findings_exist", - "inspector2_is_enabled", - "kafka_cluster_encryption_at_rest_uses_cmk", - "kafka_cluster_enhanced_monitoring_enabled", - "kafka_cluster_in_transit_encryption_enabled", - "kafka_cluster_is_public", - "kafka_cluster_mutual_tls_authentication_enabled", - "kafka_cluster_unrestricted_access_disabled", - "kafka_cluster_uses_latest_version", - "kafka_connector_in_transit_encryption_enabled", - "kinesis_stream_encrypted_at_rest", - "kms_cmk_are_used", - "kms_cmk_not_deleted_unintentionally", - "kms_cmk_rotation_enabled", - "kms_key_not_publicly_accessible", - "lightsail_database_public", - "lightsail_instance_automated_snapshots", - "lightsail_instance_public", - "lightsail_static_ip_unused", - "macie_automated_sensitive_data_discovery_enabled", - "macie_is_enabled", - "mq_broker_active_deployment_mode", - "mq_broker_auto_minor_version_upgrades", - "mq_broker_cluster_deployment_mode", - "mq_broker_logging_enabled", - "neptune_cluster_backup_enabled", - "neptune_cluster_copy_tags_to_snapshots", - "neptune_cluster_deletion_protection", - "neptune_cluster_iam_authentication_enabled", - "neptune_cluster_integration_cloudwatch_logs", - "neptune_cluster_multi_az", - "neptune_cluster_public_snapshot", - "neptune_cluster_snapshot_encrypted", - "neptune_cluster_storage_encrypted", - "neptune_cluster_uses_public_subnet", - "networkfirewall_deletion_protection", - "networkfirewall_in_all_vpc", - "networkfirewall_logging_enabled", - "networkfirewall_multi_az", - "networkfirewall_policy_default_action_fragmented_packets", - "networkfirewall_policy_default_action_full_packets", - "networkfirewall_policy_rule_group_associated", - "opensearch_service_domains_audit_logging_enabled", - "opensearch_service_domains_cloudwatch_logging_enabled", - "opensearch_service_domains_encryption_at_rest_enabled", - "opensearch_service_domains_fault_tolerant_data_nodes", - "opensearch_service_domains_fault_tolerant_master_nodes", - "opensearch_service_domains_https_communications_enforced", - "opensearch_service_domains_internal_user_database_enabled", - "opensearch_service_domains_node_to_node_encryption_enabled", - "opensearch_service_domains_not_publicly_accessible", - "opensearch_service_domains_updated_to_the_latest_service_software_version", - "opensearch_service_domains_use_cognito_authentication_for_kibana", - "organizations_account_part_of_organizations", - "organizations_delegated_administrators", - "organizations_opt_out_ai_services_policy", - "organizations_scp_check_deny_regions", - "organizations_tags_policies_enabled_and_attached", - "rds_cluster_backtrack_enabled", - "rds_cluster_copy_tags_to_snapshots", - "rds_cluster_critical_event_subscription", - "rds_cluster_default_admin", - "rds_cluster_deletion_protection", - "rds_cluster_iam_authentication_enabled", - "rds_cluster_integration_cloudwatch_logs", - "rds_cluster_minor_version_upgrade_enabled", - "rds_cluster_multi_az", - "rds_cluster_non_default_port", - "rds_cluster_protected_by_backup_plan", - "rds_cluster_storage_encrypted", - "rds_instance_backup_enabled", - "rds_instance_certificate_expiration", - "rds_instance_copy_tags_to_snapshots", - "rds_instance_critical_event_subscription", - "rds_instance_default_admin", - "rds_instance_deletion_protection", - "rds_instance_deprecated_engine_version", - "rds_instance_enhanced_monitoring_enabled", - "rds_instance_event_subscription_parameter_groups", - "rds_instance_event_subscription_security_groups", - "rds_instance_iam_authentication_enabled", - "rds_instance_inside_vpc", - "rds_instance_integration_cloudwatch_logs", - "rds_instance_minor_version_upgrade_enabled", - "rds_instance_multi_az", - "rds_instance_no_public_access", - "rds_instance_non_default_port", - "rds_instance_protected_by_backup_plan", - "rds_instance_storage_encrypted", - "rds_instance_transport_encrypted", - "rds_snapshots_encrypted", - "rds_snapshots_public_access", - "redshift_cluster_audit_logging", - "redshift_cluster_automated_snapshot", - "redshift_cluster_automatic_upgrades", - "redshift_cluster_encrypted_at_rest", - "redshift_cluster_enhanced_vpc_routing", - "redshift_cluster_in_transit_encryption_enabled", - "redshift_cluster_multi_az_enabled", - "redshift_cluster_non_default_database_name", - "redshift_cluster_non_default_username", - "redshift_cluster_public_access", - "resourceexplorer2_indexes_found", - "route53_dangling_ip_subdomain_takeover", - "route53_domains_privacy_protection_enabled", - "route53_domains_transferlock_enabled", - "route53_public_hosted_zones_cloudwatch_logging_enabled", - "s3_access_point_public_access_block", - "s3_account_level_public_access_blocks", - "s3_bucket_acl_prohibited", - "s3_bucket_cross_account_access", - "s3_bucket_cross_region_replication", - "s3_bucket_default_encryption", - "s3_bucket_event_notifications_enabled", - "s3_bucket_kms_encryption", - "s3_bucket_level_public_access_block", - "s3_bucket_lifecycle_enabled", - "s3_bucket_no_mfa_delete", - "s3_bucket_object_lock", - "s3_bucket_object_versioning", - "s3_bucket_policy_public_write_access", - "s3_bucket_public_access", - "s3_bucket_public_list_acl", - "s3_bucket_public_write_acl", - "s3_bucket_secure_transport_policy", - "s3_bucket_server_access_logging_enabled", - "s3_multi_region_access_point_public_access_block", - "sagemaker_endpoint_config_prod_variant_instances", - "sagemaker_models_network_isolation_enabled", - "sagemaker_models_vpc_settings_configured", - "sagemaker_notebook_instance_encryption_enabled", - "sagemaker_notebook_instance_root_access_disabled", - "sagemaker_notebook_instance_vpc_settings_configured", - "sagemaker_notebook_instance_without_direct_internet_access_configured", - "sagemaker_training_jobs_intercontainer_encryption_enabled", - "sagemaker_training_jobs_network_isolation_enabled", - "sagemaker_training_jobs_volume_and_output_encryption_enabled", - "sagemaker_training_jobs_vpc_settings_configured", - "secretsmanager_automatic_rotation_enabled", - "secretsmanager_not_publicly_accessible", - "secretsmanager_secret_rotated_periodically", - "secretsmanager_secret_unused", - "securityhub_enabled", - "ses_identity_not_publicly_accessible", - "shield_advanced_protection_in_associated_elastic_ips", - "shield_advanced_protection_in_classic_load_balancers", - "shield_advanced_protection_in_cloudfront_distributions", - "shield_advanced_protection_in_global_accelerators", - "shield_advanced_protection_in_internet_facing_load_balancers", - "shield_advanced_protection_in_route53_hosted_zones", - "sns_subscription_not_using_http_endpoints", - "sns_topics_kms_encryption_at_rest_enabled", - "sns_topics_not_publicly_accessible", - "sqs_queues_not_publicly_accessible", - "sqs_queues_server_side_encryption_enabled", - "ssm_document_secrets", - "ssm_documents_set_as_public", - "ssm_managed_compliant_patching", - "ssmincidents_enabled_with_plans", - "storagegateway_fileshare_encryption_enabled", - "transfer_server_in_transit_encryption_enabled", - "trustedadvisor_errors_and_warnings", - "trustedadvisor_premium_support_plan_subscribed", - "vpc_different_regions", - "vpc_endpoint_connections_trust_boundaries", - "vpc_endpoint_for_ec2_enabled", - "vpc_endpoint_multi_az_enabled", - "vpc_endpoint_services_allowed_principals_trust_boundaries", - "vpc_flow_logs_enabled", - "vpc_peering_routing_tables_with_least_privilege", - "vpc_subnet_different_az", - "vpc_subnet_no_public_ip_by_default", - "vpc_subnet_separate_private_public", - "vpc_vpn_connection_tunnels_up", - "waf_global_rule_with_conditions", - "waf_global_rulegroup_not_empty", - "waf_global_webacl_logging_enabled", - "waf_global_webacl_with_rules", - "waf_regional_rule_with_conditions", - "waf_regional_rulegroup_not_empty", - "waf_regional_webacl_with_rules", - "wafv2_webacl_logging_enabled", - "wafv2_webacl_rule_logging_enabled", - "wafv2_webacl_with_rules", - "wellarchitected_workload_no_high_or_medium_risks", - "workspaces_volume_encryption_enabled", - "workspaces_vpc_2private_1public_subnets_nat" + "inspector2_is_enabled" ], "Attributes": [ { @@ -4583,16 +3657,11 @@ "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": [ - "apigateway_restapi_logging_enabled", - "apigateway_restapi_public", - "apigateway_restapi_tracing_enabled", - "apigatewayv2_api_access_logging_enabled", - "athena_workgroup_logging_enabled", - "bedrock_model_invocation_logging_enabled", - "cloudfront_distributions_logging_enabled", + "cloudtrail_cloudwatch_logging_enabled", "cloudtrail_insights_exist", - "cloudtrail_log_file_validation_enabled", "cloudtrail_logs_s3_bucket_access_logging_enabled", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", "cloudtrail_threat_detection_enumeration", "cloudtrail_threat_detection_llm_jacking", @@ -4614,51 +3683,18 @@ "cloudwatch_log_metric_filter_security_group_changes", "cloudwatch_log_metric_filter_sign_in_without_mfa", "cloudwatch_log_metric_filter_unauthorized_api_calls", - "codebuild_project_logging_enabled", - "cognito_user_pool_advanced_security_enabled", "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", "config_recorder_all_regions_enabled", - "datasync_task_logging_enabled", - "directoryservice_directory_log_forwarding_enabled", - "directoryservice_directory_monitor_notifications", - "documentdb_cluster_cloudwatch_log_export", - "ec2_client_vpn_endpoint_connection_logging_enabled", - "ec2_elastic_ip_shodan", - "ec2_instance_detailed_monitoring_enabled", - "ecs_cluster_container_insights_enabled", - "ecs_task_definitions_logging_enabled", - "eks_control_plane_logging_all_types_enabled", - "elasticbeanstalk_environment_cloudwatch_logging_enabled", - "elasticbeanstalk_environment_enhanced_health_reporting", - "elb_logging_enabled", - "elbv2_logging_enabled", - "glue_etl_jobs_logging_enabled", "guardduty_eks_audit_log_enabled", "guardduty_eks_runtime_monitoring_enabled", "guardduty_no_high_severity_findings", - "kafka_cluster_enhanced_monitoring_enabled", - "mq_broker_logging_enabled", - "neptune_cluster_integration_cloudwatch_logs", - "networkfirewall_logging_enabled", - "opensearch_service_domains_audit_logging_enabled", - "opensearch_service_domains_cloudwatch_logging_enabled", "rds_cluster_critical_event_subscription", - "rds_cluster_integration_cloudwatch_logs", "rds_instance_critical_event_subscription", - "rds_instance_enhanced_monitoring_enabled", "rds_instance_event_subscription_parameter_groups", "rds_instance_event_subscription_security_groups", - "rds_instance_integration_cloudwatch_logs", - "redshift_cluster_audit_logging", - "route53_public_hosted_zones_cloudwatch_logging_enabled", "s3_bucket_event_notifications_enabled", - "s3_bucket_server_access_logging_enabled", - "trustedadvisor_errors_and_warnings", - "vpc_flow_logs_enabled", - "waf_global_webacl_logging_enabled", - "wafv2_webacl_logging_enabled", - "wafv2_webacl_rule_logging_enabled" + "trustedadvisor_errors_and_warnings" ], "Attributes": [ { @@ -4717,11 +3753,7 @@ "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": [ - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered" - ], + "Checks": [], "Attributes": [ { "Domain": "2. Control Measures Requirements", @@ -4758,62 +3790,29 @@ "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": [ - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered", - "autoscaling_group_capacity_rebalance_enabled", - "autoscaling_group_multiple_az", - "autoscaling_group_multiple_instance_types", - "awslambda_function_vpc_multi_az", "backup_plans_exist", + "backup_reportplans_exist", + "backup_vaults_exist", "cloudfront_distributions_multiple_origin_failover_configured", - "directconnect_connection_redundancy", - "directconnect_virtual_interface_redundancy", - "directoryservice_directory_snapshots_limit", - "dms_instance_multi_az_enabled", + "dlm_ebs_snapshot_lifecycle_policy_exists", "documentdb_cluster_backup_enabled", - "documentdb_cluster_multi_az_enabled", "drs_job_exist", - "dynamodb_accelerator_cluster_multi_az", - "dynamodb_table_autoscaling_enabled", "dynamodb_table_protected_by_backup_plan", "dynamodb_tables_pitr_enabled", "ec2_ebs_volume_protected_by_backup_plan", "ec2_ebs_volume_snapshots_exists", "efs_have_backup_enabled", - "elasticache_redis_cluster_automatic_failover_enabled", "elasticache_redis_cluster_backup_enabled", - "elasticache_redis_cluster_multi_az_enabled", - "elb_cross_zone_load_balancing_enabled", - "elb_is_in_multiple_az", - "elbv2_cross_zone_load_balancing_enabled", - "elbv2_is_in_multiple_az", - "eventbridge_global_endpoint_event_replication_enabled", - "fsx_file_system_copy_tags_to_backups_enabled", - "fsx_windows_file_system_multi_az_enabled", "lightsail_instance_automated_snapshots", - "mq_broker_active_deployment_mode", - "mq_broker_cluster_deployment_mode", "neptune_cluster_backup_enabled", - "neptune_cluster_multi_az", - "opensearch_service_domains_fault_tolerant_data_nodes", - "opensearch_service_domains_fault_tolerant_master_nodes", "rds_cluster_backtrack_enabled", - "rds_cluster_multi_az", "rds_cluster_protected_by_backup_plan", "rds_instance_backup_enabled", - "rds_instance_multi_az", "rds_instance_protected_by_backup_plan", "redshift_cluster_automated_snapshot", - "redshift_cluster_multi_az_enabled", - "s3_bucket_cross_region_replication", "s3_bucket_lifecycle_enabled", "s3_bucket_object_lock", - "s3_bucket_object_versioning", - "sagemaker_endpoint_config_prod_variant_instances", - "vpc_different_regions", - "vpc_endpoint_multi_az_enabled", - "vpc_subnet_different_az" + "s3_bucket_object_versioning" ], "Attributes": [ { @@ -5157,16 +4156,7 @@ "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" + "Personal information processing policy" ], "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.", @@ -5291,20 +4281,9 @@ "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." - ] + "RelatedRegulations": [], + "AuditEvidence": [], + "NonComplianceCases": [] } ] }, @@ -5394,7 +4373,7 @@ "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": [ + "RelatedRegulations": [ "Personal Information Protection Act, Article 27 (Restrictions on the Transfer of Personal Information Due to Business Transfers)" ], "AuditEvidence": [ @@ -5496,7 +4475,7 @@ "Personal Information Protection Act, Article 21 (Destruction of Personal Information)" ], "AuditEvidence": [ - "Regulations regarding the retention period and destruction of personal information',", + "Regulations regarding the retention period and destruction of personal information", "Current status of separated databases (table structure, etc.)", "Access permissions for separated databases" ], @@ -5609,4 +4588,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json index acde21a745..33ca1364f4 100644 --- a/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json +++ b/prowler/compliance/aws/kisa_isms_p_2023_korean_aws.json @@ -12,7 +12,7 @@ "Attributes": [ { "Domain": "1. 관리체계 수립 및 운영", - "Subdomain": "1.1 관리체계", + "Subdomain": "1.1 관리체계 기반 마련", "Section": "1.1.1 경영진의 참여", "AuditChecklist": [ "정보보호 및 개인정보보호 관리체계의 수립 및 운영활동 전반에 경영진의 참여가 이루어질 수 있도록 보고 및 의사결정 등의 책임과 역할을 문서화하고 있는가?", @@ -41,7 +41,7 @@ "Attributes": [ { "Domain": "1. 관리체계 수립 및 운영", - "Subdomain": "1.1 관리체계", + "Subdomain": "1.1 관리체계 기반 마련", "Section": "1.1.2 최고책임자의 지정", "AuditChecklist": [ "최고경영자는 정보보호 및 개인정보보호 처리에 관한 업무를 총괄하여 책임질 최고책임자를 공식적으로 지정하고 있는가?", @@ -77,7 +77,7 @@ "Attributes": [ { "Domain": "1. 관리체계 수립 및 운영", - "Subdomain": "1.1 관리체계", + "Subdomain": "1.1 관리체계 기반 마련", "Section": "1.1.3 조직 구성", "AuditChecklist": [ "정보보호 최고책임자 및 개인정보 보호책임자의 업무를 지원하고 조직의 정보보호 및 개인정보보호 활동을 체계적으로 이행하기 위하여 전문성을 갖춘 실무조직을 구성하여 운영하고 있는가?", @@ -112,7 +112,7 @@ "Attributes": [ { "Domain": "1. 관리체계 수립 및 운영", - "Subdomain": "1.1 관리체계", + "Subdomain": "1.1 관리체계 기반 마련", "Section": "1.1.4 범위 설정", "AuditChecklist": [ "조직의 핵심 서비스 및 개인정보 처리에 영향을 줄 수 있는 핵심자산을 포함하도록 관리체계 범위를 설정하고 있는가?", @@ -145,7 +145,7 @@ "Attributes": [ { "Domain": "1. 관리체계 수립 및 운영", - "Subdomain": "1.1 관리체계", + "Subdomain": "1.1 관리체계 기반 마련", "Section": "1.1.5 정책 수립", "AuditChecklist": [ "조직이 수행하는 모든 정보보호 및 개인정보보호 활동의 근거를 포함하는 최상위 수준의 정보보호 및 개인정보보호 정책을 수립하고 있는가?", @@ -180,7 +180,7 @@ "Attributes": [ { "Domain": "1. 관리체계 수립 및 운영", - "Subdomain": "1.1 관리체계", + "Subdomain": "1.1 관리체계 기반 마련", "Section": "1.1.6 자원 할당", "AuditChecklist": [ "정보보호 및 개인정보보호 분야별 전문성을 갖춘 인력을 확보하고 있는가?", @@ -206,11 +206,7 @@ "Id": "1.2.1", "Name": "정보자산 식별", "Description": "조직의 업무특성에 따라 정보자산 분류기준을 수립하여 관리체계 범위 내 모든 정보자산을 식별·분류하고, 중요도를 산정한 후 그 목록을 최신으로 관리하여야 한다.", - "Checks": [ - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered" - ], + "Checks": [], "Attributes": [ { "Domain": "1. 관리체계 수립 및 운영", @@ -403,10 +399,7 @@ "Id": "1.3.3", "Name": "운영현황 관리", "Description": "조직이 수립한 관리체계에 따라 상시적 또는 주기적으로 수행하여야 하는 운영활동 및 수행 내역은 식별 및 추적이 가능하도록 기록하여 관리하고, 경영진은 주기적으로 운영활동의 효과성을 확인하여 관리하여야 한다.", - "Checks": [ - "cloudwatch_log_metric_filter_aws_organizations_changes", - "codebuild_project_older_90_days" - ], + "Checks": [], "Attributes": [ { "Domain": "1. 관리체계 수립 및 운영", @@ -574,11 +567,7 @@ "Id": "2.1.2", "Name": "조직의 유지관리", "Description": "조직의 각 구성원에게 정보보호와 개인정보보호 관련 역할 및 책임을 할당하고, 그 활동을 평가할 수 있는 체계와 조직 및 조직의 구성원 간 상호 의사소통할 수 있는 체계를 수립하여 운영하여야 한다.", - "Checks": [ - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered" - ], + "Checks": [], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -617,18 +606,13 @@ "Name": "정보자산 관리", "Description": "정보자산의 용도와 중요도에 따른 취급 절차 및 보호대책을 수립·이행하고, 자산별 책임소재를 명확히 정의하여 관리하여야 한다.", "Checks": [ - "ec2_elastic_ip_unassigned", - "ec2_instance_older_than_specific_days", - "ecr_repositories_lifecycle_policy_enabled", + "fsx_file_system_copy_tags_to_backups_enabled", "fsx_file_system_copy_tags_to_volumes_enabled", "neptune_cluster_copy_tags_to_snapshots", - "organizations_account_part_of_organizations", - "organizations_delegated_administrators", - "organizations_scp_check_deny_regions", "organizations_tags_policies_enabled_and_attached", "rds_cluster_copy_tags_to_snapshots", "rds_instance_copy_tags_to_snapshots", - "resourceexplorer2_indexes_found" + "redshift_cluster_non_default_username" ], "Attributes": [ { @@ -658,10 +642,7 @@ "Id": "2.2.1", "Name": "주요 직무자 지정 및 관리", "Description": "개인정보 및 중요정보의 취급이나 주요 시스템 접근 등 주요 직무의 기준과 관리방안을 수립하고, 주요 직무자를 최소한으로 지정하여 그 목록을 최신으로 관리하여야 한다.", - "Checks": [ - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered" - ], + "Checks": [], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -697,9 +678,7 @@ "Id": "2.2.2", "Name": "직무 분리", "Description": "권한 오·남용 등으로 인한 잠재적인 피해 예방을 위하여 직무 분리 기준을 수립하고 적용하여야 한다. 다만 불가피하게 직무 분리가 어려운 경우 별도의 보완대책을 마련하여 이행하여야 한다.", - "Checks": [ - "account_maintain_different_contact_details_to_security_billing_and_operations" - ], + "Checks": [], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -1105,7 +1084,7 @@ { "Id": "2.4.5 ", "Name": "보호구역 내 작업", - "Description": "보호구역 내에서의 비인가행위 및 권한 오·남용 등을 방지하기 위한 작업 절차를 수립 및이행하고, 작업 기록을 주기적으로 검토하여야 한다.", + "Description": "보호구역 내에서의 비인가행위 및 권한 오·남용 등을 방지하기 위한 작업 절차를 수립·이행하고, 작업 기록을 주기적으로 검토하여야 한다.", "Checks": [], "Attributes": [ { @@ -1132,7 +1111,7 @@ { "Id": "2.4.6", "Name": "반출입 기기 통제", - "Description": "보호구역 내 정보시스템, 모바일 기기, 저장매체 등에 대한 반출입 통제절차를 수립 및 이행하고 주기적으로 검토하여야 한다.", + "Description": "보호구역 내 정보시스템, 모바일 기기, 저장매체 등에 대한 반출입 통제절차를 수립·이행하고 주기적으로 검토하여야 한다.", "Checks": [], "Attributes": [ { @@ -1201,17 +1180,12 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings", - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered", "account_security_questions_are_registered_in_the_aws_account", - "apigateway_restapi_authorizers_enabled", - "autoscaling_group_launch_configuration_requires_imdsv2", + "cognito_identity_pool_guest_access_disabled", "cognito_user_pool_deletion_protection_enabled", "cognito_user_pool_self_registration_disabled", - "config_recorder_using_aws_service_role", "ec2_instance_profile_attached", - "eventbridge_schema_registry_cross_account_access", + "iam_avoid_root_usage", "iam_aws_attached_policy_no_administrative_privileges", "iam_customer_attached_policy_no_administrative_privileges", "iam_customer_unattached_policy_no_administrative_privileges", @@ -1221,7 +1195,6 @@ "iam_inline_policy_no_full_access_to_cloudtrail", "iam_inline_policy_no_full_access_to_kms", "iam_no_custom_policy_permissive_role_assumption", - "iam_no_root_access_key", "iam_policy_allows_privilege_escalation", "iam_policy_attached_only_to_group_or_roles", "iam_policy_cloudshell_admin_not_attached", @@ -1232,7 +1205,10 @@ "iam_role_cross_service_confused_deputy_prevention", "iam_securityaudit_role_created", "iam_support_role_created", - "iam_user_administrator_access_policy" + "iam_user_administrator_access_policy", + "rds_cluster_default_admin", + "rds_instance_default_admin", + "redshift_cluster_non_default_database_name" ], "Attributes": [ { @@ -1267,8 +1243,6 @@ "Name": "사용자 식별", "Description": "사용자 계정은 사용자별로 유일하게 구분할 수 있도록 식별자를 할당하고 추측 가능한 식별자 사용을 제한하여야 하며, 동일한 식별자를 공유하여 사용하는 경우 그 사유와 타당성을 검토하여 책임자의 승인 및 책임추적성 확보 등 보완대책을 수립·이행하여야 한다.", "Checks": [ - "cognito_user_pool_advanced_security_enabled", - "ec2_instance_profile_attached", "efs_access_point_enforce_user_identity" ], "Attributes": [ @@ -1303,15 +1277,14 @@ "Description": "정보시스템과 개인정보 및 중요정보에 대한 사용자의 접근은 안전한 인증절차와 필요에 따라 강화된 인증방식을 적용하여야 한다. 또한 로그인 횟수 제한, 불법 로그인 시도 경고 등 비인가자 접근 통제방안을 수립·이행하여야 한다.", "Checks": [ "account_security_questions_are_registered_in_the_aws_account", - "apigateway_restapi_client_certificate_enabled", + "apigateway_restapi_authorizers_enabled", "apigateway_restapi_public_with_authorizer", "apigatewayv2_api_authorizers_enabled", "appstream_fleet_maximum_session_duration", "appstream_fleet_session_disconnect_timeout", "appstream_fleet_session_idle_disconnect_timeout", + "autoscaling_group_launch_configuration_requires_imdsv2", "cloudtrail_bucket_requires_mfa_delete", - "cloudwatch_log_metric_filter_authentication_failures", - "cloudwatch_log_metric_filter_sign_in_without_mfa", "cognito_user_pool_advanced_security_enabled", "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", @@ -1325,24 +1298,16 @@ "elasticache_redis_replication_group_auth_enabled", "iam_administrator_access_with_mfa", "iam_check_saml_providers_sts", - "iam_no_root_access_key", "iam_root_hardware_mfa_enabled", "iam_root_mfa_enabled", - "iam_rotate_access_key_90_days", - "iam_user_accesskey_unused", "iam_user_console_access_unused", "iam_user_hardware_mfa_enabled", "iam_user_mfa_enabled_console_access", - "iam_user_no_setup_initial_access_key", - "iam_user_two_active_access_key", "iam_user_with_temporary_credentials", "neptune_cluster_iam_authentication_enabled", - "opensearch_service_domains_internal_user_database_enabled", "opensearch_service_domains_use_cognito_authentication_for_kibana", - "organizations_account_part_of_organizations", "rds_cluster_iam_authentication_enabled", "rds_instance_iam_authentication_enabled", - "s3_bucket_cross_account_access", "s3_bucket_no_mfa_delete" ], "Attributes": [ @@ -1374,7 +1339,7 @@ { "Id": "2.5.4", "Name": "비밀번호 관리", - "Description": "법적 요구사항, 외부 위협요인 등을 고려하여 정보시스템 사용자 및 고객, 회원 등 정보주체(이용자)가 사용하는 비밀번호 관리절차를 수립·이행하여야 한다.", + "Description": "법적 요구사항, 외부 위협요인 등을 고려하여 정보시스템 사용자 및 고객, 회원 등 정보주체 (이용자)가 사용하는 비밀번호 관리절차를 수립·이행하여야 한다.", "Checks": [ "cognito_user_pool_password_policy_lowercase", "cognito_user_pool_password_policy_minimum_length_14", @@ -1423,11 +1388,6 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings", - "apigateway_restapi_authorizers_enabled", - "cloudwatch_log_metric_filter_root_usage", - "cognito_user_pool_self_registration_disabled", - "config_recorder_using_aws_service_role", - "ecs_task_definitions_no_privileged_containers", "iam_administrator_access_with_mfa", "iam_avoid_root_usage", "iam_aws_attached_policy_no_administrative_privileges", @@ -1445,17 +1405,12 @@ "iam_policy_no_full_access_to_cloudtrail", "iam_policy_no_full_access_to_kms", "iam_role_administratoraccess_policy", - "iam_role_cross_account_readonlyaccess_policy", "iam_root_hardware_mfa_enabled", "iam_root_mfa_enabled", "iam_securityaudit_role_created", "iam_support_role_created", "iam_user_administrator_access_policy", - "organizations_delegated_administrators", - "rds_cluster_default_admin", - "rds_instance_default_admin", - "sagemaker_notebook_instance_root_access_disabled", - "ses_identity_not_publicly_accessible" + "organizations_delegated_administrators" ], "Attributes": [ { @@ -1473,7 +1428,6 @@ "AuditEvidence": [ "특수권한 관련 지침", "특수권한 신청·승인 내역", - "특수권한자 목록", "특수권한 검토 내용" ], "NonComplianceCases": [ @@ -1492,25 +1446,9 @@ "Checks": [ "accessanalyzer_enabled", "accessanalyzer_enabled_without_findings", - "apigateway_restapi_authorizers_enabled", - "appstream_fleet_maximum_session_duration", - "appstream_fleet_session_disconnect_timeout", - "appstream_fleet_session_idle_disconnect_timeout", - "autoscaling_group_launch_configuration_requires_imdsv2", - "cloudwatch_cross_account_sharing_disabled", - "cloudwatch_log_group_not_publicly_accessible", - "cloudwatch_log_metric_filter_policy_changes", - "cognito_identity_pool_guest_access_disabled", - "cognito_user_pool_advanced_security_enabled", - "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", - "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", - "cognito_user_pool_client_prevent_user_existence_errors", - "cognito_user_pool_client_token_revocation_enabled", - "cognito_user_pool_self_registration_disabled", "config_recorder_using_aws_service_role", "dynamodb_table_cross_account_access", "ec2_instance_internet_facing_with_instance_profile", - "ec2_instance_managed_by_ssm", "ec2_instance_profile_attached", "eventbridge_bus_cross_account_access", "eventbridge_schema_registry_cross_account_access", @@ -1538,7 +1476,6 @@ "s3_bucket_policy_public_write_access", "s3_bucket_public_list_acl", "s3_bucket_public_write_acl", - "ses_identity_not_publicly_accessible", "sns_topics_not_publicly_accessible", "sqs_queues_not_publicly_accessible", "ssm_documents_set_as_public", @@ -1550,7 +1487,7 @@ "Subdomain": "2.5 인증 및 권한관리", "Section": "2.5.6 접근권한 검토", "AuditChecklist": [ - "정보시스템과 개인정보 및 중요정보에 대한 사용자 계정 및 접근권한 생성·등록·부여· 이용·변경·말소 등의 이력을 남기고 있는가?", + "정보시스템과 개인정보 및 중요정보에 대한 사용자 계정 및 접근권한 생성·등록·부여·이용·변경·말소 등의 이력을 남기고 있는가?", "정보시스템과 개인정보 및 중요정보에 대한 사용자 계정 및 접근권한의 적정성 검토 기준, 검토주체, 검토방법, 주기 등을 수립하여 정기적 검토를 이행하고 있는가?", "접근권한 검토 결과 접근권한 과다 부여, 권한부여 절차 미준수, 권한 오·남용 등 문제점이 발견된 경우 그에 따른 조치절차를 수립·이행하고 있는가?" ], @@ -1576,27 +1513,22 @@ "Name": "네트워크 접근", "Description": "네트워크에 대한 비인가 접근을 통제하기 위하여 IP관리, 단말인증 등 관리절차를 수립 및이행하고, 업무목적 및 중요도에 따라 네트워크 분리(DMZ, 서버팜, DB존, 개발존 등)와 접근통제를 적용하여야 한다.", "Checks": [ - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_client_certificate_enabled", "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", "apigateway_restapi_waf_acl_attached", "appstream_fleet_default_internet_access_disabled", "autoscaling_group_launch_configuration_no_public_ip", "awslambda_function_inside_vpc", "awslambda_function_not_publicly_accessible", - "cloudfront_distributions_default_root_object", "cloudfront_distributions_geo_restrictions_enabled", "cloudfront_distributions_s3_origin_access_control", "cloudfront_distributions_using_waf", "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudwatch_cross_account_sharing_disabled", - "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", - "cloudwatch_log_metric_filter_security_group_changes", + "cloudwatch_log_group_not_publicly_accessible", "cognito_user_pool_waf_acl_attached", + "dms_endpoint_ssl_enabled", "dms_instance_no_public_access", "documentdb_cluster_public_snapshot", - "dynamodb_table_cross_account_access", + "ec2_elastic_ip_shodan", "ec2_instance_internet_facing_with_instance_profile", "ec2_instance_port_cassandra_exposed_to_internet", "ec2_instance_port_cifs_exposed_to_internet", @@ -1641,68 +1573,50 @@ "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", "ec2_securitygroup_allow_wide_open_public_ipv4", "ec2_securitygroup_default_restrict_traffic", - "ec2_securitygroup_from_launch_wizard", - "ec2_securitygroup_not_used", - "ec2_securitygroup_with_many_ingress_egress_rules", "ec2_transitgateway_auto_accept_vpc_attachments", "ecr_repositories_not_publicly_accessible", "ecs_service_no_assign_public_ip", "ecs_task_set_no_assign_public_ip", "efs_mount_target_not_publicly_accessible", - "efs_not_publicly_accessible", "eks_cluster_network_policy_enabled", "eks_cluster_not_publicly_accessible", "eks_cluster_private_nodes_enabled", "elasticache_cluster_uses_public_subnet", "elb_internet_facing", "elbv2_internet_facing", - "elbv2_listeners_underneath", "elbv2_waf_acl_attached", "emr_cluster_account_public_block_enabled", "emr_cluster_master_nodes_no_public_ip", "emr_cluster_publicly_accesible", - "eventbridge_bus_exposed", - "glue_data_catalogs_not_publicly_accessible", + "glacier_vaults_policy_public_access", "kafka_cluster_is_public", - "kafka_cluster_unrestricted_access_disabled", "lightsail_database_public", "lightsail_instance_public", - "lightsail_static_ip_unused", - "neptune_cluster_public_snapshot", "neptune_cluster_uses_public_subnet", - "networkfirewall_deletion_protection", - "networkfirewall_in_all_vpc", - "networkfirewall_multi_az", - "networkfirewall_policy_default_action_fragmented_packets", - "networkfirewall_policy_default_action_full_packets", - "networkfirewall_policy_rule_group_associated", "opensearch_service_domains_not_publicly_accessible", - "rds_snapshots_public_access", + "rds_cluster_non_default_port", "rds_instance_inside_vpc", + "rds_instance_no_public_access", + "rds_instance_non_default_port", "redshift_cluster_enhanced_vpc_routing", "redshift_cluster_public_access", "route53_dangling_ip_subdomain_takeover", "s3_access_point_public_access_block", + "s3_account_level_public_access_blocks", + "s3_bucket_level_public_access_block", + "s3_bucket_public_access", + "s3_multi_region_access_point_public_access_block", "sagemaker_models_network_isolation_enabled", "sagemaker_models_vpc_settings_configured", "sagemaker_notebook_instance_vpc_settings_configured", "sagemaker_notebook_instance_without_direct_internet_access_configured", "sagemaker_training_jobs_network_isolation_enabled", "sagemaker_training_jobs_vpc_settings_configured", - "sqs_queues_server_side_encryption_enabled", "vpc_endpoint_connections_trust_boundaries", "vpc_endpoint_for_ec2_enabled", "vpc_peering_routing_tables_with_least_privilege", "vpc_subnet_no_public_ip_by_default", "vpc_subnet_separate_private_public", - "vpc_vpn_connection_tunnels_up", - "waf_global_rule_with_conditions", - "waf_global_rulegroup_not_empty", - "waf_global_webacl_with_rules", - "waf_regional_rule_with_conditions", - "waf_regional_rulegroup_not_empty", - "waf_regional_webacl_with_rules", - "wafv2_webacl_with_rules", "workspaces_vpc_2private_1public_subnets_nat" ], "Attributes": [ @@ -1741,24 +1655,14 @@ "Name": "정보시스템 접근", "Description": "서버, 네트워크시스템 등 정보시스템에 접근을 허용하는 사용자, 접근제한 방식, 안전한 접근수단 등을 정의하여 통제하여야 한다.", "Checks": [ - "apigateway_restapi_client_certificate_enabled", - "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", "apigateway_restapi_waf_acl_attached", - "apigatewayv2_api_authorizers_enabled", "athena_workgroup_enforce_configuration", - "autoscaling_group_launch_configuration_requires_imdsv2", - "awslambda_function_inside_vpc", - "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", - "awslambda_function_not_publicly_accessible", - "cloudfront_distributions_s3_origin_access_control", + "awslambda_function_url_cors_policy", + "awslambda_function_url_public", + "cloudfront_distributions_default_root_object", + "cloudfront_distributions_using_waf", "cloudwatch_cross_account_sharing_disabled", - "cloudwatch_log_group_not_publicly_accessible", - "cognito_identity_pool_guest_access_disabled", - "cognito_user_pool_client_token_revocation_enabled", - "dms_instance_no_public_access", - "documentdb_cluster_public_snapshot", - "dynamodb_table_cross_account_access", + "cognito_user_pool_waf_acl_attached", "ec2_ami_public", "ec2_ebs_public_snapshot", "ec2_ebs_snapshot_account_block_public_access", @@ -1766,70 +1670,39 @@ "ec2_instance_imdsv2_enabled", "ec2_instance_internet_facing_with_instance_profile", "ec2_instance_managed_by_ssm", - "ec2_instance_port_cassandra_exposed_to_internet", "ec2_instance_port_cifs_exposed_to_internet", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", "ec2_instance_port_ftp_exposed_to_internet", - "ec2_instance_port_memcached_exposed_to_internet", - "ec2_instance_port_mongodb_exposed_to_internet", - "ec2_instance_port_mysql_exposed_to_internet", - "ec2_instance_port_oracle_exposed_to_internet", - "ec2_instance_port_postgresql_exposed_to_internet", + "ec2_instance_port_kafka_exposed_to_internet", + "ec2_instance_port_kerberos_exposed_to_internet", + "ec2_instance_port_ldap_exposed_to_internet", "ec2_instance_port_rdp_exposed_to_internet", - "ec2_instance_port_redis_exposed_to_internet", - "ec2_instance_port_sqlserver_exposed_to_internet", "ec2_instance_port_ssh_exposed_to_internet", "ec2_instance_port_telnet_exposed_to_internet", - "ec2_networkacl_allow_ingress_any_port", - "ec2_networkacl_allow_ingress_tcp_port_22", - "ec2_networkacl_allow_ingress_tcp_port_3389", - "ec2_networkacl_unused", "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", "ec2_securitygroup_allow_ingress_from_internet_to_any_port", "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", - "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", - "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_ingress_from_internet_to_tcp_port_sql_server_1433_1434", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", - "ecr_repositories_not_publicly_accessible", - "ecs_service_no_assign_public_ip", - "ecs_task_definitions_host_namespace_not_shared", - "ecs_task_definitions_host_networking_mode_users", "ecs_task_definitions_no_privileged_containers", - "ecs_task_set_no_assign_public_ip", "efs_access_point_enforce_root_directory", "efs_access_point_enforce_user_identity", - "efs_mount_target_not_publicly_accessible", "efs_not_publicly_accessible", - "eks_cluster_network_policy_enabled", - "eks_cluster_not_publicly_accessible", - "eks_cluster_private_nodes_enabled", - "elasticache_cluster_uses_public_subnet", - "elasticache_redis_replication_group_auth_enabled", - "emr_cluster_account_public_block_enabled", - "emr_cluster_master_nodes_no_public_ip", - "emr_cluster_publicly_accesible", + "elbv2_waf_acl_attached", + "eventbridge_bus_cross_account_access", "eventbridge_bus_exposed", - "glacier_vaults_policy_public_access", + "eventbridge_schema_registry_cross_account_access", "glue_data_catalogs_not_publicly_accessible", - "guardduty_lambda_protection_enabled", - "guardduty_s3_protection_enabled", "kafka_cluster_is_public", "kafka_cluster_unrestricted_access_disabled", - "lightsail_instance_public", - "lightsail_static_ip_unused", "neptune_cluster_public_snapshot", "opensearch_service_domains_not_publicly_accessible", "opensearch_service_domains_use_cognito_authentication_for_kibana", + "rds_snapshots_public_access", "s3_access_point_public_access_block", "s3_account_level_public_access_blocks", "s3_bucket_acl_prohibited", @@ -1840,7 +1713,10 @@ "s3_bucket_public_list_acl", "s3_bucket_public_write_acl", "s3_multi_region_access_point_public_access_block", - "ssm_documents_set_as_public" + "sagemaker_notebook_instance_root_access_disabled", + "ses_identity_not_publicly_accessible", + "ssm_documents_set_as_public", + "vpc_endpoint_connections_trust_boundaries" ], "Attributes": [ { @@ -1878,24 +1754,8 @@ "Name": "응용프로그램 접근", "Description": "사용자별 업무 및 접근 정보의 중요도 등에 따라 응용프로그램 접근권한을 제한하고, 불필요한 정보 또는 중요정보 노출을 최소화할 수 있도록 기준을 수립하여 적용하여야 한다.", "Checks": [ - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", - "apigatewayv2_api_authorizers_enabled", - "awslambda_function_url_cors_policy", - "awslambda_function_url_public", - "cloudfront_distributions_default_root_object", - "codeartifact_packages_external_public_publishing_disabled", - "cognito_user_pool_waf_acl_attached", - "ec2_ami_public", "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", - "ec2_instance_port_kafka_exposed_to_internet", - "ec2_instance_port_kerberos_exposed_to_internet", - "ec2_instance_port_ldap_exposed_to_internet", - "ecr_repositories_not_publicly_accessible", - "ecs_task_definitions_host_namespace_not_shared", - "ecs_task_definitions_host_networking_mode_users", - "eks_cluster_network_policy_enabled" + "opensearch_service_domains_use_cognito_authentication_for_kibana" ], "Attributes": [ { @@ -1928,7 +1788,7 @@ "사례 1 : 응용프로그램의 개인정보 처리화면 중 일부 화면의 권한 제어 기능에 오류가 존재하여 개인정보 열람 권한이 없는 사용자에게도 개인정보가 노출되고 있는 경우", "사례 2 : 응용프로그램의 관리자 페이지가 외부인터넷에 오픈되어 있으면서 안전한 인증수단이 적용되어 있지 않은 경우", "사례 3 : 응용프로그램에 대하여 타당한 사유 없이 세션 타임아웃 또는 동일 사용자 계정의 동시 접속을 제한하고 있지 않은 경우", - "사례 4 : 응용프로그램을 통하여 개인정보를 다운로드받는 경우 해당 파일 내에 주민등록번호 등 업무상 불필요한 정보가 과도하게 포함되어 있는 경우,", + "사례 4 : 응용프로그램을 통하여 개인정보를 다운로드받는 경우 해당 파일 내에 주민등록번호 등 업무상 불필요한 정보가 과도하게 포함되어 있는 경우", "사례 5 : 응용프로그램의 개인정보 조회화면에서 like 검색을 과도하게 허용하고 있어, 모든 사용자가 본인의 업무 범위를 초과하여 성씨만으로도 전체 고객 정보를 조회할 수 있는 경우", "사례 6 : 개인정보 표시제한 조치 기준이 마련되어 있지 않거나 이를 준수하지 않는 등의 사유로 동일한 개인정보 항목에 대하여 개인정보처리시스템 화면별로 서로 다른 마스킹 기준이 적용된 경우", "사례 7 : 개인정보처리시스템의 화면상에는 개인정보가 마스킹되어 표시되어 있으나, 웹브라우저 소스보기를 통하여 마스킹되지 않은 전체 개인정보가 노출되는 경우" @@ -1944,7 +1804,6 @@ "dms_endpoint_mongodb_authentication_enabled", "dms_endpoint_neptune_iam_authorization_enabled", "dms_endpoint_ssl_enabled", - "documentdb_cluster_public_snapshot", "dynamodb_table_cross_account_access", "ec2_instance_port_cassandra_exposed_to_internet", "ec2_instance_port_memcached_exposed_to_internet", @@ -1954,6 +1813,10 @@ "ec2_instance_port_postgresql_exposed_to_internet", "ec2_instance_port_redis_exposed_to_internet", "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", @@ -1961,25 +1824,17 @@ "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_ingress_from_internet_to_tcp_port_sql_server_1433_1434", - "guardduty_rds_protection_enabled", + "elasticache_redis_replication_group_auth_enabled", "lightsail_database_public", "neptune_cluster_iam_authentication_enabled", - "neptune_cluster_uses_public_subnet", + "neptune_cluster_public_snapshot", "opensearch_service_domains_internal_user_database_enabled", "rds_cluster_iam_authentication_enabled", "rds_cluster_non_default_port", - "rds_cluster_storage_encrypted", "rds_instance_iam_authentication_enabled", "rds_instance_no_public_access", "rds_instance_non_default_port", - "rds_instance_storage_encrypted", - "rds_instance_transport_encrypted", - "rds_snapshots_public_access", - "redshift_cluster_encrypted_at_rest", "redshift_cluster_enhanced_vpc_routing", - "redshift_cluster_in_transit_encryption_enabled", - "redshift_cluster_non_default_database_name", - "redshift_cluster_non_default_username", "redshift_cluster_public_access" ], "Attributes": [ @@ -2054,7 +1909,6 @@ "appstream_fleet_maximum_session_duration", "appstream_fleet_session_disconnect_timeout", "appstream_fleet_session_idle_disconnect_timeout", - "autoscaling_group_launch_configuration_requires_imdsv2", "ec2_instance_port_cifs_exposed_to_internet", "ec2_instance_port_ftp_exposed_to_internet", "ec2_instance_port_rdp_exposed_to_internet", @@ -2062,12 +1916,13 @@ "ec2_instance_port_telnet_exposed_to_internet", "ec2_networkacl_allow_ingress_tcp_port_22", "ec2_networkacl_allow_ingress_tcp_port_3389", + "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_any_port", + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", - "vpc_vpn_connection_tunnels_up", - "workspaces_volume_encryption_enabled", "workspaces_vpc_2private_1public_subnets_nat" ], "Attributes": [ @@ -2107,59 +1962,7 @@ "Id": "2.6.7", "Name": "인터넷 접속 통제", "Description": "인터넷을 통한 정보 유출, 악성코드 감염, 내부망 침투 등을 예방하기 위하여 주요 정보시스템, 주요 직무 수행 및 개인정보 취급 단말기 등에 대한 인터넷 접속 또는 서비스(P2P, 웹하드, 메신저 등)를 제한하는 등 인터넷 접속 통제 정책을 수립·이행하여야 한다.", - "Checks": [ - "apigateway_restapi_waf_acl_attached", - "appstream_fleet_default_internet_access_disabled", - "autoscaling_group_launch_configuration_no_public_ip", - "awslambda_function_not_publicly_accessible", - "awslambda_function_url_cors_policy", - "awslambda_function_url_public", - "cloudfront_distributions_using_waf", - "dms_instance_no_public_access", - "ec2_elastic_ip_shodan", - "ec2_instance_port_cassandra_exposed_to_internet", - "ec2_instance_port_cifs_exposed_to_internet", - "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", - "ec2_instance_port_ftp_exposed_to_internet", - "ec2_instance_port_kafka_exposed_to_internet", - "ec2_instance_port_kerberos_exposed_to_internet", - "ec2_instance_port_ldap_exposed_to_internet", - "ec2_instance_port_memcached_exposed_to_internet", - "ec2_instance_port_mongodb_exposed_to_internet", - "ec2_instance_port_mysql_exposed_to_internet", - "ec2_instance_port_oracle_exposed_to_internet", - "ec2_instance_port_postgresql_exposed_to_internet", - "ec2_instance_port_rdp_exposed_to_internet", - "ec2_instance_port_redis_exposed_to_internet", - "ec2_instance_port_sqlserver_exposed_to_internet", - "ec2_instance_port_ssh_exposed_to_internet", - "ec2_instance_port_telnet_exposed_to_internet", - "ec2_instance_public_ip", - "ecs_service_no_assign_public_ip", - "elb_internet_facing", - "elbv2_internet_facing", - "emr_cluster_account_public_block_enabled", - "emr_cluster_master_nodes_no_public_ip", - "emr_cluster_publicly_accesible", - "eventbridge_bus_exposed", - "glue_data_catalogs_not_publicly_accessible", - "kafka_cluster_is_public", - "kafka_cluster_unrestricted_access_disabled", - "neptune_cluster_public_snapshot", - "networkfirewall_deletion_protection", - "networkfirewall_in_all_vpc", - "networkfirewall_multi_az", - "opensearch_service_domains_not_publicly_accessible", - "route53_dangling_ip_subdomain_takeover", - "sagemaker_notebook_instance_without_direct_internet_access_configured", - "vpc_endpoint_connections_trust_boundaries", - "vpc_endpoint_for_ec2_enabled", - "vpc_peering_routing_tables_with_least_privilege", - "vpc_subnet_no_public_ip_by_default", - "vpc_subnet_separate_private_public", - "vpc_vpn_connection_tunnels_up", - "workspaces_vpc_2private_1public_subnets_nat" - ], + "Checks": [], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -2198,8 +2001,8 @@ "Checks": [ "acm_certificates_expiration_check", "acm_certificates_with_secure_key_algorithms", - "apigateway_restapi_authorizers_enabled", "apigateway_restapi_cache_encrypted", + "apigateway_restapi_client_certificate_enabled", "athena_workgroup_encryption", "awslambda_function_no_secrets_in_code", "awslambda_function_no_secrets_in_variables", @@ -2211,7 +2014,6 @@ "cloudfront_distributions_https_sni_enabled", "cloudfront_distributions_origin_traffic_encrypted", "cloudfront_distributions_using_deprecated_ssl_protocols", - "cloudwatch_log_group_no_secrets_in_logs", "codebuild_project_no_secrets_in_variables", "codebuild_project_s3_logs_encrypted", "codebuild_project_source_repo_url_no_sensitive_credentials", @@ -2229,7 +2031,6 @@ "ec2_launch_template_no_secrets", "ecs_task_definitions_no_environment_secrets", "efs_encryption_at_rest_enabled", - "eks_cluster_kms_cmk_encryption_in_secrets_enabled", "elasticache_redis_cluster_in_transit_encryption_enabled", "elasticache_redis_cluster_rest_encryption_enabled", "elb_insecure_ssl_ciphers", @@ -2248,7 +2049,7 @@ "glue_etl_jobs_cloudwatch_logs_encryption_enabled", "glue_etl_jobs_job_bookmark_encryption_enabled", "glue_ml_transform_encrypted_at_rest", - "kafka_cluster_encryption_at_rest_uses_cmk", + "iam_no_expired_server_certificates_stored", "kafka_cluster_in_transit_encryption_enabled", "kafka_cluster_mutual_tls_authentication_enabled", "kafka_connector_in_transit_encryption_enabled", @@ -2259,6 +2060,7 @@ "opensearch_service_domains_https_communications_enforced", "opensearch_service_domains_node_to_node_encryption_enabled", "rds_cluster_storage_encrypted", + "rds_instance_certificate_expiration", "rds_instance_storage_encrypted", "rds_instance_transport_encrypted", "rds_snapshots_encrypted", @@ -2268,11 +2070,11 @@ "s3_bucket_secure_transport_policy", "sagemaker_notebook_instance_encryption_enabled", "sagemaker_training_jobs_intercontainer_encryption_enabled", - "sagemaker_training_jobs_volume_and_output_encryption_enabled", "sns_subscription_not_using_http_endpoints", "sqs_queues_server_side_encryption_enabled", "ssm_document_secrets", - "transfer_server_in_transit_encryption_enabled" + "transfer_server_in_transit_encryption_enabled", + "workspaces_volume_encryption_enabled" ], "Attributes": [ { @@ -2308,40 +2110,30 @@ "Name": "암호키 관리", "Description": "암호키의 안전한 생성·이용·보관·배포·파기를 위한 관리 절차를 수립·이행하고, 필요 시 복구방안을 마련하여야 한다.", "Checks": [ - "acm_certificates_expiration_check", - "acm_certificates_transparency_logs_enabled", - "athena_workgroup_encryption", "backup_vaults_encrypted", "bedrock_model_invocation_logs_encryption_enabled", - "cloudfront_distributions_custom_ssl_certificate", - "cloudfront_distributions_https_enabled", - "cloudfront_distributions_https_sni_enabled", - "cloudfront_distributions_origin_traffic_encrypted", - "cloudfront_distributions_using_deprecated_ssl_protocols", "cloudtrail_kms_encryption_enabled", "cloudwatch_log_group_kms_encryption_enabled", - "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", - "dms_endpoint_ssl_enabled", "dynamodb_tables_kms_cmk_encryption_enabled", "eks_cluster_kms_cmk_encryption_in_secrets_enabled", - "iam_no_expired_server_certificates_stored", + "iam_no_root_access_key", "iam_rotate_access_key_90_days", "iam_user_accesskey_unused", "iam_user_no_setup_initial_access_key", "iam_user_two_active_access_key", + "kafka_cluster_encryption_at_rest_uses_cmk", "kms_cmk_are_used", "kms_cmk_not_deleted_unintentionally", "kms_cmk_rotation_enabled", "kms_key_not_publicly_accessible", - "rds_instance_certificate_expiration", "s3_bucket_kms_encryption", + "sagemaker_training_jobs_volume_and_output_encryption_enabled", "secretsmanager_automatic_rotation_enabled", "secretsmanager_not_publicly_accessible", "secretsmanager_secret_rotated_periodically", "secretsmanager_secret_unused", "sns_topics_kms_encryption_at_rest_enabled", - "storagegateway_fileshare_encryption_enabled", - "workspaces_volume_encryption_enabled" + "storagegateway_fileshare_encryption_enabled" ], "Attributes": [ { @@ -2477,7 +2269,7 @@ "Section": "2.8.4 시험 데이터 보안", "AuditChecklist": [ "정보시스템의 개발 및 시험 과정에서 실제 운영 데이터의 사용을 제한하고 있는가?", - "불가피하게 운영데이터를 시험 환경에서 사용할 경우 책임자 승인, 접근 및 유출 모니터링, 시험 후 데이터 삭제 등의 통제 절차를 수립·이행하고 있는가?" + "불가피하게 운영데이터를 시험 환경에서 사용할 경우 책임자 승인, 접근 및 유출 모니터링, 시험 후 데이터 삭제 등의 통제 절차를 수립·이행하고 있는가?" ], "RelatedRegulations": [], "AuditEvidence": [ @@ -2499,8 +2291,7 @@ "Name": "소스 프로그램 관리", "Description": "소스 프로그램은 인가된 사용자만이 접근할 수 있도록 관리하고, 운영환경에 보관하지 않는 것을 원칙으로 하여야 한다.", "Checks": [ - "codeartifact_packages_external_public_publishing_disabled", - "ecr_repositories_tag_immutability" + "codeartifact_packages_external_public_publishing_disabled" ], "Attributes": [ { @@ -2587,7 +2378,6 @@ "Name": "성능 및 장애관리", "Description": "정보시스템의 가용성 보장을 위하여 성능 및 용량 요구사항을 정의하고 현황을 지속적으로 모니터링하여야 하며, 장애 발생 시 효과적으로 대응하기 위한 탐지·기록·분석·복구·보고 등의 절차를 수립·관리하여야 한다.", "Checks": [ - "acm_certificates_expiration_check", "apigateway_restapi_tracing_enabled", "autoscaling_group_capacity_rebalance_enabled", "autoscaling_group_elb_health_check_enabled", @@ -2598,14 +2388,18 @@ "cloudfront_distributions_s3_origin_non_existent_bucket", "directconnect_connection_redundancy", "directconnect_virtual_interface_redundancy", - "dlm_ebs_snapshot_lifecycle_policy_exists", + "directoryservice_directory_snapshots_limit", "dms_instance_multi_az_enabled", "documentdb_cluster_deletion_protection", "documentdb_cluster_multi_az_enabled", "dynamodb_accelerator_cluster_multi_az", "dynamodb_table_autoscaling_enabled", "dynamodb_table_deletion_protection_enabled", + "ec2_elastic_ip_unassigned", "ec2_instance_detailed_monitoring_enabled", + "ec2_instance_older_than_specific_days", + "ec2_instance_paravirtual_type", + "ecr_repositories_lifecycle_policy_enabled", "ecs_cluster_container_insights_enabled", "ecs_task_definitions_containers_readonly_access", "ecs_task_definitions_logging_block_mode", @@ -2618,10 +2412,11 @@ "elbv2_cross_zone_load_balancing_enabled", "elbv2_deletion_protection", "elbv2_is_in_multiple_az", + "eventbridge_global_endpoint_event_replication_enabled", "fsx_windows_file_system_multi_az_enabled", - "iam_no_expired_server_certificates_stored", "kafka_cluster_enhanced_monitoring_enabled", "kms_cmk_not_deleted_unintentionally", + "lightsail_static_ip_unused", "mq_broker_active_deployment_mode", "mq_broker_cluster_deployment_mode", "neptune_cluster_deletion_protection", @@ -2632,21 +2427,13 @@ "opensearch_service_domains_fault_tolerant_master_nodes", "rds_cluster_deletion_protection", "rds_cluster_multi_az", - "rds_instance_certificate_expiration", "rds_instance_deletion_protection", - "rds_instance_deprecated_engine_version", + "rds_instance_enhanced_monitoring_enabled", "rds_instance_multi_az", "redshift_cluster_multi_az_enabled", + "resourceexplorer2_indexes_found", "s3_bucket_cross_region_replication", - "s3_bucket_no_mfa_delete", - "s3_bucket_object_lock", - "s3_bucket_object_versioning", "sagemaker_endpoint_config_prod_variant_instances", - "shield_advanced_protection_in_classic_load_balancers", - "shield_advanced_protection_in_global_accelerators", - "shield_advanced_protection_in_internet_facing_load_balancers", - "trustedadvisor_errors_and_warnings", - "trustedadvisor_premium_support_plan_subscribed", "vpc_different_regions", "vpc_endpoint_multi_az_enabled", "vpc_subnet_different_az", @@ -2659,7 +2446,7 @@ "Section": "2.9.2 성능 및 장애관리", "AuditChecklist": [ "정보시스템의 가용성 보장을 위하여 성능 및 용량을 지속적으로 모니터링할 수 있는 절차를 수립·이행하고 있는가?", - "정보시스템 성능 및 용량 요구사항(임계치)을 초과하는 경우에 대한 대응절차를 수립 및 이행하고 있는가?", + "정보시스템 성능 및 용량 요구사항(임계치)을 초과하는 경우에 대한 대응절차를 수립·이행하고 있는가?", "정보시스템 장애를 즉시 인지하고 대응하기 위한 절차를 수립·이행하고 있는가?", "장애 발생 시 절차에 따라 조치하고 장애조치보고서 등을 통하여 장애조치내역을 기록하여 관리하고 있는가?", "심각도가 높은 장애의 경우 원인분석을 통한 재발방지 대책을 마련하고 있는가?" @@ -2688,9 +2475,9 @@ "backup_plans_exist", "backup_recovery_point_encrypted", "backup_reportplans_exist", + "backup_vaults_encrypted", "backup_vaults_exist", "cloudfront_distributions_multiple_origin_failover_configured", - "directoryservice_directory_snapshots_limit", "dlm_ebs_snapshot_lifecycle_policy_exists", "documentdb_cluster_backup_enabled", "dynamodb_table_protected_by_backup_plan", @@ -2699,22 +2486,16 @@ "ec2_ebs_volume_snapshots_exists", "efs_have_backup_enabled", "elasticache_redis_cluster_backup_enabled", - "fsx_file_system_copy_tags_to_backups_enabled", "lightsail_instance_automated_snapshots", "neptune_cluster_backup_enabled", - "neptune_cluster_copy_tags_to_snapshots", - "neptune_cluster_public_snapshot", - "neptune_cluster_snapshot_encrypted", "rds_cluster_backtrack_enabled", - "rds_cluster_copy_tags_to_snapshots", "rds_cluster_protected_by_backup_plan", "rds_instance_backup_enabled", - "rds_instance_copy_tags_to_snapshots", "rds_instance_protected_by_backup_plan", - "rds_snapshots_encrypted", - "rds_snapshots_public_access", "redshift_cluster_automated_snapshot", - "s3_bucket_lifecycle_enabled" + "s3_bucket_lifecycle_enabled", + "s3_bucket_object_lock", + "s3_bucket_object_versioning" ], "Attributes": [ { @@ -2723,8 +2504,8 @@ "Section": "2.9.3 백업 및 복구관리", "AuditChecklist": [ "백업 대상, 주기, 방법, 절차 등이 포함된 백업 및 복구절차를 수립·이행하고 있는가?", - "백업된 정보의 완전성과 정확성, 복구절차의 적절성을 확인하기 위하여 정기적으로 복구 테스트를 실시하고 있는가?", - "중요정보가 저장된 백업매체의 경우 재해·재난에 대처할 수 있도록 백업매체를 물리적으로 떨어진 장소에 소산하고 있는가?" + "백업된 정보의 완전성과 정확성, 복구절차의 적절성을 확인하기 위하여 정기적으로 복구 테스트를 실시하고 있는가?", + "중요정보가 저장된 백업매체의 경우 재해·재난에 대처할 수 있도록 백업매체를 물리적으로 떨어진 장소에 소산하고 있는가?" ], "RelatedRegulations": [ "개인정보 보호법 제29조(안전조치 의무)", @@ -2756,7 +2537,6 @@ "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", "bedrock_model_invocation_logging_enabled", "bedrock_model_invocation_logs_encryption_enabled", - "cloudformation_stack_outputs_find_secrets", "cloudfront_distributions_logging_enabled", "cloudtrail_bucket_requires_mfa_delete", "cloudtrail_cloudwatch_logging_enabled", @@ -2776,14 +2556,11 @@ "cloudwatch_changes_to_network_gateways_alarm_configured", "cloudwatch_changes_to_network_route_tables_alarm_configured", "cloudwatch_changes_to_vpcs_alarm_configured", - "cloudwatch_log_group_kms_encryption_enabled", "cloudwatch_log_group_no_critical_pii_in_logs", "cloudwatch_log_group_no_secrets_in_logs", - "cloudwatch_log_group_not_publicly_accessible", "cloudwatch_log_group_retention_policy_specific_days_enabled", "codebuild_project_logging_enabled", "codebuild_project_s3_logs_encrypted", - "config_recorder_all_regions_enabled", "datasync_task_logging_enabled", "directoryservice_directory_log_forwarding_enabled", "directoryservice_directory_monitor_notifications", @@ -2795,22 +2572,14 @@ "elasticbeanstalk_environment_cloudwatch_logging_enabled", "elb_logging_enabled", "elbv2_logging_enabled", - "eventbridge_global_endpoint_event_replication_enabled", - "glue_development_endpoints_cloudwatch_logs_encryption_enabled", - "glue_etl_jobs_cloudwatch_logs_encryption_enabled", "glue_etl_jobs_logging_enabled", - "kafka_cluster_enhanced_monitoring_enabled", + "guardduty_eks_audit_log_enabled", "mq_broker_logging_enabled", "neptune_cluster_integration_cloudwatch_logs", "networkfirewall_logging_enabled", "opensearch_service_domains_audit_logging_enabled", "opensearch_service_domains_cloudwatch_logging_enabled", - "rds_cluster_critical_event_subscription", "rds_cluster_integration_cloudwatch_logs", - "rds_instance_critical_event_subscription", - "rds_instance_enhanced_monitoring_enabled", - "rds_instance_event_subscription_parameter_groups", - "rds_instance_event_subscription_security_groups", "rds_instance_integration_cloudwatch_logs", "redshift_cluster_audit_logging", "route53_public_hosted_zones_cloudwatch_logging_enabled", @@ -2862,7 +2631,7 @@ "Subdomain": "2.9 시스템 및 서비스 운영관리", "Section": "2.9.5 로그 및 접속기록 점검", "AuditChecklist": [ - "정보시스템 관련 오류, 오·남용(비인가접속, 과다조회 등), 부정행위 등 이상징후를 인지할 수 있도록 로그 검토 주기, 대상, 방법 등을 포함한 로그 검토 및 모니터링 절차를 수립·이행하고 있는가?", + "정보시스템 관련 오류, 오·남용(비인가접속, 과다조회 등), 부정행위 등 이상징후를 인지할 수 있도록 로그 검토 주기, 대상, 방법 등을 포함한 로그 검토 및 모니터링 절차를 수립·이행하고 있는가?", "로그 검토 및 모니터링 결과를 책임자에게 보고하고 이상징후 발견 시 절차에 따라 대응하고 있는가?", "개인정보처리시스템의 접속기록은 관련 법령에서 정한 주기에 따라 정기적으로 점검하고 있는가?" ], @@ -2954,8 +2723,6 @@ "Description": "보안시스템 유형별로 관리자 지정, 최신 정책 업데이트, 룰셋 변경, 이벤트 모니터링 등의 운영절차를 수립·이행하고 보안시스템별 정책적용 현황을 관리하여야 한다.", "Checks": [ "apigateway_restapi_waf_acl_attached", - "autoscaling_group_multiple_az", - "autoscaling_group_multiple_instance_types", "cloudfront_distributions_using_waf", "cloudtrail_bucket_requires_mfa_delete", "cloudtrail_cloudwatch_logging_enabled", @@ -3001,8 +2768,6 @@ "dms_instance_minor_version_upgrade_enabled", "dms_instance_multi_az_enabled", "dms_instance_no_public_access", - "ec2_client_vpn_endpoint_connection_logging_enabled", - "ec2_instance_managed_by_ssm", "elbv2_waf_acl_attached", "fms_policy_compliant", "guardduty_centrally_managed", @@ -3037,7 +2802,6 @@ "shield_advanced_protection_in_internet_facing_load_balancers", "shield_advanced_protection_in_route53_hosted_zones", "trustedadvisor_errors_and_warnings", - "trustedadvisor_premium_support_plan_subscribed", "waf_global_rule_with_conditions", "waf_global_rulegroup_not_empty", "waf_global_webacl_logging_enabled", @@ -3057,7 +2821,7 @@ "AuditChecklist": [ "조직에서 운영하고 있는 보안시스템에 대한 운영절차를 수립·이행하고 있는가?", "보안시스템 관리자 등 접근이 허용된 인원을 최소화하고 비인가자의 접근을 엄격하게 통제하고 있는가?", - "보안시스템별로 정책의 신규 등록, 변경, 삭제 등을 위한 공식적인 절차를 수립 및 이행하고 있는가?", + "보안시스템별로 정책의 신규 등록, 변경, 삭제 등을 위한 공식적인 절차를 수립·이행하고 있는가?", "보안시스템의 예외 정책 등록에 대하여 절차에 따라 관리하고 있으며, 예외 정책 사용자에 대하여 최소한의 권한으로 관리하고 있는가?", "보안시스템에 설정된 정책의 타당성 여부를 주기적으로 검토하고 있는가?", "개인정보처리시스템에 대한 불법적인 접근 및 개인정보 유출 방지를 위하여 관련 법령에서 정한 기능을 수행하는 보안시스템을 설치하여 운영하고 있는가?" @@ -3105,10 +2869,8 @@ "apigateway_restapi_logging_enabled", "apigateway_restapi_public", "apigateway_restapi_public_with_authorizer", - "apigateway_restapi_tracing_enabled", "apigateway_restapi_waf_acl_attached", "apigatewayv2_api_access_logging_enabled", - "apigatewayv2_api_authorizers_enabled", "appstream_fleet_default_internet_access_disabled", "appstream_fleet_maximum_session_duration", "appstream_fleet_session_disconnect_timeout", @@ -3116,13 +2878,8 @@ "athena_workgroup_encryption", "athena_workgroup_enforce_configuration", "athena_workgroup_logging_enabled", - "autoscaling_group_capacity_rebalance_enabled", - "autoscaling_group_elb_health_check_enabled", "autoscaling_group_launch_configuration_no_public_ip", "autoscaling_group_launch_configuration_requires_imdsv2", - "autoscaling_group_multiple_az", - "autoscaling_group_multiple_instance_types", - "autoscaling_group_using_ec2_launch_template", "awslambda_function_inside_vpc", "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", "awslambda_function_no_secrets_in_code", @@ -3131,19 +2888,14 @@ "awslambda_function_url_cors_policy", "awslambda_function_url_public", "awslambda_function_using_supported_runtimes", - "awslambda_function_vpc_multi_az", - "backup_plans_exist", "backup_recovery_point_encrypted", - "backup_reportplans_exist", "backup_vaults_encrypted", - "backup_vaults_exist", "bedrock_agent_guardrail_enabled", "bedrock_guardrail_prompt_attack_filter_enabled", "bedrock_guardrail_sensitive_information_filter_enabled", "bedrock_model_invocation_logging_enabled", "bedrock_model_invocation_logs_encryption_enabled", "cloudformation_stack_outputs_find_secrets", - "cloudformation_stacks_termination_protection_enabled", "cloudfront_distributions_custom_ssl_certificate", "cloudfront_distributions_default_root_object", "cloudfront_distributions_field_level_encryption_enabled", @@ -3151,10 +2903,8 @@ "cloudfront_distributions_https_enabled", "cloudfront_distributions_https_sni_enabled", "cloudfront_distributions_logging_enabled", - "cloudfront_distributions_multiple_origin_failover_configured", "cloudfront_distributions_origin_traffic_encrypted", "cloudfront_distributions_s3_origin_access_control", - "cloudfront_distributions_s3_origin_non_existent_bucket", "cloudfront_distributions_using_deprecated_ssl_protocols", "cloudfront_distributions_using_waf", "cloudtrail_bucket_requires_mfa_delete", @@ -3163,7 +2913,6 @@ "cloudtrail_kms_encryption_enabled", "cloudtrail_log_file_validation_enabled", "cloudtrail_logs_s3_bucket_access_logging_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", "cloudtrail_multi_region_enabled", "cloudtrail_multi_region_enabled_logging_management_events", "cloudtrail_s3_dataevents_read_enabled", @@ -3197,10 +2946,8 @@ "codeartifact_packages_external_public_publishing_disabled", "codebuild_project_logging_enabled", "codebuild_project_no_secrets_in_variables", - "codebuild_project_older_90_days", "codebuild_project_s3_logs_encrypted", "codebuild_project_source_repo_url_no_sensitive_credentials", - "codebuild_project_user_controlled_buildspec", "codebuild_report_group_export_encrypted", "cognito_identity_pool_guest_access_disabled", "cognito_user_pool_advanced_security_enabled", @@ -3221,37 +2968,24 @@ "config_recorder_all_regions_enabled", "config_recorder_using_aws_service_role", "datasync_task_logging_enabled", - "directconnect_connection_redundancy", - "directconnect_virtual_interface_redundancy", "directoryservice_directory_log_forwarding_enabled", "directoryservice_directory_monitor_notifications", - "directoryservice_directory_snapshots_limit", "directoryservice_ldap_certificate_expiration", "directoryservice_radius_server_security_protocol", "directoryservice_supported_mfa_radius_enabled", - "dlm_ebs_snapshot_lifecycle_policy_exists", "dms_endpoint_mongodb_authentication_enabled", "dms_endpoint_neptune_iam_authorization_enabled", "dms_endpoint_ssl_enabled", "dms_instance_minor_version_upgrade_enabled", - "dms_instance_multi_az_enabled", "dms_instance_no_public_access", - "documentdb_cluster_backup_enabled", "documentdb_cluster_cloudwatch_log_export", - "documentdb_cluster_deletion_protection", - "documentdb_cluster_multi_az_enabled", "documentdb_cluster_public_snapshot", "documentdb_cluster_storage_encrypted", "drs_job_exist", "dynamodb_accelerator_cluster_encryption_enabled", "dynamodb_accelerator_cluster_in_transit_encryption_enabled", - "dynamodb_accelerator_cluster_multi_az", - "dynamodb_table_autoscaling_enabled", "dynamodb_table_cross_account_access", - "dynamodb_table_deletion_protection_enabled", - "dynamodb_table_protected_by_backup_plan", "dynamodb_tables_kms_cmk_encryption_enabled", - "dynamodb_tables_pitr_enabled", "ec2_ami_public", "ec2_client_vpn_endpoint_connection_logging_enabled", "ec2_ebs_default_encryption", @@ -3259,17 +2993,11 @@ "ec2_ebs_snapshot_account_block_public_access", "ec2_ebs_snapshots_encrypted", "ec2_ebs_volume_encryption", - "ec2_ebs_volume_protected_by_backup_plan", - "ec2_ebs_volume_snapshots_exists", "ec2_elastic_ip_shodan", - "ec2_elastic_ip_unassigned", "ec2_instance_account_imdsv2_enabled", - "ec2_instance_detailed_monitoring_enabled", "ec2_instance_imdsv2_enabled", "ec2_instance_internet_facing_with_instance_profile", "ec2_instance_managed_by_ssm", - "ec2_instance_older_than_specific_days", - "ec2_instance_paravirtual_type", "ec2_instance_port_cassandra_exposed_to_internet", "ec2_instance_port_cifs_exposed_to_internet", "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", @@ -3321,17 +3049,12 @@ "ec2_securitygroup_with_many_ingress_egress_rules", "ec2_transitgateway_auto_accept_vpc_attachments", "ecr_registry_scan_images_on_push_enabled", - "ecr_repositories_lifecycle_policy_enabled", "ecr_repositories_not_publicly_accessible", "ecr_repositories_scan_vulnerabilities_in_latest_image", - "ecr_repositories_tag_immutability", - "ecs_cluster_container_insights_enabled", "ecs_service_fargate_latest_platform_version", "ecs_service_no_assign_public_ip", - "ecs_task_definitions_containers_readonly_access", "ecs_task_definitions_host_namespace_not_shared", "ecs_task_definitions_host_networking_mode_users", - "ecs_task_definitions_logging_block_mode", "ecs_task_definitions_logging_enabled", "ecs_task_definitions_no_environment_secrets", "ecs_task_definitions_no_privileged_containers", @@ -3339,7 +3062,6 @@ "efs_access_point_enforce_root_directory", "efs_access_point_enforce_user_identity", "efs_encryption_at_rest_enabled", - "efs_have_backup_enabled", "efs_mount_target_not_publicly_accessible", "efs_not_publicly_accessible", "eks_cluster_kms_cmk_encryption_in_secrets_enabled", @@ -3350,30 +3072,20 @@ "eks_control_plane_logging_all_types_enabled", "elasticache_cluster_uses_public_subnet", "elasticache_redis_cluster_auto_minor_version_upgrades", - "elasticache_redis_cluster_automatic_failover_enabled", - "elasticache_redis_cluster_backup_enabled", "elasticache_redis_cluster_in_transit_encryption_enabled", - "elasticache_redis_cluster_multi_az_enabled", "elasticache_redis_cluster_rest_encryption_enabled", "elasticache_redis_replication_group_auth_enabled", "elasticbeanstalk_environment_cloudwatch_logging_enabled", - "elasticbeanstalk_environment_enhanced_health_reporting", "elasticbeanstalk_environment_managed_updates_enabled", - "elb_connection_draining_enabled", - "elb_cross_zone_load_balancing_enabled", "elb_desync_mitigation_mode", "elb_insecure_ssl_ciphers", "elb_internet_facing", - "elb_is_in_multiple_az", "elb_logging_enabled", "elb_ssl_listeners", "elb_ssl_listeners_use_acm_certificate", - "elbv2_cross_zone_load_balancing_enabled", - "elbv2_deletion_protection", "elbv2_desync_mitigation_mode", "elbv2_insecure_ssl_ciphers", "elbv2_internet_facing", - "elbv2_is_in_multiple_az", "elbv2_listeners_underneath", "elbv2_logging_enabled", "elbv2_nlb_tls_termination_enabled", @@ -3384,12 +3096,8 @@ "emr_cluster_publicly_accesible", "eventbridge_bus_cross_account_access", "eventbridge_bus_exposed", - "eventbridge_global_endpoint_event_replication_enabled", "eventbridge_schema_registry_cross_account_access", "fms_policy_compliant", - "fsx_file_system_copy_tags_to_backups_enabled", - "fsx_file_system_copy_tags_to_volumes_enabled", - "fsx_windows_file_system_multi_az_enabled", "glacier_vaults_policy_public_access", "glue_data_catalogs_connection_passwords_encryption_enabled", "glue_data_catalogs_metadata_encryption_enabled", @@ -3457,7 +3165,6 @@ "inspector2_active_findings_exist", "inspector2_is_enabled", "kafka_cluster_encryption_at_rest_uses_cmk", - "kafka_cluster_enhanced_monitoring_enabled", "kafka_cluster_in_transit_encryption_enabled", "kafka_cluster_is_public", "kafka_cluster_mutual_tls_authentication_enabled", @@ -3466,25 +3173,16 @@ "kafka_connector_in_transit_encryption_enabled", "kinesis_stream_encrypted_at_rest", "kms_cmk_are_used", - "kms_cmk_not_deleted_unintentionally", "kms_cmk_rotation_enabled", "kms_key_not_publicly_accessible", "lightsail_database_public", - "lightsail_instance_automated_snapshots", "lightsail_instance_public", - "lightsail_static_ip_unused", "macie_automated_sensitive_data_discovery_enabled", "macie_is_enabled", - "mq_broker_active_deployment_mode", "mq_broker_auto_minor_version_upgrades", - "mq_broker_cluster_deployment_mode", "mq_broker_logging_enabled", - "neptune_cluster_backup_enabled", - "neptune_cluster_copy_tags_to_snapshots", - "neptune_cluster_deletion_protection", "neptune_cluster_iam_authentication_enabled", "neptune_cluster_integration_cloudwatch_logs", - "neptune_cluster_multi_az", "neptune_cluster_public_snapshot", "neptune_cluster_snapshot_encrypted", "neptune_cluster_storage_encrypted", @@ -3492,15 +3190,12 @@ "networkfirewall_deletion_protection", "networkfirewall_in_all_vpc", "networkfirewall_logging_enabled", - "networkfirewall_multi_az", "networkfirewall_policy_default_action_fragmented_packets", "networkfirewall_policy_default_action_full_packets", "networkfirewall_policy_rule_group_associated", "opensearch_service_domains_audit_logging_enabled", "opensearch_service_domains_cloudwatch_logging_enabled", "opensearch_service_domains_encryption_at_rest_enabled", - "opensearch_service_domains_fault_tolerant_data_nodes", - "opensearch_service_domains_fault_tolerant_master_nodes", "opensearch_service_domains_https_communications_enforced", "opensearch_service_domains_internal_user_database_enabled", "opensearch_service_domains_node_to_node_encryption_enabled", @@ -3511,52 +3206,33 @@ "organizations_delegated_administrators", "organizations_opt_out_ai_services_policy", "organizations_scp_check_deny_regions", - "organizations_tags_policies_enabled_and_attached", - "rds_cluster_backtrack_enabled", - "rds_cluster_copy_tags_to_snapshots", "rds_cluster_critical_event_subscription", - "rds_cluster_default_admin", - "rds_cluster_deletion_protection", "rds_cluster_iam_authentication_enabled", "rds_cluster_integration_cloudwatch_logs", "rds_cluster_minor_version_upgrade_enabled", - "rds_cluster_multi_az", "rds_cluster_non_default_port", - "rds_cluster_protected_by_backup_plan", "rds_cluster_storage_encrypted", - "rds_instance_backup_enabled", "rds_instance_certificate_expiration", - "rds_instance_copy_tags_to_snapshots", "rds_instance_critical_event_subscription", - "rds_instance_default_admin", - "rds_instance_deletion_protection", "rds_instance_deprecated_engine_version", - "rds_instance_enhanced_monitoring_enabled", "rds_instance_event_subscription_parameter_groups", "rds_instance_event_subscription_security_groups", "rds_instance_iam_authentication_enabled", "rds_instance_inside_vpc", "rds_instance_integration_cloudwatch_logs", "rds_instance_minor_version_upgrade_enabled", - "rds_instance_multi_az", "rds_instance_no_public_access", "rds_instance_non_default_port", - "rds_instance_protected_by_backup_plan", "rds_instance_storage_encrypted", "rds_instance_transport_encrypted", "rds_snapshots_encrypted", "rds_snapshots_public_access", "redshift_cluster_audit_logging", - "redshift_cluster_automated_snapshot", "redshift_cluster_automatic_upgrades", "redshift_cluster_encrypted_at_rest", "redshift_cluster_enhanced_vpc_routing", "redshift_cluster_in_transit_encryption_enabled", - "redshift_cluster_multi_az_enabled", - "redshift_cluster_non_default_database_name", - "redshift_cluster_non_default_username", "redshift_cluster_public_access", - "resourceexplorer2_indexes_found", "route53_dangling_ip_subdomain_takeover", "route53_domains_privacy_protection_enabled", "route53_domains_transferlock_enabled", @@ -3565,15 +3241,11 @@ "s3_account_level_public_access_blocks", "s3_bucket_acl_prohibited", "s3_bucket_cross_account_access", - "s3_bucket_cross_region_replication", "s3_bucket_default_encryption", "s3_bucket_event_notifications_enabled", "s3_bucket_kms_encryption", "s3_bucket_level_public_access_block", - "s3_bucket_lifecycle_enabled", "s3_bucket_no_mfa_delete", - "s3_bucket_object_lock", - "s3_bucket_object_versioning", "s3_bucket_policy_public_write_access", "s3_bucket_public_access", "s3_bucket_public_list_acl", @@ -3581,7 +3253,6 @@ "s3_bucket_secure_transport_policy", "s3_bucket_server_access_logging_enabled", "s3_multi_region_access_point_public_access_block", - "sagemaker_endpoint_config_prod_variant_instances", "sagemaker_models_network_isolation_enabled", "sagemaker_models_vpc_settings_configured", "sagemaker_notebook_instance_encryption_enabled", @@ -3616,18 +3287,13 @@ "storagegateway_fileshare_encryption_enabled", "transfer_server_in_transit_encryption_enabled", "trustedadvisor_errors_and_warnings", - "trustedadvisor_premium_support_plan_subscribed", - "vpc_different_regions", "vpc_endpoint_connections_trust_boundaries", "vpc_endpoint_for_ec2_enabled", - "vpc_endpoint_multi_az_enabled", "vpc_endpoint_services_allowed_principals_trust_boundaries", "vpc_flow_logs_enabled", "vpc_peering_routing_tables_with_least_privilege", - "vpc_subnet_different_az", "vpc_subnet_no_public_ip_by_default", "vpc_subnet_separate_private_public", - "vpc_vpn_connection_tunnels_up", "waf_global_rule_with_conditions", "waf_global_rulegroup_not_empty", "waf_global_webacl_logging_enabled", @@ -3648,10 +3314,10 @@ "Subdomain": "2.10 시스템 및 서비스 보안관리", "Section": "2.10.2 클라우드 보안", "AuditChecklist": [ - "클라우드 서비스 제공자와 정보보호 및 개인정보보호에 대한 책임과 역할을 명확히 정의하고 이를 계약서(SLA 등)에 반영하고 있는가?", - "클라우드 서비스 이용 시 서비스 유형에 따른 보안위험을 평가하여 비인가 접근, 설정오류 등을 방지할 수 있도록 보안 구성 및 설정 기준, 보안설정 변경 및 승인 절차, 안전한 접속방법, 권한 체계 등 보안 통제 정책을 수립·이행하고 있는가?", - "클라우드 서비스 관리자 권한은 역할에 따라 최소화하여 부여하고 관리자 권한에 대한 비인가 접근, 권한 오·남용 등을 방지할 수 있도록 강화된 인증, 암호화, 접근통제, 감사기록 등 보호대책을 적용하고 있는가?", - "클라우드 서비스의 보안 설정 변경, 운영 현황 등을 모니터링하고 그 적절성을 정기적으로 검토하고 있는가?" + "클라우드 서비스 제공자와 정보보호 및 개인정보보호에 대한 책임과 역할을 명확히 정의하고 이를 계약서(SLA 등)에 반영하고 있는가?", + "클라우드 서비스 이용 시 서비스 유형에 따른 보안위험을 평가하여 비인가 접근, 설정오류 등을 방지할 수 있도록 보안 구성 및 설정 기준, 보안설정 변경 및 승인 절차, 안전한 접속방법, 권한 체계 등 보안 통제 정책을 수립·이행하고 있는가?", + "클라우드 서비스 관리자 권한은 역할에 따라 최소화하여 부여하고 관리자 권한에 대한 비인가 접근, 권한 오·남용 등을 방지할 수 있도록 강화된 인증, 암호화, 접근통제, 감사기록 등 보호대책을 적용하고 있는가?", + "클라우드 서비스의 보안 설정 변경, 운영 현황 등을 모니터링하고 그 적절성을 정기적으로 검토하고 있는가?" ], "RelatedRegulations": [], "AuditEvidence": [ @@ -3676,41 +3342,7 @@ "Id": "2.10.3", "Name": "공개서버 보안", "Description": "외부 네트워크에 공개되는 서버의 경우 내부 네트워크와 분리하고 취약점 점검, 접근통제, 인증, 정보 수집·저장·공개 절차 등 강화된 보호대책을 수립·이행하여야 한다.", - "Checks": [ - "acm_certificates_expiration_check", - "acm_certificates_transparency_logs_enabled", - "acm_certificates_with_secure_key_algorithms", - "apigateway_restapi_client_certificate_enabled", - "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", - "apigateway_restapi_waf_acl_attached", - "cloudfront_distributions_s3_origin_non_existent_bucket", - "cloudfront_distributions_using_waf", - "cognito_user_pool_waf_acl_attached", - "elb_desync_mitigation_mode", - "elb_insecure_ssl_ciphers", - "elb_internet_facing", - "elb_ssl_listeners", - "elb_ssl_listeners_use_acm_certificate", - "elbv2_desync_mitigation_mode", - "elbv2_insecure_ssl_ciphers", - "elbv2_internet_facing", - "elbv2_listeners_underneath", - "elbv2_nlb_tls_termination_enabled", - "elbv2_ssl_listeners", - "elbv2_waf_acl_attached", - "lightsail_database_public", - "lightsail_instance_public", - "lightsail_static_ip_unused", - "route53_dangling_ip_subdomain_takeover", - "route53_domains_privacy_protection_enabled", - "shield_advanced_protection_in_associated_elastic_ips", - "shield_advanced_protection_in_classic_load_balancers", - "shield_advanced_protection_in_cloudfront_distributions", - "shield_advanced_protection_in_global_accelerators", - "shield_advanced_protection_in_internet_facing_load_balancers", - "shield_advanced_protection_in_route53_hosted_zones" - ], + "Checks": [], "Attributes": [ { "Domain": "2. 보호대책 요구사항", @@ -3747,7 +3379,7 @@ "Subdomain": "2.10 시스템 및 서비스 보안관리", "Section": "2.10.4 전자거래 및 핀테크 보안", "AuditChecklist": [ - "전자거래 및 핀테크 서비스를 제공하는 경우 거래의 안전성과 신뢰성 확보를 위한 보호대책을 수립·이행하고 있는가?", + "전자거래 및 핀테크 서비스를 제공하는 경우 거래의 안전성과 신뢰성 확보를 위한 보호대책을 수립·이행하고 있는가?", "전자거래 및 핀테크 서비스 제공을 위하여 결제시스템 등 외부 시스템과 연계하는 경우 송수신되는 관련 정보의 보호를 위한 대책을 수립·이행하고 안전성을 점검하고 있는가?" ], "RelatedRegulations": [], @@ -3756,8 +3388,8 @@ "결제시스템 연계 시 보안성 검토 결과" ], "NonComplianceCases": [ - "사례 1 : 전자결제대행업체와 위탁 계약을 맺고 연계를 하였으나, 적절한 인증 및 접근제한 없이 특정 URL을 통하여 결제 관련 정보가 모두 평문으로 전송되는 경우,", - "사례 2 : 전자결제대행업체와 외부 연계 시스템이 전용망으로 연결되어 있으나, 해당 연계 시스템에서 내부 업무 시스템으로의 접근이 침입차단시스템 등으로 적절히 통제되지 않고 있는 경우,", + "사례 1 : 전자결제대행업체와 위탁 계약을 맺고 연계를 하였으나, 적절한 인증 및 접근제한 없이 특정 URL을 통하여 결제 관련 정보가 모두 평문으로 전송되는 경우", + "사례 2 : 전자결제대행업체와 외부 연계 시스템이 전용망으로 연결되어 있으나, 해당 연계 시스템에서 내부 업무 시스템으로의 접근이 침입차단시스템 등으로 적절히 통제되지 않고 있는 경우", "사례 3 : 내부 지침에는 외부 핀테크 서비스 연계 시 정보보호팀의 보안성 검토를 받도록 되어 있으나, 최근에 신규 핀테크 서비스를 연계하면서 일정상 이유로 보안성 검토를 수행하지 않은 경우" ] } @@ -3802,7 +3434,7 @@ "Section": "2.10.6 업무용 단말기기 보안", "AuditChecklist": [ "PC, 노트북, 가상PC, 태블릿 등 업무에 사용되는 단말기에 대하여 기기인증, 승인, 접근범위 설정, 기기 보안설정 등의 보안 통제 정책을 수립·이행하고 있는가?", - "업무용 단말기를 통하여 개인정보 및 중요정보가 유출되는 것을 방지하기 위하여 자료공유프로그램 사용 금지, 공유설정 제한, 무선망 이용 통제 등의 정책을 수립 및 이행하고 있는가?", + "업무용 단말기를 통하여 개인정보 및 중요정보가 유출되는 것을 방지하기 위하여 자료공유프로그램 사용 금지, 공유설정 제한, 무선망 이용 통제 등의 정책을 수립·이행하고 있는가?", "업무용 모바일 기기의 분실, 도난 등으로 인한 개인정보 및 중요정보의 유·노출을 방지하기 위하여 보안대책을 적용하고 있는가?", "업무용 단말기기에 대한 접근통제 대책의 적절성에 대하여 주기적으로 점검하고 있는가?" ], @@ -3913,7 +3545,7 @@ { "Id": "2.10.9", "Name": "악성코드 통제", - "Description": "바이러스·웜·트로이목마·랜섬웨어 등의 악성코드로부터 개인정보 및 중요정보, 정보시스템 및 업무용 단말기 등을 보호하기 위하여 악성코드 예방·탐지·대응 등의 보호대책을 수립 및 이행하여야 한다.", + "Description": "바이러스·웜·트로이목마·랜섬웨어 등의 악성코드로부터 개인정보 및 중요정보, 정보시스템 및 업무용 단말기 등을 보호하기 위하여 악성코드 예방·탐지·대응 등의 보호대책을 수립·이행하여야 한다.", "Checks": [ "guardduty_ec2_malware_protection_enabled" ], @@ -3926,7 +3558,7 @@ "바이러스, 웜, 트로이목마, 랜섬웨어 등의 악성코드로부터 정보시스템 및 업무용단말기 등을 보호하기 위하여 보호대책을 수립·이행하고 있는가?", "백신 소프트웨어 등 보안프로그램을 통하여 최신 악성코드 예방·탐지 활동을 지속적으로 수행하고 있는가?", "백신 소프트웨어 등 보안프로그램은 최신의 상태로 유지하고 필요시 긴급 보안 업데이트를 수행하고 있는가?", - "악성코드 감염 발견 시 악성코드 확산 및 피해 최소화 등의 대응절차를 수립·이행하고 있는가?" + "악성코드 감염 발견 시 악성코드 확산 및 피해 최소화 등의 대응절차를 수립·이행하고 있는가?" ], "RelatedRegulations": [ "개인정보 보호법 제29조(안전조치의무)", @@ -3952,12 +3584,6 @@ "Name": "사고 예방 및 대응체계 구축", "Description": "침해사고 및 개인정보 유출 등을 예방하고 사고 발생 시 신속하고 효과적으로 대응할 수 있도록 내·외부 침해시도의 탐지·대응·분석 및 공유를 위한 체계와 절차를 수립하고, 관련 외부기관 및 전문가들과 협조체계를 구축하여야 한다.", "Checks": [ - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered", - "account_security_questions_are_registered_in_the_aws_account", - "acm_certificates_with_secure_key_algorithms", - "guardduty_centrally_managed", "iam_support_role_created", "ssmincidents_enabled_with_plans" ], @@ -3996,557 +3622,9 @@ "Name": "취약점 점검 및 조치", "Description": "정보시스템의 취약점이 노출되어 있는지를 확인하기 위하여 정기적으로 취약점 점검을 수행하고, 발견된 취약점에 대해서는 신속하게 조치하여야 한다. 또한 최신 보안취약점의 발생 여부를 지속적으로 파악하고, 정보시스템에 미치는 영향을 분석하여 조치하여야 한다.", "Checks": [ - "accessanalyzer_enabled", - "accessanalyzer_enabled_without_findings", - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered", - "account_security_questions_are_registered_in_the_aws_account", - "acm_certificates_expiration_check", - "acm_certificates_transparency_logs_enabled", - "acm_certificates_with_secure_key_algorithms", - "apigateway_restapi_authorizers_enabled", - "apigateway_restapi_cache_encrypted", - "apigateway_restapi_client_certificate_enabled", - "apigateway_restapi_logging_enabled", - "apigateway_restapi_public", - "apigateway_restapi_public_with_authorizer", - "apigateway_restapi_tracing_enabled", - "apigateway_restapi_waf_acl_attached", - "apigatewayv2_api_access_logging_enabled", - "apigatewayv2_api_authorizers_enabled", - "appstream_fleet_default_internet_access_disabled", - "appstream_fleet_maximum_session_duration", - "appstream_fleet_session_disconnect_timeout", - "appstream_fleet_session_idle_disconnect_timeout", - "athena_workgroup_encryption", - "athena_workgroup_enforce_configuration", - "athena_workgroup_logging_enabled", - "autoscaling_group_capacity_rebalance_enabled", - "autoscaling_group_elb_health_check_enabled", - "autoscaling_group_launch_configuration_no_public_ip", - "autoscaling_group_launch_configuration_requires_imdsv2", - "autoscaling_group_multiple_az", - "autoscaling_group_multiple_instance_types", - "autoscaling_group_using_ec2_launch_template", - "awslambda_function_inside_vpc", - "awslambda_function_invoke_api_operations_cloudtrail_logging_enabled", - "awslambda_function_no_secrets_in_code", - "awslambda_function_no_secrets_in_variables", - "awslambda_function_not_publicly_accessible", - "awslambda_function_url_cors_policy", - "awslambda_function_url_public", - "awslambda_function_using_supported_runtimes", - "awslambda_function_vpc_multi_az", - "backup_plans_exist", - "backup_recovery_point_encrypted", - "backup_reportplans_exist", - "backup_vaults_encrypted", - "backup_vaults_exist", - "bedrock_agent_guardrail_enabled", - "bedrock_guardrail_prompt_attack_filter_enabled", - "bedrock_guardrail_sensitive_information_filter_enabled", - "bedrock_model_invocation_logging_enabled", - "bedrock_model_invocation_logs_encryption_enabled", - "cloudformation_stack_outputs_find_secrets", - "cloudformation_stacks_termination_protection_enabled", - "cloudfront_distributions_custom_ssl_certificate", - "cloudfront_distributions_default_root_object", - "cloudfront_distributions_field_level_encryption_enabled", - "cloudfront_distributions_geo_restrictions_enabled", - "cloudfront_distributions_https_enabled", - "cloudfront_distributions_https_sni_enabled", - "cloudfront_distributions_logging_enabled", - "cloudfront_distributions_multiple_origin_failover_configured", - "cloudfront_distributions_origin_traffic_encrypted", - "cloudfront_distributions_s3_origin_access_control", - "cloudfront_distributions_s3_origin_non_existent_bucket", - "cloudfront_distributions_using_deprecated_ssl_protocols", - "cloudfront_distributions_using_waf", - "cloudtrail_bucket_requires_mfa_delete", - "cloudtrail_cloudwatch_logging_enabled", - "cloudtrail_insights_exist", - "cloudtrail_kms_encryption_enabled", - "cloudtrail_log_file_validation_enabled", - "cloudtrail_logs_s3_bucket_access_logging_enabled", - "cloudtrail_logs_s3_bucket_is_not_publicly_accessible", - "cloudtrail_multi_region_enabled", - "cloudtrail_multi_region_enabled_logging_management_events", - "cloudtrail_s3_dataevents_read_enabled", - "cloudtrail_s3_dataevents_write_enabled", - "cloudtrail_threat_detection_enumeration", - "cloudtrail_threat_detection_llm_jacking", - "cloudtrail_threat_detection_privilege_escalation", - "cloudwatch_alarm_actions_alarm_state_configured", - "cloudwatch_alarm_actions_enabled", - "cloudwatch_changes_to_network_acls_alarm_configured", - "cloudwatch_changes_to_network_gateways_alarm_configured", - "cloudwatch_changes_to_network_route_tables_alarm_configured", - "cloudwatch_changes_to_vpcs_alarm_configured", - "cloudwatch_cross_account_sharing_disabled", - "cloudwatch_log_group_kms_encryption_enabled", - "cloudwatch_log_group_no_critical_pii_in_logs", - "cloudwatch_log_group_no_secrets_in_logs", - "cloudwatch_log_group_not_publicly_accessible", - "cloudwatch_log_group_retention_policy_specific_days_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_aws_config_configuration_changes_enabled", - "cloudwatch_log_metric_filter_and_alarm_for_cloudtrail_configuration_changes_enabled", - "cloudwatch_log_metric_filter_authentication_failures", - "cloudwatch_log_metric_filter_aws_organizations_changes", - "cloudwatch_log_metric_filter_disable_or_scheduled_deletion_of_kms_cmk", - "cloudwatch_log_metric_filter_for_s3_bucket_policy_changes", - "cloudwatch_log_metric_filter_policy_changes", - "cloudwatch_log_metric_filter_root_usage", - "cloudwatch_log_metric_filter_security_group_changes", - "cloudwatch_log_metric_filter_sign_in_without_mfa", - "cloudwatch_log_metric_filter_unauthorized_api_calls", - "codeartifact_packages_external_public_publishing_disabled", - "codebuild_project_logging_enabled", - "codebuild_project_no_secrets_in_variables", - "codebuild_project_older_90_days", - "codebuild_project_s3_logs_encrypted", - "codebuild_project_source_repo_url_no_sensitive_credentials", - "codebuild_project_user_controlled_buildspec", - "codebuild_report_group_export_encrypted", - "cognito_identity_pool_guest_access_disabled", - "cognito_user_pool_advanced_security_enabled", - "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", - "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", - "cognito_user_pool_client_prevent_user_existence_errors", - "cognito_user_pool_client_token_revocation_enabled", - "cognito_user_pool_deletion_protection_enabled", - "cognito_user_pool_mfa_enabled", - "cognito_user_pool_password_policy_lowercase", - "cognito_user_pool_password_policy_minimum_length_14", - "cognito_user_pool_password_policy_number", - "cognito_user_pool_password_policy_symbol", - "cognito_user_pool_password_policy_uppercase", - "cognito_user_pool_self_registration_disabled", - "cognito_user_pool_temporary_password_expiration", - "cognito_user_pool_waf_acl_attached", - "config_recorder_all_regions_enabled", - "config_recorder_using_aws_service_role", - "datasync_task_logging_enabled", - "directconnect_connection_redundancy", - "directconnect_virtual_interface_redundancy", - "directoryservice_directory_log_forwarding_enabled", - "directoryservice_directory_monitor_notifications", - "directoryservice_directory_snapshots_limit", - "directoryservice_ldap_certificate_expiration", - "directoryservice_radius_server_security_protocol", - "directoryservice_supported_mfa_radius_enabled", - "dlm_ebs_snapshot_lifecycle_policy_exists", - "dms_endpoint_mongodb_authentication_enabled", - "dms_endpoint_neptune_iam_authorization_enabled", - "dms_endpoint_ssl_enabled", - "dms_instance_minor_version_upgrade_enabled", - "dms_instance_multi_az_enabled", - "dms_instance_no_public_access", - "documentdb_cluster_backup_enabled", - "documentdb_cluster_cloudwatch_log_export", - "documentdb_cluster_deletion_protection", - "documentdb_cluster_multi_az_enabled", - "documentdb_cluster_public_snapshot", - "documentdb_cluster_storage_encrypted", - "drs_job_exist", - "dynamodb_accelerator_cluster_encryption_enabled", - "dynamodb_accelerator_cluster_in_transit_encryption_enabled", - "dynamodb_accelerator_cluster_multi_az", - "dynamodb_table_autoscaling_enabled", - "dynamodb_table_cross_account_access", - "dynamodb_table_deletion_protection_enabled", - "dynamodb_table_protected_by_backup_plan", - "dynamodb_tables_kms_cmk_encryption_enabled", - "dynamodb_tables_pitr_enabled", - "ec2_ami_public", - "ec2_client_vpn_endpoint_connection_logging_enabled", - "ec2_ebs_default_encryption", - "ec2_ebs_public_snapshot", - "ec2_ebs_snapshot_account_block_public_access", - "ec2_ebs_snapshots_encrypted", - "ec2_ebs_volume_encryption", - "ec2_ebs_volume_protected_by_backup_plan", - "ec2_ebs_volume_snapshots_exists", - "ec2_elastic_ip_shodan", - "ec2_elastic_ip_unassigned", - "ec2_instance_account_imdsv2_enabled", - "ec2_instance_detailed_monitoring_enabled", - "ec2_instance_imdsv2_enabled", - "ec2_instance_internet_facing_with_instance_profile", - "ec2_instance_managed_by_ssm", - "ec2_instance_older_than_specific_days", - "ec2_instance_paravirtual_type", - "ec2_instance_port_cassandra_exposed_to_internet", - "ec2_instance_port_cifs_exposed_to_internet", - "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", - "ec2_instance_port_ftp_exposed_to_internet", - "ec2_instance_port_kafka_exposed_to_internet", - "ec2_instance_port_kerberos_exposed_to_internet", - "ec2_instance_port_ldap_exposed_to_internet", - "ec2_instance_port_memcached_exposed_to_internet", - "ec2_instance_port_mongodb_exposed_to_internet", - "ec2_instance_port_mysql_exposed_to_internet", - "ec2_instance_port_oracle_exposed_to_internet", - "ec2_instance_port_postgresql_exposed_to_internet", - "ec2_instance_port_rdp_exposed_to_internet", - "ec2_instance_port_redis_exposed_to_internet", - "ec2_instance_port_sqlserver_exposed_to_internet", - "ec2_instance_port_ssh_exposed_to_internet", - "ec2_instance_port_telnet_exposed_to_internet", - "ec2_instance_profile_attached", - "ec2_instance_public_ip", - "ec2_instance_secrets_user_data", - "ec2_instance_uses_single_eni", - "ec2_launch_template_no_public_ip", - "ec2_launch_template_no_secrets", - "ec2_networkacl_allow_ingress_any_port", - "ec2_networkacl_allow_ingress_tcp_port_22", - "ec2_networkacl_allow_ingress_tcp_port_3389", - "ec2_networkacl_unused", - "ec2_securitygroup_allow_ingress_from_internet_to_all_ports", - "ec2_securitygroup_allow_ingress_from_internet_to_any_port", - "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", - "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", - "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_ingress_from_internet_to_tcp_port_sql_server_1433_1434", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23", - "ec2_securitygroup_allow_wide_open_public_ipv4", - "ec2_securitygroup_default_restrict_traffic", - "ec2_securitygroup_from_launch_wizard", - "ec2_securitygroup_not_used", - "ec2_securitygroup_with_many_ingress_egress_rules", - "ec2_transitgateway_auto_accept_vpc_attachments", "ecr_registry_scan_images_on_push_enabled", - "ecr_repositories_lifecycle_policy_enabled", - "ecr_repositories_not_publicly_accessible", "ecr_repositories_scan_vulnerabilities_in_latest_image", - "ecr_repositories_tag_immutability", - "ecs_cluster_container_insights_enabled", - "ecs_service_fargate_latest_platform_version", - "ecs_service_no_assign_public_ip", - "ecs_task_definitions_containers_readonly_access", - "ecs_task_definitions_host_namespace_not_shared", - "ecs_task_definitions_host_networking_mode_users", - "ecs_task_definitions_logging_block_mode", - "ecs_task_definitions_logging_enabled", - "ecs_task_definitions_no_environment_secrets", - "ecs_task_definitions_no_privileged_containers", - "ecs_task_set_no_assign_public_ip", - "efs_access_point_enforce_root_directory", - "efs_access_point_enforce_user_identity", - "efs_encryption_at_rest_enabled", - "efs_have_backup_enabled", - "efs_mount_target_not_publicly_accessible", - "efs_not_publicly_accessible", - "eks_cluster_kms_cmk_encryption_in_secrets_enabled", - "eks_cluster_network_policy_enabled", - "eks_cluster_not_publicly_accessible", - "eks_cluster_private_nodes_enabled", - "eks_cluster_uses_a_supported_version", - "eks_control_plane_logging_all_types_enabled", - "elasticache_cluster_uses_public_subnet", - "elasticache_redis_cluster_auto_minor_version_upgrades", - "elasticache_redis_cluster_automatic_failover_enabled", - "elasticache_redis_cluster_backup_enabled", - "elasticache_redis_cluster_in_transit_encryption_enabled", - "elasticache_redis_cluster_multi_az_enabled", - "elasticache_redis_cluster_rest_encryption_enabled", - "elasticache_redis_replication_group_auth_enabled", - "elasticbeanstalk_environment_cloudwatch_logging_enabled", - "elasticbeanstalk_environment_enhanced_health_reporting", - "elasticbeanstalk_environment_managed_updates_enabled", - "elb_connection_draining_enabled", - "elb_cross_zone_load_balancing_enabled", - "elb_desync_mitigation_mode", - "elb_insecure_ssl_ciphers", - "elb_internet_facing", - "elb_is_in_multiple_az", - "elb_logging_enabled", - "elb_ssl_listeners", - "elb_ssl_listeners_use_acm_certificate", - "elbv2_cross_zone_load_balancing_enabled", - "elbv2_deletion_protection", - "elbv2_desync_mitigation_mode", - "elbv2_insecure_ssl_ciphers", - "elbv2_internet_facing", - "elbv2_is_in_multiple_az", - "elbv2_listeners_underneath", - "elbv2_logging_enabled", - "elbv2_nlb_tls_termination_enabled", - "elbv2_ssl_listeners", - "elbv2_waf_acl_attached", - "emr_cluster_account_public_block_enabled", - "emr_cluster_master_nodes_no_public_ip", - "emr_cluster_publicly_accesible", - "eventbridge_bus_cross_account_access", - "eventbridge_bus_exposed", - "eventbridge_global_endpoint_event_replication_enabled", - "eventbridge_schema_registry_cross_account_access", - "fms_policy_compliant", - "fsx_file_system_copy_tags_to_backups_enabled", - "fsx_file_system_copy_tags_to_volumes_enabled", - "fsx_windows_file_system_multi_az_enabled", - "glacier_vaults_policy_public_access", - "glue_data_catalogs_connection_passwords_encryption_enabled", - "glue_data_catalogs_metadata_encryption_enabled", - "glue_data_catalogs_not_publicly_accessible", - "glue_database_connections_ssl_enabled", - "glue_development_endpoints_cloudwatch_logs_encryption_enabled", - "glue_development_endpoints_job_bookmark_encryption_enabled", - "glue_development_endpoints_s3_encryption_enabled", - "glue_etl_jobs_amazon_s3_encryption_enabled", - "glue_etl_jobs_cloudwatch_logs_encryption_enabled", - "glue_etl_jobs_job_bookmark_encryption_enabled", - "glue_etl_jobs_logging_enabled", - "glue_ml_transform_encrypted_at_rest", - "guardduty_centrally_managed", - "guardduty_ec2_malware_protection_enabled", - "guardduty_eks_audit_log_enabled", - "guardduty_eks_runtime_monitoring_enabled", - "guardduty_is_enabled", - "guardduty_lambda_protection_enabled", - "guardduty_no_high_severity_findings", - "guardduty_rds_protection_enabled", - "guardduty_s3_protection_enabled", - "iam_administrator_access_with_mfa", - "iam_avoid_root_usage", - "iam_aws_attached_policy_no_administrative_privileges", - "iam_check_saml_providers_sts", - "iam_customer_attached_policy_no_administrative_privileges", - "iam_customer_unattached_policy_no_administrative_privileges", - "iam_group_administrator_access_policy", - "iam_inline_policy_allows_privilege_escalation", - "iam_inline_policy_no_administrative_privileges", - "iam_inline_policy_no_full_access_to_cloudtrail", - "iam_inline_policy_no_full_access_to_kms", - "iam_no_custom_policy_permissive_role_assumption", - "iam_no_expired_server_certificates_stored", - "iam_no_root_access_key", - "iam_password_policy_expires_passwords_within_90_days_or_less", - "iam_password_policy_lowercase", - "iam_password_policy_minimum_length_14", - "iam_password_policy_number", - "iam_password_policy_reuse_24", - "iam_password_policy_symbol", - "iam_password_policy_uppercase", - "iam_policy_allows_privilege_escalation", - "iam_policy_attached_only_to_group_or_roles", - "iam_policy_cloudshell_admin_not_attached", - "iam_policy_no_full_access_to_cloudtrail", - "iam_policy_no_full_access_to_kms", - "iam_role_administratoraccess_policy", - "iam_role_cross_account_readonlyaccess_policy", - "iam_role_cross_service_confused_deputy_prevention", - "iam_root_hardware_mfa_enabled", - "iam_root_mfa_enabled", - "iam_rotate_access_key_90_days", - "iam_securityaudit_role_created", - "iam_support_role_created", - "iam_user_accesskey_unused", - "iam_user_administrator_access_policy", - "iam_user_console_access_unused", - "iam_user_hardware_mfa_enabled", - "iam_user_mfa_enabled_console_access", - "iam_user_no_setup_initial_access_key", - "iam_user_two_active_access_key", - "iam_user_with_temporary_credentials", - "inspector2_active_findings_exist", - "inspector2_is_enabled", - "kafka_cluster_encryption_at_rest_uses_cmk", - "kafka_cluster_enhanced_monitoring_enabled", - "kafka_cluster_in_transit_encryption_enabled", - "kafka_cluster_is_public", - "kafka_cluster_mutual_tls_authentication_enabled", - "kafka_cluster_unrestricted_access_disabled", - "kafka_cluster_uses_latest_version", - "kafka_connector_in_transit_encryption_enabled", - "kinesis_stream_encrypted_at_rest", - "kms_cmk_are_used", - "kms_cmk_not_deleted_unintentionally", - "kms_cmk_rotation_enabled", - "kms_key_not_publicly_accessible", - "lightsail_database_public", - "lightsail_instance_automated_snapshots", - "lightsail_instance_public", - "lightsail_static_ip_unused", - "macie_automated_sensitive_data_discovery_enabled", - "macie_is_enabled", - "mq_broker_active_deployment_mode", - "mq_broker_auto_minor_version_upgrades", - "mq_broker_cluster_deployment_mode", - "mq_broker_logging_enabled", - "neptune_cluster_backup_enabled", - "neptune_cluster_copy_tags_to_snapshots", - "neptune_cluster_deletion_protection", - "neptune_cluster_iam_authentication_enabled", - "neptune_cluster_integration_cloudwatch_logs", - "neptune_cluster_multi_az", - "neptune_cluster_public_snapshot", - "neptune_cluster_snapshot_encrypted", - "neptune_cluster_storage_encrypted", - "neptune_cluster_uses_public_subnet", - "networkfirewall_deletion_protection", - "networkfirewall_in_all_vpc", - "networkfirewall_logging_enabled", - "networkfirewall_multi_az", - "networkfirewall_policy_default_action_fragmented_packets", - "networkfirewall_policy_default_action_full_packets", - "networkfirewall_policy_rule_group_associated", - "opensearch_service_domains_audit_logging_enabled", - "opensearch_service_domains_cloudwatch_logging_enabled", - "opensearch_service_domains_encryption_at_rest_enabled", - "opensearch_service_domains_fault_tolerant_data_nodes", - "opensearch_service_domains_fault_tolerant_master_nodes", - "opensearch_service_domains_https_communications_enforced", - "opensearch_service_domains_internal_user_database_enabled", - "opensearch_service_domains_node_to_node_encryption_enabled", - "opensearch_service_domains_not_publicly_accessible", - "opensearch_service_domains_updated_to_the_latest_service_software_version", - "opensearch_service_domains_use_cognito_authentication_for_kibana", - "organizations_account_part_of_organizations", - "organizations_delegated_administrators", - "organizations_opt_out_ai_services_policy", - "organizations_scp_check_deny_regions", - "organizations_tags_policies_enabled_and_attached", - "rds_cluster_backtrack_enabled", - "rds_cluster_copy_tags_to_snapshots", - "rds_cluster_critical_event_subscription", - "rds_cluster_default_admin", - "rds_cluster_deletion_protection", - "rds_cluster_iam_authentication_enabled", - "rds_cluster_integration_cloudwatch_logs", - "rds_cluster_minor_version_upgrade_enabled", - "rds_cluster_multi_az", - "rds_cluster_non_default_port", - "rds_cluster_protected_by_backup_plan", - "rds_cluster_storage_encrypted", - "rds_instance_backup_enabled", - "rds_instance_certificate_expiration", - "rds_instance_copy_tags_to_snapshots", - "rds_instance_critical_event_subscription", - "rds_instance_default_admin", - "rds_instance_deletion_protection", - "rds_instance_deprecated_engine_version", - "rds_instance_enhanced_monitoring_enabled", - "rds_instance_event_subscription_parameter_groups", - "rds_instance_event_subscription_security_groups", - "rds_instance_iam_authentication_enabled", - "rds_instance_inside_vpc", - "rds_instance_integration_cloudwatch_logs", - "rds_instance_minor_version_upgrade_enabled", - "rds_instance_multi_az", - "rds_instance_no_public_access", - "rds_instance_non_default_port", - "rds_instance_protected_by_backup_plan", - "rds_instance_storage_encrypted", - "rds_instance_transport_encrypted", - "rds_snapshots_encrypted", - "rds_snapshots_public_access", - "redshift_cluster_audit_logging", - "redshift_cluster_automated_snapshot", - "redshift_cluster_automatic_upgrades", - "redshift_cluster_encrypted_at_rest", - "redshift_cluster_enhanced_vpc_routing", - "redshift_cluster_in_transit_encryption_enabled", - "redshift_cluster_multi_az_enabled", - "redshift_cluster_non_default_database_name", - "redshift_cluster_non_default_username", - "redshift_cluster_public_access", - "resourceexplorer2_indexes_found", - "route53_dangling_ip_subdomain_takeover", - "route53_domains_privacy_protection_enabled", - "route53_domains_transferlock_enabled", - "route53_public_hosted_zones_cloudwatch_logging_enabled", - "s3_access_point_public_access_block", - "s3_account_level_public_access_blocks", - "s3_bucket_acl_prohibited", - "s3_bucket_cross_account_access", - "s3_bucket_cross_region_replication", - "s3_bucket_default_encryption", - "s3_bucket_event_notifications_enabled", - "s3_bucket_kms_encryption", - "s3_bucket_level_public_access_block", - "s3_bucket_lifecycle_enabled", - "s3_bucket_no_mfa_delete", - "s3_bucket_object_lock", - "s3_bucket_object_versioning", - "s3_bucket_policy_public_write_access", - "s3_bucket_public_access", - "s3_bucket_public_list_acl", - "s3_bucket_public_write_acl", - "s3_bucket_secure_transport_policy", - "s3_bucket_server_access_logging_enabled", - "s3_multi_region_access_point_public_access_block", - "sagemaker_endpoint_config_prod_variant_instances", - "sagemaker_models_network_isolation_enabled", - "sagemaker_models_vpc_settings_configured", - "sagemaker_notebook_instance_encryption_enabled", - "sagemaker_notebook_instance_root_access_disabled", - "sagemaker_notebook_instance_vpc_settings_configured", - "sagemaker_notebook_instance_without_direct_internet_access_configured", - "sagemaker_training_jobs_intercontainer_encryption_enabled", - "sagemaker_training_jobs_network_isolation_enabled", - "sagemaker_training_jobs_volume_and_output_encryption_enabled", - "sagemaker_training_jobs_vpc_settings_configured", - "secretsmanager_automatic_rotation_enabled", - "secretsmanager_not_publicly_accessible", - "secretsmanager_secret_rotated_periodically", - "secretsmanager_secret_unused", - "securityhub_enabled", - "ses_identity_not_publicly_accessible", - "shield_advanced_protection_in_associated_elastic_ips", - "shield_advanced_protection_in_classic_load_balancers", - "shield_advanced_protection_in_cloudfront_distributions", - "shield_advanced_protection_in_global_accelerators", - "shield_advanced_protection_in_internet_facing_load_balancers", - "shield_advanced_protection_in_route53_hosted_zones", - "sns_subscription_not_using_http_endpoints", - "sns_topics_kms_encryption_at_rest_enabled", - "sns_topics_not_publicly_accessible", - "sqs_queues_not_publicly_accessible", - "sqs_queues_server_side_encryption_enabled", - "ssm_document_secrets", - "ssm_documents_set_as_public", - "ssm_managed_compliant_patching", - "ssmincidents_enabled_with_plans", - "storagegateway_fileshare_encryption_enabled", - "transfer_server_in_transit_encryption_enabled", - "trustedadvisor_errors_and_warnings", - "trustedadvisor_premium_support_plan_subscribed", - "vpc_different_regions", - "vpc_endpoint_connections_trust_boundaries", - "vpc_endpoint_for_ec2_enabled", - "vpc_endpoint_multi_az_enabled", - "vpc_endpoint_services_allowed_principals_trust_boundaries", - "vpc_flow_logs_enabled", - "vpc_peering_routing_tables_with_least_privilege", - "vpc_subnet_different_az", - "vpc_subnet_no_public_ip_by_default", - "vpc_subnet_separate_private_public", - "vpc_vpn_connection_tunnels_up", - "waf_global_rule_with_conditions", - "waf_global_rulegroup_not_empty", - "waf_global_webacl_logging_enabled", - "waf_global_webacl_with_rules", - "waf_regional_rule_with_conditions", - "waf_regional_rulegroup_not_empty", - "waf_regional_webacl_with_rules", - "wafv2_webacl_logging_enabled", - "wafv2_webacl_rule_logging_enabled", - "wafv2_webacl_with_rules", - "wellarchitected_workload_no_high_or_medium_risks", - "workspaces_volume_encryption_enabled", - "workspaces_vpc_2private_1public_subnets_nat" + "inspector2_is_enabled" ], "Attributes": [ { @@ -4583,16 +3661,11 @@ "Name": "이상행위 분석 및 모니터링", "Description": "내·외부에 의한 침해시도, 개인정보유출 시도, 부정행위 등을 신속하게 탐지·대응할 수 있도록 네트워크 및 데이터 흐름 등을 수집하여 분석하며, 모니터링 및 점검 결과에 따른 사후조치는 적시에 이루어져야 한다.", "Checks": [ - "apigateway_restapi_logging_enabled", - "apigateway_restapi_public", - "apigateway_restapi_tracing_enabled", - "apigatewayv2_api_access_logging_enabled", - "athena_workgroup_logging_enabled", - "bedrock_model_invocation_logging_enabled", - "cloudfront_distributions_logging_enabled", + "cloudtrail_cloudwatch_logging_enabled", "cloudtrail_insights_exist", - "cloudtrail_log_file_validation_enabled", "cloudtrail_logs_s3_bucket_access_logging_enabled", + "cloudtrail_multi_region_enabled_logging_management_events", + "cloudtrail_s3_dataevents_read_enabled", "cloudtrail_s3_dataevents_write_enabled", "cloudtrail_threat_detection_enumeration", "cloudtrail_threat_detection_llm_jacking", @@ -4614,51 +3687,18 @@ "cloudwatch_log_metric_filter_security_group_changes", "cloudwatch_log_metric_filter_sign_in_without_mfa", "cloudwatch_log_metric_filter_unauthorized_api_calls", - "codebuild_project_logging_enabled", - "cognito_user_pool_advanced_security_enabled", "cognito_user_pool_blocks_compromised_credentials_sign_in_attempts", "cognito_user_pool_blocks_potential_malicious_sign_in_attempts", "config_recorder_all_regions_enabled", - "datasync_task_logging_enabled", - "directoryservice_directory_log_forwarding_enabled", - "directoryservice_directory_monitor_notifications", - "documentdb_cluster_cloudwatch_log_export", - "ec2_client_vpn_endpoint_connection_logging_enabled", - "ec2_elastic_ip_shodan", - "ec2_instance_detailed_monitoring_enabled", - "ecs_cluster_container_insights_enabled", - "ecs_task_definitions_logging_enabled", - "eks_control_plane_logging_all_types_enabled", - "elasticbeanstalk_environment_cloudwatch_logging_enabled", - "elasticbeanstalk_environment_enhanced_health_reporting", - "elb_logging_enabled", - "elbv2_logging_enabled", - "glue_etl_jobs_logging_enabled", "guardduty_eks_audit_log_enabled", "guardduty_eks_runtime_monitoring_enabled", "guardduty_no_high_severity_findings", - "kafka_cluster_enhanced_monitoring_enabled", - "mq_broker_logging_enabled", - "neptune_cluster_integration_cloudwatch_logs", - "networkfirewall_logging_enabled", - "opensearch_service_domains_audit_logging_enabled", - "opensearch_service_domains_cloudwatch_logging_enabled", "rds_cluster_critical_event_subscription", - "rds_cluster_integration_cloudwatch_logs", "rds_instance_critical_event_subscription", - "rds_instance_enhanced_monitoring_enabled", "rds_instance_event_subscription_parameter_groups", "rds_instance_event_subscription_security_groups", - "rds_instance_integration_cloudwatch_logs", - "redshift_cluster_audit_logging", - "route53_public_hosted_zones_cloudwatch_logging_enabled", "s3_bucket_event_notifications_enabled", - "s3_bucket_server_access_logging_enabled", - "trustedadvisor_errors_and_warnings", - "vpc_flow_logs_enabled", - "waf_global_webacl_logging_enabled", - "wafv2_webacl_logging_enabled", - "wafv2_webacl_rule_logging_enabled" + "trustedadvisor_errors_and_warnings" ], "Attributes": [ { @@ -4666,7 +3706,7 @@ "Subdomain": "2.11 사고 예방 및 대응", "Section": "2.11.3 이상행위 분석 및 모니터링", "AuditChecklist": [ - "내·외부에 의한 침해시도, 개인정보유출 시도, 부정행위 등 이상행위를 탐지할 수 있도록 주요 정보시스템, 응용프로그램, 네트워크, 보안시스템 등에서 발생한 네트워크 트래픽, 데이터 흐름, 이벤트 로그 등을 수집하여 분석 및 모니터링하고 있는가?", + "내·외부에 의한 침해시도, 개인정보유출 시도, 부정행위 등 이상행위를 탐지할 수 있도록 주요 정보시스템, 응용프로그램, 네트워크, 보안시스템 등에서 발생한 네트워크 트래픽, 데이터 흐름, 이벤트 로그 등을 수집하여 분석 및 모니터링하고 있는가?", "침해시도, 개인정보유출시도, 부정행위 등의 여부를 판단하기 위한 기준 및 임계치를 정의하고 이에 따라 이상행위의 판단 및 조사 등 후속 조치가 적시에 이루어지고 있는가?" ], "RelatedRegulations": [ @@ -4717,20 +3757,16 @@ "Id": "2.11.5", "Name": "사고 대응 및 복구", "Description": "침해사고 및 개인정보 유출 징후나 발생을 인지한 때에는 법적 통지 및 신고 의무를 준수하여야 하며, 절차에 따라 신속하게 대응 및 복구하고 사고분석 후 재발방지 대책을 수립하여 대응체계에 반영하여야 한다.", - "Checks": [ - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered" - ], + "Checks": [], "Attributes": [ { "Domain": "2. 보호대책 요구사항", "Subdomain": "2.11 사고 예방 및 대응", "Section": "2.11.5 사고 대응 및 복구", "AuditChecklist": [ - "침해사고 및 개인정보 유출의 징후 또는 발생을 인지한 경우 정의된 침해사고 대응 절차에 따라 신속하게 대응 및 보고가 이루어지고 있는가?", + "침해사고 및 개인정보 유출의 징후 또는 발생을 인지한 경우 정의된 침해사고 대응 절차에 따라 신속하게 대응 및 보고가 이루어지고 있는가?", "개인정보 침해사고 발생 시 관련 법령에 따라 정보주체 통지 및 관계기관 신고 절차를 이행하고 있는가?", - "침해사고가 종결된 후 사고의 원인을 분석하여 그 결과를 보고하고 관련 조직 및 인력과 공유하고 있는가?", + "침해사고가 종결된 후 사고의 원인을 분석하여 그 결과를 보고하고 관련 조직 및 인력과 공유하고 있는가?", "침해사고 분석을 통하여 얻은 정보를 활용하여 유사 사고가 재발하지 않도록 대책을 수립하고 필요한 경우 침해사고 대응절차 등을 변경하고 있는가?" ], "RelatedRegulations": [ @@ -4758,62 +3794,29 @@ "Name": "재해·재난 대비 안전조치", "Description": "자연재해, 통신·전력 장애, 해킹 등 조직의 핵심 서비스 및 시스템의 운영 연속성을 위협할 수 있는 재해 유형을 식별하고, 유형별 예상 피해규모 및 영향을 분석하여야 한다. 또한 복구 목표시간, 복구 목표시점을 정의하고 복구 전략 및 대책, 비상시 복구 조직, 비상연락체계, 복구 절차 등 재해 복구체계를 구축하여야 한다.", "Checks": [ - "account_maintain_current_contact_details", - "account_maintain_different_contact_details_to_security_billing_and_operations", - "account_security_contact_information_is_registered", - "autoscaling_group_capacity_rebalance_enabled", - "autoscaling_group_multiple_az", - "autoscaling_group_multiple_instance_types", - "awslambda_function_vpc_multi_az", "backup_plans_exist", + "backup_reportplans_exist", + "backup_vaults_exist", "cloudfront_distributions_multiple_origin_failover_configured", - "directconnect_connection_redundancy", - "directconnect_virtual_interface_redundancy", - "directoryservice_directory_snapshots_limit", - "dms_instance_multi_az_enabled", + "dlm_ebs_snapshot_lifecycle_policy_exists", "documentdb_cluster_backup_enabled", - "documentdb_cluster_multi_az_enabled", "drs_job_exist", - "dynamodb_accelerator_cluster_multi_az", - "dynamodb_table_autoscaling_enabled", "dynamodb_table_protected_by_backup_plan", "dynamodb_tables_pitr_enabled", "ec2_ebs_volume_protected_by_backup_plan", "ec2_ebs_volume_snapshots_exists", "efs_have_backup_enabled", - "elasticache_redis_cluster_automatic_failover_enabled", "elasticache_redis_cluster_backup_enabled", - "elasticache_redis_cluster_multi_az_enabled", - "elb_cross_zone_load_balancing_enabled", - "elb_is_in_multiple_az", - "elbv2_cross_zone_load_balancing_enabled", - "elbv2_is_in_multiple_az", - "eventbridge_global_endpoint_event_replication_enabled", - "fsx_file_system_copy_tags_to_backups_enabled", - "fsx_windows_file_system_multi_az_enabled", "lightsail_instance_automated_snapshots", - "mq_broker_active_deployment_mode", - "mq_broker_cluster_deployment_mode", "neptune_cluster_backup_enabled", - "neptune_cluster_multi_az", - "opensearch_service_domains_fault_tolerant_data_nodes", - "opensearch_service_domains_fault_tolerant_master_nodes", "rds_cluster_backtrack_enabled", - "rds_cluster_multi_az", "rds_cluster_protected_by_backup_plan", "rds_instance_backup_enabled", - "rds_instance_multi_az", "rds_instance_protected_by_backup_plan", "redshift_cluster_automated_snapshot", - "redshift_cluster_multi_az_enabled", - "s3_bucket_cross_region_replication", "s3_bucket_lifecycle_enabled", "s3_bucket_object_lock", - "s3_bucket_object_versioning", - "sagemaker_endpoint_config_prod_variant_instances", - "vpc_different_regions", - "vpc_endpoint_multi_az_enabled", - "vpc_subnet_different_az" + "s3_bucket_object_versioning" ], "Attributes": [ { @@ -4821,9 +3824,9 @@ "Subdomain": "2.12 재해 복구", "Section": "2.12.1 재해·재난 대비 안전조치", "AuditChecklist": [ - "조직의 핵심 서비스(업무) 연속성을 위협할 수 있는 IT 재해 유형을 식별하고, 유형별 피해규모 및 업무에 미치는 영향을 분석하여 핵심 IT 서비스(업무) 및 시스템을 식별하고 있는가?", + "조직의 핵심 서비스(업무) 연속성을 위협할 수 있는 IT 재해 유형을 식별하고, 유형별 피해규모 및 업무에 미치는 영향을 분석하여 핵심 IT 서비스(업무) 및 시스템을 식별하고 있는가?", "핵심 IT 서비스 및 시스템의 중요도 및 특성에 따른 복구 목표시간, 복구 목표시점을 정의하고 있는가?", - "재해·재난 발생 시에도 핵심 서비스 및 시스템의 연속성을 보장할 수 있도록 복구 전략 및 대책, 비상시 복구 조직, 비상연락체계, 복구 절차 등 재해 복구 계획을 수립 및 이행하고 있는가?" + "재해·재난 발생 시에도 핵심 서비스 및 시스템의 연속성을 보장할 수 있도록 복구 전략 및 대책, 비상시 복구 조직, 비상연락체계, 복구 절차 등 재해 복구 계획을 수립·이행하고 있는가?" ], "RelatedRegulations": [ "개인정보 보호법 제29조(안전조치의무)", @@ -4858,7 +3861,7 @@ "Subdomain": "2.12 재해 복구", "Section": "2.12.2 재해 복구 시험 및 개선", "AuditChecklist": [ - "수립된 IT 재해 복구체계의 실효성을 판단하기 위하여 재해 복구 시험계획을 수립 및 이행하고 있는가?", + "수립된 IT 재해 복구체계의 실효성을 판단하기 위하여 재해 복구 시험계획을 수립·이행하고 있는가?", "시험결과, 정보시스템 환경변화, 법률 등에 따른 변화를 반영할 수 있도록 복구전략 및 대책을 정기적으로 검토·보완하고 있는가?" ], "RelatedRegulations": [], @@ -4893,8 +3896,8 @@ "법정대리인의 동의를 받기 위하여 필요한 최소한의 개인정보만을 수집하고 있으며, 법정대리인이 자격 요건을 갖추고 있는지 확인하는 절차와 방법을 마련하고 있는가?", "만 14세 미만의 아동에게 개인정보 처리와 관련한 사항 등의 고지 시 이해하기 쉬운 양식과 명확하고 알기 쉬운 언어로 표현하고 있는가?", "정보주체 및 법정대리인에게 동의를 받은 기록을 보관하고 있는가?", - "정보주체의 동의 없이 처리할 수 있는 개인정보에 대해서는 그 항목과 처리의 법적 근거를 정보주체의 동의를 받아 처리하는 개인정보와 구분하여 개인정보 처리방침에 공개하거나 정보주체에게 알리고 있는가?", - "정보주체의 동의 없이 개인정보의 추가적인 이용 시 당초 수집 목적과의 관련성, 예측 가능성, 이익 침해 여부, 안전성 확보조치 등의 고려사항에 대한 판단기준을 수립 및 이행하고, 추가적인 이용이 지속적으로 발생하는 경우 고려사항에 대한 판단기준을 개인정보 처리방침에 공개하고 이를 점검하고 있는가?" + "정보주체의 동의 없이 처리할 수 있는 개인정보에 대해서는 그 항목과 처리의 법적 근거를 정보주체의 동의를 받아 처리하는 개인정보와 구분하여 개인정보 처리방침에 공개하거나 정보주체에게 알리고 있는가?", + "정보주체의 동의 없이 개인정보의 추가적인 이용 시 당초 수집 목적과의 관련성, 예측 가능성, 이익 침해 여부, 안전성 확보조치 등의 고려사항에 대한 판단기준을 수립·이행하고, 추가적인 이용이 지속적으로 발생하는 경우 고려사항에 대한 판단기준을 개인정보 처리방침에 공개하고 이를 점검하고 있는가?" ], "RelatedRegulations": [ "개인정보 보호법 제15조(개인정보의 수집·이용), 제22조(동의를 받는 방법), 제22조의2(아동의 개인정보 보호)", @@ -4933,8 +3936,8 @@ "Section": "3.1.2 개인정보 수집 제한", "AuditChecklist": [ "개인정보를 수집하는 경우 그 목적에 필요한 범위에서 최소한의 정보만을 수집하고 있는가?", - "정보주체의 동의를 받아 개인정보를 수집하는 경우 필요한 최소한의 정보 외의 개인정보 수집에는 동의하지 않을 수 있다는 사실을 구체적으로 알리고 있는가?", - "정보주체가 수집 목적에 필요한 최소한의 정보 이외의 개인정보 수집에 동의하지 않는다는 이유로 서비스 또는 재화의 제공을 거부하지 않도록 하고 있는가?" + "정보주체의 동의를 받아 개인정보를 수집하는 경우 필요한 최소한의 정보 외의 개인정보 수집에는 동의하지 않을 수 있다는 사실을 구체적으로 알리고 있는가?", + "정보주체가 수집 목적에 필요한 최소한의 정보 이외의 개인정보 수집에 동의하지 않는다는 이유로 서비스 또는 재화의 제공을 거부하지 않도록 하고 있는가?" ], "RelatedRegulations": [ "개인정보 보호법 제16조(개인정보의 수집제한), 제22조(동의를 받는 방법)" @@ -5001,7 +4004,7 @@ "Subdomain": "3.1 개인정보 수집 시 보호조치", "Section": "3.1.4 민감정보 및 고유식별정보의 처리 제한", "AuditChecklist": [ - "민감정보는 정보주체로부터 별도의 동의를 받거나 관련 법령에 근거가 있는 경우에만 처리하고 있는가?", + "민감정보는 정보주체로부터 별도의 동의를 받거나 관련 법령에 근거가 있는 경우에만 처리하고 있는가?", "고유식별정보(주민등록번호 제외)는 정보주체로부터 별도의 동의를 받거나 관련 법령에 구체적인 근거가 있는 경우에만 처리하고 있는가?", "재화 또는 서비스를 제공하는 과정에서 공개되는 정보에 정보주체의 민감정보가 포함됨으로써 사생활 침해의 위험성이 있다고 판단하는 때에는 재화 또는 서비스의 제공 전에 민감정보의 공개 가능성 및 비공개를 선택하는 방법을 정보주체가 알아보기 쉽게 알리고 있는가?" ], @@ -5098,7 +4101,7 @@ { "Id": "3.1.7", "Name": "마케팅 목적의 개인정보 수집·이용", - "Description": "재화나 서비스의 홍보, 판매 권유, 광고성 정보전송 등 마케팅 목적으로 개인정보를 수집 및이용하는 경우 그 목적을 정보주체가 명확하게 인지할 수 있도록 고지하고 동의를 받아야 한다.", + "Description": "재화나 서비스의 홍보, 판매 권유, 광고성 정보전송 등 마케팅 목적으로 개인정보를 수집·이용하는 경우 그 목적을 정보주체가 명확하게 인지할 수 있도록 고지하고 동의를 받아야 한다.", "Checks": [], "Attributes": [ { @@ -5157,16 +4160,7 @@ "개인정보 흐름표·흐름도", "개인정보파일 등록 현황", "개인정보파일 관리대장", - "개인정보 처리방침에 관한 사항을 기록한 개인정보파일", - "「조세범처벌법」에 따른 범칙행위 조사 및 「관세법」에 따른 범칙행위 조사에 관한 사항을 기록한 개인정보파일", - "일회성으로 운영되는 파일 등 지속적으로 관리할 필요가 낮다고 인정되어 대통령령으로 정하는 개인정보파일", - "회의 참석 수당 지급, 자료·물품의 송부, 금전의 정산 등 단순 업무 수행을 위해 운영되는 개인정보파일로서 지속적 관리 필요성이 낮은 개인정보파일", - "공중위생 등 공공의 안전과 안녕을 위하여 긴급히 필요한 경우로서 일시적으로 처리되는 개인정보파일", - "그 밖에 일회적 업무 처리만을 위해 수집된 개인정보파일로서 저장되거나 기록되지 않는 개인정보파일", - "다른 법령에 따라 비밀로 분류된 개인정보파일", - "국가안전보장과 관련된 정보 분석을 목적으로 수집 또는 제공 요청되는 개인정보파일", - "영상정보처리기기를 통하여 처리되는 개인영상정보파일", - "「금융실명거래 및 비밀보장에 관한 법률」에 따른 금융기관이 금융업무 취급을 위하여 보유하는 개인정보파일" + "개인정보 처리방침" ], "NonComplianceCases": [ "사례 1 : 개인정보파일을 홈페이지의 개인정보파일 등록 메뉴를 통하여 목록을 관리하고 있으나, 그 중 일부 홈페이지 서비스와 관련된 개인정보파일의 내용이 개인정보 처리방침에 누락되어 있는 경우", @@ -5188,7 +4182,7 @@ "Subdomain": "3.2 개인정보 보유 및 이용 시 보호조치", "Section": "3.2.2 개인정보 품질보장", "AuditChecklist": [ - "개인정보를 최신의 상태로 정확하게 유지하기 위한 절차 및 방안을 수립·이행하고 있는가?", + "개인정보를 최신의 상태로 정확하게 유지하기 위한 절차 및 방안을 수립·이행하고 있는가?", "정보주체가 본인의 개인정보에 대하여 정확성, 완전성 및 최신성을 유지할 수 있는 방법을 제공하고 있는가?" ], "RelatedRegulations": [ @@ -5325,7 +4319,7 @@ "개인정보를 제3자에게 제공하는 경우 제공 목적에 맞는 최소한의 개인정보 항목으로 제한하고 있는가?", "개인정보를 제3자에게 제공하는 경우 안전한 절차와 방법을 통해 제공하고 제공 내역을 기록하여 보관하고 있는가?", "제3자에게 개인정보의 접근을 허용하는 경우 개인정보를 안전하게 보호하기 위한 보호절차에 따라 통제하고 있는가?", - "정보주체의 동의 없이 개인정보의 추가적인 제공 시 당초 수집 목적과의 관련성, 예측 가능성, 이익 침해 여부, 안전성 확보조치 등의 고려사항에 대한 판단기준을 수립 및 이행하고, 추가적인 제공이 지속적으로 발생하는 경우 고려사항에 대한 판단기준을 개인정보 처리방침에 공개하고 이를 점검하고 있는가?" + "정보주체의 동의 없이 개인정보의 추가적인 제공 시 당초 수집 목적과의 관련성, 예측 가능성, 이익 침해 여부, 안전성 확보조치 등의 고려사항에 대한 판단기준을 수립 및 이행하고, 추가적인 제공이 지속적으로 발생하는 경우 고려사항에 대한 판단기준을 개인정보 처리방침에 공개하고 이를 점검하고 있는가?" ], "RelatedRegulations": [ "개인정보 보호법 제17조(개인정보의 제공), 제22조(동의를 받는 방법)", @@ -5552,7 +4546,7 @@ "Subdomain": "3.5 정보주체 권리보호", "Section": "3.5.2 정보주체 권리보장", "AuditChecklist": [ - "정보주체 또는 그 대리인이 개인정보에 대한 열람, 정정·삭제, 처리정지 및 동의 철회 등(이하 '열람등요구'라 함)을 개인정보 수집방법·절차보다 어렵지 아니하도록 권리 행사 방법 및 절차를 마련하여 공개하고 있는가?", + "정보주체 또는 그 대리인이 개인정보에 대한 열람, 정정·삭제, 처리정지 및 동의 철회 등(이하 '열람등요구'라 함)을 개인정보 수집방법·절차보다 어렵지 아니하도록 권리 행사 방법 및 절차를 마련하여 공개하고 있는가?", "정보주체 또는 그 대리인이 개인정보 열람등요구를 하는 경우 기간 내에 열람등요구에 따른 필요한 조치를 하고 있는가?", "정보주체 또는 그 대리인이 개인정보 수집·이용·제공 등의 동의를 철회하는 경우 지체 없이 수집된 개인정보를 파기하는 등 필요한 조치를 취하고 있는가?", "정보주체의 열람등요구에 대한 조치에 불복이 있는 경우 이의를 제기할 수 있도록 필요한 절차를 마련하여 안내하고 있는가?", @@ -5602,11 +4596,11 @@ "개인정보 이용·제공 내역 통지 양식 및 문구" ], "NonComplianceCases": [ - "사례 1 : 전년도 말 기준 직전 3개월 간 일일 평균 저장·관리하고 있는 개인정보가 100만명 이상으로서 개인정보 이용제공 내역 통지 의무 대상자에 해당 됨에도 불구하고 금년도에 개인정보 이용 및 내역을 통지하지 않은 경우", + "사례 1 : 전년도 말 기준 직전 3개월 간 일일 평균 저장·관리하고 있는 개인정보가 100만명 이상으로서 개인정보 이용제공 내역 통지 의무 대상자에 해당 됨에도 불구하고 금년도에 개인정보 이용·내역을 통지하지 않은 경우", "사례 2 : 개인정보 이용·제공 내역을 개별 정보주체에게 직접적으로 통지하는 대신 홈페이지에서 단순 팝업창이나 별도 공지사항으로 안내만 한 경우" ] } ] } ] -} +} \ No newline at end of file diff --git a/prowler/compliance/aws/prowler_threatscore_aws.json b/prowler/compliance/aws/prowler_threatscore_aws.json index 8ef5d24b93..4763f40ddd 100644 --- a/prowler/compliance/aws/prowler_threatscore_aws.json +++ b/prowler/compliance/aws/prowler_threatscore_aws.json @@ -275,25 +275,6 @@ } ] }, - { - "Id": "1.3.1", - "Description": "Ensure IAM policies that allow full *:* administrative privileges are not attached", - "Checks": [ - "iam_aws_attached_policy_no_administrative_privileges", - "iam_customer_attached_policy_no_administrative_privileges" - ], - "Attributes": [ - { - "Title": "IAM policies with full privileges not attached", - "Section": "1. IAM", - "SubSection": "1.3 Privilege Escalation Prevention", - "AttributeDescription": "IAM policies define permissions for users, groups, and roles, controlling access to AWS resources. Following the principle of least privilege, users should be granted only the permissions necessary to perform their tasks. Instead of assigning broad administrative privileges, permissions should be carefully crafted to allow only the required actions.", - "AdditionalInformation": "Starting with minimal permissions and granting additional access as needed is significantly more secure than providing excessive permissions and attempting to restrict them later. Assigning full administrative privileges increases the risk of unauthorized or accidental actions that could compromise AWS resources. IAM policies containing Effect: Allow, Action: , Resource: should be removed to prevent unrestricted access and enforce security best practices.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, { "Id": "1.2.3", "Description": "Ensure a support role has been created to manage incidents with AWS Support", @@ -330,24 +311,6 @@ } ] }, - { - "Id": "4.1.3", - "Description": "Ensure that all expired SSL/TLS certificates stored in AWS IAM are removed", - "Checks": [ - "iam_no_expired_server_certificates_stored" - ], - "Attributes": [ - { - "Title": "Expired SSL/TLS certificates removed", - "Section": "4. Encryption", - "SubSection": "4.1 In-Transit", - "AttributeDescription": "To enable HTTPS connections for applications and websites hosted on AWS, an SSL/TLS server certificate is required. AWS provides two options for managing certificates: AWS Certificate Manager (ACM) – The preferred method for managing SSL/TLS certificates, automating renewals and deployment. IAM Certificate Storage – Used only when deploying SSL/TLS certificates in regions not supported by ACM. IAM securely encrypts private keys and stores them, but certificates must be obtained from an external provider. ACM certificates cannot be uploaded to IAM, and IAM certificates cannot be managed from the IAM Console.", - "AdditionalInformation": "Removing expired SSL/TLS certificates prevents the accidental deployment of invalid certificates, which could cause service disruptions, security warnings, and loss of credibility for applications using AWS services like Elastic Load Balancer (ELB). As a best practice, expired certificates should be deleted to maintain a secure and trusted application environment.", - "LevelOfRisk": 5, - "Weight": 1000 - } - ] - }, { "Id": "1.2.5", "Description": "Avoid the use of the 'root' account", @@ -402,6 +365,25 @@ } ] }, + { + "Id": "1.3.1", + "Description": "Ensure IAM policies that allow full *:* administrative privileges are not attached", + "Checks": [ + "iam_aws_attached_policy_no_administrative_privileges", + "iam_customer_attached_policy_no_administrative_privileges" + ], + "Attributes": [ + { + "Title": "IAM policies with full privileges not attached", + "Section": "1. IAM", + "SubSection": "1.3 Privilege Escalation Prevention", + "AttributeDescription": "IAM policies define permissions for users, groups, and roles, controlling access to AWS resources. Following the principle of least privilege, users should be granted only the permissions necessary to perform their tasks. Instead of assigning broad administrative privileges, permissions should be carefully crafted to allow only the required actions.", + "AdditionalInformation": "Starting with minimal permissions and granting additional access as needed is significantly more secure than providing excessive permissions and attempting to restrict them later. Assigning full administrative privileges increases the risk of unauthorized or accidental actions that could compromise AWS resources. IAM policies containing Effect: Allow, Action: , Resource: should be removed to prevent unrestricted access and enforce security best practices.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, { "Id": "1.3.2", "Description": "Ensure access to AWSCloudShellFullAccess is restricted", @@ -438,78 +420,6 @@ } ] }, - { - "Id": "4.1.1", - "Description": "Ensure S3 Bucket Policy is set to deny HTTP requests", - "Checks": [ - "s3_bucket_secure_transport_policy" - ], - "Attributes": [ - { - "Title": "S3 bucket deny HTTP requests", - "Section": "4. Encryption", - "SubSection": "4.1 In-Transit", - "AttributeDescription": "Amazon S3 bucket permissions can be configured using a bucket policy to enforce access restrictions. To enhance security, objects within the bucket should be made accessible only via HTTPS, ensuring encrypted data transmission.", - "AdditionalInformation": "By default, Amazon S3 accepts both HTTP and HTTPS requests, which can expose data to interception. To enforce secure access, HTTP requests should be explicitly denied in the bucket policy. Simply allowing HTTPS without blocking HTTP does not fully comply with security best practices, as unencrypted requests may still be accepted.", - "LevelOfRisk": 3, - "Weight": 10 - } - ] - }, - { - "Id": "4.1.2", - "Description": "Ensure that EC2 Metadata Service only allows IMDSv2", - "Checks": [ - "ec2_instance_imdsv2_enabled" - ], - "Attributes": [ - { - "Title": "EC2 Metadata Service only allows IMDSv2", - "Section": "4. Encryption", - "SubSection": "4.1 In-Transit", - "AttributeDescription": "AWS EC2 instances allow users to choose between Instance Metadata Service Version 1 (IMDSv1), which uses a request/response model, or Instance Metadata Service Version 2 (IMDSv2), which uses a session-based approach for enhanced security", - "AdditionalInformation": "Instance metadata refers to the data about an EC2 instance, such as host names, events, and security groups, that is used for managing and configuring the instance. When enabling the Metadata Service, users can opt for either IMDSv1, which operates via a simple request/response model, or IMDSv2, which implements session authentication for additional security. With IMDSv2, each request is secured by session-based authentication, ensuring that all interactions with the instance's metadata and credentials are protected. IMDSv1, on the other hand, may expose instances to Server-Side Request Forgery (SSRF) attacks. To improve security, Amazon recommends using IMDSv2", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, - { - "Id": "2.2.1", - "Description": "Ensure MFA Delete is enabled on S3 buckets", - "Checks": [ - "s3_bucket_no_mfa_delete" - ], - "Attributes": [ - { - "Title": "MFA delete enabled on S3 buckets", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "Enabling MFA Delete on a sensitive or classified Amazon S3 bucket adds an extra layer of protection by requiring two-factor authentication for critical actions, such as deleting object versions or changing the bucket’s versioning state.", - "AdditionalInformation": "MFA Delete helps prevent accidental or malicious deletions by requiring an additional authentication step. This mitigates the risk of data loss due to compromised credentials or unauthorized access, ensuring that critical objects remain protected.", - "LevelOfRisk": 3, - "Weight": 10 - } - ] - }, - { - "Id": "2.2.2", - "Description": "Ensure all data in Amazon S3 has been discovered, classified, and secured when necessary", - "Checks": [ - "macie_is_enabled" - ], - "Attributes": [ - { - "Title": "Macie enabled", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "Amazon S3 buckets may store sensitive data that needs to be discovered, classified, monitored, and protected to maintain security and compliance. Amazon Macie, along with third-party tools, can automatically inventory S3 buckets and identify sensitive data at scale.", - "AdditionalInformation": "Using automated data discovery and classification tools, such as Amazon Macie, enhances security by continuously monitoring S3 buckets for sensitive information. Macie leverages machine learning and pattern matching to detect and protect critical data, reducing the risk of data leaks and unauthorized access.", - "LevelOfRisk": 1, - "Weight": 1 - } - ] - }, { "Id": "2.1.1", "Description": "Ensure the default security group of every VPC restricts all traffic", @@ -586,6 +496,180 @@ } ] }, + { + "Id": "2.1.5", + "Description": "Ensure ApiGateway endpoint is not public", + "Checks": [ + "apigateway_restapi_public" + ], + "Attributes": [ + { + "Title": "ApiGateway endpoint is public", + "Section": "2. Attack Surface", + "SubSection": "2.1 Network", + "AttributeDescription": "AWS API Gateway allows developers to create, deploy, and manage APIs that connect applications to backend services. By default, API Gateway endpoints can be publicly accessible, meaning they can be invoked from anywhere on the internet. To enhance security, API Gateway endpoints should be restricted to private networks using VPC links, private API settings, or access control mechanisms to ensure that only authorized entities can interact with the API.", + "AdditionalInformation": "Publicly accessible API Gateway endpoints can expose backend services to unauthorized access, data leaks, and potential exploitation. Attackers may attempt brute-force authentication, injection attacks, or abuse API functionality if access is not properly restricted. To reduce the attack surface, API Gateway endpoints should be limited to internal use or protected with authentication, IAM permissions, WAF rules, or private VPC access to ensure only trusted users and systems can invoke the API.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "2.1.6", + "Description": "Ensure that ec2 common ports from instances are not internet-exposed", + "Checks": [ + "ec2_instance_port_cassandra_exposed_to_internet", + "ec2_instance_port_cifs_exposed_to_internet", + "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", + "ec2_instance_port_ftp_exposed_to_internet", + "ec2_instance_port_kafka_exposed_to_internet", + "ec2_instance_port_kerberos_exposed_to_internet", + "ec2_instance_port_ldap_exposed_to_internet", + "ec2_instance_port_memcached_exposed_to_internet", + "ec2_instance_port_mongodb_exposed_to_internet", + "ec2_instance_port_mysql_exposed_to_internet", + "ec2_instance_port_oracle_exposed_to_internet", + "ec2_instance_port_postgresql_exposed_to_internet", + "ec2_instance_port_rdp_exposed_to_internet", + "ec2_instance_port_redis_exposed_to_internet", + "ec2_instance_port_sqlserver_exposed_to_internet", + "ec2_instance_port_ssh_exposed_to_internet", + "ec2_instance_port_telnet_exposed_to_internet" + ], + "Attributes": [ + { + "Title": "Common ports from instances are not exposed", + "Section": "2. Attack Surface", + "SubSection": "2.1 Network", + "AttributeDescription": "Amazon EC2 instances can run various services that communicate over common ports such as 22 (SSH), 3389 (RDP), 80 (HTTP), and 443 (HTTPS) (and more). If these ports are open to the internet, attackers can attempt unauthorized access, brute-force attacks, or exploit known vulnerabilities. To reduce security risks, EC2 instances should be configured so that common ports are not exposed to the public internet, unless explicitly required and properly secured.", + "AdditionalInformation": "Exposing common ports directly to the internet increases the attack surface and risks unauthorized access or system compromise. Attackers frequently scan for open ports to target misconfigured or unpatched services. To enhance security, access to EC2 common ports should be restricted using security groups, network ACLs, and VPC configurations, ensuring that only trusted networks and users can connect.", + "LevelOfRisk": 5, + "Weight": 1000 + } + ] + }, + { + "Id": "2.1.7", + "Description": "Ensure that ec2 security groups do not allow ingress from internet to common ports", + "Checks": [ + "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", + "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", + "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_ingress_from_internet_to_tcp_port_sql_server_1433_1434", + "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23" + ], + "Attributes": [ + { + "Title": "Common ports from security groups do not allow ingress traffic", + "Section": "2. Attack Surface", + "SubSection": "2.1 Network", + "AttributeDescription": "Amazon EC2 security groups act as virtual firewalls, controlling inbound and outbound traffic to instances. If a security group allows ingress (incoming traffic) from the internet (0.0.0.0/0 or ::/0) to common ports such as 22 (SSH), 3389 (RDP), 80 (HTTP), or 443 (HTTPS) (and more), it creates a significant security risk. To minimize exposure, security groups should be configured to restrict ingress access to these ports to only trusted IP addresses or internal networks.", + "AdditionalInformation": "Allowing unrestricted inbound traffic to common ports increases the risk of brute-force attacks, unauthorized access, and exploitation of vulnerabilities. Attackers actively scan for open ports on public-facing EC2 instances to gain unauthorized control. To reduce security risks, ingress rules should be restricted using least privilege principles, IP whitelisting, VPN access, or bastion hosts, ensuring that only authorized users and networks can connect.", + "LevelOfRisk": 5, + "Weight": 1000 + } + ] + }, + { + "Id": "2.1.8", + "Description": "Ensure EKS cluster Network Policy is Enabled and Set as Appropriate", + "Checks": [ + "eks_cluster_network_policy_enabled" + ], + "Attributes": [ + { + "Title": "EKS cluster Network Policy is Enabled and Set as Appropriate", + "Section": "2. Attack Surface", + "SubSection": "2.1 Network", + "AttributeDescription": "A Network Policy defines how network traffic is controlled and restricted between workloads within a cloud environment. Enforcing network policies ensures that only authorized communication occurs between services, reducing the risk of unauthorized access and lateral movement. It is recommended to enable Network Policies and configure them appropriately to enforce least privilege access and secure communication between workloads.", + "AdditionalInformation": "Without properly configured Network Policies, workloads may be exposed to unnecessary or unauthorized network traffic, increasing the risk of data leaks, exploitation, or lateral movement by attackers. By enabling and enforcing Network Policies, organizations can limit communication between workloads, ensuring that only approved and necessary network interactions are allowed, minimizing the attack surface and enhancing overall security.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "2.1.9", + "Description": "Ensure EKS Clusters are not publicly accessible", + "Checks": [ + "eks_cluster_not_publicly_accessible" + ], + "Attributes": [ + { + "Title": "EKS Clusters are not publicly accessible", + "Section": "2. Attack Surface", + "SubSection": "2.1 Network", + "AttributeDescription": "Amazon Elastic Kubernetes Service (EKS) clusters manage containerized applications and can be configured with either private or public access. If an EKS cluster is publicly accessible, it means that the Kubernetes API endpoint can be reached from the internet, increasing the risk of unauthorized access and attacks. To enhance security, EKS clusters should be restricted to private networks and accessed only through secure VPNs, VPC peering, or AWS PrivateLink.", + "AdditionalInformation": "Exposing an EKS cluster to the public internet increases the risk of brute-force attacks, credential theft, and unauthorized access to Kubernetes workloads. Attackers could exploit misconfigured RBAC policies or API vulnerabilities to gain control over the cluster. To reduce security risks, EKS clusters should be configured with private endpoints, ensuring that only trusted networks and IAM-authenticated users can manage Kubernetes resources.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "2.1.10", + "Description": "Ensure EKS Clusters are created with Private Nodes", + "Checks": [ + "eks_cluster_private_nodes_enabled" + ], + "Attributes": [ + { + "Title": "EKS Clusters are created with Private Nodes", + "Section": "2. Attack Surface", + "SubSection": "2.1 Network", + "AttributeDescription": "Amazon Elastic Kubernetes Service (EKS) clusters run workloads on worker nodes, which can be either public or private. If EKS clusters are created with public nodes, these nodes are assigned public IP addresses, making them accessible from the internet, which increases the risk of unauthorized access and potential attacks. To enhance security, EKS clusters should be created with private nodes that operate within private subnets and are only accessible through secured networking configurations such as VPNs, VPC peering, or AWS PrivateLink.", + "AdditionalInformation": "Using public nodes in EKS exposes Kubernetes workloads to the internet, increasing the risk of unauthorized access, lateral movement, and potential exploitation. Attackers can target misconfigured workloads, open services, or unsecured API endpoints. By creating EKS clusters with private nodes, organizations can restrict access, limit exposure to public threats, and enforce network segmentation, ensuring that workloads remain secure and isolated within a private VPC environment.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "2.2.1", + "Description": "Ensure MFA Delete is enabled on S3 buckets", + "Checks": [ + "s3_bucket_no_mfa_delete" + ], + "Attributes": [ + { + "Title": "MFA delete enabled on S3 buckets", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "Enabling MFA Delete on a sensitive or classified Amazon S3 bucket adds an extra layer of protection by requiring two-factor authentication for critical actions, such as deleting object versions or changing the bucket’s versioning state.", + "AdditionalInformation": "MFA Delete helps prevent accidental or malicious deletions by requiring an additional authentication step. This mitigates the risk of data loss due to compromised credentials or unauthorized access, ensuring that critical objects remain protected.", + "LevelOfRisk": 3, + "Weight": 10 + } + ] + }, + { + "Id": "2.2.2", + "Description": "Ensure all data in Amazon S3 has been discovered, classified, and secured when necessary", + "Checks": [ + "macie_is_enabled" + ], + "Attributes": [ + { + "Title": "Macie enabled", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "Amazon S3 buckets may store sensitive data that needs to be discovered, classified, monitored, and protected to maintain security and compliance. Amazon Macie, along with third-party tools, can automatically inventory S3 buckets and identify sensitive data at scale.", + "AdditionalInformation": "Using automated data discovery and classification tools, such as Amazon Macie, enhances security by continuously monitoring S3 buckets for sensitive information. Macie leverages machine learning and pattern matching to detect and protect critical data, reducing the risk of data leaks and unauthorized access.", + "LevelOfRisk": 1, + "Weight": 1 + } + ] + }, { "Id": "2.2.3", "Description": "Ensure EBS snapshots are not publicly accessible", @@ -604,60 +688,6 @@ } ] }, - { - "Id": "4.2.1", - "Description": "Ensure EBS volume encryption is enabled in all regions", - "Checks": [ - "ec2_ebs_volume_encryption" - ], - "Attributes": [ - { - "Title": "EBS volume encryption", - "Section": "4. Encryption", - "SubSection": "4.2 At-Rest", - "AttributeDescription": "Amazon Elastic Compute Cloud (EC2) supports encryption at rest for Elastic Block Store (EBS) volumes, ensuring that stored data remains protected. While EBS encryption is disabled by default, organizations can enforce automatic encryption of newly created volumes to enhance data security and compliance.", - "AdditionalInformation": "Enforcing EBS volume encryption reduces the risk of data exposure, unauthorized access, and compliance violations. If encryption remains intact, even if storage is compromised, data remains unreadable to unauthorized users. Encrypting data at rest ensures that sensitive information is protected against accidental disclosure, insider threats, and external attacks.", - "LevelOfRisk": 3, - "Weight": 10 - } - ] - }, - { - "Id": "4.2.2", - "Description": "Ensure that encryption-at-rest is enabled for RDS instances", - "Checks": [ - "rds_instance_storage_encrypted" - ], - "Attributes": [ - { - "Title": "RDS instances encryption at rest enabled", - "Section": "4. Encryption", - "SubSection": "4.2 At-Rest", - "AttributeDescription": "Amazon Relational Database Service (RDS) supports encryption at rest using the industry-standard AES-256 encryption algorithm to secure database instances and their associated storage. Once enabled, RDS encryption automatically handles access authentication and decryption, ensuring secure data storage with minimal performance impact.", - "AdditionalInformation": "Databases often contain sensitive and business-critical information, making encryption essential to protect against unauthorized access and data breaches. Enabling RDS encryption ensures that underlying storage, automated backups, read replicas, and snapshots are all encrypted, preventing accidental or malicious data exposure while maintaining compliance with security best practices.", - "LevelOfRisk": 3, - "Weight": 10 - } - ] - }, - { - "Id": "2.3.1", - "Description": "Ensure SNS topics do not allow global send or subscribe", - "Checks": [ - "sns_topics_not_publicly_accessible" - ], - "Attributes": [ - { - "Title": "SNS topics do not allow global send or subscribe", - "Section": "2. Attack Surface", - "SubSection": "2.3 Application", - "AttributeDescription": "Amazon Simple Notification Service (SNS) topics enable messaging between AWS services, applications, and users. By default, SNS topics should be restricted to trusted AWS accounts or IAM roles to prevent unauthorized access. Allowing global send (sns:Publish) or subscribe (sns:Subscribe) permissions means any AWS account or unauthenticated entity could send messages or subscribe to the topic, potentially leading to spam, data leaks, or misuse of notifications.", - "AdditionalInformation": "SNS topics with global send or subscribe permissions expose AWS environments to unauthorized message injection, data exfiltration, and Denial-of-Service (DoS) attacks. An attacker could flood an SNS topic with malicious or fraudulent messages, leading to unexpected charges or service disruptions. Restricting access ensures that only authorized AWS accounts, applications, or IAM roles can send and receive messages, reducing security risks and protecting system integrity.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, { "Id": "2.2.4", "Description": "Ensure RDS snapshots are not publicly accessible", @@ -713,18 +743,234 @@ ] }, { - "Id": "2.1.5", - "Description": "Ensure ApiGateway endpoint is not public", + "Id": "2.2.7", + "Description": "Ensure DocumentDB manual cluster snapshot is not public", "Checks": [ - "apigateway_restapi_public" + "documentdb_cluster_public_snapshot" ], "Attributes": [ { - "Title": "ApiGateway endpoint is public", + "Title": "DocumentDB manual cluster snapshot is not public", "Section": "2. Attack Surface", - "SubSection": "2.1 Network", - "AttributeDescription": "AWS API Gateway allows developers to create, deploy, and manage APIs that connect applications to backend services. By default, API Gateway endpoints can be publicly accessible, meaning they can be invoked from anywhere on the internet. To enhance security, API Gateway endpoints should be restricted to private networks using VPC links, private API settings, or access control mechanisms to ensure that only authorized entities can interact with the API.", - "AdditionalInformation": "Publicly accessible API Gateway endpoints can expose backend services to unauthorized access, data leaks, and potential exploitation. Attackers may attempt brute-force authentication, injection attacks, or abuse API functionality if access is not properly restricted. To reduce the attack surface, API Gateway endpoints should be limited to internal use or protected with authentication, IAM permissions, WAF rules, or private VPC access to ensure only trusted users and systems can invoke the API.", + "SubSection": "2.2 Storage", + "AttributeDescription": "AWS DocumentDB manual cluster snapshots store backups of DocumentDB clusters, containing sensitive database information such as application data, configurations, and credentials. By default, snapshots are private, but they can be manually shared or made public, which poses a significant security risk. To prevent unauthorized access, DocumentDB manual cluster snapshots should never be publicly accessible unless explicitly required and properly secured.", + "AdditionalInformation": "Publicly accessible DocumentDB snapshots expose critical database information, increasing the risk of data breaches, unauthorized access, and compliance violations. Attackers could restore the snapshot in their own AWS account and gain full access to the database content. To protect sensitive data, DocumentDB snapshots should only be shared with specific AWS accounts or remain private, following least privilege principles and AWS security best practices.", + "LevelOfRisk": 5, + "Weight": 1000 + } + ] + }, + { + "Id": "2.2.8", + "Description": "Ensure that public access to EBS snapshots is disabled", + "Checks": [ + "ec2_ebs_snapshot_account_block_public_access" + ], + "Attributes": [ + { + "Title": "Public access to EBS snapshots is disabled", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "Amazon Elastic Block Store (EBS) snapshots are backups of EC2 volumes that may contain sensitive data, such as credentials, application configurations, and customer records. By default, EBS snapshots are private, but they can be manually shared or made public, allowing anyone to copy or restore them. To prevent unauthorized access and data exposure, public access to EBS snapshots should always be disabled.", + "AdditionalInformation": "Publicly accessible EBS snapshots pose a significant security risk, as attackers can restore and extract sensitive data if a snapshot is exposed. Misconfigured public snapshots have led to data breaches and compliance violations in the past. To mitigate this risk, EBS snapshots should be kept private or explicitly shared only with trusted AWS accounts, following least privilege principles to protect critical data and maintain security compliance.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "2.2.9", + "Description": "Ensure EFS mount targets are not publicly accessible", + "Checks": [ + "efs_mount_target_not_publicly_accessible" + ], + "Attributes": [ + { + "Title": "No EFS mount targets are publicly accessible", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "Amazon Elastic File System (EFS) provides scalable, shared file storage for AWS services. EFS mount targets allow instances to connect to the file system within a VPC. By default, EFS mount targets can be configured with public accessibility, making them reachable from the internet. To enhance security, EFS mount targets should be restricted to private networks and should not be publicly accessible unless explicitly required and properly secured.", + "AdditionalInformation": "Publicly accessible EFS mount targets expose stored data to unauthorized access, cyberattacks, and data breaches. Attackers could exploit misconfigured security groups or network ACLs to access or modify files. To reduce security risks, EFS mount targets should be restricted to private subnets, with access limited to trusted VPCs, security groups, and IAM roles, ensuring secure file storage and controlled access.", + "LevelOfRisk": 3, + "Weight": 10 + } + ] + }, + { + "Id": "2.2.10", + "Description": "Ensure that EFS do not have policies which allow access to any client within the VPC", + "Checks": [ + "efs_not_publicly_accessible" + ], + "Attributes": [ + { + "Title": "No EFS have policies which allow access to any client within the VPC", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "Amazon Elastic File System (EFS) provides shared storage that can be accessed by multiple EC2 instances and services within a VPC. EFS access is controlled through resource-based policies that define which clients can connect. If an EFS policy allows access to any client within the VPC, it increases the risk of unauthorized access and data exposure. To enhance security, EFS policies should be restricted to specific IAM roles, security groups, or trusted resources instead of granting broad access to all VPC clients.", + "AdditionalInformation": "Allowing any client within a VPC to access an EFS file system increases the risk of data leaks, accidental modifications, or unauthorized access by compromised instances or misconfigured services. To minimize exposure, EFS policies should enforce least privilege access, restricting permissions to specific instances, roles, or users that require access, ensuring secure file storage and controlled data access.", + "LevelOfRisk": 3, + "Weight": 10 + } + ] + }, + { + "Id": "2.2.11", + "Description": "Ensure Elasticache Cluster is not using a public subnet", + "Checks": [ + "elasticache_cluster_uses_public_subnet" + ], + "Attributes": [ + { + "Title": "Elasticache Cluster is not using a public subnet", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "Amazon ElastiCache provides in-memory caching services using Redis and Memcached. By default, ElastiCache clusters can be deployed in either public or private subnets. If an ElastiCache cluster is placed in a public subnet, it becomes accessible from the internet, which significantly increases the risk of unauthorized access and data breaches. To enhance security, ElastiCache clusters should only be deployed in private subnets, ensuring restricted access within a VPC.", + "AdditionalInformation": "Deploying an ElastiCache cluster in a public subnet exposes it to external threats, such as unauthorized access, brute-force attacks, and potential data exfiltration. Attackers could exploit misconfigurations to access cached data or disrupt services. By restricting ElastiCache clusters to private subnets, organizations can limit access to trusted resources, enforce VPC security controls, and reduce the attack surface, ensuring secure and efficient caching operations.", + "LevelOfRisk": 3, + "Weight": 10 + } + ] + }, + { + "Id": "2.2.12", + "Description": "Ensure S3 Glacier vaults have not policies which allow access to everyone", + "Checks": [ + "glacier_vaults_policy_public_access" + ], + "Attributes": [ + { + "Title": "S3 Glacier vaults have not policies which allow access to everyone", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "Amazon S3 Glacier provides low-cost, long-term storage for archival data. Glacier vaults can be configured with resource-based policies that control access. If a Glacier vault policy allows access to everyone, unauthorized users could retrieve or delete archived data, leading to data exposure or loss. To enhance security, Glacier vault policies should be restricted to specific AWS accounts, IAM roles, or trusted entities, ensuring only authorized users can access or manage archived data.", + "AdditionalInformation": "Allowing public access to S3 Glacier vaults poses a significant security risk, increasing the chance of data breaches, unauthorized deletions, or compliance violations. Attackers could restore and download sensitive archived data if the vault is misconfigured. To prevent unauthorized access, Glacier vaults should have strict access controls, using IAM policies, encryption, and resource-based permissions, ensuring that only trusted users and systems can interact with archived data.", + "LevelOfRisk": 5, + "Weight": 1000 + } + ] + }, + { + "Id": "2.2.13", + "Description": "Ensure Glue Data Catalogs are not publicly accessible", + "Checks": [ + "glue_data_catalogs_not_publicly_accessible" + ], + "Attributes": [ + { + "Title": "Glue Data Catalogs are not publicly accessible", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "AWS Glue Data Catalog is a centralized metadata repository used to store and manage schema information for data lakes and analytics workflows. By default, Glue Data Catalogs can be configured to allow public access, which poses a significant security risk if sensitive metadata is exposed. To enhance security, Glue Data Catalogs should be restricted to specific AWS accounts, IAM roles, or trusted services, ensuring that only authorized users can access or modify catalog information.", + "AdditionalInformation": "Allowing public access to Glue Data Catalogs increases the risk of unauthorized access, data leaks, and compliance violations. Attackers could gain insights into an organization’s data structure or modify catalog entries, leading to potential data corruption or unauthorized data exposure. To reduce security risks, Glue Data Catalogs should be secured using IAM policies, resource-based permissions, and AWS Lake Formation, ensuring that only trusted accounts and services can interact with metadata.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "2.2.14", + "Description": "Ensure there are no internet exposed KMS keys", + "Checks": [ + "kms_key_not_publicly_accessible" + ], + "Attributes": [ + { + "Title": "No internet exposed KMS keys", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "AWS Key Management Service (KMS) provides secure encryption key management for data encryption and cryptographic operations. If KMS keys are exposed to the internet, unauthorized entities could potentially use, modify, or compromise encryption keys, leading to data breaches and security vulnerabilities. To enhance security, KMS keys should be restricted to trusted AWS accounts, IAM roles, and specific AWS services, ensuring that only authorized users and systems can access and manage them.", + "AdditionalInformation": "Exposing KMS keys to the public poses a critical security risk, as compromised keys can lead to unauthorized data decryption, loss of data integrity, and compliance violations. Attackers could potentially use public KMS keys to encrypt or decrypt sensitive data, undermining security controls. To prevent unauthorized access, KMS key policies should enforce strict access control using IAM permissions, VPC endpoint policies, and AWS PrivateLink, ensuring that encryption operations remain fully secured and isolated from the public internet.", + "LevelOfRisk": 3, + "Weight": 10 + } + ] + }, + { + "Id": "2.2.15", + "Description": "Ensure that S3 buckets have not policies which allow WRITE access", + "Checks": [ + "s3_bucket_policy_public_write_access" + ], + "Attributes": [ + { + "Title": "S3 buckets have not policies which allow WRITE access", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "Amazon S3 buckets store and manage data, files, and application assets. Bucket policies control access permissions, and if an S3 bucket has a policy that allows WRITE access to everyone, unauthorized users can upload, modify, or delete objects, leading to data tampering, security breaches, or service disruptions. To enhance security, S3 bucket policies should be restricted to specific AWS accounts, IAM roles, or trusted services, ensuring only authorized users have WRITE permissions.", + "AdditionalInformation": "Allowing unrestricted WRITE access to an S3 bucket increases the risk of unauthorized modifications, data injection attacks, and accidental data loss. Attackers could upload malicious files, delete critical data, or overwrite important configurations. To prevent unauthorized changes, S3 bucket policies should explicitly deny public WRITE access, enforce least privilege access control, and use AWS Block Public Access settings to ensure secure and controlled data storage.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "2.2.16", + "Description": "Ensure there are no S3 buckets listable by Everyone or Any AWS customer", + "Checks": [ + "s3_bucket_public_list_acl" + ], + "Attributes": [ + { + "Title": "No S3 buckets are listable by Everyone or Any AWS customer", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "Amazon S3 buckets store sensitive data and should have restricted access permissions. If an S3 bucket is listable by Everyone or Any AWS customer, unauthorized users can enumerate the objects within the bucket, potentially exposing sensitive information such as filenames, metadata, or even public datasets. To enhance security, S3 bucket permissions should be configured to restrict LIST access to only authorized IAM roles, AWS accounts, or specific services.", + "AdditionalInformation": "Allowing public or AWS-wide LIST access increases the risk of data enumeration, unauthorized access, and information leaks. Attackers or unauthorized users could identify and analyze stored files, extract metadata, or infer sensitive data. To mitigate this risk, S3 bucket policies should explicitly deny public LIST access, enforce least privilege permissions, and use AWS Block Public Access settings to prevent unintended data exposure.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "2.2.17", + "Description": "Ensure there are no S3 buckets writable by Everyone or Any AWS customer", + "Checks": [ + "s3_bucket_public_write_acl" + ], + "Attributes": [ + { + "Title": "No S3 buckets writable by Everyone or Any AWS customer", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "Amazon S3 buckets should have strict access controls to prevent unauthorized modifications. If an S3 bucket is writable by Everyone or Any AWS customer, it allows unauthorized users to upload, modify, or delete objects, leading to data corruption, security breaches, and compliance risks. To enhance security, S3 bucket permissions should be restricted to trusted IAM roles, AWS accounts, or specific services.", + "AdditionalInformation": "Allowing public or AWS-wide WRITE access creates a significant security risk, as attackers can inject malicious files, overwrite critical data, or delete essential objects. This could lead to data loss, malware distribution, or unauthorized system modifications. To prevent unauthorized changes, S3 bucket policies should explicitly deny public WRITE access, enforce least privilege access, and use AWS Block Public Access settings to secure data integrity and prevent unauthorized modifications.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "2.2.18", + "Description": "Ensure Secrets Manager secrets are not publicly accessible", + "Checks": [ + "secretsmanager_not_publicly_accessible" + ], + "Attributes": [ + { + "Title": "Secrets Manager secrets are not publicly accessible", + "Section": "2. Attack Surface", + "SubSection": "2.2 Storage", + "AttributeDescription": "AWS Secrets Manager is used to securely store and manage sensitive information, such as API keys, database credentials, and encryption keys. By default, Secrets Manager secrets should be restricted to authorized IAM roles and AWS services. If a secret is publicly accessible, it can be exposed to unauthorized users, leading to data leaks, security breaches, and potential exploitation of sensitive credentials. To enhance security, Secrets Manager secrets should be strictly controlled using IAM policies and resource-based permissions.", + "AdditionalInformation": "Allowing public access to Secrets Manager secrets creates a critical security vulnerability, as attackers could retrieve, misuse, or exfiltrate sensitive information. Compromised secrets could lead to unauthorized access to databases, applications, or cloud services, resulting in data breaches, financial loss, or compliance violations. To mitigate this risk, Secrets Manager secrets should be restricted using least privilege IAM permissions, encrypted with AWS KMS, and accessed only by trusted AWS services and roles, ensuring secure and controlled secret management.", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "2.3.1", + "Description": "Ensure SNS topics do not allow global send or subscribe", + "Checks": [ + "sns_topics_not_publicly_accessible" + ], + "Attributes": [ + { + "Title": "SNS topics do not allow global send or subscribe", + "Section": "2. Attack Surface", + "SubSection": "2.3 Application", + "AttributeDescription": "Amazon Simple Notification Service (SNS) topics enable messaging between AWS services, applications, and users. By default, SNS topics should be restricted to trusted AWS accounts or IAM roles to prevent unauthorized access. Allowing global send (sns:Publish) or subscribe (sns:Subscribe) permissions means any AWS account or unauthenticated entity could send messages or subscribe to the topic, potentially leading to spam, data leaks, or misuse of notifications.", + "AdditionalInformation": "SNS topics with global send or subscribe permissions expose AWS environments to unauthorized message injection, data exfiltration, and Denial-of-Service (DoS) attacks. An attacker could flood an SNS topic with malicious or fraudulent messages, leading to unexpected charges or service disruptions. Restricting access ensures that only authorized AWS accounts, applications, or IAM roles can send and receive messages, reducing security risks and protecting system integrity.", "LevelOfRisk": 4, "Weight": 100 } @@ -802,24 +1048,6 @@ } ] }, - { - "Id": "2.2.7", - "Description": "Ensure DocumentDB manual cluster snapshot is not public", - "Checks": [ - "documentdb_cluster_public_snapshot" - ], - "Attributes": [ - { - "Title": "DocumentDB manual cluster snapshot is not public", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "AWS DocumentDB manual cluster snapshots store backups of DocumentDB clusters, containing sensitive database information such as application data, configurations, and credentials. By default, snapshots are private, but they can be manually shared or made public, which poses a significant security risk. To prevent unauthorized access, DocumentDB manual cluster snapshots should never be publicly accessible unless explicitly required and properly secured.", - "AdditionalInformation": "Publicly accessible DocumentDB snapshots expose critical database information, increasing the risk of data breaches, unauthorized access, and compliance violations. Attackers could restore the snapshot in their own AWS account and gain full access to the database content. To protect sensitive data, DocumentDB snapshots should only be shared with specific AWS accounts or remain private, following least privilege principles and AWS security best practices.", - "LevelOfRisk": 5, - "Weight": 1000 - } - ] - }, { "Id": "2.3.6", "Description": "Ensure there are no EC2 AMIs set as Public", @@ -838,90 +1066,6 @@ } ] }, - { - "Id": "2.2.8", - "Description": "Ensure that public access to EBS snapshots is disabled", - "Checks": [ - "ec2_ebs_snapshot_account_block_public_access" - ], - "Attributes": [ - { - "Title": "Public access to EBS snapshots is disabled", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "Amazon Elastic Block Store (EBS) snapshots are backups of EC2 volumes that may contain sensitive data, such as credentials, application configurations, and customer records. By default, EBS snapshots are private, but they can be manually shared or made public, allowing anyone to copy or restore them. To prevent unauthorized access and data exposure, public access to EBS snapshots should always be disabled.", - "AdditionalInformation": "Publicly accessible EBS snapshots pose a significant security risk, as attackers can restore and extract sensitive data if a snapshot is exposed. Misconfigured public snapshots have led to data breaches and compliance violations in the past. To mitigate this risk, EBS snapshots should be kept private or explicitly shared only with trusted AWS accounts, following least privilege principles to protect critical data and maintain security compliance.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, - { - "Id": "2.1.6", - "Description": "Ensure that ec2 common ports from instances are not internet-exposed", - "Checks": [ - "ec2_instance_port_cassandra_exposed_to_internet", - "ec2_instance_port_cifs_exposed_to_internet", - "ec2_instance_port_elasticsearch_kibana_exposed_to_internet", - "ec2_instance_port_ftp_exposed_to_internet", - "ec2_instance_port_kafka_exposed_to_internet", - "ec2_instance_port_kerberos_exposed_to_internet", - "ec2_instance_port_ldap_exposed_to_internet", - "ec2_instance_port_memcached_exposed_to_internet", - "ec2_instance_port_mongodb_exposed_to_internet", - "ec2_instance_port_mysql_exposed_to_internet", - "ec2_instance_port_oracle_exposed_to_internet", - "ec2_instance_port_postgresql_exposed_to_internet", - "ec2_instance_port_rdp_exposed_to_internet", - "ec2_instance_port_redis_exposed_to_internet", - "ec2_instance_port_sqlserver_exposed_to_internet", - "ec2_instance_port_ssh_exposed_to_internet", - "ec2_instance_port_telnet_exposed_to_internet" - ], - "Attributes": [ - { - "Title": "Common ports from instances are not exposed", - "Section": "2. Attack Surface", - "SubSection": "2.1 Network", - "AttributeDescription": "Amazon EC2 instances can run various services that communicate over common ports such as 22 (SSH), 3389 (RDP), 80 (HTTP), and 443 (HTTPS) (and more). If these ports are open to the internet, attackers can attempt unauthorized access, brute-force attacks, or exploit known vulnerabilities. To reduce security risks, EC2 instances should be configured so that common ports are not exposed to the public internet, unless explicitly required and properly secured.", - "AdditionalInformation": "Exposing common ports directly to the internet increases the attack surface and risks unauthorized access or system compromise. Attackers frequently scan for open ports to target misconfigured or unpatched services. To enhance security, access to EC2 common ports should be restricted using security groups, network ACLs, and VPC configurations, ensuring that only trusted networks and users can connect.", - "LevelOfRisk": 5, - "Weight": 1000 - } - ] - }, - { - "Id": "2.1.7", - "Description": "Ensure that ec2 security groups do not allow ingress from internet to common ports", - "Checks": [ - "ec2_securitygroup_allow_ingress_from_internet_to_high_risk_tcp_ports", - "ec2_securitygroup_allow_ingress_from_internet_to_port_mongodb_27017_27018", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_ftp_port_20_21", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_22", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_3389", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_cassandra_7199_9160_8888", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_elasticsearch_kibana_9200_9300_5601", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_kafka_9092", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_memcached_11211", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_mysql_3306", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_oracle_1521_2483", - "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_ingress_from_internet_to_tcp_port_sql_server_1433_1434", - "ec2_securitygroup_allow_ingress_from_internet_to_tcp_port_telnet_23" - ], - "Attributes": [ - { - "Title": "Common ports from security groups do not allow ingress traffic", - "Section": "2. Attack Surface", - "SubSection": "2.1 Network", - "AttributeDescription": "Amazon EC2 security groups act as virtual firewalls, controlling inbound and outbound traffic to instances. If a security group allows ingress (incoming traffic) from the internet (0.0.0.0/0 or ::/0) to common ports such as 22 (SSH), 3389 (RDP), 80 (HTTP), or 443 (HTTPS) (and more), it creates a significant security risk. To minimize exposure, security groups should be configured to restrict ingress access to these ports to only trusted IP addresses or internal networks.", - "AdditionalInformation": "Allowing unrestricted inbound traffic to common ports increases the risk of brute-force attacks, unauthorized access, and exploitation of vulnerabilities. Attackers actively scan for open ports on public-facing EC2 instances to gain unauthorized control. To reduce security risks, ingress rules should be restricted using least privilege principles, IP whitelisting, VPN access, or bastion hosts, ensuring that only authorized users and networks can connect.", - "LevelOfRisk": 5, - "Weight": 1000 - } - ] - }, { "Id": "2.3.7", "Description": "Ensure there are no ECR repositories set as Public", @@ -976,114 +1120,6 @@ } ] }, - { - "Id": "2.2.9", - "Description": "Ensure EFS mount targets are not publicly accessible", - "Checks": [ - "efs_mount_target_not_publicly_accessible" - ], - "Attributes": [ - { - "Title": "No EFS mount targets are publicly accessible", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "Amazon Elastic File System (EFS) provides scalable, shared file storage for AWS services. EFS mount targets allow instances to connect to the file system within a VPC. By default, EFS mount targets can be configured with public accessibility, making them reachable from the internet. To enhance security, EFS mount targets should be restricted to private networks and should not be publicly accessible unless explicitly required and properly secured.", - "AdditionalInformation": "Publicly accessible EFS mount targets expose stored data to unauthorized access, cyberattacks, and data breaches. Attackers could exploit misconfigured security groups or network ACLs to access or modify files. To reduce security risks, EFS mount targets should be restricted to private subnets, with access limited to trusted VPCs, security groups, and IAM roles, ensuring secure file storage and controlled access.", - "LevelOfRisk": 3, - "Weight": 10 - } - ] - }, - { - "Id": "2.2.10", - "Description": "Ensure that EFS do not have policies which allow access to any client within the VPC", - "Checks": [ - "efs_not_publicly_accessible" - ], - "Attributes": [ - { - "Title": "No EFS have policies which allow access to any client within the VPC", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "Amazon Elastic File System (EFS) provides shared storage that can be accessed by multiple EC2 instances and services within a VPC. EFS access is controlled through resource-based policies that define which clients can connect. If an EFS policy allows access to any client within the VPC, it increases the risk of unauthorized access and data exposure. To enhance security, EFS policies should be restricted to specific IAM roles, security groups, or trusted resources instead of granting broad access to all VPC clients.", - "AdditionalInformation": "Allowing any client within a VPC to access an EFS file system increases the risk of data leaks, accidental modifications, or unauthorized access by compromised instances or misconfigured services. To minimize exposure, EFS policies should enforce least privilege access, restricting permissions to specific instances, roles, or users that require access, ensuring secure file storage and controlled data access.", - "LevelOfRisk": 3, - "Weight": 10 - } - ] - }, - { - "Id": "2.1.8", - "Description": "Ensure EKS cluster Network Policy is Enabled and Set as Appropriate", - "Checks": [ - "eks_cluster_network_policy_enabled" - ], - "Attributes": [ - { - "Title": "EKS cluster Network Policy is Enabled and Set as Appropriate", - "Section": "2. Attack Surface", - "SubSection": "2.1 Network", - "AttributeDescription": "A Network Policy defines how network traffic is controlled and restricted between workloads within a cloud environment. Enforcing network policies ensures that only authorized communication occurs between services, reducing the risk of unauthorized access and lateral movement. It is recommended to enable Network Policies and configure them appropriately to enforce least privilege access and secure communication between workloads.", - "AdditionalInformation": "Without properly configured Network Policies, workloads may be exposed to unnecessary or unauthorized network traffic, increasing the risk of data leaks, exploitation, or lateral movement by attackers. By enabling and enforcing Network Policies, organizations can limit communication between workloads, ensuring that only approved and necessary network interactions are allowed, minimizing the attack surface and enhancing overall security.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, - { - "Id": "2.1.9", - "Description": "Ensure EKS Clusters are not publicly accessible", - "Checks": [ - "eks_cluster_not_publicly_accessible" - ], - "Attributes": [ - { - "Title": "EKS Clusters are not publicly accessible", - "Section": "2. Attack Surface", - "SubSection": "2.1 Network", - "AttributeDescription": "Amazon Elastic Kubernetes Service (EKS) clusters manage containerized applications and can be configured with either private or public access. If an EKS cluster is publicly accessible, it means that the Kubernetes API endpoint can be reached from the internet, increasing the risk of unauthorized access and attacks. To enhance security, EKS clusters should be restricted to private networks and accessed only through secure VPNs, VPC peering, or AWS PrivateLink.", - "AdditionalInformation": "Exposing an EKS cluster to the public internet increases the risk of brute-force attacks, credential theft, and unauthorized access to Kubernetes workloads. Attackers could exploit misconfigured RBAC policies or API vulnerabilities to gain control over the cluster. To reduce security risks, EKS clusters should be configured with private endpoints, ensuring that only trusted networks and IAM-authenticated users can manage Kubernetes resources.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, - { - "Id": "2.1.10", - "Description": "Ensure EKS Clusters are created with Private Nodes", - "Checks": [ - "eks_cluster_private_nodes_enabled" - ], - "Attributes": [ - { - "Title": "EKS Clusters are created with Private Nodes", - "Section": "2. Attack Surface", - "SubSection": "2.1 Network", - "AttributeDescription": "Amazon Elastic Kubernetes Service (EKS) clusters run workloads on worker nodes, which can be either public or private. If EKS clusters are created with public nodes, these nodes are assigned public IP addresses, making them accessible from the internet, which increases the risk of unauthorized access and potential attacks. To enhance security, EKS clusters should be created with private nodes that operate within private subnets and are only accessible through secured networking configurations such as VPNs, VPC peering, or AWS PrivateLink.", - "AdditionalInformation": "Using public nodes in EKS exposes Kubernetes workloads to the internet, increasing the risk of unauthorized access, lateral movement, and potential exploitation. Attackers can target misconfigured workloads, open services, or unsecured API endpoints. By creating EKS clusters with private nodes, organizations can restrict access, limit exposure to public threats, and enforce network segmentation, ensuring that workloads remain secure and isolated within a private VPC environment.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, - { - "Id": "2.2.11", - "Description": "Ensure Elasticache Cluster is not using a public subnet", - "Checks": [ - "elasticache_cluster_uses_public_subnet" - ], - "Attributes": [ - { - "Title": "Elasticache Cluster is not using a public subnet", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "Amazon ElastiCache provides in-memory caching services using Redis and Memcached. By default, ElastiCache clusters can be deployed in either public or private subnets. If an ElastiCache cluster is placed in a public subnet, it becomes accessible from the internet, which significantly increases the risk of unauthorized access and data breaches. To enhance security, ElastiCache clusters should only be deployed in private subnets, ensuring restricted access within a VPC.", - "AdditionalInformation": "Deploying an ElastiCache cluster in a public subnet exposes it to external threats, such as unauthorized access, brute-force attacks, and potential data exfiltration. Attackers could exploit misconfigurations to access cached data or disrupt services. By restricting ElastiCache clusters to private subnets, organizations can limit access to trusted resources, enforce VPC security controls, and reduce the attack surface, ensuring secure and efficient caching operations.", - "LevelOfRisk": 3, - "Weight": 10 - } - ] - }, { "Id": "2.3.10", "Description": "Ensure no Elastic Load Balancers are facing internet", @@ -1156,42 +1192,6 @@ } ] }, - { - "Id": "2.2.12", - "Description": "Ensure S3 Glacier vaults have not policies which allow access to everyone", - "Checks": [ - "glacier_vaults_policy_public_access" - ], - "Attributes": [ - { - "Title": "S3 Glacier vaults have not policies which allow access to everyone", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "Amazon S3 Glacier provides low-cost, long-term storage for archival data. Glacier vaults can be configured with resource-based policies that control access. If a Glacier vault policy allows access to everyone, unauthorized users could retrieve or delete archived data, leading to data exposure or loss. To enhance security, Glacier vault policies should be restricted to specific AWS accounts, IAM roles, or trusted entities, ensuring only authorized users can access or manage archived data.", - "AdditionalInformation": "Allowing public access to S3 Glacier vaults poses a significant security risk, increasing the chance of data breaches, unauthorized deletions, or compliance violations. Attackers could restore and download sensitive archived data if the vault is misconfigured. To prevent unauthorized access, Glacier vaults should have strict access controls, using IAM policies, encryption, and resource-based permissions, ensuring that only trusted users and systems can interact with archived data.", - "LevelOfRisk": 5, - "Weight": 1000 - } - ] - }, - { - "Id": "2.2.13", - "Description": "Ensure Glue Data Catalogs are not publicly accessible", - "Checks": [ - "glue_data_catalogs_not_publicly_accessible" - ], - "Attributes": [ - { - "Title": "Glue Data Catalogs are not publicly accessible", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "AWS Glue Data Catalog is a centralized metadata repository used to store and manage schema information for data lakes and analytics workflows. By default, Glue Data Catalogs can be configured to allow public access, which poses a significant security risk if sensitive metadata is exposed. To enhance security, Glue Data Catalogs should be restricted to specific AWS accounts, IAM roles, or trusted services, ensuring that only authorized users can access or modify catalog information.", - "AdditionalInformation": "Allowing public access to Glue Data Catalogs increases the risk of unauthorized access, data leaks, and compliance violations. Attackers could gain insights into an organization’s data structure or modify catalog entries, leading to potential data corruption or unauthorized data exposure. To reduce security risks, Glue Data Catalogs should be secured using IAM policies, resource-based permissions, and AWS Lake Formation, ensuring that only trusted accounts and services can interact with metadata.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, { "Id": "2.3.14", "Description": "Ensure Kafka Cluster is not exposed to the public", @@ -1210,24 +1210,6 @@ } ] }, - { - "Id": "2.2.14", - "Description": "Ensure there are no internet exposed KMS keys", - "Checks": [ - "kms_key_not_publicly_accessible" - ], - "Attributes": [ - { - "Title": "No internet exposed KMS keys", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "AWS Key Management Service (KMS) provides secure encryption key management for data encryption and cryptographic operations. If KMS keys are exposed to the internet, unauthorized entities could potentially use, modify, or compromise encryption keys, leading to data breaches and security vulnerabilities. To enhance security, KMS keys should be restricted to trusted AWS accounts, IAM roles, and specific AWS services, ensuring that only authorized users and systems can access and manage them.", - "AdditionalInformation": "Exposing KMS keys to the public poses a critical security risk, as compromised keys can lead to unauthorized data decryption, loss of data integrity, and compliance violations. Attackers could potentially use public KMS keys to encrypt or decrypt sensitive data, undermining security controls. To prevent unauthorized access, KMS key policies should enforce strict access control using IAM permissions, VPC endpoint policies, and AWS PrivateLink, ensuring that encryption operations remain fully secured and isolated from the public internet.", - "LevelOfRisk": 3, - "Weight": 10 - } - ] - }, { "Id": "2.3.15", "Description": "Ensure lightsail database is not in public mode", @@ -1336,60 +1318,6 @@ } ] }, - { - "Id": "2.2.15", - "Description": "Ensure that S3 buckets have not policies which allow WRITE access", - "Checks": [ - "s3_bucket_policy_public_write_access" - ], - "Attributes": [ - { - "Title": "S3 buckets have not policies which allow WRITE access", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "Amazon S3 buckets store and manage data, files, and application assets. Bucket policies control access permissions, and if an S3 bucket has a policy that allows WRITE access to everyone, unauthorized users can upload, modify, or delete objects, leading to data tampering, security breaches, or service disruptions. To enhance security, S3 bucket policies should be restricted to specific AWS accounts, IAM roles, or trusted services, ensuring only authorized users have WRITE permissions.", - "AdditionalInformation": "Allowing unrestricted WRITE access to an S3 bucket increases the risk of unauthorized modifications, data injection attacks, and accidental data loss. Attackers could upload malicious files, delete critical data, or overwrite important configurations. To prevent unauthorized changes, S3 bucket policies should explicitly deny public WRITE access, enforce least privilege access control, and use AWS Block Public Access settings to ensure secure and controlled data storage.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, - { - "Id": "2.2.16", - "Description": "Ensure there are no S3 buckets listable by Everyone or Any AWS customer", - "Checks": [ - "s3_bucket_public_list_acl" - ], - "Attributes": [ - { - "Title": "No S3 buckets are listable by Everyone or Any AWS customer", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "Amazon S3 buckets store sensitive data and should have restricted access permissions. If an S3 bucket is listable by Everyone or Any AWS customer, unauthorized users can enumerate the objects within the bucket, potentially exposing sensitive information such as filenames, metadata, or even public datasets. To enhance security, S3 bucket permissions should be configured to restrict LIST access to only authorized IAM roles, AWS accounts, or specific services.", - "AdditionalInformation": "Allowing public or AWS-wide LIST access increases the risk of data enumeration, unauthorized access, and information leaks. Attackers or unauthorized users could identify and analyze stored files, extract metadata, or infer sensitive data. To mitigate this risk, S3 bucket policies should explicitly deny public LIST access, enforce least privilege permissions, and use AWS Block Public Access settings to prevent unintended data exposure.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, - { - "Id": "2.2.17", - "Description": "Ensure there are no S3 buckets writable by Everyone or Any AWS customer", - "Checks": [ - "s3_bucket_public_write_acl" - ], - "Attributes": [ - { - "Title": "No S3 buckets writable by Everyone or Any AWS customer", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "Amazon S3 buckets should have strict access controls to prevent unauthorized modifications. If an S3 bucket is writable by Everyone or Any AWS customer, it allows unauthorized users to upload, modify, or delete objects, leading to data corruption, security breaches, and compliance risks. To enhance security, S3 bucket permissions should be restricted to trusted IAM roles, AWS accounts, or specific services.", - "AdditionalInformation": "Allowing public or AWS-wide WRITE access creates a significant security risk, as attackers can inject malicious files, overwrite critical data, or delete essential objects. This could lead to data loss, malware distribution, or unauthorized system modifications. To prevent unauthorized changes, S3 bucket policies should explicitly deny public WRITE access, enforce least privilege access, and use AWS Block Public Access settings to secure data integrity and prevent unauthorized modifications.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, { "Id": "2.3.21", "Description": "Ensure Amazon SageMaker Notebook instances have not direct internet access", @@ -1408,24 +1336,6 @@ } ] }, - { - "Id": "2.2.18", - "Description": "Ensure Secrets Manager secrets are not publicly accessible", - "Checks": [ - "secretsmanager_not_publicly_accessible" - ], - "Attributes": [ - { - "Title": "Secrets Manager secrets are not publicly accessible", - "Section": "2. Attack Surface", - "SubSection": "2.2 Storage", - "AttributeDescription": "AWS Secrets Manager is used to securely store and manage sensitive information, such as API keys, database credentials, and encryption keys. By default, Secrets Manager secrets should be restricted to authorized IAM roles and AWS services. If a secret is publicly accessible, it can be exposed to unauthorized users, leading to data leaks, security breaches, and potential exploitation of sensitive credentials. To enhance security, Secrets Manager secrets should be strictly controlled using IAM policies and resource-based permissions.", - "AdditionalInformation": "Allowing public access to Secrets Manager secrets creates a critical security vulnerability, as attackers could retrieve, misuse, or exfiltrate sensitive information. Compromised secrets could lead to unauthorized access to databases, applications, or cloud services, resulting in data breaches, financial loss, or compliance violations. To mitigate this risk, Secrets Manager secrets should be restricted using least privilege IAM permissions, encrypted with AWS KMS, and accessed only by trusted AWS services and roles, ensuring secure and controlled secret management.", - "LevelOfRisk": 4, - "Weight": 100 - } - ] - }, { "Id": "2.3.22", "Description": "Ensure that SES identities are not publicly accessible", @@ -1516,24 +1426,6 @@ } ] }, - { - "Id": "3.3.1", - "Description": "Ensure AWS Config is enabled in all regions", - "Checks": [ - "config_recorder_all_regions_enabled" - ], - "Attributes": [ - { - "Title": "AWS Config is enabled", - "Section": "3. Logging and Monitoring", - "SubSection": "3.3 Monitoring", - "AttributeDescription": "AWS Config is a service that continuously monitors, records, and evaluates configuration changes in AWS resources within an account. It tracks configuration items, relationships between resources, and changes over time, delivering logs for security analysis, change management, and compliance auditing. To ensure comprehensive monitoring, AWS Config should be enabled in all regions.", - "AdditionalInformation": "Enabling AWS Config in all regions improves security, visibility, and compliance by: Tracking resource changes, allowing for quick identification of misconfigurations. Supporting security audits and forensic investigations by maintaining a historical record of configurations.", - "LevelOfRisk": 2, - "Weight": 8 - } - ] - }, { "Id": "3.1.3", "Description": "Ensure that server access logging is enabled on the CloudTrail S3 bucket", @@ -1552,42 +1444,6 @@ } ] }, - { - "Id": "4.2.3", - "Description": "Ensure CloudTrail logs are encrypted at rest using KMS CMKs", - "Checks": [ - "cloudtrail_kms_encryption_enabled" - ], - "Attributes": [ - { - "Title": "CloudTrail logs are encrypted at rest using KMS CMKs", - "Section": "4. Encryption", - "SubSection": "4.2 At-Rest", - "AttributeDescription": "AWS CloudTrail records API activity across an AWS account, and its logs contain sensitive security and operational data. AWS Key Management Service (KMS) provides encryption key management using customer-managed keys (CMKs) and Hardware Security Modules (HSMs) to ensure secure key storage and usage. CloudTrail logs can be encrypted using Server-Side Encryption (SSE) with KMS (SSE-KMS) to add an extra layer of protection and access control", - "AdditionalInformation": "Using SSE-KMS encryption for CloudTrail logs enhances security by adding an extra layer of access control. This ensures that only authorized users with both S3 read permissions and KMS decryption rights can access log data, protecting sensitive security information from unauthorized access or tampering. It also helps maintain compliance with security and regulatory standards by enforcing strict encryption controls.", - "LevelOfRisk": 3, - "Weight": 10 - } - ] - }, - { - "Id": "3.2.1", - "Description": "Ensure rotation for customer-created symmetric CMKs is enabled", - "Checks": [ - "kms_cmk_rotation_enabled" - ], - "Attributes": [ - { - "Title": "Rotation for customer-created symmetric CMKs enabled", - "Section": "3. Logging and Monitoring", - "SubSection": "3.2 Retention", - "AttributeDescription": "AWS Key Management Service (KMS) allows users to manage encryption keys securely. Key rotation enables the automatic replacement of the backing key (the cryptographic material tied to a customer-managed key (CMK)), ensuring continuous security without disrupting access to previously encrypted data. AWS automatically retains previous backing keys to allow seamless decryption of older data while using a newly generated key for encryption. It is recommended to enable key rotation for symmetric CMKs, as asymmetric keys do not support this feature.", - "AdditionalInformation": "Regularly rotating encryption keys minimizes the risk associated with key compromise by ensuring that newly encrypted data is protected with a fresh key, reducing the potential impact of an exposed key. Since AWS KMS retains prior backing keys for seamless decryption, rotation does not disrupt access to previously encrypted data. Implementing key rotation enhances security by limiting the exposure window of any single encryption key and aligning with best practices for cryptographic hygiene.", - "LevelOfRisk": 3, - "Weight": 10 - } - ] - }, { "Id": "3.1.4", "Description": "Ensure VPC flow logging is enabled in all VPCs", @@ -1642,6 +1498,60 @@ } ] }, + { + "Id": "3.2.1", + "Description": "Ensure rotation for customer-created symmetric CMKs is enabled", + "Checks": [ + "kms_cmk_rotation_enabled" + ], + "Attributes": [ + { + "Title": "Rotation for customer-created symmetric CMKs enabled", + "Section": "3. Logging and Monitoring", + "SubSection": "3.2 Retention", + "AttributeDescription": "AWS Key Management Service (KMS) allows users to manage encryption keys securely. Key rotation enables the automatic replacement of the backing key (the cryptographic material tied to a customer-managed key (CMK)), ensuring continuous security without disrupting access to previously encrypted data. AWS automatically retains previous backing keys to allow seamless decryption of older data while using a newly generated key for encryption. It is recommended to enable key rotation for symmetric CMKs, as asymmetric keys do not support this feature.", + "AdditionalInformation": "Regularly rotating encryption keys minimizes the risk associated with key compromise by ensuring that newly encrypted data is protected with a fresh key, reducing the potential impact of an exposed key. Since AWS KMS retains prior backing keys for seamless decryption, rotation does not disrupt access to previously encrypted data. Implementing key rotation enhances security by limiting the exposure window of any single encryption key and aligning with best practices for cryptographic hygiene.", + "LevelOfRisk": 3, + "Weight": 10 + } + ] + }, + { + "Id": "3.2.2", + "Description": "Ensure CloudWatch Log Groups have a retention policy of specific days", + "Checks": [ + "cloudwatch_log_group_retention_policy_specific_days_enabled" + ], + "Attributes": [ + { + "Title": "CloudWatch Log Groups have a retention policy of specific days", + "Section": "3. Logging and Monitoring", + "SubSection": "3.2 Retention", + "AttributeDescription": "AWS CloudWatch Log Groups store logs from various AWS services and applications, enabling monitoring, debugging, and security auditing. By default, CloudWatch logs are retained indefinitely, which can lead to unnecessary data storage costs and compliance risks. To manage log lifecycle effectively, it is recommended to set a retention policy for CloudWatch Log Groups, ensuring logs are retained only for a specific number of days based on operational and compliance requirements.", + "AdditionalInformation": "Setting a retention policy for CloudWatch logs helps balance cost management, compliance, and security. Retaining logs for too long increases storage costs and potential exposure to sensitive data, while keeping them for too short a duration can limit forensic investigations and compliance reporting. By defining a specific retention period, organizations can ensure logs are available for troubleshooting and audits while adhering to data retention best practices and regulatory requirements.", + "LevelOfRisk": 2, + "Weight": 8 + } + ] + }, + { + "Id": "3.3.1", + "Description": "Ensure AWS Config is enabled in all regions", + "Checks": [ + "config_recorder_all_regions_enabled" + ], + "Attributes": [ + { + "Title": "AWS Config is enabled", + "Section": "3. Logging and Monitoring", + "SubSection": "3.3 Monitoring", + "AttributeDescription": "AWS Config is a service that continuously monitors, records, and evaluates configuration changes in AWS resources within an account. It tracks configuration items, relationships between resources, and changes over time, delivering logs for security analysis, change management, and compliance auditing. To ensure comprehensive monitoring, AWS Config should be enabled in all regions.", + "AdditionalInformation": "Enabling AWS Config in all regions improves security, visibility, and compliance by: Tracking resource changes, allowing for quick identification of misconfigurations. Supporting security audits and forensic investigations by maintaining a historical record of configurations.", + "LevelOfRisk": 2, + "Weight": 8 + } + ] + }, { "Id": "3.3.2", "Description": "Ensure unauthorized API calls are monitored", @@ -1931,20 +1841,110 @@ ] }, { - "Id": "3.2.2", - "Description": "Ensure CloudWatch Log Groups have a retention policy of specific days", + "Id": "4.1.1", + "Description": "Ensure S3 Bucket Policy is set to deny HTTP requests", "Checks": [ - "cloudwatch_log_group_retention_policy_specific_days_enabled" + "s3_bucket_secure_transport_policy" ], "Attributes": [ { - "Title": "CloudWatch Log Groups have a retention policy of specific days", - "Section": "3. Logging and Monitoring", - "SubSection": "3.2 Retention", - "AttributeDescription": "AWS CloudWatch Log Groups store logs from various AWS services and applications, enabling monitoring, debugging, and security auditing. By default, CloudWatch logs are retained indefinitely, which can lead to unnecessary data storage costs and compliance risks. To manage log lifecycle effectively, it is recommended to set a retention policy for CloudWatch Log Groups, ensuring logs are retained only for a specific number of days based on operational and compliance requirements.", - "AdditionalInformation": "Setting a retention policy for CloudWatch logs helps balance cost management, compliance, and security. Retaining logs for too long increases storage costs and potential exposure to sensitive data, while keeping them for too short a duration can limit forensic investigations and compliance reporting. By defining a specific retention period, organizations can ensure logs are available for troubleshooting and audits while adhering to data retention best practices and regulatory requirements.", - "LevelOfRisk": 2, - "Weight": 8 + "Title": "S3 bucket deny HTTP requests", + "Section": "4. Encryption", + "SubSection": "4.1 In-Transit", + "AttributeDescription": "Amazon S3 bucket permissions can be configured using a bucket policy to enforce access restrictions. To enhance security, objects within the bucket should be made accessible only via HTTPS, ensuring encrypted data transmission.", + "AdditionalInformation": "By default, Amazon S3 accepts both HTTP and HTTPS requests, which can expose data to interception. To enforce secure access, HTTP requests should be explicitly denied in the bucket policy. Simply allowing HTTPS without blocking HTTP does not fully comply with security best practices, as unencrypted requests may still be accepted.", + "LevelOfRisk": 3, + "Weight": 10 + } + ] + }, + { + "Id": "4.1.2", + "Description": "Ensure that EC2 Metadata Service only allows IMDSv2", + "Checks": [ + "ec2_instance_imdsv2_enabled" + ], + "Attributes": [ + { + "Title": "EC2 Metadata Service only allows IMDSv2", + "Section": "4. Encryption", + "SubSection": "4.1 In-Transit", + "AttributeDescription": "AWS EC2 instances allow users to choose between Instance Metadata Service Version 1 (IMDSv1), which uses a request/response model, or Instance Metadata Service Version 2 (IMDSv2), which uses a session-based approach for enhanced security", + "AdditionalInformation": "Instance metadata refers to the data about an EC2 instance, such as host names, events, and security groups, that is used for managing and configuring the instance. When enabling the Metadata Service, users can opt for either IMDSv1, which operates via a simple request/response model, or IMDSv2, which implements session authentication for additional security. With IMDSv2, each request is secured by session-based authentication, ensuring that all interactions with the instance's metadata and credentials are protected. IMDSv1, on the other hand, may expose instances to Server-Side Request Forgery (SSRF) attacks. To improve security, Amazon recommends using IMDSv2", + "LevelOfRisk": 4, + "Weight": 100 + } + ] + }, + { + "Id": "4.1.3", + "Description": "Ensure that all expired SSL/TLS certificates stored in AWS IAM are removed", + "Checks": [ + "iam_no_expired_server_certificates_stored" + ], + "Attributes": [ + { + "Title": "Expired SSL/TLS certificates removed", + "Section": "4. Encryption", + "SubSection": "4.1 In-Transit", + "AttributeDescription": "To enable HTTPS connections for applications and websites hosted on AWS, an SSL/TLS server certificate is required. AWS provides two options for managing certificates: AWS Certificate Manager (ACM) – The preferred method for managing SSL/TLS certificates, automating renewals and deployment. IAM Certificate Storage – Used only when deploying SSL/TLS certificates in regions not supported by ACM. IAM securely encrypts private keys and stores them, but certificates must be obtained from an external provider. ACM certificates cannot be uploaded to IAM, and IAM certificates cannot be managed from the IAM Console.", + "AdditionalInformation": "Removing expired SSL/TLS certificates prevents the accidental deployment of invalid certificates, which could cause service disruptions, security warnings, and loss of credibility for applications using AWS services like Elastic Load Balancer (ELB). As a best practice, expired certificates should be deleted to maintain a secure and trusted application environment.", + "LevelOfRisk": 5, + "Weight": 1000 + } + ] + }, + { + "Id": "4.2.1", + "Description": "Ensure EBS volume encryption is enabled in all regions", + "Checks": [ + "ec2_ebs_volume_encryption" + ], + "Attributes": [ + { + "Title": "EBS volume encryption", + "Section": "4. Encryption", + "SubSection": "4.2 At-Rest", + "AttributeDescription": "Amazon Elastic Compute Cloud (EC2) supports encryption at rest for Elastic Block Store (EBS) volumes, ensuring that stored data remains protected. While EBS encryption is disabled by default, organizations can enforce automatic encryption of newly created volumes to enhance data security and compliance.", + "AdditionalInformation": "Enforcing EBS volume encryption reduces the risk of data exposure, unauthorized access, and compliance violations. If encryption remains intact, even if storage is compromised, data remains unreadable to unauthorized users. Encrypting data at rest ensures that sensitive information is protected against accidental disclosure, insider threats, and external attacks.", + "LevelOfRisk": 3, + "Weight": 10 + } + ] + }, + { + "Id": "4.2.2", + "Description": "Ensure that encryption-at-rest is enabled for RDS instances", + "Checks": [ + "rds_instance_storage_encrypted" + ], + "Attributes": [ + { + "Title": "RDS instances encryption at rest enabled", + "Section": "4. Encryption", + "SubSection": "4.2 At-Rest", + "AttributeDescription": "Amazon Relational Database Service (RDS) supports encryption at rest using the industry-standard AES-256 encryption algorithm to secure database instances and their associated storage. Once enabled, RDS encryption automatically handles access authentication and decryption, ensuring secure data storage with minimal performance impact.", + "AdditionalInformation": "Databases often contain sensitive and business-critical information, making encryption essential to protect against unauthorized access and data breaches. Enabling RDS encryption ensures that underlying storage, automated backups, read replicas, and snapshots are all encrypted, preventing accidental or malicious data exposure while maintaining compliance with security best practices.", + "LevelOfRisk": 3, + "Weight": 10 + } + ] + }, + { + "Id": "4.2.3", + "Description": "Ensure CloudTrail logs are encrypted at rest using KMS CMKs", + "Checks": [ + "cloudtrail_kms_encryption_enabled" + ], + "Attributes": [ + { + "Title": "CloudTrail logs are encrypted at rest using KMS CMKs", + "Section": "4. Encryption", + "SubSection": "4.2 At-Rest", + "AttributeDescription": "AWS CloudTrail records API activity across an AWS account, and its logs contain sensitive security and operational data. AWS Key Management Service (KMS) provides encryption key management using customer-managed keys (CMKs) and Hardware Security Modules (HSMs) to ensure secure key storage and usage. CloudTrail logs can be encrypted using Server-Side Encryption (SSE) with KMS (SSE-KMS) to add an extra layer of protection and access control", + "AdditionalInformation": "Using SSE-KMS encryption for CloudTrail logs enhances security by adding an extra layer of access control. This ensures that only authorized users with both S3 read permissions and KMS decryption rights can access log data, protecting sensitive security information from unauthorized access or tampering. It also helps maintain compliance with security and regulatory standards by enforcing strict encryption controls.", + "LevelOfRisk": 3, + "Weight": 10 } ] } diff --git a/prowler/compliance/github/cis_1.0_github.json b/prowler/compliance/github/cis_1.0_github.json index bc01cfccdc..cf58f60452 100644 --- a/prowler/compliance/github/cis_1.0_github.json +++ b/prowler/compliance/github/cis_1.0_github.json @@ -6,11 +6,12 @@ "Requirements": [ { "Id": "1.1.1", - "Description": "Ensure any changes to code are tracked in a version control platform", + "Description": "Manage all code projects in a version control platform.", "Checks": [], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Manage all code projects in a version control platform.", @@ -19,18 +20,19 @@ "RemediationProcedure": "Upload existing code projects to a dedicated Github organization and repositories and create an identity for each active team member who might contribute or need access to it.", "AuditProcedure": "Ensure that all code activity is managed through Github repository for every micro-service or application developed by an organization.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.1.2", - "Description": "Ensure any change to code can be traced back to its associated task", + "Description": "Use a task management system to trace any code back to its associated task.", "Checks": [], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Use a task management system to trace any code back to its associated task.", @@ -39,829 +41,869 @@ "RemediationProcedure": "Use a task management system to manage tasks as the starting point for each code change. Whether it is a new feature, bug fix, or security fix - all should originate from a dedicated task (ticket) in your organization's task management system. These tasks should also be linked to the code changes themselves in a way that is easy to follow: from code to task, and from task back to code.", "AuditProcedure": "Ensure every code change can be traced back to its origin task in a task management system.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.1.3", - "Description": "Ensure any change to code receives approval of two strongly authenticated users", + "Description": "Ensure that every code change is reviewed and approved by two authorized contributors who are both strongly authenticated - using Multi-Factor Authentication (MFA), from the team relevant to the code change.", "Checks": [ "repository_default_branch_requires_multiple_approvals" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Automated", "Description": "Ensure that every code change is reviewed and approved by two authorized contributors who are both strongly authenticated - using Multi-Factor Authentication (MFA), from the team relevant to the code change.", "RationaleStatement": "To prevent malicious or unauthorized code changes, the first layer of protection is the process of code review. This process involves engineer teammates reviewing each other's code for errors, optimizations, and general knowledge-sharing. With proper peer reviews in place, an organization can detect unwanted code changes very early in the process of release. In order to help facilitate code review, companies should employ automation to verify that every code change has been reviewed and approved by at least two team members before it is pushed into the code base. These team members should be from the team that is related to the code change, so it will be a meaningful review.", "ImpactStatement": "To enforce a code review requirement, verification for a minimum of two reviewers must be put into place. This will ensure new code will not be able to be pushed to the code base before it has received two independent approvals.", - "RemediationProcedure": "For every code repository in use, perform the next steps to require two approvals from the specific code repository team in order to push new code to the code base:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you added the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Check **Require a pull request before merging** and **Require approvals**, and set **Required number of approvals before merging** to 2.\n 5. Click **Create** or **Save changes**.", - "AuditProcedure": "For every code repository in use, perform the next steps to verify that two approvals from the specific code repository team are required to push new code to the code base:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Require a pull request before merging** and **Require approvals** are checked, and verify that **Required number of approvals before merging** is set to 2.", + "RemediationProcedure": "For every code repository in use, perform the next steps to require two approvals from the specific code repository team in order to push new code to the code base:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you added the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Check **Require a pull request before merging** and **Require approvals**, and set **Required number of approvals before merging** to 2.\n5. Click **Create** or **Save changes**.", + "AuditProcedure": "For every code repository in use, perform the next steps to verify that two approvals from the specific code repository team are required to push new code to the code base:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Require a pull request before merging** and **Require approvals** are checked, and verify that **Required number of approvals before merging** is set to 2.", "AdditionalInformation": "", - "DefaultValue": "0", - "References": "" + "References": "", + "DefaultValue": "0" } ] }, { "Id": "1.1.4", - "Description": "Ensure previous approvals are dismissed when updates are introduced to a code change proposal", + "Description": "Ensure that when a proposed code change is updated, previous approvals are declined, and new approvals are required.", "Checks": [], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure that when a proposed code change is updated, previous approvals are declined, and new approvals are required.", "RationaleStatement": "An approval process is necessary when code changes are suggested. Through this approval process, however, changes can still be made to the original proposal even after some approvals have already been given. This means malicious code can find its way into the code base even if the organization has enforced a review policy. To ensure this is not possible, outdated approvals must be declined when changes to the suggestion are introduced.", "ImpactStatement": "If new code changes are pushed to a specific proposal, all previously accepted code change proposals must be declined.", - "RemediationProcedure": "For each code repository in use, perform the next steps to enforce dismissal of given approvals to code change suggestions if those suggestions were updated:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you added the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Select **Require pull request reviews before merging** and then **Dismiss stale pull request approvals when new commits are pushed**.\n 5. Click **Create** or **Save changes**.", - "AuditProcedure": "For each code repository in use, perform the next steps to verify that each updated code suggestion declines the previously received approvals:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Require a pull request before merging** is checked, and verify that **Dismiss stale pull request approvals when new commits are pushed** is checked.", + "RemediationProcedure": "For each code repository in use, perform the next steps to enforce dismissal of given approvals to code change suggestions if those suggestions were updated:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you added the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Select **Require pull request reviews before merging** and then **Dismiss stale pull request approvals when new commits are pushed**.\n5. Click **Create** or **Save changes**.", + "AuditProcedure": "For each code repository in use, perform the next steps to verify that each updated code suggestion declines the previously received approvals:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Require a pull request before merging** is checked, and verify that **Dismiss stale pull request approvals when new commits are pushed** is checked.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.1.5", - "Description": "Ensure there are restrictions on who can dismiss code change reviews", + "Description": "Only trusted users should be allowed to dismiss code change reviews.", "Checks": [], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Only trusted users should be allowed to dismiss code change reviews.", "RationaleStatement": "Dismissing a code change review permits users to merge new suggested code changes without going through the standard process of approvals. Controlling who can perform this action will prevent malicious actors from simply dismissing the required reviews to code changes and merging malicious or dysfunctional code into the code base.", - "ImpactStatement": "In cases where a code change proposal has been updated since it was last reviewed and the person who reviewed it isn't available for approval, a general collaborator would not be able to merge their code changes until a user with \"dismiss review\" abilities could dismiss the open review.\n \n\n Users who are not allowed to dismiss code change reviews will not be permitted to do so, and thus are unable to waive the standard flow of approvals.", - "RemediationProcedure": "For each code repository in use, perform the next steps to restrict dismissal of code changes reviews unless it is necessary:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you added the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 4. Select **Require pull request reviews before merging** and **Restrict who can dismiss pull request reviews**.\n 5. Do not add any user or team unless it is obligatory. If it is obligatory, carefully select the users or teams whom you trust with the ability to dismiss code change reviews.\n 6. Click **Create** or **Save changes**.", - "AuditProcedure": "For each code repository in use, perform the next steps to verify that only trusted users are allowed to dismiss code change reviews: \n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 4. Verify that **Require a pull request before merging** and **Restrict who can dismiss pull request reviews** is checked.\n 5. Verify that no users and teams are specified except for organization and repository admins. If it is obligatory, verify that the users or teams specified were carefully selected to be trusted with the ability to dismiss code change reviews.", + "ImpactStatement": "In cases where a code change proposal has been updated since it was last reviewed and the person who reviewed it isn't available for approval, a general collaborator would not be able to merge their code changes until a user with \"dismiss review\" abilities could dismiss the open review.\n\nUsers who are not allowed to dismiss code change reviews will not be permitted to do so, and thus are unable to waive the standard flow of approvals.", + "RemediationProcedure": "For each code repository in use, perform the next steps to restrict dismissal of code changes reviews unless it is necessary:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you added the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n4. Select **Require pull request reviews before merging** and **Restrict who can dismiss pull request reviews**.\n5. Do not add any user or team unless it is obligatory. If it is obligatory, carefully select the users or teams whom you trust with the ability to dismiss code change reviews.\n6. Click **Create** or **Save changes**.", + "AuditProcedure": "For each code repository in use, perform the next steps to verify that only trusted users are allowed to dismiss code change reviews: \n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n4. Verify that **Require a pull request before merging** and **Restrict who can dismiss pull request reviews** is checked.\n5. Verify that no users and teams are specified except for organization and repository admins. If it is obligatory, verify that the users or teams specified were carefully selected to be trusted with the ability to dismiss code change reviews.", "AdditionalInformation": "", - "DefaultValue": "By default, all users who have write access to the code repository are able to dismiss code change reviews.", - "References": "" + "References": "", + "DefaultValue": "By default, all users who have write access to the code repository are able to dismiss code change reviews." } ] }, { "Id": "1.1.6", - "Description": "Ensure code owners are set for extra sensitive code or configuration", + "Description": "Code owners are trusted users that are responsible for reviewing and managing an important piece of code or configuration. An organization is advised to set code owners for every extremely sensitive code or configuration.", "Checks": [ "repository_has_codeowners_file" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Code owners are trusted users that are responsible for reviewing and managing an important piece of code or configuration. An organization is advised to set code owners for every extremely sensitive code or configuration.", "RationaleStatement": "Configuring code owners protects data by verifying that trusted users will notice and review every edit, thus preventing unwanted or malicious changes from potentially compromising sensitive code or configurations.", "ImpactStatement": "Code owner users will receive notifications for every change that occurs to the code and subsequently added as reviewers of pull requests automatically.", - "RemediationProcedure": "In every code repository create a CODEOWNERS file in the root, docs/, or .github/ directory of the repository.\n In the file, specify codeowners for the .github/workflows/ directory atleast. Specify organization members you trust. For example:\n ```\n .github/workflows/ @user1 @user2\n ```", - "AuditProcedure": "In every code repository, verify that a file named CODEOWNERS exists in the root, docs/, or .github/ directory of the repository.\n In the CODEOWNERS file, verify that the users specified are users you trust.", + "RemediationProcedure": "In every code repository create a CODEOWNERS file in the root, docs/, or .github/ directory of the repository.\nIn the file, specify codeowners for the .github/workflows/ directory atleast. Specify organization members you trust. For example:\n```\n.github/workflows/ @user1 @user2\n```", + "AuditProcedure": "In every code repository, verify that a file named CODEOWNERS exists in the root, docs/, or .github/ directory of the repository.\nIn the CODEOWNERS file, verify that the users specified are users you trust.", "AdditionalInformation": "", - "DefaultValue": "None", - "References": "https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners" + "References": "https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners", + "DefaultValue": "None" } ] }, { "Id": "1.1.7", - "Description": "Ensure code owner's review is required when a change affects owned code", + "Description": "Ensure trusted code owners are required to review and approve any code change proposal made to their respective owned areas in the code base.", "Checks": [ "repository_default_branch_requires_codeowners_review" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure trusted code owners are required to review and approve any code change proposal made to their respective owned areas in the code base.", "RationaleStatement": "Configuring code owners ensures that no code, especially code which could prove malicious, will slip into the source code or configuration files of a repository. This allows an organization to mark areas in the code base that are especially sensitive or more prone to an attack. It can also enforce review by specific individuals who are designated as owners to those areas so that they may filter out unauthorized or unwanted changes beforehand.", "ImpactStatement": "If an organization enforces code owner-based reviews, some code change proposals would not be able to be merged to the codebase before specific, trusted individuals approve them.", - "RemediationProcedure": "For every code repository in use, perform the following steps to require code owners' approvals for each change proposal related to code they own:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Select **Require pull request reviews before merging** and **Require review from Code Owners**.\n 7. Click **Create** or **Save changes**.", - "AuditProcedure": "For every code repository in use, perform the following steps to verify that code owners are required to review all code change proposals relevant to areas they own before code merge:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 4. Ensure that **Require a pull request before merging** and **Require review from Code Owners** are checked.", + "RemediationProcedure": "For every code repository in use, perform the following steps to require code owners' approvals for each change proposal related to code they own:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Select **Require pull request reviews before merging** and **Require review from Code Owners**.\n7. Click **Create** or **Save changes**.", + "AuditProcedure": "For every code repository in use, perform the following steps to verify that code owners are required to review all code change proposals relevant to areas they own before code merge:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n4. Ensure that **Require a pull request before merging** and **Require review from Code Owners** are checked.", "AdditionalInformation": "", - "DefaultValue": "Code owners are not required to review changes by default.", - "References": "" + "References": "", + "DefaultValue": "Code owners are not required to review changes by default." } ] }, { "Id": "1.1.8", - "Description": "Ensure inactive branches are periodically reviewed and removed", + "Description": "Keep track of code branches that are inactive for a lengthy period of time and periodically remove them.", "Checks": [ "repository_inactive_not_archived", "repository_branch_delete_on_merge_enabled" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Keep track of code branches that are inactive for a lengthy period of time and periodically remove them.", "RationaleStatement": "Git branches that have been inactive (i.e., no new changes introduced) for a long period of time are enlarging the surface of attack for malicious code injection, sensitive data leaks, and CI pipeline exploitation. They potentially contain outdated dependencies which may leave them highly vulnerable. They are more likely to be improperly managed, and could possibly be accessed by a large number of members of the organization.", "ImpactStatement": "Removing inactive Git branches means that any code changes they contain would be removed along with them, thus work done in the past might not be accessible after auditing for inactivity.", - "RemediationProcedure": "For each code repository in use, review existing Git branches and remove those which have not been active for a period of time by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Above the list of files, click **Branches**.\n 3. Use the navigation at the top of the page to view the Stale branches. The Stale view shows all branches that no one has committed to in the last three months, ordered by the branches with the oldest commits first.\n 4. For each branch listed, either delete it by clicking the trash bin icon, or find the valid reason it still exists.\n \n\n You can perform the next steps to prevent pull request branches from becoming stale branches:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click Settings.\n 3. Under \"Pull Requests\", select **Automatically delete head branches**.", - "AuditProcedure": "For each code repository in use, verify that all existing Git branches are active or have yet to be checked for inactivity by performing the next steps:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Above the list of files, click **Branches**.\n 3. Use the navigation at the top of the page to view the Stale branches. The Stale view shows all branches that no one has committed to in the last three months, ordered by the branches with the oldest commits first.\n 4. If the list is empty, you are compliant. If the list is not empty, but there is a valid reason the branches listed are not deleted, you are compliant.", + "RemediationProcedure": "For each code repository in use, review existing Git branches and remove those which have not been active for a period of time by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Above the list of files, click **Branches**.\n3. Use the navigation at the top of the page to view the Stale branches. The Stale view shows all branches that no one has committed to in the last three months, ordered by the branches with the oldest commits first.\n4. For each branch listed, either delete it by clicking the trash bin icon, or find the valid reason it still exists.\n\nYou can perform the next steps to prevent pull request branches from becoming stale branches:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click Settings.\n3. Under \"Pull Requests\", select **Automatically delete head branches**.", + "AuditProcedure": "For each code repository in use, verify that all existing Git branches are active or have yet to be checked for inactivity by performing the next steps:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Above the list of files, click **Branches**.\n3. Use the navigation at the top of the page to view the Stale branches. The Stale view shows all branches that no one has committed to in the last three months, ordered by the branches with the oldest commits first.\n4. If the list is empty, you are compliant. If the list is not empty, but there is a valid reason the branches listed are not deleted, you are compliant.", "AdditionalInformation": "", - "DefaultValue": "By default, newly opened Git branches would never be removed, regardless of activity or inactivity.", - "References": "" + "References": "", + "DefaultValue": "By default, newly opened Git branches would never be removed, regardless of activity or inactivity." } ] }, { "Id": "1.1.9", - "Description": "Ensure all checks have passed before merging new code", + "Description": "Before a code change request can be merged to the code base, all predefined checks must successfully pass.", "Checks": [ "repository_default_branch_status_checks_required" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Before a code change request can be merged to the code base, all predefined checks must successfully pass.", "RationaleStatement": "On top of manual reviews of code changes, a code protect should contain a set of prescriptive checks which validate each change. Organizations should enforce those status checks so that changes can only be introduced if all checks have successfully passed. This set of checks should serve as the absolute quality, stability, and security conditions which must be met in order to merge new code to a project.", "ImpactStatement": "Code changes in which all checks do not pass successfully would not be able to be pushed into the code base of the specific code repository.", - "RemediationProcedure": "For each code repository in use, require all status checks to pass before permitting a merge of new code by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", check if there is a rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Select **Require status checks to pass before merging**.\n 7. Click **Create** or **Save changes**.", - "AuditProcedure": "For each code repository in use, verify that status checks are required to pass before allowing any code change proposal merge by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Require status checks to pass before merging** is checked.", + "RemediationProcedure": "For each code repository in use, require all status checks to pass before permitting a merge of new code by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", check if there is a rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Select **Require status checks to pass before merging**.\n7. Click **Create** or **Save changes**.", + "AuditProcedure": "For each code repository in use, verify that status checks are required to pass before allowing any code change proposal merge by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Require status checks to pass before merging** is checked.", "AdditionalInformation": "", - "DefaultValue": "By default, no checks are defined per project, and thus no enforcement of checks is made.", - "References": "" + "References": "", + "DefaultValue": "By default, no checks are defined per project, and thus no enforcement of checks is made." } ] }, { "Id": "1.1.10", - "Description": "Ensure open Git branches are up to date before they can be merged into code base", + "Description": "Organizations should make sure each suggested code change is in full sync with the existing state of its origin code repository before allowing merging.", "Checks": [], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Organizations should make sure each suggested code change is in full sync with the existing state of its origin code repository before allowing merging.", "RationaleStatement": "Git branches can easily become outdated since the origin code repository is constantly being edited. This means engineers working on separate code branches can accidentally include outdated code with potential security issues which might have already been fixed, overriding the potential solutions for those security issues when merging their own changes.", "ImpactStatement": "If enforced, outdated branches would not be able to be merged into their origin repository without first being updated to contain any recent changes.", - "RemediationProcedure": "For each code repository in use, enforce a policy to only allow merging open branches if they are current with the latest change from their original repository by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Select **Require status checks to pass before merging** and **Require branches to be up to date before merging**.\n 7. Click **Create** or **Save changes**.", - "AuditProcedure": "For each code repository in use, verify that open branches must be updated before merging is permitted by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Require status checks to pass before merging** and **Require branches to be up to date before merging** are checked.", + "RemediationProcedure": "For each code repository in use, enforce a policy to only allow merging open branches if they are current with the latest change from their original repository by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Select **Require status checks to pass before merging** and **Require branches to be up to date before merging**.\n7. Click **Create** or **Save changes**.", + "AuditProcedure": "For each code repository in use, verify that open branches must be updated before merging is permitted by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Require status checks to pass before merging** and **Require branches to be up to date before merging** are checked.", "AdditionalInformation": "", - "DefaultValue": "By default, there is no requirement to update a branch before merging it.", - "References": "https://github.blog/changelog/2022-02-03-more-ways-to-keep-your-pull-request-branch-up-to-date/" + "References": "https://github.blog/changelog/2022-02-03-more-ways-to-keep-your-pull-request-branch-up-to-date/", + "DefaultValue": "By default, there is no requirement to update a branch before merging it." } ] }, { "Id": "1.1.11", - "Description": "Ensure all open comments are resolved before allowing code change merging", + "Description": "Organizations should enforce a \"no open comments\" policy before allowing code change merging.", "Checks": [ "repository_default_branch_requires_conversation_resolution" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Organizations should enforce a \"no open comments\" policy before allowing code change merging.", "RationaleStatement": "In an open code change proposal, reviewers can leave comments containing their questions and suggestions. These comments can also include potential bugs and security issues. Requiring all comments on a code change proposal to be resolved before it can be merged ensures that every concern is properly addressed or acknowledged before the new code changes are introduced to the code base.", "ImpactStatement": "Code change proposals containing open comments would not be able to be merged into the code base.", - "RemediationProcedure": "For each code repository in use, require open comments to be resolved before the relevant code change can be merged by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Select **Require conversation resolution before merging**.\n 7. Click **Create** or **Save changes**.", - "AuditProcedure": "For every code repository in use, verify that each merged code change does not contain open, unattended comments by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Require conversation resolution before merging** is checked.", + "RemediationProcedure": "For each code repository in use, require open comments to be resolved before the relevant code change can be merged by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Select **Require conversation resolution before merging**.\n7. Click **Create** or **Save changes**.", + "AuditProcedure": "For every code repository in use, verify that each merged code change does not contain open, unattended comments by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Require conversation resolution before merging** is checked.", "AdditionalInformation": "", - "DefaultValue": "By default, code changes with open comments on them are able to be merged into the code base.", - "References": "" + "References": "", + "DefaultValue": "By default, code changes with open comments on them are able to be merged into the code \nbase." } ] }, { "Id": "1.1.12", - "Description": "Ensure verification of signed commits for new changes before merging", + "Description": "Ensure every commit in a pull request is signed and verified before merging.", "Checks": [ "repository_default_branch_requires_signed_commits" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Ensure every commit in a pull request is signed and verified before merging.", "RationaleStatement": "Signing commits, or requiring to sign commits, gives other users confidence about the origin of a specific code change. It ensures that the author of the change is not hidden and is verified by the version control system, thus the change comes from a trusted source.", "ImpactStatement": "Pull requests with unsigned commits cannot be merged.", - "RemediationProcedure": "For each repository in use, enforce the branch protection rule of requiring signed commits, and make sure only signed commits are capable of merging by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Select **Require signed commits**.\n 7. Click **Create** or **Save changes**.", - "AuditProcedure": "Ensure only signed commits can be merged for every branch, especially the main branch, via branch protection rules by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Require signed commits** is checked.", + "RemediationProcedure": "For each repository in use, enforce the branch protection rule of requiring signed commits, and make sure only signed commits are capable of merging by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Select **Require signed commits**.\n7. Click **Create** or **Save changes**.", + "AuditProcedure": "Ensure only signed commits can be merged for every branch, especially the main branch, via branch protection rules by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Require signed commits** is checked.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits" + "References": "https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits", + "DefaultValue": "" } ] }, { "Id": "1.1.13", - "Description": "Ensure linear history is required", + "Description": "Linear history is the name for Git history where all commits are listed in chronological order, one after another. Such history exists if a pull request is merged either by rebase merge (re-order the commits history) or squash merge (squashes all commits to one). Ensure that linear history is required by requiring the use of rebase or squash merge when merging a pull request.", "Checks": [ "repository_default_branch_requires_linear_history" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Linear history is the name for Git history where all commits are listed in chronological order, one after another. Such history exists if a pull request is merged either by rebase merge (re-order the commits history) or squash merge (squashes all commits to one). Ensure that linear history is required by requiring the use of rebase or squash merge when merging a pull request.", "RationaleStatement": "Enforcing linear history produces a clear record of activity, and as such it offers specific advantages: it is easier to follow, easier to revert a change, and bugs can be found more easily.", "ImpactStatement": "Pull request cannot be merged except squash or rebase merge.", - "RemediationProcedure": "For every code repository in use, perform the following steps to require linear history and/or allow only rebase merge and squash merge:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Select **Require linear history**.\n 7. Click **Create** or **Save changes**.", - "AuditProcedure": "For every code repository in use, perform the following steps to verify that linear history is required and/or that only squash merge and rebase merge are allowed:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Require linear history** is checked.", + "RemediationProcedure": "For every code repository in use, perform the following steps to require linear history and/or allow only rebase merge and squash merge:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Select **Require linear history**.\n7. Click **Create** or **Save changes**.", + "AuditProcedure": "For every code repository in use, perform the following steps to verify that linear history is required and/or that only squash merge and rebase merge are allowed:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Require linear history** is checked.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.1.14", - "Description": "Ensure branch protection rules are enforced for administrators", + "Description": "Ensure administrators are subject to branch protection rules.", "Checks": [ "repository_default_branch_protection_applies_to_admins" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure administrators are subject to branch protection rules.", "RationaleStatement": "Administrators by default are excluded from any branch protection rules. This means these privileged users (both on the repository and organization levels) are not subject to protections meant to prevent untrusted code insertion, including malicious code. This is extremely important since administrator accounts are often targeted for account hijacking due to their privileged role.", "ImpactStatement": "Administrator users won't be able to push code directly to the protected branch without being compliant with listed branch protection rules.", - "RemediationProcedure": "For every code repository in use, enforce branch protection rules on administrators as well, by performing the following:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Select **Do not allow bypassing the above settings**.\n 7. Click **Create** or **Save changes**.", - "AuditProcedure": "For every code repository in use, validate branch protection rules also apply to administrator accounts by performing the next steps:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Do not allow bypassing the above settings** is checked.", + "RemediationProcedure": "For every code repository in use, enforce branch protection rules on administrators as well, by performing the following:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Select **Do not allow bypassing the above settings**.\n7. Click **Create** or **Save changes**.", + "AuditProcedure": "For every code repository in use, validate branch protection rules also apply to administrator accounts by performing the next steps:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Do not allow bypassing the above settings** is checked.", "AdditionalInformation": "", - "DefaultValue": "Administrator accounts are not subject to branch protection rules by default.", - "References": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule" + "References": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule", + "DefaultValue": "Administrator accounts are not subject to branch protection rules by default." } ] }, { "Id": "1.1.15", - "Description": "Ensure pushing or merging of new code is restricted to specific individuals or teams", + "Description": "Ensure that only trusted users can push or merge new code to protected branches.", "Checks": [], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Ensure that only trusted users can push or merge new code to protected branches.", "RationaleStatement": "Requiring that only trusted users may push or merge new changes reduces the risk of unverified code, especially malicious code, to a protected branch by reducing the number of trusted users who are capable of doing such.", "ImpactStatement": "Only administrators and trusted users can push or merge to the protected branch.", - "RemediationProcedure": "For every code repository in use, allow only trusted and responsible users to push or merge new code by performing the following:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4.Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Select **Restrict who can push to matching branches** and choose trusted and responsible users and teams who will have the permission to do so. \n 7. Click **Create** or **Save changes**.", - "AuditProcedure": "For every code repository in use, ensure only trusted and responsible users can push or merge new code by performing the following:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Restrict who can push to matching branches** is checked and that only trusted and responsible users and teams are selected.", + "RemediationProcedure": "For every code repository in use, allow only trusted and responsible users to push or merge new code by performing the following:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4.Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Select **Restrict who can push to matching branches** and choose trusted and responsible users and teams who will have the permission to do so. \n7. Click **Create** or **Save changes**.", + "AuditProcedure": "For every code repository in use, ensure only trusted and responsible users can push or merge new code by performing the following:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Restrict who can push to matching branches** is checked and that only trusted and responsible users and teams are selected.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule" + "References": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule", + "DefaultValue": "" } ] }, { "Id": "1.1.16", - "Description": "Ensure force push code to branches is denied", + "Description": "The \"Force Push\" option allows users with \"Push\" permissions to force their changes directly to the branch without a pull request, and thus should be disabled.", "Checks": [ "repository_default_branch_disallows_force_push" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "The \"Force Push\" option allows users with \"Push\" permissions to force their changes directly to the branch without a pull request, and thus should be disabled.", "RationaleStatement": "The \"Force Push\" option allows users to override the existing code with their own code. This can lead to both intentional and unintentional data loss, as well as data infection with malicious code. Disabling the \"Force Push\" option prohibits users from forcing their changes to the master branch, which ultimately prevents malicious code from entering source code.", "ImpactStatement": "Users cannot force push to protected branches.", - "RemediationProcedure": "For each repository in use, block the option to \"Force Push\" code by performing the following: \n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Uncheck **Allow force pushes**.\n 7. Click **Create** or **Save changes**.", - "AuditProcedure": "For every code repository in use, validate that no one can force push code by performing the following:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Allow force pushes** is not checked.", + "RemediationProcedure": "For each repository in use, block the option to \"Force Push\" code by performing the following: \n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Uncheck **Allow force pushes**.\n7. Click **Create** or **Save changes**.", + "AuditProcedure": "For every code repository in use, validate that no one can force push code by performing the following:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Allow force pushes** is not checked.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule" + "References": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule", + "DefaultValue": "" } ] }, { "Id": "1.1.17", - "Description": "Ensure branch deletions are denied", + "Description": "Ensure that users with only push access are incapable of deleting a protected branch.", "Checks": [ "repository_default_branch_deletion_disabled" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure that users with only push access are incapable of deleting a protected branch.", "RationaleStatement": "When enabling deletion of a protected branch, any user with at least push access to the repository can delete a branch. This can be potentially dangerous, as a simple human mistake or a hacked account can lead to data loss if a branch is deleted. It is therefore crucial to prevent such incidents by denying protected branch deletion.", "ImpactStatement": "Protected branches cannot be deleted.", - "RemediationProcedure": "For each repository that is being used, block the option to delete protected branches by performing the following:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n 5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n 6. Uncheck **Allow deletions**.\n 7. Click **Create** or **Save changes**.", - "AuditProcedure": "For each repository that is being used, verify that protected branches cannot be deleted by performing the following:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n 5. Ensure that **Allow deletions** is not checked.", + "RemediationProcedure": "For each repository that is being used, block the option to delete protected branches by performing the following:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, click **Add rule**.\n5. If you add the rule, under \"Branch name pattern\", type the branch name or pattern you want to protect.\n6. Uncheck **Allow deletions**.\n7. Click **Create** or **Save changes**.", + "AuditProcedure": "For each repository that is being used, verify that protected branches cannot be deleted by performing the following:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", verify that there is at least one rule for your main branch. If there is, click **Edit** to its right. If there isn't, you are not compliant.\n5. Ensure that **Allow deletions** is not checked.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule" + "References": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/managing-a-branch-protection-rule", + "DefaultValue": "" } ] }, { "Id": "1.1.18", - "Description": "Ensure any merging of code is automatically scanned for risks", + "Description": "Ensure that every pull request is required to be scanned for risks.", "Checks": [], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure that every pull request is required to be scanned for risks.", "RationaleStatement": "Scanning pull requests to detect risks allows for early detection of vulnerable code and/or dependencies and helps mitigate potentially malicious code.", "ImpactStatement": "", - "RemediationProcedure": "For every repository in use, enforce risk scanning on every pull request by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Actions**.\n 3. If the repository already has at least one workflow set up and running, click **New workflow** and go to step 5. If there are currently no workflows configured for the repository, go to the next step.\n 4. Scroll down to the \"Security\" category and click **Configure** under the workflow you want to configure or click **View all** to see all available security workflows.\n 5. On the right pane of the workflow page, click **Documentation** and follow the on-screen instructions to tailor the workflow to your needs.", - "AuditProcedure": "For each repository in use, ensure that every pull request must be scanned for risks by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Actions**.\n 3. Ensure that at least one workflow is configured to run like that: \n ```\n on:\n push:\n branches: [ \"main\" ]\n pull_request:\n branches: [ \"main\" ]\n ```\n and that it has a step that runs code scanning on the code.", + "RemediationProcedure": "For every repository in use, enforce risk scanning on every pull request by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Actions**.\n3. If the repository already has at least one workflow set up and running, click **New workflow** and go to step 5. If there are currently no workflows configured for the repository, go to the next step.\n4. Scroll down to the \"Security\" category and click **Configure** under the workflow you want to configure or click **View all** to see all available security workflows.\n5. On the right pane of the workflow page, click **Documentation** and follow the on-screen instructions to tailor the workflow to your needs.", + "AuditProcedure": "For each repository in use, ensure that every pull request must be scanned for risks by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Actions**.\n3. Ensure that at least one workflow is configured to run like that: \n```\non:\n push:\n branches: [ \"main\" ]\n pull_request:\n branches: [ \"main\" ]\n```\nand that it has a step that runs code scanning on the code.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.1.19", - "Description": "Ensure any changes to branch protection rules are audited", + "Description": "Ensure that changes in the branch protection rules are audited.", "Checks": [], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure that changes in the branch protection rules are audited.", "RationaleStatement": "Branch protection rules should be configured on every repository. The only users who may change such rules are administrators. In a case of an attack on an administrator account or of human error on the part of an administrator, protection rules could be disabled, and thus decrease source code confidentiality as a result. It is important to track and audit such changes to prevent potential incidents as soon as possible.", "ImpactStatement": "", - "RemediationProcedure": "Use the audit log to audit changes in branch protection rules by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Archives\" section of the sidebar, click **Logs**, then click **Audit log**.\n 4. Use the action qualifier in your query and look for **protected_branch** category. Ensure every action is reasonable and secure and investigate if not.", - "AuditProcedure": "Ensure changes in branch protection rules are audited by performing the following regularly:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Archives\" section of the sidebar, click **Logs**, then click **Audit log**.\n 4. Use the action qualifier in your query and look for **protected_branch** category. Ensure every action is reasonable and secure and is investigated if not.", + "RemediationProcedure": "Use the audit log to audit changes in branch protection rules by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Archives\" section of the sidebar, click **Logs**, then click **Audit log**.\n4. Use the action qualifier in your query and look for **protected_branch** category. Ensure every action is reasonable and secure and investigate if not.", + "AuditProcedure": "Ensure changes in branch protection rules are audited by performing the following regularly:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Archives\" section of the sidebar, click **Logs**, then click **Audit log**.\n4. Use the action qualifier in your query and look for **protected_branch** category. Ensure every action is reasonable and secure and is investigated if not.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches" + "References": "https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/defining-the-mergeability-of-pull-requests/about-protected-branches", + "DefaultValue": "" } ] }, { "Id": "1.1.20", - "Description": "Ensure branch protection is enforced on the default branch", + "Description": "Enforce branch protection on the default and main branch.", "Checks": [ "repository_default_branch_protection_enabled" ], "Attributes": [ { - "Section": "1.1", + "Section": "1 Source Code", + "Subsection": "1.1 Code Changes", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Enforce branch protection on the default and main branch.", "RationaleStatement": "The default or main branch of repositories is considered very important, as it is eventually gets deployed to the production. Therefore it needs protection. By enforcing branch protection rules on this branch, it is secured from unwanted or unauthorized changes. It can also be protected from untested and unreviewed changes and more.", "ImpactStatement": "", - "RemediationProcedure": "Perform the following to enforce branch protection on the main branch:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Next to \"Branch protection rules\", click **Add rule**.\n 5. Under \"Branch name pattern\", type the branch name or pattern you want to protect. Ensure it applies to the main branch name.\n 6. Configure policies you want.\n 7. Click Create.", - "AuditProcedure": "Perform the following to ensure branch protection is enforced on the main branch:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n 4. Under **Branch protection rules**, verify that there is a rule applied to the \"main\" or default branch.", + "RemediationProcedure": "Perform the following to enforce branch protection on the main branch:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Next to \"Branch protection rules\", click **Add rule**.\n5. Under \"Branch name pattern\", type the branch name or pattern you want to protect. Ensure it applies to the main branch name.\n6. Configure policies you want.\n7. Click Create.", + "AuditProcedure": "Perform the following to ensure branch protection is enforced on the main branch:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Code and automation\" section of the sidebar, click **Branches**.\n4. Under **Branch protection rules**, verify that there is a rule applied to the \"main\" or default branch.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.2.1", - "Description": "Ensure all public repositories contain a SECURITY.md file", + "Description": "A SECURITY.md file is a security policy file that offers instruction on reporting security vulnerabilities in a project. When someone creates an issue within a specific project, a link to the SECURITY.md file will subsequently be shown.", "Checks": [ "repository_public_has_securitymd_file" ], "Attributes": [ { - "Section": "1.2", + "Section": "1 Source Code", + "Subsection": "1.2 Repository Management", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "A SECURITY.md file is a security policy file that offers instruction on reporting security vulnerabilities in a project. When someone creates an issue within a specific project, a link to the SECURITY.md file will subsequently be shown.", "RationaleStatement": "A SECURITY.md file provides users with crucial security information. It can also serve an important role in project maintenance, encouraging users to think ahead about how to properly handle potential security issues, updates, and general security practices.", "ImpactStatement": "", - "RemediationProcedure": "Enforce that each public repository has a SECURITY.md file by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under the repository name, click **Security**.\n 3. In the left sidebar, click **Security policy**.\n 4. Click **Start setup**.\n 5. In the new SECURITY.md file, add information about supported versions of your project and how to report a vulnerability.\n 6. At the bottom of the page, type a commit message.\n 7. Below the commit message fields, choose to create a new branch for your commit and then create a pull request.\n 8. Click **Propose file change**.", - "AuditProcedure": "Verify that each public repository has a SECURITY.md file by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under the repository name, click **Security**.\n 3. Verify that **Security policy** is enabled.", + "RemediationProcedure": "Enforce that each public repository has a SECURITY.md file by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under the repository name, click **Security**.\n3. In the left sidebar, click **Security policy**.\n4. Click **Start setup**.\n5. In the new SECURITY.md file, add information about supported versions of your project and how to report a vulnerability.\n6. At the bottom of the page, type a commit message.\n7. Below the commit message fields, choose to create a new branch for your commit and then create a pull request.\n8. Click **Propose file change**.", + "AuditProcedure": "Verify that each public repository has a SECURITY.md file by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under the repository name, click **Security**.\n3. Verify that **Security policy** is enabled.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.2.2", - "Description": "Ensure repository creation is limited to specific members", + "Description": "Limit the ability to create repositories to trusted users and teams.", "Checks": [], "Attributes": [ { - "Section": "1.2", + "Section": "1 Source Code", + "Subsection": "1.2 Repository Management", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Limit the ability to create repositories to trusted users and teams.", "RationaleStatement": "Restricting repository creation to trusted users and teams is recommended in order to keep the organization properly structured, track fewer items, prevent impersonation, and to not overload the version-control system. It will allow administrators easier source code tracking and management capabilities, as they will have fewer repositories to track. The process of detecting potential attacks also becomes far more straightforward, as well, since the easier it is to track the source code, the easier it is to detect malicious acts within it. Additionally, the possibility of a member creating a public repository and sharing the organization's data externally is significantly decreased.", "ImpactStatement": "Specific users will not be permitted to create repositories.", - "RemediationProcedure": "Restrict repository creation to trusted users and teams only by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Access\" section of the sidebar, click **Member privileges**.\n 4. Under \"Repository creation\", unselect both options - **Public** and **Private**.\n 5. Click **Save**.", - "AuditProcedure": "Verify that only trusted users and teams can create repositories by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Access\" section of the sidebar, click **Member privileges**.\n 4. Under \"Repository creation\", ensure that **Public** and **Private** are not checked. This means only owners are able to create repositories.", + "RemediationProcedure": "Restrict repository creation to trusted users and teams only by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Access\" section of the sidebar, click **Member privileges**.\n4. Under \"Repository creation\", unselect both options - **Public** and **Private**.\n5. Click **Save**.", + "AuditProcedure": "Verify that only trusted users and teams can create repositories by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Access\" section of the sidebar, click **Member privileges**.\n4. Under \"Repository creation\", ensure that **Public** and **Private** are not checked. This means only owners are able to create repositories.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.2.3", - "Description": "Ensure repository deletion is limited to specific users", + "Description": "Ensure only a limited number of trusted users can delete repositories.", "Checks": [], "Attributes": [ { - "Section": "1.2", + "Section": "1 Source Code", + "Subsection": "1.2 Repository Management", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure only a limited number of trusted users can delete repositories.", "RationaleStatement": "Restricting the ability to delete repositories protects the organization from intentional and unintentional data loss. This ensures that users cannot delete repositories or cause other potential damage — whether by accident or due to their account being hacked — unless they have the correct privileges.", "ImpactStatement": "Certain users will not be permitted to delete repositories.", - "RemediationProcedure": "Enforce repository deletion by a few trusted and responsible users only by performing either of the following steps:\n \n\n If Your organizations > Settings > Access > Member privileges > Allow members to delete or transfer repositories for this organization is selected, allow only trusted members to have admin privileges:\n 1. In every repository, on GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings** and then in the \"Access\" section of the sidebar, click **Collaborators & teams**.\n 3. Under \"Manage access\" use the dropdown **Role** menu to filter and search for admin members. Change their role by clicking the **Role** dropdown next to the username until there are only two trusted and qualified users with admin privileges. \n \n\n If it is not selected, allow only trusted users to become an organization owner:\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Click the name of your organization and under your organization name, click **People**.\n 3. You will see a list of the people in your organization. Click Role and select Owners. Check every member you want to change permissions to. Above the list of members, use the drop-down menu and click Change role and choose Member > change role. Do that until there are only a few trusted and qualified users with organization owner privileges. \n \n\n In any case, only members with administrator or organization owner privileges can delete repositories, regardless of your setting.", - "AuditProcedure": "Verify that only a limited number of trusted users can delete repositories by performing either of the following steps:\n \n\n If Your organizations > Settings > Access > Member privileges > Allow members to delete or transfer repositories for this organization is selected, verify that every admin member is trusted by you:\n 1. In every repository, on GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings** and then in the \"Access\" section of the sidebar, click **Collaborators & teams**.\n 3. Under \"Manage access\" use the dropdown **Role** menu to filter and search for admin members. Verify that there are only two of them and that they are trusted and qualified. \n \n\n If it is not selected, verify that every organization owner is trusted by you:\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Click the name of your organization and under your organization name, click **People**.\n 3. You will see a list of the people in your organization. Click Role and select Owners. Verify that there are only a few of them and that they are trusted and qualified. \n \n\n In any case, only members with administrator or organization owner privileges can delete repositories, regardless of your setting.", + "RemediationProcedure": "Enforce repository deletion by a few trusted and responsible users only by performing either of the following steps:\n\nIf Your organizations > Settings > Access > Member privileges > Allow members to delete or transfer repositories for this organization is selected, allow only trusted members to have admin privileges:\n1. In every repository, on GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings** and then in the \"Access\" section of the sidebar, click **Collaborators & teams**.\n3. Under \"Manage access\" use the dropdown **Role** menu to filter and search for admin members. Change their role by clicking the **Role** dropdown next to the username until there are only two trusted and qualified users with admin privileges. \n\nIf it is not selected, allow only trusted users to become an organization owner:\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Click the name of your organization and under your organization name, click **People**.\n3. You will see a list of the people in your organization. Click Role and select Owners. Check every member you want to change permissions to. Above the list of members, use the drop-down menu and click Change role and choose Member > change role. Do that until there are only a few trusted and qualified users with organization owner privileges. \n\nIn any case, only members with administrator or organization owner privileges can delete repositories, regardless of your setting.", + "AuditProcedure": "Verify that only a limited number of trusted users can delete repositories by performing either of the following steps:\n\nIf Your organizations > Settings > Access > Member privileges > Allow members to delete or transfer repositories for this organization is selected, verify that every admin member is trusted by you:\n1. In every repository, on GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings** and then in the \"Access\" section of the sidebar, click **Collaborators & teams**.\n3. Under \"Manage access\" use the dropdown **Role** menu to filter and search for admin members. Verify that there are only two of them and that they are trusted and qualified. \n\nIf it is not selected, verify that every organization owner is trusted by you:\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Click the name of your organization and under your organization name, click **People**.\n3. You will see a list of the people in your organization. Click Role and select Owners. Verify that there are only a few of them and that they are trusted and qualified. \n\nIn any case, only members with administrator or organization owner privileges can delete repositories, regardless of your setting.", "AdditionalInformation": "", - "DefaultValue": "Only organization owners or members with admin privileges can delete repositories.", - "References": "" + "References": "", + "DefaultValue": "Only organization owners or members with admin privileges can delete repositories." } ] }, { "Id": "1.2.4", - "Description": "Ensure issue deletion is limited to specific users", + "Description": "Ensure only trusted and responsible users can delete issues.", "Checks": [], "Attributes": [ { - "Section": "1.2", + "Section": "1 Source Code", + "Subsection": "1.2 Repository Management", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure only trusted and responsible users can delete issues.", "RationaleStatement": "Issues are a way to keep track of things happening in repositories, such as setting new milestones or requesting urgent fixes. Deleting an issue is not a benign activity, as it might harm the development workflow or attempt to hide malicious behavior. Because of this, it should be restricted and allowed only by trusted and responsible users.", "ImpactStatement": "Certain users will not be permitted to delete issues.", - "RemediationProcedure": "Restrict issue deletion to a few trusted and responsible users only by performing either of the following steps:\n \n\n If Your organizations > Settings > Access > Member privileges > Allow members to delete issues for this organization is selected, allow only trusted members to have admin privileges:\n 1. In every repository, on GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings** and then in the \"Access\" section of the sidebar, click **Collaborators & teams**.\n 3. Under \"Manage access\" use the dropdown **Role** menu to filter and search for admin members. Change their role by clicking the **Role** dropdown next to the username until there are only two trusted and qualified users with admin privileges.\n \n\n If it is not selected, allow only trusted users to become an organization owner:\n 1. In the top right corner of GitHub.com, click your profile photo, then click Your organizations.\n 2. Click the name of your organization and under your organization name, click **People**.\n 3. You will see a list of the people in your organization. Click **Role** and select **Owners**. Check every member you want to change permissions to. Above the list of members, use the drop-down menu and click **Change role** and choose Member > change role. Do that until there are only a few trusted and qualified users with organization owner privileges.\n \n\n In any case, only members with administrator or organization owner privileges can delete issues, regardless of your setting.", - "AuditProcedure": "Verify that only a limited number of trusted users can delete issues by performing either of the following steps:\n \n\n If Your organizations > Settings > Access > Member privileges > Allow members to delete issues for this organization is selected, verify that every admin member is trusted by you:\n 1. In every repository, on GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings** and then in the \"Access\" section of the sidebar, click **Collaborators & teams**.\n 3. Under \"Manage access\" use the dropdown **Role** menu to filter and search for admin members. Verify that there are only two of them and that they are trusted and qualified.\n \n\n If it is not selected, verify that every organization owner is trusted by you:\n 1. In the top right corner of GitHub.com, click your profile photo, then click Your organizations.\n 2. Click the name of your organization and under your organization name, click **People**.\n 3. You will see a list of the people in your organization. Click **Role** and select **Owners**. Verify that there are only a few of them and that they are trusted and qualified.\n \n\n In any case, only members with administrator or organization owner privileges can delete issues, regardless of your setting.", + "RemediationProcedure": "Restrict issue deletion to a few trusted and responsible users only by performing either of the following steps:\n\nIf Your organizations > Settings > Access > Member privileges > Allow members to delete issues for this organization is selected, allow only trusted members to have admin privileges:\n1. In every repository, on GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings** and then in the \"Access\" section of the sidebar, click **Collaborators & teams**.\n3. Under \"Manage access\" use the dropdown **Role** menu to filter and search for admin members. Change their role by clicking the **Role** dropdown next to the username until there are only two trusted and qualified users with admin privileges.\n\nIf it is not selected, allow only trusted users to become an organization owner:\n1. In the top right corner of GitHub.com, click your profile photo, then click Your organizations.\n2. Click the name of your organization and under your organization name, click **People**.\n3. You will see a list of the people in your organization. Click **Role** and select **Owners**. Check every member you want to change permissions to. Above the list of members, use the drop-down menu and click **Change role** and choose Member > change role. Do that until there are only a few trusted and qualified users with organization owner privileges.\n\nIn any case, only members with administrator or organization owner privileges can delete issues, regardless of your setting.", + "AuditProcedure": "Verify that only a limited number of trusted users can delete issues by performing either of the following steps:\n\nIf Your organizations > Settings > Access > Member privileges > Allow members to delete issues for this organization is selected, verify that every admin member is trusted by you:\n1. In every repository, on GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings** and then in the \"Access\" section of the sidebar, click **Collaborators & teams**.\n3. Under \"Manage access\" use the dropdown **Role** menu to filter and search for admin members. Verify that there are only two of them and that they are trusted and qualified.\n\nIf it is not selected, verify that every organization owner is trusted by you:\n1. In the top right corner of GitHub.com, click your profile photo, then click Your organizations.\n2. Click the name of your organization and under your organization name, click **People**.\n3. You will see a list of the people in your organization. Click **Role** and select **Owners**. Verify that there are only a few of them and that they are trusted and qualified.\n\nIn any case, only members with administrator or organization owner privileges can delete issues, regardless of your setting.", "AdditionalInformation": "", - "DefaultValue": "Only organization owners or members with admin privileges can delete issues.", - "References": "" + "References": "", + "DefaultValue": "Only organization owners or members with admin privileges can delete issues." } ] }, { "Id": "1.2.5", - "Description": "Ensure all copies (forks) of code are tracked and accounted for", + "Description": "Track every fork of code and ensure it is accounted for.", "Checks": [], "Attributes": [ { - "Section": "1.2", + "Section": "1 Source Code", + "Subsection": "1.2 Repository Management", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Track every fork of code and ensure it is accounted for.", "RationaleStatement": "A fork is a copy of a repository. On top of being a plain copy, any updates to the original repository itself can be pulled and reflected by the fork under certain conditions. A large number of repository copies (forks) become difficult to manage and properly secure. New and sensitive changes can often be pushed into a critical repository without developer knowledge of an updated copy of the very same repository. If there is no limit on doing this, then it is recommended to track and delete copies of organization repositories as needed.", "ImpactStatement": "Disabling forks completely may slow down the development process as more actions will be necessary to take in order to fork a repository.", - "RemediationProcedure": "Track forks and examine them by performing the following on a regular basis:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Insights**.\n 3. In the left sidebar, click **Forks**.\n 4. Examine the forks listed there.", - "AuditProcedure": "Verify that the following steps are done regularly to track and examine forks:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Insights**.\n 3. In the left sidebar, click **Forks**.\n 4. Examine the forks listed there.", + "RemediationProcedure": "Track forks and examine them by performing the following on a regular basis:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Insights**.\n3. In the left sidebar, click **Forks**.\n4. Examine the forks listed there.", + "AuditProcedure": "Verify that the following steps are done regularly to track and examine forks:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Insights**.\n3. In the left sidebar, click **Forks**.\n4. Examine the forks listed there.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.2.6", - "Description": "Ensure all code projects are tracked for changes in visibility status", + "Description": "Ensure every change in visibility of projects is tracked.", "Checks": [], "Attributes": [ { - "Section": "1.2", + "Section": "1 Source Code", + "Subsection": "1.2 Repository Management", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure every change in visibility of projects is tracked.", "RationaleStatement": "Visibility of projects determines who can access a project and/or fork it: anyone, designated users, or only members of the organization. If a private project becomes public, this may point to a potential attack, which can ultimately lead to data loss, the leaking of sensitive information, and finally to a supply chain attack. It is crucial to track these changes in order to prevent such incidents.", "ImpactStatement": "", - "RemediationProcedure": "Track every change in project visibility and investigate if suspicious behavior occurs, by performing the following regularly:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Archives\" section of the sidebar, click **Logs**, then click **Audit log**.\n 4. Use the **repo** qualifier in your query and look for **access** category. Ensure every change is reasonable and secure and investigate if it is not.", - "AuditProcedure": "Ensure that every change in project visibility is tracked and investigated, by performing the following regularly:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Archives\" section of the sidebar, click **Logs**, then click **Audit log**.\n 4. Use the **repo** qualifier in your query and look for **access** category. Ensure every change is reasonable and secure and is investigated if it is not.", + "RemediationProcedure": "Track every change in project visibility and investigate if suspicious behavior occurs, by performing the following regularly:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Archives\" section of the sidebar, click **Logs**, then click **Audit log**.\n4. Use the **repo** qualifier in your query and look for **access** category. Ensure every change is reasonable and secure and investigate if it is not.", + "AuditProcedure": "Ensure that every change in project visibility is tracked and investigated, by performing the following regularly:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Archives\" section of the sidebar, click **Logs**, then click **Audit log**.\n4. Use the **repo** qualifier in your query and look for **access** category. Ensure every change is reasonable and secure and is investigated if it is not.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.2.7", - "Description": "Ensure inactive repositories are reviewed and archived periodically", + "Description": "Track inactive repositories and remove them periodically.", "Checks": [], "Attributes": [ { - "Section": "1.2", + "Section": "1 Source Code", + "Subsection": "1.2 Repository Management", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Track inactive repositories and remove them periodically.", "RationaleStatement": "Inactive repositories (i.e., no new changes introduced for a long period of time) can enlarge the surface of a potential attack or data leak. These repositories are more likely to be improperly managed, and thus could possibly be accessed by a large number of users in an organization.", "ImpactStatement": "Bug fixes and deployment of necessary changes could prove complicated for archived repositories.", - "RemediationProcedure": "Perform the following to review all inactive repositories and archive them periodically:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Click on your organization name and then on **repositories**.\n 3. Ensure every repository listed has been active in the last 3 to 6 months. Every repository that isn't active you should either review or archive by performing the next steps:\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. Under \"Danger Zone\", click **Archive this repository**.\n 4. Read the warnings.\n 5. Type the name of the repository you want to archive.\n 6. Click **I understand the consequences, archive this repository**.", - "AuditProcedure": "Perform the following to ensure that all the repositories in the organization are active, and those that are not reviewed or archived:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Click on your organization name and then on **repositories**.\n 3. Ensure every repository listed has been active in the last 3 to 6 months. If it's not, then ensure it is archived or reviewed regularly.", + "RemediationProcedure": "Perform the following to review all inactive repositories and archive them periodically:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Click on your organization name and then on **repositories**.\n3. Ensure every repository listed has been active in the last 3 to 6 months. Every repository that isn't active you should either review or archive by performing the next steps:\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. Under \"Danger Zone\", click **Archive this repository**.\n 4. Read the warnings.\n 5. Type the name of the repository you want to archive.\n 6. Click **I understand the consequences, archive this repository**.", + "AuditProcedure": "Perform the following to ensure that all the repositories in the organization are active, and those that are not reviewed or archived:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Click on your organization name and then on **repositories**.\n3. Ensure every repository listed has been active in the last 3 to 6 months. If it's not, then ensure it is archived or reviewed regularly.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.3.1", - "Description": "Ensure inactive users are reviewed and removed periodically", + "Description": "Track inactive user accounts and periodically remove them.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Track inactive user accounts and periodically remove them.", "RationaleStatement": "User accounts that have been inactive for a long period of time are enlarging the surface of attack. Inactive users with high-level privileges are of particular concern, as these accounts are more likely to be targets for attackers. This could potentially allow access to large portions of an organization should such an attack prove successful. It is recommended to remove them as soon as possible in order to prevent this.", "ImpactStatement": "", - "RemediationProcedure": "If you have GitHub AE, perform the following to review inactive user accounts and remove them:\n \n\n 1. From an administrative account on GitHub AE, in the upper-right corner of any page, click the rocket icon.\n 2. If you're not already on the \"Site admin\" page, in the upper-left corner, click **Site admin**.\n 3. In the left sidebar, click **Dormant users**.\n 4. Find the users listed there under **Your organizations** > your organization > **People** and select them.\n 5. Click **Remove from organization** and **Remove members**.\n \n\n If you have GitHub Enterprise Cloud, perform the following:\n \n\n 1. In the top-right corner of GitHub.com, click your profile photo, then click **Your enterprises**.\n 2. In the list of enterprises, click the enterprise you want to view.\n 3. In the enterprise account sidebar, click **Compliance**.\n 4. To download your Dormant Users (beta) report as a CSV file, under \"Other\", click **Download**.\n 5. Find the users listed in the file under **Your organizations** > your organization > **People** and select them.\n 6. Click **Remove from organization** and **Remove members**.", - "AuditProcedure": "If you have GitHub AE, verify that all user accounts are active by performing the following:\n \n\n 1. From an administrative account on GitHub AE, in the upper-right corner of any page, click the rocket icon.\n 2. If you're not already on the \"Site admin\" page, in the upper-left corner, click **Site admin**.\n 3. In the left sidebar, click **Dormant users**.\n 4. Verify that the list is empty.\n \n\n If you have GitHub Enterprise Cloud, perform the following:\n \n\n 1. In the top-right corner of GitHub.com, click your profile photo, then click **Your enterprises**.\n 2. In the list of enterprises, click the enterprise you want to view.\n 3. In the enterprise account sidebar, click **Compliance**.\n 4. To download your Dormant Users (beta) report as a CSV file, under \"Other\", click **Download**.\n 5. Verify that there are no users listed.", + "RemediationProcedure": "If you have GitHub AE, perform the following to review inactive user accounts and remove them:\n\n1. From an administrative account on GitHub AE, in the upper-right corner of any page, click the rocket icon.\n2. If you're not already on the \"Site admin\" page, in the upper-left corner, click **Site admin**.\n3. In the left sidebar, click **Dormant users**.\n4. Find the users listed there under **Your organizations** > your organization > **People** and select them.\n5. Click **Remove from organization** and **Remove members**.\n\nIf you have GitHub Enterprise Cloud, perform the following:\n\n1. In the top-right corner of GitHub.com, click your profile photo, then click **Your enterprises**.\n2. In the list of enterprises, click the enterprise you want to view.\n3. In the enterprise account sidebar, click **Compliance**.\n4. To download your Dormant Users (beta) report as a CSV file, under \"Other\", click **Download**.\n5. Find the users listed in the file under **Your organizations** > your organization > **People** and select them.\n6. Click **Remove from organization** and **Remove members**.", + "AuditProcedure": "If you have GitHub AE, verify that all user accounts are active by performing the following:\n\n1. From an administrative account on GitHub AE, in the upper-right corner of any page, click the rocket icon.\n2. If you're not already on the \"Site admin\" page, in the upper-left corner, click **Site admin**.\n3. In the left sidebar, click **Dormant users**.\n4. Verify that the list is empty.\n\nIf you have GitHub Enterprise Cloud, perform the following:\n\n1. In the top-right corner of GitHub.com, click your profile photo, then click **Your enterprises**.\n2. In the list of enterprises, click the enterprise you want to view.\n3. In the enterprise account sidebar, click **Compliance**.\n4. To download your Dormant Users (beta) report as a CSV file, under \"Other\", click **Download**.\n5. Verify that there are no users listed.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.3.2", - "Description": "Ensure team creation is limited to specific members", + "Description": "Limit ability to create teams to trusted and specific users.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Limit ability to create teams to trusted and specific users.", "RationaleStatement": "The ability to create new teams should be restricted to specific members in order to keep the organization orderly and ensure users have access to only the lowest privilege level necessary. Teams typically inherit permissions from their parent team, thus if base permissions are less restricted and any user has the ability to create a team, a permission leverage could occur in which certain data is made available to users who should not have access to it. Such a situation could potentially lead to the creation of shadow teams by an attacker. Restricting team creation will also reduce additional clutter in the organizational structure, and as a result will make it easier to track changes and anomalies.", "ImpactStatement": "Only specific users will be able to create new teams.", - "RemediationProcedure": "For every organization, limit team creation to specific, trusted users by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Access\" section of the sidebar, click **Member privileges**.\n 4. Under \"Team creation rules\", deselect **Allow members to create teams**.\n 5. Click **Save**.", - "AuditProcedure": "For every organization, ensure that team creation is limited to specific, trusted users by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Access\" section of the sidebar, click **Member privileges**.\n 4. Under \"Team creation rules\", verify that **Allow members to create teams** is not selected.", + "RemediationProcedure": "For every organization, limit team creation to specific, trusted users by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Access\" section of the sidebar, click **Member privileges**.\n4. Under \"Team creation rules\", deselect **Allow members to create teams**.\n5. Click **Save**.", + "AuditProcedure": "For every organization, ensure that team creation is limited to specific, trusted users by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Access\" section of the sidebar, click **Member privileges**.\n4. Under \"Team creation rules\", verify that **Allow members to create teams** is not selected.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.3.3", - "Description": "Ensure minimum number of administrators are set for the organization", + "Description": "Ensure the organization has a minimum number of administrators.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure the organization has a minimum number of administrators.", "RationaleStatement": "Organization administrators have the highest level of permissions, including the ability to add/remove collaborators, create or delete repositories, change branch protection policy, and convert to a publicly-accessible repository. Due to the permissive access granted to an organization administrator, it is highly recommended to keep the number of administrator accounts as minimal as possible.", "ImpactStatement": "", - "RemediationProcedure": "Set the minimum number of administrators in your organization by performing the following:\n \n\n 1. In the top right corner of GitHub, click your profile photo, then click **Your profile**.\n 2. On the left side of your profile page, under \"Organizations\", click the icon for your organization.\n 3. Under your organization name, click **People**. \n 4. In the Role drop-down, choose **Owners**.\n 5. Select the person or people you'd like to remove from owner role.\n 6. Above the list of members, use the drop-down menu and click Change role.\n 7. Select **Member**, then click **Change role**.", - "AuditProcedure": "Verify the minimum number of administrators in your organization by performing the following:\n \n\n 1. In the top right corner of GitHub, click your profile photo, then click **Your profile**.\n 2. On the left side of your profile page, under \"Organizations\", click the icon for your organization.\n 3. Under your organization name, click **People**. \n 4. In the Role drop-down, choose **Owners**.\n 5. If there are minimum number of members in the list, you are compliant.", + "RemediationProcedure": "Set the minimum number of administrators in your organization by performing the following:\n\n1. In the top right corner of GitHub, click your profile photo, then click **Your profile**.\n2. On the left side of your profile page, under \"Organizations\", click the icon for your organization.\n3. Under your organization name, click **People**. \n4. In the Role drop-down, choose **Owners**.\n5. Select the person or people you'd like to remove from owner role.\n6. Above the list of members, use the drop-down menu and click Change role.\n7. Select **Member**, then click **Change role**.", + "AuditProcedure": "Verify the minimum number of administrators in your organization by performing the following:\n\n1. In the top right corner of GitHub, click your profile photo, then click **Your profile**.\n2. On the left side of your profile page, under \"Organizations\", click the icon for your organization.\n3. Under your organization name, click **People**. \n4. In the Role drop-down, choose **Owners**.\n5. If there are minimum number of members in the list, you are compliant.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.3.4", - "Description": "Ensure Multi-Factor Authentication (MFA) is required for contributors of new code", + "Description": "Require collaborators from outside the organization to use Multi-Factor Authentication (MFA) in addition to a standard user name and password when authenticating to the source code management platform.", "Checks": [ "organization_members_mfa_required" ], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Require collaborators from outside the organization to use Multi-Factor Authentication (MFA) in addition to a standard user name and password when authenticating to the source code management platform.", "RationaleStatement": "By default every user authenticates within the system by password only. If the password of a user is compromised, however, the user account and every repository to which they have access are in danger of data loss, malicious code commits, and data theft. It is therefore recommended that each user has Multi-Factor Authentication enabled. This adds an additional layer of protection to ensure the account remains secure even if the user's password is compromised.", "ImpactStatement": "A member without enabled Multi-Factor Authentication cannot contribute to the project. They must enable Multi-Factor Authentication a before they can contribute any code.", - "RemediationProcedure": "For each repository in use, enforce Multi-Factor Authentication is the only way to authenticate for contributors, by doing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Security\" section of the sidebar, click **Authentication security**.\n 4. Under \"Authentication\", select **Require two-factor authentication for everyone in your organization**, then click **Save**.", - "AuditProcedure": "For each repository in use, verify that Multi-Factor Authentication is enforced for contributors and is the only way to authenticate, by doing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Security\" section of the sidebar, click **Authentication security**.\n 4. Under \"Authentication\", check if **Require two-factor authentication for everyone in your organization** is checked. If so, you are compliant.", + "RemediationProcedure": "For each repository in use, enforce Multi-Factor Authentication is the only way to authenticate for contributors, by doing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Security\" section of the sidebar, click **Authentication security**.\n4. Under \"Authentication\", select **Require two-factor authentication for everyone in your organization**, then click **Save**.", + "AuditProcedure": "For each repository in use, verify that Multi-Factor Authentication is enforced for contributors and is the only way to authenticate, by doing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Security\" section of the sidebar, click **Authentication security**.\n4. Under \"Authentication\", check if **Require two-factor authentication for everyone in your organization** is checked. If so, you are compliant.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.3.5", - "Description": "Ensure the organization is requiring members to use Multi-Factor Authentication (MFA)", + "Description": "Require members of the organization to use Multi-Factor Authentication (MFA) in addition to a standard user name and password when authenticating to the source code management platform.", "Checks": [ "organization_members_mfa_required" ], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Require members of the organization to use Multi-Factor Authentication (MFA) in addition to a standard user name and password when authenticating to the source code management platform.", "RationaleStatement": "By default every user authenticates within the system by password only. If the password of a user is compromised, however, the user account and every repository to which they have access are in danger of data loss, malicious code commits, and data theft. It is therefore recommended that each user has Multi-Factor Authentication enabled. This adds an additional layer of protection to ensure the account remains secure even if the user's password is compromised.", "ImpactStatement": "Members will be removed from the organization if they don't have Multi-Factor Authentication already enabled. If this is the case, it is recommended that an invitation be sent to reinstate the user's access and former privileges. They must enable Multi-Factor Authentication to accept the invitation.", - "RemediationProcedure": "For every organization that exists in your GitHub platform, enforce Multi-Factor Authentication and define it as the only way to authenticate, by doing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Security\" section of the sidebar, click **Authentication security**.\n 4. Under \"Authentication\", select **Require two-factor authentication for everyone in your organization**, then click **Save**.\n 5. If prompted, read the information about members and outside collaborators who will be removed from the organization. Type your organization's name to confirm the change, then click **Remove members & require two-factor authentication**.", - "AuditProcedure": "For every organization that exists in your GitHub platform, verify that Multi-Factor Authentication is enforced and is the only way to authenticate, by doing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Security\" section of the sidebar, click **Authentication security**.\n 4. Under \"Authentication\", check if **Require two-factor authentication for everyone in your organization** is checked. If so, you are compliant.", + "RemediationProcedure": "For every organization that exists in your GitHub platform, enforce Multi-Factor Authentication and define it as the only way to authenticate, by doing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Security\" section of the sidebar, click **Authentication security**.\n4. Under \"Authentication\", select **Require two-factor authentication for everyone in your organization**, then click **Save**.\n5. If prompted, read the information about members and outside collaborators who will be removed from the organization. Type your organization's name to confirm the change, then click **Remove members & require two-factor authentication**.", + "AuditProcedure": "For every organization that exists in your GitHub platform, verify that Multi-Factor Authentication is enforced and is the only way to authenticate, by doing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Security\" section of the sidebar, click **Authentication security**.\n4. Under \"Authentication\", check if **Require two-factor authentication for everyone in your organization** is checked. If so, you are compliant.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.3.6", - "Description": "Ensure new members are required to be invited using company-approved email", + "Description": "Existing members of an organization can invite new members to join, however new members must only be invited with their company-approved email.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Existing members of an organization can invite new members to join, however new members must only be invited with their company-approved email.", "RationaleStatement": "Ensuring new members of an organization have company-approved email prevents existing members of the organization from inviting arbitrary new users to join. Without this verification, they can invite anyone who is using the organization's version control system or has an active email account, thus allowing outside users (and potential threat actors) to easily gain access to company private code and resources. This practice will subsequently reduce the chance of human error or typos when inviting a new member.", "ImpactStatement": "Existing members would not be able to invite new users who do not have a company-approved email address.", - "RemediationProcedure": "For each organization, allow only users with company-approved email to be invited. If a user was invited without company-approved email, perform the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Click the name of your organization and under your organization name, click **People**.\n 3. On the People tab, click **Invitations**. Next to the username or email address of the person whose invitation you'd like to cancel, click **Edit invitation**.\n 4. To cancel the user's invitation to join your organization, click **Cancel invitation**.", - "AuditProcedure": "For each organization in use, verify for every invitation that the invited email address is company-approved by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Click the name of your organization and under your organization name, click **People**.\n 3. On the People tab, click **Invitations**. Verify that each invitation email is company approved by your company.", + "RemediationProcedure": "For each organization, allow only users with company-approved email to be invited. If a user was invited without company-approved email, perform the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Click the name of your organization and under your organization name, click **People**.\n3. On the People tab, click **Invitations**. Next to the username or email address of the person whose invitation you'd like to cancel, click **Edit invitation**.\n4. To cancel the user's invitation to join your organization, click **Cancel invitation**.", + "AuditProcedure": "For each organization in use, verify for every invitation that the invited email address is company-approved by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Click the name of your organization and under your organization name, click **People**.\n3. On the People tab, click **Invitations**. Verify that each invitation email is company approved by your company.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.3.7", - "Description": "Ensure two administrators are set for each repository", + "Description": "Ensure every repository has two users with administrative permissions.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Ensure every repository has two users with administrative permissions.", "RationaleStatement": "Repository administrators have the highest permissions to said repository. These include the ability to add/remove collaborators, change branch protection policy, and convert to a publicly-accessible repository. Due to the liberal access granted to a repository administrator, it is highly recommended that only two contributors occupy this role.", "ImpactStatement": "Removing administrative users from a repository would result in them losing high-level access to that repository.", - "RemediationProcedure": "For every repository in use, set two administrators by performing the following:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Access\" section of the sidebar, click **Collaborators & teams**.\n 4. Under **Manage access**, find the team or person whose you'd like to revoke admin permissions, then select the Role drop-down and click a new role.", - "AuditProcedure": "For every repository in use, verify there are two administrators by performing the following:\n \n\n 1. On GitHub, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Access\" section of the sidebar, click **Collaborators & teams**.\n 4. Under **Manage access**, verify that there are only 2 members with Admin permission.", + "RemediationProcedure": "For every repository in use, set two administrators by performing the following:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Access\" section of the sidebar, click **Collaborators & teams**.\n4. Under **Manage access**, find the team or person whose you'd like to revoke admin permissions, then select the Role drop-down and click a new role.", + "AuditProcedure": "For every repository in use, verify there are two administrators by performing the following:\n\n1. On GitHub, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Access\" section of the sidebar, click **Collaborators & teams**.\n4. Under **Manage access**, verify that there are only 2 members with Admin permission.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.3.8", - "Description": "Ensure strict base permissions are set for repositories", + "Description": "Base permissions define the permission level automatically granted to all organization members. Define strict base access permissions for all of the repositories in the organization, including new ones.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Base permissions define the permission level automatically granted to all organization members. Define strict base access permissions for all of the repositories in the organization, including new ones.", "RationaleStatement": "Defining strict base permissions is the best practice in every role-based access control (RBAC) system. If the base permission is high — for example, \"write\" permission — every member of the organization will have \"write\" permission to every repository in the organization. This will apply regardless of the specific permissions a user might need, which generally differ between organization repositories. The higher the permission, the higher the risk for incidents such as bad code commit or data breach. It is therefore recommended to set the base permissions to the strictest level possible.", "ImpactStatement": "Users might not be able to access organization repositories or perform some acts as commits. These specific permissions should be granted individually for each user or team, as needed.", - "RemediationProcedure": "Set strict base permissions for the organization repositories with the next steps:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Access\" section of the sidebar, click **Member privileges**.\n 4. Under \"Base permissions\", use the drop-down to select new base permissions - \"Read\" or \"None\".\n 5. Review the changes. To confirm, click **Change default permission to PERMISSION**.", - "AuditProcedure": "Verify that strict base permissions are set for the organization repositories by doing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Access\" section of the sidebar, click **Member privileges**.\n 4. Under \"Base permissions\", verify that it is set to \"Read\" or \"None\". If it does, you are compliant.", + "RemediationProcedure": "Set strict base permissions for the organization repositories with the next steps:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Access\" section of the sidebar, click **Member privileges**.\n4. Under \"Base permissions\", use the drop-down to select new base permissions - \"Read\" or \"None\".\n5. Review the changes. To confirm, click **Change default permission to PERMISSION**.", + "AuditProcedure": "Verify that strict base permissions are set for the organization repositories by doing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Access\" section of the sidebar, click **Member privileges**.\n4. Under \"Base permissions\", verify that it is set to \"Read\" or \"None\". If it does, you are compliant.", "AdditionalInformation": "", - "DefaultValue": "Read permission", - "References": "" + "References": "", + "DefaultValue": "Read permission" } ] }, { "Id": "1.3.9", - "Description": "Ensure an organization’s identity is confirmed with a “Verified” badge", + "Description": "Confirm the domains an organization owns with a \"Verified\" badge.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Confirm the domains an organization owns with a \"Verified\" badge.", "RationaleStatement": "Verifying the organization's domain gives developers assurance that a given domain is truly the official home for a public organization. Attackers can pretend to be an organization and steal information via a faked/spoof domain, therefore the use of a \"Verified\" badge instills more confidence and trust between developers and the open-source community.", "ImpactStatement": "", - "RemediationProcedure": "Only if you have an enterprise account, verify the organization's domains and secure a \"Verified\" badge next to its name by performing the following:\n \n\n 1. In the top-right corner of GitHub.com, click your profile photo, then click **Your enterprises**.\n 2. In the list of enterprises, click the enterprise you want to view. Then in the enterprise account sidebar, click **Settings**.\n 3. Under \"Settings\", click **Verified & approved domains**.\n 4. Click **Add a domain**.\n 5. In the domain field, type the domain you'd like to verify, then click **Add domain**.\n 6. Follow the instructions under **Add a DNS TXT record** to create a DNS TXT record with your domain hosting service. Wait for your DNS configuration to change, which may take up to 72 hours. You can confirm your DNS configuration has changed by running the `dig` command on the command line, replacing ENTERPRISE-ACCOUNT with the name of your enterprise account, and example.com with the domain you'd like to verify. You should see your new TXT record listed in the command output.\n ```\n dig _github-challenge-ENTERPRISE-ACCOUNT.DOMAIN-NAME +nostats +nocomments +nocmd TXT\n ```\n 7. After confirming your TXT record is added to your DNS, follow steps one through three above to navigate to your enterprise account's approved and verified domains.\n 8. To the right of the domain that's pending verification, click the 3-dots, then click **Continue verifying**. Click **Verify**.\n 9. Optionally, after the \"Verified\" badge is visible on your organizations' profiles, delete the TXT entry from the DNS record at your domain hosting service.", + "RemediationProcedure": "Only if you have an enterprise account, verify the organization's domains and secure a \"Verified\" badge next to its name by performing the following:\n\n1. In the top-right corner of GitHub.com, click your profile photo, then click **Your enterprises**.\n2. In the list of enterprises, click the enterprise you want to view. Then in the enterprise account sidebar, click **Settings**.\n3. Under \"Settings\", click **Verified & approved domains**.\n4. Click **Add a domain**.\n5. In the domain field, type the domain you'd like to verify, then click **Add domain**.\n6. Follow the instructions under **Add a DNS TXT record** to create a DNS TXT record with your domain hosting service. Wait for your DNS configuration to change, which may take up to 72 hours. You can confirm your DNS configuration has changed by running the `dig` command on the command line, replacing ENTERPRISE-ACCOUNT with the name of your enterprise account, and example.com with the domain you'd like to verify. You should see your new TXT record listed in the command output.\n```\ndig _github-challenge-ENTERPRISE-ACCOUNT.DOMAIN-NAME +nostats +nocomments +nocmd TXT\n```\n7. After confirming your TXT record is added to your DNS, follow steps one through three above to navigate to your enterprise account's approved and verified domains.\n8. To the right of the domain that's pending verification, click the 3-dots, then click **Continue verifying**. Click **Verify**.\n9. Optionally, after the \"Verified\" badge is visible on your organizations' profiles, delete the TXT entry from the DNS record at your domain hosting service.", "AuditProcedure": "Only if you have an enterprise account, view the enterprise organization profile page and ensure it has a \"Verified\" badge in it.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://docs.github.com/en/organizations/managing-organization-settings/verifying-or-approving-a-domain-for-your-organization" + "References": "https://docs.github.com/en/organizations/managing-organization-settings/verifying-or-approving-a-domain-for-your-organization", + "DefaultValue": "" } ] }, { "Id": "1.3.10", - "Description": "Ensure Source Code Management (SCM) email notifications are restricted to verified domains", + "Description": "Restrict the Source Code Management (SCM) organization's email notifications to approved domains only.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Restrict the Source Code Management (SCM) organization's email notifications to approved domains only.", "RationaleStatement": "Restricting Source Code Management email notifications to verified domains only prevents data leaks, as personal emails and custom domains are more prone to account takeover via DNS hijacking or password breach.", "ImpactStatement": "Only members with approved email would be able to receive Source Code Management notifications.", - "RemediationProcedure": "Only if you have an enterprise account, restrict Source Code Management email notifications to approved domains only by performing the following:\n \n\n 1. In the top-right corner of GitHub.com, click your profile photo, then click **Your enterprises**.\n 2. In the list of enterprises, click the enterprise you want to view. Then in the enterprise account sidebar, click **Settings**.\n 3. Under \"Settings\", click **Verified & approved domains**.\n 4. Under \"Notification preferences\", select **Restrict email notifications to only approved or verified domains**.\n 5. Click **Save**.", - "AuditProcedure": "Only if you have an enterprise account, ensure Source Code Management email notifications are restricted to approved domains only by performing the following:\n \n\n 1. In the top-right corner of GitHub.com, click your profile photo, then click **Your enterprises**.\n 2. In the list of enterprises, click the enterprise you want to view. Then in the enterprise account sidebar, click **Settings**.\n 3. Under \"Settings\", click **Verified & approved domains**.\n 4. Under \"Notification preferences\", verify that **Restrict email notifications to only approved or verified domains** is selected.", + "RemediationProcedure": "Only if you have an enterprise account, restrict Source Code Management email notifications to approved domains only by performing the following:\n\n1. In the top-right corner of GitHub.com, click your profile photo, then click **Your enterprises**.\n2. In the list of enterprises, click the enterprise you want to view. Then in the enterprise account sidebar, click **Settings**.\n3. Under \"Settings\", click **Verified & approved domains**.\n4. Under \"Notification preferences\", select **Restrict email notifications to only approved or verified domains**.\n5. Click **Save**.", + "AuditProcedure": "Only if you have an enterprise account, ensure Source Code Management email notifications are restricted to approved domains only by performing the following:\n\n1. In the top-right corner of GitHub.com, click your profile photo, then click **Your enterprises**.\n2. In the list of enterprises, click the enterprise you want to view. Then in the enterprise account sidebar, click **Settings**.\n3. Under \"Settings\", click **Verified & approved domains**.\n4. Under \"Notification preferences\", verify that **Restrict email notifications to only approved or verified domains** is selected.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.3.11", - "Description": "Ensure an organization provides SSH certificates", + "Description": "As an organization, become an SSH Certificate Authority and provide SSH keys for accessing repositories.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "As an organization, become an SSH Certificate Authority and provide SSH keys for accessing repositories.", - "RationaleStatement": "There are two ways for remotely working with Source Code Management: via HTTPS, which requires authentication by user/password, or via SSH, which requires the use of SSH keys. SSH authentication is better in terms of security; key creation and distribution, however, must be done in a secure manner. This can be accomplished by implementing SSH certificates, which are used to validate the server's identity. A developer will not be able to connect to a Git server if its key cannot be verified by the SSH Certificate Authority (CA) server.\n As an organization, one can verify the SSH certificate signature used to authenticate if a CA is defined and used. This ensures that only verified developers can access organization repositories, as their SSH key will be the only one signed by the CA certificate. This reduces the risk of misuse and malicious code commits.", + "RationaleStatement": "There are two ways for remotely working with Source Code Management: via HTTPS, which requires authentication by user/password, or via SSH, which requires the use of SSH keys. SSH authentication is better in terms of security; key creation and distribution, however, must be done in a secure manner. This can be accomplished by implementing SSH certificates, which are used to validate the server's identity. A developer will not be able to connect to a Git server if its key cannot be verified by the SSH Certificate Authority (CA) server.\nAs an organization, one can verify the SSH certificate signature used to authenticate if a CA is defined and used. This ensures that only verified developers can access organization repositories, as their SSH key will be the only one signed by the CA certificate. This reduces the risk of misuse and malicious code commits.", "ImpactStatement": "Members with unverified keys will not be able to clone organization repositories. Signing, certification, and verification might also slow down the development process.", - "RemediationProcedure": "Only if you have an enterprise account, deploy an SSH Certificate Authority server and configure it to provide an SSH certificate with which to sign keys by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Security\" section of the sidebar, click **Authentication security**.\n 4. To the right of \"SSH Certificate Authorities\", click **New CA**.\n 5. Under \"Key,\" paste your public SSH key.\n 6. Click **Add CA**.\n 7. Click **Save**.", - "AuditProcedure": "Only if you have an enterprise account, verify that the enterprise organization has an SSH Certificate Authority server and provides an SSH certificate with which to sign keys by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Security\" section of the sidebar, click **Authentication security**.\n 4. Verify that there's an SSH certificate authority listed there.", + "RemediationProcedure": "Only if you have an enterprise account, deploy an SSH Certificate Authority server and configure it to provide an SSH certificate with which to sign keys by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Security\" section of the sidebar, click **Authentication security**.\n4. To the right of \"SSH Certificate Authorities\", click **New CA**.\n5. Under \"Key,\" paste your public SSH key.\n6. Click **Add CA**.\n7. Click **Save**.", + "AuditProcedure": "Only if you have an enterprise account, verify that the enterprise organization has an SSH Certificate Authority server and provides an SSH certificate with which to sign keys by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Security\" section of the sidebar, click **Authentication security**.\n4. Verify that there's an SSH certificate authority listed there.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.3.12", - "Description": "Ensure Git access is limited based on IP addresses", + "Description": "Limit Git access based on IP addresses by having a allowlist of IP addresses from which connection is possible.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Limit Git access based on IP addresses by having a allowlist of IP addresses from which connection is possible.", "RationaleStatement": "Allowing access to Git repositories (source code) only from specific IP addresses adds yet another layer of restriction and reduces the risk of unauthorized connection to the organization's assets. This will prevent attackers from accessing Source Code Management (SCM), as they would first need to know the allowed IP addresses to gain access to them.", "ImpactStatement": "Only members with allowlisted IP addresses will be able to access the organization's Git repositories.", - "RemediationProcedure": "Only if you have an enterprise account, create an IP allowlist and forbid all other IPs from accessing the source code by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Security\" section of the sidebar, click **Authentication security**.\n 4. At the bottom of the \"IP allow list\" section, enter an IP address, or a range of addresses in CIDR notation. Optionally, enter a description of the allowed IP address or range.\n 5. Click **Add**.\n 6. After that, under \"IP allow list\", select **Enable IP allow list**.\n 7. Click **Save**.", - "AuditProcedure": "Only if you have an enterprise account, in every organization of yours, ensure access is allowed only by IP allowlist, and that access is forbidden for all other IPs by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Security\" section of the sidebar, click **Authentication security**.\n 4. Verify that there's an IP address, or a range of addresses in CIDR notation listed. Also verify that **Enable IP allow list** is selected.", + "RemediationProcedure": "Only if you have an enterprise account, create an IP allowlist and forbid all other IPs from accessing the source code by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Security\" section of the sidebar, click **Authentication security**.\n4. At the bottom of the \"IP allow list\" section, enter an IP address, or a range of addresses in CIDR notation. Optionally, enter a description of the allowed IP address or range.\n5. Click **Add**.\n6. After that, under \"IP allow list\", select **Enable IP allow list**.\n7. Click **Save**.", + "AuditProcedure": "Only if you have an enterprise account, in every organization of yours, ensure access is allowed only by IP allowlist, and that access is forbidden for all other IPs by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Security\" section of the sidebar, click **Authentication security**.\n4. Verify that there's an IP address, or a range of addresses in CIDR notation listed. Also verify that **Enable IP allow list** is selected.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-allowed-ip-addresses-for-your-organization" + "References": "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-allowed-ip-addresses-for-your-organization", + "DefaultValue": "" } ] }, { "Id": "1.3.13", - "Description": "Ensure anomalous code behavior is tracked", + "Description": "Track code anomalies.", "Checks": [], "Attributes": [ { - "Section": "1.3", + "Section": "1 Source Code", + "Subsection": "1.3 Contribution Access", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Track code anomalies.", - "RationaleStatement": "Carefully analyze any code anomalies within the organization. For example, a code anomaly could be a push made outside of working hours. Such a code push has a higher likelihood of being the result of an attack, as most if not all members of the organization would likely be outside the office. Another example is an activity that exceeds the average activity of a particular user.\n Tracking and auditing such behaviors creates additional layers of security and can aid in early detection of potential attacks.", + "RationaleStatement": "Carefully analyze any code anomalies within the organization. For example, a code anomaly could be a push made outside of working hours. Such a code push has a higher likelihood of being the result of an attack, as most if not all members of the organization would likely be outside the office. Another example is an activity that exceeds the average activity of a particular user.\nTracking and auditing such behaviors creates additional layers of security and can aid in early detection of potential attacks.", "ImpactStatement": "", "RemediationProcedure": "For every repository in use, track and investigate anomalous code behavior and activity.", "AuditProcedure": "For every repository in use, ensure code anomalies relevant to the organization are promptly investigated.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.4.1", - "Description": "Ensure administrator approval is required for every installed application", + "Description": "Ensure an administrator approval is required when installing applications.", "Checks": [], "Attributes": [ { - "Section": "1.4", + "Section": "1 Source Code", + "Subsection": "1.4 Third-Party", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure an administrator approval is required when installing applications.", "RationaleStatement": "Applications are typically automated integrations that improve the workflow of an organization. They are written by third-party developers, and therefore should be validated before using in case they're malicious or not trustable. Because administrators are expected to be the most qualified and trusted members of the organization, they should review the applications being installed and decide whether they are both trusted and necessary.", "ImpactStatement": "Applications will not be installed without administrator approval.", - "RemediationProcedure": "Require an administrator approval for every installed application:\n \n\n a. For GitHub Apps, you are compliant by default. That is because by default only organization owners and administrators can install them.\n \n\n b. For OAuth Apps, perform the following:\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Third-party Access\" section of the sidebar, click **OAuth application policy**.\n 4. Under \"Third-party application access policy\", click **Setup application access restrictions**.\n 5. After you review the information about third-party access restrictions, click **Restrict third-party application access**.", - "AuditProcedure": "Verify that applications are installed only after receiving administrator approval:\n \n\n a. For GitHub Apps, you are compliant by default. That is because by default only organization owners and administrators can install them.\n \n\n b. For OAuth Apps, perform the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Third-party Access\" section of the sidebar, click **OAuth application policy**.\n 4. Under \"Third-party application access policy\" verify that the policy status is **Access restricted**.", + "RemediationProcedure": "Require an administrator approval for every installed application:\n\na. For GitHub Apps, you are compliant by default. That is because by default only organization owners and administrators can install them.\n\nb. For OAuth Apps, perform the following:\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Third-party Access\" section of the sidebar, click **OAuth application policy**.\n4. Under \"Third-party application access policy\", click **Setup application access restrictions**.\n5. After you review the information about third-party access restrictions, click **Restrict third-party application access**.", + "AuditProcedure": "Verify that applications are installed only after receiving administrator approval:\n\na. For GitHub Apps, you are compliant by default. That is because by default only organization owners and administrators can install them.\n\nb. For OAuth Apps, perform the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Third-party Access\" section of the sidebar, click **OAuth application policy**.\n4. Under \"Third-party application access policy\" verify that the policy status is **Access restricted**.", "AdditionalInformation": "", - "DefaultValue": "Maintainers are organization owners.", - "References": "" + "References": "", + "DefaultValue": "Maintainers are organization owners." } ] }, { "Id": "1.4.2", - "Description": "Ensure stale applications are reviewed and inactive ones are removed", + "Description": "Ensure stale (inactive) applications are reviewed and removed if no longer in use.", "Checks": [], "Attributes": [ { - "Section": "1.4", + "Section": "1 Source Code", + "Subsection": "1.4 Third-Party", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure stale (inactive) applications are reviewed and removed if no longer in use.", @@ -870,80 +912,84 @@ "RemediationProcedure": "Review all stale applications and periodically remove them.", "AuditProcedure": "Verify that all the applications in the organization are actively used, and remove those that are no longer in use.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.4.3", - "Description": "Ensure the access granted to each installed application is limited to the least privilege needed", + "Description": "Ensure installed application permissions are limited to the lowest privilege level required.", "Checks": [], "Attributes": [ { - "Section": "1.4", + "Section": "1 Source Code", + "Subsection": "1.4 Third-Party", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure installed application permissions are limited to the lowest privilege level required.", "RationaleStatement": "Applications are typically automated integrations that can improve the workflow of an organization. They are written by third-party developers, and therefore should be reviewed carefully before use. It is recommended to use the \"least privilege\" principle, granting applications the lowest level of permissions required. This may prevent harm from a potentially malicious application with unnecessarily high-level permissions leaking data or modifying source code.", "ImpactStatement": "", - "RemediationProcedure": "Grant permissions to applications by the \"least privilege\" principle, meaning the lowest possible permission necessary:\n \n\n a. For GitHub Apps, perform the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Integrations\" section of the sidebar, click **GitHub Apps**.\n 4. Next to every GitHub App, click **Configure**.\n 5. Review the GitHub App's permissions and repository access. Edit the permissions granted to the least possible. For example, restrict the number of repositories the App can access.\n 6. Click **Save**.", - "AuditProcedure": "Verify that each installed application has the least privilege needed:\n \n\n a. For GitHub Apps, perform the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the \"Integrations\" section of the sidebar, click **GitHub Apps**.\n 4. Next to every GitHub App, click **Configure**.\n 5. Review the GitHub App's permissions and repository access. Verify that the App permissions are the least possible and that it can access only necessary repositories.", + "RemediationProcedure": "Grant permissions to applications by the \"least privilege\" principle, meaning the lowest possible permission necessary:\n\na. For GitHub Apps, perform the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Integrations\" section of the sidebar, click **GitHub Apps**.\n4. Next to every GitHub App, click **Configure**.\n5. Review the GitHub App's permissions and repository access. Edit the permissions granted to the least possible. For example, restrict the number of repositories the App can access.\n6. Click **Save**.", + "AuditProcedure": "Verify that each installed application has the least privilege needed:\n\na. For GitHub Apps, perform the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the \"Integrations\" section of the sidebar, click **GitHub Apps**.\n4. Next to every GitHub App, click **Configure**.\n5. Review the GitHub App's permissions and repository access. Verify that the App permissions are the least possible and that it can access only necessary repositories.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.4.4", - "Description": "Ensure only secured webhooks are used", + "Description": "Use only secured webhooks in the source code management platform.", "Checks": [], "Attributes": [ { - "Section": "1.4", + "Section": "1 Source Code", + "Subsection": "1.4 Third-Party", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Use only secured webhooks in the source code management platform.", "RationaleStatement": "A webhook is an event listener, attached to critical and sensitive parts of the software delivery process. It is triggered by a list of events (such as a new code being committed), and when triggered, the webhook sends out a notification with some payload to specific internet endpoints. Since the payload of the webhook contains sensitive organization data, it's important all webhooks are directed to an endpoint (URL) protected by SSL verification (HTTPS). This helps ensure that the data sent is delivered to securely without any man-in-the-middle, who could easily access and even alter the payload of the request.", - "ImpactStatement": "Perform the following to ensure all webhooks used are secured (HTTPS):\n \n\n 1. Navigate to your organization or repository and select **Settings**.\n 2. Select **Webhooks** on the side menu.\n 3. Verify that each webhook URL starts with 'https'.", - "RemediationProcedure": "Perform the following to secure all webhooks used(over HTTPS):\n \n\n 1. Navigate to your organization or repository and select **Settings**.\n 2. Select **Webhooks** on the side menu.\n 3. Find the webhooks that starts with 'http' and not 'https'.\n 4. Ensure the endpoint (URL) of the webhook listens to secured port (443) and uses certificate.\n 5. Click **Edit**.\n 6. Change the payload URL to https and ensure **Enable SSL verification** is checked.\n 7. Click **Update webhook**.", - "AuditProcedure": "Perform the following to secure all webhooks used(over HTTPS):\n \n\n 1. Navigate to your organization or repository and select **Settings**.\n 2. Select **Webhooks** on the side menu.\n 3. Ensure all webhooks starts with 'https'.", + "ImpactStatement": "Perform the following to ensure all webhooks used are secured (HTTPS):\n\n1. Navigate to your organization or repository and select **Settings**.\n2. Select **Webhooks** on the side menu.\n3. Verify that each webhook URL starts with 'https'.", + "RemediationProcedure": "Perform the following to secure all webhooks used(over HTTPS):\n\n1. Navigate to your organization or repository and select **Settings**.\n2. Select **Webhooks** on the side menu.\n3. Find the webhooks that starts with 'http' and not 'https'.\n4. Ensure the endpoint (URL) of the webhook listens to secured port (443) and uses certificate.\n5. Click **Edit**.\n6. Change the payload URL to https and ensure **Enable SSL verification** is checked.\n7. Click **Update webhook**.", + "AuditProcedure": "Perform the following to secure all webhooks used(over HTTPS):\n\n1. Navigate to your organization or repository and select **Settings**.\n2. Select **Webhooks** on the side menu.\n3. Ensure all webhooks starts with 'https'.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.5.1", - "Description": "Ensure scanners are in place to identify and prevent sensitive data in code", + "Description": "Detect and prevent sensitive data in code, such as confidential ID numbers, passwords, etc.", "Checks": [ "repository_secret_scanning_enabled" ], "Attributes": [ { - "Section": "1.5", + "Section": "1 Source Code", + "Subsection": "1.5 Code Risks", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Detect and prevent sensitive data in code, such as confidential ID numbers, passwords, etc.", "RationaleStatement": "Having sensitive data in the source code makes it easier for attackers to maliciously use such information. In order to avoid this, designate scanners to identify and prevent the existence of sensitive data in the code.", "ImpactStatement": "", - "RemediationProcedure": "For every repository in use, designate scanners to identify and prevent sensitive data in code by performing the following (enterprise only):\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Security\" section of the sidebar, click **Code security and analysis**.\n 4. Scroll down to the bottom of the page and click **Enable** for secret scanning.", - "AuditProcedure": "For every repository in use, verify that scanners are set to identify and prevent the existence of sensitive data in code by performing the following (enterprise only):\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the \"Security\" section of the sidebar, click **Code security and analysis**.\n 4. Scroll down to the bottom of the page. If you see a **Disable** button, it means that secret scanning is already enabled for the repository.", + "RemediationProcedure": "For every repository in use, designate scanners to identify and prevent sensitive data in code by performing the following (enterprise only):\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Security\" section of the sidebar, click **Code security and analysis**.\n4. Scroll down to the bottom of the page and click **Enable** for secret scanning.", + "AuditProcedure": "For every repository in use, verify that scanners are set to identify and prevent the existence of sensitive data in code by performing the following (enterprise only):\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the \"Security\" section of the sidebar, click **Code security and analysis**.\n4. Scroll down to the bottom of the page. If you see a **Disable** button, it means that secret scanning is already enabled for the repository.", "AdditionalInformation": "By January 2023, this feature is supposed to be open to all plans. Until then it is only for enterprise users.", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.5.2", - "Description": "Ensure scanners are in place to secure Continuous Integration (CI) pipeline instructions", + "Description": "Detect and prevent misconfigurations and insecure instructions in CI pipelines", "Checks": [], "Attributes": [ { - "Section": "1.5", + "Section": "1 Source Code", + "Subsection": "1.5 Code Risks", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Detect and prevent misconfigurations and insecure instructions in CI pipelines", @@ -952,18 +998,19 @@ "RemediationProcedure": "Set a CI instructions scanning tool to identify and prevent misconfigurations and insecure instructions and scans all CI pipelines.", "AuditProcedure": "Verify that a CI instructions scanning tool is set to identify and prevent misconfigurations and insecure instructions and that it scans all CI pipelines.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.5.3", - "Description": "Ensure scanners are in place to secure Infrastructure as Code (IaC) instructions", + "Description": "Detect and prevent misconfigurations or insecure instructions in Infrastructure as Code (IaC) files, such as Terraform files.", "Checks": [], "Attributes": [ { - "Section": "1.5", + "Section": "1 Source Code", + "Subsection": "1.5 Code Risks", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Detect and prevent misconfigurations or insecure instructions in Infrastructure as Code (IaC) files, such as Terraform files.", @@ -972,60 +1019,63 @@ "RemediationProcedure": "For every repository that holds IaC instructions files, set a scanning tool to identify and prevent misconfigurations and insecure instructions.", "AuditProcedure": "For every repository that holds IaC instructions files, verify that a scanning tool is set to identify and prevent misconfigurations and insecure instructions.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.5.4", - "Description": "Ensure scanners are in place for code vulnerabilities", + "Description": "Detect and prevent known open source vulnerabilities in the code.", "Checks": [], "Attributes": [ { - "Section": "1.5", + "Section": "1 Source Code", + "Subsection": "1.5 Code Risks", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Detect and prevent known open source vulnerabilities in the code.", "RationaleStatement": "Open source code blocks are used a lot in developed software. This has its own advantages, but it also has risks. Because the code is open for everyone, it means that attackers can publish or add malicious code to these open-source code blocks, or use their knowledge to find vulnerabilities in an existing code. Detecting and fixing such code vulnerabilities, by SCA (Software Composition Analysis) prevents insecure flaws from reaching production. It gives another opportunity for developers to secure the source code before it is deployed in production, where it is far more exposed and vulnerable to attacks.", "ImpactStatement": "", - "RemediationProcedure": "For every repository that is in use, set a scanning tool to identify and prevent code vulnerabilities by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under the repository name, click **Security**.\n 3. To the right of \"Code scanning alerts\", click **Set up code scanning**.\n 4. Under \"Get started with code scanning\", click Set up this workflow on a workflow of your choice.\n 5. To customize how code scanning scans your code, edit the workflow.\n 6. Use the **Start commit** drop-down and type a commit message.\n 7. Choose whether you'd like to commit directly to the default branch or create a new branch and start a pull request.\n 8. Click **Commit new file** or **Propose new file**.", - "AuditProcedure": "For every repository that is in use, verify that a scanning tool is set to identify and prevent code vulnerabilities by performing the following:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under the repository name, click **Security**.\n 3. Verify that \"Code scanning alerts\" is **Enabled** or that there is a workflow that scans your code.", + "RemediationProcedure": "For every repository that is in use, set a scanning tool to identify and prevent code vulnerabilities by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under the repository name, click **Security**.\n3. To the right of \"Code scanning alerts\", click **Set up code scanning**.\n4. Under \"Get started with code scanning\", click Set up this workflow on a workflow of your choice.\n5. To customize how code scanning scans your code, edit the workflow.\n6. Use the **Start commit** drop-down and type a commit message.\n7. Choose whether you'd like to commit directly to the default branch or create a new branch and start a pull request.\n8. Click **Commit new file** or **Propose new file**.", + "AuditProcedure": "For every repository that is in use, verify that a scanning tool is set to identify and prevent code vulnerabilities by performing the following:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under the repository name, click **Security**.\n3. Verify that \"Code scanning alerts\" is **Enabled** or that there is a workflow that scans your code.", "AdditionalInformation": "All public repositories in all plans can use code scanning in GitHub. In private repositories, only GitHub Enterprise can use code scanning.", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.5.5", - "Description": "Ensure scanners are in place for open-source vulnerabilities in used packages", + "Description": "Detect, prevent and monitor known open-source vulnerabilities in packages that are being used.", "Checks": [ "repository_dependency_scanning_enabled" ], "Attributes": [ { - "Section": "1.5", + "Section": "1 Source Code", + "Subsection": "1.5 Code Risks", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Detect, prevent and monitor known open-source vulnerabilities in packages that are being used.", "RationaleStatement": "Open-source vulnerabilities might exist before one starts to use a package, but they are also discovered over time. New attacks and vulnerabilities are announced every now and then. It is important to keep track of these and to monitor whether the dependencies used are affected by the recent vulnerability. Detecting and fixing those packages' vulnerabilities decreases the attack surface within deployed and running applications that use such packages. It prevents security flaws from reaching the production environment which could eventually lead to a security breach.", "ImpactStatement": "", - "RemediationProcedure": "Set scanners that will monitor, identify, and prevent open-source vulnerabilities in packages by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**. In the \"Security\" section of the sidebar, click **Code security and analysis**.\n 3. Under \"Code security and analysis\", to the right of Dependabot alerts, click **Disable all** or **Enable all**.\n 4. Check the **Enable by default for new repositories** option and then click **Enable Dependabot alerts**.\n \n\n It is also recommended to use another additional solution for package scanning because GitHub doesn't always recognize the vulnerabilities.", - "AuditProcedure": "Verify that scanners are set to monitor, identify, and prevent open-source vulnerabilities in packages by performing the following:\n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**. In the \"Security\" section of the sidebar, click **Code security and analysis**.\n 3. Verify that the Dependabot alerts is enabled and that **Enable by default for new repositories** is checked.\n \n\n It is also recommended to verify that you're using another additional solution for package scanning because GitHub doesn't always recognize the vulnerabilities.", + "RemediationProcedure": "Set scanners that will monitor, identify, and prevent open-source vulnerabilities in packages by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**. In the \"Security\" section of the sidebar, click **Code security and analysis**.\n3. Under \"Code security and analysis\", to the right of Dependabot alerts, click **Disable all** or **Enable all**.\n4. Check the **Enable by default for new repositories** option and then click **Enable Dependabot alerts**.\n\nIt is also recommended to use another additional solution for package scanning because GitHub doesn't always recognize the vulnerabilities.", + "AuditProcedure": "Verify that scanners are set to monitor, identify, and prevent open-source vulnerabilities in packages by performing the following:\n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**. In the \"Security\" section of the sidebar, click **Code security and analysis**.\n3. Verify that the Dependabot alerts is enabled and that **Enable by default for new repositories** is checked.\n\nIt is also recommended to verify that you're using another additional solution for package scanning because GitHub doesn't always recognize the vulnerabilities.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "1.5.6", - "Description": "Ensure scanners are in place for open-source license issues in used packages", + "Description": "Detect open-source license problems in used dependencies and fix them.", "Checks": [], "Attributes": [ { - "Section": "1.5", + "Section": "1 Source Code", + "Subsection": "1.5 Code Risks", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Detect open-source license problems in used dependencies and fix them.", @@ -1034,18 +1084,19 @@ "RemediationProcedure": "Designate a license scanning tool to identify open-source license problems and fix them and scan every package you use.", "AuditProcedure": "Ensure a license scanning tool is set up to identify open-source license problems and that every package you use is scanned by it.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.1.1", - "Description": "Ensure each pipeline has a single responsibility", + "Description": "Ensure each pipeline has a single responsibility in the build process.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Ensure each pipeline has a single responsibility in the build process.", @@ -1054,18 +1105,19 @@ "RemediationProcedure": "Divide each multi-responsibility pipeline into multiple pipelines, each having a single responsibility with the least privilege. Additionally, create all new pipelines with a sole purpose going forward.", "AuditProcedure": "For each pipeline, ensure it has only one responsibility in the build process.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs" + "References": "https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs", + "DefaultValue": "" } ] }, { "Id": "2.1.2", - "Description": "Ensure all aspects of the pipeline infrastructure and configuration are immutable", + "Description": "Ensure the pipeline orchestrator and its configuration are immutable.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure the pipeline orchestrator and its configuration are immutable.", @@ -1074,18 +1126,19 @@ "RemediationProcedure": "Use an immutable pipeline orchestrator and ensure that its configuration and all other aspects of the built environment are immutable, as well.", "AuditProcedure": "Verify that the pipeline orchestrator, its configuration, and all other aspects of the build environment are immutable.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.1.3", - "Description": "Ensure the build environment is logged", + "Description": "Keep build logs of the build environment detailing configuration and all activity within it. Also, consider to store them in a centralized organizational log store.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Keep build logs of the build environment detailing configuration and all activity within it. Also, consider to store them in a centralized organizational log store.", @@ -1094,18 +1147,19 @@ "RemediationProcedure": "Keep logs of the build environment. For example, use the .buildinfo file for Debian build workers. Also, store the logs in a centralized organizational log store.", "AuditProcedure": "Verify that the build environment is logged and stored in a centralized organizational log store.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.1.4", - "Description": "Ensure the creation of the build environment is automated", + "Description": "Automate the creation of the build environment.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Automate the creation of the build environment.", @@ -1114,18 +1168,19 @@ "RemediationProcedure": "Automate the deployment of the build environment.", "AuditProcedure": "Verify that the deployment of the build environment is automated and can be easily redeployed.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.1.5", - "Description": "Ensure access to build environments is limited", + "Description": "Restrict access to the build environment (orchestrator, pipeline executor, their environment, etc.) to trusted and qualified users only.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Restrict access to the build environment (orchestrator, pipeline executor, their environment, etc.) to trusted and qualified users only.", @@ -1134,18 +1189,19 @@ "RemediationProcedure": "Restrict access to the build environment to trusted and qualified users.", "AuditProcedure": "Verify each build environment is accessible only to known and authorized users.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.1.6", - "Description": "Ensure users must authenticate to access the build environment", + "Description": "Require users to login in to access the build environment - where the orchestrator, the pipeline executer, where the build workers are running, etc.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Require users to login in to access the build environment - where the orchestrator, the pipeline executer, where the build workers are running, etc.", @@ -1154,58 +1210,61 @@ "RemediationProcedure": "Require authentication to access the build environment and disable anonymous access.", "AuditProcedure": "Ensure authentication is required to access the build environment.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.1.7", - "Description": "Ensure build secrets are limited to the minimal necessary scope", + "Description": "Build tools providers offer a secure way to store secrets that should be used during the build process.\nThese secrets will often be credentials used to access other tools, for example for pulling code or for uploading artifacts.\nAccess to these secrets can be defined on various scopes, for example in github it could be on an organization level or a repository level and there is also control on whether these secrets are passed to forked pull request.\nTo protect these critical assets it is important to choose the most restrictive scope necessary.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 2", "AssessmentStatus": "Manual", - "Description": "Build tools providers offer a secure way to store secrets that should be used during the build process.\n These secrets will often be credentials used to access other tools, for example for pulling code or for uploading artifacts.\n Access to these secrets can be defined on various scopes, for example in github it could be on an organization level or a repository level and there is also control on whether these secrets are passed to forked pull request.\n To protect these critical assets it is important to choose the most restrictive scope necessary.", - "RationaleStatement": "Allowing over permissive access to these secrets may affect on their exposure.\n For example if a secret is defined in an organization level, and users can create new repositories, there is a scenario where a user can create a new repo and run a controlled build just to exfiltrate these secrets.", + "Description": "Build tools providers offer a secure way to store secrets that should be used during the build process.\nThese secrets will often be credentials used to access other tools, for example for pulling code or for uploading artifacts.\nAccess to these secrets can be defined on various scopes, for example in github it could be on an organization level or a repository level and there is also control on whether these secrets are passed to forked pull request.\nTo protect these critical assets it is important to choose the most restrictive scope necessary.", + "RationaleStatement": "Allowing over permissive access to these secrets may affect on their exposure.\nFor example if a secret is defined in an organization level, and users can create new repositories, there is a scenario where a user can create a new repo and run a controlled build just to exfiltrate these secrets.", "ImpactStatement": "Increased risk of exposure of build related secrets.", "RemediationProcedure": "For each build tool, review the secrets defined and their permissions scope and change over permissive scopes to more restrictive ones based on the required access.", "AuditProcedure": "For each build tool in use, review the secrets defined and the permission scopes they are assigned.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.1.8", - "Description": "Ensure the build infrastructure is automatically scanned for vulnerabilities", + "Description": "Scan the build infrastructure and its dependencies for vulnerabilities. It is recommended that this be done automatically.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Scan the build infrastructure and its dependencies for vulnerabilities. It is recommended that this be done automatically.", - "RationaleStatement": "Automatic scanning for vulnerabilities detects known vulnerabilities in the tooling used by the build infrastructure and its dependencies. These vulnerabilities can lead\n to a potentially massive breach if not handled as fast as possible, as attackers might also be\n aware of such vulnerabilities.", + "RationaleStatement": "Automatic scanning for vulnerabilities detects known vulnerabilities in the tooling used by the build infrastructure and its dependencies. These vulnerabilities can lead\nto a potentially massive breach if not handled as fast as possible, as attackers might also be\naware of such vulnerabilities.", "ImpactStatement": "", "RemediationProcedure": "Set an automated vulnerability scanning for your build infrastructure.", "AuditProcedure": "Verify that your build infrastructure is automatically scanned for vulnerabilities.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.1.9", - "Description": "Ensure default passwords are not used", + "Description": "Do not use default passwords of build tools and components.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Do not use default passwords of build tools and components.", @@ -1214,18 +1273,19 @@ "RemediationProcedure": "For each build tool, change the default password.", "AuditProcedure": "For each build tool, ensure the password used is not the default one.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.1.10", - "Description": "Ensure webhooks of the build environment are secured", + "Description": "Use secured webhooks of the build environment.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Use secured webhooks of the build environment.", @@ -1234,18 +1294,19 @@ "RemediationProcedure": "For each webhook in use, change it to secured (over HTTPS).", "AuditProcedure": "For each webhook in use, ensure it is secured (HTTPS).", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.1.11", - "Description": "Ensure minimum number of administrators are set for the build environment", + "Description": "Ensure the build environment has a minimum number of administrators.", "Checks": [], "Attributes": [ { - "Section": "2.1", + "Section": "2 Build Pipelines", + "Subsection": "2.1 Build Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure the build environment has a minimum number of administrators.", @@ -1254,18 +1315,19 @@ "RemediationProcedure": "Set the minimum number of administrators in the build environment.", "AuditProcedure": "Verify that the build environment has only the minimum number of administrators.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.2.1", - "Description": "Ensure build workers are single-used", + "Description": "Use a clean instance of build worker for every pipeline run.", "Checks": [], "Attributes": [ { - "Section": "2.2", + "Section": "2 Build Pipelines", + "Subsection": "2.2 Build Worker", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Use a clean instance of build worker for every pipeline run.", @@ -1274,18 +1336,19 @@ "RemediationProcedure": "Create a clean build worker for every pipeline that is being run, or use build platform-hosted runners, as they typically offer a clean instance for every run.", "AuditProcedure": "Ensure that every pipeline that is being run has its own clean, new runner.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.2.2", - "Description": "Ensure build worker environments and commands are passed and not pulled", + "Description": "A worker’s environment can be passed (for example, a pod in a Kubernetes cluster in which an environment variable is passed to it). It also can be pulled, like a virtual machine that is installing a package. Ensure that the environment and commands are passed to the workers and not pulled from it.", "Checks": [], "Attributes": [ { - "Section": "2.2", + "Section": "2 Build Pipelines", + "Subsection": "2.2 Build Worker", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "A worker’s environment can be passed (for example, a pod in a Kubernetes cluster in which an environment variable is passed to it). It also can be pulled, like a virtual machine that is installing a package. Ensure that the environment and commands are passed to the workers and not pulled from it.", @@ -1294,18 +1357,19 @@ "RemediationProcedure": "For each build worker, pass its environment and commands to it instead of pulling it.", "AuditProcedure": "For each build worker, ensure its environment and commands are passed and not pulled.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.2.3", - "Description": "Ensure the duties of each build worker are segregated", + "Description": "Separate responsibilities in the build workflow, such as testing, compiling, pushing artifacts, etc., to different build workers so that each worker will have a single duty.", "Checks": [], "Attributes": [ { - "Section": "2.2", + "Section": "2 Build Pipelines", + "Subsection": "2.2 Build Worker", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Separate responsibilities in the build workflow, such as testing, compiling, pushing artifacts, etc., to different build workers so that each worker will have a single duty.", @@ -1314,18 +1378,19 @@ "RemediationProcedure": "For each build worker, limit its responsibility to one duty.", "AuditProcedure": "For each build worker, ensure it has the least responsibility possible, preferably only one duty.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.2.4", - "Description": "Ensure build workers have minimal network connectivity", + "Description": "Ensure that build workers have minimal network connectivity.", "Checks": [], "Attributes": [ { - "Section": "2.2", + "Section": "2 Build Pipelines", + "Subsection": "2.2 Build Worker", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure that build workers have minimal network connectivity.", @@ -1334,18 +1399,19 @@ "RemediationProcedure": "Limit the network connectivity of build workers, environment, and any other components to the necessary minimum.", "AuditProcedure": "Verify that build workers, environment, and any other components have only the required minimum of network connectivity.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.2.5", - "Description": "Ensure run-time security is enforced for build workers", + "Description": "Add traces to build workers' operating systems and installed applications so that in run time, collected events can be analyzed to detect suspicious behavior patterns and malware.", "Checks": [], "Attributes": [ { - "Section": "2.2", + "Section": "2 Build Pipelines", + "Subsection": "2.2 Build Worker", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Add traces to build workers' operating systems and installed applications so that in run time, collected events can be analyzed to detect suspicious behavior patterns and malware.", @@ -1354,18 +1420,19 @@ "RemediationProcedure": "Deploy and enforce a run-time security solution on build workers.", "AuditProcedure": "Verify that a run-time security solution is enforced on every active build worker.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.2.6", - "Description": "Ensure build workers are automatically scanned for vulnerabilities", + "Description": "Scan build workers for vulnerabilities. It is recommended that this be done automatically.", "Checks": [], "Attributes": [ { - "Section": "2.2", + "Section": "2 Build Pipelines", + "Subsection": "2.2 Build Worker", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Scan build workers for vulnerabilities. It is recommended that this be done automatically.", @@ -1374,18 +1441,19 @@ "RemediationProcedure": "For each build worker, automatically scan its environmental sources, such as docker image, for vulnerabilities.", "AuditProcedure": "For each build worker, ensure the environmental sources it uses are scanned for vulnerabilities.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.2.7", - "Description": "Ensure build workers' deployment configuration is stored in a version control platform", + "Description": "Store the deployment configuration of build workers in a version control platform, such as Github.", "Checks": [], "Attributes": [ { - "Section": "2.2", + "Section": "2 Build Pipelines", + "Subsection": "2.2 Build Worker", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Store the deployment configuration of build workers in a version control platform, such as Github.", @@ -1394,18 +1462,19 @@ "RemediationProcedure": "Document and store every deployment configuration of build workers in a version control platform.", "AuditProcedure": "Verify that the deployment configuration of build workers is stored in a version control platform.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.2.8", - "Description": "Ensure resource consumption of build workers is monitored", + "Description": "Monitor the resource consumption of build workers and set alerts for high consumption that can lead to resource exhaustion.", "Checks": [], "Attributes": [ { - "Section": "2.2", + "Section": "2 Build Pipelines", + "Subsection": "2.2 Build Worker", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Monitor the resource consumption of build workers and set alerts for high consumption that can lead to resource exhaustion.", @@ -1414,18 +1483,19 @@ "RemediationProcedure": "Set reources consumption monitoring for each build worker.", "AuditProcedure": "Verify that there is monitoring of resources consumption for each build worker.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.3.1", - "Description": "Ensure all build steps are defined as code", + "Description": "Use pipeline as code for build pipelines and their defined steps.", "Checks": [], "Attributes": [ { - "Section": "2.3", + "Section": "2 Build Pipelines", + "Subsection": "2.3 Pipeline Instructions", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Use pipeline as code for build pipelines and their defined steps.", @@ -1434,18 +1504,19 @@ "RemediationProcedure": "Convert pipeline instructions into code-based syntax and upload them to the organization's version control platform.", "AuditProcedure": "Verify that all build steps are defined as code and stored in a version control system.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.3.2", - "Description": "Ensure steps have clearly defined build stage input and output", + "Description": "Define clear expected input and output for each build stage.", "Checks": [], "Attributes": [ { - "Section": "2.3", + "Section": "2 Build Pipelines", + "Subsection": "2.3 Pipeline Instructions", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Define clear expected input and output for each build stage.", @@ -1454,18 +1525,19 @@ "RemediationProcedure": "For each build stage, clearly define what is expected for input and output.", "AuditProcedure": "For each build stage, verify that the expected input and output are clearly defined.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.3.3", - "Description": "Ensure output is written to a separate, secured storage repository", + "Description": "Write pipeline output artifacts to a secured storage repository.", "Checks": [], "Attributes": [ { - "Section": "2.3", + "Section": "2 Build Pipelines", + "Subsection": "2.3 Pipeline Instructions", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Write pipeline output artifacts to a secured storage repository.", @@ -1474,18 +1546,19 @@ "RemediationProcedure": "For each pipeline that produces output artifacts, write them to a secured storage repository.", "AuditProcedure": "For each pipeline that produces output artifacts, ensure that they're written to a secured storage repository.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.3.4", - "Description": "Ensure changes to pipeline files are tracked and reviewed", + "Description": "Track and review changes to pipeline files.", "Checks": [], "Attributes": [ { - "Section": "2.3", + "Section": "2 Build Pipelines", + "Subsection": "2.3 Pipeline Instructions", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Track and review changes to pipeline files.", @@ -1494,18 +1567,19 @@ "RemediationProcedure": "For each pipeline file, track changes to it and review them.", "AuditProcedure": "For each pipeline file, ensure changes to it are being tracked and reviewed.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.3.5", - "Description": "Ensure access to build process triggering is minimized", + "Description": "Restrict access to pipeline triggers.", "Checks": [], "Attributes": [ { - "Section": "2.3", + "Section": "2 Build Pipelines", + "Subsection": "2.3 Pipeline Instructions", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Restrict access to pipeline triggers.", @@ -1514,18 +1588,19 @@ "RemediationProcedure": "For every pipeline in use, grant only the necessary users permission to trigger it.", "AuditProcedure": "For every pipeline in use, verify only the necessary users have permission to trigger it.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.3.6", - "Description": "Ensure pipelines are automatically scanned for misconfigurations", + "Description": "Scan the pipeline for misconfigurations. It is recommended that this be performed automatically.", "Checks": [], "Attributes": [ { - "Section": "2.3", + "Section": "2 Build Pipelines", + "Subsection": "2.3 Pipeline Instructions", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Scan the pipeline for misconfigurations. It is recommended that this be performed automatically.", @@ -1534,18 +1609,19 @@ "RemediationProcedure": "For each pipeline, set automated misconfiguration scanning.", "AuditProcedure": "For each pipeline, verify that it is automatically scanned for misconfigurations.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.3.7", - "Description": "Ensure pipelines are automatically scanned for vulnerabilities", + "Description": "Scan pipelines for vulnerabilities. It is recommended that this be implemented automatically.", "Checks": [], "Attributes": [ { - "Section": "2.3", + "Section": "2 Build Pipelines", + "Subsection": "2.3 Pipeline Instructions", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Scan pipelines for vulnerabilities. It is recommended that this be implemented automatically.", @@ -1554,18 +1630,19 @@ "RemediationProcedure": "For each pipeline, set automated vulnerability scanning.", "AuditProcedure": "For each pipeline, verify that it is automatically scanned for vulnerabilities.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.3.8", - "Description": "Ensure scanners are in place to identify and prevent sensitive data in pipeline files", + "Description": "Detect and prevent sensitive data, such as confidential ID numbers, passwords, etc., in pipelines.", "Checks": [], "Attributes": [ { - "Section": "2.3", + "Section": "2 Build Pipelines", + "Subsection": "2.3 Pipeline Instructions", "Profile": "Level 2", "AssessmentStatus": "Automated", "Description": "Detect and prevent sensitive data, such as confidential ID numbers, passwords, etc., in pipelines.", @@ -1574,18 +1651,40 @@ "RemediationProcedure": "For every pipeline that is in use, set scanners that will identify and prevent sensitive data within it.", "AuditProcedure": "For every pipeline that is in use, verify that scanners are set to identify and prevent the existence of sensitive data within it.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.4.1", - "Description": "Ensure all artifacts on all releases are signed", + "Description": "Sign all artifacts in all releases with user or organization keys.", "Checks": [], "Attributes": [ { - "Section": "2.4", + "Section": "2 Build Pipelines", + "Subsection": "2.4 Pipeline Integrity", + "Profile": "Level 1", + "AssessmentStatus": "Manual", + "Description": "Sign all artifacts in all releases with user or organization keys.", + "RationaleStatement": "Signing artifacts is used to validate both their integrity and security. Organizations signal that artifacts may be trusted and they themselves produced them by ensuring that every artifact is properly signed. The presence of this signature also makes potentially malicious activity far more difficult.", + "ImpactStatement": "", + "RemediationProcedure": "For every artifact in every release, verify that all are properly signed.", + "AuditProcedure": "Ensure every artifact in every release is signed.", + "AdditionalInformation": "", + "References": "", + "DefaultValue": "" + } + ] + }, + { + "Id": "2.4.1", + "Description": "Sign all artifacts in all releases with user or organization keys.", + "Checks": [], + "Attributes": [ + { + "Section": "2 Build Pipelines", + "Subsection": "2.4 Pipeline Integrity", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Sign all artifacts in all releases with user or organization keys.", @@ -1594,18 +1693,19 @@ "RemediationProcedure": "For every artifact in every release, verify that all are properly signed.", "AuditProcedure": "Ensure every artifact in every release is signed.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.4.2", - "Description": "Ensure all external dependencies used in the build process are locked", + "Description": "External dependencies may be public packages needed in the pipeline, or perhaps the public image being used for the build worker. Lock these external dependencies in every build pipeline.", "Checks": [], "Attributes": [ { - "Section": "2.4", + "Section": "2 Build Pipelines", + "Subsection": "2.4 Pipeline Integrity", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "External dependencies may be public packages needed in the pipeline, or perhaps the public image being used for the build worker. Lock these external dependencies in every build pipeline.", @@ -1614,18 +1714,19 @@ "RemediationProcedure": "For all external dependencies being used in pipelines, verify they are locked.", "AuditProcedure": "Ensure every external dependency being used in pipelines is locked.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://argon.io/blog/pipeline-composition-analysis-how-your-ci-pipeline-presents-new-opportunities-for-attackers/" + "References": "https://argon.io/blog/pipeline-composition-analysis-how-your-ci-pipeline-presents-new-opportunities-for-attackers/", + "DefaultValue": "" } ] }, { "Id": "2.4.3", - "Description": "Ensure dependencies are validated before being used", + "Description": "Validate every dependency of the pipeline before use.", "Checks": [], "Attributes": [ { - "Section": "2.4", + "Section": "2 Build Pipelines", + "Subsection": "2.4 Pipeline Integrity", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Validate every dependency of the pipeline before use.", @@ -1634,18 +1735,19 @@ "RemediationProcedure": "For every dependency used in every pipeline, validate each one.", "AuditProcedure": "For every dependency used in every pipeline, ensure it has been validated.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.4.4", - "Description": "Ensure the build pipeline creates reproducible artifacts", + "Description": "Verify that the build pipeline creates reproducible artifacts, meaning that an artifact of the build pipeline is the same in every run when given the same input.", "Checks": [], "Attributes": [ { - "Section": "2.4", + "Section": "2 Build Pipelines", + "Subsection": "2.4 Pipeline Integrity", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Verify that the build pipeline creates reproducible artifacts, meaning that an artifact of the build pipeline is the same in every run when given the same input.", @@ -1654,18 +1756,19 @@ "RemediationProcedure": "Create build pipelines that produce the same artifact given the same input (for example, artifacts that do not rely on timestamps).", "AuditProcedure": "Ensure that build pipelines create reproducible artifacts.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.4.5", - "Description": "Ensure pipeline steps produce a Software Bill of Materials (SBOM)", + "Description": "SBOM (Software Bill of Materials) is a file that specifies each component of software or a build process. Generate an SBOM after each run of a pipeline.", "Checks": [], "Attributes": [ { - "Section": "2.4", + "Section": "2 Build Pipelines", + "Subsection": "2.4 Pipeline Integrity", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "SBOM (Software Bill of Materials) is a file that specifies each component of software or a build process. Generate an SBOM after each run of a pipeline.", @@ -1674,18 +1777,19 @@ "RemediationProcedure": "For each pipeline, configure it to produce a Software Bill of Materials on every run.", "AuditProcedure": "For each pipeline, ensure it produces a Software Bill of Materials on every run.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "2.4.6", - "Description": "Ensure pipeline steps sign the Software Bill of Materials (SBOM) produced", + "Description": "SBOM (Software Bill of Materials) is a file that specifies each component of software or a build process. It should be generated after every pipeline run. After it is generated, it must then be signed.", "Checks": [], "Attributes": [ { - "Section": "2.4", + "Section": "2 Build Pipelines", + "Subsection": "2.4 Pipeline Integrity", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "SBOM (Software Bill of Materials) is a file that specifies each component of software or a build process. It should be generated after every pipeline run. After it is generated, it must then be signed.", @@ -1694,38 +1798,40 @@ "RemediationProcedure": "For each pipeline, configure it to sign its produced Software Bill of Materials on every run.", "AuditProcedure": "For each pipeline, ensure it signs the Software Bill of Materials it produces on every run.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.1.1", - "Description": "Ensure third-party artifacts and open-source libraries are verified", + "Description": "Ensure third-party artifacts and open-source libraries in use are trusted and verified.", "Checks": [], "Attributes": [ { - "Section": "3.1", + "Section": "3 Dependencies", + "Subsection": "3.1 Third-Party Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure third-party artifacts and open-source libraries in use are trusted and verified.", "RationaleStatement": "Verify third-party artifacts used in code are trusted and have not been infected by a malicious actor before use. This can be accomplished, for example, by comparing the checksum of the dependency to its checksum in a trusted source. If a difference arises, this may be a sign that someone interfered and added malicious code. If this dependency is used, it will infect the environment and could end in a massive breach, leaving the organization exposed to data leaks and more.", "ImpactStatement": "", - "RemediationProcedure": "Verify every GitHub action (building block of a GitHub workflow) in use by performing the following: \n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the left sidebar, click **Actions**, then click **General**.\n 4. Under \"Policies\", check **Allow OWNER, and select non-OWNER, actions and reusable workflows**, and then **Allow actions created by GitHub**.\n 5. Click **Save**.\n \n\n Alternatively, perform the following for each repository where GitHub actions are used:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the left sidebar, click **Actions**, then click **General**.\n 4. Under \"Actions permissions\", check **Allow OWNER, and select non-OWNER, actions and reusable workflows**, and then **Allow actions created by GitHub**.\n 5. Click **Save**.", - "AuditProcedure": "For every GitHub action (building block of a GitHub workflow), ensure verification before use by performing the following: \n \n\n 1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n 2. Next to the organization, click **Settings**.\n 3. In the left sidebar, click **Actions**, then click **General**.\n 4. Under \"Policies\", ensure **Allow OWNER, and select non-OWNER, actions and reusable workflows**, and **Allow actions created by GitHub** are checked.\n \n\n Alternatively, perform the following for each repository where GitHub actions are used:\n \n\n 1. On GitHub.com, navigate to the main page of the repository.\n 2. Under your repository name, click **Settings**.\n 3. In the left sidebar, click **Actions**, then click **General**.\n 4. Under \"Actions permissions\", ensure **Allow OWNER, and select non-OWNER, actions and reusable workflows** and **Allow actions created by GitHub** are checked.", + "RemediationProcedure": "Verify every GitHub action (building block of a GitHub workflow) in use by performing the following: \n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the left sidebar, click **Actions**, then click **General**.\n4. Under \"Policies\", check **Allow OWNER, and select non-OWNER, actions and reusable workflows**, and then **Allow actions created by GitHub**.\n5. Click **Save**.\n\nAlternatively, perform the following for each repository where GitHub actions are used:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the left sidebar, click **Actions**, then click **General**.\n4. Under \"Actions permissions\", check **Allow OWNER, and select non-OWNER, actions and reusable workflows**, and then **Allow actions created by GitHub**.\n5. Click **Save**.", + "AuditProcedure": "For every GitHub action (building block of a GitHub workflow), ensure verification before use by performing the following: \n\n1. In the top right corner of GitHub.com, click your profile photo, then click **Your organizations**.\n2. Next to the organization, click **Settings**.\n3. In the left sidebar, click **Actions**, then click **General**.\n4. Under \"Policies\", ensure **Allow OWNER, and select non-OWNER, actions and reusable workflows**, and **Allow actions created by GitHub** are checked.\n\nAlternatively, perform the following for each repository where GitHub actions are used:\n\n1. On GitHub.com, navigate to the main page of the repository.\n2. Under your repository name, click **Settings**.\n3. In the left sidebar, click **Actions**, then click **General**.\n4. Under \"Actions permissions\", ensure **Allow OWNER, and select non-OWNER, actions and reusable workflows** and **Allow actions created by GitHub** are checked.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.1.2", - "Description": "Ensure Software Bill of Materials (SBOM) is required from all third-party suppliers", + "Description": "A Software Bill Of Materials (SBOM) is a file that specifies each component of software or a build process. Require an SBOM from every third-party provider.", "Checks": [], "Attributes": [ { - "Section": "3.1", + "Section": "3 Dependencies", + "Subsection": "3.1 Third-Party Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "A Software Bill Of Materials (SBOM) is a file that specifies each component of software or a build process. Require an SBOM from every third-party provider.", @@ -1734,18 +1840,19 @@ "RemediationProcedure": "For every third-party dependency in use, require a Software Bill of Materials from its supplier.", "AuditProcedure": "For every third-party dependency in use, ensure it has a Software Bill of Materials.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.1.3", - "Description": "Ensure signed metadata of the build process is required and verified", + "Description": "Require and verify signed metadata of the build process for all dependencies in use.", "Checks": [], "Attributes": [ { - "Section": "3.1", + "Section": "3 Dependencies", + "Subsection": "3.1 Third-Party Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Require and verify signed metadata of the build process for all dependencies in use.", @@ -1754,18 +1861,19 @@ "RemediationProcedure": "For each artifact in use, require and verify signed metadata of the build process.", "AuditProcedure": "For each artifact used, ensure it was supplied with verified and signed metadata of its build process. The signature should be the organizational signature and should be verifiable by common Certificate Authority servers.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.1.4", - "Description": "Ensure dependencies are monitored between open-source components", + "Description": "Monitor, or ask software suppliers to monitor, dependencies between open-source components in use.", "Checks": [], "Attributes": [ { - "Section": "3.1", + "Section": "3 Dependencies", + "Subsection": "3.1 Third-Party Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Monitor, or ask software suppliers to monitor, dependencies between open-source components in use.", @@ -1774,18 +1882,19 @@ "RemediationProcedure": "For each open-source component, monitor its dependencies.", "AuditProcedure": "For each open-source component, ensure its dependencies are monitored.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.1.5", - "Description": "Ensure trusted package managers and repositories are defined and prioritized", + "Description": "Prioritize trusted package registries over others when pulling a package.", "Checks": [], "Attributes": [ { - "Section": "3.1", + "Section": "3 Dependencies", + "Subsection": "3.1 Third-Party Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Prioritize trusted package registries over others when pulling a package.", @@ -1794,18 +1903,19 @@ "RemediationProcedure": "For each package to be downloaded, configure it to be downloaded from a trusted source.", "AuditProcedure": "For each package registry in use, ensure it is trusted.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.1.6", - "Description": "Ensure a signed Software Bill of Materials (SBOM) of the code is supplied", + "Description": "A Software Bill of Materials (SBOM) is a file that specifies each component of software or a build process. When using a dependency, demand its SBOM and ensure it is signed for validation purposes.", "Checks": [], "Attributes": [ { - "Section": "3.1", + "Section": "3 Dependencies", + "Subsection": "3.1 Third-Party Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "A Software Bill of Materials (SBOM) is a file that specifies each component of software or a build process. When using a dependency, demand its SBOM and ensure it is signed for validation purposes.", @@ -1814,18 +1924,19 @@ "RemediationProcedure": "For every artifact supplied, require and verify a signed Software Bill of Materials from its supplier.", "AuditProcedure": "For every artifact supplied, ensure it has a validated, signed Software Bill of Materials.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.1.7", - "Description": "Ensure dependencies are pinned to a specific, verified version", + "Description": "Pin dependencies to a specific version. Avoid using the \"latest\" tag or broad version.", "Checks": [], "Attributes": [ { - "Section": "3.1", + "Section": "3 Dependencies", + "Subsection": "3.1 Third-Party Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Pin dependencies to a specific version. Avoid using the \"latest\" tag or broad version.", @@ -1834,18 +1945,19 @@ "RemediationProcedure": "For every dependency in use, pin to a specific version.", "AuditProcedure": "For every dependency in use, ensure it is pinned to a specific version.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.1.8", - "Description": "Ensure all packages used are more than 60 days old", + "Description": "Use packages that are more than 60 days old.", "Checks": [], "Attributes": [ { - "Section": "3.1", + "Section": "3 Dependencies", + "Subsection": "3.1 Third-Party Packages", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Use packages that are more than 60 days old.", @@ -1854,18 +1966,19 @@ "RemediationProcedure": "If a package used is less than 60 days old, stop using it and find another solution.", "AuditProcedure": "For every package used, ensure it is more than 60 days old.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.2.1", - "Description": "Ensure an organization-wide dependency usage policy is enforced", + "Description": "Enforce a policy for dependency usage across the organization. For example, disallow the use of packages less than 60 days old.", "Checks": [], "Attributes": [ { - "Section": "3.2", + "Section": "3 Dependencies", + "Subsection": "3.2 Validate Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Enforce a policy for dependency usage across the organization. For example, disallow the use of packages less than 60 days old.", @@ -1874,18 +1987,19 @@ "RemediationProcedure": "Enforce policies for dependency usage across the organization.", "AuditProcedure": "Verify that a policy for dependency usage is enforced across the organization.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.2.2", - "Description": "Ensure packages are automatically scanned for known vulnerabilities", + "Description": "Automatically scan every package for vulnerabilities.", "Checks": [], "Attributes": [ { - "Section": "3.2", + "Section": "3 Dependencies", + "Subsection": "3.2 Validate Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Automatically scan every package for vulnerabilities.", @@ -1894,18 +2008,19 @@ "RemediationProcedure": "Set automatic scanning of packages for vulnerabilities.", "AuditProcedure": "Ensure automatic scanning of packages for vulnerabilities is enabled.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.2.3", - "Description": "Ensure packages are automatically scanned for license implications", + "Description": "A software license is a document that provides legal conditions and guidelines for the use and distribution of software, usually defined by the author. It is recommended to scan for any legal implications automatically.", "Checks": [], "Attributes": [ { - "Section": "3.2", + "Section": "3 Dependencies", + "Subsection": "3.2 Validate Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "A software license is a document that provides legal conditions and guidelines for the use and distribution of software, usually defined by the author. It is recommended to scan for any legal implications automatically.", @@ -1914,18 +2029,19 @@ "RemediationProcedure": "Set automatic package scanning for license implications.", "AuditProcedure": "Ensure license implication rules are configured and are scanned automatically.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "3.2.4", - "Description": "Ensure packages are automatically scanned for ownership change", + "Description": "Scan every package automatically for ownership change.", "Checks": [], "Attributes": [ { - "Section": "3.2", + "Section": "3 Dependencies", + "Subsection": "3.2 Validate Packages", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Scan every package automatically for ownership change.", @@ -1934,38 +2050,40 @@ "RemediationProcedure": "Set automatic scanning of packages for ownership change.", "AuditProcedure": "Ensure automatic scanning of packages for ownership change is set.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://blog.npmjs.org/post/182828408610/the-security-risks-of-changing-package-owners.html:https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident" + "References": "https://blog.npmjs.org/post/182828408610/the-security-risks-of-changing-package-owners.html:https://blog.npmjs.org/post/180565383195/details-about-the-event-stream-incident", + "DefaultValue": "" } ] }, { "Id": "4.1.1", - "Description": "Ensure all artifacts are signed by the build pipeline itself", + "Description": "Configure the build pipeline to sign every artifact it produces and verify that each artifact has the appropriate signature.", "Checks": [], "Attributes": [ { - "Section": "4.1", + "Section": "4 Artifacts", + "Subsection": "4.1 Verification", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Configure the build pipeline to sign every artifact it produces and verify that each artifact has the appropriate signature.", "RationaleStatement": "A cryptographic signature can be used to verify artifact authenticity. The signature created with a certain key is unique and not reversible, thus making it unique to the author. This means that an attacker tampering with a signed artifact will be noticed immediately using a simple verification step because the signature will change. Signing artifacts by the build pipeline that produces them ensures the integrity of those artifacts.", "ImpactStatement": "", - "RemediationProcedure": "Sign every artifact produced with the build pipeline that created it. Configure the build pipeline to sign each artifact.\n \n\n Steps from GitHub Documentation:\n \n\n You can follow the steps below to sign artifacts in GitHub actions. The trick involves loading in your private key into GitHub Actions using the gpg command-line commands.\n \n\n Export your gpg private key from the system that you have created it.\n Find your key-id (using gpg --list-secret-keys --keyid-format=long)\n Export the gpg secret key to an ASCII file using gpg --export-secret-keys -a > secret.txt\n Edit secret.txt using a plain text editor, and replace all newlines with a literal \"\\n\" until everything is on a single line\n Set up GitHub Actions secrets\n Create a secret called OSSRH_GPG_SECRET_KEY using the text from your edited secret.txt file (the whole text should be in a single line)\n Create a secret called OSSRH_GPG_SECRET_KEY_PASSWORD containing the password for your gpg secret key\n Create a GitHub Actions step to install the gpg secret key\n Add an action similar to:\n ```\n - id: install-secret-key\n name: Install gpg secret key\n run: |\n cat <(echo -e \"${{ secrets.OSSRH_GPG_SECRET_KEY }}\") | gpg --batch --import\n gpg --list-secret-keys --keyid-format LONG\n ```\n Verify that the secret key is shown in the GitHub Actions logs\n You can remove the output from list secret keys if you are confident that this action will work, but it is better to leave it in there\n Bring it all together, and create a GitHub Actions step to publish\n Add an action similar to:\n ```\n - id: publish-to-central\n name: Publish to Central Repository\n env:\n MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}\n MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}\n run: |\n mvn \\\n --no-transfer-progress \\\n --batch-mode \\\n -Dgpg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} \\\n clean deploy\n ```\n After a couple of hours, verify that the artifact got published to The Central Repository", - "AuditProcedure": "Verify that the build pipeline signs every new artifact it produces and all artifacts are signed.\n \n\n There are many different signing tools or options each have there own method or commands to verify that the code or package created is signed.", + "RemediationProcedure": "Sign every artifact produced with the build pipeline that created it. Configure the build pipeline to sign each artifact.\n\nSteps from GitHub Documentation:\n\nYou can follow the steps below to sign artifacts in GitHub actions. The trick involves loading in your private key into GitHub Actions using the gpg command-line commands.\n\nExport your gpg private key from the system that you have created it.\nFind your key-id (using gpg --list-secret-keys --keyid-format=long)\nExport the gpg secret key to an ASCII file using gpg --export-secret-keys -a > secret.txt\nEdit secret.txt using a plain text editor, and replace all newlines with a literal \"\\n\" until everything is on a single line\nSet up GitHub Actions secrets\nCreate a secret called OSSRH_GPG_SECRET_KEY using the text from your edited secret.txt file (the whole text should be in a single line)\nCreate a secret called OSSRH_GPG_SECRET_KEY_PASSWORD containing the password for your gpg secret key\nCreate a GitHub Actions step to install the gpg secret key\nAdd an action similar to:\n```\n- id: install-secret-key\n name: Install gpg secret key\n run: |\n cat <(echo -e \"${{ secrets.OSSRH_GPG_SECRET_KEY }}\") | gpg --batch --import\n gpg --list-secret-keys --keyid-format LONG\n```\nVerify that the secret key is shown in the GitHub Actions logs\nYou can remove the output from list secret keys if you are confident that this action will work, but it is better to leave it in there\nBring it all together, and create a GitHub Actions step to publish\nAdd an action similar to:\n```\n- id: publish-to-central\n name: Publish to Central Repository\n env:\n MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}\n MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}\n run: |\n mvn \\\n --no-transfer-progress \\\n --batch-mode \\\n -Dgpg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} \\\n clean deploy\n```\nAfter a couple of hours, verify that the artifact got published to The Central Repository", + "AuditProcedure": "Verify that the build pipeline signs every new artifact it produces and all artifacts are signed.\n\nThere are many different signing tools or options each have there own method or commands to verify that the code or package created is signed.", "AdditionalInformation": "", - "DefaultValue": "Artifacts are not signed by Default.", - "References": "https://docs.github.com/en/code-security/supply-chain-security/end-to-end-supply-chain/securing-builds:https://gist.github.com/sualeh/ae78dc16123899d7942bc38baba5203c" + "References": "https://docs.github.com/en/code-security/supply-chain-security/end-to-end-supply-chain/securing-builds:https://gist.github.com/sualeh/ae78dc16123899d7942bc38baba5203c", + "DefaultValue": "Artifacts are not signed by Default." } ] }, { "Id": "4.1.2", - "Description": "Ensure artifacts are encrypted before distribution", + "Description": "Encrypt artifacts before they are distributed and ensure only trusted platforms have decryption capabilities.", "Checks": [], "Attributes": [ { - "Section": "4.1", + "Section": "4 Artifacts", + "Subsection": "4.1 Verification", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Encrypt artifacts before they are distributed and ensure only trusted platforms have decryption capabilities.", @@ -1974,18 +2092,19 @@ "RemediationProcedure": "Encrypt every artifact before distribution.", "AuditProcedure": "Ensure every artifact is encrypted before it is delivered.", "AdditionalInformation": "", - "DefaultValue": "Artifacts do not get encrypted by default.", - "References": "" + "References": "", + "DefaultValue": "Artifacts do not get encrypted by default." } ] }, { "Id": "4.1.3", - "Description": "Ensure only authorized platforms have decryption capabilities of artifacts", + "Description": "Grant decryption capabilities of artifacts only to trusted and authorized platforms.", "Checks": [], "Attributes": [ { - "Section": "4.1", + "Section": "4 Artifacts", + "Subsection": "4.1 Verification", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Grant decryption capabilities of artifacts only to trusted and authorized platforms.", @@ -1994,18 +2113,19 @@ "RemediationProcedure": "Grant decryption capabilities of the organization's artifacts only for trusted and authorized platforms.", "AuditProcedure": "Ensure only trusted and authorized platforms have decryption capabilities of the organization's artifacts.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "4.2.1", - "Description": "Ensure the authority to certify artifacts is limited", + "Description": "Software certification is used to verify the safety of certain software usage and to establish trust between the supplier and the consumer. Any artifact can be certified. Limit the authority to certify different artifacts.", "Checks": [], "Attributes": [ { - "Section": "4.2", + "Section": "4 Artifacts", + "Subsection": "4.2 Access to Artifacts", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Software certification is used to verify the safety of certain software usage and to establish trust between the supplier and the consumer. Any artifact can be certified. Limit the authority to certify different artifacts.", @@ -2014,18 +2134,19 @@ "RemediationProcedure": "Limit which artifact can be certified by which authority.", "AuditProcedure": "Ensure only certain artifacts can be certified by certain parties.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "4.2.2", - "Description": "Ensure number of permitted users who may upload new artifacts is minimized", + "Description": "Minimize ability to upload artifacts to the lowest number of trusted users possible.", "Checks": [], "Attributes": [ { - "Section": "4.2", + "Section": "4 Artifacts", + "Subsection": "4.2 Access to Artifacts", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Minimize ability to upload artifacts to the lowest number of trusted users possible.", @@ -2034,18 +2155,19 @@ "RemediationProcedure": "Allow only trusted and qualified users to upload new artifacts and limit them in number.", "AuditProcedure": "Ensure only trusted and qualified users can upload new artifacts, and that their number is the lowest possible.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "4.2.3", - "Description": "Ensure user access to the package registry utilizes Multi-Factor Authentication (MFA)", + "Description": "Enforce Multi-Factor Authentication (MFA) for user access to the package registry.", "Checks": [], "Attributes": [ { - "Section": "4.2", + "Section": "4 Artifacts", + "Subsection": "4.2 Access to Artifacts", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Enforce Multi-Factor Authentication (MFA) for user access to the package registry.", @@ -2054,18 +2176,19 @@ "RemediationProcedure": "For each package registry in use, enforce Multi-Factor Authentication as the only way to authenticate.", "AuditProcedure": "For each package registry in use, verify that Multi-Factor Authentication is enforced and is the only way to authenticate.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "4.2.4", - "Description": "Ensure user management of the package registry is not local", + "Description": "Manage users and their access to the package registry with an external authentication server and not with the package registry itself.", "Checks": [], "Attributes": [ { - "Section": "4.2", + "Section": "4 Artifacts", + "Subsection": "4.2 Access to Artifacts", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Manage users and their access to the package registry with an external authentication server and not with the package registry itself.", @@ -2074,118 +2197,124 @@ "RemediationProcedure": "For each package registry, use the main authentication server of the organization for user management and do not manage locally.", "AuditProcedure": "For each package registry, verify that its user access is not managed locally, but instead with the main authentication server of the organization.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "4.2.5", - "Description": "Ensure anonymous access to artifacts is revoked", + "Description": "For GitHub Private or Internal repositories anonymous access is not available. Verify that all repos that require access controls are Private or Internal.", "Checks": [], "Attributes": [ { - "Section": "4.2", + "Section": "4 Artifacts", + "Subsection": "4.2 Access to Artifacts", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "For GitHub Private or Internal repositories anonymous access is not available. Verify that all repos that require access controls are Private or Internal.", "RationaleStatement": "Disable the option to view artifacts as an anonymous user in order to protect private artifacts from being exposed.", "ImpactStatement": "Only logged and authorized users will be able to access artifacts.", - "RemediationProcedure": "Changing a repository's visibility\n \n\n 1. On your GitHub Enterprise Server instance, navigate to the main page of the repository.\n 1. Under your repository name, click Settings\n 1. Under \"Danger Zone\", to the right of to \"Change repository visibility\", click Change visibility.\n 1. Select a visibility.\n \n\n 1. Choose \n - Mark private\n - Make Internal\n \n\n 6. To verify that you're changing the correct repository's visibility, type the name of the repository you want to change the visibility of.", - "AuditProcedure": "To Audit the existing settings:\n \n\n 1. On your GitHub Enterprise Server instance, navigate to the main page of the repository.\n 1. Under your repository name, click Settings\n 3. Under \"Danger Zone\", to the right of to \"Change repository visibility\", click Change visibility.\n \n\n Review the current selection.", + "RemediationProcedure": "Changing a repository's visibility\n\n1. On your GitHub Enterprise Server instance, navigate to the main page of the repository.\n1. Under your repository name, click Settings\n1. Under \"Danger Zone\", to the right of to \"Change repository visibility\", click Change visibility.\n1. Select a visibility.\n\n1. Choose \n - Mark private\n - Make Internal\n\n6. To verify that you're changing the correct repository's visibility, type the name of the repository you want to change the visibility of.", + "AuditProcedure": "To Audit the existing settings:\n\n1. On your GitHub Enterprise Server instance, navigate to the main page of the repository.\n1. Under your repository name, click Settings\n3. Under \"Danger Zone\", to the right of to \"Change repository visibility\", click Change visibility.\n\nReview the current selection.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://docs.github.com/en/enterprise-server@3.3/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/setting-repository-visibility" + "References": "https://docs.github.com/en/enterprise-server@3.3/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/setting-repository-visibility", + "DefaultValue": "" } ] }, { "Id": "4.2.6", - "Description": "Ensure minimum number of administrators are set for the package registry", + "Description": "Ensure the package registry has a minimum number of administrators.", "Checks": [], "Attributes": [ { - "Section": "4.2", + "Section": "4 Artifacts", + "Subsection": "4.2 Access to Artifacts", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Ensure the package registry has a minimum number of administrators.", "RationaleStatement": "Package registry admins have the ability to add/remove users, repositories, packages. Due to the permissive access granted to an admin, it is highly recommended to keep the number of administrator accounts as minimal as possible.", "ImpactStatement": "Administrator privileges are required to provide and maintain a secure and stable platform but allowing extraneous administrator accounts can create a vulnerability.", - "RemediationProcedure": "Set the minimum number of administrators in your package registry.\n \n\n To accomplish this:\n \n\n For each repository that you administer on GitHub, you can see an overview of every team or person with access to the repository. From the overview, choose Manage access and provide access to the appropriate people or teams.", - "AuditProcedure": "Verify that your package registry has only the minimum number of administrators.\n \n\n For each repository that you administer on GitHub, you can see an overview of every team or person with access to the repository. From the overview, you can also invite new teams or people, change each team or person's role for the repository, or remove access to the repository.", + "RemediationProcedure": "Set the minimum number of administrators in your package registry.\n\nTo accomplish this:\n\nFor each repository that you administer on GitHub, you can see an overview of every team or person with access to the repository. From the overview, choose Manage access and provide access to the appropriate people or teams.", + "AuditProcedure": "Verify that your package registry has only the minimum number of administrators.\n\nFor each repository that you administer on GitHub, you can see an overview of every team or person with access to the repository. From the overview, you can also invite new teams or people, change each team or person's role for the repository, or remove access to the repository.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-teams-and-people-with-access-to-your-repository" + "References": "https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-teams-and-people-with-access-to-your-repository", + "DefaultValue": "" } ] }, { "Id": "4.3.1", - "Description": "Ensure all signed artifacts are validated upon uploading the package registry", + "Description": "Validate artifact signatures before uploading to the package registry.", "Checks": [], "Attributes": [ { - "Section": "4.3", + "Section": "4 Artifacts", + "Subsection": "4.3 Package Registries", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Validate artifact signatures before uploading to the package registry.", "RationaleStatement": "Cryptographic signature is a tool to verify artifact authenticity. Every artifact is supposed to be signed by its creator in order to confirm that it was not compromised before reaching the client. Validating an artifact signature before delivering it is another level of protection which ensures the signature has not been changed, meaning no one tried or succeeded in tampering with the artifact. This creates trust between the supplier and the client.", "ImpactStatement": "", "RemediationProcedure": "Validate every artifact with its signature before uploading it to the package registry. It is recommended to do so automatically.", - "AuditProcedure": "Ensure every artifact in the package registry has been validated with its signature.\n \n\n 1. On GitHub, navigate to a pull request\n 2. On the pull request, click <> Commits and view the detailed information regarding the signature.", + "AuditProcedure": "Ensure every artifact in the package registry has been validated with its signature.\n\n1. On GitHub, navigate to a pull request\n2. On the pull request, click <> Commits and view the detailed information regarding the signature.", "AdditionalInformation": "", - "DefaultValue": "Artifacts are not scanned by default.", - "References": "https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification:https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits" + "References": "https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification:https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits", + "DefaultValue": "Artifacts are not scanned by default." } ] }, { "Id": "4.3.2", - "Description": "Ensure all versions of an existing artifact have their signatures validated", + "Description": "Validate the signature of all versions of an existing artifact.", "Checks": [], "Attributes": [ { - "Section": "4.3", + "Section": "4 Artifacts", + "Subsection": "4.3 Package Registries", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Validate the signature of all versions of an existing artifact.", "RationaleStatement": "In order to be certain a version of an existing and trusted artifact is not malicious or delivered by someone looking to interfere with the supply chain, it is a good practice to validate the signatures of each version. Doing so decreases the risk of using a compromised artifact, which might lead to a breach.", "ImpactStatement": "", "RemediationProcedure": "For each artifact, sign and validate each version before uploading or using the artifact.", - "AuditProcedure": "For each artifact, ensure that all of its versions are signed and validated before it is uploaded or used.\n \n\n Ensure every artifact in the package registry has been validated with its signature.\n \n\n On GitHub, navigate to a pull request\n On the pull request, click <> Commits and view the detailed information regarding the signature.", + "AuditProcedure": "For each artifact, ensure that all of its versions are signed and validated before it is uploaded or used.\n\nEnsure every artifact in the package registry has been validated with its signature.\n\nOn GitHub, navigate to a pull request\nOn the pull request, click <> Commits and view the detailed information regarding the signature.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "4.3.3", - "Description": "Ensure changes in package registry configuration are audited", + "Description": "Audit changes of the package registry configuration.", "Checks": [], "Attributes": [ { - "Section": "4.3", + "Section": "4 Artifacts", + "Subsection": "4.3 Package Registries", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Audit changes of the package registry configuration.", "RationaleStatement": "The package registry is a crucial component in the software supply chain. It stores artifacts with potentially sensitive data that will eventually be deployed and used in production. Every change made to the package registry configuration must be examined carefully to ensure no exposure of the registry's sensitive data. This examination also ensures no malicious actors have performed modifications to a stored artifact. Auditing the configuration and its changes helps in decreasing such risks.", "ImpactStatement": "", "RemediationProcedure": "Audit the changes to the package registry configuration.", - "AuditProcedure": "Verify that all changes to the packages registry configuration are audited.\n \n\n Search the audit log with\n \n\n repo category actions", + "AuditProcedure": "Verify that all changes to the packages registry configuration are audited.\n\nSearch the audit log with\n\nrepo category actions", "AdditionalInformation": "", - "DefaultValue": "GitHub audits this by default.", - "References": "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/reviewing-the-audit-log-for-your-organization" + "References": "https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/reviewing-the-audit-log-for-your-organization", + "DefaultValue": "GitHub audits this by default." } ] }, { "Id": "4.3.4", - "Description": "Ensure webhooks of the repository are secured", + "Description": "Use secured webhooks to reduce the possibility of malicious payloads.", "Checks": [], "Attributes": [ { - "Section": "4.3", + "Section": "4 Artifacts", + "Subsection": "4.3 Package Registries", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Use secured webhooks to reduce the possibility of malicious payloads.", @@ -2194,18 +2323,19 @@ "RemediationProcedure": "For each webhook in use, change it to secured (over HTTPS).", "AuditProcedure": "", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "4.4.1", - "Description": "Ensure artifacts contain information about their origin", + "Description": "When delivering artifacts, ensure they have information about their origin. This may be done by providing a Software Bill of Manufacture (SBOM) or some metadata files.", "Checks": [], "Attributes": [ { - "Section": "4.4", + "Section": "4 Artifacts", + "Subsection": "4.4 Origin Traceability", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "When delivering artifacts, ensure they have information about their origin. This may be done by providing a Software Bill of Manufacture (SBOM) or some metadata files.", @@ -2214,18 +2344,19 @@ "RemediationProcedure": "For each artifact supplied, supply information about its origin. For each artifact in use, ask for information about its origin.", "AuditProcedure": "For each artifact, ensure it has information about its origin.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.1.1", - "Description": "Ensure deployment configuration files are separated from source code", + "Description": "Deployment configurations are often stored in a version control system. Separate deployment configuration files from source code repositories.", "Checks": [], "Attributes": [ { - "Section": "5.1", + "Section": "5 Deployment", + "Subsection": "5.1 Deployment Configuration", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Deployment configurations are often stored in a version control system. Separate deployment configuration files from source code repositories.", @@ -2234,18 +2365,19 @@ "RemediationProcedure": "Store each deployment configuration file in a dedicated repository separately from source code.", "AuditProcedure": "Ensure each deployment configuration file is stored separately from source code.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.1.2", - "Description": "Ensure changes in deployment configuration are audited", + "Description": "Audit and track changes made in deployment configuration.", "Checks": [], "Attributes": [ { - "Section": "5.1", + "Section": "5 Deployment", + "Subsection": "5.1 Deployment Configuration", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Audit and track changes made in deployment configuration.", @@ -2254,18 +2386,19 @@ "RemediationProcedure": "For each deployment configuration, track and audit changes made to it.", "AuditProcedure": "For each deployment configuration, ensure changes made to it are audited and tracked.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.1.3", - "Description": "Ensure scanners are in place to identify and prevent sensitive data in deployment configuration", + "Description": "Detect and prevent sensitive data – such as confidential ID numbers, passwords, etc. – in deployment configurations.", "Checks": [], "Attributes": [ { - "Section": "5.1", + "Section": "5 Deployment", + "Subsection": "5.1 Deployment Configuration", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Detect and prevent sensitive data – such as confidential ID numbers, passwords, etc. – in deployment configurations.", @@ -2274,18 +2407,19 @@ "RemediationProcedure": "For each deployment configuration file, set scanners to identify and prevent sensitive data within it.", "AuditProcedure": "For each deployment configuration file, verify that scanners are set to identify and prevent the existence of sensitive data within it.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.1.4", - "Description": "Limit access to deployment configurations", + "Description": "Restrict access to the deployment configuration to trusted and qualified users only.", "Checks": [], "Attributes": [ { - "Section": "5.1", + "Section": "5 Deployment", + "Subsection": "5.1 Deployment Configuration", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Restrict access to the deployment configuration to trusted and qualified users only.", @@ -2294,18 +2428,19 @@ "RemediationProcedure": "Restrict access to the deployment configuration to trusted and qualified users.", "AuditProcedure": "Verify each deployment configuration is accessible only to known and authorized users.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.1.5", - "Description": "Scan Infrastructure as Code (IaC)", + "Description": "Detect and prevent misconfigurations or insecure instructions in Infrastructure as Code (IaC) files, such as Terraform files.", "Checks": [], "Attributes": [ { - "Section": "5.1", + "Section": "5 Deployment", + "Subsection": "5.1 Deployment Configuration", "Profile": "Level 2", "AssessmentStatus": "Manual", "Description": "Detect and prevent misconfigurations or insecure instructions in Infrastructure as Code (IaC) files, such as Terraform files.", @@ -2314,18 +2449,19 @@ "RemediationProcedure": "For every Infrastructure as Code (IaC) instructions file, set scanners to identify and prevent misconfigurations and insecure instructions.", "AuditProcedure": "For every Infrastructure as Code (IaC) instructions file, verify that scanners are set to identify and prevent misconfigurations and insecure instructions.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.1.6", - "Description": "Ensure deployment configuration manifests are verified", + "Description": "Verify the deployment configuration manifests.", "Checks": [], "Attributes": [ { - "Section": "5.1", + "Section": "5 Deployment", + "Subsection": "5.1 Deployment Configuration", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Verify the deployment configuration manifests.", @@ -2334,18 +2470,19 @@ "RemediationProcedure": "Verify each deployment configuration manifest in use.", "AuditProcedure": "For each deployment configuration manifest in use, ensure it has been verified.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.1.7", - "Description": "Ensure deployment configuration manifests are pinned to a specific, verified version", + "Description": "Deployment configuration is often stored in a version control system and is pulled from there. Pin the configuration used to a specific, verified version or commit Secure Hash Algorithm (SHA). Avoid referring configuration without its version tag specified.", "Checks": [], "Attributes": [ { - "Section": "5.1", + "Section": "5 Deployment", + "Subsection": "5.1 Deployment Configuration", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Deployment configuration is often stored in a version control system and is pulled from there. Pin the configuration used to a specific, verified version or commit Secure Hash Algorithm (SHA). Avoid referring configuration without its version tag specified.", @@ -2354,18 +2491,19 @@ "RemediationProcedure": "For every deployment configuration manifest in use, pin to a specific version or commit Secure Hash Algorithm (SHA).", "AuditProcedure": "For every deployment configuration manifest in use, ensure it is pinned to a specific version or commit Secure Hash Algorithm (SHA).", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.2.1", - "Description": "Ensure deployments are automated", + "Description": "Automate deployments of production environment and application.", "Checks": [], "Attributes": [ { - "Section": "5.2", + "Section": "5 Deployment", + "Subsection": "5.2 Deployment Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Automate deployments of production environment and application.", @@ -2374,18 +2512,19 @@ "RemediationProcedure": "Automate each deployment process of the production environment and application.", "AuditProcedure": "For each deployment process, ensure it is automated.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.2.2", - "Description": "Ensure the deployment environment is reproducible", + "Description": "Verify that the deployment environment – the orchestrator and the production environment where the application is deployed – is reproducible. This means that the environment stays the same in each deployment if the configuration has not changed.", "Checks": [], "Attributes": [ { - "Section": "5.2", + "Section": "5 Deployment", + "Subsection": "5.2 Deployment Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Verify that the deployment environment – the orchestrator and the production environment where the application is deployed – is reproducible. This means that the environment stays the same in each deployment if the configuration has not changed.", @@ -2394,18 +2533,19 @@ "RemediationProcedure": "Adjust the process that deploys the deployment/production environment to build the same environment each time when the configuration has not changed.", "AuditProcedure": "Verify that the deployment/production environment is reproducible.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.2.3", - "Description": "Ensure access to production environment is limited", + "Description": "Restrict access to the production environment to a few trusted and qualified users only.", "Checks": [], "Attributes": [ { - "Section": "5.2", + "Section": "5 Deployment", + "Subsection": "5.2 Deployment Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Restrict access to the production environment to a few trusted and qualified users only.", @@ -2414,18 +2554,19 @@ "RemediationProcedure": "Restrict access to the production environment to trusted and qualified users.", "AuditProcedure": "Verify that the production environment is accessible only to trusted and qualified users.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] }, { "Id": "5.2.4", - "Description": "Ensure default passwords are not used", + "Description": "Do not use default passwords of deployment tools and components.", "Checks": [], "Attributes": [ { - "Section": "5.2", + "Section": "5 Deployment", + "Subsection": "5.2 Deployment Environment", "Profile": "Level 1", "AssessmentStatus": "Manual", "Description": "Do not use default passwords of deployment tools and components.", @@ -2434,8 +2575,8 @@ "RemediationProcedure": "For each deployment tool, change the password.", "AuditProcedure": "For each deployment tool, ensure the password is not the default one.", "AdditionalInformation": "", - "DefaultValue": "", - "References": "" + "References": "", + "DefaultValue": "" } ] } diff --git a/prowler/config/config.yaml b/prowler/config/config.yaml index 0613780bd2..7932131af6 100644 --- a/prowler/config/config.yaml +++ b/prowler/config/config.yaml @@ -459,6 +459,9 @@ azure: "Standard_DS3_v2", "Standard_D4s_v3", ] + # Azure VM Backup Configuration + # azure.vm_sufficient_daily_backup_retention_period + vm_backup_min_daily_retention_days: 7 # GCP Configuration gcp: diff --git a/prowler/lib/check/models.py b/prowler/lib/check/models.py index aa24834b89..1c2dd44533 100644 --- a/prowler/lib/check/models.py +++ b/prowler/lib/check/models.py @@ -550,9 +550,7 @@ class Check_Report_GCP(Check_Report): or "" ) self.resource_name = ( - resource_name - or getattr(resource, "name", "") - or getattr(resource, "id", "") + resource_name or getattr(resource, "name", "") or "GCP Project" ) self.project_id = project_id or getattr(resource, "project_id", "") self.location = ( diff --git a/prowler/lib/scan_filters/scan_filters.py b/prowler/lib/scan_filters/scan_filters.py index 6f93907fea..4801934370 100644 --- a/prowler/lib/scan_filters/scan_filters.py +++ b/prowler/lib/scan_filters/scan_filters.py @@ -8,7 +8,7 @@ def is_resource_filtered(resource: str, audit_resources: list) -> bool: Returns True if it is filtered and False if it does not match the input filters """ try: - if resource in str(audit_resources): + if resource in audit_resources: return True return False except Exception as error: diff --git a/prowler/providers/aws/aws_regions_by_service.json b/prowler/providers/aws/aws_regions_by_service.json index 1c2bbd2647..7943a1078f 100644 --- a/prowler/providers/aws/aws_regions_by_service.json +++ b/prowler/providers/aws/aws_regions_by_service.json @@ -1430,7 +1430,9 @@ "us-west-2" ], "aws-cn": [], - "aws-us-gov": [] + "aws-us-gov": [ + "us-gov-west-1" + ] } }, "bedrock-runtime": { @@ -4269,15 +4271,18 @@ "ap-southeast-1", "ap-southeast-2", "ap-southeast-3", + "ap-southeast-4", "ca-central-1", "ca-west-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", @@ -5705,12 +5710,15 @@ "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-5", "ca-central-1", "eu-central-1", "eu-north-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", + "me-central-1", "me-south-1", "sa-east-1", "us-east-1", @@ -5916,9 +5924,11 @@ "ap-south-1", "ap-southeast-1", "ap-southeast-2", + "ap-southeast-5", "ca-central-1", "eu-central-1", "eu-north-1", + "eu-south-2", "eu-west-1", "eu-west-2", "eu-west-3", @@ -6580,6 +6590,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -6673,6 +6684,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -8260,6 +8272,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", @@ -9914,6 +9927,7 @@ "aws": [ "af-south-1", "ap-east-1", + "ap-east-2", "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", diff --git a/prowler/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/__init__.py b/prowler/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/eks_cluster_deletion_protection_enabled.metadata.json b/prowler/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/eks_cluster_deletion_protection_enabled.metadata.json new file mode 100644 index 0000000000..5a0ec82bb7 --- /dev/null +++ b/prowler/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/eks_cluster_deletion_protection_enabled.metadata.json @@ -0,0 +1,32 @@ +{ + "Provider": "aws", + "CheckID": "eks_cluster_deletion_protection_enabled", + "CheckTitle": "Ensure EKS clusters have deletion protection enabled", + "CheckType": [ + "Software and Configuration Checks/AWS Security Best Practices/Resource Management" + ], + "ServiceName": "eks", + "SubServiceName": "", + "ResourceIdTemplate": "arn:partition:service:region:account-id:resource-id", + "Severity": "high", + "ResourceType": "AwsEksCluster", + "Description": "Ensure that your Amazon EKS clusters have deletion protection enabled to prevent accidental deletion of critical Kubernetes clusters.", + "Risk": "Without deletion protection, EKS clusters can be accidentally deleted through Terraform automation, AWS CLI commands, or the AWS console, leading to data loss and service disruption.", + "RelatedUrl": "https://docs.aws.amazon.com/eks/latest/userguide/deletion-protection.html", + "Remediation": { + "Code": { + "CLI": "aws eks update-cluster-config --region --name --deletion-protection", + "NativeIaC": "", + "Other": "", + "Terraform": "resource \"aws_eks_cluster\" \"example\" {\n name = \"example-cluster\"\n role_arn = aws_iam_role.example.arn\n deletion_protection = true\n # ... other configuration\n}" + }, + "Recommendation": { + "Text": "Enable deletion protection on all EKS clusters to prevent accidental deletion. This is especially important for production clusters and those managed through Infrastructure as Code (IaC) tools.", + "Url": "https://docs.aws.amazon.com/eks/latest/userguide/deletion-protection.html" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/eks_cluster_deletion_protection_enabled.py b/prowler/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/eks_cluster_deletion_protection_enabled.py new file mode 100644 index 0000000000..4f57c25524 --- /dev/null +++ b/prowler/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/eks_cluster_deletion_protection_enabled.py @@ -0,0 +1,21 @@ +from prowler.lib.check.models import Check, Check_Report_AWS +from prowler.providers.aws.services.eks.eks_client import eks_client + + +class eks_cluster_deletion_protection_enabled(Check): + def execute(self): + findings = [] + for cluster in eks_client.clusters: + report = Check_Report_AWS(metadata=self.metadata(), resource=cluster) + report.status = "PASS" + report.status_extended = ( + f"EKS cluster {cluster.name} has deletion protection enabled." + ) + if cluster.deletion_protection is False: + report.status = "FAIL" + report.status_extended = ( + f"EKS cluster {cluster.name} has deletion protection disabled." + ) + findings.append(report) + + return findings diff --git a/prowler/providers/aws/services/eks/eks_service.py b/prowler/providers/aws/services/eks/eks_service.py index e7b3ec389c..b0ccace49b 100644 --- a/prowler/providers/aws/services/eks/eks_service.py +++ b/prowler/providers/aws/services/eks/eks_service.py @@ -83,6 +83,10 @@ class EKS(AWSService): ]["publicAccessCidrs"] if "encryptionConfig" in describe_cluster["cluster"]: cluster.encryptionConfig = True + if "deletionProtection" in describe_cluster["cluster"]: + cluster.deletion_protection = describe_cluster["cluster"][ + "deletionProtection" + ] cluster.tags = [describe_cluster["cluster"].get("tags")] cluster.version = describe_cluster["cluster"].get("version", "") @@ -108,4 +112,5 @@ class EKSCluster(BaseModel): endpoint_private_access: bool = None public_access_cidrs: list[str] = [] encryptionConfig: bool = None + deletion_protection: bool = None tags: Optional[list] = [] diff --git a/prowler/providers/aws/services/iam/lib/privilege_escalation.py b/prowler/providers/aws/services/iam/lib/privilege_escalation.py index 76245ce2c9..9a4a67d3ab 100644 --- a/prowler/providers/aws/services/iam/lib/privilege_escalation.py +++ b/prowler/providers/aws/services/iam/lib/privilege_escalation.py @@ -24,7 +24,6 @@ privilege_escalation_policies_combination = { "IAMPut": {"iam:Put*"}, "CreatePolicyVersion": {"iam:CreatePolicyVersion"}, "SetDefaultPolicyVersion": {"iam:SetDefaultPolicyVersion"}, - "iam:PassRole": {"iam:PassRole"}, "PassRole+EC2": { "iam:PassRole", "ec2:RunInstances", @@ -69,6 +68,21 @@ privilege_escalation_policies_combination = { }, "GlueUpdateDevEndpoint": {"glue:UpdateDevEndpoint"}, "lambda:UpdateFunctionCode": {"lambda:UpdateFunctionCode"}, + "lambda:UpdateFunctionConfiguration": {"lambda:UpdateFunctionConfiguration"}, + "PassRole+CodeStar": { + "iam:PassRole", + "codestar:CreateProject", + }, + "PassRole+CreateAutoScaling": { + "iam:PassRole", + "autoscaling:CreateAutoScalingGroup", + "autoscaling:CreateLaunchConfiguration", + }, + "PassRole+UpdateAutoScaling": { + "iam:PassRole", + "autoscaling:UpdateAutoScalingGroup", + "autoscaling:CreateLaunchConfiguration", + }, "iam:CreateAccessKey": {"iam:CreateAccessKey"}, "iam:CreateLoginProfile": {"iam:CreateLoginProfile"}, "iam:UpdateLoginProfile": {"iam:UpdateLoginProfile"}, @@ -86,6 +100,12 @@ privilege_escalation_policies_combination = { "sts:AssumeRole", "iam:UpdateAssumeRolePolicy", }, + # AgentCore privilege escalation patterns + "PassRole+AgentCoreCreateInterpreter+InvokeInterpreter": { + "iam:PassRole", + "bedrock-agentcore:CreateCodeInterpreter", + "bedrock-agentcore:InvokeCodeInterpreter", + }, # TO-DO: We have to handle AssumeRole just if the resource is * and without conditions # "sts:AssumeRole": {"sts:AssumeRole"}, } diff --git a/prowler/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public.py b/prowler/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public.py index 597839978d..426ff04490 100644 --- a/prowler/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public.py +++ b/prowler/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public.py @@ -10,13 +10,13 @@ class kafka_cluster_is_public(Check): report = Check_Report_AWS(metadata=self.metadata(), resource=cluster) report.status = "FAIL" report.status_extended = ( - f"Kafka cluster '{cluster.name}' is publicly accessible." + f"Kafka cluster {cluster.name} is publicly accessible." ) - if cluster.public_access: + if not cluster.public_access: report.status = "PASS" report.status_extended = ( - f"Kafka cluster '{cluster.name}' is not publicly accessible." + f"Kafka cluster {cluster.name} is not publicly accessible." ) findings.append(report) diff --git a/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py b/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py index ea70bb708a..137ec3c494 100644 --- a/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py +++ b/prowler/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled.py @@ -22,6 +22,10 @@ class app_http_logs_enabled(Check): report.status = "PASS" report.status_extended = f"App {app.name} has HTTP Logs enabled in diagnostic setting {diagnostic_setting.name} in subscription {subscription_name}" break + elif log.category_group == "allLogs" and log.enabled: + report.status = "PASS" + report.status_extended = f"App {app.name} has allLogs category group which includes HTTP Logs enabled in diagnostic setting {diagnostic_setting.name} in subscription {subscription_name}" + break findings.append(report) return findings diff --git a/prowler/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact.py b/prowler/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact.py index 8c69f4437a..06cb1f06d2 100644 --- a/prowler/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact.py +++ b/prowler/providers/azure/services/defender/defender_additional_email_configured_with_a_security_contact/defender_additional_email_configured_with_a_security_contact.py @@ -14,6 +14,11 @@ class defender_additional_email_configured_with_a_security_contact(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=contact_configuration ) + report.resource_name = ( + contact_configuration.name + if contact_configuration.name + else "Security Contact" + ) report.subscription = subscription_name if len(contact_configuration.emails) > 0: diff --git a/prowler/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured.py b/prowler/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured.py index aeacaacac4..8a9935457d 100644 --- a/prowler/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured.py +++ b/prowler/providers/azure/services/defender/defender_attack_path_notifications_properly_configured/defender_attack_path_notifications_properly_configured.py @@ -31,6 +31,11 @@ class defender_attack_path_notifications_properly_configured(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=contact_configuration ) + report.resource_name = ( + contact_configuration.name + if contact_configuration.name + else "Security Contact" + ) report.subscription = subscription_name actual_risk_level = getattr( contact_configuration, "attack_path_minimal_risk_level", None diff --git a/prowler/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high.py b/prowler/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high.py index e0ad128e05..d01fec8966 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high.py +++ b/prowler/providers/azure/services/defender/defender_ensure_notify_alerts_severity_is_high/defender_ensure_notify_alerts_severity_is_high.py @@ -14,6 +14,11 @@ class defender_ensure_notify_alerts_severity_is_high(Check): report = Check_Report_Azure( metadata=self.metadata(), resource=contact_configuration ) + report.resource_name = ( + contact_configuration.name + if contact_configuration.name + else "Security Contact" + ) report.subscription = subscription_name report.status = "FAIL" report.status_extended = f"Notifications are not enabled for alerts with a minimum severity of high or lower in subscription {subscription_name}." diff --git a/prowler/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners.py b/prowler/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners.py index ce041464b4..ed16c609c3 100644 --- a/prowler/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners.py +++ b/prowler/providers/azure/services/defender/defender_ensure_notify_emails_to_owners/defender_ensure_notify_emails_to_owners.py @@ -12,7 +12,13 @@ class defender_ensure_notify_emails_to_owners(Check): ) in defender_client.security_contact_configurations.items(): for contact_configuration in security_contact_configurations.values(): report = Check_Report_Azure( - metadata=self.metadata(), resource=contact_configuration + metadata=self.metadata(), + resource=contact_configuration, + ) + report.resource_name = ( + contact_configuration.name + if contact_configuration.name + else "Security Contact" ) report.subscription = subscription_name if ( diff --git a/prowler/providers/azure/services/defender/defender_service.py b/prowler/providers/azure/services/defender/defender_service.py index f39dfdf484..500441cfb9 100644 --- a/prowler/providers/azure/services/defender/defender_service.py +++ b/prowler/providers/azure/services/defender/defender_service.py @@ -25,6 +25,7 @@ class Defender(AzureService): ).token ) self.iot_security_solutions = self._get_iot_security_solutions() + self.jit_policies = self._get_jit_policies() def _get_pricings(self): logger.info("Defender - Getting pricings...") @@ -246,6 +247,44 @@ class Defender(AzureService): ) return iot_security_solutions + def _get_jit_policies(self) -> dict[str, dict]: + """ + Get all JIT policies for all subscriptions. + + Returns: + A dictionary of JIT policies for each subscription. The format will be: + { + "subscription_name": { + "jit_policy_id": JITPolicy + } + } + """ + logger.info("Defender - Getting JIT policies...") + jit_policies = {} + for subscription_name, client in self.clients.items(): + try: + jit_policies[subscription_name] = {} + policies = client.jit_network_access_policies.list() + for policy in policies: + vm_ids = set() + for vm in getattr(policy, "virtual_machines", []): + vm_ids.add(vm.id) + jit_policies[subscription_name].update( + { + policy.id: JITPolicy( + id=policy.id, + name=policy.name, + location=getattr(policy, "location", "Global"), + vm_ids=vm_ids, + ), + } + ) + except Exception as error: + logger.error( + f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return jit_policies + class Pricing(BaseModel): resource_id: str @@ -317,3 +356,10 @@ class IoTSecuritySolution(BaseModel): resource_id: str name: str status: str + + +class JITPolicy(BaseModel): + id: str + name: str + location: str = "" + vm_ids: list[str] = [] diff --git a/prowler/providers/azure/services/recovery/recovery_service.py b/prowler/providers/azure/services/recovery/recovery_service.py index e39a6fc1f7..efc7630bf7 100644 --- a/prowler/providers/azure/services/recovery/recovery_service.py +++ b/prowler/providers/azure/services/recovery/recovery_service.py @@ -11,20 +11,30 @@ from prowler.providers.azure.lib.service.service import AzureService class BackupItem(BaseModel): - """Minimal BackupItem: only essential identifying and descriptive fields.""" + """Model that represents a backup item.""" id: str name: str workload_type: Optional[DataSourceType] + backup_policy_id: Optional[str] = None + + +class BackupPolicy(BaseModel): + """Model that represents a backup policy.""" + + id: str + name: str + retention_days: Optional[int] = None class BackupVault(BaseModel): - """Minimal BackupVault: only essential identifying fields and its backup items.""" + """Model that represents a backup vault.""" id: str name: str location: str backup_protected_items: dict[str, BackupItem] = Field(default_factory=dict) + backup_policies: dict[str, BackupPolicy] = Field(default_factory=dict) class Recovery(AzureService): @@ -71,6 +81,9 @@ class RecoveryBackup(AzureService): vault.backup_protected_items = self._get_backup_protected_items( subscription_name=subscription_name, vault=vault ) + vault.backup_policies = self._get_backup_policies( + subscription_name=subscription_name, vault=vault + ) def _get_backup_protected_items( self, subscription_name: str, vault: BackupVault @@ -95,7 +108,58 @@ class RecoveryBackup(AzureService): workload_type=( item_properties.workload_type if item_properties else None ), + backup_policy_id=( + item_properties.policy_id if item_properties else None + ), ) - except Exception as e: - logger.error(f"Recovery - Error getting backup protected items: {e}") + except Exception as error: + logger.error( + f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) return backup_protected_items_dict + + def _get_backup_policies( + self, subscription_name: str, vault: BackupVault + ) -> dict[str, BackupPolicy]: + """ + Retrieve all backup policies for a given vault. + """ + logger.info("Recovery - Getting backup policies...") + backup_policies_dict: dict[str, BackupPolicy] = {} + unique_backup_policies: set[str] = set() + try: + for item in vault.backup_protected_items.values(): + if item.backup_policy_id: + unique_backup_policies.add(item.backup_policy_id) + for policy_id in unique_backup_policies: + policy = self.clients[subscription_name].protection_policies.get( + vault_name=vault.name, + resource_group_name=vault.id.split("/")[4], + policy_name=policy_id.split("/")[-1], + ) + backup_policies_dict[policy_id] = BackupPolicy( + id=policy.id, + name=policy.name, + retention_days=getattr( + getattr( + getattr( + getattr( + getattr(policy, "properties", None), + "retention_policy", + None, + ), + "daily_schedule", + None, + ), + "retention_duration", + None, + ), + "count", + None, + ), + ) + except Exception as error: + logger.error( + f"Subscription name: {subscription_name} -- {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return backup_policies_dict diff --git a/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py b/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py index 971b440b0e..ee2d49e53d 100644 --- a/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py +++ b/prowler/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled.py @@ -1,6 +1,5 @@ from prowler.lib.check.models import Check, Check_Report_Azure from prowler.providers.azure.services.storage.storage_client import storage_client -from prowler.providers.azure.services.storage.storage_service import ReplicationSettings class storage_geo_redundant_enabled(Check): @@ -27,14 +26,16 @@ class storage_geo_redundant_enabled(Check): report.subscription = subscription if ( - storage_account.replication_settings - == ReplicationSettings.STANDARD_GRS + storage_account.replication_settings == "Standard_GRS" + or storage_account.replication_settings == "Standard_GZRS" + or storage_account.replication_settings == "Standard_RAGRS" + or storage_account.replication_settings == "Standard_RAGZRS" ): report.status = "PASS" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has Geo-redundant storage (GRS) enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} has Geo-redundant storage {storage_account.replication_settings} enabled." else: report.status = "FAIL" - report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} does not have Geo-redundant storage (GRS) enabled." + report.status_extended = f"Storage account {storage_account.name} from subscription {subscription} does not have Geo-redundant storage enabled, it has {storage_account.replication_settings} instead." findings.append(report) diff --git a/prowler/providers/azure/services/storage/storage_service.py b/prowler/providers/azure/services/storage/storage_service.py index e93b920380..cd5cd07d98 100644 --- a/prowler/providers/azure/services/storage/storage_service.py +++ b/prowler/providers/azure/services/storage/storage_service.py @@ -1,4 +1,3 @@ -from enum import Enum from typing import Optional from azure.mgmt.storage import StorageManagementClient @@ -35,7 +34,6 @@ class Storage(AzureService): key_expiration_period_in_days = int( storage_account.key_policy.key_expiration_period_in_days ) - replication_settings = ReplicationSettings(storage_account.sku.name) storage_accounts[subscription].append( Account( id=storage_account.id, @@ -84,7 +82,7 @@ class Storage(AzureService): False, ) ), - replication_settings=replication_settings, + replication_settings=storage_account.sku.name, allow_cross_tenant_replication=( True if getattr( @@ -273,17 +271,6 @@ class PrivateEndpointConnection(BaseModel): type: str -class ReplicationSettings(Enum): - STANDARD_LRS = "Standard_LRS" - STANDARD_GRS = "Standard_GRS" - STANDARD_RAGRS = "Standard_RAGRS" - STANDARD_ZRS = "Standard_ZRS" - PREMIUM_LRS = "Premium_LRS" - PREMIUM_ZRS = "Premium_ZRS" - STANDARD_GZRS = "Standard_GZRS" - STANDARD_RAGZRS = "Standard_RAGZRS" - - class SMBProtocolSettings(BaseModel): channel_encryption: list[str] supported_versions: list[str] @@ -310,7 +297,7 @@ class Account(BaseModel): minimum_tls_version: str private_endpoint_connections: list[PrivateEndpointConnection] key_expiration_period_in_days: Optional[int] = None - replication_settings: ReplicationSettings = ReplicationSettings.STANDARD_LRS + replication_settings: str = "Standard_LRS" allow_cross_tenant_replication: bool = True allow_shared_key_access: bool = True blob_properties: Optional[BlobProperties] = None diff --git a/prowler/providers/azure/services/vm/vm_jit_access_enabled/__init__.py b/prowler/providers/azure/services/vm/vm_jit_access_enabled/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.metadata.json b/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.metadata.json new file mode 100644 index 0000000000..df9f7d4d10 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "vm_jit_access_enabled", + "CheckTitle": "Enable Just-In-Time Access for Virtual Machines", + "CheckType": [], + "ServiceName": "vm", + "SubServiceName": "", + "ResourceIdTemplate": "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}", + "Severity": "high", + "ResourceType": "Microsoft.Compute/virtualMachines", + "Description": "Ensure that Microsoft Azure virtual machines are configured to use Just-in-Time (JIT) access.", + "Risk": "Without JIT access, management ports such as 22 (SSH) and 3389 (RDP) may be exposed, increasing the risk of brute-force and DDoS attacks.", + "RelatedUrl": "https://docs.microsoft.com/en-us/azure/security-center/security-center-just-in-time?tabs=jit-config-asc%2Cjit-request-asc", + "Remediation": { + "Code": { + "CLI": "az security jit-policy list --query '[*].virtualMachines[*].id | []'", + "NativeIaC": "", + "Other": "", + "Terraform": "" + }, + "Recommendation": { + "Text": "Enable Just-in-Time (JIT) network access for your Microsoft Azure virtual machines using the Azure Portal under Security Center > Just-in-time VM access.", + "Url": "https://docs.microsoft.com/en-us/azure/security-center/security-center-just-in-time?tabs=jit-config-asc%2Cjit-request-asc" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "JIT access can only be enabled via the Azure Portal. Ensure Security Center standard pricing tier for servers is enabled." +} diff --git a/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.py b/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.py new file mode 100644 index 0000000000..9434608454 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled.py @@ -0,0 +1,33 @@ +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.defender.defender_client import defender_client +from prowler.providers.azure.services.vm.vm_client import vm_client + + +class vm_jit_access_enabled(Check): + """ + Ensure that Microsoft Azure virtual machines are configured to use Just-in-Time (JIT) access. + + This check evaluates whether JIT access is enabled for each VM to reduce the attack surface. + - PASS: VM has JIT access enabled. + - FAIL: VM does not have JIT access enabled. + """ + + def execute(self): + findings = [] + jit_enabled_vms = set() + for subscription_name, vms in vm_client.virtual_machines.items(): + for jit_policy in defender_client.jit_policies[subscription_name].values(): + jit_enabled_vms.update(jit_policy.vm_ids) + for vm in vms.values(): + report = Check_Report_Azure(metadata=self.metadata(), resource=vm) + report.subscription = subscription_name + if vm.resource_id.lower() in { + vm_id.lower() for vm_id in jit_enabled_vms + }: + report.status = "PASS" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} has JIT (Just-in-Time) access enabled." + else: + report.status = "FAIL" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription_name} does not have JIT (Just-in-Time) access enabled." + findings.append(report) + return findings diff --git a/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/__init__.py b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.metadata.json b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.metadata.json new file mode 100644 index 0000000000..60b498ba17 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.metadata.json @@ -0,0 +1,30 @@ +{ + "Provider": "azure", + "CheckID": "vm_sufficient_daily_backup_retention_period", + "CheckTitle": "Ensure there is a sufficient daily backup retention period configured for Azure virtual machines.", + "CheckType": [], + "ServiceName": "vm", + "SubServiceName": "", + "ResourceIdTemplate": "", + "Severity": "medium", + "ResourceType": "Microsoft.Compute/virtualMachines", + "Description": "Ensure there is a sufficient daily backup retention period configured for Azure virtual machines.", + "Risk": "Having an optimal daily backup retention period for your Azure virtual machines will enforce your backup strategy to follow the best practices as specified in the compliance regulations promoted by your organization. Retaining VM backups for a longer period of time will allow you to handle more efficiently your data restoration process in the event of a failure.", + "RelatedUrl": "https://docs.microsoft.com/en-us/azure/backup/backup-azure-vms-introduction", + "Remediation": { + "Code": { + "CLI": "", + "NativeIaC": "", + "Other": "https://www.trendmicro.com/cloudoneconformity/knowledge-base/azure/VirtualMachines/sufficient-backup-retention-period.html", + "Terraform": "" + }, + "Recommendation": { + "Text": "Set the daily backup retention period for each VM's backup policy to meet or exceed your organization's minimum requirement.", + "Url": "https://docs.microsoft.com/en-us/azure/backup/backup-azure-vms-introduction" + } + }, + "Categories": [], + "DependsOn": [], + "RelatedTo": [], + "Notes": "" +} diff --git a/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py new file mode 100644 index 0000000000..221df85351 --- /dev/null +++ b/prowler/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period.py @@ -0,0 +1,51 @@ +from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType + +from prowler.lib.check.models import Check, Check_Report_Azure +from prowler.providers.azure.services.recovery.recovery_client import recovery_client +from prowler.providers.azure.services.vm.vm_client import vm_client + + +class vm_sufficient_daily_backup_retention_period(Check): + """ + Ensure there is a sufficient daily backup retention period configured for Azure virtual machines. + - PASS: The VM has a backup policy with sufficient daily retention period. + - FAIL: The VM does not have a backup policy or the retention period is insufficient. + """ + + def execute(self) -> list[Check_Report_Azure]: + findings = [] + min_retention_days = getattr(vm_client, "audit_config", {}).get( + "vm_backup_min_daily_retention_days", 7 + ) + + for subscription, vms in vm_client.virtual_machines.items(): + vaults = recovery_client.vaults.get(subscription, {}) + for vm in vms.values(): + backup_found = False + retention_days = None + for vault in vaults.values(): + for backup_item in vault.backup_protected_items.values(): + if ( + backup_item.workload_type == DataSourceType.VM + and backup_item.name.split(";")[-1] == vm.resource_name + ): + backup_found = True + policy_id = backup_item.backup_policy_id + if policy_id and policy_id in vault.backup_policies: + retention_days = vault.backup_policies[ + policy_id + ].retention_days + break + if backup_found: + break + if backup_found and retention_days: + report = Check_Report_Azure(metadata=self.metadata(), resource=vm) + report.subscription = subscription + if retention_days >= min_retention_days: + report.status = "PASS" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription} has a daily backup retention period of {retention_days} days (minimum required: {min_retention_days})." + else: + report.status = "FAIL" + report.status_extended = f"VM {vm.resource_name} in subscription {subscription} has insufficient daily backup retention period of {retention_days} days (minimum required: {min_retention_days})." + findings.append(report) + return findings diff --git a/prowler/providers/common/provider.py b/prowler/providers/common/provider.py index f0e233017b..5f3ee07160 100644 --- a/prowler/providers/common/provider.py +++ b/prowler/providers/common/provider.py @@ -222,6 +222,8 @@ class Provider(ABC): env_auth=arguments.env_auth, az_cli_auth=arguments.az_cli_auth, browser_auth=arguments.browser_auth, + certificate_auth=arguments.certificate_auth, + certificate_path=arguments.certificate_path, tenant_id=arguments.tenant_id, init_modules=arguments.init_modules, fixer_config=fixer_config, diff --git a/prowler/providers/gcp/gcp_provider.py b/prowler/providers/gcp/gcp_provider.py index 55351b7db3..7cdbba4401 100644 --- a/prowler/providers/gcp/gcp_provider.py +++ b/prowler/providers/gcp/gcp_provider.py @@ -659,6 +659,9 @@ class GcpProvider(Provider): if asset["resource"]["data"].get("name") else project_id ) + # Handle empty or null project names + if not project_name or project_name.strip() == "": + project_name = "GCP Project" gcp_project = GCPProject( number=project_number, id=project_id, @@ -717,6 +720,9 @@ class GcpProvider(Provider): if project.get("name") else project_id ) + # Handle empty or null project names + if not project_name or project_name.strip() == "": + project_name = "GCP Project" project_id = project["projectId"] gcp_project = GCPProject( number=project_number, @@ -757,9 +763,15 @@ class GcpProvider(Provider): # If no projects were able to be accessed via API, add them manually if provided by the user in arguments if project_ids: for input_project in project_ids: + # Handle empty or null project names + project_name = ( + input_project + if input_project and input_project.strip() != "" + else "GCP Project" + ) projects[input_project] = GCPProject( id=input_project, - name=input_project, + name=project_name, number=0, labels={}, lifecycle_state="ACTIVE", @@ -768,9 +780,15 @@ class GcpProvider(Provider): elif credentials_file: with open(credentials_file, "r", encoding="utf-8") as file: project_id = json.load(file)["project_id"] + # Handle empty or null project names + project_name = ( + project_id + if project_id and project_id.strip() != "" + else "GCP Project" + ) projects[project_id] = GCPProject( id=project_id, - name=project_id, + name=project_name, number=0, labels={}, lifecycle_state="ACTIVE", diff --git a/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.py b/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.py index c10c5e1088..b26841260d 100644 --- a/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.py +++ b/prowler/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level.py @@ -13,7 +13,7 @@ class iam_no_service_roles_at_project_level(Check): metadata=self.metadata(), resource=binding, resource_id=binding.role, - resource_name=binding.role, + resource_name=binding.role if binding.role else "Service Role", location=cloudresourcemanager_client.region, ) if binding.role in [ @@ -31,7 +31,6 @@ class iam_no_service_roles_at_project_level(Check): metadata=self.metadata(), resource=cloudresourcemanager_client.projects[project], project_id=project, - resource_name=project, location=cloudresourcemanager_client.region, ) report.status = "PASS" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.py index 394f6db170..fd48f9dd4b 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled.py @@ -20,6 +20,7 @@ class logging_log_metric_filter_and_alert_for_audit_configuration_changes_enable metadata=self.metadata(), resource=metric, location=logging_client.region, + resource_name=metric.name if metric.name else "Log Metric Filter", ) projects_with_metric.add(metric.project_id) report.status = "FAIL" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.py index ee854778f1..db9ea7e95e 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled.py @@ -18,6 +18,7 @@ class logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled( metadata=self.metadata(), resource=metric, location=logging_client.region, + resource_name=metric.name if metric.name else "Log Metric Filter", ) projects_with_metric.add(metric.project_id) report.status = "FAIL" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.py index b579a776d9..13a2d45737 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled.py @@ -18,6 +18,7 @@ class logging_log_metric_filter_and_alert_for_custom_role_changes_enabled(Check) metadata=self.metadata(), resource=metric, location=logging_client.region, + resource_name=metric.name if metric.name else "Log Metric Filter", ) projects_with_metric.add(metric.project_id) report.status = "FAIL" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.py index 840f8f0af5..e56feb3318 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled.py @@ -18,6 +18,7 @@ class logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled( metadata=self.metadata(), resource=metric, location=logging_client.region, + resource_name=metric.name if metric.name else "Log Metric Filter", ) projects_with_metric.add(metric.project_id) report.status = "FAIL" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.py index 1d9b84f2c6..5e37003409 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled.py @@ -17,6 +17,7 @@ class logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes metadata=self.metadata(), resource=metric, location=logging_client.region, + resource_name=metric.name if metric.name else "Log Metric Filter", ) projects_with_metric.add(metric.project_id) report.status = "FAIL" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.py index d05b755838..754f651d4d 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled.py @@ -18,6 +18,7 @@ class logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled( metadata=self.metadata(), resource=metric, location=logging_client.region, + resource_name=metric.name if metric.name else "Log Metric Filter", ) projects_with_metric.add(metric.project_id) report.status = "FAIL" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.py index 1985e73103..5073b59879 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled.py @@ -18,6 +18,7 @@ class logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled(Check) metadata=self.metadata(), resource=metric, location=logging_client.region, + resource_name=metric.name if metric.name else "Log Metric Filter", ) projects_with_metric.add(metric.project_id) report.status = "FAIL" diff --git a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.py b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.py index aa518eb077..9b78bc314a 100644 --- a/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.py +++ b/prowler/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled.py @@ -18,6 +18,7 @@ class logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled( metadata=self.metadata(), resource=metric, location=logging_client.region, + resource_name=metric.name if metric.name else "Log Metric Filter", ) projects_with_metric.add(metric.project_id) report.status = "FAIL" diff --git a/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.py b/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.py index d180a211ca..a72d45e07e 100644 --- a/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.py +++ b/prowler/providers/gcp/services/logging/logging_sink_created/logging_sink_created.py @@ -26,6 +26,11 @@ class logging_sink_created(Check): metadata=self.metadata(), resource=projects_with_logging_sink[project], location=logging_client.region, + resource_name=( + projects_with_logging_sink[project].name + if projects_with_logging_sink[project].name + else "Logging Sink" + ), ) report.status = "PASS" report.status_extended = f"Sink {projects_with_logging_sink[project].name} is enabled exporting copies of all the log entries in project {project}." diff --git a/prowler/providers/github/github_provider.py b/prowler/providers/github/github_provider.py index d338933f20..5b86e9c76a 100644 --- a/prowler/providers/github/github_provider.py +++ b/prowler/providers/github/github_provider.py @@ -133,6 +133,12 @@ class GithubProvider(Provider): """ logger.info("Instantiating GitHub Provider...") + # Mute GitHub library logs to reduce noise since it is already handled by the Prowler logger + import logging + + logging.getLogger("github").setLevel(logging.CRITICAL) + logging.getLogger("github.GithubRetry").setLevel(logging.CRITICAL) + # Set repositories and organizations for scoping self._repositories = repositories or [] self._organizations = organizations or [] @@ -359,8 +365,16 @@ class GithubProvider(Provider): elif session.id != 0 and session.key: auth = Auth.AppAuth(session.id, session.key) gi = GithubIntegration(auth=auth, retry=retry_config) + installations = [] + for installation in gi.get_installations(): + installations.append( + installation.raw_data.get("account", {}).get("login") + ) try: - identity = GithubAppIdentityInfo(app_id=gi.get_app().id) + identity = GithubAppIdentityInfo( + app_id=gi.get_app().id, + installations=installations, + ) return identity except Exception as error: diff --git a/prowler/providers/github/models.py b/prowler/providers/github/models.py index b133dc9a69..bc1c382bc2 100644 --- a/prowler/providers/github/models.py +++ b/prowler/providers/github/models.py @@ -18,6 +18,7 @@ class GithubIdentityInfo(BaseModel): class GithubAppIdentityInfo(BaseModel): app_id: str + installations: list[str] class GithubOutputOptions(ProviderOutputOptions): diff --git a/prowler/providers/github/services/repository/repository_service.py b/prowler/providers/github/services/repository/repository_service.py index dbec869cb2..501bd72601 100644 --- a/prowler/providers/github/services/repository/repository_service.py +++ b/prowler/providers/github/services/repository/repository_service.py @@ -2,10 +2,12 @@ from datetime import datetime from typing import Optional import github +import requests from pydantic.v1 import BaseModel from prowler.lib.logger import logger from prowler.providers.github.lib.service.service import GithubService +from prowler.providers.github.models import GithubAppIdentityInfo class Repository(GithubService): @@ -50,31 +52,74 @@ class Repository(GithubService): return True + def _get_accessible_repos_graphql(self) -> list[str]: + """ + Use the GitHub GraphQL API to list all repositories that the authentication token has access to. + This works with high-granularity (fine-grained) PATs. + """ + graphql_url = "https://api.github.com/graphql" + token = self.provider.session.token + headers = { + "Authorization": f"bearer {token}", + "Content-Type": "application/json", + } + query = """ + { + viewer { + repositories(first: 100, affiliations: [OWNER, ORGANIZATION_MEMBER]) { + nodes { + nameWithOwner + } + } + } + } + """ + + try: + response = requests.post( + graphql_url, json={"query": query}, headers=headers + ) + response.raise_for_status() + data = response.json() + + if "errors" in data: + logger.error(f"Error in GraphQL query: {data['errors']}") + return [] + + repo_nodes = ( + data.get("data", {}) + .get("viewer", {}) + .get("repositories", {}) + .get("nodes", []) + ) + return [repo["nameWithOwner"] for repo in repo_nodes] + + except requests.exceptions.RequestException as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + return [] + def _list_repositories(self): """ List repositories based on provider scoping configuration. - - Scoping behavior: - - No scoping: Returns all accessible repositories for authenticated user - - Repository scoping: Returns only specified repositories - Example: --repository owner1/repo1 owner2/repo2 - - Organization scoping: Returns all repositories from specified organizations - Example: --organization org1 org2 - - Combined scoping: Returns specified repositories + all repos from organizations - Example: --repository owner1/repo1 --organization org2 - - Returns: - dict: Dictionary of repository ID to Repo objects - - Raises: - github.GithubException: When GitHub API access fails - github.RateLimitExceededException: When API rate limits are exceeded + If the provider is a GitHub App, it will list repositories in the organizations that the GitHub App is installed in. + If the provider is a user, it will list repositories where the user is a member or owner. + If input repositories are provided, it will list repositories that match the input repositories. + If input organizations are provided, it will list repositories in the organizations that match the input organizations. """ logger.info("Repository - Listing Repositories...") repos = {} try: for client in self.clients: - if self.provider.repositories or self.provider.organizations: + if ( + self.provider.repositories + or self.provider.organizations + or ( + isinstance(self.provider.identity, GithubAppIdentityInfo) + and self.provider.identity.installations + ) + ): if self.provider.repositories: logger.info( f"Filtering for specific repositories: {self.provider.repositories}" @@ -108,12 +153,57 @@ class Repository(GithubService): self._handle_github_api_error( error, "processing organization", org_name ) + if ( + isinstance(self.provider.identity, GithubAppIdentityInfo) + and self.provider.identity.installations + ): + logger.info( + f"Filtering for repositories in the organizations or accounts that the GitHub App is installed in: {', '.join(self.provider.identity.installations)}" + ) + for org_name in self.provider.identity.installations: + try: + repos_list, _ = self._get_repositories_from_owner( + client, org_name + ) + for repo in repos_list: + self._process_repository(repo, repos) + except Exception as error: + self._handle_github_api_error( + error, "processing organization", org_name + ) else: - for repo in client.get_user().get_repos(): - self._process_repository(repo, repos) + logger.info( + "No repository or organization specified, discovering accessible repositories via GraphQL API..." + ) + accessible_repo_names = self._get_accessible_repos_graphql() + + if not accessible_repo_names: + logger.warning( + "Could not find any accessible repositories with the provided token." + ) + + for repo_name in accessible_repo_names: + try: + repo = client.get_repo(repo_name) + logger.info( + f"Processing repository found via GraphQL: {repo.full_name}" + ) + self._process_repository(repo, repos) + except Exception as error: + if hasattr(self, "_handle_github_api_error"): + self._handle_github_api_error( + error, + "accessing repository discovered via GraphQL", + repo_name, + ) + else: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + except github.RateLimitExceededException as error: logger.error(f"GitHub API rate limit exceeded: {error}") - raise # Re-raise rate limit errors as they need special handling + raise except github.GithubException as error: logger.error(f"GitHub API error while listing repositories: {error}") except Exception as error: @@ -124,158 +214,167 @@ class Repository(GithubService): def _process_repository(self, repo, repos): """Process a single repository and extract all its information.""" - default_branch = repo.default_branch - securitymd_exists = self._file_exists(repo, "SECURITY.md") - # CODEOWNERS file can be in .github/, root, or docs/ - # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location - codeowners_paths = [ - ".github/CODEOWNERS", - "CODEOWNERS", - "docs/CODEOWNERS", - ] - codeowners_files = [self._file_exists(repo, path) for path in codeowners_paths] - if True in codeowners_files: - codeowners_exists = True - elif all(file is None for file in codeowners_files): - codeowners_exists = None - else: - codeowners_exists = False - delete_branch_on_merge = ( - repo.delete_branch_on_merge - if repo.delete_branch_on_merge is not None - else False - ) - - require_pr = False - approval_cnt = 0 - branch_protection = False - required_linear_history = False - allow_force_pushes = True - branch_deletion = True - require_code_owner_reviews = False - require_signed_commits = False - status_checks = False - enforce_admins = False - conversation_resolution = False try: - branch = repo.get_branch(default_branch) - if branch.protected: - protection = branch.get_protection() - if protection: - require_pr = protection.required_pull_request_reviews is not None - approval_cnt = ( - protection.required_pull_request_reviews.required_approving_review_count - if require_pr - else 0 - ) - required_linear_history = protection.required_linear_history - allow_force_pushes = protection.allow_force_pushes - branch_deletion = protection.allow_deletions - status_checks = protection.required_status_checks is not None - enforce_admins = protection.enforce_admins - conversation_resolution = ( - protection.required_conversation_resolution - ) - branch_protection = True - require_code_owner_reviews = ( - protection.required_pull_request_reviews.require_code_owner_reviews - if require_pr - else False - ) - require_signed_commits = branch.get_required_signatures() - except Exception as error: - # If the branch is not found, it is not protected - if "404" in str(error): - logger.warning( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) - # Any other error, we cannot know if the branch is protected or not + default_branch = repo.default_branch + securitymd_exists = self._file_exists(repo, "SECURITY.md") + # CODEOWNERS file can be in .github/, root, or docs/ + # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners#codeowners-file-location + codeowners_paths = [ + ".github/CODEOWNERS", + "CODEOWNERS", + "docs/CODEOWNERS", + ] + codeowners_files = [ + self._file_exists(repo, path) for path in codeowners_paths + ] + if True in codeowners_files: + codeowners_exists = True + elif all(file is None for file in codeowners_files): + codeowners_exists = None else: - require_pr = None - approval_cnt = None - branch_protection = None - required_linear_history = None - allow_force_pushes = None - branch_deletion = None - require_code_owner_reviews = None - require_signed_commits = None - status_checks = None - enforce_admins = None - conversation_resolution = None - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" - ) + codeowners_exists = False + delete_branch_on_merge = ( + repo.delete_branch_on_merge + if repo.delete_branch_on_merge is not None + else False + ) - secret_scanning_enabled = False - dependabot_alerts_enabled = False - try: - if ( - repo.security_and_analysis - and repo.security_and_analysis.secret_scanning - ): - secret_scanning_enabled = ( - repo.security_and_analysis.secret_scanning.status == "enabled" - ) + require_pr = False + approval_cnt = 0 + branch_protection = False + required_linear_history = False + allow_force_pushes = True + branch_deletion = True + require_code_owner_reviews = False + require_signed_commits = False + status_checks = False + enforce_admins = False + conversation_resolution = False try: - # Use get_dependabot_alerts to check if Dependabot alerts are enabled - repo.get_dependabot_alerts().totalCount - # If the call succeeds, Dependabot is enabled (even if no alerts) - dependabot_alerts_enabled = True + branch = repo.get_branch(default_branch) + if branch.protected: + protection = branch.get_protection() + if protection: + require_pr = ( + protection.required_pull_request_reviews is not None + ) + approval_cnt = ( + protection.required_pull_request_reviews.required_approving_review_count + if require_pr + else 0 + ) + required_linear_history = protection.required_linear_history + allow_force_pushes = protection.allow_force_pushes + branch_deletion = protection.allow_deletions + status_checks = protection.required_status_checks is not None + enforce_admins = protection.enforce_admins + conversation_resolution = ( + protection.required_conversation_resolution + ) + branch_protection = True + require_code_owner_reviews = ( + protection.required_pull_request_reviews.require_code_owner_reviews + if require_pr + else False + ) + require_signed_commits = branch.get_required_signatures() except Exception as error: - error_str = str(error) - if ( - "403" in error_str - and "Dependabot alerts are disabled for this repository." - in error_str - ): - dependabot_alerts_enabled = False - else: - logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + # If the branch is not found, it is not protected + if "404" in str(error): + logger.warning( + f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - dependabot_alerts_enabled = None + # Any other error, we cannot know if the branch is protected or not + else: + require_pr = None + approval_cnt = None + branch_protection = None + required_linear_history = None + allow_force_pushes = None + branch_deletion = None + require_code_owner_reviews = None + require_signed_commits = None + status_checks = None + enforce_admins = None + conversation_resolution = None + logger.error( + f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + + secret_scanning_enabled = False + dependabot_alerts_enabled = False + try: + if ( + repo.security_and_analysis + and repo.security_and_analysis.secret_scanning + ): + secret_scanning_enabled = ( + repo.security_and_analysis.secret_scanning.status == "enabled" + ) + try: + # Use get_dependabot_alerts to check if Dependabot alerts are enabled + repo.get_dependabot_alerts().totalCount + # If the call succeeds, Dependabot is enabled (even if no alerts) + dependabot_alerts_enabled = True + except Exception as error: + error_str = str(error) + if ( + "403" in error_str + and "Dependabot alerts are disabled for this repository." + in error_str + ): + dependabot_alerts_enabled = False + else: + logger.error( + f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + dependabot_alerts_enabled = None + except Exception as error: + logger.error( + f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + ) + secret_scanning_enabled = None + dependabot_alerts_enabled = None + repos[repo.id] = Repo( + id=repo.id, + name=repo.name, + owner=repo.owner.login, + full_name=repo.full_name, + default_branch=Branch( + name=default_branch, + protected=branch_protection, + default_branch=True, + require_pull_request=require_pr, + approval_count=approval_cnt, + required_linear_history=required_linear_history, + allow_force_pushes=allow_force_pushes, + branch_deletion=branch_deletion, + status_checks=status_checks, + enforce_admins=enforce_admins, + conversation_resolution=conversation_resolution, + require_code_owner_reviews=require_code_owner_reviews, + require_signed_commits=require_signed_commits, + ), + private=repo.private, + archived=repo.archived, + pushed_at=repo.pushed_at, + securitymd=securitymd_exists, + codeowners_exists=codeowners_exists, + secret_scanning_enabled=secret_scanning_enabled, + dependabot_alerts_enabled=dependabot_alerts_enabled, + delete_branch_on_merge=delete_branch_on_merge, + ) except Exception as error: logger.error( - f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" + f"{repo.full_name}: {error.__class__.__name__}[{error.__traceback__.tb_lineno}]: {error}" ) - secret_scanning_enabled = None - dependabot_alerts_enabled = None - repos[repo.id] = Repo( - id=repo.id, - name=repo.name, - owner=repo.owner.login, - full_name=repo.full_name, - default_branch=Branch( - name=default_branch, - protected=branch_protection, - default_branch=True, - require_pull_request=require_pr, - approval_count=approval_cnt, - required_linear_history=required_linear_history, - allow_force_pushes=allow_force_pushes, - branch_deletion=branch_deletion, - status_checks=status_checks, - enforce_admins=enforce_admins, - conversation_resolution=conversation_resolution, - require_code_owner_reviews=require_code_owner_reviews, - require_signed_commits=require_signed_commits, - ), - private=repo.private, - archived=repo.archived, - pushed_at=repo.pushed_at, - securitymd=securitymd_exists, - codeowners_exists=codeowners_exists, - secret_scanning_enabled=secret_scanning_enabled, - dependabot_alerts_enabled=dependabot_alerts_enabled, - delete_branch_on_merge=delete_branch_on_merge, - ) class Branch(BaseModel): """Model for Github Branch""" name: str - protected: bool + protected: Optional[bool] default_branch: bool require_pull_request: Optional[bool] approval_count: Optional[int] diff --git a/prowler/providers/m365/exceptions/exceptions.py b/prowler/providers/m365/exceptions/exceptions.py index 7f9dae2c31..871607aeff 100644 --- a/prowler/providers/m365/exceptions/exceptions.py +++ b/prowler/providers/m365/exceptions/exceptions.py @@ -126,6 +126,18 @@ class M365BaseException(ProwlerException): "message": "Failed to establish connection to Exchange Online API.", "remediation": "Ensure the application has proper permission granted to access Exchange Online.", }, + (6030, "M365CertificateCreationError"): { + "message": "Failed to create X.509 certificate object from provided certificate content.", + "remediation": "Ensure the certificate content is valid base64 encoded X.509 certificate data and is properly formatted.", + }, + (6031, "M365NotValidCertificateContentError"): { + "message": "The provided certificate content is not valid base64 encoded data.", + "remediation": "Ensure the certificate content is valid base64 encoded X.509 certificate data without line breaks or invalid characters.", + }, + (6032, "M365NotValidCertificatePathError"): { + "message": "The provided certificate path is not valid or the file cannot be accessed.", + "remediation": "Ensure the certificate path exists, is accessible, and points to a valid certificate file.", + }, } def __init__(self, code, file=None, original_exception=None, message=None): @@ -357,3 +369,24 @@ class M365ExchangeConnectionError(M365CredentialsError): super().__init__( 6029, file=file, original_exception=original_exception, message=message ) + + +class M365CertificateCreationError(M365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6030, file=file, original_exception=original_exception, message=message + ) + + +class M365NotValidCertificateContentError(M365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6031, file=file, original_exception=original_exception, message=message + ) + + +class M365NotValidCertificatePathError(M365CredentialsError): + def __init__(self, file=None, original_exception=None, message=None): + super().__init__( + 6032, file=file, original_exception=original_exception, message=message + ) diff --git a/prowler/providers/m365/lib/arguments/arguments.py b/prowler/providers/m365/lib/arguments/arguments.py index e88ef7982b..fffbc3e5bb 100644 --- a/prowler/providers/m365/lib/arguments/arguments.py +++ b/prowler/providers/m365/lib/arguments/arguments.py @@ -28,6 +28,17 @@ def init_parser(self): action="store_true", help="Use Azure interactive browser authentication to log in against Microsoft 365", ) + m365_auth_modes_group.add_argument( + "--certificate-auth", + action="store_true", + help="Use Certificate authentication to log in against Microsoft 365", + ) + m365_parser.add_argument( + "--certificate-path", + nargs="?", + default=None, + help="Path to the certificate file to be used with --certificate-auth option", + ) m365_parser.add_argument( "--tenant-id", nargs="?", diff --git a/prowler/providers/m365/lib/powershell/m365_powershell.py b/prowler/providers/m365/lib/powershell/m365_powershell.py index 7692700988..3cd2dda4df 100644 --- a/prowler/providers/m365/lib/powershell/m365_powershell.py +++ b/prowler/providers/m365/lib/powershell/m365_powershell.py @@ -4,6 +4,7 @@ import platform from prowler.lib.logger import logger from prowler.lib.powershell.powershell import PowerShellSession from prowler.providers.m365.exceptions.exceptions import ( + M365CertificateCreationError, M365GraphConnectionError, M365UserCredentialsError, M365UserNotBelongingToTenantError, @@ -51,23 +52,71 @@ class M365PowerShell(PowerShellSession): self.tenant_identity = identity self.init_credential(credentials) + def clean_certificate_content(self, cert_content: str) -> str: + """ + Clean certificate content for PowerShell consumption. + + Removes newlines, carriage returns, and extra spaces from base64 content + to ensure proper parsing in PowerShell. + + Args: + cert_content (str): Base64 encoded certificate content + + Returns: + str: Cleaned base64 certificate content + """ + # Clean base64 content - remove any newlines or whitespace + clean_content = ( + cert_content.strip().replace("\n", "").replace("\r", "").replace(" ", "") + ) + logger.info(f"Cleaned certificate content length: {len(clean_content)}") + return clean_content + def init_credential(self, credentials: M365Credentials) -> None: """ Initialize PowerShell credential object for Microsoft 365 authentication. - Sanitizes the username and password, then creates a PSCredential object - in the PowerShell session for use with Microsoft 365 cmdlets. + Supports three authentication methods: + 1. User authentication (username/password) - Will be deprecated in September 2025 + 2. Application authentication (client_id/client_secret) + 3. Certificate authentication (certificate_content in base64/application_id) Args: credentials (M365Credentials): The credentials object containing - username and password. + authentication information. Note: The credentials are sanitized to prevent command injection and stored securely in the PowerShell session. """ + # Certificate Auth + if credentials.certificate_content and credentials.client_id: + # Clean certificate content for PowerShell consumption + clean_cert_content = self.clean_certificate_content( + credentials.certificate_content + ) + + # Sanitize credentials + sanitized_client_id = self.sanitize(credentials.client_id) + sanitized_tenant_id = self.sanitize(credentials.tenant_id) + + self.execute( + f'$certBytes = [Convert]::FromBase64String("{clean_cert_content}")' + ) + error = self.execute( + "$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(,$certBytes)" + ) + if error: + raise M365CertificateCreationError( + f"[{os.path.basename(__file__)}] Error creating certificate: {error}" + ) + + self.execute(f'$clientID = "{sanitized_client_id}"') + self.execute(f'$tenantID = "{sanitized_tenant_id}"') + self.execute(f'$tenantDomain = "{credentials.tenant_domains[0]}"') + # User Auth (Will be deprecated in September 2025) - if credentials.user and credentials.passwd: + elif credentials.user and credentials.passwd: credentials.encrypted_passwd = self.encrypt_password(credentials.passwd) # Sanitize user and password @@ -135,14 +184,28 @@ class M365PowerShell(PowerShellSession): """ Test Microsoft 365 credentials by attempting to authenticate against Entra ID. + Supports testing three authentication methods: + 1. User authentication (username/password) + 2. Application authentication (client_id/client_secret) + 3. Certificate authentication (certificate_content in base64/application_id) + Args: credentials (M365Credentials): The credentials object containing - username and password to test. + authentication information to test. Returns: bool: True if credentials are valid and authentication succeeds, False otherwise. """ - if credentials.user and credentials.passwd: + # Test Certificate Auth + if credentials.certificate_content and credentials.client_id: + try: + self.test_teams_certificate_connection() or self.test_exchange_certificate_connection() + return True + except Exception as e: + logger.error(f"Exchange Online Certificate connection failed: {e}") + + # Test User Auth + elif credentials.user and credentials.passwd: self.execute( f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString' # encrypted password already sanitized ) @@ -161,6 +224,7 @@ class M365PowerShell(PowerShellSession): ) # Validate credentials + # Test Exchange Online connection result = self.execute("Connect-ExchangeOnline -Credential $credential") if "https://aka.ms/exov3-module" not in result: if "AADSTS" in result: # Entra Security Token Service Error @@ -168,21 +232,19 @@ class M365PowerShell(PowerShellSession): file=os.path.basename(__file__), message=result, ) - else: # Could not connect to Exchange Online, try Microsoft Teams - result = self.execute( - "Connect-MicrosoftTeams -Credential $credential" - ) - if self.tenant_identity.tenant_id not in result: - if "AADSTS" in result: # Entra Security Token Service Error - raise M365UserCredentialsError( - file=os.path.basename(__file__), - message=result, - ) - else: # Unknown error, could be a permission issue or modules not installed - raise Exception( - file=os.path.basename(__file__), - message=f"Error connecting to PowerShell modules: {result}", - ) + # Test Microsoft Teams connection + result = self.execute("Connect-MicrosoftTeams -Credential $credential") + if self.tenant_identity.user not in result: + if "AADSTS" in result: # Entra Security Token Service Error + raise M365UserCredentialsError( + file=os.path.basename(__file__), + message=result, + ) + else: # Unknown error, could be a permission issue or modules not installed + raise M365UserCredentialsError( + file=os.path.basename(__file__), + message=f"Error connecting to PowerShell modules: {result if result else 'Unknown error'}", + ) return True @@ -235,6 +297,9 @@ class M365PowerShell(PowerShellSession): "Microsoft Teams connection failed: Please check your permissions and try again." ) return False + self.execute( + 'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")' + ) return True except Exception as e: logger.error( @@ -242,6 +307,31 @@ class M365PowerShell(PowerShellSession): ) return False + def test_teams_certificate_connection(self) -> bool: + """Test Microsoft Teams API connection using certificate and raise exception if it fails.""" + result = self.execute( + "Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID" + ) + if self.tenant_identity.identity_id not in result: + logger.error(f"Microsoft Teams Certificate connection failed: {result}") + return False + return True + + def test_teams_user_connection(self) -> bool: + """Test Microsoft Teams API connection using user authentication and raise exception if it fails.""" + result = self.execute("Connect-MicrosoftTeams -Credential $credential") + if self.tenant_identity.user not in result: + logger.error(f"Microsoft Teams User Auth connection failed: {result}.") + return False + + connection = self.execute("Get-CsTeamsClientConfiguration") + if not connection: + logger.error( + "Microsoft Teams User Auth connection failed: Please check your permissions and try again." + ) + return False + return True + def test_exchange_connection(self) -> bool: """Test Exchange Online API connection and raise exception if it fails.""" try: @@ -258,6 +348,9 @@ class M365PowerShell(PowerShellSession): "Exchange Online connection failed: Please check your permissions and try again." ) return False + self.execute( + 'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"' + ) return True except Exception as e: logger.error( @@ -265,11 +358,40 @@ class M365PowerShell(PowerShellSession): ) return False + def test_exchange_certificate_connection(self) -> bool: + """Test Exchange Online API connection using certificate and raise exception if it fails.""" + result = self.execute( + "Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain" + ) + if "https://aka.ms/exov3-module" not in result: + logger.error(f"Exchange Online Certificate connection failed: {result}") + return False + return True + + def test_exchange_user_connection(self) -> bool: + """Test Exchange Online API connection using user authentication and raise exception if it fails.""" + result = self.execute("Connect-ExchangeOnline -Credential $credential") + if "https://aka.ms/exov3-module" not in result: + logger.error(f"Exchange Online User Auth connection failed: {result}.") + return False + + connection = self.execute("Get-OrganizationConfig") + if not connection: + logger.error( + "Exchange Online User Auth connection failed: Please check your permissions and try again." + ) + return False + return True + def connect_microsoft_teams(self) -> dict: """ Connect to Microsoft Teams Module PowerShell Module. Establishes a connection to Microsoft Teams using the initialized credentials. + Supports three authentication methods: + 1. User authentication (username/password) + 2. Application authentication (client_id/client_secret) + 3. Certificate authentication (certificate_content in base64/application_id) Returns: dict: Connection status information in JSON format. @@ -277,26 +399,15 @@ class M365PowerShell(PowerShellSession): Note: This method requires the Microsoft Teams PowerShell module to be installed. """ + # Certificate Auth + if self.execute("Write-Output $certificate") != "": + return self.test_teams_certificate_connection() # User Auth if self.execute("Write-Output $credential") != "": - self.execute("Connect-MicrosoftTeams -Credential $credential") - # Test connection with a simple call - connection = self.execute("Get-CsTeamsClientConfiguration") - if connection: - return True - else: - logger.error( - "Microsoft Teams User Auth connection failed: Please check your permissions and try again." - ) - return connection + return self.test_teams_user_connection() # Application Auth else: - connection = self.test_teams_connection() - if connection: - self.execute( - 'Connect-MicrosoftTeams -AccessTokens @("$graphToken","$teamsToken")' - ) - return connection + return self.test_teams_connection() def get_teams_settings(self) -> dict: """ @@ -383,6 +494,10 @@ class M365PowerShell(PowerShellSession): Connect to Exchange Online PowerShell Module. Establishes a connection to Exchange Online using the initialized credentials. + Supports three authentication methods: + 1. User authentication (username/password) + 2. Application authentication (client_id/client_secret) + 3. Certificate authentication (certificate_content in base64/application_id) Returns: dict: Connection status information in JSON format. @@ -390,25 +505,15 @@ class M365PowerShell(PowerShellSession): Note: This method requires the Exchange Online PowerShell module to be installed. """ + # Certificate Auth + if self.execute("Write-Output $certificate") != "": + return self.test_exchange_certificate_connection() # User Auth if self.execute("Write-Output $credential") != "": - self.execute("Connect-ExchangeOnline -Credential $credential") - connection = self.execute("Get-OrganizationConfig") - if connection: - return True - else: - logger.error( - "Exchange Online User Auth connection failed: Please check your permissions and try again." - ) - return False + return self.test_exchange_user_connection() # Application Auth else: - connection = self.test_exchange_connection() - if connection: - self.execute( - 'Connect-ExchangeOnline -AccessToken $exchangeToken.AccessToken -Organization "$tenantID"' - ) - return connection + return self.test_exchange_connection() def get_audit_log_config(self) -> dict: """ diff --git a/prowler/providers/m365/m365_provider.py b/prowler/providers/m365/m365_provider.py index 1910e5de91..4be03f5a70 100644 --- a/prowler/providers/m365/m365_provider.py +++ b/prowler/providers/m365/m365_provider.py @@ -1,4 +1,5 @@ import asyncio +import base64 import os from argparse import ArgumentTypeError from os import getenv @@ -6,6 +7,7 @@ from uuid import UUID from azure.core.exceptions import ClientAuthenticationError, HttpResponseError from azure.identity import ( + CertificateCredential, ClientSecretCredential, CredentialUnavailableError, DefaultAzureCredential, @@ -41,6 +43,8 @@ from prowler.providers.m365.exceptions.exceptions import ( M365MissingEnvironmentCredentialsError, M365NoAuthenticationMethodError, M365NotTenantIdButClientIdAndClientSecretError, + M365NotValidCertificateContentError, + M365NotValidCertificatePathError, M365NotValidClientIdError, M365NotValidClientSecretError, M365NotValidTenantIdError, @@ -111,11 +115,14 @@ class M365Provider(Provider): env_auth: bool = False, az_cli_auth: bool = False, browser_auth: bool = False, + certificate_auth: bool = False, tenant_id: str = None, client_id: str = None, client_secret: str = None, user: str = None, password: str = None, + certificate_content: str = None, + certificate_path: str = None, init_modules: bool = False, region: str = "M365Global", config_content: dict = None, @@ -158,11 +165,14 @@ class M365Provider(Provider): sp_env_auth, env_auth, browser_auth, + certificate_auth, tenant_id, client_id, client_secret, user, password, + certificate_content, + certificate_path, ) logger.info("Checking if region is different than default one") @@ -170,13 +180,15 @@ class M365Provider(Provider): # Get the dict from the static credentials m365_credentials = None - if tenant_id and client_id and client_secret: + if tenant_id and client_id: m365_credentials = self.validate_static_credentials( tenant_id=tenant_id, client_id=client_id, client_secret=client_secret, user=user, password=password, + certificate_content=certificate_content, + certificate_path=certificate_path, ) # Set up the M365 session @@ -185,6 +197,8 @@ class M365Provider(Provider): sp_env_auth, env_auth, browser_auth, + certificate_auth, + certificate_path, tenant_id, m365_credentials, self._region_config, @@ -196,6 +210,7 @@ class M365Provider(Provider): env_auth, browser_auth, az_cli_auth, + certificate_auth, self._session, ) @@ -203,6 +218,8 @@ class M365Provider(Provider): self._credentials = self.setup_powershell( env_auth=env_auth, sp_env_auth=sp_env_auth, + certificate_auth=certificate_auth, + certificate_path=certificate_path, m365_credentials=m365_credentials, identity=self.identity, init_modules=init_modules, @@ -279,11 +296,14 @@ class M365Provider(Provider): sp_env_auth: bool, env_auth: bool, browser_auth: bool, + certificate_auth: bool, tenant_id: str, client_id: str, client_secret: str, user: str, password: str, + certificate_content: str, + certificate_path: str, ): """ Validates the authentication arguments for the M365 provider. @@ -293,11 +313,14 @@ class M365Provider(Provider): sp_env_auth (bool): Flag indicating whether application authentication with environment variables is enabled. env_auth: (bool): Flag indicating whether to use application and PowerShell authentication with environment variables. browser_auth (bool): Flag indicating whether browser authentication is enabled. + certificate_auth (bool): Flag indicating whether certificate authentication is enabled. tenant_id (str): The M365 Tenant ID. client_id (str): The M365 Client ID. client_secret (str): The M365 Client Secret. user (str): The M365 User Account. password (str): The M365 User Password. + certificate_content (str): The M365 Certificate Content. + certificate_path (str): The path to the certificate file. Raises: M365BrowserAuthNoTenantIDError: If browser authentication is enabled but the tenant ID is not found. @@ -314,28 +337,33 @@ class M365Provider(Provider): and not sp_env_auth and not browser_auth and not env_auth + and not certificate_auth ): raise M365NoAuthenticationMethodError( file=os.path.basename(__file__), - message="M365 provider requires at least one authentication method set: [--env-auth | --az-cli-auth | --sp-env-auth | --browser-auth]", + message="M365 provider requires at least one authentication method set: [--env-auth | --az-cli-auth | --sp-env-auth | --browser-auth | --certificate-auth]", ) elif browser_auth and not tenant_id: raise M365BrowserAuthNoTenantIDError( file=os.path.basename(__file__), message="M365 Tenant ID (--tenant-id) is required for browser authentication mode", ) - elif env_auth: - if not user or not password or not tenant_id: - raise M365MissingEnvironmentCredentialsError( - file=os.path.basename(__file__), - message="M365 provider requires AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, M365_USER and M365_PASSWORD environment variables to be set when using --env-auth", - ) else: if not tenant_id: raise M365NotTenantIdButClientIdAndClientSecretError( file=os.path.basename(__file__), message="Tenant Id is required for M365 static credentials. Make sure you are using the correct credentials.", ) + if ( + not certificate_content + and not certificate_path + and not (user and password) + and not client_secret + ): + raise M365ConfigCredentialsError( + file=os.path.basename(__file__), + message="You must provide a valid set of credentials. Please check your credentials and try again.", + ) @staticmethod def setup_region_config(region): @@ -378,6 +406,8 @@ class M365Provider(Provider): def setup_powershell( env_auth: bool = False, sp_env_auth: bool = False, + certificate_auth: bool = False, + certificate_path: str = None, m365_credentials: dict = {}, identity: M365IdentityInfo = None, init_modules: bool = False, @@ -402,6 +432,7 @@ class M365Provider(Provider): client_id=m365_credentials.get("client_id", ""), client_secret=m365_credentials.get("client_secret", ""), tenant_id=m365_credentials.get("tenant_id", ""), + certificate_content=m365_credentials.get("certificate_content", ""), tenant_domains=identity.tenant_domains, ) elif env_auth: @@ -440,10 +471,28 @@ class M365Provider(Provider): tenant_domains=identity.tenant_domains, ) + elif certificate_auth: + client_id = getenv("AZURE_CLIENT_ID") + tenant_id = getenv("AZURE_TENANT_ID") + if certificate_path: + with open(certificate_path, "rb") as cert_file: + # Encode the certificate content to base64 since PowerShell expects a base64 string + certificate_content = base64.b64encode(cert_file.read()) + else: + certificate_content = getenv("M365_CERTIFICATE_CONTENT") + credentials = M365Credentials( + client_id=client_id, + tenant_id=tenant_id, + certificate_content=certificate_content, + tenant_domains=identity.tenant_domains, + ) + if credentials: if identity and credentials.user: identity.user = credentials.user identity.identity_type = "Service Principal and User Credentials" + if identity and credentials.certificate_content: + identity.identity_type = "Service Principal with Certificate" test_session = M365PowerShell(credentials, identity) try: if init_modules: @@ -478,6 +527,10 @@ class M365Provider(Provider): report_lines.append( f"M365 User: {Fore.YELLOW}{self.credentials.user}{Style.RESET_ALL}" ) + elif self.credentials and self.credentials.certificate_content: + report_lines.append( + f"M365 Certificate Thumbprint: {Fore.YELLOW}{self._identity.certificate_thumbprint}{Style.RESET_ALL}" + ) report_title = ( f"{Style.BRIGHT}Using the M365 credentials below:{Style.RESET_ALL}" ) @@ -491,6 +544,8 @@ class M365Provider(Provider): sp_env_auth: bool, env_auth: bool, browser_auth: bool, + certificate_auth: bool, + certificate_path: str, tenant_id: str, m365_credentials: dict, region_config: M365RegionConfig, @@ -510,6 +565,8 @@ class M365Provider(Provider): - client_secret: The M365 client secret - user: The M365 user email - password: The M365 user password + - certificate_content: The M365 certificate content + - certificate_path: The path to the certificate file. - provider_id: The M365 provider ID (in this case the Tenant ID). region_config (M365RegionConfig): The region configuration object. @@ -530,16 +587,45 @@ class M365Provider(Provider): f"{environment_credentials_error.__class__.__name__}[{environment_credentials_error.__traceback__.tb_lineno}] -- {environment_credentials_error}" ) raise environment_credentials_error + elif certificate_auth: + try: + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=not certificate_path + ) + except M365EnvironmentVariableError as environment_variable_error: + logger.critical( + f"{environment_variable_error.__class__.__name__}[{environment_variable_error.__traceback__.tb_lineno}] -- {environment_variable_error}" + ) + raise environment_variable_error try: if m365_credentials: try: - credentials = ClientSecretCredential( - tenant_id=m365_credentials["tenant_id"], - client_id=m365_credentials["client_id"], - client_secret=m365_credentials["client_secret"], - user=m365_credentials["user"], - password=m365_credentials["password"], - ) + if m365_credentials["certificate_content"]: + credentials = CertificateCredential( + tenant_id=m365_credentials["tenant_id"], + client_id=m365_credentials["client_id"], + certificate_data=base64.b64decode( + m365_credentials["certificate_content"] + ), + ) + elif m365_credentials["certificate_path"]: + with open( + m365_credentials["certificate_path"], "rb" + ) as cert_file: + certificate_data = cert_file.read() + credentials = CertificateCredential( + tenant_id=m365_credentials["tenant_id"], + client_id=m365_credentials["client_id"], + certificate_data=certificate_data, + ) + else: + credentials = ClientSecretCredential( + tenant_id=m365_credentials["tenant_id"], + client_id=m365_credentials["client_id"], + client_secret=m365_credentials["client_secret"], + user=m365_credentials["user"], + password=m365_credentials["password"], + ) return credentials except ClientAuthenticationError as error: logger.error( @@ -562,13 +648,34 @@ class M365Provider(Provider): raise M365ConfigCredentialsError( file=os.path.basename(__file__), original_exception=error ) + elif certificate_auth: + try: + if certificate_path: + with open(certificate_path, "rb") as cert_file: + certificate_data = cert_file.read() + else: + certificate_data = base64.b64decode( + getenv("M365_CERTIFICATE_CONTENT") + ) + credentials = CertificateCredential( + tenant_id=getenv("AZURE_TENANT_ID"), + client_id=getenv("AZURE_CLIENT_ID"), + certificate_data=certificate_data, + ) + except ClientAuthenticationError as error: + logger.error( + f"{error.__class__.__name__}[{error.__traceback__.tb_lineno}] -- {error}" + ) + raise M365ClientAuthenticationError( + file=os.path.basename(__file__), original_exception=error + ) else: # Since the authentication method to be used will come as True, we have to negate it since # DefaultAzureCredential sets just one authentication method, excluding the others try: credentials = DefaultAzureCredential( exclude_environment_credential=not ( - sp_env_auth or env_auth + sp_env_auth or env_auth or certificate_auth ), exclude_cli_credential=not az_cli_auth, # M365 Auth using Managed Identity is not supported @@ -633,6 +740,7 @@ class M365Provider(Provider): sp_env_auth: bool = False, env_auth: bool = False, browser_auth: bool = False, + certificate_auth: bool = False, tenant_id: str = None, region: str = "M365Global", raise_on_exception: bool = True, @@ -640,6 +748,8 @@ class M365Provider(Provider): client_secret: str = None, user: str = None, password: str = None, + certificate_content: str = None, + certificate_path: str = None, provider_id: str = None, ) -> Connection: """Test connection to M365 tenant and PowerShell modules. @@ -652,6 +762,7 @@ class M365Provider(Provider): sp_env_auth (bool): Flag indicating whether to use application authentication with environment variables. env_auth: (bool): Flag indicating whether to use application and PowerShell authentication with environment variables. browser_auth (bool): Flag indicating whether to use interactive browser authentication. + certificate_auth (bool): Flag indicating whether to use certificate authentication. tenant_id (str): The M365 Active Directory tenant ID. region (str): The M365 region. raise_on_exception (bool): Flag indicating whether to raise an exception if the connection fails. @@ -688,11 +799,14 @@ class M365Provider(Provider): sp_env_auth, env_auth, browser_auth, + certificate_auth, tenant_id, client_id, client_secret, user, password, + certificate_content, + certificate_path, ) region_config = M365Provider.setup_region_config(region) @@ -704,8 +818,6 @@ class M365Provider(Provider): tenant_id=tenant_id, client_id=client_id, client_secret=client_secret, - user=None, - password=None, ) else: m365_credentials = M365Provider.validate_static_credentials( @@ -715,6 +827,12 @@ class M365Provider(Provider): user=user, password=password, ) + elif tenant_id and client_id and certificate_content: + m365_credentials = M365Provider.validate_static_credentials( + tenant_id=tenant_id, + client_id=client_id, + certificate_content=certificate_content, + ) # Set up the M365 session session = M365Provider.setup_session( @@ -722,6 +840,8 @@ class M365Provider(Provider): sp_env_auth, env_auth, browser_auth, + certificate_auth, + certificate_path, tenant_id, m365_credentials, region_config, @@ -737,6 +857,7 @@ class M365Provider(Provider): env_auth, browser_auth, az_cli_auth, + certificate_auth, session, ) @@ -758,6 +879,8 @@ class M365Provider(Provider): M365Provider.setup_powershell( env_auth, sp_env_auth, + certificate_auth, + certificate_path, m365_credentials, identity, ) @@ -889,12 +1012,44 @@ class M365Provider(Provider): message=f"Missing environment variable {env_var} required to authenticate.", ) + @staticmethod + def check_certificate_creds_env_vars(check_certificate_content: bool): + """ + Checks the presence of required environment variables for service principal authentication against Azure. + + This method checks for the presence of the following environment variables: + - AZURE_CLIENT_ID: Azure client ID + - AZURE_TENANT_ID: Azure tenant ID + - M365_CERTIFICATE_CONTENT: Azure certificate content + + If any of the environment variables is missing, it logs a critical error and exits the program. + """ + logger.info( + "M365 provider: checking service principal environment variables ..." + ) + env_vars = [ + "AZURE_CLIENT_ID", + "AZURE_TENANT_ID", + ] + if check_certificate_content: + env_vars.append("M365_CERTIFICATE_CONTENT") + for env_var in env_vars: + if not getenv(env_var): + logger.critical( + f"M365 provider: Missing environment variable {env_var} needed to authenticate against M365." + ) + raise M365EnvironmentVariableError( + file=os.path.basename(__file__), + message=f"Missing environment variable {env_var} required to authenticate.", + ) + @staticmethod def setup_identity( sp_env_auth, env_auth, browser_auth, az_cli_auth, + certificate_auth, session, ): """ @@ -968,6 +1123,16 @@ class M365Provider(Provider): or session.credentials[0]._credential.client_id or "Unknown user id (Missing AAD permissions)" ) + elif certificate_auth: + identity.identity_type = "Service Principal with Certificate" + identity.identity_id = ( + getenv("AZURE_CLIENT_ID") + or session.credentials[0]._credential.client_id + or "Unknown user id (Missing AAD permissions)" + ) + identity.certificate_thumbprint = session._client_credential.get( + "thumbprint", "Unknown certificate thumbprint" + ) elif browser_auth or az_cli_auth: identity.identity_type = "User" try: @@ -987,8 +1152,14 @@ class M365Provider(Provider): ) else: # Static Credentials - identity.identity_type = "Service Principal" identity.identity_id = session._client_id + if isinstance(session, CertificateCredential): + identity.identity_type = "Service Principal with Certificate" + identity.certificate_thumbprint = session._client_credential.get( + "thumbprint", "Unknown certificate thumbprint" + ) + else: + identity.identity_type = "Service Principal" # Retrieve tenant id from the client client = GraphServiceClient(credentials=session) @@ -1005,6 +1176,8 @@ class M365Provider(Provider): client_secret: str = None, user: str = None, password: str = None, + certificate_content: str = None, + certificate_path: str = None, ) -> dict: """ Validates the static credentials for the M365 provider. @@ -1015,6 +1188,8 @@ class M365Provider(Provider): client_secret (str): The M365 client secret. user (str): The M365 user email. password (str): The M365 user password. + certificate_content (str): The M365 Certificate Content. + certificate_path (str): The path to the certificate file. Raises: M365NotValidTenantIdError: If the provided M365 Tenant ID is not valid. @@ -1045,21 +1220,47 @@ class M365Provider(Provider): message="The provided Client ID is not valid.", ) - # Validate the Client Secret - if not client_secret: + if not certificate_content and not certificate_path and not client_secret: raise M365NotValidClientSecretError( file=os.path.basename(__file__), - message="The provided Client Secret is not valid.", + message="You must provide a client secret, certificate content or certificate path. Please check your credentials and try again.", ) + if certificate_content: + try: + # Validate that certificate content can be properly decoded from base64 + base64.b64decode(certificate_content) + except Exception as e: + raise M365NotValidCertificateContentError( + file=os.path.basename(__file__), + message=f"The provided certificate content is not valid base64 encoded data: {str(e)}", + ) + if certificate_path: + try: + with open(certificate_path, "rb") as cert_file: + certificate_content = cert_file.read() + except Exception as e: + raise M365NotValidCertificatePathError( + file=os.path.basename(__file__), + message=f"The provided certificate path is not valid: {str(e)}", + ) + try: - M365Provider.verify_client(tenant_id, client_id, client_secret) + M365Provider.verify_client( + tenant_id, + client_id, + client_secret, + certificate_content, + certificate_path, + ) return { "tenant_id": tenant_id, "client_id": client_id, "client_secret": client_secret, "user": user, "password": password, + "certificate_content": certificate_content, + "certificate_path": certificate_path, } except M365NotValidTenantIdError as tenant_id_error: logger.error( @@ -1087,7 +1288,9 @@ class M365Provider(Provider): ) @staticmethod - def verify_client(tenant_id, client_id, client_secret) -> None: + def verify_client( + tenant_id, client_id, client_secret, certificate_content, certificate_path + ) -> None: """ Verifies the M365 client credentials using the specified tenant ID, client ID, and client secret. @@ -1095,52 +1298,106 @@ class M365Provider(Provider): tenant_id (str): The M365 Active Directory tenant ID. client_id (str): The M365 client ID. client_secret (str): The M365 client secret. + certificate_content (str): The M365 certificate content. + certificate_path (str): The path to the certificate file. Raises: M365NotValidTenantIdError: If the provided M365 Tenant ID is not valid. M365NotValidClientIdError: If the provided M365 Client ID is not valid. M365NotValidClientSecretError: If the provided M365 Client Secret is not valid. + M365NotValidCertificateContentError: If the provided M365 Certificate Content is not valid. + M365NotValidCertificatePathError: If the provided M365 Certificate Path is not valid. Returns: None """ authority = f"https://login.microsoftonline.com/{tenant_id}" try: - # Create a ConfidentialClientApplication instance - app = ConfidentialClientApplication( - client_id=client_id, - client_credential=client_secret, - authority=authority, - ) + if client_secret: + # Create a ConfidentialClientApplication instance + app = ConfidentialClientApplication( + client_id=client_id, + client_credential=client_secret, + authority=authority, + ) + # Attempt to acquire a token + result = app.acquire_token_for_client( + scopes=["https://graph.microsoft.com/.default"] + ) - # Attempt to acquire a token - result = app.acquire_token_for_client( - scopes=["https://graph.microsoft.com/.default"] - ) + # Check if token acquisition was successful + if "access_token" not in result: + # Handle specific errors based on the MSAL response + error_description = result.get("error_description", "") + if f"Tenant '{tenant_id}'" in error_description: + raise M365NotValidTenantIdError( + file=os.path.basename(__file__), + message="The provided Tenant ID is not valid for the specified Client ID and Client Secret.", + ) + if ( + f"Application with identifier '{client_id}'" + in error_description + ): + raise M365NotValidClientIdError( + file=os.path.basename(__file__), + message="The provided Client ID is not valid for the specified Tenant ID and Client Secret.", + ) + if "Invalid client secret provided" in error_description: + raise M365NotValidClientSecretError( + file=os.path.basename(__file__), + message="The provided Client Secret is not valid for the specified Tenant ID and Client ID.", + ) + elif certificate_content: + credential = CertificateCredential( + client_id=client_id, + tenant_id=tenant_id, + certificate_data=base64.b64decode(certificate_content), + ) + client = GraphServiceClient(credentials=credential) - # Check if token acquisition was successful - if "access_token" not in result: - # Handle specific errors based on the MSAL response - error_description = result.get("error_description", "") - if f"Tenant '{tenant_id}'" in error_description: - raise M365NotValidTenantIdError( + # Verify that the certificate is valid + async def verify_certificate(): + result = await client.domains.get() + return result.value + + result = asyncio.get_event_loop().run_until_complete( + verify_certificate() + ) + if not result: + raise M365NotValidCertificateContentError( file=os.path.basename(__file__), - message="The provided Tenant ID is not valid for the specified Client ID and Client Secret.", + message="The provided certificate content is not valid.", ) - if f"Application with identifier '{client_id}'" in error_description: - raise M365NotValidClientIdError( + elif certificate_path: + with open(certificate_path, "rb") as cert_file: + certificate_content = cert_file.read() + credential = CertificateCredential( + client_id=client_id, + tenant_id=tenant_id, + certificate_data=certificate_content, + ) + client = GraphServiceClient(credentials=credential) + + # Verify that the certificate is valid + async def verify_certificate(): + result = await client.domains.get() + return result.value + + result = asyncio.get_event_loop().run_until_complete( + verify_certificate() + ) + if not result: + raise M365NotValidCertificatePathError( file=os.path.basename(__file__), - message="The provided Client ID is not valid for the specified Tenant ID and Client Secret.", - ) - if "Invalid client secret provided" in error_description: - raise M365NotValidClientSecretError( - file=os.path.basename(__file__), - message="The provided Client Secret is not valid for the specified Tenant ID and Client ID.", + message="The provided certificate is not valid.", ) + except ( M365NotValidTenantIdError, M365NotValidClientIdError, M365NotValidClientSecretError, + M365NotValidCertificateContentError, + M365NotValidCertificatePathError, ) as m365_error: # M365 specific errors already raised raise RuntimeError(f"{m365_error}") diff --git a/prowler/providers/m365/models.py b/prowler/providers/m365/models.py index ff2cec2fd5..3459b31688 100644 --- a/prowler/providers/m365/models.py +++ b/prowler/providers/m365/models.py @@ -11,6 +11,7 @@ class M365IdentityInfo(BaseModel): identity_type: str = "" tenant_id: str = "" tenant_domain: str = "Unknown tenant domain (missing Entra permissions)" + certificate_thumbprint: str = "" tenant_domains: list[str] = [] location: str = "" user: str = None @@ -28,9 +29,10 @@ class M365Credentials(BaseModel): passwd: Optional[str] = None encrypted_passwd: Optional[str] = None client_id: str = "" - client_secret: str = "" + client_secret: Optional[str] = None tenant_id: str = "" tenant_domains: list[str] = [] + certificate_content: Optional[str] = None class M365OutputOptions(ProviderOutputOptions): diff --git a/prowler/providers/m365/services/admincenter/admincenter_service.py b/prowler/providers/m365/services/admincenter/admincenter_service.py index 3a3e1f3284..1cd85faa2e 100644 --- a/prowler/providers/m365/services/admincenter/admincenter_service.py +++ b/prowler/providers/m365/services/admincenter/admincenter_service.py @@ -87,9 +87,11 @@ class AdminCenter(M365Service): { user.id: User( id=user.id, - name=user.display_name, + name=getattr(user, "display_name", ""), license=( - license_details.value[0].sku_part_number + getattr( + license_details.value[0], "sku_part_number", None + ) if license_details.value else None ), @@ -149,8 +151,8 @@ class AdminCenter(M365Service): { group.id: Group( id=group.id, - name=group.display_name, - visibility=group.visibility, + name=getattr(group, "display_name", ""), + visibility=getattr(group, "visibility", ""), ) } ) @@ -168,14 +170,21 @@ class AdminCenter(M365Service): domains_list = await self.client.domains.get() domains.update({}) for domain in domains_list.value: - domains.update( - { - domain.id: Domain( - id=domain.id, - password_validity_period=domain.password_validity_period_in_days, - ) - } - ) + if domain: + password_validity_period = getattr( + domain, "password_validity_period_in_days", None + ) + if password_validity_period is None: + password_validity_period = 0 + + domains.update( + { + domain.id: Domain( + id=domain.id, + password_validity_period=password_validity_period, + ) + } + ) except Exception as error: logger.error( diff --git a/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.py b/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.py index 04f81f62b3..b0530c4308 100644 --- a/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.py +++ b/prowler/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled.py @@ -166,4 +166,4 @@ class defender_antispam_outbound_policy_forwarding_disabled(Check): policy.default or defender_client.outbound_spam_rules[policy.name].state.lower() == "enabled" - ) and not policy.auto_forwarding_mode + ) and policy.auto_forwarding_mode == "Off" diff --git a/prowler/providers/m365/services/defender/defender_service.py b/prowler/providers/m365/services/defender/defender_service.py index cdfd53a6b7..5d198b5888 100644 --- a/prowler/providers/m365/services/defender/defender_service.py +++ b/prowler/providers/m365/services/defender/defender_service.py @@ -44,6 +44,23 @@ class Defender(M365Service): malware_policy = [malware_policy] for policy in malware_policy: if policy: + file_types_raw = policy.get("FileTypes", []) + file_types = [] + if file_types_raw is not None: + if isinstance(file_types_raw, list): + file_types = file_types_raw + else: + try: + if isinstance(file_types_raw, str): + file_types = [file_types_raw] + else: + file_types = [str(file_types_raw)] + except (ValueError, TypeError): + logger.warning( + f"Skipping invalid file_types value: {file_types_raw}" + ) + file_types = [] + malware_policies.append( MalwarePolicy( enable_file_filter=policy.get("EnableFileFilter", False), @@ -54,7 +71,7 @@ class Defender(M365Service): internal_sender_admin_address=policy.get( "InternalSenderAdminAddress", "" ), - file_types=policy.get("FileTypes", []), + file_types=file_types, is_default=policy.get("IsDefault", False), ) ) @@ -207,7 +224,7 @@ class Defender(M365Service): notify_sender_blocked_addresses=policy.get( "NotifyOutboundSpamRecipients", [] ), - auto_forwarding_mode=policy.get("AutoForwardingMode", True), + auto_forwarding_mode=policy.get("AutoForwardingMode", "On"), default=policy.get("IsDefault", False), ) @@ -257,12 +274,43 @@ class Defender(M365Service): inbound_spam_policy = [inbound_spam_policy] for policy in inbound_spam_policy: if policy: + allowed_domains_raw = policy.get("AllowedSenderDomains", []) + allowed_domains = [] + + if isinstance(allowed_domains_raw, str): + try: + import json + + parsed_domains = json.loads(allowed_domains_raw) + if isinstance(parsed_domains, list): + allowed_domains_raw = parsed_domains + else: + logger.warning( + f"Expected list from JSON string, got: {type(parsed_domains)}" + ) + allowed_domains_raw = [] + except (json.JSONDecodeError, ValueError) as e: + logger.warning( + f"Failed to parse AllowedSenderDomains as JSON: {e}" + ) + allowed_domains_raw = [] + + if allowed_domains_raw: + for domain in allowed_domains_raw: + if isinstance(domain, str): + allowed_domains.append(domain) + else: + try: + allowed_domains.append(str(domain)) + except (ValueError, TypeError): + logger.warning( + f"Skipping invalid domain value: {domain}" + ) + inbound_spam_policies.append( DefenderInboundSpamPolicy( identity=policy.get("Identity", ""), - allowed_sender_domains=policy.get( - "AllowedSenderDomains", [] - ), + allowed_sender_domains=allowed_domains, default=policy.get("IsDefault", False), ) ) @@ -389,7 +437,7 @@ class OutboundSpamPolicy(BaseModel): notify_limit_exceeded: bool notify_limit_exceeded_addresses: List[str] notify_sender_blocked_addresses: List[str] - auto_forwarding_mode: bool + auto_forwarding_mode: str default: bool diff --git a/prowler/providers/m365/services/exchange/exchange_service.py b/prowler/providers/m365/services/exchange/exchange_service.py index 96bbd4e13c..35d4068007 100644 --- a/prowler/providers/m365/services/exchange/exchange_service.py +++ b/prowler/providers/m365/services/exchange/exchange_service.py @@ -123,12 +123,20 @@ class Exchange(M365Service): rules_data = [rules_data] for rule in rules_data: if rule: + sender_domain_is = rule.get("SenderDomainIs", []) + if sender_domain_is is None: + sender_domain_is = [] + + redirect_message_to = rule.get("RedirectMessageTo", []) + if redirect_message_to is None: + redirect_message_to = [] + transport_rules.append( TransportRule( name=rule.get("Name", ""), scl=rule.get("SetSCL", None), - sender_domain_is=rule.get("SenderDomainIs", []), - redirect_message_to=rule.get("RedirectMessageTo", None), + sender_domain_is=sender_domain_is, + redirect_message_to=redirect_message_to, ) ) except Exception as error: diff --git a/tests/lib/outputs/finding_test.py b/tests/lib/outputs/finding_test.py index 5b22fa675d..f9d4aaf7de 100644 --- a/tests/lib/outputs/finding_test.py +++ b/tests/lib/outputs/finding_test.py @@ -585,7 +585,9 @@ class TestFinding: provider = MagicMock() provider.type = "github" # GitHub App identity only has app_id, not account_name/account_id - provider.identity = GithubAppIdentityInfo(app_id=APP_ID) + provider.identity = GithubAppIdentityInfo( + app_id=APP_ID, installations=["test-org"] + ) provider.auth_method = "GitHub App Token" # Mock check result diff --git a/tests/lib/outputs/html/html_test.py b/tests/lib/outputs/html/html_test.py index e3a834e452..e85567b20a 100644 --- a/tests/lib/outputs/html/html_test.py +++ b/tests/lib/outputs/html/html_test.py @@ -673,7 +673,7 @@ class TestHTML: provider = set_mocked_github_provider( auth_method="GitHub App Token", - identity=GithubAppIdentityInfo(app_id=APP_ID), + identity=GithubAppIdentityInfo(app_id=APP_ID, installations=["test-org"]), ) summary = output.get_assessment_summary(provider) diff --git a/tests/lib/scan_filters/scan_filters_test.py b/tests/lib/scan_filters/scan_filters_test.py index 5c71c21e1f..94daa96dba 100644 --- a/tests/lib/scan_filters/scan_filters_test.py +++ b/tests/lib/scan_filters/scan_filters_test.py @@ -13,5 +13,4 @@ class Test_Scan_Filters: assert not is_resource_filtered( "arn:aws:iam::123456789012:user/test1", audit_resources ) - assert is_resource_filtered("test_bucket", audit_resources) assert is_resource_filtered("arn:aws:s3:::test_bucket", audit_resources) diff --git a/tests/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/eks_cluster_deletion_protection_enabled_test.py b/tests/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/eks_cluster_deletion_protection_enabled_test.py new file mode 100644 index 0000000000..2317317e20 --- /dev/null +++ b/tests/providers/aws/services/eks/eks_cluster_deletion_protection_enabled/eks_cluster_deletion_protection_enabled_test.py @@ -0,0 +1,122 @@ +from unittest import mock + +from prowler.providers.aws.services.eks.eks_service import EKSCluster +from tests.providers.aws.utils import AWS_ACCOUNT_NUMBER, AWS_REGION_EU_WEST_1 + +cluster_name = "cluster_test" +cluster_arn = ( + f"arn:aws:eks:{AWS_REGION_EU_WEST_1}:{AWS_ACCOUNT_NUMBER}:cluster/{cluster_name}" +) + + +class Test_eks_cluster_deletion_protection_enabled: + def test_no_clusters(self): + eks_client = mock.MagicMock + eks_client.clusters = [] + with mock.patch( + "prowler.providers.aws.services.eks.eks_service.EKS", + eks_client, + ): + from prowler.providers.aws.services.eks.eks_cluster_deletion_protection_enabled.eks_cluster_deletion_protection_enabled import ( + eks_cluster_deletion_protection_enabled, + ) + + check = eks_cluster_deletion_protection_enabled() + result = check.execute() + assert len(result) == 0 + + def test_cluster_deletion_protection_disabled(self): + eks_client = mock.MagicMock + eks_client.clusters = [] + eks_client.clusters.append( + EKSCluster( + name=cluster_name, + arn=cluster_arn, + region=AWS_REGION_EU_WEST_1, + deletion_protection=False, + ) + ) + + with mock.patch( + "prowler.providers.aws.services.eks.eks_service.EKS", + eks_client, + ): + from prowler.providers.aws.services.eks.eks_cluster_deletion_protection_enabled.eks_cluster_deletion_protection_enabled import ( + eks_cluster_deletion_protection_enabled, + ) + + check = eks_cluster_deletion_protection_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].status_extended == ( + f"EKS cluster {cluster_name} has deletion protection disabled." + ) + assert result[0].resource_id == cluster_name + assert result[0].resource_arn == cluster_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_cluster_deletion_protection_enabled(self): + eks_client = mock.MagicMock + eks_client.clusters = [] + eks_client.clusters.append( + EKSCluster( + name=cluster_name, + arn=cluster_arn, + region=AWS_REGION_EU_WEST_1, + deletion_protection=True, + ) + ) + + with mock.patch( + "prowler.providers.aws.services.eks.eks_service.EKS", + eks_client, + ): + from prowler.providers.aws.services.eks.eks_cluster_deletion_protection_enabled.eks_cluster_deletion_protection_enabled import ( + eks_cluster_deletion_protection_enabled, + ) + + check = eks_cluster_deletion_protection_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + f"EKS cluster {cluster_name} has deletion protection enabled." + ) + assert result[0].resource_id == cluster_name + assert result[0].resource_arn == cluster_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION_EU_WEST_1 + + def test_cluster_deletion_protection_none(self): + eks_client = mock.MagicMock + eks_client.clusters = [] + eks_client.clusters.append( + EKSCluster( + name=cluster_name, + arn=cluster_arn, + region=AWS_REGION_EU_WEST_1, + deletion_protection=None, + ) + ) + + with mock.patch( + "prowler.providers.aws.services.eks.eks_service.EKS", + eks_client, + ): + from prowler.providers.aws.services.eks.eks_cluster_deletion_protection_enabled.eks_cluster_deletion_protection_enabled import ( + eks_cluster_deletion_protection_enabled, + ) + + check = eks_cluster_deletion_protection_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].status_extended == ( + f"EKS cluster {cluster_name} has deletion protection enabled." + ) + assert result[0].resource_id == cluster_name + assert result[0].resource_arn == cluster_arn + assert result[0].resource_tags == [] + assert result[0].region == AWS_REGION_EU_WEST_1 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 2463dbd34f..176942ea0b 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 @@ -362,17 +362,16 @@ class Test_iam_inline_policy_allows_privilege_escalation: check = iam_inline_policy_allows_privilege_escalation() result = check.execute() assert len(result) == 1 - assert result[0].status == "FAIL" + assert result[0].status == "PASS" 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_name} allows privilege escalation using the following actions: ", + f"Inline policy {policy_name} attached to role {role_name} does not allow privilege escalation", result[0].status_extended, ) - assert search("iam:PassRole", result[0].status_extended) @mock_aws def test_iam_inline_policy_allows_privilege_escalation_two_combinations( @@ -511,17 +510,16 @@ class Test_iam_inline_policy_allows_privilege_escalation: check = iam_inline_policy_allows_privilege_escalation() result = check.execute() assert len(result) == 1 - assert result[0].status == "FAIL" + assert result[0].status == "PASS" 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_name} allows privilege escalation using the following actions: ", + f"Inline policy {policy_name} attached to role {role_name} does not allow privilege escalation", result[0].status_extended, ) - assert search("iam:PassRole", result[0].status_extended) @mock_aws def test_iam_inline_policy_allows_privilege_escalation_policies_combination( @@ -1219,3 +1217,75 @@ class Test_iam_inline_policy_allows_privilege_escalation: f"Inline Policy '{policy_name}' attached to role {role_arn} allows privilege escalation using the following actions:", finding.status_extended, ) + + @mock_aws + def test_iam_inline_role_policy_allows_privilege_escalation_agentcore(self): + """Test detection of AWS Bedrock AgentCore privilege escalation in inline role policy.""" + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + # Create IAM Role + role_name = "agentcore_test_role" + role_arn = iam_client.create_role( + RoleName=role_name, + AssumeRolePolicyDocument=dumps(ADMINISTRATOR_ROLE_ASSUME_ROLE_POLICY), + )["Role"]["Arn"] + + # Put Role Policy with AgentCore privilege escalation permissions + policy_name = "agentcore_policy" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:PassRole", + "bedrock-agentcore:CreateCodeInterpreter", + "bedrock-agentcore:InvokeCodeInterpreter", + ], + "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), + ), + ): + # Test Check + 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 + assert result[0].status == "FAIL" + assert result[0].resource_id == f"{role_name}/{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_name} allows privilege escalation using the following actions: ", + result[0].status_extended, + ) + assert search("iam:PassRole", result[0].status_extended) + assert search( + "bedrock-agentcore:CreateCodeInterpreter", result[0].status_extended + ) + assert search( + "bedrock-agentcore:InvokeCodeInterpreter", result[0].status_extended + ) 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 d1a636a009..79e1b25955 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 @@ -322,17 +322,16 @@ class Test_iam_policy_allows_privilege_escalation: check = iam_policy_allows_privilege_escalation() result = check.execute() assert len(result) == 1 - assert result[0].status == "FAIL" + assert result[0].status == "PASS" assert result[0].resource_id == policy_name assert result[0].resource_arn == policy_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] assert search( - f"Custom Policy {policy_arn} allows privilege escalation using the following actions: ", + f"Custom Policy {policy_arn} does not allow privilege escalation", result[0].status_extended, ) - assert search("iam:PassRole", result[0].status_extended) @mock_aws def test_iam_policy_allows_privilege_escalation_iam_PassRole_using_wildcard( @@ -375,17 +374,16 @@ class Test_iam_policy_allows_privilege_escalation: check = iam_policy_allows_privilege_escalation() result = check.execute() assert len(result) == 1 - assert result[0].status == "FAIL" + assert result[0].status == "PASS" assert result[0].resource_id == policy_name assert result[0].resource_arn == policy_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] assert search( - f"Custom Policy {policy_arn} allows privilege escalation using the following actions: ", + f"Custom Policy {policy_arn} does not allow privilege escalation", result[0].status_extended, ) - assert search("iam:PassRole", result[0].status_extended) @mock_aws def test_iam_policy_allows_privilege_escalation_two_combinations( @@ -508,17 +506,16 @@ class Test_iam_policy_allows_privilege_escalation: check = iam_policy_allows_privilege_escalation() result = check.execute() assert len(result) == 1 - assert result[0].status == "FAIL" + assert result[0].status == "PASS" assert result[0].resource_id == policy_name assert result[0].resource_arn == policy_arn assert result[0].region == AWS_REGION_US_EAST_1 assert result[0].resource_tags == [] assert search( - f"Custom Policy {policy_arn} allows privilege escalation using the following actions: ", + f"Custom Policy {policy_arn} does not allow privilege escalation", result[0].status_extended, ) - assert search("iam:PassRole", result[0].status_extended) @mock_aws def test_iam_policy_allows_privilege_escalation_policies_combination( @@ -915,6 +912,71 @@ class Test_iam_policy_allows_privilege_escalation: ]: assert search(permission, finding.status_extended) + @mock_aws + def test_iam_policy_allows_privilege_escalation_agentcore_passrole_create_invoke( + self, + ): + """Test detection of AWS Bedrock AgentCore privilege escalation pattern.""" + aws_provider = set_mocked_aws_provider([AWS_REGION_US_EAST_1]) + iam_client = client("iam", region_name=AWS_REGION_US_EAST_1) + policy_name = "agentcore_privilege_escalation_policy" + policy_document = { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "iam:PassRole", + "bedrock-agentcore:CreateCodeInterpreter", + "bedrock-agentcore:InvokeCodeInterpreter", + ], + "Resource": "*", + } + ], + } + + policy_arn = iam_client.create_policy( + PolicyName=policy_name, PolicyDocument=dumps(policy_document) + )["Policy"]["Arn"] + + 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), + ), + ): + # Test Check + 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 + assert result[0].status == "FAIL" + assert result[0].resource_id == policy_name + assert result[0].resource_arn == policy_arn + assert result[0].region == AWS_REGION_US_EAST_1 + assert result[0].resource_tags == [] + + assert search( + f"Custom Policy {policy_arn} allows privilege escalation using the following actions: ", + result[0].status_extended, + ) + assert search("iam:PassRole", result[0].status_extended) + assert search( + "bedrock-agentcore:CreateCodeInterpreter", result[0].status_extended + ) + assert search( + "bedrock-agentcore:InvokeCodeInterpreter", result[0].status_extended + ) + @mock_aws def test_iam_policy_allows_privilege_escalation_iam_put( self, 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 3b3123709f..018af5e1ec 100644 --- a/tests/providers/aws/services/iam/lib/privilege_escalation_test.py +++ b/tests/providers/aws/services/iam/lib/privilege_escalation_test.py @@ -52,7 +52,6 @@ class Test_PrivilegeEscalation: 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 @@ -78,9 +77,9 @@ class Test_PrivilegeEscalation: ], } result = check_privilege_escalation(policy) - assert "iam:PassRole" in result + assert result == "" - def test_check_privilege_escalation_priv_escalation_iam_PassRole_using_wildcard( + def test_check_privilege_escalation_priv_escalation_iam_wildcard( self, ): policy = { @@ -88,13 +87,16 @@ class Test_PrivilegeEscalation: "Statement": [ { "Effect": "Allow", - "Action": ["iam:*Role"], # Should expand to include PassRole + "Action": [ + "iam:*" + ], # Should expand to include multiple IAM actions "Resource": ["*"], } ], } result = check_privilege_escalation(policy) - assert "iam:PassRole" in result + # iam:* should expand to include PutUserPolicy and other privilege escalation actions + assert "iam:PutUserPolicy" in result def test_check_privilege_escalation_priv_escalation_not_action( self, @@ -117,7 +119,6 @@ class Test_PrivilegeEscalation: assert "'iam:PutGroupPolicy'" 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 diff --git a/tests/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public_test.py b/tests/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public_test.py index 5925a435cd..392582b0ee 100644 --- a/tests/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public_test.py +++ b/tests/providers/aws/services/kafka/kafka_cluster_is_public/kafka_cluster_is_public_test.py @@ -72,10 +72,10 @@ class Test_kafka_cluster_is_public: result = check.execute() assert len(result) == 1 - assert result[0].status == "FAIL" + assert result[0].status == "PASS" assert ( result[0].status_extended - == "Kafka cluster 'demo-cluster-1' is publicly accessible." + == "Kafka cluster demo-cluster-1 is not publicly accessible." ) assert ( result[0].resource_arn @@ -126,10 +126,10 @@ class Test_kafka_cluster_is_public: result = check.execute() assert len(result) == 1 - assert result[0].status == "PASS" + assert result[0].status == "FAIL" assert ( result[0].status_extended - == "Kafka cluster 'demo-cluster-1' is not publicly accessible." + == "Kafka cluster demo-cluster-1 is publicly accessible." ) assert ( result[0].resource_arn diff --git a/tests/providers/azure/azure_provider_test.py b/tests/providers/azure/azure_provider_test.py index 820ea8785f..5796b15ada 100644 --- a/tests/providers/azure/azure_provider_test.py +++ b/tests/providers/azure/azure_provider_test.py @@ -85,6 +85,7 @@ class TestAzureProvider: "python_latest_version": "3.12", "java_latest_version": "17", "recommended_minimal_tls_versions": ["1.2", "1.3"], + "vm_backup_min_daily_retention_days": 7, "desired_vm_sku_sizes": [ "Standard_A8_v2", "Standard_DS3_v2", diff --git a/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py b/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py index d318175a22..7f24fe6284 100644 --- a/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py +++ b/tests/providers/azure/services/app/app_http_logs_enabled/app_http_logs_enabled_test.py @@ -136,26 +136,32 @@ class Test_app_http_logs_enabled: logs=[ mock.MagicMock( category="AppServiceHTTPLogs", + category_group=None, enabled=True, ), mock.MagicMock( category="AppServiceConsoleLogs", + category_group=None, enabled=False, ), mock.MagicMock( category="AppServiceAppLogs", + category_group=None, enabled=True, ), mock.MagicMock( category="AppServiceAuditLogs", + category_group=None, enabled=False, ), mock.MagicMock( category="AppServiceIPSecAuditLogs", + category_group=None, enabled=False, ), mock.MagicMock( category="AppServicePlatformLogs", + category_group=None, enabled=False, ), ], @@ -181,26 +187,32 @@ class Test_app_http_logs_enabled: logs=[ mock.MagicMock( category="AppServiceHTTPLogs", + category_group=None, enabled=True, ), mock.MagicMock( category="AppServiceConsoleLogs", + category_group=None, enabled=True, ), mock.MagicMock( category="AppServiceAppLogs", + category_group=None, enabled=True, ), mock.MagicMock( category="AppServiceAuditLogs", + category_group=None, enabled=False, ), mock.MagicMock( category="AppServiceIPSecAuditLogs", + category_group=None, enabled=True, ), mock.MagicMock( category="AppServicePlatformLogs", + category_group=None, enabled=False, ), ], @@ -223,3 +235,129 @@ class Test_app_http_logs_enabled: result[0].status_extended == f"App app_id-2 has HTTP Logs enabled in diagnostic setting name_diagnostic_setting2 in subscription {AZURE_SUBSCRIPTION_ID}" ) + + def test_diagnostic_setting_with_all_logs_category_group(self): + app_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.app.app_http_logs_enabled.app_http_logs_enabled.app_client", + new=app_client, + ), + ): + from prowler.providers.azure.services.app.app_http_logs_enabled.app_http_logs_enabled import ( + app_http_logs_enabled, + ) + from prowler.providers.azure.services.app.app_service import WebApp + from prowler.providers.azure.services.monitor.monitor_service import ( + DiagnosticSetting, + ) + + app_client.apps = { + AZURE_SUBSCRIPTION_ID: { + "resource_id3": WebApp( + resource_id="resource_id3", + name="app_id-3", + auth_enabled=True, + configurations=None, + client_cert_mode="Ignore", + https_only=False, + kind="WebApp", + identity=mock.MagicMock, + location="West Europe", + monitor_diagnostic_settings=[ + DiagnosticSetting( + id="id3/id3", + logs=[ + mock.MagicMock( + category=None, + category_group="allLogs", + enabled=True, + ), + ], + storage_account_name="storage_account_name3", + storage_account_id="storage_account_id3", + name="name_diagnostic_setting3", + ), + ], + ), + } + } + check = app_http_logs_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "app_id-3" + assert result[0].resource_id == "resource_id3" + assert ( + result[0].status_extended + == f"App app_id-3 has allLogs category group which includes HTTP Logs enabled in diagnostic setting name_diagnostic_setting3 in subscription {AZURE_SUBSCRIPTION_ID}" + ) + + def test_diagnostic_setting_with_all_logs_category_group_disabled(self): + app_client = mock.MagicMock + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.app.app_http_logs_enabled.app_http_logs_enabled.app_client", + new=app_client, + ), + ): + from prowler.providers.azure.services.app.app_http_logs_enabled.app_http_logs_enabled import ( + app_http_logs_enabled, + ) + from prowler.providers.azure.services.app.app_service import WebApp + from prowler.providers.azure.services.monitor.monitor_service import ( + DiagnosticSetting, + ) + + app_client.apps = { + AZURE_SUBSCRIPTION_ID: { + "resource_id4": WebApp( + resource_id="resource_id4", + name="app_id-4", + auth_enabled=True, + configurations=None, + client_cert_mode="Ignore", + https_only=False, + kind="WebApp", + identity=mock.MagicMock, + location="West Europe", + monitor_diagnostic_settings=[ + DiagnosticSetting( + id="id4/id4", + logs=[ + mock.MagicMock( + category=None, + category_group="allLogs", + enabled=False, # Disabled + ), + ], + storage_account_name="storage_account_name4", + storage_account_id="storage_account_id4", + name="name_diagnostic_setting4", + ), + ], + ), + } + } + check = app_http_logs_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == "app_id-4" + assert result[0].resource_id == "resource_id4" + assert ( + result[0].status_extended + == f"App app_id-4 does not have HTTP Logs enabled in diagnostic setting name_diagnostic_setting4 in subscription {AZURE_SUBSCRIPTION_ID}" + ) diff --git a/tests/providers/azure/services/defender/defender_service_test.py b/tests/providers/azure/services/defender/defender_service_test.py index 20156640ee..c3189a8e20 100644 --- a/tests/providers/azure/services/defender/defender_service_test.py +++ b/tests/providers/azure/services/defender/defender_service_test.py @@ -6,6 +6,7 @@ from prowler.providers.azure.services.defender.defender_service import ( AutoProvisioningSetting, Defender, IoTSecuritySolution, + JITPolicy, Pricing, SecurityContactConfiguration, Setting, @@ -103,6 +104,19 @@ def mock_defender_get_iot_security_solutions(_): } +def mock_defender_get_jit_policies(_): + return { + AZURE_SUBSCRIPTION_ID: { + "policy-1": JITPolicy( + id="policy-1", + name="JITPolicy1", + location="eastus", + vm_ids=["vm-1", "vm-2"], + ) + } + } + + @patch( "prowler.providers.azure.services.defender.defender_service.Defender._get_pricings", new=mock_defender_get_pricings, @@ -127,6 +141,10 @@ def mock_defender_get_iot_security_solutions(_): "prowler.providers.azure.services.defender.defender_service.Defender._get_iot_security_solutions", new=mock_defender_get_iot_security_solutions, ) +@patch( + "prowler.providers.azure.services.defender.defender_service.Defender._get_jit_policies", + new=mock_defender_get_jit_policies, +) class Test_Defender_Service: def test_get_client(self): defender = Defender(set_mocked_azure_provider()) @@ -255,3 +273,13 @@ class Test_Defender_Service: ].status == "Enabled" ) + + def test_get_jit_policies(self): + defender = Defender(set_mocked_azure_provider()) + assert AZURE_SUBSCRIPTION_ID in defender.jit_policies + assert "policy-1" in defender.jit_policies[AZURE_SUBSCRIPTION_ID] + policy1 = defender.jit_policies[AZURE_SUBSCRIPTION_ID]["policy-1"] + assert policy1.id == "policy-1" + assert policy1.name == "JITPolicy1" + assert policy1.location == "eastus" + assert set(policy1.vm_ids) == {"vm-1", "vm-2"} diff --git a/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py b/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py index 7ae8dc32eb..cfe2f5a00b 100644 --- a/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py +++ b/tests/providers/azure/services/storage/storage_geo_redundant_enabled/storage_geo_redundant_enabled_test.py @@ -4,7 +4,6 @@ from uuid import uuid4 from prowler.providers.azure.services.storage.storage_service import ( Account, NetworkRuleSet, - ReplicationSettings, ) from tests.providers.azure.azure_fixtures import ( AZURE_SUBSCRIPTION_ID, @@ -35,10 +34,11 @@ class Test_storage_geo_redundant_enabled: result = check.execute() assert len(result) == 0 - def test_storage_geo_redundant_enabled(self): + def test_storage_account_standard_grs_enabled(self): storage_account_id = str(uuid4()) storage_account_name = "Test Storage Account GRS" storage_client = mock.MagicMock() + replication_setting = "Standard_GRS" storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -56,7 +56,7 @@ class Test_storage_geo_redundant_enabled: private_endpoint_connections=[], key_expiration_period_in_days=None, location="westeurope", - replication_settings=ReplicationSettings.STANDARD_GRS, + replication_settings=replication_setting, ) ] } @@ -81,17 +81,18 @@ class Test_storage_geo_redundant_enabled: assert result[0].status == "PASS" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has Geo-redundant storage (GRS) enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has Geo-redundant storage {replication_setting} enabled." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name assert result[0].resource_id == storage_account_id assert result[0].location == "westeurope" - def test_storage_account_geo_redundant_disabled(self): + def test_storage_account_standard_ragrs_enabled(self): storage_account_id = str(uuid4()) - storage_account_name = "Test Storage Account LRS" + storage_account_name = "Test Storage Account RAGRS" storage_client = mock.MagicMock() + replication_setting = "Standard_RAGRS" storage_client.storage_accounts = { AZURE_SUBSCRIPTION_ID: [ Account( @@ -109,7 +110,169 @@ class Test_storage_geo_redundant_enabled: private_endpoint_connections=[], key_expiration_period_in_days=None, location="westeurope", - replication_settings=ReplicationSettings.STANDARD_LRS, + replication_settings=replication_setting, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled import ( + storage_geo_redundant_enabled, + ) + + check = storage_geo_redundant_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has Geo-redundant storage {replication_setting} enabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == storage_account_id + assert result[0].location == "westeurope" + + def test_storage_account_standard_gzrs_enabled(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account GZRS" + storage_client = mock.MagicMock() + replication_setting = "Standard_GZRS" + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="westeurope", + replication_settings=replication_setting, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled import ( + storage_geo_redundant_enabled, + ) + + check = storage_geo_redundant_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has Geo-redundant storage {replication_setting} enabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == storage_account_id + assert result[0].location == "westeurope" + + def test_storage_account_standard_ragzrs_enabled(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account RAGZRS" + storage_client = mock.MagicMock() + replication_setting = "Standard_RAGZRS" + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="westeurope", + replication_settings=replication_setting, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled import ( + storage_geo_redundant_enabled, + ) + + check = storage_geo_redundant_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert ( + result[0].status_extended + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} has Geo-redundant storage {replication_setting} enabled." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == storage_account_id + assert result[0].location == "westeurope" + + def test_storage_account_standard_lrs_disabled(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account LRS" + storage_client = mock.MagicMock() + replication_setting = "Standard_LRS" + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="westeurope", + replication_settings=replication_setting, ) ] } @@ -134,7 +297,169 @@ class Test_storage_geo_redundant_enabled: assert result[0].status == "FAIL" assert ( result[0].status_extended - == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have Geo-redundant storage (GRS) enabled." + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have Geo-redundant storage enabled, it has {replication_setting} instead." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == storage_account_id + assert result[0].location == "westeurope" + + def test_storage_account_standard_zrs_disabled(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account ZRS" + storage_client = mock.MagicMock() + replication_setting = "Standard_ZRS" + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="westeurope", + replication_settings=replication_setting, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled import ( + storage_geo_redundant_enabled, + ) + + check = storage_geo_redundant_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have Geo-redundant storage enabled, it has {replication_setting} instead." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == storage_account_id + assert result[0].location == "westeurope" + + def test_storage_account_premium_lrs_disabled(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account Premium LRS" + storage_client = mock.MagicMock() + replication_setting = "Premium_LRS" + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="westeurope", + replication_settings=replication_setting, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled import ( + storage_geo_redundant_enabled, + ) + + check = storage_geo_redundant_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have Geo-redundant storage enabled, it has {replication_setting} instead." + ) + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == storage_account_name + assert result[0].resource_id == storage_account_id + assert result[0].location == "westeurope" + + def test_storage_account_premium_zrs_disabled(self): + storage_account_id = str(uuid4()) + storage_account_name = "Test Storage Account Premium ZRS" + storage_client = mock.MagicMock() + replication_setting = "Premium_ZRS" + storage_client.storage_accounts = { + AZURE_SUBSCRIPTION_ID: [ + Account( + id=storage_account_id, + name=storage_account_name, + resouce_group_name="rg", + enable_https_traffic_only=False, + infrastructure_encryption=False, + allow_blob_public_access=False, + network_rule_set=NetworkRuleSet( + bypass="AzureServices", default_action="Allow" + ), + encryption_type="None", + minimum_tls_version="TLS1_2", + private_endpoint_connections=[], + key_expiration_period_in_days=None, + location="westeurope", + replication_settings=replication_setting, + ) + ] + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled.storage_client", + new=storage_client, + ), + ): + from prowler.providers.azure.services.storage.storage_geo_redundant_enabled.storage_geo_redundant_enabled import ( + storage_geo_redundant_enabled, + ) + + check = storage_geo_redundant_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert ( + result[0].status_extended + == f"Storage account {storage_account_name} from subscription {AZURE_SUBSCRIPTION_ID} does not have Geo-redundant storage enabled, it has {replication_setting} instead." ) assert result[0].subscription == AZURE_SUBSCRIPTION_ID assert result[0].resource_name == storage_account_name diff --git a/tests/providers/azure/services/storage/storage_service_test.py b/tests/providers/azure/services/storage/storage_service_test.py index 6eb21052a2..912403a0d3 100644 --- a/tests/providers/azure/services/storage/storage_service_test.py +++ b/tests/providers/azure/services/storage/storage_service_test.py @@ -6,7 +6,6 @@ from prowler.providers.azure.services.storage.storage_service import ( DeleteRetentionPolicy, FileServiceProperties, NetworkRuleSet, - ReplicationSettings, SMBProtocolSettings, Storage, ) @@ -53,7 +52,7 @@ def mock_storage_get_storage_accounts(_): location="westeurope", blob_properties=blob_properties, default_to_entra_authorization=True, - replication_settings=ReplicationSettings.STANDARD_LRS, + replication_settings="Standard_LRS", allow_cross_tenant_replication=True, allow_shared_key_access=True, file_service_properties=file_service_properties, @@ -150,7 +149,7 @@ class Test_Storage_Service: ].default_to_entra_authorization assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][0].replication_settings - == ReplicationSettings.STANDARD_LRS + == "Standard_LRS" ) assert ( storage.storage_accounts[AZURE_SUBSCRIPTION_ID][ diff --git a/tests/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled_test.py b/tests/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled_test.py new file mode 100644 index 0000000000..03991f8049 --- /dev/null +++ b/tests/providers/azure/services/vm/vm_jit_access_enabled/vm_jit_access_enabled_test.py @@ -0,0 +1,290 @@ +from unittest import mock +from uuid import uuid4 + +from prowler.providers.azure.services.defender.defender_service import JITPolicy +from prowler.providers.azure.services.vm.vm_service import VirtualMachine +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_vm_jit_access_enabled: + def test_no_subscriptions(self): + vm_client = mock.MagicMock() + vm_client.virtual_machines = {} + defender_client = mock.MagicMock() + defender_client.jit_policies = {} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import ( + vm_jit_access_enabled, + ) + + check = vm_jit_access_enabled() + result = check.execute() + assert result == [] + + def test_no_vms(self): + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} + defender_client = mock.MagicMock() + defender_client.jit_policies = {AZURE_SUBSCRIPTION_ID: {}} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import ( + vm_jit_access_enabled, + ) + + check = vm_jit_access_enabled() + result = check.execute() + assert result == [] + + def test_vm_with_jit_enabled(self): + vm_id = str(uuid4()) + vm_name = "TestVM" + vm_location = "eastus" + vm = VirtualMachine( + resource_id=vm_id, + resource_name=vm_name, + location=vm_location, + security_profile=None, + extensions=[], + storage_profile=None, + ) + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + defender_client = mock.MagicMock() + jit_policy = JITPolicy( + id="policy1", + name="JITPolicy1", + location="eastus", + vm_ids={vm_id}, + ) + defender_client.jit_policies = { + AZURE_SUBSCRIPTION_ID: {jit_policy.id: jit_policy} + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import ( + vm_jit_access_enabled, + ) + + check = vm_jit_access_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_id == vm_id + assert result[0].resource_name == vm_name + assert "has JIT (Just-in-Time) access enabled" in result[0].status_extended + + def test_vm_with_jit_disabled(self): + vm_id = str(uuid4()) + vm_name = "TestVM" + vm_location = "eastus" + vm = VirtualMachine( + resource_id=vm_id, + resource_name=vm_name, + location=vm_location, + security_profile=None, + extensions=[], + storage_profile=None, + ) + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + defender_client = mock.MagicMock() + # JIT policy does not include this VM + jit_policy = JITPolicy( + id="policy1", + name="JITPolicy1", + location="eastus", + vm_ids={"some-other-id"}, + ) + defender_client.jit_policies = { + AZURE_SUBSCRIPTION_ID: {jit_policy.id: jit_policy} + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import ( + vm_jit_access_enabled, + ) + + check = vm_jit_access_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_id == vm_id + assert result[0].resource_name == vm_name + assert ( + "does not have JIT (Just-in-Time) access enabled" + in result[0].status_extended + ) + + def test_vm_id_case_insensitivity(self): + vm_id = str(uuid4()) + vm_name = "TestVM" + vm_location = "eastus" + upper_vm_id = vm_id.upper() + vm = VirtualMachine( + resource_id=upper_vm_id, + resource_name=vm_name, + location=vm_location, + security_profile=None, + extensions=[], + storage_profile=None, + ) + vm_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {upper_vm_id: vm}} + defender_client = mock.MagicMock() + jit_policy = JITPolicy( + id="policy1", + name="JITPolicy1", + location="eastus", + vm_ids={vm_id.lower()}, + ) + defender_client.jit_policies = { + AZURE_SUBSCRIPTION_ID: {jit_policy.id: jit_policy} + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import ( + vm_jit_access_enabled, + ) + + check = vm_jit_access_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].resource_id == upper_vm_id + assert "has JIT (Just-in-Time) access enabled" in result[0].status_extended + + def test_multiple_vms_and_policies(self): + vm_id_1 = str(uuid4()) + vm_id_2 = str(uuid4()) + vm1 = VirtualMachine( + resource_id=vm_id_1, + resource_name="VM1", + location="eastus", + security_profile=None, + extensions=[], + storage_profile=None, + ) + vm2 = VirtualMachine( + resource_id=vm_id_2, + resource_name="VM2", + location="eastus", + security_profile=None, + extensions=[], + storage_profile=None, + ) + vm_client = mock.MagicMock() + vm_client.virtual_machines = { + AZURE_SUBSCRIPTION_ID: {vm_id_1: vm1, vm_id_2: vm2} + } + defender_client = mock.MagicMock() + jit_policy_1 = JITPolicy( + id="policy1", + name="JITPolicy1", + location="eastus", + vm_ids={vm_id_1}, + ) + jit_policy_2 = JITPolicy( + id="policy2", + name="JITPolicy2", + location="eastus", + vm_ids=set(), + ) + defender_client.jit_policies = { + AZURE_SUBSCRIPTION_ID: { + jit_policy_1.id: jit_policy_1, + jit_policy_2.id: jit_policy_2, + } + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled.defender_client", + new=defender_client, + ), + ): + from prowler.providers.azure.services.vm.vm_jit_access_enabled.vm_jit_access_enabled import ( + vm_jit_access_enabled, + ) + + check = vm_jit_access_enabled() + result = check.execute() + assert len(result) == 2 + for r in result: + if r.resource_id == vm_id_1: + assert r.status == "PASS" + elif r.resource_id == vm_id_2: + assert r.status == "FAIL" diff --git a/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py b/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py new file mode 100644 index 0000000000..b43b75548a --- /dev/null +++ b/tests/providers/azure/services/vm/vm_sufficient_daily_backup_retention_period/vm_sufficient_daily_backup_retention_period_test.py @@ -0,0 +1,323 @@ +from unittest import mock +from uuid import uuid4 + +from tests.providers.azure.azure_fixtures import ( + AZURE_SUBSCRIPTION_ID, + set_mocked_azure_provider, +) + + +class Test_vm_sufficient_daily_backup_retention_period: + def test_no_subscriptions(self): + vm_client = mock.MagicMock() + recovery_client = mock.MagicMock() + vm_client.virtual_machines = {} + recovery_client.vaults = {} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client", + new=recovery_client, + ), + ): + from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import ( + vm_sufficient_daily_backup_retention_period, + ) + + check = vm_sufficient_daily_backup_retention_period() + result = check.execute() + assert len(result) == 0 + + def test_no_vms(self): + vm_client = mock.MagicMock() + recovery_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {}} + recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {}} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client", + new=recovery_client, + ), + ): + from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import ( + vm_sufficient_daily_backup_retention_period, + ) + + check = vm_sufficient_daily_backup_retention_period() + result = check.execute() + assert len(result) == 0 + + def test_vm_with_sufficient_retention(self): + from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType + + from prowler.providers.azure.services.recovery.recovery_service import ( + BackupItem, + BackupPolicy, + BackupVault, + ) + from prowler.providers.azure.services.vm.vm_service import ( + ManagedDiskParameters, + OSDisk, + StorageProfile, + VirtualMachine, + ) + + vm_id = str(uuid4()) + vm_name = "VMTest" + vault_id = str(uuid4()) + policy_id = str(uuid4()) + retention_days = 14 + min_retention_days = 7 + + vm = VirtualMachine( + resource_id=vm_id, + resource_name=vm_name, + location="eastus", + security_profile=None, + extensions=[], + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters(id="managed_disk_id"), + ), + data_disks=[], + ), + ) + backup_item = BackupItem( + id=str(uuid4()), + name=f"someprefix;{vm_name}", + workload_type=DataSourceType.VM, + backup_policy_id=policy_id, + ) + backup_policy = BackupPolicy( + id=policy_id, + name="policy1", + retention_days=retention_days, + ) + vault = BackupVault( + id=vault_id, + name="vault1", + location="eastus", + backup_protected_items={backup_item.id: backup_item}, + backup_policies={policy_id: backup_policy}, + ) + vm_client = mock.MagicMock() + recovery_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} + vm_client.audit_config = { + "vm_backup_min_daily_retention_days": min_retention_days + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider( + audit_config=vm_client.audit_config + ), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client", + new=recovery_client, + ), + ): + from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import ( + vm_sufficient_daily_backup_retention_period, + ) + + check = vm_sufficient_daily_backup_retention_period() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == vm_name + assert result[0].resource_id == vm_id + assert ( + f"has a daily backup retention period of {retention_days} days" + in result[0].status_extended + ) + + def test_vm_with_insufficient_retention(self): + from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType + + from prowler.providers.azure.services.recovery.recovery_service import ( + BackupItem, + BackupPolicy, + BackupVault, + ) + from prowler.providers.azure.services.vm.vm_service import ( + ManagedDiskParameters, + OSDisk, + StorageProfile, + VirtualMachine, + ) + + vm_id = str(uuid4()) + vm_name = "VMTest" + vault_id = str(uuid4()) + policy_id = str(uuid4()) + retention_days = 3 + min_retention_days = 7 + + vm = VirtualMachine( + resource_id=vm_id, + resource_name=vm_name, + location="eastus", + security_profile=None, + extensions=[], + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters(id="managed_disk_id"), + ), + data_disks=[], + ), + ) + backup_item = BackupItem( + id=str(uuid4()), + name=f"someprefix;{vm_name}", + workload_type=DataSourceType.VM, + backup_policy_id=policy_id, + ) + backup_policy = BackupPolicy( + id=policy_id, + name="policy1", + retention_days=retention_days, + ) + vault = BackupVault( + id=vault_id, + name="vault1", + location="eastus", + backup_protected_items={backup_item.id: backup_item}, + backup_policies={policy_id: backup_policy}, + ) + vm_client = mock.MagicMock() + recovery_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} + vm_client.audit_config = { + "vm_backup_min_daily_retention_days": min_retention_days + } + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider( + audit_config=vm_client.audit_config + ), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client", + new=recovery_client, + ), + ): + from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import ( + vm_sufficient_daily_backup_retention_period, + ) + + check = vm_sufficient_daily_backup_retention_period() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "FAIL" + assert result[0].subscription == AZURE_SUBSCRIPTION_ID + assert result[0].resource_name == vm_name + assert result[0].resource_id == vm_id + assert ( + f"has insufficient daily backup retention period of {retention_days} days" + in result[0].status_extended + ) + + def test_vm_with_no_backup_policy(self): + from azure.mgmt.recoveryservicesbackup.activestamp.models import DataSourceType + + from prowler.providers.azure.services.recovery.recovery_service import ( + BackupItem, + BackupVault, + ) + from prowler.providers.azure.services.vm.vm_service import ( + ManagedDiskParameters, + OSDisk, + StorageProfile, + VirtualMachine, + ) + + vm_id = str(uuid4()) + vm_name = "VMTest" + vault_id = str(uuid4()) + + vm = VirtualMachine( + resource_id=vm_id, + resource_name=vm_name, + location="eastus", + security_profile=None, + extensions=[], + storage_profile=StorageProfile( + os_disk=OSDisk( + name="os_disk_name", + operating_system_type="Linux", + managed_disk=ManagedDiskParameters(id="managed_disk_id"), + ), + data_disks=[], + ), + ) + backup_item = BackupItem( + id=str(uuid4()), + name=f"someprefix;{vm_name}", + workload_type=DataSourceType.VM, + backup_policy_id=None, + ) + vault = BackupVault( + id=vault_id, + name="vault1", + location="eastus", + backup_protected_items={backup_item.id: backup_item}, + backup_policies={}, + ) + vm_client = mock.MagicMock() + recovery_client = mock.MagicMock() + vm_client.virtual_machines = {AZURE_SUBSCRIPTION_ID: {vm_id: vm}} + recovery_client.vaults = {AZURE_SUBSCRIPTION_ID: {vault_id: vault}} + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_azure_provider(), + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.vm_client", + new=vm_client, + ), + mock.patch( + "prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period.recovery_client", + new=recovery_client, + ), + ): + from prowler.providers.azure.services.vm.vm_sufficient_daily_backup_retention_period.vm_sufficient_daily_backup_retention_period import ( + vm_sufficient_daily_backup_retention_period, + ) + + check = vm_sufficient_daily_backup_retention_period() + result = check.execute() + assert len(result) == 0 diff --git a/tests/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled_test.py b/tests/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled_test.py index 0dcacf5513..51654533b6 100644 --- a/tests/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled_test.py +++ b/tests/providers/gcp/services/compute/compute_project_os_login_enabled/compute_project_os_login_enabled_test.py @@ -175,7 +175,7 @@ class Test_compute_project_os_login_enabled: result[0].status_extended, ) assert result[0].resource_id == project.id - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].location == "global" assert result[0].project_id == GCP_PROJECT_ID @@ -225,6 +225,6 @@ class Test_compute_project_os_login_enabled: result[0].status_extended, ) assert result[0].resource_id == project.id - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].location == "global" assert result[0].project_id == GCP_PROJECT_ID diff --git a/tests/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled_test.py b/tests/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled_test.py index 395cffb6f0..fab9264535 100644 --- a/tests/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled_test.py +++ b/tests/providers/gcp/services/iam/iam_account_access_approval_enabled/iam_account_access_approval_enabled_test.py @@ -44,6 +44,7 @@ class Test_iam_account_access_approval_enabled: result[0].status_extended, ) assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == "test" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == "global" @@ -95,5 +96,58 @@ class Test_iam_account_access_approval_enabled: result[0].status_extended, ) assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == "test" + assert result[0].project_id == GCP_PROJECT_ID + assert result[0].location == "global" + + def test_iam_project_with_settings_empty_project_name(self): + cloudresourcemanager_client = mock.MagicMock() + accessapproval_client = mock.MagicMock() + accessapproval_client.project_ids = [GCP_PROJECT_ID] + accessapproval_client.region = "global" + accessapproval_client.projects = { + GCP_PROJECT_ID: GCPProject( + id=GCP_PROJECT_ID, + number="123456789012", + name="", + labels={}, + lifecycle_state="ACTIVE", + ) + } + + with ( + mock.patch( + "prowler.providers.common.provider.Provider.get_global_provider", + return_value=set_mocked_gcp_provider(), + ), + mock.patch( + "prowler.providers.gcp.services.iam.iam_account_access_approval_enabled.iam_account_access_approval_enabled.accessapproval_client", + new=accessapproval_client, + ), + mock.patch( + "prowler.providers.gcp.services.iam.iam_service.cloudresourcemanager_client", + new=cloudresourcemanager_client, + ), + ): + from prowler.providers.gcp.services.iam.iam_service import Setting + + accessapproval_client.settings = { + GCP_PROJECT_ID: Setting(name="test", project_id=GCP_PROJECT_ID) + } + + from prowler.providers.gcp.services.iam.iam_account_access_approval_enabled.iam_account_access_approval_enabled import ( + iam_account_access_approval_enabled, + ) + + check = iam_account_access_approval_enabled() + result = check.execute() + assert len(result) == 1 + assert result[0].status == "PASS" + assert search( + "has Access Approval enabled", + result[0].status_extended, + ) + assert result[0].resource_id == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == "global" diff --git a/tests/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled_test.py b/tests/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled_test.py index df1185cac3..c5a773f127 100644 --- a/tests/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled_test.py +++ b/tests/providers/gcp/services/iam/iam_audit_logs_enabled/iam_audit_logs_enabled_test.py @@ -176,7 +176,7 @@ class Test_iam_audit_logs_enabled: r.status_extended, ) assert r.resource_id == GCP_PROJECT_ID - assert r.resource_name == GCP_PROJECT_ID + assert r.resource_name == "GCP Project" assert r.project_id == GCP_PROJECT_ID assert r.location == cloudresourcemanager_client.region @@ -226,6 +226,6 @@ class Test_iam_audit_logs_enabled: r.status_extended, ) assert r.resource_id == GCP_PROJECT_ID - assert r.resource_name == GCP_PROJECT_ID + assert r.resource_name == "GCP Project" assert r.project_id == GCP_PROJECT_ID assert r.location == cloudresourcemanager_client.region diff --git a/tests/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level_test.py b/tests/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level_test.py index 66744d7ef6..45bbdd4b74 100644 --- a/tests/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level_test.py +++ b/tests/providers/gcp/services/iam/iam_no_service_roles_at_project_level/iam_no_service_roles_at_project_level_test.py @@ -113,7 +113,7 @@ class Test_iam_no_service_roles_at_project_level: result[0].status_extended, ) assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "test" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == cloudresourcemanager_client.region @@ -255,6 +255,6 @@ class Test_iam_no_service_roles_at_project_level: result[0].status_extended, ) assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == cloudresourcemanager_client.region diff --git a/tests/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties_test.py b/tests/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties_test.py index 95948eb8a6..87efc77174 100644 --- a/tests/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties_test.py +++ b/tests/providers/gcp/services/iam/iam_role_kms_enforce_separation_of_duties/iam_role_kms_enforce_separation_of_duties_test.py @@ -213,7 +213,7 @@ class Test_iam_role_kms_enforce_separation_of_duties: r.status_extended, ) assert r.resource_id == GCP_PROJECT_ID - assert r.resource_name == GCP_PROJECT_ID + assert r.resource_name == "GCP Project" assert r.project_id == GCP_PROJECT_ID assert r.location == cloudresourcemanager_client.region @@ -277,6 +277,6 @@ class Test_iam_role_kms_enforce_separation_of_duties: r.status_extended, ) assert r.resource_id == GCP_PROJECT_ID - assert r.resource_name == GCP_PROJECT_ID + assert r.resource_name == "GCP Project" assert r.project_id == GCP_PROJECT_ID assert r.location == cloudresourcemanager_client.region diff --git a/tests/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties_test.py b/tests/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties_test.py index 5672fc78ad..a15ade396c 100644 --- a/tests/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties_test.py +++ b/tests/providers/gcp/services/iam/iam_role_sa_enforce_separation_of_duties/iam_role_sa_enforce_separation_of_duties_test.py @@ -213,7 +213,7 @@ class Test_iam_role_sa_enforce_separation_of_duties: r.status_extended, ) assert r.resource_id == GCP_PROJECT_ID - assert r.resource_name == GCP_PROJECT_ID + assert r.resource_name == "GCP Project" assert r.project_id == GCP_PROJECT_ID assert r.location == cloudresourcemanager_client.region @@ -277,6 +277,6 @@ class Test_iam_role_sa_enforce_separation_of_duties: r.status_extended, ) assert r.resource_id == GCP_PROJECT_ID - assert r.resource_name == GCP_PROJECT_ID + assert r.resource_name == "GCP Project" assert r.project_id == GCP_PROJECT_ID assert r.location == cloudresourcemanager_client.region diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py index 99fbd43d5e..a38f97d81c 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_audit_configuration_changes_enabled_test.py @@ -141,7 +141,7 @@ class Test_logging_log_metric_filter_and_alert_for_audit_configuration_changes_e == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." ) assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py index 3faf9c6309..371168fc28 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled/logging_log_metric_filter_and_alert_for_bucket_permission_changes_enabled_test.py @@ -141,7 +141,7 @@ class Test_logging_log_metric_filter_and_alert_for_bucket_permission_changes_ena == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." ) assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py index fe4ca5e344..4ec94be657 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled/logging_log_metric_filter_and_alert_for_custom_role_changes_enabled_test.py @@ -141,7 +141,7 @@ class Test_logging_log_metric_filter_and_alert_for_custom_role_changes_enabled: == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." ) assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py index a3279bca0d..4732becb7f 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled/logging_log_metric_filter_and_alert_for_project_ownership_changes_enabled_test.py @@ -141,7 +141,7 @@ class Test_logging_log_metric_filter_and_alert_for_project_ownership_changes_ena == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." ) assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py index 18562913de..1a8a1d0da3 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled/logging_log_metric_filter_and_alert_for_sql_instance_configuration_changes_enabled_test.py @@ -141,7 +141,7 @@ class Test_logging_log_metric_filter_and_alert_for_sql_instance_configuration_ch == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." ) assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py index ce773d041d..a9460e6b46 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_enabled_test.py @@ -141,7 +141,7 @@ class Test_logging_log_metric_filter_and_alert_for_vpc_firewall_rule_changes_ena == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." ) assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py index e97aba2ec3..9c59d56a81 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled_test.py @@ -141,7 +141,7 @@ class Test_logging_log_metric_filter_and_alert_for_vpc_network_changes_enabled: == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." ) assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION diff --git a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py index 1f413a03f2..254c41bb5f 100644 --- a/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py +++ b/tests/providers/gcp/services/logging/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled/logging_log_metric_filter_and_alert_for_vpc_network_route_changes_enabled_test.py @@ -141,7 +141,7 @@ class Test_logging_log_metric_filter_and_alert_for_vpc_network_route_changes_ena == f"There are no log metric filters or alerts associated in project {GCP_PROJECT_ID}." ) assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION diff --git a/tests/providers/gcp/services/logging/logging_sink_created/logging_sink_created_test.py b/tests/providers/gcp/services/logging/logging_sink_created/logging_sink_created_test.py index f13b19d0cf..a2bab6e5cd 100644 --- a/tests/providers/gcp/services/logging/logging_sink_created/logging_sink_created_test.py +++ b/tests/providers/gcp/services/logging/logging_sink_created/logging_sink_created_test.py @@ -204,7 +204,7 @@ class Test_logging_sink_created: assert len(result) == 1 assert result[0].status == "FAIL" assert result[0].resource_id == GCP_PROJECT_ID - assert result[0].resource_name == GCP_PROJECT_ID + assert result[0].resource_name == "GCP Project" assert result[0].project_id == GCP_PROJECT_ID assert result[0].location == GCP_EU1_LOCATION assert ( diff --git a/tests/providers/github/github_provider_test.py b/tests/providers/github/github_provider_test.py index a3c74bca86..199964a743 100644 --- a/tests/providers/github/github_provider_test.py +++ b/tests/providers/github/github_provider_test.py @@ -135,6 +135,7 @@ class TestGitHubProvider: "prowler.providers.github.github_provider.GithubProvider.setup_identity", return_value=GithubAppIdentityInfo( app_id=APP_ID, + installations=["test-org"], ), ), ): @@ -147,7 +148,9 @@ class TestGitHubProvider: assert provider._type == "github" assert provider.session == GithubSession(token="", id=APP_ID, key=APP_KEY) - assert provider.identity == GithubAppIdentityInfo(app_id=APP_ID) + assert provider.identity == GithubAppIdentityInfo( + app_id=APP_ID, installations=["test-org"] + ) assert provider._audit_config == { "inactive_not_archived_days_threshold": 180, } @@ -206,7 +209,9 @@ class TestGitHubProvider: ), patch( "prowler.providers.github.github_provider.GithubProvider.setup_identity", - return_value=GithubAppIdentityInfo(app_id=APP_ID), + return_value=GithubAppIdentityInfo( + app_id=APP_ID, installations=["test-org"] + ), ), ): connection = GithubProvider.test_connection( diff --git a/tests/providers/github/services/repository/repository_service_test.py b/tests/providers/github/services/repository/repository_service_test.py index ada89ee072..20e5887d54 100644 --- a/tests/providers/github/services/repository/repository_service_test.py +++ b/tests/providers/github/services/repository/repository_service_test.py @@ -1,6 +1,7 @@ from datetime import datetime, timezone from unittest.mock import MagicMock, patch +import requests from github import GithubException, RateLimitExceededException from prowler.providers.github.services.repository.repository_service import ( @@ -40,6 +41,7 @@ def mock_list_repositories(_): archived=False, pushed_at=datetime.now(timezone.utc), delete_branch_on_merge=True, + dependabot_alerts_enabled=True, ), } @@ -110,6 +112,105 @@ class Test_Repository_FileExists: assert mock_logger.error.called +class Test_Repository_GraphQL: + def setup_method(self): + self.mock_repo1 = MagicMock() + self.mock_repo1.id = 1 + self.mock_repo1.name = "repo1" + self.mock_repo1.owner.login = "owner1" + self.mock_repo1.full_name = "owner1/repo1" + self.mock_repo1.default_branch = "main" + self.mock_repo1.private = False + self.mock_repo1.archived = False + self.mock_repo1.pushed_at = datetime.now(timezone.utc) + self.mock_repo1.delete_branch_on_merge = False + self.mock_repo1.security_and_analysis = None + self.mock_repo1.get_contents.side_effect = [None, None, None] + self.mock_repo1.get_branch.side_effect = Exception("404 Not Found") + self.mock_repo1.get_dependabot_alerts.side_effect = Exception("403 Forbidden") + + def test_no_scoping_uses_graphql(self): + """Test that no scoping triggers the GraphQL discovery method successfully""" + provider = set_mocked_github_provider() + provider.repositories = [] + provider.organizations = [] + + with patch.object(Repository, "__init__", lambda x, y: None): + repository_service = Repository(provider) + mock_client = MagicMock() + repository_service.clients = [mock_client] + repository_service.provider = provider + + with patch.object( + repository_service, + "_get_accessible_repos_graphql", + return_value=["owner1/repo1"], + ) as mock_graphql_call: + mock_client.get_repo.return_value = self.mock_repo1 + + repos = repository_service._list_repositories() + + assert len(repos) == 1 + assert 1 in repos + assert repos[1].name == "repo1" + + mock_graphql_call.assert_called_once() + mock_client.get_repo.assert_called_once_with("owner1/repo1") + + def test_graphql_call_api_error(self): + """Test that an error during the GraphQL call is handled gracefully""" + provider = set_mocked_github_provider() + provider.repositories = [] + provider.organizations = [] + + with patch.object(Repository, "__init__", lambda x, y: None): + repository_service = Repository(provider) + repository_service.clients = [MagicMock()] + repository_service.provider = provider + + with patch( + "requests.post", + side_effect=requests.exceptions.RequestException("API Error"), + ): + with patch( + "prowler.providers.github.services.repository.repository_service.logger" + ) as mock_logger: + + repos = repository_service._list_repositories() + + assert len(repos) == 0 + mock_logger.error.assert_called_once() + + log_output = str(mock_logger.error.call_args) + assert "RequestException" in log_output + assert "API Error" in log_output + + def test_graphql_returns_empty_list(self): + """Test the case where GraphQL returns no repositories""" + provider = set_mocked_github_provider() + provider.repositories = [] + provider.organizations = [] + + with patch.object(Repository, "__init__", lambda x, y: None): + repository_service = Repository(provider) + repository_service.clients = [MagicMock()] + repository_service.provider = provider + + with patch.object( + repository_service, "_get_accessible_repos_graphql", return_value=[] + ): + with patch( + "prowler.providers.github.services.repository.repository_service.logger" + ) as mock_logger: + + repos = repository_service._list_repositories() + + assert len(repos) == 0 + mock_logger.warning.assert_called_with( + "Could not find any accessible repositories with the provided token." + ) + + class Test_Repository_Scoping: def setup_method(self): self.mock_repo1 = MagicMock() @@ -123,7 +224,7 @@ class Test_Repository_Scoping: self.mock_repo1.pushed_at = datetime.now(timezone.utc) self.mock_repo1.delete_branch_on_merge = True self.mock_repo1.security_and_analysis = None - self.mock_repo1.get_contents.return_value = None + self.mock_repo1.get_contents.side_effect = [None, None, None] self.mock_repo1.get_branch.side_effect = Exception("404 Not Found") self.mock_repo1.get_dependabot_alerts.side_effect = Exception("404 Not Found") @@ -138,200 +239,10 @@ class Test_Repository_Scoping: self.mock_repo2.pushed_at = datetime.now(timezone.utc) self.mock_repo2.delete_branch_on_merge = True self.mock_repo2.security_and_analysis = None - self.mock_repo2.get_contents.return_value = None + self.mock_repo2.get_contents.side_effect = [None, None, None] self.mock_repo2.get_branch.side_effect = Exception("404 Not Found") self.mock_repo2.get_dependabot_alerts.side_effect = Exception("404 Not Found") - def test_no_repository_scoping(self): - """Test that all repositories are returned when no scoping is specified""" - provider = set_mocked_github_provider() - provider.repositories = [] - provider.organizations = [] - - mock_client = MagicMock() - mock_user = MagicMock() - mock_user.get_repos.return_value = [self.mock_repo1, self.mock_repo2] - mock_client.get_user.return_value = mock_user - - with patch( - "prowler.providers.github.services.repository.repository_service.GithubService.__init__" - ): - repository_service = Repository(provider) - repository_service.clients = [mock_client] - repository_service.provider = provider - - repos = repository_service._list_repositories() - - assert len(repos) == 2 - assert 1 in repos - assert 2 in repos - assert repos[1].name == "repo1" - assert repos[2].name == "repo2" - - def test_specific_repository_scoping(self): - """Test that only specified repositories are returned""" - provider = set_mocked_github_provider() - provider.repositories = ["owner1/repo1"] - provider.organizations = [] - - mock_client = MagicMock() - mock_client.get_repo.return_value = self.mock_repo1 - - with patch( - "prowler.providers.github.services.repository.repository_service.GithubService.__init__" - ): - repository_service = Repository(provider) - repository_service.clients = [mock_client] - repository_service.provider = provider - - repos = repository_service._list_repositories() - - assert len(repos) == 1 - assert 1 in repos - assert repos[1].name == "repo1" - assert repos[1].full_name == "owner1/repo1" - mock_client.get_repo.assert_called_once_with("owner1/repo1") - - def test_multiple_repository_scoping(self): - """Test that multiple specified repositories are returned""" - provider = set_mocked_github_provider() - provider.repositories = ["owner1/repo1", "owner2/repo2"] - provider.organizations = [] - - mock_client = MagicMock() - mock_client.get_repo.side_effect = [self.mock_repo1, self.mock_repo2] - - with patch( - "prowler.providers.github.services.repository.repository_service.GithubService.__init__" - ): - repository_service = Repository(provider) - repository_service.clients = [mock_client] - repository_service.provider = provider - - repos = repository_service._list_repositories() - - assert len(repos) == 2 - assert 1 in repos - assert 2 in repos - assert repos[1].name == "repo1" - assert repos[2].name == "repo2" - assert mock_client.get_repo.call_count == 2 - - def test_invalid_repository_format(self): - """Test that invalid repository formats are skipped with warning""" - provider = set_mocked_github_provider() - provider.repositories = ["invalid-repo-name", "owner/valid-repo"] - provider.organizations = [] - - mock_client = MagicMock() - mock_client.get_repo.return_value = self.mock_repo1 - - with patch( - "prowler.providers.github.services.repository.repository_service.GithubService.__init__" - ): - repository_service = Repository(provider) - repository_service.clients = [mock_client] - repository_service.provider = provider - - with patch( - "prowler.providers.github.services.repository.repository_service.logger" - ) as mock_logger: - repos = repository_service._list_repositories() - - # Should only have the valid repository - assert len(repos) == 1 - assert 1 in repos - # Should log warning for invalid format - assert mock_logger.warning.call_count >= 1 - # Check that at least one warning is about invalid format - warning_calls = [ - call[0][0] for call in mock_logger.warning.call_args_list - ] - assert any( - "should be in 'owner/repo-name' format" in call - for call in warning_calls - ) - - def test_repository_not_found(self): - """Test that inaccessible repositories are skipped with warning""" - provider = set_mocked_github_provider() - provider.repositories = ["owner/nonexistent-repo"] - provider.organizations = [] - - mock_client = MagicMock() - mock_client.get_repo.side_effect = Exception("404 Not Found") - - with patch( - "prowler.providers.github.services.repository.repository_service.GithubService.__init__" - ): - repository_service = Repository(provider) - repository_service.clients = [mock_client] - repository_service.provider = provider - - repos = repository_service._list_repositories() - - # Should be empty since repository wasn't found - assert len(repos) == 0 - - def test_organization_scoping(self): - """Test that repositories from specified organizations are returned""" - provider = set_mocked_github_provider() - provider.repositories = [] - provider.organizations = ["org1"] - - mock_client = MagicMock() - mock_org = MagicMock() - mock_org.get_repos.return_value = [self.mock_repo1] - mock_client.get_organization.return_value = mock_org - - with patch( - "prowler.providers.github.services.repository.repository_service.GithubService.__init__" - ): - repository_service = Repository(provider) - repository_service.clients = [mock_client] - repository_service.provider = provider - - repos = repository_service._list_repositories() - - assert len(repos) == 1 - assert 1 in repos - assert repos[1].name == "repo1" - mock_client.get_organization.assert_called_once_with("org1") - - def test_organization_as_user_fallback(self): - """Test that organization scoping falls back to user when organization not found""" - provider = set_mocked_github_provider() - provider.repositories = [] - provider.organizations = ["user1"] - - mock_client = MagicMock() - # Organization lookup fails - mock_client.get_organization.side_effect = GithubException( - 404, "Not Found", None - ) - # User lookup succeeds - mock_user = MagicMock() - mock_user.get_repos.return_value = [self.mock_repo1] - mock_client.get_user.return_value = mock_user - - # Create service without calling the parent constructor - repository_service = Repository.__new__(Repository) - repository_service.clients = [mock_client] - repository_service.provider = provider - - with patch( - "prowler.providers.github.services.repository.repository_service.logger" - ) as mock_logger: - repos = repository_service._list_repositories() - - assert len(repos) == 1 - assert 1 in repos - assert repos[1].name == "repo1" - mock_client.get_organization.assert_called_once_with("user1") - mock_client.get_user.assert_called_once_with("user1") - # Should log info about trying as user - mock_logger.info.assert_called() - def test_combined_repository_and_organization_scoping(self): """Test that both repository and organization scoping can be used together""" provider = set_mocked_github_provider() diff --git a/tests/providers/m365/lib/arguments/m365_arguments_test.py b/tests/providers/m365/lib/arguments/m365_arguments_test.py new file mode 100644 index 0000000000..220ff40f8f --- /dev/null +++ b/tests/providers/m365/lib/arguments/m365_arguments_test.py @@ -0,0 +1,641 @@ +import argparse +from unittest.mock import MagicMock + +from prowler.providers.m365.lib.arguments import arguments + + +class TestM365Arguments: + def setup_method(self): + """Setup mock ArgumentParser for testing""" + self.mock_parser = MagicMock() + self.mock_subparsers = MagicMock() + self.mock_m365_parser = MagicMock() + self.mock_auth_group = MagicMock() + self.mock_auth_modes_group = MagicMock() + self.mock_regions_group = MagicMock() + + # Setup the mock chain + self.mock_parser.add_subparsers.return_value = self.mock_subparsers + self.mock_subparsers.add_parser.return_value = self.mock_m365_parser + self.mock_m365_parser.add_argument_group.side_effect = [ + self.mock_auth_group, + self.mock_regions_group, + ] + self.mock_auth_group.add_mutually_exclusive_group.return_value = ( + self.mock_auth_modes_group + ) + + def test_init_parser_creates_subparser(self): + """Test that init_parser creates the M365 subparser correctly""" + # Create a mock object that has the necessary attributes + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + # Call init_parser + arguments.init_parser(mock_m365_args) + + # Verify subparser was created + self.mock_subparsers.add_parser.assert_called_once_with( + "m365", + parents=[mock_m365_args.common_providers_parser], + help="M365 Provider", + ) + + def test_init_parser_creates_argument_groups(self): + """Test that init_parser creates the correct argument groups""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Verify argument groups were created + assert self.mock_m365_parser.add_argument_group.call_count == 2 + calls = self.mock_m365_parser.add_argument_group.call_args_list + assert calls[0][0][0] == "Authentication Modes" + assert calls[1][0][0] == "Regions" + + def test_init_parser_creates_mutually_exclusive_auth_group(self): + """Test that init_parser creates mutually exclusive authentication group""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Verify mutually exclusive group was created for authentication modes + self.mock_auth_group.add_mutually_exclusive_group.assert_called_once() + + def test_init_parser_adds_authentication_arguments(self): + """Test that init_parser adds all authentication arguments""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Verify authentication arguments were added to the mutually exclusive group + assert self.mock_auth_modes_group.add_argument.call_count == 5 + + # Check that all authentication arguments are present + calls = self.mock_auth_modes_group.add_argument.call_args_list + auth_args = [call[0][0] for call in calls] + + assert "--az-cli-auth" in auth_args + assert "--env-auth" in auth_args + assert "--sp-env-auth" in auth_args + assert "--browser-auth" in auth_args + assert "--certificate-auth" in auth_args + + def test_init_parser_adds_non_exclusive_arguments(self): + """Test that init_parser adds non-exclusive arguments directly to parser""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Verify non-exclusive arguments were added to main parser + assert self.mock_m365_parser.add_argument.call_count == 3 + + # Check that non-exclusive arguments are present + calls = self.mock_m365_parser.add_argument.call_args_list + non_exclusive_args = [call[0][0] for call in calls] + + assert "--tenant-id" in non_exclusive_args + assert "--init-modules" in non_exclusive_args + assert "--certificate-path" in non_exclusive_args + + def test_init_parser_adds_region_arguments(self): + """Test that init_parser adds region arguments""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Verify region arguments were added to regions group + assert self.mock_regions_group.add_argument.call_count == 1 + + # Check that region argument is present + calls = self.mock_regions_group.add_argument.call_args_list + region_args = [call[0][0] for call in calls] + + assert "--region" in region_args + + def test_az_cli_auth_argument_configuration(self): + """Test that az-cli-auth argument is configured correctly""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Find the az-cli-auth argument call + calls = self.mock_auth_modes_group.add_argument.call_args_list + az_cli_call = None + for call in calls: + if call[0][0] == "--az-cli-auth": + az_cli_call = call + break + + assert az_cli_call is not None + + # Check argument configuration + kwargs = az_cli_call[1] + assert kwargs["action"] == "store_true" + assert "Azure CLI authentication" in kwargs["help"] + + def test_env_auth_argument_configuration(self): + """Test that env-auth argument is configured correctly""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Find the env-auth argument call + calls = self.mock_auth_modes_group.add_argument.call_args_list + env_auth_call = None + for call in calls: + if call[0][0] == "--env-auth": + env_auth_call = call + break + + assert env_auth_call is not None + + # Check argument configuration + kwargs = env_auth_call[1] + assert kwargs["action"] == "store_true" + assert "User and Password environment variables" in kwargs["help"] + + def test_sp_env_auth_argument_configuration(self): + """Test that sp-env-auth argument is configured correctly""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Find the sp-env-auth argument call + calls = self.mock_auth_modes_group.add_argument.call_args_list + sp_env_call = None + for call in calls: + if call[0][0] == "--sp-env-auth": + sp_env_call = call + break + + assert sp_env_call is not None + + # Check argument configuration + kwargs = sp_env_call[1] + assert kwargs["action"] == "store_true" + assert "Azure Service Principal environment variables" in kwargs["help"] + + def test_browser_auth_argument_configuration(self): + """Test that browser-auth argument is configured correctly""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Find the browser-auth argument call + calls = self.mock_auth_modes_group.add_argument.call_args_list + browser_auth_call = None + for call in calls: + if call[0][0] == "--browser-auth": + browser_auth_call = call + break + + assert browser_auth_call is not None + + # Check argument configuration + kwargs = browser_auth_call[1] + assert kwargs["action"] == "store_true" + assert "Azure interactive browser authentication" in kwargs["help"] + + def test_certificate_auth_argument_configuration(self): + """Test that certificate-auth argument is configured correctly""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Find the certificate-auth argument call + calls = self.mock_auth_modes_group.add_argument.call_args_list + cert_auth_call = None + for call in calls: + if call[0][0] == "--certificate-auth": + cert_auth_call = call + break + + assert cert_auth_call is not None + + # Check argument configuration + kwargs = cert_auth_call[1] + assert kwargs["action"] == "store_true" + assert "Certificate authentication" in kwargs["help"] + + def test_tenant_id_argument_configuration(self): + """Test that tenant-id argument is configured correctly""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Find the tenant-id argument call + calls = self.mock_m365_parser.add_argument.call_args_list + tenant_id_call = None + for call in calls: + if call[0][0] == "--tenant-id": + tenant_id_call = call + break + + assert tenant_id_call is not None + + # Check argument configuration + kwargs = tenant_id_call[1] + assert kwargs["nargs"] == "?" + assert kwargs["default"] is None + assert "Microsoft 365 Tenant ID" in kwargs["help"] + assert "--browser-auth" in kwargs["help"] + + def test_init_modules_argument_configuration(self): + """Test that init-modules argument is configured correctly""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Find the init-modules argument call + calls = self.mock_m365_parser.add_argument.call_args_list + init_modules_call = None + for call in calls: + if call[0][0] == "--init-modules": + init_modules_call = call + break + + assert init_modules_call is not None + + # Check argument configuration + kwargs = init_modules_call[1] + assert kwargs["action"] == "store_true" + assert "Initialize Microsoft 365 PowerShell modules" in kwargs["help"] + + def test_region_argument_configuration(self): + """Test that region argument is configured correctly""" + mock_m365_args = MagicMock() + mock_m365_args.subparsers = self.mock_subparsers + mock_m365_args.common_providers_parser = MagicMock() + + arguments.init_parser(mock_m365_args) + + # Find the region argument call + calls = self.mock_regions_group.add_argument.call_args_list + region_call = None + for call in calls: + if call[0][0] == "--region": + region_call = call + break + + assert region_call is not None + + # Check argument configuration + kwargs = region_call[1] + assert kwargs["nargs"] == "?" + assert kwargs["default"] == "M365Global" + assert kwargs["choices"] == [ + "M365Global", + "M365GlobalChina", + "M365USGovernment", + ] + assert "Microsoft 365 region" in kwargs["help"] + assert "M365Global" in kwargs["help"] + + +class TestM365ArgumentsIntegration: + def test_real_argument_parsing_az_cli_auth(self): + """Test parsing arguments with Azure CLI authentication""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + # Create a mock object that mimics the structure used by the init_parser function + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments with Azure CLI auth + args = parser.parse_args(["m365", "--az-cli-auth"]) + + assert args.az_cli_auth is True + assert args.env_auth is False + assert args.sp_env_auth is False + assert args.browser_auth is False + assert args.certificate_auth is False + + def test_real_argument_parsing_env_auth(self): + """Test parsing arguments with environment authentication""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments with environment auth + args = parser.parse_args(["m365", "--env-auth"]) + + assert args.az_cli_auth is False + assert args.env_auth is True + assert args.sp_env_auth is False + assert args.browser_auth is False + assert args.certificate_auth is False + + def test_real_argument_parsing_sp_env_auth(self): + """Test parsing arguments with service principal environment authentication""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments with service principal environment auth + args = parser.parse_args(["m365", "--sp-env-auth"]) + + assert args.az_cli_auth is False + assert args.env_auth is False + assert args.sp_env_auth is True + assert args.browser_auth is False + assert args.certificate_auth is False + + def test_real_argument_parsing_browser_auth_with_tenant_id(self): + """Test parsing arguments with browser authentication and tenant ID""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments with browser auth and tenant ID + args = parser.parse_args( + [ + "m365", + "--browser-auth", + "--tenant-id", + "12345678-1234-5678-abcd-123456789012", + ] + ) + + assert args.az_cli_auth is False + assert args.env_auth is False + assert args.sp_env_auth is False + assert args.browser_auth is True + assert args.certificate_auth is False + assert args.tenant_id == "12345678-1234-5678-abcd-123456789012" + + def test_real_argument_parsing_certificate_auth(self): + """Test parsing arguments with certificate authentication""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments with certificate auth + args = parser.parse_args(["m365", "--certificate-auth"]) + + assert args.az_cli_auth is False + assert args.env_auth is False + assert args.sp_env_auth is False + assert args.browser_auth is False + assert args.certificate_auth is True + + def test_real_argument_parsing_with_init_modules(self): + """Test parsing arguments with init modules flag""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments with init modules + args = parser.parse_args(["m365", "--az-cli-auth", "--init-modules"]) + + assert args.az_cli_auth is True + assert args.init_modules is True + + def test_real_argument_parsing_with_different_regions(self): + """Test parsing arguments with different region options""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Test M365Global (default) + args = parser.parse_args(["m365", "--az-cli-auth"]) + assert args.region == "M365Global" + + # Test M365GlobalChina + args = parser.parse_args( + ["m365", "--az-cli-auth", "--region", "M365GlobalChina"] + ) + assert args.region == "M365GlobalChina" + + # Test M365USGovernment + args = parser.parse_args( + ["m365", "--az-cli-auth", "--region", "M365USGovernment"] + ) + assert args.region == "M365USGovernment" + + def test_real_argument_parsing_no_authentication_defaults(self): + """Test parsing arguments without any authentication flags (should have defaults)""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments without explicit auth (defaults should apply) + args = parser.parse_args(["m365"]) + + assert args.az_cli_auth is False + assert args.env_auth is False + assert args.sp_env_auth is False + assert args.browser_auth is False + assert args.certificate_auth is False + assert args.tenant_id is None + assert args.init_modules is False + assert args.region == "M365Global" + + def test_real_argument_parsing_complete_configuration(self): + """Test parsing arguments with all non-exclusive options""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments with complete configuration + args = parser.parse_args( + [ + "m365", + "--browser-auth", + "--tenant-id", + "12345678-1234-5678-abcd-123456789012", + "--init-modules", + "--region", + "M365USGovernment", + ] + ) + + assert args.browser_auth is True + assert args.tenant_id == "12345678-1234-5678-abcd-123456789012" + assert args.init_modules is True + assert args.region == "M365USGovernment" + + def test_mutually_exclusive_authentication_enforcement(self): + """Test that authentication methods are mutually exclusive""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # This should raise SystemExit due to mutually exclusive group + try: + parser.parse_args(["m365", "--az-cli-auth", "--env-auth"]) + assert False, "Expected SystemExit due to mutually exclusive arguments" + except SystemExit: + # This is expected + pass + + def test_tenant_id_without_arguments(self): + """Test that tenant-id can be specified without an argument (optional value)""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments with tenant-id but no value (should be None due to nargs="?") + args = parser.parse_args(["m365", "--az-cli-auth", "--tenant-id"]) + + assert args.tenant_id is None + + def test_certificate_path_argument_configuration(self): + """Test that certificate_path argument is properly configured""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments with certificate-path + args = parser.parse_args( + ["m365", "--certificate-auth", "--certificate-path", "/path/to/cert.pem"] + ) + + assert args.certificate_auth is True + assert args.certificate_path == "/path/to/cert.pem" + + def test_certificate_path_without_value(self): + """Test certificate_path argument without value (should be None due to nargs='?')""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse arguments with certificate-path but no value + args = parser.parse_args(["m365", "--certificate-auth", "--certificate-path"]) + + assert args.certificate_auth is True + assert args.certificate_path is None + + def test_certificate_auth_with_certificate_path_integration(self): + """Test certificate authentication with certificate path integration""" + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers() + common_parser = argparse.ArgumentParser(add_help=False) + + mock_m365_args = MagicMock() + mock_m365_args.subparsers = subparsers + mock_m365_args.common_providers_parser = common_parser + + arguments.init_parser(mock_m365_args) + + # Parse complete certificate authentication arguments + args = parser.parse_args( + [ + "m365", + "--certificate-auth", + "--certificate-path", + "/home/user/cert.pem", + "--tenant-id", + "12345678-1234-1234-1234-123456789012", + ] + ) + + assert args.certificate_auth is True + assert args.certificate_path == "/home/user/cert.pem" + assert args.tenant_id == "12345678-1234-1234-1234-123456789012" + assert args.az_cli_auth is False + assert args.env_auth is False + assert args.sp_env_auth is False + assert args.browser_auth is False diff --git a/tests/providers/m365/lib/powershell/m365_powershell_test.py b/tests/providers/m365/lib/powershell/m365_powershell_test.py index 2e6a34a032..bd45956de0 100644 --- a/tests/providers/m365/lib/powershell/m365_powershell_test.py +++ b/tests/providers/m365/lib/powershell/m365_powershell_test.py @@ -1,9 +1,11 @@ +import base64 from unittest.mock import MagicMock, call, patch import pytest from prowler.lib.powershell.powershell import PowerShellSession from prowler.providers.m365.exceptions.exceptions import ( + M365CertificateCreationError, M365GraphConnectionError, M365UserCredentialsError, M365UserNotBelongingToTenantError, @@ -112,7 +114,7 @@ class Testm365PowerShell: session.close() @patch("subprocess.Popen") - def test_test_credentials(self, mock_popen): + def test_test_credentials_exchange_success(self, mock_popen): mock_process = MagicMock() mock_popen.return_value = mock_process @@ -131,16 +133,20 @@ class Testm365PowerShell: tenant_domain="contoso.onmicrosoft.com", tenant_domains=["contoso.onmicrosoft.com"], location="test_location", + user="test@contoso.onmicrosoft.com", ) session = M365PowerShell(credentials, identity) # Mock encrypt_password to return a known value session.encrypt_password = MagicMock(return_value="encrypted_password") - # Mock execute to simulate successful Connect-ExchangeOnline - session.execute = MagicMock( - return_value="Connected successfully https://aka.ms/exov3-module" - ) + # Mock execute to simulate successful Exchange connection + def mock_execute_side_effect(command): + if "Connect-ExchangeOnline" in command: + return "Connected successfully https://aka.ms/exov3-module" + return "" + + session.execute = MagicMock(side_effect=mock_execute_side_effect) # Execute the test result = session.test_credentials(credentials) @@ -153,42 +159,110 @@ class Testm365PowerShell: session.execute.assert_any_call( f'$credential = New-Object System.Management.Automation.PSCredential("{session.sanitize(credentials.user)}", $securePassword)' ) + # Exchange connection should be tested session.execute.assert_any_call( "Connect-ExchangeOnline -Credential $credential" ) + # Verify Teams connection was NOT called (since Exchange succeeded) + teams_calls = [ + call + for call in session.execute.call_args_list + if "Connect-MicrosoftTeams" in str(call) + ] + assert ( + len(teams_calls) == 0 + ), "Teams connection should not be called when Exchange succeeds" + + session.close() + + @patch("subprocess.Popen") + def test_test_credentials_exchange_fail_teams_success(self, mock_popen): + mock_process = MagicMock() + mock_popen.return_value = mock_process + + credentials = M365Credentials( + user="test@contoso.onmicrosoft.com", + passwd="test_password", + encrypted_passwd="test_encrypted_password", + client_id="test_client_id", + client_secret="test_client_secret", + tenant_id="test_tenant_id", + ) + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="User", + tenant_id="test_tenant", + tenant_domain="contoso.onmicrosoft.com", + tenant_domains=["contoso.onmicrosoft.com"], + location="test_location", + user="test@contoso.onmicrosoft.com", + ) + session = M365PowerShell(credentials, identity) + + # Mock encrypt_password to return a known value + session.encrypt_password = MagicMock(return_value="encrypted_password") + + # Mock execute to simulate Exchange fail and Teams success + def mock_execute_side_effect(command): + if "Connect-ExchangeOnline" in command: + return ( + "Connection failed" # No "https://aka.ms/exov3-module" in response + ) + elif "Connect-MicrosoftTeams" in command: + return "Connected successfully test@contoso.onmicrosoft.com" + return "" + + session.execute = MagicMock(side_effect=mock_execute_side_effect) + + # Execute the test + result = session.test_credentials(credentials) + assert result is True + + # Verify execute was called with the correct commands + session.execute.assert_any_call( + f'$securePassword = "{credentials.encrypted_passwd}" | ConvertTo-SecureString' + ) + session.execute.assert_any_call( + f'$credential = New-Object System.Management.Automation.PSCredential("{session.sanitize(credentials.user)}", $securePassword)' + ) + # Both Exchange and Teams connections should be tested + session.execute.assert_any_call( + "Connect-ExchangeOnline -Credential $credential" + ) + session.execute.assert_any_call( + "Connect-MicrosoftTeams -Credential $credential" + ) + session.close() @patch("subprocess.Popen") def test_test_credentials_application_auth(self, mock_popen): mock_process = MagicMock() mock_popen.return_value = mock_process - with patch( - "prowler.providers.m365.lib.powershell.m365_powershell.M365Credentials", - autospec=True, - ) as mock_creds: - credentials = mock_creds.return_value - credentials.user = "" - credentials.passwd = "" - credentials.encrypted_passwd = "" - credentials.client_id = "test_client_id" - credentials.client_secret = "test_client_secret" - credentials.tenant_id = "test_tenant_id" - identity = M365IdentityInfo( - identity_id="test_id", - identity_type="Application", - tenant_id="test_tenant", - tenant_domain="contoso.onmicrosoft.com", - tenant_domains=["contoso.onmicrosoft.com"], - location="test_location", - ) - session = M365PowerShell(credentials, identity) - session.execute = MagicMock(return_value="sometoken") + credentials = M365Credentials( + user="", + passwd="", + encrypted_passwd="", + client_id="test_client_id", + client_secret="test_client_secret", + tenant_id="test_tenant_id", + ) + identity = M365IdentityInfo( + identity_id="test_id", + identity_type="Application", + tenant_id="test_tenant", + tenant_domain="contoso.onmicrosoft.com", + tenant_domains=["contoso.onmicrosoft.com"], + location="test_location", + ) + session = M365PowerShell(credentials, identity) + session.execute = MagicMock(return_value="sometoken") - result = session.test_credentials(credentials) - assert result is True - session.execute.assert_any_call("Write-Output $graphToken") - session.close() + result = session.test_credentials(credentials) + assert result is True + session.execute.assert_any_call("Write-Output $graphToken") + session.close() @patch("subprocess.Popen") @patch("msal.ConfidentialClientApplication") @@ -748,7 +822,8 @@ class Testm365PowerShell: assert result is True # Verify all expected PowerShell commands were called - assert session.execute.call_count == 3 + # 4 calls: teamstokenBody, teamsToken, Write-Output $teamsToken, Connect-MicrosoftTeams + assert session.execute.call_count == 4 mock_decode_jwt.assert_called_once_with("valid_teams_token") session.close() @@ -849,7 +924,8 @@ class Testm365PowerShell: assert result is True # Verify all expected PowerShell commands were called - assert session.execute.call_count == 3 + # 4 calls: SecureSecret, exchangeToken, Write-Output $exchangeToken, Connect-ExchangeOnline + assert session.execute.call_count == 4 mock_decode_msal_token.assert_called_once_with("valid_exchange_token") session.close() @@ -970,3 +1046,623 @@ class Testm365PowerShell: del sys.modules["win32crypt"] session.close() + + @patch("subprocess.Popen") + def test_clean_certificate_content(self, mock_popen): + """Test clean_certificate_content method with various certificate content formats""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + credentials = M365Credentials() + identity = M365IdentityInfo() + session = M365PowerShell(credentials, identity) + + # Test with clean base64 content + clean_cert = "MIIDXTCCAkWgAwIBAgIJAJc1HiIAZAiIMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV" + result = session.clean_certificate_content(clean_cert) + assert result == clean_cert + + # Test with newlines + cert_with_newlines = "MIIDXTCCAkWgAwIBAgIJAJc1HiIAZAiIMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV\nBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldA==" + expected = "MIIDXTCCAkWgAwIBAgIJAJc1HiIAZAiIMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldA==" + result = session.clean_certificate_content(cert_with_newlines) + assert result == expected + + # Test with carriage returns + cert_with_cr = "MIIDXTCCAkWgAwIBAgIJAJc1HiIAZAiIMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV\rBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldA==" + result = session.clean_certificate_content(cert_with_cr) + assert result == expected + + # Test with spaces + cert_with_spaces = "MIIDXTCCAkWgAwIBAgIJAJc1HiIAZAiIMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldA==" + result = session.clean_certificate_content(cert_with_spaces) + assert result == expected + + # Test with combination of all whitespace types + cert_mixed = "MIIDXTCCAkWgAwIBAgIJAJc1HiIAZAiIMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV\n\r BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldA==" + result = session.clean_certificate_content(cert_mixed) + assert result == expected + + # Test with leading/trailing whitespace + cert_with_whitespace = " MIIDXTCCAkWgAwIBAgIJAJc1HiIAZAiIMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldA== " + result = session.clean_certificate_content(cert_with_whitespace) + assert result == expected + + session.close() + + @patch("subprocess.Popen") + def test_init_credential_certificate_auth(self, mock_popen): + """Test init_credential method with certificate authentication""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + credentials = M365Credentials( + client_id="test_client_id", + tenant_id="test_tenant_id", + certificate_content=certificate_content, + tenant_domains=["example.com"], + ) + identity = M365IdentityInfo(tenant_domains=["example.com"]) + + # Create session without calling init_credential + with patch.object(M365PowerShell, "init_credential"): + session = M365PowerShell(credentials, identity) + + # Mock the execute method + execute_calls = [] + + def mock_execute(command): + execute_calls.append(command) + if ( + "New-Object System.Security.Cryptography.X509Certificates.X509Certificate2" + in command + ): + return "" # No error + return "" + + session.execute = MagicMock(side_effect=mock_execute) + session.sanitize = MagicMock(side_effect=lambda x: x) + session.clean_certificate_content = MagicMock(return_value=certificate_content) + + # Now call init_credential + session.init_credential(credentials) + + # Verify clean_certificate_content was called + session.clean_certificate_content.assert_called_once_with(certificate_content) + + # Verify sanitize was called for client_id and tenant_id + session.sanitize.assert_any_call(credentials.client_id) + session.sanitize.assert_any_call(credentials.tenant_id) + + # Verify execute was called with correct commands + session.execute.assert_any_call( + f'$certBytes = [Convert]::FromBase64String("{certificate_content}")' + ) + session.execute.assert_any_call( + "$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2(,$certBytes)" + ) + session.execute.assert_any_call(f'$clientID = "{credentials.client_id}"') + session.execute.assert_any_call(f'$tenantID = "{credentials.tenant_id}"') + session.execute.assert_any_call( + f'$tenantDomain = "{credentials.tenant_domains[0]}"' + ) + + session.close() + + @patch("subprocess.Popen") + def test_init_credential_certificate_auth_creation_error(self, mock_popen): + """Test init_credential method with certificate authentication when certificate creation fails""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + certificate_content = base64.b64encode(b"invalid_certificate").decode("utf-8") + credentials = M365Credentials( + client_id="test_client_id", + tenant_id="test_tenant_id", + certificate_content=certificate_content, + tenant_domains=["example.com"], + ) + identity = M365IdentityInfo(tenant_domains=["example.com"]) + + # Create session without calling init_credential + with patch.object(M365PowerShell, "init_credential"): + session = M365PowerShell(credentials, identity) + + # Mock the execute method to simulate certificate creation error + def mock_execute(command): + if ( + "New-Object System.Security.Cryptography.X509Certificates.X509Certificate2" + in command + ): + return "Certificate creation failed: Invalid certificate format" + return "" + + session.execute = MagicMock(side_effect=mock_execute) + session.sanitize = MagicMock(side_effect=lambda x: x) + session.clean_certificate_content = MagicMock(return_value=certificate_content) + + with pytest.raises(M365CertificateCreationError) as exc_info: + session.init_credential(credentials) + + # The actual error message format from the exception + assert "Failed to create X.509 certificate object" in str(exc_info.value) + + session.close() + + @patch("subprocess.Popen") + def test_test_exchange_certificate_connection_success(self, mock_popen): + """Test test_exchange_certificate_connection method with successful connection""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + credentials = M365Credentials() + identity = M365IdentityInfo() + session = M365PowerShell(credentials, identity) + + # Mock successful Exchange connection + session.execute = MagicMock( + return_value="Connected successfully https://aka.ms/exov3-module" + ) + + result = session.test_exchange_certificate_connection() + assert result is True + + session.execute.assert_called_once_with( + "Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain" + ) + + session.close() + + @patch("subprocess.Popen") + def test_test_exchange_certificate_connection_failure(self, mock_popen): + """Test test_exchange_certificate_connection method with failed connection""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + credentials = M365Credentials() + identity = M365IdentityInfo() + session = M365PowerShell(credentials, identity) + + # Mock failed Exchange connection + session.execute = MagicMock( + return_value="Connection failed: Authentication error" + ) + + result = session.test_exchange_certificate_connection() + assert result is False + + session.execute.assert_called_once_with( + "Connect-ExchangeOnline -Certificate $certificate -AppId $clientID -Organization $tenantDomain" + ) + + session.close() + + @patch("subprocess.Popen") + def test_test_teams_certificate_connection_success(self, mock_popen): + """Test test_teams_certificate_connection method with successful connection""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + credentials = M365Credentials() + identity = M365IdentityInfo(identity_id="test_identity_id") + + # Create session without calling init_credential + with patch.object(M365PowerShell, "init_credential"): + session = M365PowerShell(credentials, identity) + + # Mock successful Teams connection - the method returns bool + def mock_execute_side_effect(command): + if "Connect-MicrosoftTeams" in command: + # Return result that contains the identity_id for success + return "Connected successfully test_identity_id" + return "" + + session.execute = MagicMock(side_effect=mock_execute_side_effect) + + result = session.test_teams_certificate_connection() + assert result is True + + session.execute.assert_called_once_with( + "Connect-MicrosoftTeams -Certificate $certificate -ApplicationId $clientID -TenantId $tenantID" + ) + + session.close() + + @patch("subprocess.Popen") + def test_test_teams_certificate_connection_failure(self, mock_popen): + """Test test_teams_certificate_connection method with failed connection""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + credentials = M365Credentials() + identity = M365IdentityInfo() + session = M365PowerShell(credentials, identity) + + # Mock failed Teams connection + def mock_execute_side_effect(command, json_parse=False): + if "Connect-MicrosoftTeams" in command: + raise Exception("Connection failed: Authentication error") + return "" + + session.execute = MagicMock(side_effect=mock_execute_side_effect) + + # Should raise exception on connection failure + with pytest.raises(Exception) as exc_info: + session.test_teams_certificate_connection() + + assert "Connection failed: Authentication error" in str(exc_info.value) + + session.close() + + @patch("subprocess.Popen") + def test_test_credentials_certificate_auth_success(self, mock_popen): + """Test test_credentials method with certificate authentication - successful""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + credentials = M365Credentials( + client_id="test_client_id", certificate_content=certificate_content + ) + identity = M365IdentityInfo() + + # Create session without calling init_credential + with patch.object(M365PowerShell, "init_credential"): + session = M365PowerShell(credentials, identity) + + # Mock successful certificate connections + # Note: The actual implementation uses "or" so if teams succeeds, exchange won't be called + session.test_teams_certificate_connection = MagicMock(return_value=True) + session.test_exchange_certificate_connection = MagicMock(return_value=True) + + result = session.test_credentials(credentials) + assert result is True + + session.test_teams_certificate_connection.assert_called_once() + # Exchange connection should NOT be called if teams connection succeeds (due to "or" logic) + session.test_exchange_certificate_connection.assert_not_called() + + session.close() + + @patch("subprocess.Popen") + def test_test_credentials_certificate_auth_failure(self, mock_popen): + """Test test_credentials method with certificate authentication - failure""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + credentials = M365Credentials( + client_id="test_client_id", certificate_content=certificate_content + ) + identity = M365IdentityInfo() + + # Create session without calling init_credential + with patch.object(M365PowerShell, "init_credential"): + session = M365PowerShell(credentials, identity) + + # Mock failed certificate connections - teams fails, so exchange is tried + session.test_teams_certificate_connection = MagicMock(return_value=False) + session.test_exchange_certificate_connection = MagicMock(return_value=False) + + result = session.test_credentials(credentials) + assert result is True # Method always returns True after the try block + + session.close() + + @patch("subprocess.Popen") + def test_connect_microsoft_teams_certificate_auth(self, mock_popen): + """Test connect_microsoft_teams method with certificate authentication""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + credentials = M365Credentials() + identity = M365IdentityInfo() + session = M365PowerShell(credentials, identity) + + # Mock certificate variable check and teams connection + execute_calls = [] + + def mock_execute_side_effect(command, json_parse=False): + execute_calls.append(command) + if "Write-Output $certificate" in command: + return "certificate_content" # Non-empty means certificate exists + elif "Connect-MicrosoftTeams" in command: + return {"TenantId": "test-tenant-id", "Account": "test-account"} + return "" + + session.execute = MagicMock(side_effect=mock_execute_side_effect) + session.test_teams_certificate_connection = MagicMock( + return_value={"success": True} + ) + + result = session.connect_microsoft_teams() + assert result == {"success": True} + + session.test_teams_certificate_connection.assert_called_once() + + session.close() + + @patch("subprocess.Popen") + def test_connect_exchange_online_certificate_auth(self, mock_popen): + """Test connect_exchange_online method with certificate authentication""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + + credentials = M365Credentials() + identity = M365IdentityInfo() + session = M365PowerShell(credentials, identity) + + # Mock certificate variable check and exchange connection + execute_calls = [] + + def mock_execute_side_effect(command, json_parse=False): + execute_calls.append(command) + if "Write-Output $certificate" in command: + return "certificate_content" # Non-empty means certificate exists + return "" + + session.execute = MagicMock(side_effect=mock_execute_side_effect) + session.test_exchange_certificate_connection = MagicMock(return_value=True) + + result = session.connect_exchange_online() + assert result is True + + session.test_exchange_certificate_connection.assert_called_once() + + session.close() + + @patch("subprocess.Popen") + def test_clean_certificate_content_basic(self, mock_popen): + """Test clean_certificate_content method with basic certificate content""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + mock_process.returncode = 0 + + session = M365PowerShell( + M365Credentials( + client_id="test_client_id", + client_secret="test_secret", + tenant_id="test_tenant_id", + tenant_domains=["contoso.com"], + ), + M365IdentityInfo( + tenant_id="test_tenant_id", + tenant_domain="contoso.com", + tenant_domains=["contoso.com"], + identity_id="test_identity_id", + identity_type="Service Principal", + ), + ) + + # Test basic certificate content cleaning + cert_content = "LS0tLS1CRUdJTi\nBFUlRJRklD\rQVRFLS0tLS0\n" + expected = "LS0tLS1CRUdJTiBFUlRJRklDQVRFLS0tLS0" + + result = session.clean_certificate_content(cert_content) + + assert result == expected + session.close() + + @patch("subprocess.Popen") + def test_clean_certificate_content_with_spaces(self, mock_popen): + """Test clean_certificate_content method with spaces in certificate content""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + mock_process.returncode = 0 + + session = M365PowerShell( + M365Credentials( + client_id="test_client_id", + client_secret="test_secret", + tenant_id="test_tenant_id", + tenant_domains=["contoso.com"], + ), + M365IdentityInfo( + tenant_id="test_tenant_id", + tenant_domain="contoso.com", + tenant_domains=["contoso.com"], + identity_id="test_identity_id", + identity_type="Service Principal", + ), + ) + + # Test certificate content with spaces + cert_content = " LS0tLS1CRUdJTi BFUlRJRklD QVRFLS0tLS0 " + expected = "LS0tLS1CRUdJTiBFUlRJRklDQVRFLS0tLS0" + + result = session.clean_certificate_content(cert_content) + + assert result == expected + session.close() + + @patch("subprocess.Popen") + def test_clean_certificate_content_empty(self, mock_popen): + """Test clean_certificate_content method with empty certificate content""" + mock_process = MagicMock() + mock_popen.return_value = mock_process + mock_process.returncode = 0 + + session = M365PowerShell( + M365Credentials( + client_id="test_client_id", + client_secret="test_secret", + tenant_id="test_tenant_id", + tenant_domains=["contoso.com"], + ), + M365IdentityInfo( + tenant_id="test_tenant_id", + tenant_domain="contoso.com", + tenant_domains=["contoso.com"], + identity_id="test_identity_id", + identity_type="Service Principal", + ), + ) + + # Test empty certificate content + cert_content = "" + expected = "" + + result = session.clean_certificate_content(cert_content) + + assert result == expected + session.close() + + @patch("subprocess.Popen") + def test_init_credential_certificate_auth_with_domains(self, mock_popen): + """Test init_credential method with certificate authentication and tenant domains""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + mock_process = MagicMock() + mock_popen.return_value = mock_process + mock_process.returncode = 0 + + executed_commands = [] + + def mock_execute(command): + executed_commands.append(command) + if "Error creating certificate" in command: + return None # No error + return "" + + # Create session with non-certificate credentials first + session = M365PowerShell( + M365Credentials( + client_id="test_client_id", + client_secret="test_secret", + tenant_id="test_tenant_id", + tenant_domains=["contoso.com", "subdomain.contoso.com"], + ), + M365IdentityInfo( + tenant_id="test_tenant_id", + tenant_domain="contoso.com", + tenant_domains=["contoso.com", "subdomain.contoso.com"], + identity_id="test_identity_id", + identity_type="Service Principal with Certificate", + ), + ) + + # Mock methods before calling init_credential + session.execute = MagicMock(side_effect=mock_execute) + session.sanitize = MagicMock(side_effect=lambda x: x) + session.clean_certificate_content = MagicMock(return_value=certificate_content) + + # Now test certificate authentication + session.init_credential( + M365Credentials( + client_id="test_client_id", + tenant_id="test_tenant_id", + certificate_content=certificate_content, + tenant_domains=["contoso.com", "subdomain.contoso.com"], + ) + ) + + # Verify that certificate content was cleaned + session.clean_certificate_content.assert_called_once_with(certificate_content) + + # Verify certificate creation commands were executed + assert any( + "$certBytes = [Convert]::FromBase64String" in cmd + for cmd in executed_commands + ) + assert any( + "$certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2" + in cmd + for cmd in executed_commands + ) + assert any('$clientID = "test_client_id"' in cmd for cmd in executed_commands) + assert any('$tenantID = "test_tenant_id"' in cmd for cmd in executed_commands) + assert any('$tenantDomain = "contoso.com"' in cmd for cmd in executed_commands) + + session.close() + + @patch("subprocess.Popen") + def test_test_credentials_certificate_auth_with_or_logic(self, mock_popen): + """Test test_credentials method with certificate auth using OR logic between Teams and Exchange""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + mock_process = MagicMock() + mock_popen.return_value = mock_process + mock_process.returncode = 0 + + # Create session with non-certificate credentials first + session = M365PowerShell( + M365Credentials( + client_id="test_client_id", + client_secret="test_secret", + tenant_id="test_tenant_id", + tenant_domains=["contoso.com"], + ), + M365IdentityInfo( + tenant_id="test_tenant_id", + tenant_domain="contoso.com", + tenant_domains=["contoso.com"], + identity_id="test_identity_id", + identity_type="Service Principal with Certificate", + ), + ) + + # Mock that Teams connection fails but Exchange succeeds + session.test_teams_certificate_connection = MagicMock(return_value=False) + session.test_exchange_certificate_connection = MagicMock(return_value=True) + + result = session.test_credentials( + M365Credentials( + client_id="test_client_id", + tenant_id="test_tenant_id", + certificate_content=certificate_content, + tenant_domains=["contoso.com"], + ) + ) + + assert result is True + session.test_teams_certificate_connection.assert_called_once() + session.test_exchange_certificate_connection.assert_called_once() + + session.close() + + @patch("subprocess.Popen") + def test_test_credentials_certificate_auth_both_fail(self, mock_popen): + """Test test_credentials method with certificate auth when both Teams and Exchange fail""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + mock_process = MagicMock() + mock_popen.return_value = mock_process + mock_process.returncode = 0 + + # Create session with non-certificate credentials first + session = M365PowerShell( + M365Credentials( + client_id="test_client_id", + client_secret="test_secret", + tenant_id="test_tenant_id", + tenant_domains=["contoso.com"], + ), + M365IdentityInfo( + tenant_id="test_tenant_id", + tenant_domain="contoso.com", + tenant_domains=["contoso.com"], + identity_id="test_identity_id", + identity_type="Service Principal with Certificate", + ), + ) + + # Mock that both connections fail + session.test_teams_certificate_connection = MagicMock(return_value=False) + session.test_exchange_certificate_connection = MagicMock(return_value=False) + + # Even when both fail, the method should return True (this is the intended logic) + result = session.test_credentials( + M365Credentials( + client_id="test_client_id", + tenant_id="test_tenant_id", + certificate_content=certificate_content, + tenant_domains=["contoso.com"], + ) + ) + + assert result is True + session.test_teams_certificate_connection.assert_called_once() + session.test_exchange_certificate_connection.assert_called_once() + + session.close() diff --git a/tests/providers/m365/m365_fixtures.py b/tests/providers/m365/m365_fixtures.py index f985f94b6a..2c1a322ece 100644 --- a/tests/providers/m365/m365_fixtures.py +++ b/tests/providers/m365/m365_fixtures.py @@ -1,5 +1,6 @@ +from unittest.mock import MagicMock + from azure.identity import DefaultAzureCredential -from mock import MagicMock from prowler.providers.m365.m365_provider import M365Provider from prowler.providers.m365.models import ( diff --git a/tests/providers/m365/m365_provider_test.py b/tests/providers/m365/m365_provider_test.py index 0f629d21a9..0af2abc7c8 100644 --- a/tests/providers/m365/m365_provider_test.py +++ b/tests/providers/m365/m365_provider_test.py @@ -1,15 +1,16 @@ +import base64 import os -from unittest.mock import patch +from unittest.mock import MagicMock, mock_open, patch from uuid import uuid4 import pytest from azure.core.credentials import AccessToken from azure.identity import ( + CertificateCredential, ClientSecretCredential, DefaultAzureCredential, InteractiveBrowserCredential, ) -from mock import MagicMock from prowler.config.config import ( default_config_file_path, @@ -18,15 +19,29 @@ from prowler.config.config import ( ) from prowler.providers.common.models import Connection from prowler.providers.m365.exceptions.exceptions import ( + M365BrowserAuthNoFlagError, + M365BrowserAuthNoTenantIDError, + M365ClientIdAndClientSecretNotBelongingToTenantIdError, + M365ConfigCredentialsError, + M365CredentialsUnavailableError, + M365DefaultAzureCredentialError, + M365EnvironmentVariableError, + M365GetTokenIdentityError, M365HTTPResponseError, M365InvalidProviderIdError, M365MissingEnvironmentCredentialsError, M365NoAuthenticationMethodError, + M365NotTenantIdButClientIdAndClientSecretError, + M365NotValidCertificateContentError, + M365NotValidCertificatePathError, M365NotValidClientIdError, M365NotValidClientSecretError, M365NotValidPasswordError, M365NotValidTenantIdError, M365NotValidUserError, + M365TenantIdAndClientIdNotBelongingToClientSecretError, + M365TenantIdAndClientSecretNotBelongingToClientIdError, + M365UserCredentialsError, M365UserNotBelongingToTenantError, ) from prowler.providers.m365.m365_provider import M365Provider @@ -296,6 +311,10 @@ class TestM365Provider: patch( "prowler.providers.m365.m365_provider.GraphServiceClient" ) as mock_graph_client, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"), patch( "prowler.providers.m365.m365_provider.M365Provider.setup_identity", return_value=M365IdentityInfo( @@ -343,6 +362,10 @@ class TestM365Provider: patch( "prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials" ) as mock_validate_static_credentials, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"), patch( "prowler.providers.m365.m365_provider.M365Provider.setup_identity", return_value=M365IdentityInfo( @@ -378,9 +401,15 @@ class TestM365Provider: def test_test_connection_tenant_id_client_id_client_secret_no_user_password( self, ): - with patch( - "prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials" - ) as mock_validate_static_credentials: + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials" + ) as mock_validate_static_credentials, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"), + ): mock_validate_static_credentials.side_effect = M365NotValidUserError( file=os.path.basename(__file__), message="The provided M365 User is not valid.", @@ -403,9 +432,15 @@ class TestM365Provider: def test_test_connection_tenant_id_client_id_client_secret_user_no_password( self, ): - with patch( - "prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials" - ) as mock_validate_static_credentials: + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials" + ) as mock_validate_static_credentials, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"), + ): mock_validate_static_credentials.side_effect = M365NotValidPasswordError( file=os.path.basename(__file__), message="The provided M365 Password is not valid.", @@ -426,9 +461,15 @@ class TestM365Provider: assert "The provided M365 Password is not valid." in str(exception.value) def test_test_connection_with_httpresponseerror(self): - with patch( - "prowler.providers.m365.m365_provider.M365Provider.setup_session" - ) as mock_setup_session: + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_session" + ) as mock_setup_session, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"), + ): mock_setup_session.side_effect = M365HTTPResponseError( file="test_file", original_exception="Simulated HttpResponseError" ) @@ -446,9 +487,15 @@ class TestM365Provider: ) def test_test_connection_with_exception(self): - with patch( - "prowler.providers.m365.m365_provider.M365Provider.setup_session" - ) as mock_setup_session: + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_session" + ) as mock_setup_session, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"), + ): mock_setup_session.side_effect = Exception("Simulated Exception") with pytest.raises(Exception) as exception: @@ -466,7 +513,7 @@ class TestM365Provider: assert exception.type == M365NoAuthenticationMethodError assert ( - "M365 provider requires at least one authentication method set: [--env-auth | --az-cli-auth | --sp-env-auth | --browser-auth]" + "M365 provider requires at least one authentication method set: [--env-auth | --az-cli-auth | --sp-env-auth | --browser-auth | --certificate-auth]" in exception.value.args[0] ) @@ -503,9 +550,15 @@ class TestM365Provider: def test_test_connection_user_not_belonging_to_tenant( self, ): - with patch( - "prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials" - ) as mock_validate_static_credentials: + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials" + ) as mock_validate_static_credentials, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"), + ): mock_validate_static_credentials.side_effect = M365UserNotBelongingToTenantError( file=os.path.basename(__file__), message="The provided M365 User does not belong to the specified tenant.", @@ -559,24 +612,30 @@ class TestM365Provider: user="test@example.com", password="test_password", ) - assert "The provided Client Secret is not valid." in str(exception.value) + assert ( + "You must provide a client secret, certificate content or certificate path. Please check your credentials and try again." + in str(exception.value) + ) def test_validate_arguments_missing_env_credentials(self): - with pytest.raises(M365MissingEnvironmentCredentialsError) as exception: + with pytest.raises(M365ConfigCredentialsError) as exception: M365Provider.validate_arguments( az_cli_auth=False, sp_env_auth=False, env_auth=True, browser_auth=False, - tenant_id=None, + certificate_auth=False, + tenant_id="test_tenant_id", client_id="test_client_id", - client_secret="test_secret", + client_secret=None, user=None, password=None, + certificate_content=None, + certificate_path=None, ) assert ( - "M365 provider requires AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, M365_USER and M365_PASSWORD environment variables to be set when using --env-auth" + "You must provide a valid set of credentials. Please check your credentials and try again." in str(exception.value) ) @@ -588,6 +647,10 @@ class TestM365Provider: patch( "prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials" ) as mock_validate_static_credentials, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"), patch( "prowler.providers.m365.m365_provider.M365Provider.setup_identity", return_value=M365IdentityInfo( @@ -742,6 +805,10 @@ class TestM365Provider: patch( "prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials" ) as mock_validate_static_credentials, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"), patch( "prowler.providers.m365.m365_provider.M365Provider.setup_identity", return_value=M365IdentityInfo( @@ -780,3 +847,1287 @@ class TestM365Provider: f"The provider ID {provider_id} does not match any of the service principal tenant domains: contoso.onmicrosoft.com, contoso.com" in str(exception.value) ) + + def test_m365_provider_certificate_auth(self): + """Test M365 Provider initialization with certificate authentication""" + tenant_id = None + client_id = None + client_secret = None + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + fixer_config = load_and_validate_config_file( + "m365", default_fixer_config_file_path + ) + azure_region = "M365Global" + + # Mock certificate credential + mock_cert_credential = MagicMock() + mock_cert_credential.__class__ = CertificateCredential + + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_session", + return_value=mock_cert_credential, + ), + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_identity", + return_value=M365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="Service Principal with Certificate", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + certificate_thumbprint="ABC123", + ), + ), + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_powershell", + return_value=M365Credentials( + client_id=CLIENT_ID, + tenant_id=TENANT_ID, + certificate_content=certificate_content, + ), + ), + ): + m365_provider = M365Provider( + sp_env_auth=False, + az_cli_auth=False, + browser_auth=False, + env_auth=False, + certificate_auth=True, + tenant_id=tenant_id, + client_id=client_id, + client_secret=client_secret, + certificate_content=certificate_content, + region=azure_region, + config_path=default_config_file_path, + fixer_config=fixer_config, + ) + + assert m365_provider.region_config == M365RegionConfig( + name="M365Global", + authority=None, + base_url="https://graph.microsoft.com", + credential_scopes=["https://graph.microsoft.com/.default"], + ) + assert ( + m365_provider.identity.identity_type + == "Service Principal with Certificate" + ) + assert m365_provider.session == mock_cert_credential + + def test_check_service_principal_creds_env_vars_missing_client_id(self): + """Test check_service_principal_creds_env_vars with missing AZURE_CLIENT_ID""" + with ( + patch.dict(os.environ, {}, clear=True), + pytest.raises(M365EnvironmentVariableError) as exception, + ): + M365Provider.check_service_principal_creds_env_vars() + + assert exception.type == M365EnvironmentVariableError + assert ( + "Missing environment variable AZURE_CLIENT_ID required to authenticate." + in str(exception.value) + ) + + def test_check_service_principal_creds_env_vars_missing_tenant_id(self): + """Test check_service_principal_creds_env_vars with missing AZURE_TENANT_ID""" + with ( + patch.dict(os.environ, {"AZURE_CLIENT_ID": "test_client_id"}, clear=True), + pytest.raises(M365EnvironmentVariableError) as exception, + ): + M365Provider.check_service_principal_creds_env_vars() + + assert exception.type == M365EnvironmentVariableError + assert ( + "Missing environment variable AZURE_TENANT_ID required to authenticate." + in str(exception.value) + ) + + def test_check_service_principal_creds_env_vars_missing_client_secret(self): + """Test check_service_principal_creds_env_vars with missing AZURE_CLIENT_SECRET""" + with ( + patch.dict( + os.environ, + { + "AZURE_CLIENT_ID": "test_client_id", + "AZURE_TENANT_ID": "test_tenant_id", + }, + clear=True, + ), + pytest.raises(M365EnvironmentVariableError) as exception, + ): + M365Provider.check_service_principal_creds_env_vars() + + assert exception.type == M365EnvironmentVariableError + assert ( + "Missing environment variable AZURE_CLIENT_SECRET required to authenticate." + in str(exception.value) + ) + + def test_check_service_principal_creds_env_vars_success(self): + """Test check_service_principal_creds_env_vars with all required variables""" + with patch.dict( + os.environ, + { + "AZURE_CLIENT_ID": "test_client_id", + "AZURE_TENANT_ID": "test_tenant_id", + "AZURE_CLIENT_SECRET": "test_client_secret", + }, + ): + # Should not raise any exception + M365Provider.check_service_principal_creds_env_vars() + + def test_check_certificate_creds_env_vars_missing_client_id(self): + """Test check_certificate_creds_env_vars with missing AZURE_CLIENT_ID""" + with ( + patch.dict(os.environ, {}, clear=True), + pytest.raises(M365EnvironmentVariableError) as exception, + ): + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=True + ) + + assert exception.type == M365EnvironmentVariableError + assert ( + "Missing environment variable AZURE_CLIENT_ID required to authenticate." + in str(exception.value) + ) + + def test_check_certificate_creds_env_vars_missing_tenant_id(self): + """Test check_certificate_creds_env_vars with missing AZURE_TENANT_ID""" + with ( + patch.dict(os.environ, {"AZURE_CLIENT_ID": "test_client_id"}, clear=True), + pytest.raises(M365EnvironmentVariableError) as exception, + ): + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=True + ) + + assert exception.type == M365EnvironmentVariableError + assert ( + "Missing environment variable AZURE_TENANT_ID required to authenticate." + in str(exception.value) + ) + + def test_check_certificate_creds_env_vars_missing_certificate_content(self): + """Test check_certificate_creds_env_vars with missing M365_CERTIFICATE_CONTENT""" + with ( + patch.dict( + os.environ, + { + "AZURE_CLIENT_ID": "test_client_id", + "AZURE_TENANT_ID": "test_tenant_id", + }, + clear=True, + ), + pytest.raises(M365EnvironmentVariableError) as exception, + ): + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=True + ) + + assert exception.type == M365EnvironmentVariableError + assert ( + "Missing environment variable M365_CERTIFICATE_CONTENT required to authenticate." + in str(exception.value) + ) + + def test_check_certificate_creds_env_vars_missing_certificate_content_but_certificate_path( + self, + ): + """Test check_certificate_creds_env_vars with missing M365_CERTIFICATE_CONTENT""" + with patch.dict( + os.environ, + { + "AZURE_CLIENT_ID": "test_client_id", + "AZURE_TENANT_ID": "test_tenant_id", + }, + clear=True, + ): + # This should not raise an exception since check_certificate_content=False + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=False + ) + + def test_check_certificate_creds_env_vars_success(self): + """Test check_certificate_creds_env_vars with all required variables""" + with patch.dict( + os.environ, + { + "AZURE_CLIENT_ID": "test_client_id", + "AZURE_TENANT_ID": "test_tenant_id", + "M365_CERTIFICATE_CONTENT": base64.b64encode( + b"fake_certificate" + ).decode("utf-8"), + }, + ): + # Should not raise any exception + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=True + ) + + def test_setup_powershell_env_auth_missing_credentials(self): + """Test setup_powershell with env_auth but missing environment variables""" + with ( + patch.dict(os.environ, {}, clear=True), + pytest.raises(M365MissingEnvironmentCredentialsError) as exception, + ): + M365Provider.setup_powershell( + env_auth=True, + identity=M365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + tenant_domains=["test.onmicrosoft.com"], + location=LOCATION, + ), + ) + + assert exception.type == M365MissingEnvironmentCredentialsError + assert ( + "Missing M365_USER or M365_PASSWORD environment variables required for credentials authentication." + in str(exception.value) + ) + + def test_setup_powershell_env_auth_success(self): + """Test setup_powershell with env_auth and valid environment variables""" + with ( + patch.dict( + os.environ, + { + "M365_USER": "test@example.com", + "M365_PASSWORD": "password", + "AZURE_CLIENT_ID": CLIENT_ID, + "AZURE_CLIENT_SECRET": CLIENT_SECRET, + "AZURE_TENANT_ID": TENANT_ID, + }, + ), + patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials", + return_value=True, + ), + ): + result = M365Provider.setup_powershell( + env_auth=True, + identity=M365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + tenant_domains=["test.onmicrosoft.com"], + location=LOCATION, + ), + ) + + assert result.user == "test@example.com" + assert result.passwd == "password" + assert result.client_id == CLIENT_ID + + def test_setup_powershell_sp_env_auth_success(self): + """Test setup_powershell with sp_env_auth and valid environment variables""" + with ( + patch.dict( + os.environ, + { + "AZURE_CLIENT_ID": CLIENT_ID, + "AZURE_CLIENT_SECRET": CLIENT_SECRET, + "AZURE_TENANT_ID": TENANT_ID, + }, + ), + patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials", + return_value=True, + ), + ): + result = M365Provider.setup_powershell( + sp_env_auth=True, + identity=M365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="Service Principal", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + tenant_domains=["test.onmicrosoft.com"], + location=LOCATION, + ), + ) + + assert result.client_id == CLIENT_ID + assert result.client_secret == CLIENT_SECRET + assert result.tenant_id == TENANT_ID + + def test_setup_powershell_certificate_auth_success(self): + """Test setup_powershell with certificate_auth and valid environment variables""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + with ( + patch.dict( + os.environ, + { + "AZURE_CLIENT_ID": CLIENT_ID, + "AZURE_TENANT_ID": TENANT_ID, + "M365_CERTIFICATE_CONTENT": certificate_content, + }, + ), + patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials", + return_value=True, + ), + ): + identity = M365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="Service Principal with Certificate", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + tenant_domains=["test.onmicrosoft.com"], + location=LOCATION, + ) + + result = M365Provider.setup_powershell( + certificate_auth=True, + identity=identity, + ) + + assert result.client_id == CLIENT_ID + assert result.tenant_id == TENANT_ID + assert result.certificate_content == certificate_content + assert identity.identity_type == "Service Principal with Certificate" + + def test_setup_powershell_invalid_credentials(self): + """Test setup_powershell with invalid credentials""" + credentials_dict = { + "user": "test@example.com", + "password": "test_password", + "client_id": "test_client_id", + "tenant_id": "test_tenant_id", + "client_secret": "test_client_secret", + } + + with ( + patch( + "prowler.providers.m365.lib.powershell.m365_powershell.M365PowerShell.test_credentials", + return_value=False, + ), + pytest.raises(M365UserCredentialsError) as exception, + ): + M365Provider.setup_powershell( + env_auth=False, + m365_credentials=credentials_dict, + identity=M365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="User", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + tenant_domains=["test.onmicrosoft.com"], + location=LOCATION, + ), + ) + + assert exception.type == M365UserCredentialsError + assert "The provided User credentials are not valid." in str(exception.value) + + def test_validate_arguments_browser_auth_without_tenant_id(self): + """Test validate_arguments with browser_auth but missing tenant_id""" + with pytest.raises(M365BrowserAuthNoTenantIDError) as exception: + M365Provider.validate_arguments( + az_cli_auth=False, + sp_env_auth=False, + env_auth=False, + browser_auth=True, + certificate_auth=False, + tenant_id=None, + client_id=None, + client_secret=None, + user=None, + password=None, + certificate_content=None, + certificate_path=None, + ) + + assert exception.type == M365BrowserAuthNoTenantIDError + assert ( + "M365 Tenant ID (--tenant-id) is required for browser authentication mode" + in str(exception.value) + ) + + def test_validate_arguments_tenant_id_without_browser_flag(self): + """Test validate_arguments with tenant_id but without browser auth flag""" + with pytest.raises(M365BrowserAuthNoFlagError) as exception: + M365Provider.validate_arguments( + az_cli_auth=False, + sp_env_auth=False, + env_auth=False, + browser_auth=False, + certificate_auth=False, + tenant_id=TENANT_ID, + client_id=None, + client_secret=None, + user=None, + password=None, + certificate_content=None, + certificate_path=None, + ) + + assert exception.type == M365BrowserAuthNoFlagError + assert "browser authentication flag (--browser-auth) not found" in str( + exception.value + ) + + def test_validate_arguments_missing_tenant_id_with_credentials(self): + """Test validate_arguments with client credentials but missing tenant_id""" + with pytest.raises(M365NotTenantIdButClientIdAndClientSecretError) as exception: + M365Provider.validate_arguments( + az_cli_auth=False, + sp_env_auth=False, + env_auth=False, + browser_auth=False, + certificate_auth=False, + tenant_id=None, + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + user=None, + password=None, + certificate_content=None, + certificate_path=None, + ) + + assert exception.type == M365NotTenantIdButClientIdAndClientSecretError + assert "Tenant Id is required for M365 static credentials" in str( + exception.value + ) + + def test_test_connection_certificate_auth(self): + """Test test_connection with certificate authentication""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_session" + ) as mock_setup_session, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch("prowler.providers.m365.m365_provider.M365Provider.setup_powershell"), + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_identity", + return_value=M365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="Service Principal with Certificate", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + tenant_domains=["test.onmicrosoft.com"], + location=LOCATION, + ), + ), + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_static_credentials", + return_value={ + "tenant_id": TENANT_ID, + "client_id": CLIENT_ID, + "client_secret": None, + "user": None, + "password": None, + "certificate_content": certificate_content, + }, + ), + ): + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + test_connection = M365Provider.test_connection( + certificate_auth=True, + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_content=certificate_content, + region="M365Global", + raise_on_exception=False, + provider_id="test.onmicrosoft.com", + ) + + assert isinstance(test_connection, Connection) + assert test_connection.is_connected + assert test_connection.error is None + + def test_test_connection_get_token_identity_error(self): + """Test test_connection when setup_identity returns None""" + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_session" + ) as mock_setup_session, + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_identity", + return_value=None, + ), + pytest.raises(M365GetTokenIdentityError) as exception, + ): + mock_session = MagicMock() + mock_setup_session.return_value = mock_session + + M365Provider.test_connection( + az_cli_auth=True, + raise_on_exception=True, + ) + + assert exception.type == M365GetTokenIdentityError + assert "Failed to retrieve M365 identity" in str(exception.value) + + def test_validate_static_credentials_client_id_secret_tenant_error(self): + """Test validate_static_credentials with client/secret/tenant mismatch errors""" + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.verify_client", + side_effect=M365NotValidTenantIdError( + file="test", message="Invalid tenant" + ), + ), + pytest.raises( + M365ClientIdAndClientSecretNotBelongingToTenantIdError + ) as exception, + ): + M365Provider.validate_static_credentials( + tenant_id=str(uuid4()), + client_id=str(uuid4()), + client_secret="test_secret", + user="test@example.com", + password="test_password", + ) + + assert exception.type == M365ClientIdAndClientSecretNotBelongingToTenantIdError + assert ( + "The provided Client ID and Client Secret do not belong to the specified Tenant ID." + in str(exception.value) + ) + + def test_validate_static_credentials_tenant_secret_client_error(self): + """Test validate_static_credentials with tenant/secret/client mismatch errors""" + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.verify_client", + side_effect=M365NotValidClientIdError( + file="test", message="Invalid client ID" + ), + ), + pytest.raises( + M365TenantIdAndClientSecretNotBelongingToClientIdError + ) as exception, + ): + M365Provider.validate_static_credentials( + tenant_id=str(uuid4()), + client_id=str(uuid4()), + client_secret="test_secret", + user="test@example.com", + password="test_password", + ) + + assert exception.type == M365TenantIdAndClientSecretNotBelongingToClientIdError + assert ( + "The provided Tenant ID and Client Secret do not belong to the specified Client ID." + in str(exception.value) + ) + + def test_validate_static_credentials_tenant_client_secret_error(self): + """Test validate_static_credentials with tenant/client/secret mismatch errors""" + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.verify_client", + side_effect=M365NotValidClientSecretError( + file="test", message="Invalid client secret" + ), + ), + pytest.raises( + M365TenantIdAndClientIdNotBelongingToClientSecretError + ) as exception, + ): + M365Provider.validate_static_credentials( + tenant_id=str(uuid4()), + client_id=str(uuid4()), + client_secret="test_secret", + user="test@example.com", + password="test_password", + ) + + assert exception.type == M365TenantIdAndClientIdNotBelongingToClientSecretError + assert ( + "The provided Tenant ID and Client ID do not belong to the specified Client Secret." + in str(exception.value) + ) + + def test_test_connection_default_azure_credential_error(self): + """Test test_connection with DefaultAzureCredential error in exception handling""" + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_session", + side_effect=M365DefaultAzureCredentialError( + file="test", + original_exception=Exception("Default credential error"), + ), + ), + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + pytest.raises(M365DefaultAzureCredentialError) as exception, + ): + M365Provider.test_connection( + az_cli_auth=True, + raise_on_exception=True, + ) + + assert exception.type == M365DefaultAzureCredentialError + + def test_test_connection_credentials_unavailable_error_handling(self): + """Test test_connection with CredentialsUnavailableError in exception handling""" + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_session", + side_effect=M365CredentialsUnavailableError( + file="test", original_exception=Exception("Credentials unavailable") + ), + ), + patch( + "prowler.providers.m365.m365_provider.M365Provider.validate_arguments" + ), + pytest.raises(M365CredentialsUnavailableError) as exception, + ): + M365Provider.test_connection( + sp_env_auth=True, + raise_on_exception=True, + ) + + assert exception.type == M365CredentialsUnavailableError + + def test_setup_session_certificate_auth_success(self): + """Test setup_session method with certificate authentication - success""" + with ( + patch.dict( + os.environ, + { + "AZURE_CLIENT_ID": CLIENT_ID, + "AZURE_TENANT_ID": TENANT_ID, + "M365_CERTIFICATE_CONTENT": base64.b64encode( + b"fake_certificate" + ).decode("utf-8"), + }, + ), + patch( + "prowler.providers.m365.m365_provider.CertificateCredential" + ) as mock_cert_credential, + ): + mock_credential_instance = MagicMock() + mock_cert_credential.return_value = mock_credential_instance + + region_config = M365RegionConfig( + name="M365Global", + authority=None, + base_url="https://graph.microsoft.com", + credential_scopes=["https://graph.microsoft.com/.default"], + ) + + result = M365Provider.setup_session( + az_cli_auth=False, + sp_env_auth=False, + env_auth=False, + browser_auth=False, + certificate_auth=True, + certificate_path=None, + tenant_id=None, + m365_credentials=None, + region_config=region_config, + ) + + assert result == mock_credential_instance + mock_cert_credential.assert_called_once_with( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_data=base64.b64decode( + base64.b64encode(b"fake_certificate").decode("utf-8") + ), + ) + + def test_setup_session_certificate_auth_client_authentication_error(self): + """Test setup_session method with certificate authentication - ClientAuthenticationError""" + from azure.core.exceptions import ClientAuthenticationError + + from prowler.providers.m365.exceptions.exceptions import M365SetUpSessionError + + with ( + patch.dict( + os.environ, + { + "AZURE_CLIENT_ID": CLIENT_ID, + "AZURE_TENANT_ID": TENANT_ID, + "M365_CERTIFICATE_CONTENT": base64.b64encode( + b"fake_certificate" + ).decode("utf-8"), + }, + ), + patch( + "prowler.providers.m365.m365_provider.CertificateCredential", + side_effect=ClientAuthenticationError("Authentication failed"), + ), + pytest.raises(M365SetUpSessionError) as exception, + ): + region_config = M365RegionConfig( + name="M365Global", + authority=None, + base_url="https://graph.microsoft.com", + credential_scopes=["https://graph.microsoft.com/.default"], + ) + + M365Provider.setup_session( + az_cli_auth=False, + sp_env_auth=False, + env_auth=False, + browser_auth=False, + certificate_auth=True, + certificate_path=None, + tenant_id=None, + m365_credentials=None, + region_config=region_config, + ) + + # The error should be wrapped in M365SetUpSessionError and contain the ClientAuthenticationError + assert exception.type == M365SetUpSessionError + assert "M365ClientAuthenticationError" in str(exception.value) + + def test_setup_session_certificate_auth_with_static_credentials(self): + """Test setup_session method with certificate authentication using static credentials""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + m365_credentials = { + "tenant_id": TENANT_ID, + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "user": None, + "password": None, + "certificate_content": certificate_content, + } + + with ( + patch( + "prowler.providers.m365.m365_provider.CertificateCredential" + ) as mock_credential, + ): + mock_credential_instance = MagicMock() + mock_credential.return_value = mock_credential_instance + + region_config = M365RegionConfig( + name="M365Global", + authority=None, + base_url="https://graph.microsoft.com", + credential_scopes=["https://graph.microsoft.com/.default"], + ) + + result = M365Provider.setup_session( + az_cli_auth=False, + sp_env_auth=False, + env_auth=False, + browser_auth=False, + certificate_auth=False, + certificate_path=None, + tenant_id=None, + m365_credentials=m365_credentials, + region_config=region_config, + ) + + assert result == mock_credential_instance + mock_credential.assert_called_once_with( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_data=base64.b64decode(certificate_content), + ) + + def test_setup_powershell_certificate_auth_missing_env_vars(self): + """Test setup_powershell with certificate_auth but missing environment variables""" + from pydantic.v1.error_wrappers import ValidationError + + with (patch.dict(os.environ, {}, clear=True),): + identity = M365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="Service Principal with Certificate", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + tenant_domains=["test.onmicrosoft.com"], + location=LOCATION, + ) + + # Should raise ValidationError when trying to create credentials with None values + with pytest.raises(ValidationError) as exc_info: + M365Provider.setup_powershell( + certificate_auth=True, + identity=identity, + ) + + # Verify the error is about None values not being allowed + assert "none is not an allowed value" in str(exc_info.value).lower() + + def test_validate_arguments_certificate_auth_valid(self): + """Test validate_arguments method with valid certificate authentication arguments""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + # Should not raise any exception + M365Provider.validate_arguments( + az_cli_auth=False, + sp_env_auth=False, + env_auth=False, + browser_auth=False, + certificate_auth=True, + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + client_secret=None, + user=None, + password=None, + certificate_content=certificate_content, + certificate_path=None, + ) + + def test_validate_arguments_certificate_auth_missing_certificate_content(self): + """Test validate_arguments method with certificate auth but missing certificate content""" + with pytest.raises(M365ConfigCredentialsError) as exception: + M365Provider.validate_arguments( + az_cli_auth=False, + sp_env_auth=False, + env_auth=False, + browser_auth=False, + certificate_auth=False, + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + client_secret=None, + user=None, + password=None, + certificate_content=None, + certificate_path=None, + ) + + assert "You must provide a valid set of credentials" in str(exception.value) + + def test_print_credentials_with_certificate(self): + """Test print_credentials method with certificate authentication""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + # Mock certificate credential + mock_cert_credential = MagicMock() + mock_cert_credential.__class__ = CertificateCredential + + with ( + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_session", + return_value=mock_cert_credential, + ), + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_identity", + return_value=M365IdentityInfo( + identity_id=IDENTITY_ID, + identity_type="Service Principal with Certificate", + tenant_id=TENANT_ID, + tenant_domain=DOMAIN, + location=LOCATION, + certificate_thumbprint="ABC123DEF456", + ), + ), + patch( + "prowler.providers.m365.m365_provider.M365Provider.setup_powershell", + return_value=M365Credentials( + client_id=CLIENT_ID, + tenant_id=TENANT_ID, + certificate_content=certificate_content, + ), + ), + patch( + "prowler.providers.m365.m365_provider.print_boxes" + ) as mock_print_boxes, + ): + m365_provider = M365Provider( + certificate_auth=True, + region="M365Global", + ) + + m365_provider.print_credentials() + + # Verify print_boxes was called + mock_print_boxes.assert_called_once() + args, _ = mock_print_boxes.call_args + report_lines = args[0] + + # Check that certificate thumbprint is in the output + cert_line_found = any( + "Certificate Thumbprint" in line for line in report_lines + ) + assert ( + cert_line_found + ), "Certificate thumbprint should be in printed credentials" + + def test_validate_static_credentials_invalid_certificate_content(self): + """Test validate_static_credentials method with invalid certificate content""" + invalid_cert_content = "invalid_base64_content" + + with pytest.raises(RuntimeError) as exception: + M365Provider.validate_static_credentials( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_content=invalid_cert_content, + ) + + assert "An unexpected error occurred" in str(exception.value) + + def test_validate_static_credentials_invalid_certificate_path(self): + """Test validate_static_credentials method with invalid certificate path""" + invalid_cert_path = "/non/existent/path/cert.pem" + + with pytest.raises(M365NotValidCertificatePathError) as exception: + M365Provider.validate_static_credentials( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_path=invalid_cert_path, + ) + + assert "certificate path is not valid" in str(exception.value) + + def test_validate_static_credentials_certificate_base64_validation_error(self): + """Test validate_static_credentials method with invalid base64 certificate content""" + invalid_cert_content = "not_valid_base64!@#$%" + + with pytest.raises(M365NotValidCertificateContentError) as exception: + M365Provider.validate_static_credentials( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_content=invalid_cert_content, + ) + + assert "certificate content is not valid base64 encoded data" in str( + exception.value + ) + + def test_validate_static_credentials_certificate_path_file_not_found(self): + """Test validate_static_credentials method with certificate path that doesn't exist""" + invalid_cert_path = "/totally/non/existent/path/cert.pem" + + with pytest.raises(M365NotValidCertificatePathError) as exception: + M365Provider.validate_static_credentials( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_path=invalid_cert_path, + ) + + assert "certificate path is not valid" in str(exception.value) + + def test_validate_static_credentials_valid_certificate_content(self): + """Test validate_static_credentials method with valid certificate content""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + with patch( + "prowler.providers.m365.m365_provider.M365Provider.verify_client" + ) as mock_verify: + mock_verify.return_value = None + + result = M365Provider.validate_static_credentials( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_content=certificate_content, + ) + + assert result["tenant_id"] == TENANT_ID + assert result["client_id"] == CLIENT_ID + assert result["certificate_content"] == certificate_content + mock_verify.assert_called_once_with( + TENANT_ID, CLIENT_ID, None, certificate_content, None + ) + + @patch("builtins.open", mock_open(read_data=b"fake_certificate_data")) + def test_validate_static_credentials_valid_certificate_path(self): + """Test validate_static_credentials method with valid certificate path""" + certificate_path = "/path/to/cert.pem" + + with patch( + "prowler.providers.m365.m365_provider.M365Provider.verify_client" + ) as mock_verify: + mock_verify.return_value = None + + result = M365Provider.validate_static_credentials( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_path=certificate_path, + ) + + assert result["tenant_id"] == TENANT_ID + assert result["client_id"] == CLIENT_ID + assert result["certificate_path"] == certificate_path + mock_verify.assert_called_once_with( + TENANT_ID, CLIENT_ID, None, b"fake_certificate_data", certificate_path + ) + + @patch("prowler.providers.m365.m365_provider.asyncio.get_event_loop") + @patch("prowler.providers.m365.m365_provider.GraphServiceClient") + @patch("prowler.providers.m365.m365_provider.CertificateCredential") + def test_verify_client_certificate_content_success( + self, mock_cert_cred, mock_graph, mock_loop + ): + """Test verify_client method with valid certificate content""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + # Mock the async call + mock_loop_instance = MagicMock() + mock_loop.return_value = mock_loop_instance + mock_loop_instance.run_until_complete.return_value = [{"id": "domain.com"}] + + # Mock credential and graph client + mock_credential = MagicMock() + mock_cert_cred.return_value = mock_credential + mock_client = MagicMock() + mock_graph.return_value = mock_client + + # Should not raise any exception + M365Provider.verify_client( + TENANT_ID, CLIENT_ID, None, certificate_content, None + ) + + mock_cert_cred.assert_called_once() + mock_graph.assert_called_once_with(credentials=mock_credential) + + @patch("prowler.providers.m365.m365_provider.asyncio.get_event_loop") + @patch("prowler.providers.m365.m365_provider.GraphServiceClient") + @patch("prowler.providers.m365.m365_provider.CertificateCredential") + def test_verify_client_certificate_content_failure( + self, mock_cert_cred, mock_graph, mock_loop + ): + """Test verify_client method with certificate content that fails validation""" + certificate_content = base64.b64encode(b"fake_certificate").decode("utf-8") + + # Mock the async call to return empty result (invalid certificate) + mock_loop_instance = MagicMock() + mock_loop.return_value = mock_loop_instance + mock_loop_instance.run_until_complete.return_value = None + + # Mock credential and graph client + mock_credential = MagicMock() + mock_cert_cred.return_value = mock_credential + mock_client = MagicMock() + mock_graph.return_value = mock_client + + with pytest.raises(RuntimeError) as exception: + M365Provider.verify_client( + TENANT_ID, CLIENT_ID, None, certificate_content, None + ) + + assert "certificate content is not valid" in str(exception.value) + + @patch("builtins.open", mock_open(read_data=b"fake_certificate_data")) + @patch("prowler.providers.m365.m365_provider.asyncio.get_event_loop") + @patch("prowler.providers.m365.m365_provider.GraphServiceClient") + @patch("prowler.providers.m365.m365_provider.CertificateCredential") + def test_verify_client_certificate_path_success( + self, mock_cert_cred, mock_graph, mock_loop + ): + """Test verify_client method with valid certificate path""" + certificate_path = "/path/to/cert.pem" + + # Mock the async call + mock_loop_instance = MagicMock() + mock_loop.return_value = mock_loop_instance + mock_loop_instance.run_until_complete.return_value = [{"id": "domain.com"}] + + # Mock credential and graph client + mock_credential = MagicMock() + mock_cert_cred.return_value = mock_credential + mock_client = MagicMock() + mock_graph.return_value = mock_client + + # Should not raise any exception + M365Provider.verify_client(TENANT_ID, CLIENT_ID, None, None, certificate_path) + + mock_cert_cred.assert_called_once() + mock_graph.assert_called_once_with(credentials=mock_credential) + + @patch("builtins.open", mock_open(read_data=b"fake_certificate_data")) + @patch("prowler.providers.m365.m365_provider.asyncio.get_event_loop") + @patch("prowler.providers.m365.m365_provider.GraphServiceClient") + @patch("prowler.providers.m365.m365_provider.CertificateCredential") + def test_verify_client_certificate_path_failure( + self, mock_cert_cred, mock_graph, mock_loop + ): + """Test verify_client method with certificate path that fails validation""" + certificate_path = "/path/to/cert.pem" + + # Mock the async call to return empty result (invalid certificate) + mock_loop_instance = MagicMock() + mock_loop.return_value = mock_loop_instance + mock_loop_instance.run_until_complete.return_value = None + + # Mock credential and graph client + mock_credential = MagicMock() + mock_cert_cred.return_value = mock_credential + mock_client = MagicMock() + mock_graph.return_value = mock_client + + with pytest.raises(RuntimeError) as exception: + M365Provider.verify_client( + TENANT_ID, CLIENT_ID, None, None, certificate_path + ) + + assert "certificate is not valid" in str(exception.value) + + def test_setup_session_certificate_auth_with_certificate_path(self): + """Test setup_session method with certificate authentication using certificate path""" + certificate_path = "/path/to/cert.pem" + mock_cert_data = b"fake_certificate_data" + + with ( + patch("builtins.open", mock_open(read_data=mock_cert_data)), + patch("prowler.providers.m365.m365_provider.getenv") as mock_getenv, + patch( + "prowler.providers.m365.m365_provider.CertificateCredential" + ) as mock_cert_cred, + ): + mock_getenv.side_effect = lambda var: { + "AZURE_TENANT_ID": TENANT_ID, + "AZURE_CLIENT_ID": CLIENT_ID, + }.get(var) + + mock_credential = MagicMock() + mock_cert_cred.return_value = mock_credential + + result = M365Provider.setup_session( + az_cli_auth=False, + sp_env_auth=False, + env_auth=False, + browser_auth=False, + certificate_auth=True, + certificate_path=certificate_path, + tenant_id=None, + m365_credentials=None, + region_config=MagicMock(), + ) + + assert result == mock_credential + mock_cert_cred.assert_called_once_with( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_data=mock_cert_data, + ) + + def test_setup_session_certificate_auth_with_static_credentials_certificate_path( + self, + ): + """Test setup_session method with certificate authentication using static credentials with certificate path""" + certificate_path = "/path/to/cert.pem" + mock_cert_data = b"fake_certificate_data" + + m365_credentials = { + "tenant_id": TENANT_ID, + "client_id": CLIENT_ID, + "client_secret": None, + "user": None, + "password": None, + "certificate_content": None, + "certificate_path": certificate_path, + } + + with ( + patch("builtins.open", mock_open(read_data=mock_cert_data)), + patch( + "prowler.providers.m365.m365_provider.CertificateCredential" + ) as mock_cert_cred, + ): + mock_credential = MagicMock() + mock_cert_cred.return_value = mock_credential + + result = M365Provider.setup_session( + az_cli_auth=False, + sp_env_auth=False, + env_auth=False, + browser_auth=False, + certificate_auth=False, + certificate_path=None, + tenant_id=None, + m365_credentials=m365_credentials, + region_config=MagicMock(), + ) + + assert result == mock_credential + mock_cert_cred.assert_called_once_with( + tenant_id=TENANT_ID, + client_id=CLIENT_ID, + certificate_data=mock_cert_data, + ) + + def test_check_certificate_creds_env_vars_with_certificate_content_success(self): + """Test check_certificate_creds_env_vars method with certificate content check""" + with patch("prowler.providers.m365.m365_provider.getenv") as mock_getenv: + mock_getenv.side_effect = lambda var: { + "AZURE_CLIENT_ID": CLIENT_ID, + "AZURE_TENANT_ID": TENANT_ID, + "M365_CERTIFICATE_CONTENT": "fake_certificate_content", + }.get(var) + + # Should not raise any exception + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=True + ) + + def test_check_certificate_creds_env_vars_without_certificate_content_success(self): + """Test check_certificate_creds_env_vars method without certificate content check""" + with patch("prowler.providers.m365.m365_provider.getenv") as mock_getenv: + mock_getenv.side_effect = lambda var: { + "AZURE_CLIENT_ID": CLIENT_ID, + "AZURE_TENANT_ID": TENANT_ID, + }.get(var) + + # Should not raise any exception + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=False + ) + + def test_check_certificate_creds_env_vars_missing_client_id_with_certificate(self): + """Test check_certificate_creds_env_vars method missing client ID with certificate content""" + with patch("prowler.providers.m365.m365_provider.getenv") as mock_getenv: + mock_getenv.side_effect = lambda var: { + "AZURE_TENANT_ID": TENANT_ID, + "M365_CERTIFICATE_CONTENT": "fake_certificate_content", + }.get(var) + + with pytest.raises(M365EnvironmentVariableError) as exception: + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=True + ) + + assert "Missing environment variable AZURE_CLIENT_ID" in str( + exception.value + ) + + def test_check_certificate_creds_env_vars_missing_tenant_id_with_certificate(self): + """Test check_certificate_creds_env_vars method missing tenant ID with certificate content""" + with patch("prowler.providers.m365.m365_provider.getenv") as mock_getenv: + mock_getenv.side_effect = lambda var: { + "AZURE_CLIENT_ID": CLIENT_ID, + "M365_CERTIFICATE_CONTENT": "fake_certificate_content", + }.get(var) + + with pytest.raises(M365EnvironmentVariableError) as exception: + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=True + ) + + assert "Missing environment variable AZURE_TENANT_ID" in str( + exception.value + ) + + def test_check_certificate_creds_env_vars_missing_certificate_content_when_required( + self, + ): + """Test check_certificate_creds_env_vars method missing certificate content when required""" + with patch("prowler.providers.m365.m365_provider.getenv") as mock_getenv: + mock_getenv.side_effect = lambda var: { + "AZURE_CLIENT_ID": CLIENT_ID, + "AZURE_TENANT_ID": TENANT_ID, + }.get(var) + + with pytest.raises(M365EnvironmentVariableError) as exception: + M365Provider.check_certificate_creds_env_vars( + check_certificate_content=True + ) + + assert "Missing environment variable M365_CERTIFICATE_CONTENT" in str( + exception.value + ) diff --git a/tests/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured_test.py b/tests/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured_test.py index 7baedbeb61..c3200b2e36 100644 --- a/tests/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured_test.py +++ b/tests/providers/m365/services/defender/defender_antispam_outbound_policy_configured/defender_antispam_outbound_policy_configured_test.py @@ -36,7 +36,7 @@ class Test_defender_antispam_outbound_policy_configured: notify_sender_blocked=True, notify_limit_exceeded_addresses=["admin@example.com"], notify_sender_blocked_addresses=["admin@example.com"], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=True, ) } @@ -93,7 +93,7 @@ class Test_defender_antispam_outbound_policy_configured: notify_sender_blocked=True, notify_limit_exceeded_addresses=["admin@example.com"], notify_sender_blocked_addresses=["admin@example.com"], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=True, ), "Policy1": OutboundSpamPolicy( @@ -102,7 +102,7 @@ class Test_defender_antispam_outbound_policy_configured: notify_sender_blocked=True, notify_limit_exceeded_addresses=["admin@example.com"], notify_sender_blocked_addresses=["admin@example.com"], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=False, ), } @@ -177,7 +177,7 @@ class Test_defender_antispam_outbound_policy_configured: notify_sender_blocked=True, notify_limit_exceeded_addresses=["admin@example.com"], notify_sender_blocked_addresses=["admin@example.com"], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=True, ), "Policy1": OutboundSpamPolicy( @@ -186,7 +186,7 @@ class Test_defender_antispam_outbound_policy_configured: notify_sender_blocked=False, notify_limit_exceeded_addresses=[], notify_sender_blocked_addresses=[], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=False, ), } @@ -261,7 +261,7 @@ class Test_defender_antispam_outbound_policy_configured: notify_sender_blocked=False, notify_limit_exceeded_addresses=[], notify_sender_blocked_addresses=[], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=True, ), "Policy1": OutboundSpamPolicy( @@ -270,7 +270,7 @@ class Test_defender_antispam_outbound_policy_configured: notify_sender_blocked=True, notify_limit_exceeded_addresses=["admin@example.com"], notify_sender_blocked_addresses=["admin@example.com"], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=False, ), } @@ -344,7 +344,7 @@ class Test_defender_antispam_outbound_policy_configured: notify_sender_blocked=False, notify_limit_exceeded_addresses=[], notify_sender_blocked_addresses=[], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=True, ) } @@ -398,7 +398,7 @@ class Test_defender_antispam_outbound_policy_configured: notify_sender_blocked=False, notify_limit_exceeded_addresses=[], notify_sender_blocked_addresses=[], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=True, ), "Policy1": OutboundSpamPolicy( @@ -407,7 +407,7 @@ class Test_defender_antispam_outbound_policy_configured: notify_sender_blocked=False, notify_limit_exceeded_addresses=[], notify_sender_blocked_addresses=[], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=False, ), } diff --git a/tests/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled_test.py b/tests/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled_test.py index 64fce5f88b..72f5c104ff 100644 --- a/tests/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled_test.py +++ b/tests/providers/m365/services/defender/defender_antispam_outbound_policy_forwarding_disabled/defender_antispam_outbound_policy_forwarding_disabled_test.py @@ -32,7 +32,7 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: defender_client.outbound_spam_policies = { "Default": OutboundSpamPolicy( name="Default", - auto_forwarding_mode=False, + auto_forwarding_mode="Off", notify_limit_exceeded=True, notify_sender_blocked=True, notify_limit_exceeded_addresses=["admin@example.com"], @@ -86,7 +86,7 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: defender_client.outbound_spam_policies = { "Default": OutboundSpamPolicy( name="Default", - auto_forwarding_mode=False, + auto_forwarding_mode="Off", notify_limit_exceeded=True, notify_sender_blocked=True, notify_limit_exceeded_addresses=["admin@example.com"], @@ -95,7 +95,7 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: ), "Policy1": OutboundSpamPolicy( name="Policy1", - auto_forwarding_mode=False, + auto_forwarding_mode="Off", notify_limit_exceeded=True, notify_sender_blocked=True, notify_limit_exceeded_addresses=["admin@example.com"], @@ -172,7 +172,7 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: defender_client.outbound_spam_policies = { "Default": OutboundSpamPolicy( name="Default", - auto_forwarding_mode=False, + auto_forwarding_mode="Off", notify_limit_exceeded=True, notify_sender_blocked=True, notify_limit_exceeded_addresses=["admin@example.com"], @@ -181,7 +181,7 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: ), "Policy1": OutboundSpamPolicy( name="Policy1", - auto_forwarding_mode=True, + auto_forwarding_mode="On", notify_limit_exceeded=False, notify_sender_blocked=False, notify_limit_exceeded_addresses=[], @@ -258,7 +258,7 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: defender_client.outbound_spam_policies = { "Default": OutboundSpamPolicy( name="Default", - auto_forwarding_mode=True, + auto_forwarding_mode="On", notify_limit_exceeded=False, notify_sender_blocked=False, notify_limit_exceeded_addresses=[], @@ -267,7 +267,7 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: ), "Policy1": OutboundSpamPolicy( name="Policy1", - auto_forwarding_mode=False, + auto_forwarding_mode="Off", notify_limit_exceeded=True, notify_sender_blocked=True, notify_limit_exceeded_addresses=["admin@example.com"], @@ -343,7 +343,7 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: defender_client.outbound_spam_policies = { "Default": OutboundSpamPolicy( name="Default", - auto_forwarding_mode=True, + auto_forwarding_mode="On", notify_limit_exceeded=False, notify_sender_blocked=False, notify_limit_exceeded_addresses=[], @@ -397,7 +397,7 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: defender_client.outbound_spam_policies = { "Default": OutboundSpamPolicy( name="Default", - auto_forwarding_mode=True, + auto_forwarding_mode="On", notify_limit_exceeded=False, notify_sender_blocked=False, notify_limit_exceeded_addresses=[], @@ -406,7 +406,7 @@ class Test_defender_antispam_outbound_policy_forwarding_disabled: ), "Policy1": OutboundSpamPolicy( name="Policy1", - auto_forwarding_mode=True, + auto_forwarding_mode="On", notify_limit_exceeded=False, notify_sender_blocked=False, notify_limit_exceeded_addresses=[], diff --git a/tests/providers/m365/services/defender/m365_defender_service_test.py b/tests/providers/m365/services/defender/m365_defender_service_test.py index 35534a53e3..a78cc4b10a 100644 --- a/tests/providers/m365/services/defender/m365_defender_service_test.py +++ b/tests/providers/m365/services/defender/m365_defender_service_test.py @@ -180,7 +180,7 @@ def mock_defender_get_outbound_spam_filter_policy(_): notify_limit_exceeded=True, notify_limit_exceeded_addresses=["security@example.com"], notify_sender_blocked_addresses=["security@example.com"], - auto_forwarding_mode=False, + auto_forwarding_mode="Off", default=False, ), "Policy2": OutboundSpamPolicy( @@ -189,7 +189,7 @@ def mock_defender_get_outbound_spam_filter_policy(_): notify_limit_exceeded=False, notify_limit_exceeded_addresses=[], notify_sender_blocked_addresses=[], - auto_forwarding_mode=True, + auto_forwarding_mode="On", default=True, ), } @@ -438,7 +438,7 @@ class Test_Defender_Service: assert outbound_spam_policies[ "Policy1" ].notify_sender_blocked_addresses == ["security@example.com"] - assert outbound_spam_policies["Policy1"].auto_forwarding_mode is False + assert outbound_spam_policies["Policy1"].auto_forwarding_mode == "Off" assert outbound_spam_policies["Policy1"].default is False assert outbound_spam_policies["Policy2"].name == "Policy2" assert outbound_spam_policies["Policy2"].notify_sender_blocked is False @@ -449,7 +449,7 @@ class Test_Defender_Service: assert ( outbound_spam_policies["Policy2"].notify_sender_blocked_addresses == [] ) - assert outbound_spam_policies["Policy2"].auto_forwarding_mode is True + assert outbound_spam_policies["Policy2"].auto_forwarding_mode == "On" assert outbound_spam_policies["Policy2"].default is True defender_client.powershell.close() diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index a067e0ecab..ee939ada4e 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -8,11 +8,18 @@ All notable changes to the **Prowler UI** are documented in this file. - `Cloud Provider` type filter to providers page [(#8473)](https://github.com/prowler-cloud/prowler/pull/8473) - New menu item under Configuration section for quick access to the Mutelist [(#8444)](https://github.com/prowler-cloud/prowler/pull/8444) +- Resource agent to Lighthouse for querying resource information [(#8509)](https://github.com/prowler-cloud/prowler/pull/8509) +- Lighthouse support for OpenAI GPT-5 [(#8527)](https://github.com/prowler-cloud/prowler/pull/8527) ### 🔄 Changed +- Disable `See Compliance` button until scan completes [(#8487)](https://github.com/prowler-cloud/prowler/pull/8487) +- Provider connection filter now shows "Connected/Disconnected" instead of "true/false" for better UX [(#8520)](https://github.com/prowler-cloud/prowler/pull/8520) + ### 🐞 Fixed +- DataTable column headers set to single-line [(#8480)](https://github.com/prowler-cloud/prowler/pull/8480) + ### ❌ Removed --- @@ -25,7 +32,6 @@ All notable changes to the **Prowler UI** are documented in this file. - `GitHub` submenu to High Risk Findings [(#8488)](https://github.com/prowler-cloud/prowler/pull/8488) - Improved Overview chart `Findings by Severity` spacing [(#8491)](https://github.com/prowler-cloud/prowler/pull/8491) - ## [1.10.0] (Prowler v5.10.0) ### 🚀 Added @@ -78,6 +84,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Error message when launching a scan if user has no permissions [(#8280)](https://github.com/prowler-cloud/prowler/pull/8280) - Include compliance in the download button tooltip [(#8307)](https://github.com/prowler-cloud/prowler/pull/8307) +- Redirection and error handling issues after deleting a provider groups [(#8389)](https://github.com/prowler-cloud/prowler/pull/8389) --- diff --git a/ui/actions/lighthouse/resources.ts b/ui/actions/lighthouse/resources.ts index 21beaabc19..cf763909e0 100644 --- a/ui/actions/lighthouse/resources.ts +++ b/ui/actions/lighthouse/resources.ts @@ -1,12 +1,18 @@ import { apiBaseUrl, getAuthHeaders, parseStringify } from "@/lib/helper"; -export async function getLighthouseResources( - page: number = 1, - query: string = "", - sort: string = "", - filters: any = {}, - fields: string[] = [], -) { +export async function getLighthouseResources({ + page = 1, + query = "", + sort = "", + filters = {}, + fields = [], +}: { + page?: number; + query?: string; + sort?: string; + filters?: any; + fields?: string[]; +}) { const headers = await getAuthHeaders({ contentType: false }); const url = new URL(`${apiBaseUrl}/resources`); @@ -29,7 +35,7 @@ export async function getLighthouseResources( if (filters) { for (const [key, value] of Object.entries(filters)) { - url.searchParams.append(`filter[${key}]`, value as string); + url.searchParams.append(`${key}`, value as string); } } @@ -46,11 +52,67 @@ export async function getLighthouseResources( } } -export async function getLighthouseResourceById( - id: string, - fields: string[] = [], - include: string[] = [], -) { +export async function getLighthouseLatestResources({ + page = 1, + query = "", + sort = "", + filters = {}, + fields = [], +}: { + page?: number; + query?: string; + sort?: string; + filters?: any; + fields?: string[]; +}) { + const headers = await getAuthHeaders({ contentType: false }); + + const url = new URL(`${apiBaseUrl}/resources/latest`); + + if (page) { + url.searchParams.append("page[number]", page.toString()); + } + + if (sort) { + url.searchParams.append("sort", sort); + } + + if (query) { + url.searchParams.append("filter[search]", query); + } + + if (fields.length > 0) { + url.searchParams.append("fields[resources]", fields.join(",")); + } + + if (filters) { + for (const [key, value] of Object.entries(filters)) { + url.searchParams.append(`${key}`, value as string); + } + } + + try { + const response = await fetch(url.toString(), { + headers, + }); + const data = await response.json(); + const parsedData = parseStringify(data); + return parsedData; + } catch (error) { + console.error("Error fetching resources:", error); + return undefined; + } +} + +export async function getLighthouseResourceById({ + id, + fields = [], + include = [], +}: { + id: string; + fields?: string[]; + include?: string[]; +}) { const headers = await getAuthHeaders({ contentType: false }); const url = new URL(`${apiBaseUrl}/resources/${id}`); diff --git a/ui/actions/manage-groups/manage-groups.ts b/ui/actions/manage-groups/manage-groups.ts index f2b1ca7289..3b12f619f9 100644 --- a/ui/actions/manage-groups/manage-groups.ts +++ b/ui/actions/manage-groups/manage-groups.ts @@ -201,7 +201,9 @@ export const deleteProviderGroup = async (formData: FormData) => { const providerGroupId = formData.get("id"); if (!providerGroupId) { - return { error: "Provider Group ID is required" }; + return { + errors: [{ detail: "Provider Group ID is required." }], + }; } const url = new URL(`${apiBaseUrl}/provider-groups/${providerGroupId}`); @@ -233,6 +235,7 @@ export const deleteProviderGroup = async (formData: FormData) => { } catch (error) { // eslint-disable-next-line no-console console.error("Error deleting provider group:", error); - return { error: getErrorMessage(error) }; + const message = await getErrorMessage(error); + return { errors: [{ detail: message }] }; } }; diff --git a/ui/app/api/lighthouse/analyst/route.ts b/ui/app/api/lighthouse/analyst/route.ts index 6bbf076f6e..51cf2bec4f 100644 --- a/ui/app/api/lighthouse/analyst/route.ts +++ b/ui/app/api/lighthouse/analyst/route.ts @@ -26,7 +26,7 @@ export async function POST(req: Request) { // Get AI configuration to access business context const aiConfig = await getLighthouseConfig(); - const businessContext = aiConfig?.data?.attributes?.business_context; + const businessContext = aiConfig?.attributes?.business_context; // Get current user data const currentData = await getCurrentDataSection(); @@ -86,14 +86,12 @@ export async function POST(req: Request) { } controller.close(); } catch (error) { - const errorName = - error instanceof Error ? error.constructor.name : "UnknownError"; const errorMessage = error instanceof Error ? error.message : String(error); controller.enqueue({ id: "error-" + Date.now(), role: "assistant", - content: `[LIGHTHOUSE_ANALYST_ERROR]: ${errorName}: ${errorMessage}`, + content: `[LIGHTHOUSE_ANALYST_ERROR]: ${errorMessage}`, }); controller.close(); } diff --git a/ui/components/filters/data-filters.ts b/ui/components/filters/data-filters.ts index 489e23bb54..6bcbd40161 100644 --- a/ui/components/filters/data-filters.ts +++ b/ui/components/filters/data-filters.ts @@ -1,11 +1,13 @@ -import { FilterType } from "@/types/filters"; +import { CONNECTION_STATUS_MAPPING } from "@/lib/helper-filters"; +import { FilterOption, FilterType } from "@/types/filters"; import { PROVIDER_TYPES } from "@/types/providers"; -export const filterProviders = [ +export const filterProviders: FilterOption[] = [ { key: "connected", labelCheckboxGroup: "Connection", - values: ["false", "true"], + values: ["true", "false"], + valueLabelMapping: CONNECTION_STATUS_MAPPING, }, { key: "provider__in", diff --git a/ui/components/findings/table/column-findings.tsx b/ui/components/findings/table/column-findings.tsx index 51e3d4625d..fb4040149d 100644 --- a/ui/components/findings/table/column-findings.tsx +++ b/ui/components/findings/table/column-findings.tsx @@ -228,7 +228,7 @@ export const ColumnFindings: ColumnDef[] = [ }, { accessorKey: "cloudProvider", - header: "Cloud provider", + header: "Cloud Provider", cell: ({ row }) => { const provider = getProviderData(row, "provider"); const alias = getProviderData(row, "alias"); diff --git a/ui/components/lighthouse/chatbot-config.tsx b/ui/components/lighthouse/chatbot-config.tsx index 4ad5d46d4e..159722a501 100644 --- a/ui/components/lighthouse/chatbot-config.tsx +++ b/ui/components/lighthouse/chatbot-config.tsx @@ -124,6 +124,15 @@ export const ChatbotConfig = ({ > GPT-4o Mini + + GPT-5 + + + GPT-5 Mini + )} /> diff --git a/ui/components/manage-groups/forms/delete-group-form.tsx b/ui/components/manage-groups/forms/delete-group-form.tsx index 8d268aa7fc..1d51dcd959 100644 --- a/ui/components/manage-groups/forms/delete-group-form.tsx +++ b/ui/components/manage-groups/forms/delete-group-form.tsx @@ -1,6 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/navigation"; import React, { Dispatch, SetStateAction } from "react"; import { useForm } from "react-hook-form"; import * as z from "zod"; @@ -26,6 +27,7 @@ export const DeleteGroupForm = ({ resolver: zodResolver(formSchema), }); const { toast } = useToast(); + const router = useRouter(); const isLoading = form.formState.isSubmitting; async function onSubmitClient(formData: FormData) { @@ -46,6 +48,7 @@ export const DeleteGroupForm = ({ title: "Success!", description: "The provider group was removed successfully.", }); + router.push("/manage-groups"); } setIsOpen(false); // Close the modal on success } diff --git a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx index 348c1be8df..894fc48183 100644 --- a/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx +++ b/ui/components/overview/new-findings-table/table/column-new-findings-to-date.tsx @@ -174,7 +174,7 @@ export const ColumnNewFindingsToDate: ColumnDef[] = [ }, { accessorKey: "cloudProvider", - header: "Cloud provider", + header: "Cloud Provider", cell: ({ row }) => { const provider = getProviderData(row, "provider"); const alias = getProviderData(row, "alias"); diff --git a/ui/components/scans/table/scans/column-get-scans.tsx b/ui/components/scans/table/scans/column-get-scans.tsx index f0b832e5d1..310e1c3815 100644 --- a/ui/components/scans/table/scans/column-get-scans.tsx +++ b/ui/components/scans/table/scans/column-get-scans.tsx @@ -47,7 +47,7 @@ export const ColumnGetScans: ColumnDef[] = [ }, { accessorKey: "cloudProvider", - header: () =>

Cloud provider

, + header: () =>

Cloud Provider

, cell: ({ row }) => { const providerInfo = row.original.providerInfo; @@ -123,7 +123,7 @@ export const ColumnGetScans: ColumnDef[] = [ return ( ); diff --git a/ui/components/ui/custom/custom-dropdown-filter.tsx b/ui/components/ui/custom/custom-dropdown-filter.tsx index 30bfea3a29..50a80f3349 100644 --- a/ui/components/ui/custom/custom-dropdown-filter.tsx +++ b/ui/components/ui/custom/custom-dropdown-filter.tsx @@ -22,7 +22,7 @@ import React, { import { ComplianceScanInfo } from "@/components/compliance/compliance-header/compliance-scan-info"; import { EntityInfoShort } from "@/components/ui/entities"; -import { isScanEntity } from "@/lib/helper-filters"; +import { isConnectionStatus, isScanEntity } from "@/lib/helper-filters"; import { CustomDropdownFilterProps, FilterEntity, @@ -189,6 +189,10 @@ export const CustomDropdownFilter = ({ )?.[value]; if (!entity) return value; + if (isConnectionStatus(entity)) { + return entity.label; + } + if (isScanEntity(entity as ScanEntity)) { return ( (entity as ScanEntity).attributes?.name || @@ -320,7 +324,9 @@ export const CustomDropdownFilter = ({ value={value} > {entity ? ( - isScanEntity(entity as ScanEntity) ? ( + isConnectionStatus(entity) ? ( + getDisplayLabel(value) + ) : isScanEntity(entity as ScanEntity) ? ( ) : ( ) ) : ( - value + getDisplayLabel(value) )} ); diff --git a/ui/components/ui/table/data-table-column-header.tsx b/ui/components/ui/table/data-table-column-header.tsx index a9a6c4beb5..6a61fdaec4 100644 --- a/ui/components/ui/table/data-table-column-header.tsx +++ b/ui/components/ui/table/data-table-column-header.tsx @@ -83,7 +83,7 @@ export const DataTableColumnHeader = ({ className="flex h-10 w-full items-center justify-between whitespace-nowrap bg-transparent px-0 text-left align-middle text-tiny font-semibold text-foreground-500 outline-none dark:text-slate-400" onPress={getToggleSortingHandler} > - {title} + {title} {renderSortIcon()} ); diff --git a/ui/lib/helper-filters.ts b/ui/lib/helper-filters.ts index 15ac934c29..6598baf33c 100644 --- a/ui/lib/helper-filters.ts +++ b/ui/lib/helper-filters.ts @@ -1,4 +1,6 @@ import { ProviderProps, ProvidersApiResponse, ScanProps } from "@/types"; +import { FilterEntity } from "@/types/filters"; +import { ProviderConnectionStatus } from "@/types/providers"; import { ScanEntity } from "@/types/scans"; /** @@ -125,3 +127,28 @@ export const createScanDetailsMapping = ( return scanMappings; }; + +// Helper to check if entity is a ProviderConnectionStatus (simple label/value object) +export const isConnectionStatus = ( + entity: FilterEntity, +): entity is ProviderConnectionStatus => { + return !!(entity && "label" in entity && "value" in entity); +}; + +/** + * Connection status mapping for provider filters. + * Maps boolean string values to user-friendly labels. + */ +export const CONNECTION_STATUS_MAPPING: Array<{ + [key: string]: FilterEntity; +}> = [ + { + true: { label: "Connected", value: "true" } as ProviderConnectionStatus, + }, + { + false: { + label: "Disconnected", + value: "false", + } as ProviderConnectionStatus, + }, +]; diff --git a/ui/lib/lighthouse/prompts.ts b/ui/lib/lighthouse/prompts.ts index 318ac1479d..5c5ddfd9d1 100644 --- a/ui/lib/lighthouse/prompts.ts +++ b/ui/lib/lighthouse/prompts.ts @@ -136,6 +136,11 @@ You operate in an agent loop, iterating through these steps: - Fetches available user roles in Prowler - Can get detailed information about the role +### resources_agent + +- Fetches information about resources found during Prowler scans +- Can get detailed information about a specific resource + ## Interacting with Agents - Don't invoke agents if you have the necessary information in your prompt. @@ -469,11 +474,39 @@ const rolesAgentPrompt = `You are Prowler's Roles Agent, specializing in role an - Mentioning all keys in the function call is mandatory. Don't skip any keys. - Don't add empty filters in the function call.`; +const resourcesAgentPrompt = `You are Prowler's Resource Agent, specializing in fetching resource information within Prowler. + +## Available Tools + +- getResourcesTool: List available resource with filtering options +- getResourceTool: Get detailed information about a specific resource by its UUID +- getLatestResourcesTool: List available resources from the latest scans across all providers without scan UUID + +## Response Guidelines + +- Keep the response concise +- Only share information relevant to the query +- Answer directly without unnecessary introductions or conclusions +- Ensure all responses are based on tools' output and information available in the prompt + +## Additional Guidelines + +- Focus only on resource-related information +- Format resource IDs, permissions, and descriptions consistently +- When user asks for resources without a specific scan UUID, use getLatestResourcesTool tool to fetch the resources +- To get the resource UUID, use getResourcesTool if scan UUID is present. If scan UUID is not present, use getLatestResourcesTool. + +## Tool Calling Guidelines + +- Mentioning all keys in the function call is mandatory. Don't skip any keys. +- Don't add empty filters in the function call.`; + export { complianceAgentPrompt, findingsAgentPrompt, overviewAgentPrompt, providerAgentPrompt, + resourcesAgentPrompt, rolesAgentPrompt, scansAgentPrompt, supervisorPrompt, diff --git a/ui/lib/lighthouse/tools/resources.ts b/ui/lib/lighthouse/tools/resources.ts index 62b0bcc2e6..408ad5fc6d 100644 --- a/ui/lib/lighthouse/tools/resources.ts +++ b/ui/lib/lighthouse/tools/resources.ts @@ -1,6 +1,7 @@ import { tool } from "@langchain/core/tools"; import { + getLighthouseLatestResources, getLighthouseResourceById, getLighthouseResources, } from "@/actions/lighthouse/resources"; @@ -8,22 +9,42 @@ import { getResourceSchema, getResourcesSchema } from "@/types/lighthouse"; export const getResourcesTool = tool( async ({ page, query, sort, filters, fields }) => { - return await getLighthouseResources(page, query, sort, filters, fields); + return await getLighthouseResources({ page, query, sort, filters, fields }); }, { name: "getResources", - description: "Fetches all resource information", + description: + "Retrieve a list of all resources found during scans with options for filtering by various criteria. Mandatory to pass in scan UUID.", schema: getResourcesSchema, }, ); export const getResourceTool = tool( async ({ id, fields, include }) => { - return await getLighthouseResourceById(id, fields, include); + return await getLighthouseResourceById({ id, fields, include }); }, { name: "getResource", - description: "Fetches information about a resource by its UUID.", + description: + "Fetch detailed information about a specific resource by their Prowler assigned UUID. A Resource is an object that is discovered by Prowler. It can be anything from a single host to a whole VPC.", schema: getResourceSchema, }, ); + +export const getLatestResourcesTool = tool( + async ({ page, query, sort, filters, fields }) => { + return await getLighthouseLatestResources({ + page, + query, + sort, + filters, + fields, + }); + }, + { + name: "getLatestResources", + description: + "Retrieve a list of the latest resources from the latest scans across all providers with options for filtering by various criteria.", + schema: getResourcesSchema, // Schema is same as getResourcesSchema + }, +); diff --git a/ui/lib/lighthouse/workflow.ts b/ui/lib/lighthouse/workflow.ts index faa7f02f4c..ff5996b95b 100644 --- a/ui/lib/lighthouse/workflow.ts +++ b/ui/lib/lighthouse/workflow.ts @@ -8,6 +8,7 @@ import { findingsAgentPrompt, overviewAgentPrompt, providerAgentPrompt, + resourcesAgentPrompt, rolesAgentPrompt, scansAgentPrompt, supervisorPrompt, @@ -35,6 +36,11 @@ import { getProvidersTool, getProviderTool, } from "@/lib/lighthouse/tools/providers"; +import { + getLatestResourcesTool, + getResourcesTool, + getResourceTool, +} from "@/lib/lighthouse/tools/resources"; import { getRolesTool, getRoleTool } from "@/lib/lighthouse/tools/roles"; import { getScansTool, getScanTool } from "@/lib/lighthouse/tools/scans"; import { @@ -127,6 +133,13 @@ export async function initLighthouseWorkflow() { prompt: rolesAgentPrompt, }); + const resourcesAgent = createReactAgent({ + llm: llm, + tools: [getResourceTool, getResourcesTool, getLatestResourcesTool], + name: "resources_agent", + prompt: resourcesAgentPrompt, + }); + const agents = [ userInfoAgent, providerAgent, @@ -135,6 +148,7 @@ export async function initLighthouseWorkflow() { complianceAgent, findingsAgent, rolesAgent, + resourcesAgent, ]; // Create supervisor workflow diff --git a/ui/types/filters.ts b/ui/types/filters.ts index 911a67651c..7f7986d6e6 100644 --- a/ui/types/filters.ts +++ b/ui/types/filters.ts @@ -1,7 +1,10 @@ -import { ProviderEntity } from "./providers"; +import { ProviderConnectionStatus, ProviderEntity } from "./providers"; import { ScanEntity } from "./scans"; -export type FilterEntity = ProviderEntity | ScanEntity; +export type FilterEntity = + | ProviderEntity + | ScanEntity + | ProviderConnectionStatus; export interface FilterOption { key: string; diff --git a/ui/types/lighthouse/resources.ts b/ui/types/lighthouse/resources.ts index 7b70dfe96b..354cbb89d0 100644 --- a/ui/types/lighthouse/resources.ts +++ b/ui/types/lighthouse/resources.ts @@ -11,6 +11,7 @@ const resourceFieldsEnum = z.enum([ "tags", "provider", "findings", + "failed_findings_count", "url", "type", ]); @@ -129,6 +130,10 @@ export const getResourcesSchema = z.object({ .optional() .describe("Filter by multiple tags separated by commas."), "filter[type]": z.string().optional().describe("Filter by type."), + "filter[type__icontains]": z + .string() + .optional() + .describe("Filter by substring."), "filter[type__in]": z .string() .optional() @@ -138,18 +143,15 @@ export const getResourcesSchema = z.object({ .string() .optional() .describe("Filter by substring."), - "filter[updated_at]": z - .string() - .optional() - .describe("The uid to filter by."), + "filter[updated_at]": z.string().optional().describe("Filter by date."), "filter[updated_at__gte]": z .string() .optional() - .describe("The uid to filter by."), + .describe("Filter by date greater than or equal to."), "filter[updated_at__lte]": z .string() .optional() - .describe("The uid to filter by."), + .describe("Filter by date less than or equal to."), }) .optional() .describe("The filters to apply to the resources."), diff --git a/ui/types/providers.ts b/ui/types/providers.ts index 0967d3304e..86f4e90533 100644 --- a/ui/types/providers.ts +++ b/ui/types/providers.ts @@ -60,6 +60,11 @@ export interface ProviderEntity { alias: string | null; } +export interface ProviderConnectionStatus { + label: string; + value: string; +} + export interface ProviderOverviewProps { data: { type: "provider-overviews"; diff --git a/util/generate_compliance_json_from_csv_for_cis10_github.py b/util/generate_compliance_json_from_csv_for_cis10_github.py index 3297c70640..7b925a53c2 100644 --- a/util/generate_compliance_json_from_csv_for_cis10_github.py +++ b/util/generate_compliance_json_from_csv_for_cis10_github.py @@ -10,13 +10,14 @@ import sys file_name = sys.argv[1] # read the CSV file rows and use the column fields to form the Prowler compliance JSON file 'cis_1.0_github.json' -output = {"Framework": "CIS-GitHub", "Version": "1.5", "Requirements": []} +output = {"Framework": "CIS-GitHub", "Version": "1.0", "Requirements": []} with open(file_name, newline="", encoding="utf-8") as f: - reader = csv.reader(f, delimiter=",") + reader = csv.reader(f, delimiter=";") for row in reader: attribute = { - "Section": row[3], - "Profile": row[4], + "Section": row[0], + "Subsection": row[1], + "Profile": row[3], "AssessmentStatus": row[5], "Description": row[6], "RationaleStatement": row[7], @@ -24,17 +25,19 @@ with open(file_name, newline="", encoding="utf-8") as f: "RemediationProcedure": row[9], "AuditProcedure": row[10], "AdditionalInformation": row[11], - "References": row[12], + "References": row[25], + "DefaultValue": row[26], } output["Requirements"].append( { - "Id": row[0], - "Description": row[1], - "Checks": list(map(str.strip, row[2].split(","))), + "Id": row[2], + "Description": row[6], + "Checks": [], "Attributes": [attribute], } ) + # Write the output Prowler compliance JSON file 'cis_1.0_github.json' locally with open("cis_1.0_github.json", "w", encoding="utf-8") as outfile: json.dump(output, outfile, indent=4, ensure_ascii=False)