diff --git a/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/metadata.json b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/metadata.json new file mode 100644 index 00000000000..7c3a4bc5640 --- /dev/null +++ b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "721c26ff-776f-4d7a-b151-45447712343b", + "queryName": "Beta - App Service Application Insights Not Configured", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that Azure App Services and Function Apps are linked to Application Insights for performance monitoring and error tracking.", + "descriptionUrl": "https://learn.microsoft.com/en-us/azure/azure-functions/configure-monitoring?tabs=v2#enable-application-insights-integration", + "platform": "Terraform", + "descriptionID": "721c26ff", + "cloudProvider": "azure", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} diff --git a/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/query.rego b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/query.rego new file mode 100644 index 00000000000..7fb4c00492d --- /dev/null +++ b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/query.rego @@ -0,0 +1,58 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +targets := { + "azurerm_linux_web_app", + "azurerm_windows_web_app", + "azurerm_linux_function_app", + "azurerm_windows_function_app" +} + +application_insights_keys := [ + "APPLICATIONINSIGHTS_CONNECTION_STRING", + "APPINSIGHTS_INSTRUMENTATIONKEY", +] + +# RULE 1: The 'app_settings' block is missing entirely. +CxPolicy[result] { + doc := input.document[i] + resource_type := targets[_] + app := doc.resource[resource_type][name] + + not app.app_settings + + result := { + "documentId": doc.id, + "resourceType": resource_type, + "resourceName": tf_lib.get_resource_name(app, name), + "searchKey": sprintf("%s[%s]", [resource_type, name]), + "searchLine": common_lib.build_search_line(["resource", resource_type, name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'%s.%s' should have 'app_settings' with an Application Insights key configured", [resource_type, name]), + "keyActualValue": sprintf("'%s.%s' is missing 'app_settings'", [resource_type, name]), + } +} + +# RULE 2: 'app_settings' exists but neither Application Insights key is configured. +# Iterates application_insights_keys and counts how many are present; fires when none are found. +CxPolicy[result] { + doc := input.document[i] + resource_type := targets[_] + app := doc.resource[resource_type][name] + + app.app_settings + count({k | k := application_insights_keys[_]; app.app_settings[k]}) == 0 + + result := { + "documentId": doc.id, + "resourceType": resource_type, + "resourceName": tf_lib.get_resource_name(app, name), + "searchKey": sprintf("%s[%s].app_settings", [resource_type, name]), + "searchLine": common_lib.build_search_line(["resource", resource_type, name, "app_settings"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'app_settings' should contain '%s' or '%s'", [application_insights_keys[0], application_insights_keys[1]]), + "keyActualValue": "'app_settings' does not contain any Application Insights configuration key", + } +} diff --git a/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/negative1.tf b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/negative1.tf new file mode 100644 index 00000000000..e528d3c0ebc --- /dev/null +++ b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/negative1.tf @@ -0,0 +1,11 @@ +# Case: Connection String (Recommended) +resource "azurerm_linux_web_app" "pass_connection_string" { + name = "pass-app-1" + resource_group_name = "rg" + location = "West Europe" + service_plan_id = "plan-id" + + app_settings = { + "APPLICATIONINSIGHTS_CONNECTION_STRING" = "InstrumentationKey=0000;IngestionEndpoint=https://..." + } +} diff --git a/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/negative2.tf b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/negative2.tf new file mode 100644 index 00000000000..c719508e879 --- /dev/null +++ b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/negative2.tf @@ -0,0 +1,11 @@ +# Case: Instrumentation Key (Legacy) +resource "azurerm_windows_web_app" "pass_instrumentation_key" { + name = "pass-app-2" + resource_group_name = "rg" + location = "West Europe" + service_plan_id = "plan-id" + + app_settings = { + "APPINSIGHTS_INSTRUMENTATIONKEY" = "0000-0000-0000-0000" + } +} diff --git a/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/positive1.tf b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/positive1.tf new file mode 100644 index 00000000000..f44da5b65d5 --- /dev/null +++ b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/positive1.tf @@ -0,0 +1,6 @@ +resource "azurerm_linux_web_app" "fail_no_settings" { + name = "fail-app-no-settings" + resource_group_name = "rg" + location = "West Europe" + service_plan_id = "plan-id" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/positive2.tf b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/positive2.tf new file mode 100644 index 00000000000..7ed5d045e78 --- /dev/null +++ b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/positive2.tf @@ -0,0 +1,10 @@ +resource "azurerm_windows_web_app" "fail_incomplete_settings" { + name = "fail-app-incomplete" + resource_group_name = "rg" + location = "West Europe" + service_plan_id = "plan-id" + + app_settings = { + "WEBSITE_NODE_DEFAULT_VERSION" = "14.15.0" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/positive_expected_result.json new file mode 100644 index 00000000000..6ef4443546d --- /dev/null +++ b/assets/queries/terraform/azure/azure_app_service_application_insights_not_configured/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - App Service Application Insights Not Configured", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - App Service Application Insights Not Configured", + "severity": "MEDIUM", + "line": 7, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/metadata.json b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/metadata.json new file mode 100644 index 00000000000..e4fbaa2c65f --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "0117fb32-4265-444d-8030-a0a034958489", + "queryName": "Beta - Backup Vault Cross Region Restore Disabled", + "severity": "MEDIUM", + "category": "Backup", + "descriptionText": "Ensures that 'Cross Region Restore' is enabled for Azure Backup Vaults. This allows backup data to be restored in a secondary region, which is critical for disaster recovery scenarios.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/data_protection_backup_vault#cross_region_restore_enabled", + "platform": "Terraform", + "descriptionID": "0117fb32", + "cloudProvider": "azure", + "cwe": "668", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/query.rego b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/query.rego new file mode 100644 index 00000000000..9439840bb7e --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/query.rego @@ -0,0 +1,45 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: Vault redundancy is GeoRedundant but 'cross_region_restore_enabled' is not defined. +# The attribute defaults to false when absent; it is only applicable to GeoRedundant vaults. +CxPolicy[result] { + doc := input.document[i] + vault := doc.resource.azurerm_data_protection_backup_vault[name] + + vault.redundancy == "GeoRedundant" + object.get(vault, "cross_region_restore_enabled", null) == null + + result := { + "documentId": doc.id, + "resourceType": "azurerm_data_protection_backup_vault", + "resourceName": tf_lib.get_resource_name(vault, name), + "searchKey": sprintf("azurerm_data_protection_backup_vault[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_data_protection_backup_vault", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_data_protection_backup_vault.%s' should have 'cross_region_restore_enabled' set to true when 'redundancy' is 'GeoRedundant'", [name]), + "keyActualValue": sprintf("'azurerm_data_protection_backup_vault.%s' is missing 'cross_region_restore_enabled' (defaults to false)", [name]), + } +} + +# RULE 2: Vault redundancy is GeoRedundant but 'cross_region_restore_enabled' is explicitly false. +CxPolicy[result] { + doc := input.document[i] + vault := doc.resource.azurerm_data_protection_backup_vault[name] + + vault.redundancy == "GeoRedundant" + vault.cross_region_restore_enabled == false + + result := { + "documentId": doc.id, + "resourceType": "azurerm_data_protection_backup_vault", + "resourceName": tf_lib.get_resource_name(vault, name), + "searchKey": sprintf("azurerm_data_protection_backup_vault[%s].cross_region_restore_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_data_protection_backup_vault", name, "cross_region_restore_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'cross_region_restore_enabled' should be set to true when 'redundancy' is 'GeoRedundant'", + "keyActualValue": "'cross_region_restore_enabled' is explicitly set to false", + } +} diff --git a/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/negative1.tf new file mode 100644 index 00000000000..1897017fb9d --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/negative1.tf @@ -0,0 +1,9 @@ +resource "azurerm_data_protection_backup_vault" "pass" { + name = "vault-ok" + resource_group_name = "rg-test" + location = "West Europe" + datastore_type = "VaultStore" + redundancy = "GeoRedundant" + + cross_region_restore_enabled = true +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/negative2.tf b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/negative2.tf new file mode 100644 index 00000000000..a02fd159688 --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/negative2.tf @@ -0,0 +1,8 @@ +# PASS: LocallyRedundant vault — cross_region_restore_enabled is not applicable +resource "azurerm_data_protection_backup_vault" "pass_local_redundant" { + name = "vault-local-redundant" + resource_group_name = "rg-test" + location = "West Europe" + datastore_type = "VaultStore" + redundancy = "LocallyRedundant" +} diff --git a/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/positive1.tf new file mode 100644 index 00000000000..9be26f59a56 --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/positive1.tf @@ -0,0 +1,8 @@ +resource "azurerm_data_protection_backup_vault" "fail_missing" { + name = "vault-missing-crr" + resource_group_name = "rg-test" + location = "West Europe" + datastore_type = "VaultStore" + redundancy = "GeoRedundant" + # FAIL: Missing cross_region_restore_enabled +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/positive2.tf b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/positive2.tf new file mode 100644 index 00000000000..4291a252752 --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/positive2.tf @@ -0,0 +1,9 @@ +resource "azurerm_data_protection_backup_vault" "fail_explicit" { + name = "vault-disabled-crr" + resource_group_name = "rg-test" + location = "West Europe" + datastore_type = "VaultStore" + redundancy = "GeoRedundant" + + cross_region_restore_enabled = false # FAIL +} diff --git a/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..2479f5831c8 --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_cross_region_restore_disabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Backup Vault Cross Region Restore Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Backup Vault Cross Region Restore Disabled", + "severity": "MEDIUM", + "line": 8, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/metadata.json b/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/metadata.json new file mode 100644 index 00000000000..e0750921ce6 --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "f7bf03d5-ae0e-4b36-aab3-f8d2346a8843", + "queryName": "Beta - Backup Vault Managed Identity Not Configured", + "severity": "MEDIUM", + "category": "Encryption", + "descriptionText": "Ensures that Azure Backup Vaults have a managed identity configured. The 'identity' block is required to enable Customer-Managed Keys (CMK) for vault encryption. Without it, the vault relies solely on Microsoft-managed platform encryption, removing customer control over key lifecycle and rotation.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/data_protection_backup_vault#identity", + "platform": "Terraform", + "descriptionID": "f7bf03d5", + "cloudProvider": "azure", + "cwe": "312", + "riskScore": "3.0", + "experimental": "true" +} diff --git a/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/query.rego b/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/query.rego new file mode 100644 index 00000000000..3bc8deb84a4 --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/query.rego @@ -0,0 +1,25 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: The 'identity' block is absent, preventing Customer-Managed Key (CMK) encryption. +# Without a managed identity the vault is limited to Microsoft-managed platform encryption, +# removing customer control over key lifecycle and rotation. +CxPolicy[result] { + doc := input.document[i] + vault := doc.resource.azurerm_data_protection_backup_vault[name] + + not vault.identity + + result := { + "documentId": doc.id, + "resourceType": "azurerm_data_protection_backup_vault", + "resourceName": tf_lib.get_resource_name(vault, name), + "searchKey": sprintf("azurerm_data_protection_backup_vault[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_data_protection_backup_vault", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_data_protection_backup_vault.%s' should have an 'identity' block to enable Customer-Managed Key (CMK) encryption", [name]), + "keyActualValue": sprintf("'azurerm_data_protection_backup_vault.%s' is missing the 'identity' block; vault uses Microsoft-managed platform encryption only", [name]), + } +} diff --git a/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/test/negative1.tf new file mode 100644 index 00000000000..339fa323b7f --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/test/negative1.tf @@ -0,0 +1,11 @@ +resource "azurerm_data_protection_backup_vault" "pass" { + name = "vault-with-identity" + resource_group_name = "rg" + location = "West Europe" + datastore_type = "VaultStore" + redundancy = "LocallyRedundant" + + identity { + type = "SystemAssigned" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/test/positive1.tf new file mode 100644 index 00000000000..d57a662820e --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/test/positive1.tf @@ -0,0 +1,8 @@ +resource "azurerm_data_protection_backup_vault" "fail" { + name = "vault-no-identity" + resource_group_name = "rg" + location = "West Europe" + datastore_type = "VaultStore" + redundancy = "LocallyRedundant" + # FAIL: Missing identity block +} diff --git a/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..eb1392bfedc --- /dev/null +++ b/assets/queries/terraform/azure/azure_backup_vault_infrastructure_encryption_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Backup Vault Managed Identity Not Configured", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_bastion_host_missing/metadata.json b/assets/queries/terraform/azure/azure_bastion_host_missing/metadata.json new file mode 100644 index 00000000000..e90cb06a967 --- /dev/null +++ b/assets/queries/terraform/azure/azure_bastion_host_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "a3e941c5-7708-40d1-943b-7093446ef5e6", + "queryName": "Beta - Azure Bastion Host Missing", + "severity": "MEDIUM", + "category": "Networking and Firewall", + "descriptionText": "Ensures Azure Virtual Networks are protected by a Bastion Host with a valid ip_configuration block. Azure Bastion provides secure RDP/SSH access to VMs without public internet exposure. A complete setup requires an azurerm_bastion_host with ip_configuration referencing AzureBastionSubnet and a Standard-tier public IP. Note: subnet-level VNet association is not verified.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/bastion_host", + "platform": "Terraform", + "descriptionID": "a3e941c5", + "cloudProvider": "azure", + "cwe": "284", + "riskScore": "3.0", + "experimental": "true" +} diff --git a/assets/queries/terraform/azure/azure_bastion_host_missing/query.rego b/assets/queries/terraform/azure/azure_bastion_host_missing/query.rego new file mode 100644 index 00000000000..b46bf4f8d53 --- /dev/null +++ b/assets/queries/terraform/azure/azure_bastion_host_missing/query.rego @@ -0,0 +1,47 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: A VNet exists but no azurerm_bastion_host is declared in the same document. +# Note: this is a document-level check; Bastion association to a specific VNet subnet +# cannot be verified without cross-resource reference resolution. +CxPolicy[result] { + doc := input.document[i] + + vnet := doc.resource.azurerm_virtual_network[name] + + count([b | b := doc.resource.azurerm_bastion_host[_]]) == 0 + + result := { + "documentId": doc.id, + "resourceType": "azurerm_virtual_network", + "resourceName": tf_lib.get_resource_name(vnet, name), + "searchKey": sprintf("azurerm_virtual_network[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_virtual_network", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("An 'azurerm_bastion_host' resource should be defined to protect Virtual Network '%s'", [name]), + "keyActualValue": "No 'azurerm_bastion_host' resource was found in the configuration", + } +} + +# RULE 2: An azurerm_bastion_host is declared but missing the required ip_configuration block. +# ip_configuration is mandatory in the Terraform schema and must reference a dedicated +# subnet named 'AzureBastionSubnet' and a Standard-tier public IP address. +CxPolicy[result] { + doc := input.document[i] + + bastion := doc.resource.azurerm_bastion_host[name] + not bastion.ip_configuration + + result := { + "documentId": doc.id, + "resourceType": "azurerm_bastion_host", + "resourceName": tf_lib.get_resource_name(bastion, name), + "searchKey": sprintf("azurerm_bastion_host[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_bastion_host", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_bastion_host.%s' should have an 'ip_configuration' block referencing an 'AzureBastionSubnet' subnet", [name]), + "keyActualValue": sprintf("'azurerm_bastion_host.%s' is missing the required 'ip_configuration' block", [name]), + } +} diff --git a/assets/queries/terraform/azure/azure_bastion_host_missing/test/negative1.tf b/assets/queries/terraform/azure/azure_bastion_host_missing/test/negative1.tf new file mode 100644 index 00000000000..88141688bb2 --- /dev/null +++ b/assets/queries/terraform/azure/azure_bastion_host_missing/test/negative1.tf @@ -0,0 +1,18 @@ +resource "azurerm_virtual_network" "pass_vnet" { + name = "secure-network" + resource_group_name = "rg-test" + location = "West Europe" + address_space = ["10.0.0.0/16"] +} + +resource "azurerm_bastion_host" "pass_bastion" { + name = "bastion-host" + location = "West Europe" + resource_group_name = "rg-test" + + ip_configuration { + name = "config" + subnet_id = "dummy-id" + public_ip_address_id = "dummy-pip-id" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_bastion_host_missing/test/positive1.tf b/assets/queries/terraform/azure/azure_bastion_host_missing/test/positive1.tf new file mode 100644 index 00000000000..2c9968f79d6 --- /dev/null +++ b/assets/queries/terraform/azure/azure_bastion_host_missing/test/positive1.tf @@ -0,0 +1,6 @@ +resource "azurerm_virtual_network" "fail_vnet" { + name = "vulnerable-network" + resource_group_name = "rg-test" + location = "West Europe" + address_space = ["10.0.0.0/16"] +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_bastion_host_missing/test/positive2.tf b/assets/queries/terraform/azure/azure_bastion_host_missing/test/positive2.tf new file mode 100644 index 00000000000..ff716f48445 --- /dev/null +++ b/assets/queries/terraform/azure/azure_bastion_host_missing/test/positive2.tf @@ -0,0 +1,6 @@ +resource "azurerm_bastion_host" "fail_no_ip_config" { + name = "bastion-incomplete" + location = "West Europe" + resource_group_name = "rg-test" + # FAIL: Missing required ip_configuration block +} diff --git a/assets/queries/terraform/azure/azure_bastion_host_missing/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_bastion_host_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..98256c5a43c --- /dev/null +++ b/assets/queries/terraform/azure/azure_bastion_host_missing/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Azure Bastion Host Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Azure Bastion Host Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/metadata.json b/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/metadata.json new file mode 100644 index 00000000000..fe022f2df7f --- /dev/null +++ b/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "660863a5-bb45-4907-8afe-d7478177a446", + "queryName": "Beta - Elastic SAN Volume Group Network Rules Not Configured", + "severity": "HIGH", + "category": "Networking and Firewall", + "descriptionText": "Ensures that Azure Elastic SAN Volume Groups have network rules configured to restrict access to approved virtual networks only. The network_rule block defines a whitelist of subnets permitted to access the volume group; without it, no subnet-level access restriction is applied. Note: public_network_access_enabled is a separate attribute on azurerm_elastic_san (the SAN resource itself) and controls network access at the SAN level.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/elastic_san_volume_group#network_rule", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "284", + "descriptionID": "660863a5", + "riskScore": "6.0", + "experimental": "true" +} diff --git a/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/query.rego b/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/query.rego new file mode 100644 index 00000000000..6f8f91cae8d --- /dev/null +++ b/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/query.rego @@ -0,0 +1,27 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: The 'network_rule' block is missing from an azurerm_elastic_san_volume_group. +# network_rule defines a subnet whitelist; its absence means no subnet-level access +# restriction is applied to the volume group. +# Note: public_network_access_enabled is a separate attribute on azurerm_elastic_san +# (the SAN resource itself) and is not present on azurerm_elastic_san_volume_group. +CxPolicy[result] { + doc := input.document[i] + vg := doc.resource.azurerm_elastic_san_volume_group[name] + + not vg.network_rule + + result := { + "documentId": doc.id, + "resourceType": "azurerm_elastic_san_volume_group", + "resourceName": tf_lib.get_resource_name(vg, name), + "searchKey": sprintf("azurerm_elastic_san_volume_group[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_elastic_san_volume_group", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_elastic_san_volume_group.%s' should have a 'network_rule' block to restrict access to approved subnets only", [name]), + "keyActualValue": sprintf("'azurerm_elastic_san_volume_group.%s' has no 'network_rule' block; no subnet-level access restriction is applied", [name]), + } +} diff --git a/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/test/negative1.tf b/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/test/negative1.tf new file mode 100644 index 00000000000..b2d4f4b0b26 --- /dev/null +++ b/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/test/negative1.tf @@ -0,0 +1,8 @@ +resource "azurerm_elastic_san_volume_group" "pass" { + name = "secure-vg" + elastic_san_id = "san-id" + + network_rule { + subnet_id = "/subscriptions/0000/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/s1" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/test/positive1.tf b/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/test/positive1.tf new file mode 100644 index 00000000000..d579f9af843 --- /dev/null +++ b/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/test/positive1.tf @@ -0,0 +1,5 @@ +resource "azurerm_elastic_san_volume_group" "fail" { + name = "insecure-vg" + elastic_san_id = "san-id" + # FAIL: Missing network_rule block +} diff --git a/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/test/positive_expected_result.json new file mode 100644 index 00000000000..669a653127c --- /dev/null +++ b/assets/queries/terraform/azure/azure_elastic_san_public_access_enabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Elastic SAN Volume Group Network Rules Not Configured", + "severity": "HIGH", + "line": 1, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/metadata.json b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/metadata.json new file mode 100644 index 00000000000..5c9ef2f2a75 --- /dev/null +++ b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "13be738b-78fb-4d0f-a5ed-13e0f36fd385", + "queryName": "Beta - Azure IoT Hub Defender Disabled", + "severity": "MEDIUM", + "category": "Networking and Firewall", + "descriptionText": "Ensures that Microsoft Defender for IoT is enabled for Azure IoT Hubs. Defender for IoT provides threat detection and security posture management for IoT environments.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/iot_security_solution", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "693", + "descriptionID": "13be738b", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/query.rego b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/query.rego new file mode 100644 index 00000000000..ec49f0a99bf --- /dev/null +++ b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/query.rego @@ -0,0 +1,62 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: IoT Hub has no associated azurerm_iot_security_solution. +# Checked at document level via iothub_ids reference matching. +CxPolicy[result] { + doc := input.document[i] + iot_hub := doc.resource.azurerm_iothub[hub_name] + + hub_ref := sprintf("azurerm_iothub.%s.id", [hub_name]) + + count([sol | + sol := doc.resource.azurerm_iot_security_solution[_] + check_hub_id(sol.iothub_ids[_], hub_ref) + ]) == 0 + + result := { + "documentId": doc.id, + "resourceType": "azurerm_iothub", + "resourceName": tf_lib.get_resource_name(iot_hub, hub_name), + "searchKey": sprintf("azurerm_iothub[%s]", [hub_name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_iothub", hub_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_iothub.%s' should be included in 'iothub_ids' of an 'azurerm_iot_security_solution'", [hub_name]), + "keyActualValue": sprintf("'azurerm_iothub.%s' is not associated with any 'azurerm_iot_security_solution'", [hub_name]), + } +} + +# RULE 2: IoT Hub has an associated security solution but it is explicitly disabled. +# azurerm_iot_security_solution.enabled defaults to true; setting it to false disables +# Defender for IoT while leaving the resource declared in the configuration. +CxPolicy[result] { + doc := input.document[i] + iot_hub := doc.resource.azurerm_iothub[hub_name] + + hub_ref := sprintf("azurerm_iothub.%s.id", [hub_name]) + + sol := doc.resource.azurerm_iot_security_solution[sol_name] + check_hub_id(sol.iothub_ids[_], hub_ref) + sol.enabled == false + + result := { + "documentId": doc.id, + "resourceType": "azurerm_iot_security_solution", + "resourceName": tf_lib.get_resource_name(sol, sol_name), + "searchKey": sprintf("azurerm_iot_security_solution[%s].enabled", [sol_name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_iot_security_solution", sol_name, "enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'azurerm_iot_security_solution.%s' should have 'enabled' set to true", [sol_name]), + "keyActualValue": sprintf("'azurerm_iot_security_solution.%s' has 'enabled' explicitly set to false", [sol_name]), + } +} + +check_hub_id(current, target) { + current == target +} + +check_hub_id(current, target) { + current == sprintf("${%s}", [target]) +} diff --git a/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/negative1.tf new file mode 100644 index 00000000000..f28ce4531df --- /dev/null +++ b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/negative1.tf @@ -0,0 +1,18 @@ +resource "azurerm_iothub" "pass" { + name = "example-iothub-pass" + resource_group_name = "rg-test" + location = "West Europe" + + sku { + name = "S1" + capacity = "1" + } +} + +resource "azurerm_iot_security_solution" "pass" { + name = "example-security-solution" + resource_group_name = "rg-test" + location = "West Europe" + display_name = "Iot Security Solution" + iothub_ids = [azurerm_iothub.pass.id] +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/positive1.tf new file mode 100644 index 00000000000..d91f9e76260 --- /dev/null +++ b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/positive1.tf @@ -0,0 +1,10 @@ +resource "azurerm_iothub" "fail" { + name = "example-iothub-fail" + resource_group_name = "rg-test" + location = "West Europe" + + sku { + name = "S1" + capacity = "1" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/positive2.tf b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/positive2.tf new file mode 100644 index 00000000000..3f8660c043e --- /dev/null +++ b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/positive2.tf @@ -0,0 +1,19 @@ +resource "azurerm_iothub" "fail_disabled" { + name = "example-iothub-fail-disabled" + resource_group_name = "rg-test" + location = "West Europe" + + sku { + name = "S1" + capacity = "1" + } +} + +resource "azurerm_iot_security_solution" "fail_disabled" { + name = "example-security-solution-disabled" + resource_group_name = "rg-test" + location = "West Europe" + display_name = "Iot Security Solution" + iothub_ids = [azurerm_iothub.fail_disabled.id] + enabled = false +} diff --git a/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..1ff1d0ff6b6 --- /dev/null +++ b/assets/queries/terraform/azure/azure_iot_hub_defender_disabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Azure IoT Hub Defender Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Azure IoT Hub Defender Disabled", + "severity": "MEDIUM", + "line": 18, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/metadata.json b/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/metadata.json new file mode 100644 index 00000000000..2303d141fc8 --- /dev/null +++ b/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "ca1ffce5-e2f9-4d8a-978e-719d1ed7c7fa", + "queryName": "Beta - Key Vault Key Rotation Disabled", + "severity": "MEDIUM", + "category": "Encryption", + "descriptionText": "Ensures that cryptographic keys in Azure Key Vault have an automatic rotation policy configured. Regular key rotation reduces the risk of compromised keys being used for extended periods.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_key#rotation_policy", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "320", + "descriptionID": "ca1ffce5", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/query.rego b/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/query.rego new file mode 100644 index 00000000000..bbcb4f2f952 --- /dev/null +++ b/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/query.rego @@ -0,0 +1,23 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: The Key Vault key does not have a rotation policy configured. +CxPolicy[result] { + doc := input.document[i] + key := doc.resource.azurerm_key_vault_key[name] + + not key.rotation_policy + + result := { + "documentId": doc.id, + "resourceType": "azurerm_key_vault_key", + "resourceName": tf_lib.get_resource_name(key, name), + "searchKey": sprintf("azurerm_key_vault_key[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_key_vault_key", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_key_vault_key.%s' should have a 'rotation_policy' block defined", [name]), + "keyActualValue": sprintf("'azurerm_key_vault_key.%s' does not have a 'rotation_policy' block defined", [name]), + } +} diff --git a/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/test/negative1.tf new file mode 100644 index 00000000000..af70d50c07c --- /dev/null +++ b/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/test/negative1.tf @@ -0,0 +1,16 @@ +resource "azurerm_key_vault_key" "pass" { + name = "pass-key" + key_vault_id = "vault-id" + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt"] + + rotation_policy { + expire_after = "P90D" + notify_before_expiry = "P29D" + + automatic { + time_before_expiry = "P30D" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/test/positive1.tf new file mode 100644 index 00000000000..4944d9b302e --- /dev/null +++ b/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_key_vault_key" "fail" { + name = "fail-key" + key_vault_id = "vault-id" + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt"] +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..fe87fbfef1e --- /dev/null +++ b/assets/queries/terraform/azure/azure_key_vault_key_rotation_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Key Vault Key Rotation Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/metadata.json b/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/metadata.json new file mode 100644 index 00000000000..ddc5845c943 --- /dev/null +++ b/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "7fc653e2-af91-4379-9219-5095caba0ff6", + "queryName": "Beta - Azure Managed Lustre Not Encrypted with CMK", + "severity": "MEDIUM", + "category": "Encryption", + "descriptionText": "Ensures that Azure Managed Lustre file systems are encrypted using a Customer-Managed Key (CMK) via the 'encryption_key' block. By default, data is encrypted with platform-managed keys; CMK provides full control over key lifecycle and access policies. The 'encryption_key' block is documented in the azurerm provider source and requires 'key_url' (Key Vault key URL) and 'source_vault_id' (Key Vault resource ID).", + "descriptionUrl": "https://learn.microsoft.com/en-us/azure/azure-managed-lustre/customer-managed-encryption-keys", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "326", + "descriptionID": "7fc653e2", + "riskScore": "3.0", + "experimental": "true" +} diff --git a/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/query.rego b/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/query.rego new file mode 100644 index 00000000000..1a59490463c --- /dev/null +++ b/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/query.rego @@ -0,0 +1,28 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: The 'encryption_key' block is not defined. +# 'encryption_key' is an optional block on azurerm_managed_lustre_file_system documented +# in the azurerm provider source. It requires: +# key_url - Key Vault key URL +# source_vault_id - Key Vault resource ID +# Reference: https://github.com/hashicorp/terraform-provider-azurerm/blob/main/website/docs/r/managed_lustre_file_system.html.markdown +CxPolicy[result] { + doc := input.document[i] + lustre := doc.resource.azurerm_managed_lustre_file_system[name] + + not lustre.encryption_key + + result := { + "documentId": doc.id, + "resourceType": "azurerm_managed_lustre_file_system", + "resourceName": tf_lib.get_resource_name(lustre, name), + "searchKey": sprintf("azurerm_managed_lustre_file_system[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_managed_lustre_file_system", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_managed_lustre_file_system.%s' should have an 'encryption_key' block with 'key_url' and 'source_vault_id'", [name]), + "keyActualValue": sprintf("'azurerm_managed_lustre_file_system.%s' is missing the 'encryption_key' block; data is encrypted with platform-managed keys only", [name]), + } +} diff --git a/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/test/negative1.tf new file mode 100644 index 00000000000..e259cb89f05 --- /dev/null +++ b/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/test/negative1.tf @@ -0,0 +1,13 @@ +resource "azurerm_managed_lustre_file_system" "pass" { + name = "pass-lustre" + resource_group_name = "rg" + location = "West Europe" + sku_name = "AMLFS-Durable-Premium-250" + subnet_id = "subnet-id" + storage_capacity_in_tb = 48 + + encryption_key { + key_url = "https://kv.vault.azure.net/keys/key/v1" + source_vault_id = "/subscriptions/000/resourceGroups/rg/providers/Microsoft.KeyVault/vaults/kv" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/test/positive1.tf new file mode 100644 index 00000000000..33d647029e0 --- /dev/null +++ b/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/test/positive1.tf @@ -0,0 +1,8 @@ +resource "azurerm_managed_lustre_file_system" "fail" { + name = "fail-lustre" + resource_group_name = "rg" + location = "West Europe" + sku_name = "AMLFS-Durable-Premium-250" + subnet_id = "subnet-id" + storage_capacity_in_tb = 48 +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..b3d1a6397b5 --- /dev/null +++ b/assets/queries/terraform/azure/azure_managed_lustre_cmk_encryption_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Azure Managed Lustre Not Encrypted with CMK", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/metadata.json b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/metadata.json new file mode 100644 index 00000000000..bb5d0594e09 --- /dev/null +++ b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "dda11a20-c7c2-43f8-bd52-a4c029ad15e4", + "queryName": "Beta - Ensure Private Endpoints Are Used Where Possible", + "severity": "MEDIUM", + "category": "Networking and Firewall", + "descriptionText": "Ensures that key Azure PaaS services are accessed via Private Endpoints, preventing exposure over the public internet. Checked resources: azurerm_cosmosdb_account, azurerm_storage_account, azurerm_mssql_server, azurerm_key_vault, azurerm_container_registry, azurerm_servicebus_namespace, azurerm_mariadb_server, azurerm_postgresql_server, azurerm_mysql_server, azurerm_redis_cache, azurerm_eventhub_namespace, azurerm_automation_account, azurerm_data_factory, azurerm_synapse_workspace, azurerm_search_service. Note: document-level check only; private endpoints defined in separate Terraform state files are not evaluated. The target resource list is defined inline; consider moving it to a platform library for easier maintenance.", + "descriptionUrl": "https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "200", + "descriptionID": "dda11a20", + "riskScore": "3.0", + "experimental": "true" +} diff --git a/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/query.rego b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/query.rego new file mode 100644 index 00000000000..9cae495829c --- /dev/null +++ b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/query.rego @@ -0,0 +1,60 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +targets := { + "azurerm_cosmosdb_account", + "azurerm_storage_account", + "azurerm_mssql_server", + "azurerm_key_vault", + "azurerm_container_registry", + "azurerm_servicebus_namespace", + "azurerm_mariadb_server", + "azurerm_postgresql_server", + "azurerm_mysql_server", + "azurerm_redis_cache", + "azurerm_eventhub_namespace", + "azurerm_automation_account", + "azurerm_data_factory", + "azurerm_synapse_workspace", + "azurerm_search_service" +} + +# MASTER RULE: Checks whether resources in the targets list have a linked Private Endpoint. +CxPolicy[result] { + doc := input.document[i] + + resource_type := targets[t] + resource_instances := doc.resource[resource_type] + resource_instance := resource_instances[name] + + target_id := sprintf("%s.%s.id", [resource_type, name]) + + private_endpoints := [pe | + pe := doc.resource.azurerm_private_endpoint[_] + conn := pe.private_service_connection + check_resource_id(conn.private_connection_resource_id, target_id) + ] + + count(private_endpoints) == 0 + + result := { + "documentId": doc.id, + "resourceType": resource_type, + "resourceName": tf_lib.get_resource_name(resource_instance, name), + "searchKey": sprintf("%s[%s]", [resource_type, name]), + "searchLine": common_lib.build_search_line(["resource", resource_type, name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'%s.%s' should be linked to an 'azurerm_private_endpoint'", [resource_type, name]), + "keyActualValue": sprintf("'%s.%s' is not linked to any 'azurerm_private_endpoint' in this file", [resource_type, name]), + } +} + +check_resource_id(current, target) { + current == target +} + +check_resource_id(current, target) { + current == sprintf("${%s}", [target]) +} diff --git a/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/negative1.tf b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/negative1.tf new file mode 100644 index 00000000000..cfd1a9fb026 --- /dev/null +++ b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/negative1.tf @@ -0,0 +1,22 @@ +resource "azurerm_mssql_server" "pass_sql" { + name = "sqlpassserver" + resource_group_name = "rg" + location = "West Europe" + version = "12.0" + administrator_login = "admin" + administrator_login_password = "Password123!" +} + +resource "azurerm_private_endpoint" "pass_pe" { + name = "pe-sql" + location = "West Europe" + resource_group_name = "rg" + subnet_id = "subnet-id" + + private_service_connection { + name = "psc-sql" + private_connection_resource_id = azurerm_mssql_server.pass_sql.id + is_manual_connection = false + subresource_names = ["sqlServer"] + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive1.tf b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive1.tf new file mode 100644 index 00000000000..0d675c14d24 --- /dev/null +++ b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_storage_account" "fail_storage" { + name = "storagefail" + resource_group_name = "rg" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive2.tf b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive2.tf new file mode 100644 index 00000000000..f65468b751e --- /dev/null +++ b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive2.tf @@ -0,0 +1,7 @@ +resource "azurerm_key_vault" "fail_kv" { + name = "kvfail" + location = "West Europe" + resource_group_name = "rg" + tenant_id = "00000000-0000-0000-0000-000000000000" + sku_name = "standard" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive3.tf b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive3.tf new file mode 100644 index 00000000000..79f67220ac4 --- /dev/null +++ b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive3.tf @@ -0,0 +1,15 @@ +resource "azurerm_cosmosdb_account" "fail_cosmos" { + name = "cosmos-fail" + location = "West Europe" + resource_group_name = "rg" + offer_type = "Standard" + kind = "GlobalDocumentDB" + + consistency_policy { + consistency_level = "Session" + } + geo_location { + location = "West Europe" + failover_priority = 0 + } +} diff --git a/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive4.tf b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive4.tf new file mode 100644 index 00000000000..3b4df216107 --- /dev/null +++ b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive4.tf @@ -0,0 +1,8 @@ +resource "azurerm_redis_cache" "fail_redis" { + name = "redis-fail" + location = "West Europe" + resource_group_name = "rg" + capacity = 1 + family = "C" + sku_name = "Standard" +} diff --git a/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..b5c3e42c266 --- /dev/null +++ b/assets/queries/terraform/azure/azure_paas_private_endpoint_missing/test/positive_expected_result.json @@ -0,0 +1,26 @@ +[ + { + "queryName": "Beta - Ensure Private Endpoints Are Used Where Possible", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Ensure Private Endpoints Are Used Where Possible", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Ensure Private Endpoints Are Used Where Possible", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive3.tf" + }, + { + "queryName": "Beta - Ensure Private Endpoints Are Used Where Possible", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive4.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/metadata.json b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/metadata.json new file mode 100644 index 00000000000..d998032989b --- /dev/null +++ b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "bd3a6fc3-fadb-472b-b06c-54c7d7645f15", + "queryName": "Beta - Production Workload using Basic or Consumption SKU", + "severity": "LOW", + "category": "Resource Management", + "descriptionText": "Detects Azure Service Plans and API Management instances using tiers unsuitable for production. Flagged azurerm_service_plan SKUs: B1/B2/B3 (Basic \u2014 no SLA, limited scale-out), F1/FREE (Free \u2014 no SLA, no custom domains), Y1 (Consumption \u2014 cold-start latency, no always-on). Flagged azurerm_api_management SKUs: Basic and Consumption (no VNet integration, no built-in cache, no multi-region deployment). Standard, Premium, or Isolated SKUs are recommended for production workloads.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/service_plan#sku_name", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "1038", + "descriptionID": "bd3a6fc3", + "riskScore": "1.0", + "experimental": "true" +} diff --git a/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/query.rego b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/query.rego new file mode 100644 index 00000000000..bfdfaa48d71 --- /dev/null +++ b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/query.rego @@ -0,0 +1,43 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: Azure Service Plan using Basic (B), Free (F), or Consumption (Y1) SKU. +CxPolicy[result] { + doc := input.document[i] + plan := doc.resource.azurerm_service_plan[name] + + invalid_skus := ["B1", "B2", "B3", "F1", "FREE", "Y1"] + plan.sku_name == invalid_skus[_] + + result := { + "documentId": doc.id, + "resourceType": "azurerm_service_plan", + "resourceName": tf_lib.get_resource_name(plan, name), + "searchKey": sprintf("azurerm_service_plan[%s].sku_name", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_service_plan", name, "sku_name"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'%s.sku_name' should be Standard (S), Premium (P) or Isolated (I) for production", [name]), + "keyActualValue": sprintf("'%s.sku_name' is set to '%s' (Basic/Free/Consumption)", [name, plan.sku_name]), + } +} + +# RULE 2: Azure API Management using Basic or Consumption SKU. +CxPolicy[result] { + doc := input.document[i] + apim := doc.resource.azurerm_api_management[name] + + regex.match("(Basic|Consumption).*", apim.sku_name) + + result := { + "documentId": doc.id, + "resourceType": "azurerm_api_management", + "resourceName": tf_lib.get_resource_name(apim, name), + "searchKey": sprintf("azurerm_api_management[%s].sku_name", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_api_management", name, "sku_name"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'%s.sku_name' should be Standard or Premium for production features", [name]), + "keyActualValue": sprintf("'%s.sku_name' is set to '%s'", [name, apim.sku_name]), + } +} diff --git a/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/negative1.tf b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/negative1.tf new file mode 100644 index 00000000000..78a4362db6a --- /dev/null +++ b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/negative1.tf @@ -0,0 +1,7 @@ +resource "azurerm_service_plan" "pass_sp" { + name = "pass-sp" + resource_group_name = "rg" + location = "West Europe" + os_type = "Linux" + sku_name = "S1" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/negative2.tf b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/negative2.tf new file mode 100644 index 00000000000..af8313b8440 --- /dev/null +++ b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/negative2.tf @@ -0,0 +1,8 @@ +resource "azurerm_api_management" "pass_apim" { + name = "pass-apim" + location = "West Europe" + resource_group_name = "rg" + publisher_name = "Company" + publisher_email = "email@test.com" + sku_name = "Standard_1" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/positive1.tf b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/positive1.tf new file mode 100644 index 00000000000..97884022dc3 --- /dev/null +++ b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_service_plan" "fail_sp" { + name = "fail-sp" + resource_group_name = "rg" + location = "West Europe" + os_type = "Linux" + sku_name = "B1" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/positive2.tf b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/positive2.tf new file mode 100644 index 00000000000..fe09c001e18 --- /dev/null +++ b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/positive2.tf @@ -0,0 +1,8 @@ +resource "azurerm_api_management" "fail_apim" { + name = "fail-apim" + location = "West Europe" + resource_group_name = "rg" + publisher_name = "Company" + publisher_email = "email@test.com" + sku_name = "Consumption_0" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/positive_expected_result.json new file mode 100644 index 00000000000..a04beb0966f --- /dev/null +++ b/assets/queries/terraform/azure/azure_production_workload_basic_consumption_sku/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Production Workload using Basic or Consumption SKU", + "severity": "LOW", + "line": 6, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Production Workload using Basic or Consumption SKU", + "severity": "LOW", + "line": 7, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/metadata.json b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/metadata.json new file mode 100644 index 00000000000..f318ebd8313 --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "b7b0bc16-aedc-405e-a27e-cf159c200f1e", + "queryName": "Beta - Recovery Services Vault Cross Region Restore Disabled", + "severity": "MEDIUM", + "category": "Backup", + "descriptionText": "Ensures that 'Cross Region Restore' is enabled for Azure Recovery Services Vaults. This feature allows you to restore data in the secondary region, which is essential for business continuity during a primary region disaster.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/recovery_services_vault#cross_region_restore_enabled", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "668", + "descriptionID": "b7b0bc16", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/query.rego b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/query.rego new file mode 100644 index 00000000000..1dd57e477b0 --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/query.rego @@ -0,0 +1,46 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: Vault storage_mode_type is GeoRedundant but 'cross_region_restore_enabled' is not defined. +# cross_region_restore_enabled can only be enabled when storage_mode_type is GeoRedundant; +# when absent it defaults to false. +CxPolicy[result] { + doc := input.document[i] + vault := doc.resource.azurerm_recovery_services_vault[name] + + vault.storage_mode_type == "GeoRedundant" + object.get(vault, "cross_region_restore_enabled", null) == null + + result := { + "documentId": doc.id, + "resourceType": "azurerm_recovery_services_vault", + "resourceName": tf_lib.get_resource_name(vault, name), + "searchKey": sprintf("azurerm_recovery_services_vault[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_recovery_services_vault", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_recovery_services_vault.%s' should have 'cross_region_restore_enabled' set to true when 'storage_mode_type' is 'GeoRedundant'", [name]), + "keyActualValue": sprintf("'azurerm_recovery_services_vault.%s' is missing 'cross_region_restore_enabled' (defaults to false)", [name]), + } +} + +# RULE 2: Vault storage_mode_type is GeoRedundant but 'cross_region_restore_enabled' is explicitly false. +CxPolicy[result] { + doc := input.document[i] + vault := doc.resource.azurerm_recovery_services_vault[name] + + vault.storage_mode_type == "GeoRedundant" + vault.cross_region_restore_enabled == false + + result := { + "documentId": doc.id, + "resourceType": "azurerm_recovery_services_vault", + "resourceName": tf_lib.get_resource_name(vault, name), + "searchKey": sprintf("azurerm_recovery_services_vault[%s].cross_region_restore_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_recovery_services_vault", name, "cross_region_restore_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'cross_region_restore_enabled' should be set to true when 'storage_mode_type' is 'GeoRedundant'", + "keyActualValue": "'cross_region_restore_enabled' is explicitly set to false", + } +} diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/negative1.tf new file mode 100644 index 00000000000..93b14ef30fd --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/negative1.tf @@ -0,0 +1,8 @@ +resource "azurerm_recovery_services_vault" "pass" { + name = "pass-vault" + location = "West Europe" + resource_group_name = "rg-test" + sku = "Standard" + storage_mode_type = "GeoRedundant" + cross_region_restore_enabled = true +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/negative2.tf b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/negative2.tf new file mode 100644 index 00000000000..537260fdd1b --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/negative2.tf @@ -0,0 +1,8 @@ +# PASS: LocallyRedundant vault — cross_region_restore_enabled is not applicable +resource "azurerm_recovery_services_vault" "pass_local" { + name = "pass-vault-local" + location = "West Europe" + resource_group_name = "rg-test" + sku = "Standard" + storage_mode_type = "LocallyRedundant" +} diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/positive1.tf new file mode 100644 index 00000000000..dc1ac3cdc8c --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_recovery_services_vault" "fail_omission" { + name = "fail-vault-omission" + location = "West Europe" + resource_group_name = "rg-test" + sku = "Standard" + storage_mode_type = "GeoRedundant" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/positive2.tf b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/positive2.tf new file mode 100644 index 00000000000..21220fbc5f4 --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/positive2.tf @@ -0,0 +1,8 @@ +resource "azurerm_recovery_services_vault" "fail_explicit" { + name = "fail-vault-explicit" + location = "West Europe" + resource_group_name = "rg-test" + sku = "Standard" + storage_mode_type = "GeoRedundant" + cross_region_restore_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..c36f1fcf6ef --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_cross_region_restore_disabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Recovery Services Vault Cross Region Restore Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Recovery Services Vault Cross Region Restore Disabled", + "severity": "MEDIUM", + "line": 7, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/metadata.json b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/metadata.json new file mode 100644 index 00000000000..dacd0c00d53 --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "95e32d9c-77b5-423a-8746-35ab59a85148", + "queryName": "Beta - Recovery Services Vault Infrastructure Encryption Disabled", + "severity": "MEDIUM", + "category": "Encryption", + "descriptionText": "Ensures that 'Infrastructure Encryption' (Double Encryption) is enabled for Azure Recovery Services Vaults. This provides a second layer of encryption for data at rest using platform-managed keys.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/recovery_services_vault#infrastructure_encryption_enabled", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "312", + "descriptionID": "95e32d9c", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/query.rego b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/query.rego new file mode 100644 index 00000000000..acb87f881d1 --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/query.rego @@ -0,0 +1,43 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: The 'encryption' block is not defined. +CxPolicy[result] { + doc := input.document[i] + vault := doc.resource.azurerm_recovery_services_vault[name] + + not vault.encryption + + result := { + "documentId": doc.id, + "resourceType": "azurerm_recovery_services_vault", + "resourceName": tf_lib.get_resource_name(vault, name), + "searchKey": sprintf("azurerm_recovery_services_vault[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_recovery_services_vault", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_recovery_services_vault.%s' should have an 'encryption' block defined", [name]), + "keyActualValue": sprintf("'azurerm_recovery_services_vault.%s' is missing the 'encryption' block", [name]), + } +} + +# RULE 2: The 'infrastructure_encryption_enabled' attribute is not set to true. +CxPolicy[result] { + doc := input.document[i] + vault := doc.resource.azurerm_recovery_services_vault[name] + + vault.encryption + object.get(vault.encryption, "infrastructure_encryption_enabled", false) != true + + result := { + "documentId": doc.id, + "resourceType": "azurerm_recovery_services_vault", + "resourceName": tf_lib.get_resource_name(vault, name), + "searchKey": sprintf("azurerm_recovery_services_vault[%s].encryption.infrastructure_encryption_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_recovery_services_vault", name, "encryption", "infrastructure_encryption_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "encryption.infrastructure_encryption_enabled should be set to true", + "keyActualValue": sprintf("encryption.infrastructure_encryption_enabled is set to %v", [object.get(vault.encryption, "infrastructure_encryption_enabled", false)]), + } +} diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/negative1.tf new file mode 100644 index 00000000000..1e10cf5603c --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/negative1.tf @@ -0,0 +1,16 @@ +resource "azurerm_recovery_services_vault" "pass" { + name = "vault-pass" + resource_group_name = "rg" + location = "West Europe" + sku = "Standard" + + identity { + type = "SystemAssigned" + } + + encryption { + key_id = "https://kv.vault.azure.net/keys/key/v1" + infrastructure_encryption_enabled = true + use_system_assigned_identity = true + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/positive1.tf new file mode 100644 index 00000000000..51f73d0b5e7 --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/positive1.tf @@ -0,0 +1,6 @@ +resource "azurerm_recovery_services_vault" "fail_1" { + name = "fail-missing-encryption" + resource_group_name = "rg" + location = "West Europe" + sku = "Standard" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/positive2.tf b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/positive2.tf new file mode 100644 index 00000000000..2ee8fe240fc --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/positive2.tf @@ -0,0 +1,12 @@ +resource "azurerm_recovery_services_vault" "fail_2" { + name = "fail-disabled-infrastructure" + resource_group_name = "rg" + location = "West Europe" + sku = "Standard" + + encryption { + key_id = "key-id" + infrastructure_encryption_enabled = false + use_system_assigned_identity = true + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..001591f57a8 --- /dev/null +++ b/assets/queries/terraform/azure/azure_recovery_services_vault_infrastructure_encryption_disabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Recovery Services Vault Infrastructure Encryption Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Recovery Services Vault Infrastructure Encryption Disabled", + "severity": "MEDIUM", + "line": 9, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/metadata.json b/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/metadata.json new file mode 100644 index 00000000000..ca38a370736 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "56c748b8-9ec8-4f3e-84ac-c9a6992e1b20", + "queryName": "Beta - Storage Account Geo-Redundancy Disabled", + "severity": "MEDIUM", + "category": "Availability", + "descriptionText": "Ensures that Azure Storage Accounts use Geo-Redundant Storage (GRS, RAGRS, GZRS, or RAGZRS). This is critical for disaster recovery, ensuring data is durable even in the event of a complete regional outage.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account#account_replication_type", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "668", + "descriptionID": "56c748b8", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/query.rego b/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/query.rego new file mode 100644 index 00000000000..fb754480e3f --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/query.rego @@ -0,0 +1,27 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +geo_redundant_types := {"GRS", "RAGRS", "GZRS", "RAGZRS"} + +# RULE 1: The replication type is not Geo-Redundant (e.g., it is LRS or ZRS). +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[name] + + current_type := sa.account_replication_type + + not geo_redundant_types[current_type] + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, name), + "searchKey": sprintf("azurerm_storage_account[%s].account_replication_type", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name, "account_replication_type"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'account_replication_type' should be 'GRS', 'RAGRS', 'GZRS', or 'RAGZRS'", + "keyActualValue": sprintf("'account_replication_type' is set to '%s'", [current_type]), + } +} diff --git a/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/test/negative1.tf new file mode 100644 index 00000000000..01fd017e155 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/test/negative1.tf @@ -0,0 +1,7 @@ +resource "azurerm_storage_account" "pass" { + name = "storagepass" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "GRS" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/test/positive1.tf new file mode 100644 index 00000000000..40dd592b5df --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_storage_account" "fail" { + name = "storagefail" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..40e6c325211 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_geo_redundancy_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Storage Account Geo-Redundancy Disabled", + "severity": "MEDIUM", + "line": 6, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/metadata.json b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/metadata.json new file mode 100644 index 00000000000..1e7c68de645 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "d02793e5-b3b5-4cfa-b379-7a81d5bfe170", + "queryName": "Beta - Storage Account Infrastructure Encryption Disabled", + "severity": "MEDIUM", + "category": "Encryption", + "descriptionText": "Ensures that 'Infrastructure Encryption' is enabled for Azure Storage Accounts. This provides a second layer of encryption (double encryption) for data at rest, protecting against the compromise of any single encryption algorithm or key.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account#infrastructure_encryption_enabled", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "312", + "descriptionID": "d02793e5", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/query.rego b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/query.rego new file mode 100644 index 00000000000..4aba3fefbe3 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/query.rego @@ -0,0 +1,42 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: The 'infrastructure_encryption_enabled' attribute is missing. +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[name] + + object.get(sa, "infrastructure_encryption_enabled", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, name), + "searchKey": sprintf("azurerm_storage_account[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_storage_account.%s' should have 'infrastructure_encryption_enabled' set to true", [name]), + "keyActualValue": sprintf("'azurerm_storage_account.%s' is missing 'infrastructure_encryption_enabled'", [name]), + } +} + +# RULE 2: The 'infrastructure_encryption_enabled' attribute is set to false. +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[name] + + sa.infrastructure_encryption_enabled == false + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, name), + "searchKey": sprintf("azurerm_storage_account[%s].infrastructure_encryption_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name, "infrastructure_encryption_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'infrastructure_encryption_enabled' should be set to true", + "keyActualValue": "'infrastructure_encryption_enabled' is set to false", + } +} diff --git a/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/negative1.tf new file mode 100644 index 00000000000..1c168bb3be0 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/negative1.tf @@ -0,0 +1,8 @@ +resource "azurerm_storage_account" "pass" { + name = "stpass" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" + infrastructure_encryption_enabled = true +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/positive1.tf new file mode 100644 index 00000000000..acc79de247b --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_storage_account" "fail_omission" { + name = "stfailomission" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/positive2.tf b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/positive2.tf new file mode 100644 index 00000000000..4be506a4672 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/positive2.tf @@ -0,0 +1,8 @@ +resource "azurerm_storage_account" "fail_explicit" { + name = "stfailexplicit" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" + infrastructure_encryption_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..21705fb2395 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_infrastructure_encryption_disabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Storage Account Infrastructure Encryption Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Storage Account Infrastructure Encryption Disabled", + "severity": "MEDIUM", + "line": 7, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/metadata.json b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/metadata.json new file mode 100644 index 00000000000..e9dcc3ecc19 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "3a716202-a630-4f23-b8ba-2dcbc40e2fa7", + "queryName": "Beta - Storage Account ReadOnly Lock Missing", + "severity": "LOW", + "category": "Resource Management", + "descriptionText": "Ensures that Azure Storage Accounts have a Resource Manager lock with 'ReadOnly' level applied. This prevents accidental modification or deletion of the storage account configuration.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/management_lock", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "400", + "descriptionID": "3a716202", + "riskScore": "1.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/query.rego b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/query.rego new file mode 100644 index 00000000000..cd71dfae1d5 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/query.rego @@ -0,0 +1,67 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +has_readonly_lock(doc, sa_id) { + l := doc.resource.azurerm_management_lock[_] + check_lock_scope(l.scope, sa_id) + l.lock_level == "ReadOnly" +} + +check_lock_scope(current, target) { + current == target +} + +check_lock_scope(current, target) { + current == sprintf("${%s}", [target]) +} + +# CASE 1: Storage Account completely unprotected (no associated locks) +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[sa_name] + sa_id := sprintf("azurerm_storage_account.%s.id", [sa_name]) + + associated_locks := [l | + l := doc.resource.azurerm_management_lock[_] + check_lock_scope(l.scope, sa_id) + ] + count(associated_locks) == 0 + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, sa_name), + "searchKey": sprintf("azurerm_storage_account[%s]", [sa_name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", sa_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_storage_account.%s' should have a 'ReadOnly' lock associated", [sa_name]), + "keyActualValue": sprintf("'azurerm_storage_account.%s' has no locks associated", [sa_name]), + } +} + +# CASE 2: Storage Account with incorrect lock (level other than ReadOnly) +CxPolicy[result] { + doc := input.document[i] + lock := doc.resource.azurerm_management_lock[lock_name] + + lock.lock_level != "ReadOnly" + + sa := doc.resource.azurerm_storage_account[sa_name] + sa_id := sprintf("azurerm_storage_account.%s.id", [sa_name]) + check_lock_scope(lock.scope, sa_id) + + not has_readonly_lock(doc, sa_id) + + result := { + "documentId": doc.id, + "resourceType": "azurerm_management_lock", + "resourceName": tf_lib.get_resource_name(lock, lock_name), + "searchKey": sprintf("azurerm_management_lock[%s].lock_level", [lock_name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_management_lock", lock_name, "lock_level"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'azurerm_management_lock.%s.lock_level' should be 'ReadOnly'", [lock_name]), + "keyActualValue": sprintf("'azurerm_management_lock.%s.lock_level' is '%s'", [lock_name, lock.lock_level]), + } +} diff --git a/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/negative1.tf b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/negative1.tf new file mode 100644 index 00000000000..d1f2a9b1aae --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/negative1.tf @@ -0,0 +1,13 @@ +resource "azurerm_storage_account" "pass" { + name = "stpass" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_management_lock" "pass_lock" { + name = "readonly-lock" + scope = azurerm_storage_account.pass.id + lock_level = "ReadOnly" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/positive1.tf b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/positive1.tf new file mode 100644 index 00000000000..22d586ce54a --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_storage_account" "fail_1" { + name = "stfailnoblocking" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/positive2.tf b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/positive2.tf new file mode 100644 index 00000000000..60837ce3bc1 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/positive2.tf @@ -0,0 +1,13 @@ +resource "azurerm_storage_account" "sa_fail_2" { + name = "stfailwronglevel" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_management_lock" "fail_lock_level" { + name = "not-readonly" + scope = azurerm_storage_account.sa_fail_2.id + lock_level = "CanNotDelete" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..77f6226307f --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_read_only_lock_missing/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Storage Account ReadOnly Lock Missing", + "severity": "LOW", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Storage Account ReadOnly Lock Missing", + "severity": "LOW", + "line": 12, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/metadata.json b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/metadata.json new file mode 100644 index 00000000000..67556653ddf --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "0f437572-8be4-4e05-9f76-dd266d7ad90b", + "queryName": "Beta - Storage Account Blob Versioning Disabled", + "severity": "MEDIUM", + "category": "Backup", + "descriptionText": "Ensures that 'Versioning' is enabled for Azure Storage Account Blob services. Blob versioning enables automatic retention of previous versions of an object.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account#versioning_enabled", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "226", + "descriptionID": "0f437572", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/query.rego b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/query.rego new file mode 100644 index 00000000000..ba4c7943869 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/query.rego @@ -0,0 +1,63 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# CASE 1: The complete 'blob_properties' block is missing. +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[name] + + not sa.blob_properties + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, name), + "searchKey": sprintf("azurerm_storage_account[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_storage_account.%s' should have 'blob_properties' defined", [name]), + "keyActualValue": sprintf("'azurerm_storage_account.%s' is missing 'blob_properties'", [name]), + } +} + +# CASE 2: 'blob_properties' exists but the 'versioning_enabled' attribute is missing. +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[name] + + sa.blob_properties + + object.get(sa.blob_properties, "versioning_enabled", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, name), + "searchKey": sprintf("azurerm_storage_account[%s].blob_properties", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name, "blob_properties"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "blob_properties.versioning_enabled should be defined and set to true", + "keyActualValue": "blob_properties.versioning_enabled is missing", + } +} + +# CASE 3: 'versioning_enabled' exists but is explicitly set to false. +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[name] + + sa.blob_properties.versioning_enabled == false + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, name), + "searchKey": sprintf("azurerm_storage_account[%s].blob_properties.versioning_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name, "blob_properties", "versioning_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "blob_properties.versioning_enabled should be set to true", + "keyActualValue": "blob_properties.versioning_enabled is set to false", + } +} diff --git a/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/negative1.tf new file mode 100644 index 00000000000..efc6cad605c --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/negative1.tf @@ -0,0 +1,11 @@ +resource "azurerm_storage_account" "pass" { + name = "stpass" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" + + blob_properties { + versioning_enabled = true + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive1.tf new file mode 100644 index 00000000000..219f4827eb9 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_storage_account" "fail_missing_block" { + name = "stfailblock" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive2.tf b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive2.tf new file mode 100644 index 00000000000..e5d343549ae --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive2.tf @@ -0,0 +1,11 @@ +resource "azurerm_storage_account" "fail_explicit_false" { + name = "stfailexplicit" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" + + blob_properties { + change_feed_enabled = false + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive3.tf b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive3.tf new file mode 100644 index 00000000000..ebf179fbecd --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive3.tf @@ -0,0 +1,11 @@ +resource "azurerm_storage_account" "fail_explicit_false" { + name = "stfailexplicit" + resource_group_name = "rg-test" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" + + blob_properties { + versioning_enabled = false + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..bd93877ba51 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_account_versioning_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Storage Account Blob Versioning Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Storage Account Blob Versioning Disabled", + "severity": "MEDIUM", + "line": 8, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Storage Account Blob Versioning Disabled", + "severity": "MEDIUM", + "line": 9, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/metadata.json b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/metadata.json new file mode 100644 index 00000000000..7da8e4b09dc --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "11fa88c0-2064-48ee-8431-953c7d961c71", + "queryName": "Beta - Storage Blob Service Logging Disabled", + "severity": "LOW", + "category": "Observability", + "descriptionText": "Ensures that Storage Logging is enabled for the Blob service for 'Read', 'Write', and 'Delete' requests. This provides an audit trail of operations performed on blobs and containers.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_diagnostic_setting", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "778", + "descriptionID": "11fa88c0", + "riskScore": "1.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/query.rego b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/query.rego new file mode 100644 index 00000000000..06f2b1dfa55 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/query.rego @@ -0,0 +1,88 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +is_target_linked(target, sa_name) { + ref := sprintf("azurerm_storage_account.%s.id", [sa_name]) + contains(target, ref) + contains(target, "blobServices/default") +} + +is_target_linked(target, sa_name) { + ref := sprintf("${azurerm_storage_account.%s.id}", [sa_name]) + contains(target, ref) + contains(target, "blobServices/default") +} + +# CASE 1: The storage account has no Diagnostic Setting for Blobs. +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[name] + + not diag_exists_for_sa(doc, name) + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, name), + "searchKey": sprintf("azurerm_storage_account[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_storage_account.%s' should have an 'azurerm_monitor_diagnostic_setting' for its blob service", [name]), + "keyActualValue": sprintf("'azurerm_storage_account.%s' does not have diagnostic logging enabled for blobs", [name]), + } +} + +diag_exists_for_sa(doc, sa_name) { + diag := doc.resource.azurerm_monitor_diagnostic_setting[_] + is_target_linked(diag.target_resource_id, sa_name) +} + +# CASE 2: The Diagnostic Setting exists but has no 'enabled_log' blocks. +CxPolicy[result] { + doc := input.document[i] + diag := doc.resource.azurerm_monitor_diagnostic_setting[diag_name] + + contains(diag.target_resource_id, "blobServices/default") + not diag.enabled_log + + result := { + "documentId": doc.id, + "resourceType": "azurerm_monitor_diagnostic_setting", + "resourceName": tf_lib.get_resource_name(diag, diag_name), + "searchKey": sprintf("azurerm_monitor_diagnostic_setting[%s]", [diag_name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_monitor_diagnostic_setting", diag_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "Diagnostic Setting should have 'enabled_log' blocks defined", + "keyActualValue": "Diagnostic Setting has no 'enabled_log' blocks", + } +} + +# CASE 3: The Diagnostic Setting has 'enabled_log' blocks but the set is incomplete. +CxPolicy[result] { + doc := input.document[i] + diag := doc.resource.azurerm_monitor_diagnostic_setting[diag_name] + + contains(diag.target_resource_id, "blobServices/default") + diag.enabled_log + + required_categories := {"StorageRead", "StorageWrite", "StorageDelete"} + present_categories := {cat | + log := diag.enabled_log[_] + cat := log.category + } + + not count(required_categories - present_categories) == 0 + + result := { + "documentId": doc.id, + "resourceType": "azurerm_monitor_diagnostic_setting", + "resourceName": tf_lib.get_resource_name(diag, diag_name), + "searchKey": sprintf("azurerm_monitor_diagnostic_setting[%s].enabled_log", [diag_name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_monitor_diagnostic_setting", diag_name, "enabled_log"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "All required log categories (StorageRead, StorageWrite, StorageDelete) should be present", + "keyActualValue": "One or more required log categories are missing in the 'enabled_log' configuration", + } +} diff --git a/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..13a3e1790f3 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/negative1.tf @@ -0,0 +1,17 @@ +resource "azurerm_storage_account" "pass" { + name = "st-pass" + resource_group_name = "rg" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_monitor_diagnostic_setting" "pass_diag" { + name = "complete-diag" + target_resource_id = "${azurerm_storage_account.pass.id}/blobServices/default" + storage_account_id = "target" + + enabled_log { category = "StorageRead" } + enabled_log { category = "StorageWrite" } + enabled_log { category = "StorageDelete" } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..ea0d8402fd3 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_storage_account" "fail_none" { + name = "st-no-blob-diag" + resource_group_name = "rg" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive2.tf b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive2.tf new file mode 100644 index 00000000000..00f606b3c99 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive2.tf @@ -0,0 +1,6 @@ +resource "azurerm_monitor_diagnostic_setting" "fail_no_logs" { + name = "empty-diag" + target_resource_id = "${azurerm_storage_account.example.id}/blobServices/default" + storage_account_id = "id" + # enabled_log bloque missing +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive3.tf b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive3.tf new file mode 100644 index 00000000000..d595f8bf0b2 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive3.tf @@ -0,0 +1,10 @@ +resource "azurerm_monitor_diagnostic_setting" "fail_partial" { + name = "partial-diag" + target_resource_id = "${azurerm_storage_account.example.id}/blobServices/default" + storage_account_id = "id" + + enabled_log { + category = "StorageRead" + } + # Faltan Write y Delete. Solo debe saltar 1 vez. +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..9f8f5b883cd --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_blob_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Storage Blob Service Logging Disabled", + "severity": "LOW", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Storage Blob Service Logging Disabled", + "severity": "LOW", + "line": 1, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Storage Blob Service Logging Disabled", + "severity": "LOW", + "line": 6, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/metadata.json b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/metadata.json new file mode 100644 index 00000000000..e6c03a72bd2 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "9b959753-3a9b-41e7-82b2-28bef91e6b53", + "queryName": "Beta - Storage Immutability Policy Not Locked", + "severity": "MEDIUM", + "category": "Access Control", + "descriptionText": "Ensures that the Immutability Policy for Azure Storage Containers is set to 'Locked'. An unlocked policy can be removed or modified, failing to provide true WORM (Write Once, Read Many) compliance for business-critical data.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_container_immutability_policy#locked", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "284", + "descriptionID": "9b959753", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/query.rego b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/query.rego new file mode 100644 index 00000000000..cd85af72e4a --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/query.rego @@ -0,0 +1,42 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: The 'locked' attribute is not defined (Default is false/unlocked). +CxPolicy[result] { + doc := input.document[i] + policy := doc.resource.azurerm_storage_container_immutability_policy[name] + + object.get(policy, "locked", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_container_immutability_policy", + "resourceName": tf_lib.get_resource_name(policy, name), + "searchKey": sprintf("azurerm_storage_container_immutability_policy[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_container_immutability_policy", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_storage_container_immutability_policy.%s' should have 'locked' set to true", [name]), + "keyActualValue": sprintf("'azurerm_storage_container_immutability_policy.%s' is missing 'locked' attribute (default is false)", [name]), + } +} + +# RULE 2: The 'locked' attribute is explicitly set to false. +CxPolicy[result] { + doc := input.document[i] + policy := doc.resource.azurerm_storage_container_immutability_policy[name] + + policy.locked == false + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_container_immutability_policy", + "resourceName": tf_lib.get_resource_name(policy, name), + "searchKey": sprintf("azurerm_storage_container_immutability_policy[%s].locked", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_container_immutability_policy", name, "locked"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'locked' should be set to true", + "keyActualValue": "'locked' is set to false", + } +} diff --git a/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/negative1.tf b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/negative1.tf new file mode 100644 index 00000000000..24a49a6b0a9 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/negative1.tf @@ -0,0 +1,5 @@ +resource "azurerm_storage_container_immutability_policy" "pass" { + storage_container_resource_manager_id = "some-resource-id" + immutability_period_in_days = 90 + locked = true +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/positive1.tf b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/positive1.tf new file mode 100644 index 00000000000..f245a916668 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/positive1.tf @@ -0,0 +1,4 @@ +resource "azurerm_storage_container_immutability_policy" "fail_omission" { + storage_container_resource_manager_id = "some-resource-id" + immutability_period_in_days = 90 +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/positive2.tf b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/positive2.tf new file mode 100644 index 00000000000..65f0fe13e6e --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/positive2.tf @@ -0,0 +1,5 @@ +resource "azurerm_storage_container_immutability_policy" "fail_explicit" { + storage_container_resource_manager_id = "some-resource-id" + immutability_period_in_days = 90 + locked = false +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/positive_expected_result.json new file mode 100644 index 00000000000..a79dc0b8fb3 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_container_immutability_not_locked/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Storage Immutability Policy Not Locked", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Storage Immutability Policy Not Locked", + "severity": "MEDIUM", + "line": 4, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/metadata.json b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/metadata.json new file mode 100644 index 00000000000..e2c700e33a7 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "cddbdd74-3736-4aec-ba57-43b4db315bc2", + "queryName": "Beta - Storage Queue Service Logging Disabled", + "severity": "LOW", + "category": "Observability", + "descriptionText": "Ensures that Storage Logging is enabled for the Queue service for 'Read', 'Write', and 'Delete' requests. This provides an audit trail of operations performed on the queues.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account#logging", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "778", + "descriptionID": "cddbdd74", + "riskScore": "1.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/query.rego b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/query.rego new file mode 100644 index 00000000000..0fa23b2c725 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/query.rego @@ -0,0 +1,89 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +is_logging_valid(logging) { + logging.read == true + logging.write == true + logging.delete == true +} + +# CASE 1: 'logging' block missing in 'queue_properties' of azurerm_storage_account. +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[name] + + sa.queue_properties + not sa.queue_properties.logging + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, name), + "searchKey": sprintf("azurerm_storage_account[%s].queue_properties", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name, "queue_properties"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "queue_properties.logging should be defined with read, write, and delete enabled", + "keyActualValue": "queue_properties.logging is missing", + } +} + +# CASE 2: Incorrect 'logging' configuration in azurerm_storage_account. +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[name] + + logging := sa.queue_properties.logging + not is_logging_valid(logging) + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, name), + "searchKey": sprintf("azurerm_storage_account[%s].queue_properties.logging", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name, "queue_properties", "logging"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "logging should have read, write, and delete set to true", + "keyActualValue": "logging has one or more required actions (read, write, delete) disabled", + } +} + +# CASE 3: 'logging' block missing in the azurerm_storage_account_queue_properties resource. +CxPolicy[result] { + doc := input.document[i] + props := doc.resource.azurerm_storage_account_queue_properties[name] + + not props.logging + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account_queue_properties", + "resourceName": tf_lib.get_resource_name(props, name), + "searchKey": sprintf("azurerm_storage_account_queue_properties[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account_queue_properties", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "logging block should be defined in queue properties", + "keyActualValue": "logging block is missing", + } +} + +# CASE 4: Incorrect 'logging' configuration in azurerm_storage_account_queue_properties. +CxPolicy[result] { + doc := input.document[i] + props := doc.resource.azurerm_storage_account_queue_properties[name] + + logging := props.logging + not is_logging_valid(logging) + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account_queue_properties", + "resourceName": tf_lib.get_resource_name(props, name), + "searchKey": sprintf("azurerm_storage_account_queue_properties[%s].logging", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account_queue_properties", name, "logging"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "logging should have read, write, and delete set to true", + "keyActualValue": "logging is missing one or more required actions", + } +} diff --git a/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..ee304ada58b --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/negative1.tf @@ -0,0 +1,17 @@ +resource "azurerm_storage_account" "pass_inline" { + name = "stpassinline" + resource_group_name = "rg" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" + + queue_properties { + logging { + read = true + write = true + delete = true + version = "1.0" + retention_policy_days = 7 + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/negative2.tf b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/negative2.tf new file mode 100644 index 00000000000..d479376aa5a --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/negative2.tf @@ -0,0 +1,19 @@ +resource "azurerm_storage_account" "base" { + name = "stbase" + resource_group_name = "rg" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_storage_account_queue_properties" "pass_standalone" { + storage_account_id = azurerm_storage_account.base.id + + logging { + read = true + write = true + delete = true + version = "1.0" + retention_policy_days = 10 + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..f8e34a8aeb5 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive1.tf @@ -0,0 +1,11 @@ +resource "azurerm_storage_account" "fail_1" { + name = "stfail1" + resource_group_name = "rg" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" + + queue_properties { + # Missing bloque logging + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive2.tf b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive2.tf new file mode 100644 index 00000000000..61cd3bf4219 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive2.tf @@ -0,0 +1,16 @@ +resource "azurerm_storage_account" "fail_2" { + name = "stfail2" + resource_group_name = "rg" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" + + queue_properties { + logging { + read = true + write = false + delete = true + version = "1.0" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive3.tf b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive3.tf new file mode 100644 index 00000000000..97b7aed369b --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive3.tf @@ -0,0 +1,3 @@ +resource "azurerm_storage_account_queue_properties" "fail_3" { + storage_account_id = "some-id" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive4.tf b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive4.tf new file mode 100644 index 00000000000..218bdef5585 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive4.tf @@ -0,0 +1,9 @@ +resource "azurerm_storage_account_queue_properties" "fail_4" { + storage_account_id = "some-id" + logging { + read = false + write = true + delete = true + version = "1.0" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..05277735a7c --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_queue_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,26 @@ +[ + { + "queryName": "Beta - Storage Queue Service Logging Disabled", + "severity": "LOW", + "line": 8, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Storage Queue Service Logging Disabled", + "severity": "LOW", + "line": 9, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Storage Queue Service Logging Disabled", + "severity": "LOW", + "line": 1, + "fileName": "positive3.tf" + }, + { + "queryName": "Beta - Storage Queue Service Logging Disabled", + "severity": "LOW", + "line": 3, + "fileName": "positive4.tf" + } +] diff --git a/assets/queries/terraform/azure/azure_storage_table_logging_disabled/metadata.json b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/metadata.json new file mode 100644 index 00000000000..92ff060f6f4 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "0d29e3cf-0033-4ca6-9b97-fa3e6c4d25b8", + "queryName": "Beta - Storage Table Service Logging Disabled", + "severity": "LOW", + "category": "Observability", + "descriptionText": "Ensures that Storage Logging is enabled for the Table service for 'Read', 'Write', and 'Delete' requests. This provides an audit trail of operations performed on NoSQL tables within the storage account.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_diagnostic_setting", + "platform": "Terraform", + "cloudProvider": "azure", + "cwe": "778", + "descriptionID": "0d29e3cf", + "riskScore": "1.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_table_logging_disabled/query.rego b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/query.rego new file mode 100644 index 00000000000..9c3a9b51ff1 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/query.rego @@ -0,0 +1,88 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +is_target_linked(target, sa_name) { + ref := sprintf("azurerm_storage_account.%s.id", [sa_name]) + contains(target, ref) + contains(target, "tableServices/default") +} + +is_target_linked(target, sa_name) { + ref := sprintf("${azurerm_storage_account.%s.id}", [sa_name]) + contains(target, ref) + contains(target, "tableServices/default") +} + +# CASE 1: The storage account has no Diagnostic Setting for Tables. +CxPolicy[result] { + doc := input.document[i] + sa := doc.resource.azurerm_storage_account[name] + + not diag_exists_for_sa(doc, name) + + result := { + "documentId": doc.id, + "resourceType": "azurerm_storage_account", + "resourceName": tf_lib.get_resource_name(sa, name), + "searchKey": sprintf("azurerm_storage_account[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_storage_account.%s' should have an 'azurerm_monitor_diagnostic_setting' for its table service", [name]), + "keyActualValue": sprintf("'azurerm_storage_account.%s' does not have diagnostic logging enabled for tables", [name]), + } +} + +diag_exists_for_sa(doc, sa_name) { + diag := doc.resource.azurerm_monitor_diagnostic_setting[_] + is_target_linked(diag.target_resource_id, sa_name) +} + +# CASE 2: The Diagnostic Setting exists but has no 'enabled_log' blocks. +CxPolicy[result] { + doc := input.document[i] + diag := doc.resource.azurerm_monitor_diagnostic_setting[diag_name] + + contains(diag.target_resource_id, "tableServices/default") + not diag.enabled_log + + result := { + "documentId": doc.id, + "resourceType": "azurerm_monitor_diagnostic_setting", + "resourceName": tf_lib.get_resource_name(diag, diag_name), + "searchKey": sprintf("azurerm_monitor_diagnostic_setting[%s]", [diag_name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_monitor_diagnostic_setting", diag_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "Diagnostic Setting should have 'enabled_log' blocks defined", + "keyActualValue": "Diagnostic Setting has no 'enabled_log' blocks", + } +} + +# CASE 3: The Diagnostic Setting has 'enabled_log' blocks but the set is incomplete. +CxPolicy[result] { + doc := input.document[i] + diag := doc.resource.azurerm_monitor_diagnostic_setting[diag_name] + + contains(diag.target_resource_id, "tableServices/default") + diag.enabled_log + + required_categories := {"StorageRead", "StorageWrite", "StorageDelete"} + present_categories := {cat | + log := diag.enabled_log[_] + cat := log.category + } + + not count(required_categories - present_categories) == 0 + + result := { + "documentId": doc.id, + "resourceType": "azurerm_monitor_diagnostic_setting", + "resourceName": tf_lib.get_resource_name(diag, diag_name), + "searchKey": sprintf("azurerm_monitor_diagnostic_setting[%s].enabled_log", [diag_name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_monitor_diagnostic_setting", diag_name, "enabled_log"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "All required log categories (StorageRead, StorageWrite, StorageDelete) should be present", + "keyActualValue": "One or more required log categories are missing in the 'enabled_log' configuration for Table service", + } +} diff --git a/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/negative1.tf b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..f426550f82b --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/negative1.tf @@ -0,0 +1,17 @@ +resource "azurerm_storage_account" "pass" { + name = "st-table-pass" + resource_group_name = "rg" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_monitor_diagnostic_setting" "pass_table_diag" { + name = "complete-table-diag" + target_resource_id = "${azurerm_storage_account.pass.id}/tableServices/default" + storage_account_id = "target" + + enabled_log { category = "StorageRead" } + enabled_log { category = "StorageWrite" } + enabled_log { category = "StorageDelete" } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive1.tf b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..45c4a09dfc7 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_storage_account" "fail_none" { + name = "st-no-table-diag" + resource_group_name = "rg" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive2.tf b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive2.tf new file mode 100644 index 00000000000..b7399c3f233 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive2.tf @@ -0,0 +1,6 @@ +resource "azurerm_monitor_diagnostic_setting" "fail_no_logs" { + name = "no-logs-diag" + target_resource_id = "${azurerm_storage_account.example.id}/tableServices/default" + storage_account_id = "id" + # No hay bloques enabled_log +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive3.tf b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive3.tf new file mode 100644 index 00000000000..ed9882e5258 --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive3.tf @@ -0,0 +1,18 @@ +resource "azurerm_storage_account" "example" { + name = "st-table-partial" + resource_group_name = "rg" + location = "West Europe" + account_tier = "Standard" + account_replication_type = "LRS" +} + +resource "azurerm_monitor_diagnostic_setting" "fail_partial" { + name = "partial-table-diag" + target_resource_id = "${azurerm_storage_account.example.id}/tableServices/default" + storage_account_id = "target" + + # Faltan categorías de log + enabled_log { + category = "StorageRead" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..39044a3903f --- /dev/null +++ b/assets/queries/terraform/azure/azure_storage_table_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Storage Table Service Logging Disabled", + "severity": "LOW", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Storage Table Service Logging Disabled", + "severity": "LOW", + "line": 1, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Storage Table Service Logging Disabled", + "severity": "LOW", + "line": 15, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_access_approval_disabled/metadata.json b/assets/queries/terraform/gcp/gcp_access_approval_disabled/metadata.json new file mode 100644 index 00000000000..367dac43efa --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_access_approval_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "d7df9989-f87a-45cb-80ea-d4d20e3d4530", + "queryName": "Beta - GCP Access Approval Disabled", + "severity": "MEDIUM", + "category": "Access Control", + "descriptionText": "Ensures that Access Approval is enabled for the GCP Project. Access Approval allows you to approve or deny access to your data by Google support and engineering personnel.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/access_approval_project_settings", + "platform": "Terraform", + "descriptionID": "d7df9989", + "cloudProvider": "gcp", + "cwe": "284", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_access_approval_disabled/query.rego b/assets/queries/terraform/gcp/gcp_access_approval_disabled/query.rego new file mode 100644 index 00000000000..53b3886d424 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_access_approval_disabled/query.rego @@ -0,0 +1,47 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# CASE 1: Project without Access Approval configuration. +CxPolicy[result] { + doc := input.document[i] + project := doc.resource.google_project[name] + + settings := [s | + s := doc.resource.google_access_approval_project_settings[_] + contains(s.project_id, name) + ] + + count(settings) == 0 + + result := { + "documentId": doc.id, + "resourceType": "google_project", + "resourceName": tf_lib.get_resource_name(project, name), + "searchKey": sprintf("google_project[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_project", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'google_project.%s' should have 'google_access_approval_project_settings' associated", [name]), + "keyActualValue": sprintf("'google_project.%s' does not have Access Approval configured", [name]), + } +} + +# CASE 2: Access Approval configured but without enrolled services. +CxPolicy[result] { + doc := input.document[i] + settings := doc.resource.google_access_approval_project_settings[name] + + not settings.enrolled_services + + result := { + "documentId": doc.id, + "resourceType": "google_access_approval_project_settings", + "resourceName": tf_lib.get_resource_name(settings, name), + "searchKey": sprintf("google_access_approval_project_settings[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_access_approval_project_settings", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "Must have at least one 'enrolled_services' block defined", + "keyActualValue": "'enrolled_services' is missing", + } +} diff --git a/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/negative1.tf b/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/negative1.tf new file mode 100644 index 00000000000..d4f71adbe76 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/negative1.tf @@ -0,0 +1,13 @@ +resource "google_project" "safe_project" { + name = "Safe Project" + project_id = "safe-123" + org_id = "12345" +} + +resource "google_access_approval_project_settings" "safe_settings" { + project_id = google_project.safe_project.project_id + + enrolled_services { + cloud_product = "all" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/positive1.tf b/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/positive1.tf new file mode 100644 index 00000000000..f7c28f9789d --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/positive1.tf @@ -0,0 +1,5 @@ +resource "google_project" "vulnerable_project" { + name = "Project Without Approval" + project_id = "vulnerable-123" + org_id = "12345" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/positive2.tf b/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/positive2.tf new file mode 100644 index 00000000000..7a9af635d39 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/positive2.tf @@ -0,0 +1,4 @@ +resource "google_access_approval_project_settings" "empty_settings" { + project_id = "some-project-id" + # FAIL: No tiene enrolled_services +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..d3b7f99ab01 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_access_approval_disabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - GCP Access Approval Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - GCP Access Approval Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/metadata.json b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/metadata.json new file mode 100644 index 00000000000..5546305fb78 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "8097afd6-ef1a-4a58-b5f8-5d1b70ab4818", + "queryName": "Beta - Google API Key API Targets Missing", + "severity": "MEDIUM", + "category": "Access Control", + "descriptionText": "Ensures that Google Cloud API Keys are restricted to specific APIs. By default, an API key can be used to access any API enabled in the project, which increases the blast radius if the key is compromised.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/apikeys_key#api_targets", + "platform": "Terraform", + "descriptionID": "8097afd6", + "cloudProvider": "gcp", + "cwe": "284", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/query.rego b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/query.rego new file mode 100644 index 00000000000..ce3d7101e2c --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/query.rego @@ -0,0 +1,43 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# CASE 1: The 'restrictions' block does not exist. +CxPolicy[result] { + doc := input.document[i] + key := doc.resource.google_apikeys_key[name] + + not key.restrictions + + result := { + "documentId": doc.id, + "resourceType": "google_apikeys_key", + "resourceName": tf_lib.get_resource_name(key, name), + "searchKey": sprintf("google_apikeys_key[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_apikeys_key", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'google_apikeys_key.%s' should have 'restrictions.api_targets' defined", [name]), + "keyActualValue": sprintf("'google_apikeys_key.%s' is missing the 'restrictions' block", [name]), + } +} + +# CASE 2: 'restrictions' exists, but 'api_targets' is missing. +CxPolicy[result] { + doc := input.document[i] + key := doc.resource.google_apikeys_key[name] + + key.restrictions + not key.restrictions.api_targets + + result := { + "documentId": doc.id, + "resourceType": "google_apikeys_key", + "resourceName": tf_lib.get_resource_name(key, name), + "searchKey": sprintf("google_apikeys_key[%s].restrictions", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_apikeys_key", name, "restrictions"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "restrictions.api_targets should be defined", + "keyActualValue": "restrictions.api_targets is missing", + } +} diff --git a/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/negative1.tf b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/negative1.tf new file mode 100644 index 00000000000..c15a1af37d4 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/negative1.tf @@ -0,0 +1,15 @@ +resource "google_apikeys_key" "key_fully_secure" { + name = "fully-secure-key" + display_name = "Compliant Key" + + restrictions { + browser_key_restrictions { + allowed_referrers = ["https://example.com/*"] + } + + # PASS: Acceso limitado a servicios específicos + api_targets { + service = "translate.googleapis.com" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/positive1.tf b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/positive1.tf new file mode 100644 index 00000000000..27bb02ac13f --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/positive1.tf @@ -0,0 +1,5 @@ +resource "google_apikeys_key" "key_no_restrictions" { + name = "unrestricted-key" + display_name = "Unrestricted Key" + project = "my-project" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/positive2.tf b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/positive2.tf new file mode 100644 index 00000000000..4ad804ebc15 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/positive2.tf @@ -0,0 +1,11 @@ +resource "google_apikeys_key" "key_no_targets" { + name = "partial-restricted-key" + display_name = "Key without API targets" + + restrictions { + server_key_restrictions { + allowed_ips = ["1.2.3.4"] + } + # FAIL: Missing The block api_targets + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..079973a7a80 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_api_targets_missing/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Google API Key API Targets Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Google API Key API Targets Missing", + "severity": "MEDIUM", + "line": 5, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/metadata.json b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/metadata.json new file mode 100644 index 00000000000..6baa6fe927c --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "7b3ba800-c190-4b65-ae4d-fa1e5f7bc75e", + "queryName": "Beta - Google API Key Restrictions (Manual)", + "severity": "INFO", + "category": "Access Control", + "descriptionText": "Ensures that Google Cloud API Keys are restricted. If restrictions are present, they must be manually verified to ensure they list the correct IPs, websites, or apps.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/apikeys_key", + "platform": "Terraform", + "descriptionID": "7b3ba800", + "cloudProvider": "gcp", + "cwe": "284", + "riskScore": "0.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/query.rego b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/query.rego new file mode 100644 index 00000000000..d0bcfa9e07c --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/query.rego @@ -0,0 +1,42 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# CASE 1: No hay restricciones definidas. +CxPolicy[result] { + doc := input.document[i] + key := doc.resource.google_apikeys_key[name] + + not key.restrictions + + result := { + "documentId": doc.id, + "resourceType": "google_apikeys_key", + "resourceName": tf_lib.get_resource_name(key, name), + "searchKey": sprintf("google_apikeys_key[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_apikeys_key", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'google_apikeys_key.%s' should have a 'restrictions' block defined", [name]), + "keyActualValue": sprintf("'google_apikeys_key.%s' is missing the 'restrictions' block", [name]), + } +} + +# CASE 2: Hay restricciones definidas. +CxPolicy[result] { + doc := input.document[i] + key := doc.resource.google_apikeys_key[name] + + key.restrictions + + result := { + "documentId": doc.id, + "resourceType": "google_apikeys_key", + "resourceName": tf_lib.get_resource_name(key, name), + "searchKey": sprintf("google_apikeys_key[%s].restrictions", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_apikeys_key", name, "restrictions"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "Restrictions should be verified against allowed IPs/Referrers", + "keyActualValue": "Restrictions are present. Manual verification required.", + } +} diff --git a/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/negative1.tf b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/negative1.tf new file mode 100644 index 00000000000..c37bffc22a1 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/negative1.tf @@ -0,0 +1,4 @@ +# Caso negativo: No existen recursos de API Key, por lo que no hay riesgo que auditar. +resource "google_compute_network" "vpc" { + name = "secure-network" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/positive1.tf b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/positive1.tf new file mode 100644 index 00000000000..cb327abca07 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/positive1.tf @@ -0,0 +1,6 @@ +resource "google_apikeys_key" "key_public" { + name = "public-key" + display_name = "Public Key" + project = "my-project" + # FAIL: No tiene bloque restrictions +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/positive2.tf b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/positive2.tf new file mode 100644 index 00000000000..d39236a798c --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/positive2.tf @@ -0,0 +1,11 @@ +resource "google_apikeys_key" "key_with_restrictions" { + name = "restricted-key" + display_name = "Restricted Key" + + restrictions { + # INFO: Esta sección requiere verificación manual de los valores + server_key_restrictions { + allowed_ips = ["192.168.1.1"] + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..ddbc995c264 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_api_key_restrictions_manual/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Google API Key Restrictions (Manual)", + "severity": "INFO", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Google API Key Restrictions (Manual)", + "severity": "INFO", + "line": 5, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/metadata.json b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/metadata.json new file mode 100644 index 00000000000..9a4e22f249c --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "52b7bc15-032e-4dd7-b9c3-6dc9698aa131", + "queryName": "Beta - App Engine HTTPS Enforcement (Manual)", + "severity": "INFO", + "category": "Encryption", + "descriptionText": "Ensures that Google App Engine applications enforce HTTPS connections. This is typically configured in 'app.yaml' using 'secure: always' or in Terraform via the 'handlers' block with 'security_level'. Manual verification is often required.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/app_engine_standard_app_version#security_level", + "platform": "Terraform", + "descriptionID": "52b7bc15", + "cloudProvider": "gcp", + "cwe": "319", + "riskScore": "0.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/query.rego b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/query.rego new file mode 100644 index 00000000000..9aadc258e53 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/query.rego @@ -0,0 +1,51 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +ensure_array(x) = x { is_array(x) } +ensure_array(x) = [x] { not is_array(x) } + +# CASE 1: Insecure configuration in the defined handlers. +CxPolicy[result] { + doc := input.document[i] + app := doc.resource.google_app_engine_standard_app_version[name] + + app.handlers + + handlers_list := ensure_array(app.handlers) + handler := handlers_list[_] + + sec_level := object.get(handler, "security_level", "UNSPECIFIED") + sec_level != "SECURE_ALWAYS" + + result := { + "documentId": doc.id, + "resourceType": "google_app_engine_standard_app_version", + "resourceName": tf_lib.get_resource_name(app, name), + "searchKey": sprintf("google_app_engine_standard_app_version[%s].handlers", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_app_engine_standard_app_version", name, "handlers"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'handlers.security_level' should be set to 'SECURE_ALWAYS'", + "keyActualValue": sprintf("'handlers.security_level' is set to '%s'", [sec_level]), + } +} + +# CASE 2: Configuration Missing in Terraform (Requires review of app.yaml). +CxPolicy[result] { + doc := input.document[i] + app := doc.resource.google_app_engine_standard_app_version[name] + + not app.handlers + + result := { + "documentId": doc.id, + "resourceType": "google_app_engine_standard_app_version", + "resourceName": tf_lib.get_resource_name(app, name), + "searchKey": sprintf("google_app_engine_standard_app_version[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_app_engine_standard_app_version", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "If handlers are not defined in Terraform, verify 'app.yaml' contains 'secure: always'", + "keyActualValue": "Terraform does not define handlers. Manual verification of 'app.yaml' required.", + } +} diff --git a/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/negative1.tf b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/negative1.tf new file mode 100644 index 00000000000..c677e558ac9 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/negative1.tf @@ -0,0 +1,10 @@ +resource "google_app_engine_standard_app_version" "app_secure" { + service = "frontend" + version_id = "v1" + runtime = "php81" + + handlers { + url_regex = "/.*" + security_level = "SECURE_ALWAYS" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/positive1.tf b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/positive1.tf new file mode 100644 index 00000000000..eef70148e59 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/positive1.tf @@ -0,0 +1,11 @@ +resource "google_app_engine_standard_app_version" "app_insecure" { + service = "default" + version_id = "v1" + runtime = "python39" + + handlers { + url_regex = "/.*" + # FAIL: security_level no es SECURE_ALWAYS + security_level = "SECURE_OPTIONAL" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/positive2.tf b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/positive2.tf new file mode 100644 index 00000000000..3e87e494cba --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/positive2.tf @@ -0,0 +1,6 @@ +resource "google_app_engine_standard_app_version" "app_no_handlers" { + service = "backend" + version_id = "v1" + runtime = "go119" + # FAIL: Missing The block handlers, requiere revisión de app.yaml +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..28ef3d2aa89 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_app_engine_https_enforcement_manual/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - App Engine HTTPS Enforcement (Manual)", + "severity": "INFO", + "line": 6, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - App Engine HTTPS Enforcement (Manual)", + "severity": "INFO", + "line": 1, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/metadata.json b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/metadata.json new file mode 100644 index 00000000000..0fb133488c5 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "ccefc589-0310-45a4-a2b6-d558e629e019", + "queryName": "Beta - GCP Compute Logging Service Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that the 'google-logging-enabled' metadata flag is set to 'true' for Google Compute Engine instances. This flag configures the instance to send logs to Cloud Logging.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance#metadata", + "platform": "Terraform", + "descriptionID": "ccefc589", + "cloudProvider": "gcp", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/query.rego b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/query.rego new file mode 100644 index 00000000000..26f383ccd64 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/query.rego @@ -0,0 +1,63 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: The 'metadata' block does not exist. +CxPolicy[result] { + doc := input.document[i] + instance := doc.resource.google_compute_instance[name] + + not instance.metadata + + result := { + "documentId": doc.id, + "resourceType": "google_compute_instance", + "resourceName": tf_lib.get_resource_name(instance, name), + "searchKey": sprintf("google_compute_instance[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_compute_instance", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'metadata' block should be defined and contain 'google-logging-enabled'", + "keyActualValue": "'metadata' block is missing", + } +} + +# RULE 2: The 'metadata' block exists but is missing the 'google-logging-enabled' key. +CxPolicy[result] { + doc := input.document[i] + instance := doc.resource.google_compute_instance[name] + + instance.metadata + not instance.metadata["google-logging-enabled"] + + result := { + "documentId": doc.id, + "resourceType": "google_compute_instance", + "resourceName": tf_lib.get_resource_name(instance, name), + "searchKey": sprintf("google_compute_instance[%s].metadata", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_compute_instance", name, "metadata"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'google-logging-enabled' should be defined within metadata", + "keyActualValue": "'google-logging-enabled' is missing in metadata", + } +} + +# RULE 3: The key exists but its value is 'false'. +CxPolicy[result] { + doc := input.document[i] + instance := doc.resource.google_compute_instance[name] + + val := instance.metadata["google-logging-enabled"] + val == "false" + + result := { + "documentId": doc.id, + "resourceType": "google_compute_instance", + "resourceName": tf_lib.get_resource_name(instance, name), + "searchKey": sprintf("google_compute_instance[%s].metadata.google-logging-enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_compute_instance", name, "metadata"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'google-logging-enabled' should be set to 'true'", + "keyActualValue": "'google-logging-enabled' is set to 'false'", + } +} diff --git a/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/negative1.tf b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/negative1.tf new file mode 100644 index 00000000000..c93e52f1de6 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/negative1.tf @@ -0,0 +1,6 @@ +resource "google_compute_instance" "vm_ok" { + name = "instance-compliant" + metadata = { + "google-logging-enabled" = "true" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive1.tf b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive1.tf new file mode 100644 index 00000000000..50d669dfeca --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive1.tf @@ -0,0 +1,4 @@ +resource "google_compute_instance" "vm_fail_1" { + name = "instance-no-metadata" + machine_type = "e2-medium" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive2.tf b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive2.tf new file mode 100644 index 00000000000..0ec28c90e91 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive2.tf @@ -0,0 +1,6 @@ +resource "google_compute_instance" "vm_fail_2" { + name = "instance-no-flag" + metadata = { + foo = "bar" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive3.tf b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive3.tf new file mode 100644 index 00000000000..d2d93df0e9c --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive3.tf @@ -0,0 +1,6 @@ +resource "google_compute_instance" "vm_fail_3" { + name = "instance-disabled" + metadata = { + "google-logging-enabled" = "false" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..63e548efd22 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_compute_logging_service_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - GCP Compute Logging Service Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - GCP Compute Logging Service Disabled", + "severity": "MEDIUM", + "line": 3, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - GCP Compute Logging Service Disabled", + "severity": "MEDIUM", + "line": 3, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/metadata.json b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/metadata.json new file mode 100644 index 00000000000..98af937f545 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "8ef3f681-6ef1-4bbd-94bb-f9fe6d0626da", + "queryName": "Beta - GKE Default Service Account Used", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "Ensures that GKE clusters do not use the default Compute Engine Service Account. The default account has the 'Editor' role, granting nodes excessive write permissions to the project.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster#service_account", + "platform": "Terraform", + "descriptionID": "8ef3f681", + "cloudProvider": "gcp", + "cwe": "276", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/query.rego b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/query.rego new file mode 100644 index 00000000000..d0cc02fabf6 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/query.rego @@ -0,0 +1,42 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: Service Account missing in google_container_cluster. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_container_cluster[name] + + not resource.node_config.service_account + + result := { + "documentId": doc.id, + "resourceType": "google_container_cluster", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_cluster[%s].node_config", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_cluster", name, "node_config"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'service_account' should be explicitly defined in node_config", + "keyActualValue": "'service_account' is missing, defaulting to the Compute Engine default service account", + } +} + +# RULE 2: Service Account missing in google_container_node_pool. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_container_node_pool[name] + + not resource.node_config.service_account + + result := { + "documentId": doc.id, + "resourceType": "google_container_node_pool", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_node_pool[%s].node_config", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_node_pool", name, "node_config"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'service_account' should be explicitly defined in node_config", + "keyActualValue": "'service_account' is missing, defaulting to the Compute Engine default service account", + } +} diff --git a/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/negative1.tf b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/negative1.tf new file mode 100644 index 00000000000..7454d8cb1c4 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/negative1.tf @@ -0,0 +1,6 @@ +resource "google_container_cluster" "pass_cluster" { + name = "secure-cluster" + node_config { + service_account = "dedicated-sa@project.iam.gserviceaccount.com" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/positive1.tf b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/positive1.tf new file mode 100644 index 00000000000..82a8af9ec26 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/positive1.tf @@ -0,0 +1,9 @@ +resource "google_container_cluster" "fail_cluster" { + name = "insecure-cluster" + location = "us-central1" + + node_config { + machine_type = "e2-medium" + # FAIL: Missing service_account + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/positive2.tf b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/positive2.tf new file mode 100644 index 00000000000..79ed1af857b --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/positive2.tf @@ -0,0 +1,9 @@ +resource "google_container_node_pool" "fail_pool" { + name = "insecure-pool" + cluster = "some-cluster" + + node_config { + # FAIL: Missing service_account + preemptible = true + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/positive_expected_result.json new file mode 100644 index 00000000000..eaa3a7bdedc --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_default_service_account_used/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - GKE Default Service Account Used", + "severity": "HIGH", + "line": 5, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - GKE Default Service Account Used", + "severity": "HIGH", + "line": 5, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/metadata.json b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/metadata.json new file mode 100644 index 00000000000..7969806019e --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "e858428b-7650-4a62-a7d4-5b0d4c9afdd6", + "queryName": "Beta - GKE Metadata Server Disabled", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "Ensures that the GKE Metadata Server is enabled by setting 'workload_metadata_config.mode' to 'GKE_METADATA'. This prevents pods from accessing the sensitive Compute Engine metadata server and node credentials.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_node_pool#mode", + "platform": "Terraform", + "descriptionID": "e858428b", + "cloudProvider": "gcp", + "cwe": "284", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/query.rego b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/query.rego new file mode 100644 index 00000000000..abb38ab0db9 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/query.rego @@ -0,0 +1,80 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: 'workload_metadata_config' missing in google_container_cluster +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_container_cluster[name] + + not resource.node_config.workload_metadata_config + + result := { + "documentId": doc.id, + "resourceType": "google_container_cluster", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_cluster[%s].node_config", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_cluster", name, "node_config"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'workload_metadata_config' should be defined with mode 'GKE_METADATA'", + "keyActualValue": "'workload_metadata_config' is missing", + } +} + +# RULE 2: 'workload_metadata_config' missing in google_container_node_pool +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_container_node_pool[name] + + not resource.node_config.workload_metadata_config + + result := { + "documentId": doc.id, + "resourceType": "google_container_node_pool", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_node_pool[%s].node_config", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_node_pool", name, "node_config"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'workload_metadata_config' should be defined with mode 'GKE_METADATA'", + "keyActualValue": "'workload_metadata_config' is missing", + } +} + +# RULE 3: Incorrect mode in google_container_cluster +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_container_cluster[name] + + resource.node_config.workload_metadata_config.mode != "GKE_METADATA" + + result := { + "documentId": doc.id, + "resourceType": "google_container_cluster", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_cluster[%s].node_config.workload_metadata_config.mode", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_cluster", name, "node_config", "workload_metadata_config", "mode"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'mode' should be set to 'GKE_METADATA'", + "keyActualValue": sprintf("'mode' is set to '%s'", [resource.node_config.workload_metadata_config.mode]), + } +} + +# RULE 4: Incorrect mode in google_container_node_pool +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_container_node_pool[name] + + resource.node_config.workload_metadata_config.mode != "GKE_METADATA" + + result := { + "documentId": doc.id, + "resourceType": "google_container_node_pool", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_node_pool[%s].node_config.workload_metadata_config.mode", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_node_pool", name, "node_config", "workload_metadata_config", "mode"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'mode' should be set to 'GKE_METADATA'", + "keyActualValue": sprintf("'mode' is set to '%s'", [resource.node_config.workload_metadata_config.mode]), + } +} diff --git a/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/negative1.tf b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/negative1.tf new file mode 100644 index 00000000000..d9326a50b15 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/negative1.tf @@ -0,0 +1,8 @@ +resource "google_container_cluster" "pass_cluster" { + name = "secure-cluster" + node_config { + workload_metadata_config { + mode = "GKE_METADATA" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/negative2.tf b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/negative2.tf new file mode 100644 index 00000000000..ca108391fbc --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/negative2.tf @@ -0,0 +1,9 @@ +resource "google_container_node_pool" "pass_pool" { + name = "secure-pool" + cluster = "my-cluster" + node_config { + workload_metadata_config { + mode = "GKE_METADATA" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive1.tf b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive1.tf new file mode 100644 index 00000000000..6e9f0695dfa --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive1.tf @@ -0,0 +1,9 @@ +resource "google_container_cluster" "fail_cluster_no_block" { + name = "cluster-missing-metadata-config" + location = "us-central1" + + node_config { + machine_type = "e2-medium" + # FAIL: Does not exist workload_metadata_config + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive2.tf b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive2.tf new file mode 100644 index 00000000000..176e5b080a2 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive2.tf @@ -0,0 +1,9 @@ +resource "google_container_node_pool" "fail_pool_no_block" { + name = "pool-missing-metadata-config" + cluster = "my-cluster" + + node_config { + preemptible = true + # FAIL: Does not exist workload_metadata_config + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive3.tf b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive3.tf new file mode 100644 index 00000000000..f267aff7350 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive3.tf @@ -0,0 +1,9 @@ +resource "google_container_cluster" "fail_cluster_wrong_mode" { + name = "cluster-gce-mode" + + node_config { + workload_metadata_config { + mode = "GCE_METADATA" # FALLO: Debe ser GKE_METADATA + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive4.tf b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive4.tf new file mode 100644 index 00000000000..ea2bc5ced9d --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive4.tf @@ -0,0 +1,10 @@ +resource "google_container_node_pool" "fail_pool_wrong_mode" { + name = "pool-gce-mode" + cluster = "my-cluster" + + node_config { + workload_metadata_config { + mode = "GCE_METADATA" # FALLO: Debe ser GKE_METADATA + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..db46fc5bc4c --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_metadata_server_disabled/test/positive_expected_result.json @@ -0,0 +1,26 @@ +[ + { + "queryName": "Beta - GKE Metadata Server Disabled", + "severity": "HIGH", + "line": 5, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - GKE Metadata Server Disabled", + "severity": "HIGH", + "line": 5, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - GKE Metadata Server Disabled", + "severity": "HIGH", + "line": 6, + "fileName": "positive3.tf" + }, + { + "queryName": "Beta - GKE Metadata Server Disabled", + "severity": "HIGH", + "line": 7, + "fileName": "positive4.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/metadata.json b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/metadata.json new file mode 100644 index 00000000000..bca92c1a9ea --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "6c8a93a9-c0c5-4314-8ee6-3bcb3a0dbd70", + "queryName": "Beta - GKE Sandbox (gVisor) Disabled", + "severity": "LOW", + "category": "Insecure Configurations", + "descriptionText": "GKE Sandbox (gVisor) provides an extra layer of defense for untrusted workloads. It is not enabled on this Node Pool. Consider enabling it if this pool runs untrusted code (e.g., SaaS user scripts).", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_node_pool#sandbox_type", + "platform": "Terraform", + "descriptionID": "6c8a93a9", + "cloudProvider": "gcp", + "cwe": "1038", + "riskScore": "1.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/query.rego b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/query.rego new file mode 100644 index 00000000000..b1b287951b8 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/query.rego @@ -0,0 +1,80 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: 'sandbox_config' block missing in google_container_cluster. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_container_cluster[name] + + not resource.node_config.sandbox_config + + result := { + "documentId": doc.id, + "resourceType": "google_container_cluster", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_cluster[%s].node_config", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_cluster", name, "node_config"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'sandbox_config' should be defined with sandbox_type 'gvisor'", + "keyActualValue": "'sandbox_config' is missing", + } +} + +# RULE 2: 'sandbox_config' block missing in google_container_node_pool. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_container_node_pool[name] + + not resource.node_config.sandbox_config + + result := { + "documentId": doc.id, + "resourceType": "google_container_node_pool", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_node_pool[%s].node_config", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_node_pool", name, "node_config"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'sandbox_config' should be defined with sandbox_type 'gvisor'", + "keyActualValue": "'sandbox_config' is missing", + } +} + +# RULE 3: Incorrect sandbox_type in google_container_cluster. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_container_cluster[name] + + resource.node_config.sandbox_config.sandbox_type != "gvisor" + + result := { + "documentId": doc.id, + "resourceType": "google_container_cluster", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_cluster[%s].node_config.sandbox_config.sandbox_type", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_cluster", name, "node_config", "sandbox_config", "sandbox_type"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'sandbox_type' should be 'gvisor'", + "keyActualValue": sprintf("'sandbox_type' is set to '%s'", [resource.node_config.sandbox_config.sandbox_type]), + } +} + +# RULE 4: Incorrect sandbox_type in google_container_node_pool. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_container_node_pool[name] + + resource.node_config.sandbox_config.sandbox_type != "gvisor" + + result := { + "documentId": doc.id, + "resourceType": "google_container_node_pool", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_node_pool[%s].node_config.sandbox_config.sandbox_type", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_node_pool", name, "node_config", "sandbox_config", "sandbox_type"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'sandbox_type' should be 'gvisor'", + "keyActualValue": sprintf("'sandbox_type' is set to '%s'", [resource.node_config.sandbox_config.sandbox_type]), + } +} diff --git a/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/negative1.tf b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/negative1.tf new file mode 100644 index 00000000000..5a038181abf --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/negative1.tf @@ -0,0 +1,12 @@ +# Caso Negativo 1: Configuración correcta en un clúster principal +resource "google_container_cluster" "pass_cluster" { + name = "secure-main-cluster" + location = "us-central1" + + node_config { + image_type = "COS_CONTAINERD" + sandbox_config { + sandbox_type = "gvisor" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/negative2.tf b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/negative2.tf new file mode 100644 index 00000000000..9bc60d92ffd --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/negative2.tf @@ -0,0 +1,12 @@ +# Caso Negativo 2: Configuración correcta en un pool de nodos independiente +resource "google_container_node_pool" "pass_pool" { + name = "secure-untrusted-pool" + cluster = "my-cluster" + + node_config { + image_type = "COS_CONTAINERD" + sandbox_config { + sandbox_type = "gvisor" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive1.tf b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive1.tf new file mode 100644 index 00000000000..25e54824aec --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive1.tf @@ -0,0 +1,9 @@ +resource "google_container_cluster" "fail_cluster_no_sandbox" { + name = "cluster-fail" + location = "us-central1" + + node_config { + machine_type = "e2-medium" + # FAIL: Missing sandbox_config + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive2.tf b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive2.tf new file mode 100644 index 00000000000..9569727e33d --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive2.tf @@ -0,0 +1,9 @@ +resource "google_container_node_pool" "fail_pool_no_sandbox" { + name = "pool-fail" + cluster = "my-cluster" + + node_config { + machine_type = "e2-medium" + # FAIL: Missing sandbox_config + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive3.tf b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive3.tf new file mode 100644 index 00000000000..65f09f4728b --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive3.tf @@ -0,0 +1,9 @@ +resource "google_container_cluster" "fail_cluster_wrong_type" { + name = "cluster-wrong-type" + + node_config { + sandbox_config { + sandbox_type = "other_runtime" # FALLO: Debe ser gvisor + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive4.tf b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive4.tf new file mode 100644 index 00000000000..f5c36d3aa4d --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive4.tf @@ -0,0 +1,10 @@ +resource "google_container_node_pool" "fail_pool_wrong_type" { + name = "pool-wrong-type" + cluster = "my-cluster" + + node_config { + sandbox_config { + sandbox_type = "disabled" # FALLO: Debe ser gvisor + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..74f648f06f4 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_sandbox_disabled/test/positive_expected_result.json @@ -0,0 +1,26 @@ +[ + { + "queryName": "Beta - GKE Sandbox (gVisor) Disabled", + "severity": "LOW", + "line": 5, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - GKE Sandbox (gVisor) Disabled", + "severity": "LOW", + "line": 5, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - GKE Sandbox (gVisor) Disabled", + "severity": "LOW", + "line": 6, + "fileName": "positive3.tf" + }, + { + "queryName": "Beta - GKE Sandbox (gVisor) Disabled", + "severity": "LOW", + "line": 7, + "fileName": "positive4.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/metadata.json b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/metadata.json new file mode 100644 index 00000000000..975d2c9ecbb --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "83d1c89f-e07c-4748-9d96-51a78c49637c", + "queryName": "Beta - GKE Secrets Not Encrypted with CMEK", + "severity": "HIGH", + "category": "Encryption", + "descriptionText": "Ensures that GKE clusters have Application-Layer Secrets Encryption enabled using a Cloud KMS key. This protects sensitive data in etcd with a customer-managed encryption key.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster#database_encryption", + "platform": "Terraform", + "descriptionID": "83d1c89f", + "cloudProvider": "gcp", + "cwe": "312", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/query.rego b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/query.rego new file mode 100644 index 00000000000..f9fea0a3595 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/query.rego @@ -0,0 +1,65 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: 'database_encryption' block missing. +CxPolicy[result] { + doc := input.document[i] + cluster := doc.resource.google_container_cluster[name] + + not cluster.database_encryption + + result := { + "documentId": doc.id, + "resourceType": "google_container_cluster", + "resourceName": tf_lib.get_resource_name(cluster, name), + "searchKey": sprintf("google_container_cluster[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_cluster", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'database_encryption' block should be defined", + "keyActualValue": "'database_encryption' block is missing", + } +} + +# RULE 2: Incorrect encryption state (DECRYPTED). +CxPolicy[result] { + doc := input.document[i] + cluster := doc.resource.google_container_cluster[name] + + cluster.database_encryption.state != "ENCRYPTED" + + result := { + "documentId": doc.id, + "resourceType": "google_container_cluster", + "resourceName": tf_lib.get_resource_name(cluster, name), + "searchKey": sprintf("google_container_cluster[%s].database_encryption.state", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_cluster", name, "database_encryption", "state"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'state' should be set to 'ENCRYPTED'", + "keyActualValue": sprintf("'state' is set to '%s'", [cluster.database_encryption.state]), + } +} + +# RULE 3: State is ENCRYPTED but key name (key_name) is missing. +# searchLine points to database_encryption block since key_name is absent. +CxPolicy[result] { + doc := input.document[i] + cluster := doc.resource.google_container_cluster[name] + + cluster.database_encryption.state == "ENCRYPTED" + + key_name := object.get(cluster.database_encryption, "key_name", "") + key_name == "" + + result := { + "documentId": doc.id, + "resourceType": "google_container_cluster", + "resourceName": tf_lib.get_resource_name(cluster, name), + "searchKey": sprintf("google_container_cluster[%s].database_encryption.key_name", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_cluster", name, "database_encryption"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'key_name' should be defined with a valid KMS key ID", + "keyActualValue": "'key_name' is missing or empty", + } +} diff --git a/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/negative1.tf b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/negative1.tf new file mode 100644 index 00000000000..5db9fa37360 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/negative1.tf @@ -0,0 +1,8 @@ +resource "google_container_cluster" "pass_encrypted" { + name = "secure-cluster" + + database_encryption { + state = "ENCRYPTED" + key_name = "projects/p1/locations/global/keyRings/r1/cryptoKeys/k1" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive1.tf b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive1.tf new file mode 100644 index 00000000000..cf8323e01d6 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive1.tf @@ -0,0 +1,5 @@ +resource "google_container_cluster" "fail_missing_block" { + name = "no-encryption-block" + location = "us-central1" + # FAIL: database_encryption Does not exist +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive2.tf b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive2.tf new file mode 100644 index 00000000000..3de90ed7944 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive2.tf @@ -0,0 +1,8 @@ +resource "google_container_cluster" "fail_decrypted" { + name = "explicit-decrypted" + + database_encryption { + state = "DECRYPTED" # FALLO: Debe ser ENCRYPTED + key_name = "" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive3.tf b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive3.tf new file mode 100644 index 00000000000..bea1afe8339 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive3.tf @@ -0,0 +1,8 @@ +resource "google_container_cluster" "fail_missing_key" { + name = "encrypted-no-key" + + # FAIL: Missing key_name + database_encryption { + state = "ENCRYPTED" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..547afcd982b --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_gke_secrets_encryption_cmek_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - GKE Secrets Not Encrypted with CMEK", + "severity": "HIGH", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - GKE Secrets Not Encrypted with CMEK", + "severity": "HIGH", + "line": 5, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - GKE Secrets Not Encrypted with CMEK", + "severity": "HIGH", + "line": 5, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/metadata.json b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/metadata.json new file mode 100644 index 00000000000..129d02817c7 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "0ef3f3f1-0592-475b-b073-81ac1ac869e2", + "queryName": "Beta - GCP HTTP(S) Load Balancer Logging Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that logging is enabled for Google Compute Backend Services used in HTTP(S) Load Balancers. Access logs are essential for monitoring traffic patterns, latency, and identifying potential security threats.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_backend_service#log_config", + "platform": "Terraform", + "descriptionID": "0ef3f3f1", + "cloudProvider": "gcp", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/query.rego b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/query.rego new file mode 100644 index 00000000000..1cd6ba774ef --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/query.rego @@ -0,0 +1,42 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: Bloque 'log_config' missing. +CxPolicy[result] { + doc := input.document[i] + bs := doc.resource.google_compute_backend_service[name] + + not bs.log_config + + result := { + "documentId": doc.id, + "resourceType": "google_compute_backend_service", + "resourceName": tf_lib.get_resource_name(bs, name), + "searchKey": sprintf("google_compute_backend_service[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_compute_backend_service", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "log_config should be defined with enable set to true", + "keyActualValue": "log_config is missing", + } +} + +# RULE 2: Bloque 'log_config' Exists but 'enable' es false. +CxPolicy[result] { + doc := input.document[i] + bs := doc.resource.google_compute_backend_service[name] + + bs.log_config.enable == false + + result := { + "documentId": doc.id, + "resourceType": "google_compute_backend_service", + "resourceName": tf_lib.get_resource_name(bs, name), + "searchKey": sprintf("google_compute_backend_service[%s].log_config.enable", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_compute_backend_service", name, "log_config", "enable"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "log_config.enable should be set to true", + "keyActualValue": "log_config.enable is set to false", + } +} diff --git a/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/negative1.tf b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..66e55cbbf8c --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/negative1.tf @@ -0,0 +1,8 @@ +resource "google_compute_backend_service" "pass_logs" { + name = "backend-with-logs" + + log_config { + enable = true + sample_rate = 1.0 + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/positive1.tf b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..a6b59c65cd7 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/positive1.tf @@ -0,0 +1,5 @@ +resource "google_compute_backend_service" "fail_missing_log" { + name = "backend-no-logs" + protocol = "HTTP" + # FAIL: No tiene bloque log_config +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/positive2.tf b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/positive2.tf new file mode 100644 index 00000000000..38412e41a49 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/positive2.tf @@ -0,0 +1,7 @@ +resource "google_compute_backend_service" "fail_disabled_log" { + name = "backend-disabled-logs" + + log_config { + enable = false # FALLO: Debe ser true + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..4226138823e --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_http_load_balancer_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - GCP HTTP(S) Load Balancer Logging Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - GCP HTTP(S) Load Balancer Logging Disabled", + "severity": "MEDIUM", + "line": 5, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/metadata.json b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/metadata.json new file mode 100644 index 00000000000..1b2b541a26c --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "3f1222a9-0161-4fe8-b188-ccabaf7a0ba5", + "queryName": "Beta - IAP Disabled on Backend Service", + "severity": "MEDIUM", + "category": "Access Control", + "descriptionText": "Ensures that Identity-Aware Proxy (IAP) is enabled for Google Compute Backend Services. IAP verifies user identity and context before allowing access to applications.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_backend_service#iap", + "platform": "Terraform", + "descriptionID": "3f1222a9", + "cloudProvider": "gcp", + "cwe": "284", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/query.rego b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/query.rego new file mode 100644 index 00000000000..6bd226bd2b4 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/query.rego @@ -0,0 +1,63 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +# RULE 1: The block 'iap' is missing en google_compute_backend_service. +CxPolicy[result] { + doc := input.document[i] + bs := doc.resource.google_compute_backend_service[name] + + not bs.iap + + result := { + "documentId": doc.id, + "resourceType": "google_compute_backend_service", + "resourceName": tf_lib.get_resource_name(bs, name), + "searchKey": sprintf("google_compute_backend_service[%s]", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_compute_backend_service", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'google_compute_backend_service.%s' should have an 'iap' block defined", [name]), + "keyActualValue": sprintf("'google_compute_backend_service.%s' is missing the 'iap' block", [name]), + } +} + +# RULE 2: The block 'iap' Exists but Missing 'oauth2_client_id'. +CxPolicy[result] { + doc := input.document[i] + bs := doc.resource.google_compute_backend_service[name] + + bs.iap + not bs.iap.oauth2_client_id + + result := { + "documentId": doc.id, + "resourceType": "google_compute_backend_service", + "resourceName": tf_lib.get_resource_name(bs, name), + "searchKey": sprintf("google_compute_backend_service[%s].iap", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_compute_backend_service", name, "iap"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'oauth2_client_id' should be defined within the 'iap' block", + "keyActualValue": "'oauth2_client_id' is missing", + } +} + +# RULE 3: The block 'iap' Exists but Missing 'oauth2_client_secret'. +CxPolicy[result] { + doc := input.document[i] + bs := doc.resource.google_compute_backend_service[name] + + bs.iap + not bs.iap.oauth2_client_secret + + result := { + "documentId": doc.id, + "resourceType": "google_compute_backend_service", + "resourceName": tf_lib.get_resource_name(bs, name), + "searchKey": sprintf("google_compute_backend_service[%s].iap", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_compute_backend_service", name, "iap"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'oauth2_client_secret' should be defined within the 'iap' block", + "keyActualValue": "'oauth2_client_secret' is missing", + } +} diff --git a/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/negative1.tf b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/negative1.tf new file mode 100644 index 00000000000..1d225647a1a --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/negative1.tf @@ -0,0 +1,8 @@ +resource "google_compute_backend_service" "pass_secure" { + name = "fully-secure-service" + + iap { + oauth2_client_id = "client-id" + oauth2_client_secret = "secret-val" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive1.tf b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive1.tf new file mode 100644 index 00000000000..054d5dd2c75 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive1.tf @@ -0,0 +1,4 @@ +resource "google_compute_backend_service" "fail_no_iap" { + name = "no-iap-service" + health_checks = ["check-id"] +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive2.tf b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive2.tf new file mode 100644 index 00000000000..a83e39a3ac1 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive2.tf @@ -0,0 +1,6 @@ +resource "google_compute_backend_service" "fail_no_id" { + name = "no-client-id" + iap { + oauth2_client_secret = "some-secret" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive3.tf b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive3.tf new file mode 100644 index 00000000000..33eb97d6db4 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive3.tf @@ -0,0 +1,6 @@ +resource "google_compute_backend_service" "fail_no_secret" { + name = "no-client-secret" + iap { + oauth2_client_id = "some-id" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..ab14a7e30e6 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_iap_backend_service_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - IAP Disabled on Backend Service", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - IAP Disabled on Backend Service", + "severity": "MEDIUM", + "line": 3, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - IAP Disabled on Backend Service", + "severity": "MEDIUM", + "line": 3, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/metadata.json b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/metadata.json new file mode 100644 index 00000000000..ca06ed59e94 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "80d98d7b-2f12-4a8c-8cc9-fd17ca19c569", + "queryName": "Beta - Cloud SQL PostgreSQL log_error_verbosity is Verbose", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that the 'log_error_verbosity' flag for Cloud SQL PostgreSQL instances is set to 'default' or 'terse'. Setting it to 'verbose' generates excessive logs containing internal source code details, which can be a security risk.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database_instance", + "platform": "Terraform", + "descriptionID": "80d98d7b", + "cloudProvider": "gcp", + "cwe": "532", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/query.rego b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/query.rego new file mode 100644 index 00000000000..bbae6e1880e --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/query.rego @@ -0,0 +1,31 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +ensure_array(x) = x { is_array(x) } +ensure_array(x) = [x] { is_object(x) } + +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_sql_database_instance[name] + + contains(resource.database_version, "POSTGRES") + + flags := ensure_array(resource.settings.database_flags) + flag := flags[j] + + flag.name == "log_error_verbosity" + lower(flag.value) == "verbose" + + result := { + "documentId": doc.id, + "resourceType": "google_sql_database_instance", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_sql_database_instance[%s].settings.database_flags", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_sql_database_instance", name, "settings", "database_flags"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'log_error_verbosity' should be set to 'default' or 'terse'", + "keyActualValue": sprintf("'log_error_verbosity' is set to '%s'", [flag.value]), + } +} diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/negative1.tf b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/negative1.tf new file mode 100644 index 00000000000..7d68bad7488 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/negative1.tf @@ -0,0 +1,11 @@ +resource "google_sql_database_instance" "pass" { + name = "postgres-ok" + database_version = "POSTGRES_14" + + settings { + database_flags { + name = "log_error_verbosity" + value = "default" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/positive1.tf b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/positive1.tf new file mode 100644 index 00000000000..78036f66a5e --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/positive1.tf @@ -0,0 +1,12 @@ +resource "google_sql_database_instance" "fail_single" { + name = "postgres-single-flag" + database_version = "POSTGRES_14" + + settings { + tier = "db-f1-micro" + database_flags { + name = "log_error_verbosity" + value = "verbose" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/positive2.tf b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/positive2.tf new file mode 100644 index 00000000000..d8092b0e90f --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/positive2.tf @@ -0,0 +1,18 @@ +resource "google_sql_database_instance" "fail_multiple" { + name = "postgres-multiple-flags" + database_version = "POSTGRES_14" + + settings { + tier = "db-f1-micro" + + database_flags { + name = "log_connections" + value = "on" + } + + database_flags { + name = "log_error_verbosity" + value = "verbose" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/positive_expected_result.json new file mode 100644 index 00000000000..84103c5fd5a --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_error_verbosity_verbose/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Cloud SQL PostgreSQL log_error_verbosity is Verbose", + "severity": "MEDIUM", + "line": 7, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Cloud SQL PostgreSQL log_error_verbosity is Verbose", + "severity": "MEDIUM", + "line": 8, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/metadata.json b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/metadata.json new file mode 100644 index 00000000000..e9fc93024a9 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "ede2d9e4-d3a6-4751-a89b-561bc9bacbe4", + "queryName": "Beta - Cloud SQL PostgreSQL log_statement Improperly Set", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that the 'log_statement' database flag for Cloud SQL PostgreSQL instances is set to 'ddl', 'mod', or 'all'. The default value is 'none', which disables statement logging and hinders auditing capability.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database_instance", + "platform": "Terraform", + "descriptionID": "ede2d9e4", + "cloudProvider": "gcp", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/query.rego b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/query.rego new file mode 100644 index 00000000000..12cf6d49f05 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/query.rego @@ -0,0 +1,58 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +ensure_array(x) = x { is_array(x) } +ensure_array(x) = [x] { is_object(x) } + +has_log_statement(flags_list) { + flag := flags_list[_] + flag.name == "log_statement" +} + +# RULE 1: The 'log_statement' flag is not defined (Missing). +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_sql_database_instance[name] + + contains(resource.database_version, "POSTGRES") + + flags_list := ensure_array(resource.settings.database_flags) + not has_log_statement(flags_list) + + result := { + "documentId": doc.id, + "resourceType": "google_sql_database_instance", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_sql_database_instance[%s].settings.database_flags", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_sql_database_instance", name, "settings", "database_flags"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'database_flags' should include 'log_statement' set to 'ddl', 'mod', or 'all'", + "keyActualValue": "'log_statement' flag is missing (defaults to 'none')", + } +} + +# RULE 2: The 'log_statement' flag exists but is set to 'none'. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.google_sql_database_instance[name] + + contains(resource.database_version, "POSTGRES") + + flags_list := ensure_array(resource.settings.database_flags) + flag := flags_list[_] + flag.name == "log_statement" + lower(flag.value) == "none" + + result := { + "documentId": doc.id, + "resourceType": "google_sql_database_instance", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_sql_database_instance[%s].settings.database_flags", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_sql_database_instance", name, "settings", "database_flags"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'log_statement' should be set to 'ddl', 'mod', or 'all'", + "keyActualValue": "'log_statement' is set to 'none'", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/negative1.tf b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/negative1.tf new file mode 100644 index 00000000000..f399c7b136c --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/negative1.tf @@ -0,0 +1,11 @@ +# Caso Pass: Valor DDL +resource "google_sql_database_instance" "pass_ddl" { + name = "postgres-ok-ddl" + database_version = "POSTGRES_13" + settings { + database_flags { + name = "log_statement" + value = "ddl" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/positive1.tf b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/positive1.tf new file mode 100644 index 00000000000..3816f570485 --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/positive1.tf @@ -0,0 +1,12 @@ +resource "google_sql_database_instance" "fail_missing_flag" { + name = "postgres-no-audit" + database_version = "POSTGRES_13" + + settings { + tier = "db-f1-micro" + database_flags { + name = "log_connections" + value = "on" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/positive2.tf b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/positive2.tf new file mode 100644 index 00000000000..74e7691edcc --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/positive2.tf @@ -0,0 +1,11 @@ +resource "google_sql_database_instance" "fail_explicit_none" { + name = "postgres-none-audit" + database_version = "POSTGRES_14" + + settings { + database_flags { + name = "log_statement" + value = "none" # FALLO + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/positive_expected_result.json b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/positive_expected_result.json new file mode 100644 index 00000000000..e0cd0a3e3cc --- /dev/null +++ b/assets/queries/terraform/gcp/gcp_sql_postgresql_log_statement_improperly_set/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Cloud SQL PostgreSQL log_statement Improperly Set", + "severity": "MEDIUM", + "line": 7, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Cloud SQL PostgreSQL log_statement Improperly Set", + "severity": "MEDIUM", + "line": 6, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/metadata.json b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/metadata.json new file mode 100644 index 00000000000..70c4e2a8bb7 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "479bbdfd-342a-4121-8844-7387eea26d96", + "queryName": "Beta - Activity Tracker for Global Events Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that an IBM Cloud Activity Tracker instance is provisioned to capture IAM and other global account events. For compliance and security, at least one instance must be configured in a region that supports receiving global events.", + "descriptionUrl": "https://cloud.ibm.com/docs/activity-tracker?topic=activity-tracker-global-events", + "platform": "Terraform", + "descriptionID": "479bbdfd", + "cloudProvider": "ibm", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/query.rego b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/query.rego new file mode 100644 index 00000000000..559a1197297 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/query.rego @@ -0,0 +1,44 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: No ibm_resource_instance for activity-tracker exists in the configuration. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.ibm + + all_activity_trackers := [tracker | + tracker := input.document[_].resource.ibm_resource_instance[_] + tracker.service == "activity-tracker" + ] + + count(all_activity_trackers) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.ibm", + "searchLine": common_lib.build_search_line(["provider", "ibm"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'ibm_resource_instance' with service='activity-tracker' should exist", + "keyActualValue": "No 'ibm_resource_instance' for service 'activity-tracker' was found", + } +} + +# RULE 2: An activity-tracker instance is in a region that does not support global events. +CxPolicy[result] { + global_event_regions := {"eu-de", "eu-gb", "us-south", "au-syd"} + + tracker := input.document[i].resource.ibm_resource_instance[tracker_name] + tracker.service == "activity-tracker" + + not global_event_regions[tracker.location] + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.ibm_resource_instance.%s.location", [tracker_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_resource_instance", tracker_name, "location"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "Activity Tracker 'location' should be a global event region (e.g., 'eu-de', 'us-south')", + "keyActualValue": sprintf("Activity Tracker 'location' is '%s', which is not a global event region", [tracker.location]), + } +} diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/negative1.tf b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/negative1.tf new file mode 100644 index 00000000000..2854dd08df0 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/negative1.tf @@ -0,0 +1,10 @@ +provider "ibm" { + region = "us-south" +} + +resource "ibm_resource_instance" "tracker_correct" { + name = "global-tracker" + service = "activity-tracker" + plan = "7-day" + location = "us-south" # CORRECTO: Región de eventos globales (Dallas) +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/positive1.tf b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/positive1.tf new file mode 100644 index 00000000000..92c27402faf --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/positive1.tf @@ -0,0 +1,7 @@ +provider "ibm" { + region = "us-south" +} + +resource "ibm_is_vpc" "example_vpc" { + name = "production-vpc" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/positive2.tf b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/positive2.tf new file mode 100644 index 00000000000..ca284ef7a48 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/positive2.tf @@ -0,0 +1,10 @@ +provider "ibm" { + region = "us-south" +} + +resource "ibm_resource_instance" "tracker_wrong_location" { + name = "regional-tracker" + service = "activity-tracker" + plan = "lite" + location = "us-east" # FALLO: No soporta eventos globales +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..b7a53df26d0 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_global_events_disabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Activity Tracker for Global Events Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Activity Tracker for Global Events Disabled", + "severity": "MEDIUM", + "line": 9, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/metadata.json b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/metadata.json new file mode 100644 index 00000000000..bb5f26ee9c8 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "ccf65fe5-85f2-414d-b68e-b5101bc4bc03", + "queryName": "Beta - Activity Tracker Platform Logs Disabled", + "severity": "HIGH", + "category": "Observability", + "descriptionText": "Ensures that the IBM Cloud Activity Tracker instance is configured to receive platform logs. Capturing platform management events is critical for security auditing, threat detection, and compliance.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/resource_instance#platform_logs", + "platform": "Terraform", + "descriptionID": "ccf65fe5", + "cloudProvider": "ibm", + "cwe": "778", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/query.rego b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/query.rego new file mode 100644 index 00000000000..c3604949d36 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/query.rego @@ -0,0 +1,59 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: No ibm_resource_instance for activity-tracker exists in the configuration. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.ibm + + all_activity_trackers := [tracker | + tracker := input.document[_].resource.ibm_resource_instance[_] + tracker.service == "activity-tracker" + ] + + count(all_activity_trackers) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.ibm", + "searchLine": common_lib.build_search_line(["provider", "ibm"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'ibm_resource_instance' with service='activity-tracker' should exist", + "keyActualValue": "No 'ibm_resource_instance' for service 'activity-tracker' was found", + } +} + +# RULE 2: The 'platform_logs' attribute is missing from the Activity Tracker instance. +CxPolicy[result] { + tracker := input.document[i].resource.ibm_resource_instance[tracker_name] + tracker.service == "activity-tracker" + + object.get(tracker, "platform_logs", null) == null + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.ibm_resource_instance.%s", [tracker_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_resource_instance", tracker_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'platform_logs' attribute should be present and set to 'true'", + "keyActualValue": "'platform_logs' attribute is missing and defaults to 'false'", + } +} + +# RULE 3: The 'platform_logs' attribute is explicitly set to 'false'. +CxPolicy[result] { + tracker := input.document[i].resource.ibm_resource_instance[tracker_name] + tracker.service == "activity-tracker" + + tracker.platform_logs == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.ibm_resource_instance.%s.platform_logs", [tracker_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_resource_instance", tracker_name, "platform_logs"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'platform_logs' attribute should be 'true'", + "keyActualValue": "'platform_logs' attribute is 'false'", + } +} diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/negative1.tf b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/negative1.tf new file mode 100644 index 00000000000..5500054ac55 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/negative1.tf @@ -0,0 +1,11 @@ +provider "ibm" { + region = "eu-de" +} + +resource "ibm_resource_instance" "tracker_compliant" { + name = "activity-tracker-secure" + service = "activity-tracker" + plan = "7-day" + location = "eu-de" + platform_logs = true +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive1.tf b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive1.tf new file mode 100644 index 00000000000..341279abc94 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive1.tf @@ -0,0 +1,8 @@ +provider "ibm" { + region = "eu-de" +} + +# No hay activity-tracker +resource "ibm_is_vpc" "vpc_test" { + name = "test-vpc" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive2.tf b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive2.tf new file mode 100644 index 00000000000..4bf73552723 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive2.tf @@ -0,0 +1,11 @@ +provider "ibm" { + region = "eu-de" +} + +resource "ibm_resource_instance" "tracker_no_attr" { + name = "activity-tracker-missing" + service = "activity-tracker" + plan = "lite" + location = "eu-de" + # FAIL: Missing platform_logs +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive3.tf b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive3.tf new file mode 100644 index 00000000000..3064b260dd5 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive3.tf @@ -0,0 +1,13 @@ +provider "ibm" { + region = "eu-de" +} + +resource "ibm_resource_instance" "tracker_disabled" { + name = "activity-tracker-off" + service = "activity-tracker" + plan = "lite" + location = "eu-de" + + # FAIL: Deshabilitado explícitamente + platform_logs = false +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..63b7313f3d1 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_activity_tracker_platform_logs_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Activity Tracker Platform Logs Disabled", + "severity": "HIGH", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Activity Tracker Platform Logs Disabled", + "severity": "HIGH", + "line": 5, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Activity Tracker Platform Logs Disabled", + "severity": "HIGH", + "line": 12, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/metadata.json b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/metadata.json new file mode 100644 index 00000000000..2faa7ff758a --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "6797782b-0523-4419-9297-1747e1bf82ab", + "queryName": "Beta - Certificate Manager Auto Renew Disabled", + "severity": "MEDIUM", + "category": "Best Practices", + "descriptionText": "Ensures that certificates managed by IBM Cloud Certificate Manager are configured to renew automatically before expiration. Disabling auto-renewal can lead to service disruptions and security risks due to expired certificates.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/cm_certificate#auto_renew_enabled", + "platform": "Terraform", + "descriptionID": "6797782b", + "cloudProvider": "ibm", + "cwe": "320", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/query.rego b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/query.rego new file mode 100644 index 00000000000..04df8a67194 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/query.rego @@ -0,0 +1,37 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: The 'auto_renew_enabled' attribute is missing en el certificado. +CxPolicy[result] { + doc := input.document[i] + certificate := doc.resource.ibm_cm_certificate[cert_name] + + object.get(certificate, "auto_renew_enabled", null) == null + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_cm_certificate.%s", [cert_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_cm_certificate", cert_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'auto_renew_enabled' should be present and set to 'true'", + "keyActualValue": "'auto_renew_enabled' is missing and defaults to 'false'", + } +} + +# RULE 2: The attribute 'auto_renew_enabled' está explícitamente configurado como 'false'. +CxPolicy[result] { + doc := input.document[i] + certificate := doc.resource.ibm_cm_certificate[cert_name] + + certificate.auto_renew_enabled == false + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_cm_certificate.%s.auto_renew_enabled", [cert_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_cm_certificate", cert_name, "auto_renew_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'auto_renew_enabled' attribute should be 'true'", + "keyActualValue": "'auto_renew_enabled' attribute is 'false'", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/negative1.tf b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/negative1.tf new file mode 100644 index 00000000000..093011f11df --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/negative1.tf @@ -0,0 +1,8 @@ +resource "ibm_cm_certificate" "cert_compliant" { + instance_id = "crn:v1:bluemix:public:cloudcerts:..." + name = "cert-compliant" + label = "secure" + + # PASS + auto_renew_enabled = true +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/positive1.tf b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/positive1.tf new file mode 100644 index 00000000000..039efe5432c --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/positive1.tf @@ -0,0 +1,6 @@ +resource "ibm_cm_certificate" "cert_missing_attr" { + instance_id = "crn:v1:bluemix:public:cloudcerts:..." + name = "cert-missing" + label = "test" + # FAIL: Missing auto_renew_enabled +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/positive2.tf b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/positive2.tf new file mode 100644 index 00000000000..1518beba8a7 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/positive2.tf @@ -0,0 +1,8 @@ +resource "ibm_cm_certificate" "cert_disabled" { + instance_id = "crn:v1:bluemix:public:cloudcerts:..." + name = "cert-disabled" + label = "test" + + # FAIL: Deshabilitado explícitamente + auto_renew_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..9ab1e9a9c1c --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_certificate_manager_auto_renew_disabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Certificate Manager Auto Renew Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Certificate Manager Auto Renew Disabled", + "severity": "MEDIUM", + "line": 7, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/metadata.json b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/metadata.json new file mode 100644 index 00000000000..80f3266dee0 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "124e18ee-0b42-474d-8623-185f108fa545", + "queryName": "Beta - IBM CIS DNS Record Not Proxied (Manual)", + "severity": "INFO", + "category": "Availability", + "descriptionText": "The DNS record in IBM Cloud Internet Services (CIS) is not proxied. DDoS protection, WAF, and CDN features are only active when 'proxied' is set to 'true'. Verify if this record points to a web workload that requires DDoS mitigation.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/cis_dns_record", + "platform": "Terraform", + "descriptionID": "124e18ee", + "cloudProvider": "ibm", + "cwe": "770", + "riskScore": 0.0, + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/query.rego b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/query.rego new file mode 100644 index 00000000000..d144203fcab --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/query.rego @@ -0,0 +1,37 @@ +package Cx + +import data.generic.common as common_lib + +# CASE 1: The attribute 'proxied' está explícitamente en false. +CxPolicy[result] { + doc := input.document[i] + record := doc.resource.ibm_cis_dns_record[name] + + record.proxied == false + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_cis_dns_record.%s.proxied", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_cis_dns_record", name, "proxied"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'proxied' should be set to 'true' to enable DDoS protection", + "keyActualValue": "'proxied' is set to 'false'", + } +} + +# CASE 2: The attribute 'proxied' Missing (por defecto es false). +CxPolicy[result] { + doc := input.document[i] + record := doc.resource.ibm_cis_dns_record[name] + + object.get(record, "proxied", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_cis_dns_record.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_cis_dns_record", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'proxied' should be defined and set to 'true' to enable DDoS protection", + "keyActualValue": "'proxied' is missing (DNS resolution only, no DDoS protection)", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/negative1.tf b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/negative1.tf new file mode 100644 index 00000000000..43bb8e92417 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/negative1.tf @@ -0,0 +1,10 @@ +resource "ibm_cis_dns_record" "dns_record_secure" { + cis_id = "crn:v1:bluemix:public:internet-svcs:..." + domain_id = "example-id" + name = "www" + type = "A" + content = "1.2.3.4" + + # PASS: Bajo el escudo de CIS + proxied = true +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/positive1.tf b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/positive1.tf new file mode 100644 index 00000000000..212f69b7952 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/positive1.tf @@ -0,0 +1,10 @@ +resource "ibm_cis_dns_record" "dns_record_false" { + cis_id = "crn:v1:bluemix:public:internet-svcs:..." + domain_id = "example-id" + name = "test" + type = "A" + content = "192.168.1.1" + + # FAIL: Deshabilitado explícitamente + proxied = false +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/positive2.tf b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/positive2.tf new file mode 100644 index 00000000000..9de55a90331 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/positive2.tf @@ -0,0 +1,8 @@ +resource "ibm_cis_dns_record" "dns_record_missing" { + cis_id = "crn:v1:bluemix:public:internet-svcs:..." + domain_id = "example-id" + name = "test-missing" + type = "A" + content = "192.168.1.2" + # FAIL: Missing The attribute proxied +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..96feab4cb1e --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_dns_not_proxied_manual/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - IBM CIS DNS Record Not Proxied (Manual)", + "severity": "INFO", + "line": 9, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - IBM CIS DNS Record Not Proxied (Manual)", + "severity": "INFO", + "line": 1, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/metadata.json b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/metadata.json new file mode 100644 index 00000000000..9738ed595dd --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "1e0c94f5-6178-4601-b33e-a86d8aa20bd6", + "queryName": "Beta - IBM CIS WAF Not Enabled (Manual)", + "severity": "INFO", + "category": "Networking and Firewall", + "descriptionText": "The Web Application Firewall (WAF) is not explicitly set to 'on' in the IBM Cloud Internet Services (CIS) domain settings. WAF is a critical compensatory control for protecting web workloads. Manual verification is required to ensure WAF is enabled and rules are properly tuned.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/cis_domain_settings", + "platform": "Terraform", + "descriptionID": "1e0c94f5", + "cloudProvider": "ibm", + "cwe": "1021", + "riskScore": 0.0, + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/query.rego b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/query.rego new file mode 100644 index 00000000000..937fc312224 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/query.rego @@ -0,0 +1,38 @@ +package Cx + +import data.generic.common as common_lib + +# CASE 1: The attribute 'waf' Exists but tiene un valor incorrecto (ej. "off"). +CxPolicy[result] { + doc := input.document[i] + settings := doc.resource.ibm_cis_domain_settings[name] + + settings.waf + settings.waf != "on" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_cis_domain_settings.%s.waf", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_cis_domain_settings", name, "waf"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'waf' attribute should be set to 'on'", + "keyActualValue": sprintf("'waf' attribute is set to '%s'", [settings.waf]), + } +} + +# CASE 2: The attribute 'waf' Does not exist en el resource (valor por defecto varía por plan, se requiere definición explícita). +CxPolicy[result] { + doc := input.document[i] + settings := doc.resource.ibm_cis_domain_settings[name] + + not settings.waf + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_cis_domain_settings.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_cis_domain_settings", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'waf' attribute should be defined and set to 'on'", + "keyActualValue": "'waf' attribute is missing", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/negative1.tf b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/negative1.tf new file mode 100644 index 00000000000..eb2779c7f58 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/negative1.tf @@ -0,0 +1,7 @@ +resource "ibm_cis_domain_settings" "settings_secure" { + cis_id = "crn:v1:bluemix:public:internet-svcs:..." + domain_id = "example-id" + + # PASS: WAF habilitado + waf = "on" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/positive1.tf b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/positive1.tf new file mode 100644 index 00000000000..cdc74d69831 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/positive1.tf @@ -0,0 +1,7 @@ +resource "ibm_cis_domain_settings" "settings_off" { + cis_id = "crn:v1:bluemix:public:internet-svcs:..." + domain_id = "example-id" + + # FAIL: WAF desactivado + waf = "off" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/positive2.tf b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/positive2.tf new file mode 100644 index 00000000000..6ca054580ee --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/positive2.tf @@ -0,0 +1,6 @@ +resource "ibm_cis_domain_settings" "settings_missing" { + cis_id = "crn:v1:bluemix:public:internet-svcs:..." + domain_id = "example-id" + # FAIL: Missing The attribute waf + ssl = "full" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..418d2a226b1 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cis_waf_enabled_manual/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - IBM CIS WAF Not Enabled (Manual)", + "severity": "INFO", + "line": 6, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - IBM CIS WAF Not Enabled (Manual)", + "severity": "INFO", + "line": 1, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/metadata.json b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/metadata.json new file mode 100644 index 00000000000..c0cf5257209 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "601ca2bb-20a9-4113-83b3-866a3d1a2a67", + "queryName": "Beta - IBM Cloudant Encryption with CMK (Manual)", + "severity": "INFO", + "category": "Encryption", + "descriptionText": "The IBM Cloudant instance (provisioned via 'ibm_resource_instance') does not appear to have Customer Managed Encryption configured. To enable CMK/BYOK, the 'parameters' block must contain the Key Protect root key CRN (usually 'key_protect_key'). Manual verification is required.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/resource_instance", + "platform": "Terraform", + "descriptionID": "601ca2bb", + "cloudProvider": "ibm", + "cwe": "312", + "riskScore": 0.0, + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/query.rego b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/query.rego new file mode 100644 index 00000000000..1e145200c0c --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/query.rego @@ -0,0 +1,40 @@ +package Cx + +import data.generic.common as common_lib + +# CASE 1: The block 'parameters' is missing. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.ibm_resource_instance[name] + resource.service == "cloudantnosqldb" + + object.get(resource, "parameters", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_resource_instance.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_resource_instance", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "Cloudant instance should have a 'parameters' block containing 'key_protect_key'", + "keyActualValue": "The 'parameters' block is missing (using default encryption)", + } +} + +# CASE 2: The block 'parameters' Exists but Missing 'key_protect_key'. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.ibm_resource_instance[name] + resource.service == "cloudantnosqldb" + + resource.parameters + object.get(resource.parameters, "key_protect_key", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_resource_instance.%s.parameters", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_resource_instance", name, "parameters"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "Cloudant 'parameters' should contain 'key_protect_key' with a valid Key Protect CRN", + "keyActualValue": "'key_protect_key' is missing within the parameters map", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/negative1.tf b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/negative1.tf new file mode 100644 index 00000000000..6f7be642193 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/negative1.tf @@ -0,0 +1,10 @@ +resource "ibm_resource_instance" "cloudant_secure" { + name = "cloudant-secure" + service = "cloudantnosqldb" + plan = "standard" + location = "us-south" + + parameters = { + key_protect_key = "crn:v1:bluemix:public:kms:us-south:a/test:test:key:test" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/positive1.tf b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/positive1.tf new file mode 100644 index 00000000000..31bb0b041db --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/positive1.tf @@ -0,0 +1,6 @@ +resource "ibm_resource_instance" "cloudant_no_params" { + name = "cloudant-no-params" + service = "cloudantnosqldb" + plan = "standard" + location = "us-south" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/positive2.tf b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/positive2.tf new file mode 100644 index 00000000000..48239b22f62 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/positive2.tf @@ -0,0 +1,10 @@ +resource "ibm_resource_instance" "cloudant_partial_params" { + name = "cloudant-partial" + service = "cloudantnosqldb" + plan = "standard" + location = "us-south" + + parameters = { + "db_type" = "nosql" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..3b1b66c50ea --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_cloudant_cmk_encryption_manual/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - IBM Cloudant Encryption with CMK (Manual)", + "severity": "INFO", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - IBM Cloudant Encryption with CMK (Manual)", + "severity": "INFO", + "line": 7, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/metadata.json b/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/metadata.json new file mode 100644 index 00000000000..61b79e76258 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "d2fc37af-3cd9-4830-b8a0-a3eda8fba0ed", + "queryName": "Beta - IBM Cluster Entitlement Key Missing (Automated)", + "severity": "INFO", + "category": "Supply-Chain", + "descriptionText": "The IBM Cloud Kubernetes Service cluster does not have an 'entitlement' key configured. This argument automates the creation of image pull secrets for the IBM Entitled Registry. If using IBM Cloud Paks or entitled software, this should be defined to ensure secure image pull access.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/container_cluster#entitlement", + "platform": "Terraform", + "descriptionID": "d2fc37af", + "cloudProvider": "ibm", + "cwe": "284", + "riskScore": 0.0, + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/query.rego b/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/query.rego new file mode 100644 index 00000000000..127fa1c341a --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/query.rego @@ -0,0 +1,20 @@ +package Cx + +import data.generic.common as common_lib + +# REGLA: Verificar si el cluster tiene configurada la clave de "entitlement". +CxPolicy[result] { + doc := input.document[i] + cluster := doc.resource.ibm_container_cluster[name] + + object.get(cluster, "entitlement", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_container_cluster.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_container_cluster", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'entitlement' attribute should be defined for clusters running IBM Entitled Software", + "keyActualValue": "'entitlement' attribute is missing", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/test/negative1.tf b/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/test/negative1.tf new file mode 100644 index 00000000000..e7e1f546581 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/test/negative1.tf @@ -0,0 +1,15 @@ +provider "ibm" { + region = "us-south" +} + +resource "ibm_container_cluster" "cluster_with_entitlement" { + name = "secure-cluster" + datacenter = "dal10" + machine_type = "b3c.4x16" + hardware = "shared" + public_vlan_id = "123" + private_vlan_id = "456" + + # PASS: Se automatiza el secreto de descarga + entitlement = "my-entitlement-key-crn" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/test/positive1.tf b/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/test/positive1.tf new file mode 100644 index 00000000000..de1b402967b --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/test/positive1.tf @@ -0,0 +1,13 @@ +provider "ibm" { + region = "us-south" +} + +resource "ibm_container_cluster" "cluster_without_entitlement" { + name = "test-cluster" + datacenter = "dal10" + machine_type = "b3c.4x16" + hardware = "shared" + public_vlan_id = "123" + private_vlan_id = "456" + # FAIL: No se define 'entitlement' +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/test/positive_expected_result.json new file mode 100644 index 00000000000..2c92f4ce39e --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_container_cluster_entitlement_check/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - IBM Cluster Entitlement Key Missing (Automated)", + "severity": "INFO", + "line": 5, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/metadata.json b/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/metadata.json new file mode 100644 index 00000000000..8606b13d73c --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "92440d0d-889c-49c5-a0e6-13c5f326b872", + "queryName": "Beta - IBM Container Registry VA Alerts Missing (Automated)", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that IBM Kubernetes Service (IKS) clusters have a Vulnerability Advisor notification resource (ibm_container_va_notification) configured, enabling container vulnerability alerts from IBM Cloud Container Registry.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/container_cluster", + "platform": "Terraform", + "descriptionID": "92440d0d", + "cloudProvider": "ibm", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/query.rego b/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/query.rego new file mode 100644 index 00000000000..231fc9e7254 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/query.rego @@ -0,0 +1,22 @@ +package Cx + +import data.generic.common as common_lib + +# REGLA: Verificar si el cluster de Kubernetes tiene configuradas las alertas de vulnerabilidad. +CxPolicy[result] { + doc := input.document[i] + cluster := doc.resource.ibm_container_cluster[name] + + va_notifications := [n | n := input.document[_].resource.ibm_container_va_notification[_]] + + count(va_notifications) == 0 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_container_cluster.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_container_cluster", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "A 'ibm_container_va_notification' resource should be defined to alert on image vulnerabilities", + "keyActualValue": "Vulnerability Advisor notifications are missing for this cluster environment", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/test/negative1.tf b/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/test/negative1.tf new file mode 100644 index 00000000000..caacafa3fa9 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/test/negative1.tf @@ -0,0 +1,18 @@ +provider "ibm" { + region = "us-south" +} + +resource "ibm_container_cluster" "secure_cluster" { + name = "secure-setup" + datacenter = "dal10" + machine_type = "b3c.4x16" + hardware = "shared" + public_vlan_id = "123" + private_vlan_id = "456" +} + +# PASS: Se define la notificación para el VA +resource "ibm_container_va_notification" "alerts" { + cluster_id = ibm_container_cluster.secure_cluster.id + email = "admin@example.com" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/test/positive1.tf b/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/test/positive1.tf new file mode 100644 index 00000000000..cf172e3a86f --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/test/positive1.tf @@ -0,0 +1,13 @@ +provider "ibm" { + region = "us-south" +} + +resource "ibm_container_cluster" "cluster_without_va" { + name = "vulnerable-setup" + datacenter = "dal10" + machine_type = "b3c.4x16" + hardware = "shared" + public_vlan_id = "123" + private_vlan_id = "456" + # FAIL: Does not exist un ibm_container_va_notification en el documento +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..d18a405c254 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_container_registry_va_alerts_missing/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - IBM Container Registry VA Alerts Missing (Automated)", + "severity": "MEDIUM", + "line": 5, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/metadata.json b/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/metadata.json new file mode 100644 index 00000000000..d90015ea0e6 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "c973475c-4797-42b1-8cee-4038c050283e", + "queryName": "Beta - IBM Cloud Database Without CMK (Manual)", + "severity": "INFO", + "category": "Encryption", + "descriptionText": "The IBM Cloud Database instance is not configured with a Customer Managed Key (CMK). It relies on default provider-managed encryption. Verify if the data sensitivity requires Bring Your Own Key (BYOK) or Keep Your Own Key (KYOK) via the 'key_protect_key' argument.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/database", + "platform": "Terraform", + "descriptionID": "c973475c", + "cloudProvider": "ibm", + "cwe": "312", + "riskScore": 0.0, + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/query.rego b/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/query.rego new file mode 100644 index 00000000000..86be8b519b0 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/query.rego @@ -0,0 +1,20 @@ +package Cx + +import data.generic.common as common_lib + +# CASE 1: Base de datos sin 'key_protect_key'. +CxPolicy[result] { + doc := input.document[i] + db := doc.resource.ibm_database[name] + + object.get(db, "key_protect_key", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_database.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_database", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'key_protect_key' attribute should be defined with a Key Protect/HPCS CRN", + "keyActualValue": "'key_protect_key' is missing (using default provider-managed encryption)", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/test/negative1.tf b/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/test/negative1.tf new file mode 100644 index 00000000000..924676e68d7 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/test/negative1.tf @@ -0,0 +1,13 @@ +provider "ibm" { + region = "us-south" +} + +resource "ibm_database" "db_secure" { + name = "db-protected" + service = "databases-for-postgresql" + plan = "standard" + location = "us-south" + + # PASS: Se define la clave de cifrado del cliente + key_protect_key = "crn:v1:bluemix:public:kms:us-south:a/test:test:key:test" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/test/positive1.tf b/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/test/positive1.tf new file mode 100644 index 00000000000..c4cfe72e017 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/test/positive1.tf @@ -0,0 +1,11 @@ +provider "ibm" { + region = "us-south" +} + +resource "ibm_database" "db_insecure" { + name = "db-standard" + service = "databases-for-mongodb" + plan = "standard" + location = "us-south" + # FAIL: Missing The attribute key_protect_key +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..6dc7d21c79e --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_database_cmk_encryption_manual/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - IBM Cloud Database Without CMK (Manual)", + "severity": "INFO", + "line": 5, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/metadata.json b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/metadata.json new file mode 100644 index 00000000000..4fd8870a814 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "61d76d70-0021-45c7-855d-c8c5c3116877", + "queryName": "Beta - IBM Account IP Restrictions (Manual)", + "severity": "INFO", + "category": "Networking and Firewall", + "descriptionText": "The account global settings do not enforce IP address restrictions. The 'allowed_ip_addresses' attribute should be configured to restrict login access (including the account owner) to trusted networks only.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_account_settings", + "platform": "Terraform", + "descriptionID": "61d76d70", + "cloudProvider": "ibm", + "cwe": "284", + "riskScore": 0.0, + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/query.rego b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/query.rego new file mode 100644 index 00000000000..912b2f40a06 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/query.rego @@ -0,0 +1,38 @@ +package Cx + +import data.generic.common as common_lib + +# CASE 1: Restricciones de IP no configuradas (Atributo missing). +CxPolicy[result] { + doc := input.document[i] + settings := doc.resource.ibm_iam_account_settings[name] + + object.get(settings, "allowed_ip_addresses", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_iam_account_settings.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_iam_account_settings", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'allowed_ip_addresses' should be defined with a list of trusted IPs", + "keyActualValue": "'allowed_ip_addresses' is missing (access allowed from anywhere)", + } +} + +# CASE 2: Restricciones definidas but lista vacía. +CxPolicy[result] { + doc := input.document[i] + settings := doc.resource.ibm_iam_account_settings[name] + + ips := settings.allowed_ip_addresses + count(ips) == 0 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_iam_account_settings.%s.allowed_ip_addresses", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_iam_account_settings", name, "allowed_ip_addresses"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'allowed_ip_addresses' should contain at least one trusted IP/Subnet", + "keyActualValue": "'allowed_ip_addresses' is empty", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/negative1.tf b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/negative1.tf new file mode 100644 index 00000000000..96939b1d2a8 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/negative1.tf @@ -0,0 +1,7 @@ +resource "ibm_iam_account_settings" "settings_secure" { + mfa = "TOTP" + allowed_ip_addresses = [ + "10.1.2.3", + "192.168.1.0/24" + ] +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/positive1.tf b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/positive1.tf new file mode 100644 index 00000000000..253da211578 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/positive1.tf @@ -0,0 +1,4 @@ +resource "ibm_iam_account_settings" "settings_no_ips" { + mfa = "TOTP" + session_expiration_in_seconds = 3600 +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/positive2.tf b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/positive2.tf new file mode 100644 index 00000000000..f3bbafbe44e --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/positive2.tf @@ -0,0 +1,3 @@ +resource "ibm_iam_account_settings" "settings_empty_ips" { + allowed_ip_addresses = [] +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..c01495e6203 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_ip_restrictions_manual/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - IBM Account IP Restrictions (Manual)", + "severity": "INFO", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - IBM Account IP Restrictions (Manual)", + "severity": "INFO", + "line": 1, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/metadata.json b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/metadata.json new file mode 100644 index 00000000000..0c198b17ae8 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "ebaef99e-4add-440e-8270-caeb718e7c93", + "queryName": "Beta - Account Level MFA Is Not Enforced", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "Enforces Multi-Factor Authentication (MFA) at the account level. Requiring MFA is a fundamental security practice to prevent unauthorized access resulting from compromised credentials.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_account_settings#mfa", + "platform": "Terraform", + "descriptionID": "ebaef99e", + "cloudProvider": "ibm", + "cwe": "308", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/query.rego b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/query.rego new file mode 100644 index 00000000000..45d3fea1cae --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/query.rego @@ -0,0 +1,55 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: No ibm_iam_account_settings resource exists in the configuration. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.ibm + + all_iam_settings := [settings | + settings := input.document[_].resource.ibm_iam_account_settings[_] + ] + + count(all_iam_settings) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.ibm", + "searchLine": common_lib.build_search_line(["provider", "ibm"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "Resource 'ibm_iam_account_settings' should exist to enforce MFA", + "keyActualValue": "Resource 'ibm_iam_account_settings' is missing in this IBM configuration", + } +} + +# RULE 2: The 'mfa' attribute is missing from the resource. +CxPolicy[result] { + settings := input.document[i].resource.ibm_iam_account_settings[settings_name] + object.get(settings, "mfa", null) == null + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.ibm_iam_account_settings.%s", [settings_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_iam_account_settings", settings_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'mfa' attribute should be present and set to 'LEVEL2' or 'LEVEL3'", + "keyActualValue": "'mfa' attribute is missing", + } +} + +# RULE 3: The 'mfa' attribute is set to an insecure value ('NONE' or 'LEVEL1'). +CxPolicy[result] { + settings := input.document[i].resource.ibm_iam_account_settings[settings_name] + insecure_mfa_levels := {"NONE", "LEVEL1"} + insecure_mfa_levels[settings.mfa] + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.ibm_iam_account_settings.%s.mfa", [settings_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_iam_account_settings", settings_name, "mfa"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'mfa' attribute should be 'LEVEL2' or 'LEVEL3'", + "keyActualValue": sprintf("'mfa' attribute is set to '%s'", [settings.mfa]), + } +} diff --git a/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/negative1.tf b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/negative1.tf new file mode 100644 index 00000000000..f37bc112e9f --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/negative1.tf @@ -0,0 +1,4 @@ +resource "ibm_iam_account_settings" "iam_secure" { + # PASS: LEVEL2 es seguro + mfa = "LEVEL2" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive1.tf b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive1.tf new file mode 100644 index 00000000000..34c0e3bb7e1 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive1.tf @@ -0,0 +1,8 @@ +provider "ibm" { + region = "us-south" +} + +# Solo recursos de infraestructura, nada de IAM settings +resource "ibm_is_vpc" "example" { + name = "test-vpc" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive2.tf b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive2.tf new file mode 100644 index 00000000000..8b49fe44bae --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive2.tf @@ -0,0 +1,4 @@ +resource "ibm_iam_account_settings" "iam_no_mfa" { + # Missing The attribute mfa + session_expiration_in_seconds = 3600 +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive3.tf b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive3.tf new file mode 100644 index 00000000000..7713e1ce6c1 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive3.tf @@ -0,0 +1,4 @@ +resource "ibm_iam_account_settings" "iam_weak_mfa" { + # FAIL: LEVEL1 no es suficientemente seguro + mfa = "LEVEL1" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..2b80f04faed --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_account_mfa_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Account Level MFA Is Not Enforced", + "severity": "HIGH", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Account Level MFA Is Not Enforced", + "severity": "HIGH", + "line": 1, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Account Level MFA Is Not Enforced", + "severity": "HIGH", + "line": 3, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/metadata.json b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/metadata.json new file mode 100644 index 00000000000..225cfb71711 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "3d6db2d6-fa2d-41df-a783-d3dee76f717f", + "queryName": "Beta - IBM Account Session Expiration Too Long", + "severity": "MEDIUM", + "category": "Access Control", + "descriptionText": "Ensures that the IBM Cloud account session expiration is configured to a secure limit (e.g., 3600 seconds / 1 hour). Long session timeouts increase the risk of session hijacking.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/iam_account_settings", + "platform": "Terraform", + "descriptionID": "3d6db2d6", + "cloudProvider": "ibm", + "cwe": "613", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/query.rego b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/query.rego new file mode 100644 index 00000000000..89f23ea7753 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/query.rego @@ -0,0 +1,39 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: Missing The attribute 'session_expiration_in_seconds'. +CxPolicy[result] { + doc := input.document[i] + settings := doc.resource.ibm_iam_account_settings[name] + + object.get(settings, "session_expiration_in_seconds", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_iam_account_settings.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_iam_account_settings", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'session_expiration_in_seconds' should be defined and set to 3600 (1 hour) or less", + "keyActualValue": "'session_expiration_in_seconds' is missing (using insecure default)", + } +} + +# RULE 2: La sesión dura más de 1 hora (3600 segundos). +CxPolicy[result] { + doc := input.document[i] + settings := doc.resource.ibm_iam_account_settings[name] + + expiration := to_number(settings.session_expiration_in_seconds) + + expiration > 3600 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_iam_account_settings.%s.session_expiration_in_seconds", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_iam_account_settings", name, "session_expiration_in_seconds"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'session_expiration_in_seconds' should be <= 3600", + "keyActualValue": sprintf("'session_expiration_in_seconds' is set to '%v'", [expiration]), + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/negative1.tf b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/negative1.tf new file mode 100644 index 00000000000..bb5077c463f --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/negative1.tf @@ -0,0 +1,4 @@ +resource "ibm_iam_account_settings" "settings_secure" { + mfa = "LEVEL2" + session_expiration_in_seconds = 3600 # 1 Hora +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/positive1.tf b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/positive1.tf new file mode 100644 index 00000000000..2fe6df87104 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/positive1.tf @@ -0,0 +1,3 @@ +resource "ibm_iam_account_settings" "settings_missing_attr" { + mfa = "LEVEL2" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/positive2.tf b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/positive2.tf new file mode 100644 index 00000000000..b12be11ded2 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/positive2.tf @@ -0,0 +1,3 @@ +resource "ibm_iam_account_settings" "settings_too_long" { + session_expiration_in_seconds = 86400 # 24 Horas +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/positive_expected_result.json new file mode 100644 index 00000000000..fb2df50cd07 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iam_session_expiration_too_long/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - IBM Account Session Expiration Too Long", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - IBM Account Session Expiration Too Long", + "severity": "MEDIUM", + "line": 2, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/metadata.json b/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/metadata.json new file mode 100644 index 00000000000..2c21269ef43 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "e55a0224-47b8-41a8-8cc1-8c8581e13a8d", + "queryName": "Beta - IKS Cluster Logging Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that every IBM Cloud Kubernetes Service (IKS) cluster has an associated logging service configuration. Centralized logging is essential for troubleshooting applications, monitoring cluster activity, and performing security analysis.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/ob_logging_config", + "platform": "Terraform", + "descriptionID": "e55a0224", + "cloudProvider": "ibm", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/query.rego b/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/query.rego new file mode 100644 index 00000000000..11ec4f64393 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/query.rego @@ -0,0 +1,24 @@ +package Cx + +import data.generic.common as common_lib + +CxPolicy[result] { + doc := input.document[i] + _ := doc.resource.ibm_container_cluster[cluster_name] + + matching_configs := [config | + config := input.document[_].resource.ibm_ob_logging_config[_] + contains(config.scope, cluster_name) + ] + + count(matching_configs) == 0 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_container_cluster.%s", [cluster_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_container_cluster", cluster_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "ibm_container_cluster should have an associated ibm_ob_logging_config resource", + "keyActualValue": "ibm_container_cluster does not have an associated ibm_ob_logging_config resource", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/test/negative1.tf b/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..38a35202e55 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/test/negative1.tf @@ -0,0 +1,13 @@ +resource "ibm_container_cluster" "secure_cluster" { + name = "compliant-cluster" + datacenter = "dal10" + machine_type = "b3c.4x16" + hardware = "shared" + public_vlan_id = "123" + private_vlan_id = "456" +} + +resource "ibm_ob_logging_config" "logging_ok" { + scope = ibm_container_cluster.secure_cluster.crn + instance = "crn:v1:bluemix:public:logdna:..." +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/test/positive1.tf b/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..c940fb487ab --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/test/positive1.tf @@ -0,0 +1,8 @@ +resource "ibm_container_cluster" "cluster_without_logs" { + name = "vulnerable-cluster" + datacenter = "dal10" + machine_type = "b3c.4x16" + hardware = "shared" + public_vlan_id = "123" + private_vlan_id = "456" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..5e5afe68105 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iks_cluster_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - IKS Cluster Logging Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/metadata.json b/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/metadata.json new file mode 100644 index 00000000000..2393102c4a2 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "51bac252-399d-49d0-bfa5-0bd52194fed9", + "queryName": "Beta - IKS Cluster Monitoring Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that every IBM Cloud Kubernetes Service (IKS) cluster has an associated monitoring service configuration. Monitoring is essential for observing cluster health, performance, and resource utilization.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/ob_monitoring_config", + "platform": "Terraform", + "descriptionID": "51bac252", + "cloudProvider": "ibm", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/query.rego b/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/query.rego new file mode 100644 index 00000000000..3d67f2f6865 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/query.rego @@ -0,0 +1,25 @@ +package Cx + +import data.generic.common as common_lib + +# REGLA: Detectar clústeres de IKS sin configuración de monitorización asociada. +CxPolicy[result] { + doc := input.document[i] + _ := doc.resource.ibm_container_cluster[cluster_name] + + matching_configs := [config | + config := input.document[_].resource.ibm_ob_monitoring_config[_] + contains(config.scope, cluster_name) + ] + + count(matching_configs) == 0 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_container_cluster.%s", [cluster_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_container_cluster", cluster_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "ibm_container_cluster should have an associated ibm_ob_monitoring_config resource", + "keyActualValue": "ibm_container_cluster does not have an associated ibm_ob_monitoring_config resource", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/test/negative1.tf b/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/test/negative1.tf new file mode 100644 index 00000000000..51f8af06e4f --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/test/negative1.tf @@ -0,0 +1,11 @@ +resource "ibm_container_cluster" "aks_with_monitoring" { + name = "visible-cluster" + datacenter = "dal10" + machine_type = "b3c.4x16" + hardware = "shared" +} + +resource "ibm_ob_monitoring_config" "monitoring_ok" { + scope = ibm_container_cluster.aks_with_monitoring.crn + instance = "crn:v1:bluemix:public:sysdig-monitor:..." +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/test/positive1.tf b/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/test/positive1.tf new file mode 100644 index 00000000000..7e884c04be2 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/test/positive1.tf @@ -0,0 +1,8 @@ +resource "ibm_container_cluster" "aks_no_monitoring" { + name = "blind-cluster" + datacenter = "dal10" + machine_type = "b3c.4x16" + hardware = "shared" + public_vlan_id = "vlan1" + private_vlan_id = "vlan2" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..37d0c8f249d --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_iks_cluster_monitoring_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - IKS Cluster Monitoring Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/metadata.json b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/metadata.json new file mode 100644 index 00000000000..4c484059d05 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "86f1560c-74cb-48a3-ad5a-7da25cab4d03", + "queryName": "Beta - IBM Instance OS Disk Encryption (Manual)", + "severity": "INFO", + "category": "Encryption", + "descriptionText": "The Virtual Server Instance boot volume (OS Disk) relies on default provider-managed encryption. It is not configured with 'boot_volume.encryption', which is required for Customer Managed Keys (CMK), BYOK, or KYOK.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/is_instance", + "platform": "Terraform", + "descriptionID": "86f1560c", + "cloudProvider": "ibm", + "cwe": "312", + "riskScore": 0.0, + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/query.rego b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/query.rego new file mode 100644 index 00000000000..8a4faa7db54 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/query.rego @@ -0,0 +1,43 @@ +package Cx + +import data.generic.common as common_lib + +ensure_array(x) = x { is_array(x) } +ensure_array(x) = [x] { not is_array(x) } + +# CASE 1: Bloque 'boot_volume' totalmente missing. +CxPolicy[result] { + doc := input.document[i] + instance := doc.resource.ibm_is_instance[name] + + not instance.boot_volume + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_is_instance.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_is_instance", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'boot_volume' block should be defined with 'encryption' set to a CRN", + "keyActualValue": "'boot_volume' block is missing (using default encryption)", + } +} + +# CASE 2: Bloque 'boot_volume' presente, but Missing 'encryption'. +CxPolicy[result] { + doc := input.document[i] + instance := doc.resource.ibm_is_instance[name] + + boot_volumes := ensure_array(instance.boot_volume) + boot_vol := boot_volumes[_] + + object.get(boot_vol, "encryption", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_is_instance.%s.boot_volume", [name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_is_instance", name, "boot_volume"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'boot_volume.encryption' attribute should be defined with a Key Protect/HPCS CRN", + "keyActualValue": "'encryption' is missing in boot_volume (using default encryption)", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/negative1.tf b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/negative1.tf new file mode 100644 index 00000000000..7d0952a2787 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/negative1.tf @@ -0,0 +1,10 @@ +resource "ibm_is_instance" "vsi_secure" { + name = "vsi-secure" + image = "r006-12345678" + profile = "bx2-2x8" + + boot_volume { + name = "boot-disk-cmk" + encryption = "crn:v1:bluemix:public:kms:us-south:a/test:test:key:test" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/positive1.tf b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/positive1.tf new file mode 100644 index 00000000000..717981e213b --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/positive1.tf @@ -0,0 +1,8 @@ +resource "ibm_is_instance" "vsi_no_boot_block" { + name = "vsi-default" + image = "r006-12345678" + profile = "bx2-2x8" + vpc = "vpc-id" + zone = "us-south-1" + keys = ["key-id"] +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/positive2.tf b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/positive2.tf new file mode 100644 index 00000000000..a685f31398d --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/positive2.tf @@ -0,0 +1,10 @@ +resource "ibm_is_instance" "vsi_no_encryption" { + name = "vsi-unprotected-boot" + image = "r006-12345678" + profile = "bx2-2x8" + + boot_volume { + name = "boot-disk-standard" + # FAIL: Missing atributo encryption + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..d2f4f30fe32 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_instance_os_disk_encryption_manual/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - IBM Instance OS Disk Encryption (Manual)", + "severity": "INFO", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - IBM Instance OS Disk Encryption (Manual)", + "severity": "INFO", + "line": 6, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/metadata.json b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/metadata.json new file mode 100644 index 00000000000..f6aa17272b3 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "cdaf415c-d1b9-4d15-8e6f-e27165b49d55", + "queryName": "Beta - KMS Key Rotation Disabled", + "severity": "HIGH", + "category": "Encryption", + "descriptionText": "Ensures that cryptographic keys in IBM Key Protect have an automated rotation policy enabled. Rotating keys periodically limits the amount of data exposed if a single key is compromised.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/kms_key#rotation_policy", + "platform": "Terraform", + "descriptionID": "cdaf415c", + "cloudProvider": "ibm", + "cwe": "320", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/query.rego b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/query.rego new file mode 100644 index 00000000000..21030a30386 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/query.rego @@ -0,0 +1,52 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: The 'rotation_policy' block is completely missing. +CxPolicy[result] { + key := input.document[i].resource.ibm_kms_key[key_name] + + not key.rotation_policy + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.ibm_kms_key.%s", [key_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_kms_key", key_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'rotation_policy' block should be present and configured", + "keyActualValue": "'rotation_policy' block is missing", + } +} + +# RULE 2: The block 'rotation_policy' Exists, but le Missing 'rotation_interval_month'. +CxPolicy[result] { + key := input.document[i].resource.ibm_kms_key[key_name] + + policy := key.rotation_policy + not policy.rotation_interval_month + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.ibm_kms_key.%s.rotation_policy", [key_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_kms_key", key_name, "rotation_policy"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'rotation_interval_month' should be defined within the rotation policy", + "keyActualValue": "'rotation_interval_month' is missing", + } +} + +# RULE 3: 'rotation_interval_month' está explícitamente configurado como 0. +CxPolicy[result] { + key := input.document[i].resource.ibm_kms_key[key_name] + + key.rotation_policy.rotation_interval_month == 0 + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.ibm_kms_key.%s.rotation_policy.rotation_interval_month", [key_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_kms_key", key_name, "rotation_policy", "rotation_interval_month"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'rotation_interval_month' should be a value between 1 and 12", + "keyActualValue": "'rotation_interval_month' is set to 0, disabling rotation", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/negative1.tf b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/negative1.tf new file mode 100644 index 00000000000..a15ea09d425 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/negative1.tf @@ -0,0 +1,9 @@ +resource "ibm_kms_key" "key_secure" { + instance_id = "guid-123" + key_name = "key-compliant" + standard_key = false + + rotation_policy { + rotation_interval_month = 12 + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive1.tf b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive1.tf new file mode 100644 index 00000000000..25da7b1e36f --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive1.tf @@ -0,0 +1,5 @@ +resource "ibm_kms_key" "key_no_policy" { + instance_id = "guid-123" + key_name = "key-fails-1" + standard_key = false +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive2.tf b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive2.tf new file mode 100644 index 00000000000..e9c5d7000c5 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive2.tf @@ -0,0 +1,9 @@ +resource "ibm_kms_key" "key_empty_policy" { + instance_id = "guid-123" + key_name = "key-fails-2" + standard_key = false + + rotation_policy { + # Missing rotation_interval_month + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive3.tf b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive3.tf new file mode 100644 index 00000000000..a7d95812dbf --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive3.tf @@ -0,0 +1,9 @@ +resource "ibm_kms_key" "key_zero_policy" { + instance_id = "guid-123" + key_name = "key-fails-3" + standard_key = false + + rotation_policy { + rotation_interval_month = 0 + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..5cc6a8f7111 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_kms_key_rotation_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - KMS Key Rotation Disabled", + "severity": "HIGH", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - KMS Key Rotation Disabled", + "severity": "HIGH", + "line": 6, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - KMS Key Rotation Disabled", + "severity": "HIGH", + "line": 7, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/metadata.json b/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/metadata.json new file mode 100644 index 00000000000..32db0a7adcf --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "54c5b041-1902-4e19-b414-5473f1b349d2", + "queryName": "Beta - LogDNA Archiving Is Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that every IBM LogDNA instance has archiving configured through an 'ibm_logdna_archive' resource. Archiving is critical for long-term log retention, compliance, and forensic analysis.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/logdna_archive", + "platform": "Terraform", + "descriptionID": "54c5b041", + "cloudProvider": "ibm", + "cwe": "223", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/query.rego b/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/query.rego new file mode 100644 index 00000000000..f96f09c586b --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/query.rego @@ -0,0 +1,25 @@ +package Cx + +import data.generic.common as common_lib + +# REGLA: Detectar instancias de LogDNA que no tienen un resource de archivado asociado. +CxPolicy[result] { + doc := input.document[i] + _ := doc.resource.ibm_logdna_instance[instance_name] + + matching_archives := [archive | + archive := input.document[_].resource.ibm_logdna_archive[_] + contains(archive.instance_id, instance_name) + ] + + count(matching_archives) == 0 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_logdna_instance.%s", [instance_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_logdna_instance", instance_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "ibm_logdna_instance should have an associated ibm_logdna_archive resource", + "keyActualValue": "ibm_logdna_instance does not have an associated ibm_logdna_archive resource", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/test/negative1.tf b/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/test/negative1.tf new file mode 100644 index 00000000000..9fb2f0f9d87 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/test/negative1.tf @@ -0,0 +1,15 @@ +resource "ibm_logdna_instance" "logs_with_archive" { + name = "logs-with-archiver" + plan = "lite" + target_resource_instance_id = "crn:v1:bluemix:public:logdna:..." +} + +resource "ibm_logdna_archive" "archiver_ok" { + instance_id = ibm_logdna_instance.logs_with_archive.id + cos { + api_key = "secret" + bucket = "my-bucket" + endpoint = "s3.us-south.cloud-object-storage.appdomain.cloud" + instance_crn = "crn:v1:bluemix:public:cloud-object-storage:..." + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/test/positive1.tf b/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/test/positive1.tf new file mode 100644 index 00000000000..15d09051bac --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/test/positive1.tf @@ -0,0 +1,5 @@ +resource "ibm_logdna_instance" "logs_without_archive" { + name = "logs-only-test" + plan = "lite" + target_resource_instance_id = "crn:v1:bluemix:public:logdna:..." +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..24e3d4673cc --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_logdna_archiving_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - LogDNA Archiving Is Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/metadata.json b/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/metadata.json new file mode 100644 index 00000000000..fc0f70b59ef --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "a5107e68-028f-4b4d-9fc0-f23e3b73e448", + "queryName": "Beta - LogDNA View Without Alert", + "severity": "LOW", + "category": "Observability", + "descriptionText": "Ensures that every IBM LogDNA custom view ('ibm_logdna_view') has at least one associated alert ('ibm_logdna_alert'). Views are created to filter critical events, and without an alert, notifications for these events will not be sent.", + "descriptionUrl": "https://registry.terraform.io/providers/IBM-Cloud/ibm/latest/docs/resources/logdna_alert", + "platform": "Terraform", + "descriptionID": "a5107e68", + "cloudProvider": "ibm", + "cwe": "778", + "riskScore": "1.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/query.rego b/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/query.rego new file mode 100644 index 00000000000..987301ed89f --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/query.rego @@ -0,0 +1,25 @@ +package Cx + +import data.generic.common as common_lib + +# REGLA: Detectar vistas de LogDNA que no tienen NO alerta asociada. +CxPolicy[result] { + doc := input.document[i] + view := doc.resource.ibm_logdna_view[view_name] + + matching_alerts := [alert | + alert := input.document[_].resource.ibm_logdna_alert[_] + contains(alert.view, view_name) + ] + + count(matching_alerts) == 0 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.ibm_logdna_view.%s", [view_name]), + "searchLine": common_lib.build_search_line(["resource", "ibm_logdna_view", view_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "ibm_logdna_view should have an associated ibm_logdna_alert", + "keyActualValue": "ibm_logdna_view does not have an associated ibm_logdna_alert", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/test/negative1.tf b/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/test/negative1.tf new file mode 100644 index 00000000000..8518c7b77c2 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/test/negative1.tf @@ -0,0 +1,15 @@ +resource "ibm_logdna_view" "audit_view" { + name = "Audit-Events" + query = "service:iam" +} + +resource "ibm_logdna_alert" "audit_alert" { + name = "Audit-Alert-Policy" + view = ibm_logdna_view.audit_view.name + + notification_channel { + email { + recipients = ["admin@example.com"] + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/test/positive1.tf b/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/test/positive1.tf new file mode 100644 index 00000000000..2dc3d3ec911 --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/test/positive1.tf @@ -0,0 +1,4 @@ +resource "ibm_logdna_view" "error_view" { + name = "Critical-Errors-Only" + query = "level:error" +} \ No newline at end of file diff --git a/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/test/positive_expected_result.json b/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/test/positive_expected_result.json new file mode 100644 index 00000000000..24661cc514d --- /dev/null +++ b/assets/queries/terraform/ibm/ibm_logdna_view_without_alert/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - LogDNA View Without Alert", + "severity": "LOW", + "line": 1, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/metadata.json new file mode 100644 index 00000000000..812b743e7cb --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "3ce366ab-e97d-4040-aa67-5a08dad595fb", + "queryName": "Beta - Event Rule for Cloud Guard Problems is Missing", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that an OCI event rule is configured to create notifications for problems detected by Cloud Guard. Without a notification rule, critical security findings from Cloud Guard may go unnoticed, delaying response to potential threats.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "3ce366ab", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/query.rego new file mode 100644 index 00000000000..b3d1a779102 --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/query.rego @@ -0,0 +1,27 @@ +package Cx + +import data.generic.common as common_lib + +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + expected_event_type := "com.oraclecloud.cloudguard.problem" + + rules_with_correct_event := [rule | + rule := input.document[_].resource.oci_events_rule[_] + rule.is_enabled == true + contains(rule.condition, expected_event_type) + ] + + count(rules_with_correct_event) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for Cloud Guard problems should exist in the project", + "keyActualValue": "No 'oci_events_rule' is configured to audit Cloud Guard problems", + } +} diff --git a/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..b418e886130 --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/negative1.tf @@ -0,0 +1,17 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "cg_correct" { + display_name = "CloudGuardCorrect" + is_enabled = true + condition = "{\"eventType\":[\"com.oraclecloud.cloudguard.problem\"]}" + + actions { + actions { + action_type = "ONS" + is_enabled = true + topic_id = "ocid1.onstopic..." + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..3a0b3bccec0 --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive1.tf @@ -0,0 +1,9 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_instance" "example" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard2.1" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..b0c092d9f6c --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive2.tf @@ -0,0 +1,17 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "cg_disabled" { + display_name = "CloudGuardDisabled" + is_enabled = false + condition = "{\"eventType\":[\"com.oraclecloud.cloudguard.problem\"]}" + + actions { + actions { + action_type = "ONS" + is_enabled = true + topic_id = "ocid1.onstopic..." + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive3.tf b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive3.tf new file mode 100644 index 00000000000..99de9217c22 --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive3.tf @@ -0,0 +1,17 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "bucket_event" { + display_name = "BucketEvent" + is_enabled = true + condition = "{\"eventType\":[\"com.oraclecloud.objectstorage.createbucket\"]}" + + actions { + actions { + action_type = "ONS" + is_enabled = true + topic_id = "ocid1.onstopic..." + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..05df2922272 --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_problem_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Event Rule for Cloud Guard Problems is Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for Cloud Guard Problems is Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Event Rule for Cloud Guard Problems is Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/metadata.json b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/metadata.json new file mode 100644 index 00000000000..82785af85db --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "3e784705-b3c6-4197-9a04-0b751b1a83d8", + "queryName": "Beta - Cloud Guard Not Enabled at Root Compartment", + "severity": "HIGH", + "category": "Security Services", + "descriptionText": "Ensures that OCI Cloud Guard is enabled at the tenancy's root compartment. Cloud Guard is a cloud-native security posture management service that helps monitor, identify, and maintain a strong security posture on Oracle Cloud.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/cloud_guard_configuration", + "platform": "Terraform", + "descriptionID": "3e784705", + "cloudProvider": "oci", + "cwe": "16", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/query.rego b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/query.rego new file mode 100644 index 00000000000..50cacab5242 --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/query.rego @@ -0,0 +1,57 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: No oci_cloud_guard_configuration resource exists. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + all_cloud_guards := [cg | + cg := input.document[_].resource.oci_cloud_guard_configuration[_] + ] + + count(all_cloud_guards) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "Resource 'oci_cloud_guard_configuration' should exist to enable Cloud Guard", + "keyActualValue": "Resource 'oci_cloud_guard_configuration' is missing", + } +} + +# RULE 2: Cloud Guard exists but its 'status' is not 'ENABLED'. +CxPolicy[result] { + cg := input.document[i].resource.oci_cloud_guard_configuration[cg_name] + + cg.status != "ENABLED" + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_cloud_guard_configuration.%s.status", [cg_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_cloud_guard_configuration", cg_name, "status"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'status' attribute should be 'ENABLED'", + "keyActualValue": sprintf("'status' attribute is '%s'", [cg.status]), + } +} + +# RULE 3: Cloud Guard is enabled but not at the root compartment. +CxPolicy[result] { + cg := input.document[i].resource.oci_cloud_guard_configuration[cg_name] + + cg.status == "ENABLED" + not contains(lower(cg.compartment_id), "tenancy") + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_cloud_guard_configuration.%s.compartment_id", [cg_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_cloud_guard_configuration", cg_name, "compartment_id"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'compartment_id' should be the tenancy (root compartment) OCID", + "keyActualValue": "'compartment_id' is not the tenancy OCID", + } +} diff --git a/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/negative1.tf b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/negative1.tf new file mode 100644 index 00000000000..32298f5a97c --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/negative1.tf @@ -0,0 +1,10 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_cloud_guard_configuration" "cg_correct" { + # PASS: Contiene la palabra "tenancy" + compartment_id = var.tenancy_ocid + reporting_region = "us-ashburn-1" + status = "ENABLED" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive1.tf b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive1.tf new file mode 100644 index 00000000000..f4602b1ed95 --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive1.tf @@ -0,0 +1,9 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_compartment" "example" { + compartment_id = "ocid1.tenancy..." + name = "example_compartment" + description = "Just a compartment" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive2.tf b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive2.tf new file mode 100644 index 00000000000..94e15af8b95 --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive2.tf @@ -0,0 +1,9 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_cloud_guard_configuration" "cg_disabled" { + compartment_id = var.tenancy_ocid + reporting_region = "us-ashburn-1" + status = "DISABLED" # FALLO +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive3.tf b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive3.tf new file mode 100644 index 00000000000..d2a1791c282 --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive3.tf @@ -0,0 +1,10 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_cloud_guard_configuration" "cg_child" { + # FAIL: Apunta a un compartimento hijo, no al tenancy + compartment_id = var.child_compartment_id + reporting_region = "us-ashburn-1" + status = "ENABLED" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..5f83235c125 --- /dev/null +++ b/assets/queries/terraform/oci/oci_cloud_guard_root_compartment_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Cloud Guard Not Enabled at Root Compartment", + "severity": "HIGH", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Cloud Guard Not Enabled at Root Compartment", + "severity": "HIGH", + "line": 8, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Cloud Guard Not Enabled at Root Compartment", + "severity": "HIGH", + "line": 7, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/metadata.json b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/metadata.json new file mode 100644 index 00000000000..727a7f2537a --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "96b464dc-a778-4b99-8634-c9eb9bdecf89", + "queryName": "Beta - Compute Instance Legacy Metadata Enabled", + "severity": "MEDIUM", + "category": "Insecure Configurations", + "descriptionText": "Ensures that OCI Compute Instances have legacy metadata service endpoints (IMDSv1) disabled. Enforcing the use of IMDSv2 is a security best practice to mitigate Server-Side Request Forgery (SSRF) vulnerabilities.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/core_instance#are_legacy_imds_endpoints_disabled", + "platform": "Terraform", + "descriptionID": "96b464dc", + "cloudProvider": "oci", + "cwe": "918", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/query.rego b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/query.rego new file mode 100644 index 00000000000..01b0b89b3c8 --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/query.rego @@ -0,0 +1,53 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: Missing The block 'agent_config' por completo. +CxPolicy[result] { + instance := input.document[i].resource.oci_core_instance[instance_name] + + not instance.agent_config + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_core_instance.%s", [instance_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_core_instance", instance_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'agent_config' block should be defined", + "keyActualValue": "'agent_config' block is missing", + } +} + +# RULE 2: Exists 'agent_config', but Missing The attribute dentro. +CxPolicy[result] { + instance := input.document[i].resource.oci_core_instance[instance_name] + agent_config := instance.agent_config + + object.get(agent_config, "are_legacy_imds_endpoints_disabled", null) == null + + result := { + "documentId": input.document[i].id, + # AQUI ESTA EL CAMBIO: Pointing to the block agent_config + "searchKey": sprintf("resource.oci_core_instance.%s.agent_config", [instance_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_core_instance", instance_name, "agent_config"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'are_legacy_imds_endpoints_disabled' should be present and set to 'true'", + "keyActualValue": "'are_legacy_imds_endpoints_disabled' is missing inside 'agent_config'", + } +} + +# RULE 3: The attribute Exists but es 'false'. +CxPolicy[result] { + instance := input.document[i].resource.oci_core_instance[instance_name] + + instance.agent_config.are_legacy_imds_endpoints_disabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_core_instance.%s.agent_config.are_legacy_imds_endpoints_disabled", [instance_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_core_instance", instance_name, "agent_config", "are_legacy_imds_endpoints_disabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'are_legacy_imds_endpoints_disabled' attribute should be 'true'", + "keyActualValue": "'are_legacy_imds_endpoints_disabled' attribute is 'false'", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/negative1.tf b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/negative1.tf new file mode 100644 index 00000000000..46daa2c7707 --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/negative1.tf @@ -0,0 +1,10 @@ +resource "oci_core_instance" "negative1" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard2.1" + + agent_config { + # PASS + are_legacy_imds_endpoints_disabled = true + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive1.tf b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive1.tf new file mode 100644 index 00000000000..980f71c8410 --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive1.tf @@ -0,0 +1,6 @@ +resource "oci_core_instance" "positive1" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard2.1" + # FAIL: Missing agent_config (Caso 1) +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive2.tf b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive2.tf new file mode 100644 index 00000000000..6193e1f812f --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive2.tf @@ -0,0 +1,10 @@ +resource "oci_core_instance" "positive2" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard2.1" + + agent_config { + # FAIL: Missing atributo (Caso 2) + is_monitoring_disabled = false + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive3.tf b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive3.tf new file mode 100644 index 00000000000..01ade141b02 --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive3.tf @@ -0,0 +1,10 @@ +resource "oci_core_instance" "positive3" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard2.1" + + agent_config { + # FAIL: Valor incorrecto (Caso 3) + are_legacy_imds_endpoints_disabled = false + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive_expected_result.json new file mode 100644 index 00000000000..4d3afc502f4 --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_legacy_metadata_enabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Compute Instance Legacy Metadata Enabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Compute Instance Legacy Metadata Enabled", + "severity": "MEDIUM", + "line": 6, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Compute Instance Legacy Metadata Enabled", + "severity": "MEDIUM", + "line": 8, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/metadata.json b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/metadata.json new file mode 100644 index 00000000000..88b5c0a344e --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "1f24d72d-c8be-403f-92b8-84ed910fe96e", + "queryName": "Beta - Compute Instance Secure Boot Disabled", + "severity": "MEDIUM", + "category": "Insecure Configurations", + "descriptionText": "Ensures that OCI Compute Instances have a platform configuration block (platform_config) defined with is_secure_boot_enabled set to true. Secure Boot prevents unauthorized firmware and operating systems from loading during instance startup, protecting against rootkits and boot-level malware.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/core_instance#platform_config", + "platform": "Terraform", + "descriptionID": "1f24d72d", + "cloudProvider": "oci", + "cwe": "427", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/query.rego b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/query.rego new file mode 100644 index 00000000000..ae3029b847c --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/query.rego @@ -0,0 +1,52 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: The 'platform_config' block is missing entirely. +CxPolicy[result] { + instance := input.document[i].resource.oci_core_instance[instance_name] + + not instance.platform_config + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_core_instance.%s", [instance_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_core_instance", instance_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'platform_config' block should be defined with 'is_secure_boot_enabled' set to true", + "keyActualValue": "'platform_config' block is missing", + } +} + +# RULE 2: 'platform_config' exists but 'is_secure_boot_enabled' is missing. +CxPolicy[result] { + instance := input.document[i].resource.oci_core_instance[instance_name] + platform_config := instance.platform_config + + object.get(platform_config, "is_secure_boot_enabled", null) == null + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_core_instance.%s.platform_config", [instance_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_core_instance", instance_name, "platform_config"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'is_secure_boot_enabled' should be present inside 'platform_config' and set to 'true'", + "keyActualValue": "'is_secure_boot_enabled' is missing inside 'platform_config'", + } +} + +# RULE 3: The attribute exists but is 'false'. +CxPolicy[result] { + instance := input.document[i].resource.oci_core_instance[instance_name] + + instance.platform_config.is_secure_boot_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_core_instance.%s.platform_config.is_secure_boot_enabled", [instance_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_core_instance", instance_name, "platform_config", "is_secure_boot_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_secure_boot_enabled' attribute should be 'true'", + "keyActualValue": "'is_secure_boot_enabled' attribute is 'false'", + } +} diff --git a/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/negative1.tf b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/negative1.tf new file mode 100644 index 00000000000..e408e8df210 --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/negative1.tf @@ -0,0 +1,11 @@ +resource "oci_core_instance" "negative1" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard2.1" + + platform_config { + # PASS: Secure Boot enabled + type = "AMD_VM" + is_secure_boot_enabled = true + } +} diff --git a/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive1.tf b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive1.tf new file mode 100644 index 00000000000..9eb37116c89 --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive1.tf @@ -0,0 +1,6 @@ +resource "oci_core_instance" "positive1" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard2.1" + # FAIL: Missing platform_config block (Case 1) +} diff --git a/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive2.tf b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive2.tf new file mode 100644 index 00000000000..bf41e7ca019 --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive2.tf @@ -0,0 +1,10 @@ +resource "oci_core_instance" "positive2" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard2.1" + + platform_config { + # FAIL: Missing is_secure_boot_enabled (Case 2) + type = "AMD_VM" + } +} diff --git a/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive3.tf b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive3.tf new file mode 100644 index 00000000000..7b48c9d5018 --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive3.tf @@ -0,0 +1,11 @@ +resource "oci_core_instance" "positive3" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard2.1" + + platform_config { + # FAIL: Set to false (Case 3) + type = "AMD_VM" + is_secure_boot_enabled = false + } +} diff --git a/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..d38c7976531 --- /dev/null +++ b/assets/queries/terraform/oci/oci_compute_secure_boot_disabled/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Compute Instance Secure Boot Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Compute Instance Secure Boot Disabled", + "severity": "MEDIUM", + "line": 6, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Compute Instance Secure Boot Disabled", + "severity": "MEDIUM", + "line": 8, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_default_tags_not_defined/metadata.json b/assets/queries/terraform/oci/oci_default_tags_not_defined/metadata.json new file mode 100644 index 00000000000..31b147c9007 --- /dev/null +++ b/assets/queries/terraform/oci/oci_default_tags_not_defined/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "4c294256-f477-41d5-8c7f-f31c9e48092e", + "queryName": "Beta - Default Tags Not Defined", + "severity": "LOW", + "category": "Best Practices", + "descriptionText": "Ensures that a default tagging policy is defined using the 'oci_identity_tag_default' resource. Default tags are crucial for governance as they ensure all resources within a compartment are automatically tagged for cost tracking, automation, and access control.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/identity_tag_default", + "platform": "Terraform", + "descriptionID": "4c294256", + "cloudProvider": "oci", + "cwe": "16", + "riskScore": "1.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_default_tags_not_defined/query.rego b/assets/queries/terraform/oci/oci_default_tags_not_defined/query.rego new file mode 100644 index 00000000000..355fcd1c6de --- /dev/null +++ b/assets/queries/terraform/oci/oci_default_tags_not_defined/query.rego @@ -0,0 +1,23 @@ +package Cx + +import data.generic.common as common_lib + +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + all_default_tags := [tag | + tag := input.document[_].resource.oci_identity_tag_default[_] + ] + + count(all_default_tags) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "At least one 'oci_identity_tag_default' resource should exist to define a default tagging policy", + "keyActualValue": "No 'oci_identity_tag_default' resource was found in the configuration", + } +} diff --git a/assets/queries/terraform/oci/oci_default_tags_not_defined/test/negative1.tf b/assets/queries/terraform/oci/oci_default_tags_not_defined/test/negative1.tf new file mode 100644 index 00000000000..4962573eb83 --- /dev/null +++ b/assets/queries/terraform/oci/oci_default_tags_not_defined/test/negative1.tf @@ -0,0 +1,22 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_tag_namespace" "example_ns" { + compartment_id = "ocid1.compartment..." + description = "Namespace" + name = "example-namespace" +} + +resource "oci_identity_tag" "example_tag" { + tag_namespace_id = oci_identity_tag_namespace.example_ns.id + description = "Tag" + name = "CostCenter" +} + +resource "oci_identity_tag_default" "example_default_tag" { + compartment_id = "ocid1.compartment..." + tag_definition_id = oci_identity_tag.example_tag.id + value = "IT-DEPT-123" + is_required = true +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_default_tags_not_defined/test/positive1.tf b/assets/queries/terraform/oci/oci_default_tags_not_defined/test/positive1.tf new file mode 100644 index 00000000000..df88a71ceed --- /dev/null +++ b/assets/queries/terraform/oci/oci_default_tags_not_defined/test/positive1.tf @@ -0,0 +1,8 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_vcn" "simple_vcn" { + compartment_id = "ocid1.compartment..." + cidr_block = "10.0.0.0/16" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_default_tags_not_defined/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_default_tags_not_defined/test/positive_expected_result.json new file mode 100644 index 00000000000..d0bce8b1511 --- /dev/null +++ b/assets/queries/terraform/oci/oci_default_tags_not_defined/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Default Tags Not Defined", + "severity": "LOW", + "line": 1, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_iam_group_change_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_iam_group_change_event_rule_missing/metadata.json new file mode 100644 index 00000000000..8d265583e53 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_group_change_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "3f6ba6dd-fa6d-45b4-8b27-120b7ebb4d0f", + "queryName": "Beta - Event Rule for IAM Group Changes is Missing", + "severity": "MEDIUM", + "category": "Access Control", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on changes to IAM groups. Auditing the creation, modification, and deletion of user groups is a critical security measure to detect unauthorized privilege escalation or tampering with access controls.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "3f6ba6dd", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_group_change_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_iam_group_change_event_rule_missing/query.rego new file mode 100644 index 00000000000..1a95d134691 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_group_change_event_rule_missing/query.rego @@ -0,0 +1,78 @@ +package Cx + +import data.generic.common as common_lib + +expected_event_types := [ + "com.oraclecloud.identity.creategroup", + "com.oraclecloud.identity.updategroup", + "com.oraclecloud.identity.deletegroup" +] + +# RULE 1: No rule exists in the project monitoring IAM group change events. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_iam_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(any_iam_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for IAM group changes should exist", + "keyActualValue": "No 'oci_events_rule' found for IAM group changes", + } +} + +# RULE 2: A rule exists but is missing some of the 3 required events. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(matches) > 0 + count(matches) < count(expected_event_types) + + missing_count := count(expected_event_types) - count(matches) + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.condition", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "condition"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "The rule condition should include all 3 IAM group events (create, update, delete)", + "keyActualValue": sprintf("The rule is missing %d IAM group event(s)", [missing_count]), + } +} + +# RULE 3: A rule has all events but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + count(matches) == count(expected_event_types) + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_iam_group_change_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_iam_group_change_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..3c72c48f58f --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_group_change_event_rule_missing/test/negative1.tf @@ -0,0 +1,23 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "iam_group_changes" { + display_name = "IAMGroupChanges" + is_enabled = true + + condition = < 365. +CxPolicy[result] { + doc := input.document[i] + policy := doc.resource.oci_identity_domains_password_policy[name] + + policy.password_expires_after > 365 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_domains_password_policy.%s.password_expires_after", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_domains_password_policy", name, "password_expires_after"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'password_expires_after' should be <= 365", + "keyActualValue": sprintf("'password_expires_after' is %d", [policy.password_expires_after]), + } +} + +# CASE 2: Identity Domains - Atributo faltante. +CxPolicy[result] { + doc := input.document[i] + policy := doc.resource.oci_identity_domains_password_policy[name] + + object.get(policy, "password_expires_after", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_domains_password_policy.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_domains_password_policy", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'password_expires_after' should be defined (<= 365)", + "keyActualValue": "'password_expires_after' is missing", + } +} + +# CASE 3: IAM Clásico (Legacy) - Manual. +CxPolicy[result] { + doc := input.document[i] + _ := doc.resource.oci_identity_authentication_policy[name] + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_authentication_policy.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_authentication_policy", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "Password expiration should be enforced (<= 365 days)", + "keyActualValue": "Legacy resource does not support password expiration config in Terraform. Manual console check required.", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/negative1.tf b/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/negative1.tf new file mode 100644 index 00000000000..cae1cb705e5 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/negative1.tf @@ -0,0 +1,7 @@ +resource "oci_identity_domains_password_policy" "correct_policy" { + idcs_endpoint = "https://idcs-..." + name = "CorrectPolicy" + # PASS: <= 365 + password_expires_after = 90 + schemas = ["urn:ietf:params:scim:schemas:oracle:idcs:extension:passwordState:User"] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive1.tf b/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive1.tf new file mode 100644 index 00000000000..8914da615c8 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive1.tf @@ -0,0 +1,7 @@ +resource "oci_identity_domains_password_policy" "long_expiration" { + idcs_endpoint = "https://idcs-..." + name = "LongExpirationPolicy" + # FAIL: > 365 + password_expires_after = 400 + schemas = ["urn:ietf:params:scim:schemas:oracle:idcs:extension:passwordState:User"] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive2.tf b/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive2.tf new file mode 100644 index 00000000000..767d4cf518a --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive2.tf @@ -0,0 +1,7 @@ +resource "oci_identity_domains_password_policy" "missing_attr" { + idcs_endpoint = "https://idcs-..." + name = "MissingAttrPolicy" + # FAIL: Missing The attribute + password_min_length = 14 + schemas = ["urn:ietf:params:scim:schemas:oracle:idcs:extension:passwordState:User"] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive3.tf b/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive3.tf new file mode 100644 index 00000000000..12d29e73775 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive3.tf @@ -0,0 +1,7 @@ +resource "oci_identity_authentication_policy" "legacy_policy" { + compartment_id = "ocid1.tenancy..." + # FAIL: Legacy + password_policy { + minimum_password_length = 14 + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..b04f83f241e --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_expiration_manual/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - OCI IAM Password Expiration (365 days)", + "severity": "INFO", + "line": 5, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - OCI IAM Password Expiration (365 days)", + "severity": "INFO", + "line": 1, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - OCI IAM Password Expiration (365 days)", + "severity": "INFO", + "line": 1, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_iam_password_policy_length/metadata.json b/assets/queries/terraform/oci/oci_iam_password_policy_length/metadata.json new file mode 100644 index 00000000000..190d772a3fa --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_policy_length/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "ef7a0923-a4e6-4aec-921b-1b4e45463fb6", + "queryName": "Beta - OCI IAM Password Minimum Length", + "severity": "MEDIUM", + "category": "Identity and Access Management", + "descriptionText": "The IAM password policy allows passwords shorter than 14 characters. According to CIS Benchmarks, the 'minimum_password_length' in 'oci_identity_authentication_policy' should be set to 14 or greater.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/identity_authentication_policy", + "platform": "Terraform", + "descriptionID": "ef7a0923", + "cloudProvider": "oci", + "cwe": "521", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_policy_length/query.rego b/assets/queries/terraform/oci/oci_iam_password_policy_length/query.rego new file mode 100644 index 00000000000..1cf143862d3 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_policy_length/query.rego @@ -0,0 +1,67 @@ +package Cx + +import data.generic.common as common_lib + +ensure_array(x) = x { is_array(x) } +ensure_array(x) = [x] { is_object(x) } + +# CASE 1: The block 'password_policy' Exists, but el valor es menor a 14. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.oci_identity_authentication_policy[name] + + resource.password_policy + policies := ensure_array(resource.password_policy) + policy := policies[_] + + policy.minimum_password_length + + to_number(policy.minimum_password_length) < 14 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_authentication_policy.%s.password_policy.minimum_password_length", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_authentication_policy", name, "password_policy", "minimum_password_length"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'minimum_password_length' should be 14 or greater", + "keyActualValue": sprintf("'minimum_password_length' is %d", [to_number(policy.minimum_password_length)]), + } +} + +# CASE 2: The block 'password_policy' Exists, but Missing The attribute 'minimum_password_length'. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.oci_identity_authentication_policy[name] + + resource.password_policy + policies := ensure_array(resource.password_policy) + policy := policies[_] + + object.get(policy, "minimum_password_length", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_authentication_policy.%s.password_policy", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_authentication_policy", name, "password_policy"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'minimum_password_length' should be explicitly defined (>= 14)", + "keyActualValue": "'minimum_password_length' is missing (using default)", + } +} + +# CASE 3: The block 'password_policy' Does not exist en absoluto. +CxPolicy[result] { + doc := input.document[i] + resource := doc.resource.oci_identity_authentication_policy[name] + + not resource.password_policy + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_authentication_policy.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_authentication_policy", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'password_policy' block should be defined with 'minimum_password_length' >= 14", + "keyActualValue": "'password_policy' block is missing", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_policy_length/test/negative1.tf b/assets/queries/terraform/oci/oci_iam_password_policy_length/test/negative1.tf new file mode 100644 index 00000000000..3ddc8e37095 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_policy_length/test/negative1.tf @@ -0,0 +1,9 @@ +resource "oci_identity_authentication_policy" "secure_policy" { + compartment_id = "ocid1.tenancy..." + + password_policy { + # PASS: >= 14 + minimum_password_length = 14 + is_lowercase_characters_required = true + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive1.tf b/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive1.tf new file mode 100644 index 00000000000..70f9b9c8e23 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive1.tf @@ -0,0 +1,9 @@ +resource "oci_identity_authentication_policy" "weak_policy" { + compartment_id = "ocid1.tenancy..." + + password_policy { + # FAIL: 8 es menor que 14 + minimum_password_length = 8 + is_lowercase_characters_required = true + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive2.tf b/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive2.tf new file mode 100644 index 00000000000..bc2df3dcadc --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive2.tf @@ -0,0 +1,8 @@ +resource "oci_identity_authentication_policy" "missing_attr" { + compartment_id = "ocid1.tenancy..." + + password_policy { + # FAIL: Missing minimum_password_length + is_lowercase_characters_required = true + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive3.tf b/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive3.tf new file mode 100644 index 00000000000..78477dc8e41 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive3.tf @@ -0,0 +1,4 @@ +resource "oci_identity_authentication_policy" "missing_block" { + compartment_id = "ocid1.tenancy..." + # FAIL: No hay bloque password_policy +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive_expected_result.json new file mode 100644 index 00000000000..41429b260a1 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_policy_length/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - OCI IAM Password Minimum Length", + "severity": "MEDIUM", + "line": 6, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - OCI IAM Password Minimum Length", + "severity": "MEDIUM", + "line": 4, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - OCI IAM Password Minimum Length", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_iam_password_reuse_manual/metadata.json b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/metadata.json new file mode 100644 index 00000000000..0e8c8c6fde2 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "fc42ff43-3c72-41d1-b5fb-3beebd4efe15", + "queryName": "Beta - OCI IAM Password Reuse Prevention (Manual)", + "severity": "INFO", + "category": "Identity and Access Management", + "descriptionText": "CIS Benchmark recommends preventing password reuse for at least the last 24 passwords. Check 'num_passwords_in_history' in 'oci_identity_domains_password_policy'. For legacy 'oci_identity_authentication_policy', this requires manual console verification.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/identity_domains_password_policy", + "platform": "Terraform", + "descriptionID": "fc42ff43", + "cloudProvider": "oci", + "cwe": "261", + "riskScore": 0.0, + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_reuse_manual/query.rego b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/query.rego new file mode 100644 index 00000000000..7ffe03882f4 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/query.rego @@ -0,0 +1,52 @@ +package Cx + +import data.generic.common as common_lib + +# CASE 1: Identity Domains - Historial insuficiente (< 24). +CxPolicy[result] { + doc := input.document[i] + policy := doc.resource.oci_identity_domains_password_policy[name] + + policy.num_passwords_in_history < 24 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_domains_password_policy.%s.num_passwords_in_history", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_domains_password_policy", name, "num_passwords_in_history"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'num_passwords_in_history' should be >= 24", + "keyActualValue": sprintf("Current history size is %d", [policy.num_passwords_in_history]), + } +} + +# CASE 2: Identity Domains - Atributo faltante. +CxPolicy[result] { + doc := input.document[i] + policy := doc.resource.oci_identity_domains_password_policy[name] + + object.get(policy, "num_passwords_in_history", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_domains_password_policy.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_domains_password_policy", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'num_passwords_in_history' should be defined (>= 24)", + "keyActualValue": "'num_passwords_in_history' is missing", + } +} + +# CASE 3: IAM Clásico (Legacy) - Manual. +CxPolicy[result] { + doc := input.document[i] + _ := doc.resource.oci_identity_authentication_policy[name] + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_authentication_policy.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_authentication_policy", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "Password reuse prevention should be enabled (History >= 24)", + "keyActualValue": "Legacy resource does not support password history settings in Terraform. Manual console check required.", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/negative1.tf b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/negative1.tf new file mode 100644 index 00000000000..afa1fa17928 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/negative1.tf @@ -0,0 +1,9 @@ +resource "oci_identity_domains_password_policy" "secure_policy" { + idcs_endpoint = "https://idcs-..." + name = "SecurePolicy" + + # PASS: >= 24 + num_passwords_in_history = 24 + + schemas = ["urn:ietf:params:scim:schemas:oracle:idcs:extension:passwordState:User"] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive1.tf b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive1.tf new file mode 100644 index 00000000000..45269292598 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive1.tf @@ -0,0 +1,9 @@ +resource "oci_identity_domains_password_policy" "weak_history" { + idcs_endpoint = "https://idcs-..." + name = "WeakHistoryPolicy" + + # FAIL: 5 es menor que 24 + num_passwords_in_history = 5 + + schemas = ["urn:ietf:params:scim:schemas:oracle:idcs:extension:passwordState:User"] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive2.tf b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive2.tf new file mode 100644 index 00000000000..a105c5cc4f2 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive2.tf @@ -0,0 +1,8 @@ +resource "oci_identity_domains_password_policy" "missing_attr" { + idcs_endpoint = "https://idcs-..." + name = "MissingAttrPolicy" + + # FAIL: Missing num_passwords_in_history + password_min_length = 14 + schemas = ["urn:ietf:params:scim:schemas:oracle:idcs:extension:passwordState:User"] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive3.tf b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive3.tf new file mode 100644 index 00000000000..1a164a880c4 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive3.tf @@ -0,0 +1,8 @@ +resource "oci_identity_authentication_policy" "legacy_policy" { + compartment_id = "ocid1.tenancy..." + + # FAIL: resource Legacy requiere chequeo manual + password_policy { + minimum_password_length = 14 + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..f9b0e975e10 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_password_reuse_manual/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - OCI IAM Password Reuse Prevention (Manual)", + "severity": "INFO", + "line": 6, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - OCI IAM Password Reuse Prevention (Manual)", + "severity": "INFO", + "line": 1, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - OCI IAM Password Reuse Prevention (Manual)", + "severity": "INFO", + "line": 1, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/metadata.json new file mode 100644 index 00000000000..568498c6842 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "a151d9a0-d626-447f-beab-11d53aeb0f2b", + "queryName": "Beta - Event Rule for IAM Policy Changes is Missing", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on changes to IAM policies. Auditing the creation, modification, and deletion of IAM policies is a critical security measure to detect unauthorized changes to permissions within the tenancy.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "a151d9a0", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/query.rego new file mode 100644 index 00000000000..3d019703f0e --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/query.rego @@ -0,0 +1,78 @@ +package Cx + +import data.generic.common as common_lib + +expected_event_types := [ + "com.oraclecloud.identity.createpolicy", + "com.oraclecloud.identity.updatepolicy", + "com.oraclecloud.identity.deletepolicy" +] + +# RULE 1: No rule exists in the project monitoring IAM Policy change events. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_policy_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(any_policy_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for IAM Policy changes should exist", + "keyActualValue": "No 'oci_events_rule' found for IAM Policy changes", + } +} + +# RULE 2: A rule exists but is missing some of the 3 required events. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(matches) > 0 + count(matches) < count(expected_event_types) + + missing_count := count(expected_event_types) - count(matches) + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.condition", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "condition"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "The rule condition should include all 3 IAM Policy events (create, update, delete)", + "keyActualValue": sprintf("The rule is missing %d IAM Policy event(s)", [missing_count]), + } +} + +# RULE 3: A rule has all events but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + count(matches) == count(expected_event_types) + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..9ffa462982d --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/negative1.tf @@ -0,0 +1,25 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "correct_rule" { + display_name = "CorrectPolicyRule" + compartment_id = "ocid1.tenancy.oc1.." + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identity.createpolicy", + "com.oraclecloud.identity.updatepolicy", + "com.oraclecloud.identity.deletepolicy" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic.oc1.." + is_enabled = true + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..b72e7fdfbf1 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive1.tf @@ -0,0 +1,10 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_policy" "test_policy" { + name = "test-policy" + description = "Policy for testing" + compartment_id = "ocid1.tenancy.oc1.." + statements = ["Allow group Administrators to manage all-resources in tenancy"] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..7f95191417f --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive2.tf @@ -0,0 +1,25 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "incomplete_rule" { + display_name = "IncompletePolicyRule" + compartment_id = "ocid1.tenancy.oc1.." + is_enabled = true + + # FAIL: Missing "deletepolicy" + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identity.createpolicy", + "com.oraclecloud.identity.updatepolicy" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic.oc1.." + is_enabled = true + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive3.tf b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive3.tf new file mode 100644 index 00000000000..6b515b92330 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive3.tf @@ -0,0 +1,26 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "disabled_rule" { + display_name = "DisabledPolicyRule" + compartment_id = "ocid1.tenancy.oc1.." + description = "Rule with all events but disabled" + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identity.createpolicy", + "com.oraclecloud.identity.updatepolicy", + "com.oraclecloud.identity.deletepolicy" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic.oc1.." + } + } + + is_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..9bdb1dcec81 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_policy_change_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Event Rule for IAM Policy Changes is Missing", + "severity": "HIGH", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for IAM Policy Changes is Missing", + "severity": "HIGH", + "line": 11, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Event Rule for IAM Policy Changes is Missing", + "severity": "HIGH", + "line": 25, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_iam_service_admins_manual/metadata.json b/assets/queries/terraform/oci/oci_iam_service_admins_manual/metadata.json new file mode 100644 index 00000000000..72cf95d1be8 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_service_admins_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "0401f017-91ee-4019-842a-f940d08b22aa", + "queryName": "Beta - OCI Service Level Admins (Manual)", + "severity": "INFO", + "category": "Identity and Access Management", + "descriptionText": "CIS controls recommend creating specific Service Level Administrators (e.g., Network Admin, Storage Admin) rather than relying solely on the default Tenancy Administrator. Review this policy to ensure it delegates 'manage' permissions for specific resource families to appropriate groups, implementing Least Privilege.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/identity_policy", + "platform": "Terraform", + "cloudProvider": "oci", + "cwe": "276", + "descriptionID": "0401f017", + "riskScore": 0.0, + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_service_admins_manual/query.rego b/assets/queries/terraform/oci/oci_iam_service_admins_manual/query.rego new file mode 100644 index 00000000000..f6a71857767 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_service_admins_manual/query.rego @@ -0,0 +1,22 @@ +package Cx + +import data.generic.common as common_lib + +# REGLA: Auditoría Manual de Políticas de Gestión (Service Admins). +CxPolicy[result] { + doc := input.document[i] + policy := doc.resource.oci_identity_policy[name] + + statement := policy.statements[_] + + regex.match("(?i)\\bmanage\\b", statement) + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_policy.%s.statements", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_policy", name, "statements"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "Policy should grant 'manage' on specific families to specific groups (Manual Review)", + "keyActualValue": sprintf("Policy statement grants management privileges: '%s'. Manual review required.", [statement]), + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/negative1.tf b/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/negative1.tf new file mode 100644 index 00000000000..0f5443c23d9 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/negative1.tf @@ -0,0 +1,14 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_policy" "auditors" { + name = "AuditPolicy" + description = "Read only access" + compartment_id = "ocid1.tenancy..." + + statements = [ + "Allow group Auditors to read all-resources in tenancy", + "Allow group Auditors to inspect users in tenancy" + ] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/negative2.tf b/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/negative2.tf new file mode 100644 index 00000000000..bd5c0be5230 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/negative2.tf @@ -0,0 +1,13 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_policy" "project_managers" { + name = "ProjectManagersPolicy" + compartment_id = "ocid1.tenancy..." + + statements = [ + "Allow group ProjectManagers to read instance-family in compartment ProjectA", + "Allow group KeyManagementUsers to use keys in tenancy" + ] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/positive1.tf b/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/positive1.tf new file mode 100644 index 00000000000..3d98f40f89f --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/positive1.tf @@ -0,0 +1,23 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_policy" "network_admins" { + name = "NetworkAdmins" + description = "Admins for VCN" + compartment_id = "ocid1.tenancy..." + + statements = [ + "Allow group NetworkAdmins to manage virtual-network-family in tenancy" + ] +} + +resource "oci_identity_policy" "super_admin" { + name = "SuperAdmins" + description = "God mode" + compartment_id = "ocid1.tenancy..." + + statements = [ + "Allow group SuperUsers to MANAGE all-resources in tenancy" + ] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..e2028843e90 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_service_admins_manual/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - OCI Service Level Admins (Manual)", + "severity": "INFO", + "line": 10, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - OCI Service Level Admins (Manual)", + "severity": "INFO", + "line": 20, + "fileName": "positive1.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/metadata.json new file mode 100644 index 00000000000..36349ecdc00 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "b1405757-feb9-4218-b685-ff53775ddf48", + "queryName": "Beta - Event Rule for IAM User Changes is Missing", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on changes to IAM users. Auditing the full lifecycle of user accounts (creation, modification, deletion, enable/disable) is a critical security measure to detect unauthorized account creation or privilege changes.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "b1405757", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/query.rego new file mode 100644 index 00000000000..765b09fe0c7 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/query.rego @@ -0,0 +1,80 @@ +package Cx + +import data.generic.common as common_lib + +expected_event_types := [ + "com.oraclecloud.identity.createuser", + "com.oraclecloud.identity.updateuser", + "com.oraclecloud.identity.deleteuser", + "com.oraclecloud.identity.enableuser", + "com.oraclecloud.identity.disableuser" +] + +# RULE 1: No rule exists in the project monitoring IAM User change events. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_user_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(any_user_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for IAM User changes (create, update, delete, enable, disable) should exist", + "keyActualValue": "No 'oci_events_rule' found for IAM User changes", + } +} + +# RULE 2: A rule exists but is missing some of the 5 required events. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(matches) > 0 + count(matches) < count(expected_event_types) + + missing_count := count(expected_event_types) - count(matches) + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.condition", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "condition"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "The rule condition should include all 5 IAM User events", + "keyActualValue": sprintf("The rule is missing %d IAM User event(s)", [missing_count]), + } +} + +# RULE 3: A relevant rule exists but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + count(matches) > 0 + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..db6c7e4a0c3 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/negative1.tf @@ -0,0 +1,27 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "correct_user_rule" { + display_name = "CorrectUserRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identity.createuser", + "com.oraclecloud.identity.updateuser", + "com.oraclecloud.identity.deleteuser", + "com.oraclecloud.identity.enableuser", + "com.oraclecloud.identity.disableuser" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + is_enabled = true + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..df3bc9c3799 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive1.tf @@ -0,0 +1,8 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_user" "test_user" { + name = "test-user" + description = "Test user" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..68f93d89297 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive2.tf @@ -0,0 +1,25 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "incomplete_user_rule" { + display_name = "IncompleteUserRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + # FAIL: Faltan enableuser y disableuser + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identity.createuser", + "com.oraclecloud.identity.updateuser", + "com.oraclecloud.identity.deleteuser" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive3.tf b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive3.tf new file mode 100644 index 00000000000..395f73b60a9 --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive3.tf @@ -0,0 +1,28 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "disabled_user_rule" { + display_name = "DisabledUserRule" + compartment_id = "ocid1.tenancy..." + description = "Has all events but is disabled" + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identity.createuser", + "com.oraclecloud.identity.updateuser", + "com.oraclecloud.identity.deleteuser", + "com.oraclecloud.identity.enableuser", + "com.oraclecloud.identity.disableuser" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } + + is_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..649145ecade --- /dev/null +++ b/assets/queries/terraform/oci/oci_iam_user_change_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Event Rule for IAM User Changes is Missing", + "severity": "HIGH", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for IAM User Changes is Missing", + "severity": "HIGH", + "line": 11, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Event Rule for IAM User Changes is Missing", + "severity": "HIGH", + "line": 27, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/metadata.json new file mode 100644 index 00000000000..c5994d2d53f --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "0a077b15-dc77-4490-810b-4e34f1b55500", + "queryName": "Beta - Event Rule for Identity Provider Changes is Missing", + "severity": "MEDIUM", + "category": "Access Control", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on changes to Identity Providers. Monitoring these changes is critical for detecting potentially malicious activity, such as the addition of an unauthorized external identity provider.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "0a077b15", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/query.rego new file mode 100644 index 00000000000..05f0067b85e --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/query.rego @@ -0,0 +1,45 @@ +package Cx + +import data.generic.common as common_lib + +expected_event := "com.oraclecloud.identitycontrolplane.updateidentityprovider" + +# RULE 1: No rule exists in the project monitoring Identity Provider changes. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_idp_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + contains(rule.condition, expected_event) + ] + + count(any_idp_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for Identity Provider changes should exist", + "keyActualValue": "No 'oci_events_rule' found for Identity Provider changes", + } +} + +# RULE 2: A rule monitors IdP events but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + contains(rule.condition, expected_event) + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..93206fbcabf --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/negative1.tf @@ -0,0 +1,23 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "correct_idp_rule" { + display_name = "CorrectIdPRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identitycontrolplane.updateidentityprovider" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + is_enabled = true + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..3ae5013b74c --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/positive1.tf @@ -0,0 +1,20 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "other_rule" { + display_name = "InstanceRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + condition = jsonencode({ + "eventType": ["com.oraclecloud.compute.instance.launch.end"] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..d17dc30b7d6 --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/positive2.tf @@ -0,0 +1,24 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "disabled_idp_rule" { + display_name = "DisabledIdPRule" + compartment_id = "ocid1.tenancy..." + description = "Monitors IdP but is disabled" + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identitycontrolplane.updateidentityprovider" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } + + is_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..07dcb523f80 --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_change_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Event Rule for Identity Provider Changes is Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for Identity Provider Changes is Missing", + "severity": "MEDIUM", + "line": 23, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/metadata.json new file mode 100644 index 00000000000..e985e70fecc --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "1f106aa1-a994-4a11-afbc-109f31947d6a", + "queryName": "Beta - Event Rule for IdP Group Mapping Changes is Missing", + "severity": "MEDIUM", + "category": "Access Control", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on changes to Identity Provider (IdP) group mappings. Auditing these changes is vital for detecting unauthorized modifications to user group permissions.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "1f106aa1", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/query.rego new file mode 100644 index 00000000000..95ca8cf10d1 --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/query.rego @@ -0,0 +1,45 @@ +package Cx + +import data.generic.common as common_lib + +expected_event := "com.oraclecloud.identitycontrolplane.updateidpgroupmapping" + +# RULE 1: No rule exists in the project monitoring IdP group mapping changes events. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + contains(rule.condition, expected_event) + ] + + count(any_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for IdP group mapping changes events should exist", + "keyActualValue": "No 'oci_events_rule' found for IdP group mapping changes", + } +} + +# RULE 2: A relevant rule exists but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + contains(rule.condition, expected_event) + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..9c48a70b701 --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/negative1.tf @@ -0,0 +1,23 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "correct_mapping_rule" { + display_name = "CorrectMappingRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identitycontrolplane.updateidpgroupmapping" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + is_enabled = true + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..765a4165e1b --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/positive1.tf @@ -0,0 +1,20 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "other_rule" { + display_name = "NetworkRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + condition = jsonencode({ + "eventType": ["com.oraclecloud.virtualnetwork.createvcn"] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..c4cfa1d649a --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/positive2.tf @@ -0,0 +1,24 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "disabled_mapping_rule" { + display_name = "DisabledMappingRule" + compartment_id = "ocid1.tenancy..." + description = "Monitors IdP Mapping but is disabled" + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identitycontrolplane.updateidpgroupmapping" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } + + is_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..ab3c242c5e9 --- /dev/null +++ b/assets/queries/terraform/oci/oci_idp_group_mapping_change_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Event Rule for IdP Group Mapping Changes is Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for IdP Group Mapping Changes is Missing", + "severity": "MEDIUM", + "line": 23, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_instance_transit_encryption/metadata.json b/assets/queries/terraform/oci/oci_instance_transit_encryption/metadata.json new file mode 100644 index 00000000000..130967613df --- /dev/null +++ b/assets/queries/terraform/oci/oci_instance_transit_encryption/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "c29ef8f7-33bc-4788-9085-3a561c91f7d1", + "queryName": "Beta - OCI Instance In-Transit Encryption Disabled", + "severity": "MEDIUM", + "category": "Encryption", + "descriptionText": "Compute instances should have in-transit encryption enabled for data moving between the instance and block storage. This is configured via 'is_pv_encryption_in_transit_enabled' in the 'launch_options' block.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/core_instance#is_pv_encryption_in_transit_enabled", + "platform": "Terraform", + "descriptionID": "c29ef8f7", + "cloudProvider": "oci", + "cwe": "311", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_instance_transit_encryption/query.rego b/assets/queries/terraform/oci/oci_instance_transit_encryption/query.rego new file mode 100644 index 00000000000..60bf3ed6718 --- /dev/null +++ b/assets/queries/terraform/oci/oci_instance_transit_encryption/query.rego @@ -0,0 +1,63 @@ +package Cx + +import data.generic.common as common_lib + +ensure_array(x) = x { is_array(x) } +ensure_array(x) = [x] { is_object(x) } + +# CASE 1: The block launch_options Exists, but la opción está explícitamente en FALSE. +CxPolicy[result] { + doc := input.document[i] + instance := doc.resource.oci_core_instance[name] + + options := ensure_array(instance.launch_options) + opt := options[_] + + opt.is_pv_encryption_in_transit_enabled == false + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_core_instance.%s.launch_options.is_pv_encryption_in_transit_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_core_instance", name, "launch_options", "is_pv_encryption_in_transit_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_pv_encryption_in_transit_enabled' should be set to true", + "keyActualValue": "'is_pv_encryption_in_transit_enabled' is set to false", + } +} + +# CASE 2: The block launch_options Exists, but Missing The attribute (default es false/inseguro). +CxPolicy[result] { + doc := input.document[i] + instance := doc.resource.oci_core_instance[name] + + options := ensure_array(instance.launch_options) + opt := options[_] + + object.get(opt, "is_pv_encryption_in_transit_enabled", "undefined") == "undefined" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_core_instance.%s.launch_options", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_core_instance", name, "launch_options"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'is_pv_encryption_in_transit_enabled' should be defined and set to true", + "keyActualValue": "'is_pv_encryption_in_transit_enabled' is missing", + } +} + +# CASE 3: Missing The block launch_options completo. +CxPolicy[result] { + doc := input.document[i] + instance := doc.resource.oci_core_instance[name] + + not instance.launch_options + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_core_instance.%s", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_core_instance", name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'launch_options' block with 'is_pv_encryption_in_transit_enabled = true' should be defined", + "keyActualValue": "'launch_options' block is missing", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_instance_transit_encryption/test/negative1.tf b/assets/queries/terraform/oci/oci_instance_transit_encryption/test/negative1.tf new file mode 100644 index 00000000000..59c69e1a07d --- /dev/null +++ b/assets/queries/terraform/oci/oci_instance_transit_encryption/test/negative1.tf @@ -0,0 +1,14 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_instance" "instance_compliant" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard.E4.Flex" + + launch_options { + boot_volume_type = "PARAVIRTUALIZED" + is_pv_encryption_in_transit_enabled = true + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive1.tf b/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive1.tf new file mode 100644 index 00000000000..990adbac80a --- /dev/null +++ b/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive1.tf @@ -0,0 +1,20 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_instance" "instance_explicit_false" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard.E4.Flex" + + launch_options { + boot_volume_type = "PARAVIRTUALIZED" + # FAIL: Explícitamente false (sin comentario pegado encima) + is_pv_encryption_in_transit_enabled = false + } + + source_details { + source_id = "ocid1.image..." + source_type = "image" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive2.tf b/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive2.tf new file mode 100644 index 00000000000..eecaed77dab --- /dev/null +++ b/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive2.tf @@ -0,0 +1,15 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_instance" "instance_missing_attribute" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard.E4.Flex" + + # The block Exists, but Missing The attribute de encriptación + launch_options { + boot_volume_type = "PARAVIRTUALIZED" + network_type = "PARAVIRTUALIZED" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive3.tf b/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive3.tf new file mode 100644 index 00000000000..fdbb89e89c0 --- /dev/null +++ b/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive3.tf @@ -0,0 +1,15 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_instance" "instance_missing_block" { + availability_domain = "AD-1" + compartment_id = "ocid1.compartment..." + shape = "VM.Standard.E4.Flex" + + # Does not exist The block launch_options + source_details { + source_id = "ocid1.image..." + source_type = "image" + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive_expected_result.json new file mode 100644 index 00000000000..dbd0a6d1d71 --- /dev/null +++ b/assets/queries/terraform/oci/oci_instance_transit_encryption/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - OCI Instance In-Transit Encryption Disabled", + "severity": "MEDIUM", + "line": 13, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - OCI Instance In-Transit Encryption Disabled", + "severity": "MEDIUM", + "line": 11, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - OCI Instance In-Transit Encryption Disabled", + "severity": "MEDIUM", + "line": 5, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/metadata.json new file mode 100644 index 00000000000..131b29366a6 --- /dev/null +++ b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "e0c61731-a692-44d8-bbf2-8279985a4e41", + "queryName": "Beta - Event Rule for Local User Authentication is Missing", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on authentication events from local IAM users. Auditing local user logins is a critical security measure to detect unauthorized access attempts and compromised credentials.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "e0c61731", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/query.rego new file mode 100644 index 00000000000..de4ce0814ae --- /dev/null +++ b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/query.rego @@ -0,0 +1,45 @@ +package Cx + +import data.generic.common as common_lib + +expected_event := "com.oraclecloud.identity.localuser.authenticate" + +# RULE 1: No rule exists in the project monitoring local user authentication events. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + contains(rule.condition, expected_event) + ] + + count(any_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for local user authentication events should exist", + "keyActualValue": "No 'oci_events_rule' found for local user authentication", + } +} + +# RULE 2: A relevant rule exists but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + contains(rule.condition, expected_event) + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..1f866cc41e8 --- /dev/null +++ b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/negative1.tf @@ -0,0 +1,23 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "correct_auth_rule" { + display_name = "CorrectAuthRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identity.localuser.authenticate" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + is_enabled = true + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..6b07c9992ba --- /dev/null +++ b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/positive1.tf @@ -0,0 +1,20 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "other_rule" { + display_name = "StorageRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + condition = jsonencode({ + "eventType": ["com.oraclecloud.objectstorage.createbucket"] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..b34fdad393b --- /dev/null +++ b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/positive2.tf @@ -0,0 +1,24 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "disabled_auth_rule" { + display_name = "DisabledAuthRule" + compartment_id = "ocid1.tenancy..." + description = "Monitors Auth but is disabled" + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.identity.localuser.authenticate" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } + + is_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..aceb34d8437 --- /dev/null +++ b/assets/queries/terraform/oci/oci_local_user_authentication_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Event Rule for Local User Authentication is Missing", + "severity": "HIGH", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for Local User Authentication is Missing", + "severity": "HIGH", + "line": 23, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/metadata.json new file mode 100644 index 00000000000..7b120e77164 --- /dev/null +++ b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "e3ade454-28a1-4a0c-9905-b73e82a648c0", + "queryName": "Beta - Event Rule for Network Gateway Changes is Missing", + "severity": "MEDIUM", + "category": "Networking and Firewall", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on changes to all types of Network Gateways (Internet, NAT, Service, DRG, etc.). Auditing gateway changes is crucial for detecting unauthorized modifications to network ingress/egress points.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "e3ade454", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/query.rego new file mode 100644 index 00000000000..cb4314ceed0 --- /dev/null +++ b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/query.rego @@ -0,0 +1,90 @@ +package Cx + +import data.generic.common as common_lib + +expected_event_types := [ + "com.oraclecloud.virtualnetwork.createinternetgateway", + "com.oraclecloud.virtualnetwork.updateinternetgateway", + "com.oraclecloud.virtualnetwork.deleteinternetgateway", + "com.oraclecloud.virtualnetwork.createnatgateway", + "com.oraclecloud.virtualnetwork.updatenatgateway", + "com.oraclecloud.virtualnetwork.deletenatgateway", + "com.oraclecloud.virtualnetwork.createservicegateway", + "com.oraclecloud.virtualnetwork.updateservicegateway", + "com.oraclecloud.virtualnetwork.deleteservicegateway", + "com.oraclecloud.virtualnetwork.createdrg", + "com.oraclecloud.virtualnetwork.updatedrg", + "com.oraclecloud.virtualnetwork.deletedrg", + "com.oraclecloud.virtualnetwork.createlocalpeeringgateway", + "com.oraclecloud.virtualnetwork.updatelocalpeeringgateway", + "com.oraclecloud.virtualnetwork.deletelocalpeeringgateway" +] + +# RULE 1: No rule exists in the project monitoring Network Gateway change events. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(any_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for Network Gateway changes should exist", + "keyActualValue": "No 'oci_events_rule' found for Network Gateway changes", + } +} + +# RULE 2: A rule exists but is missing some of the required events. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(matches) > 0 + count(matches) < count(expected_event_types) + + missing_count := count(expected_event_types) - count(matches) + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.condition", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "condition"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "The rule condition should include all required Network Gateway events", + "keyActualValue": sprintf("The rule is missing %d Network Gateway event(s)", [missing_count]), + } +} + +# RULE 3: A relevant rule exists but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + count(matches) > 0 + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..9bda51072cf --- /dev/null +++ b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/negative1.tf @@ -0,0 +1,43 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "compliant_gateway_rule" { + display_name = "CompliantGatewayRule" + compartment_id = "ocid1.tenancy.oc1..aaaa" + description = "Monitors all 15 Network Gateway events" + is_enabled = true + + condition = jsonencode({ + "eventType": [ + # Internet Gateway + "com.oraclecloud.virtualnetwork.createinternetgateway", + "com.oraclecloud.virtualnetwork.updateinternetgateway", + "com.oraclecloud.virtualnetwork.deleteinternetgateway", + # NAT Gateway + "com.oraclecloud.virtualnetwork.createnatgateway", + "com.oraclecloud.virtualnetwork.updatenatgateway", + "com.oraclecloud.virtualnetwork.deletenatgateway", + # Service Gateway + "com.oraclecloud.virtualnetwork.createservicegateway", + "com.oraclecloud.virtualnetwork.updateservicegateway", + "com.oraclecloud.virtualnetwork.deleteservicegateway", + # DRG (Dynamic Routing Gateway) + "com.oraclecloud.virtualnetwork.createdrg", + "com.oraclecloud.virtualnetwork.updatedrg", + "com.oraclecloud.virtualnetwork.deletedrg", + # LPG (Local Peering Gateway) + "com.oraclecloud.virtualnetwork.createlocalpeeringgateway", + "com.oraclecloud.virtualnetwork.updatelocalpeeringgateway", + "com.oraclecloud.virtualnetwork.deletelocalpeeringgateway" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic.oc1.iad.aaaa" + is_enabled = true + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..98b6d530f94 --- /dev/null +++ b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive1.tf @@ -0,0 +1,8 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_vcn" "test_vcn" { + cidr_block = "10.0.0.0/16" + compartment_id = "ocid1.compartment..." +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..c479a1b6735 --- /dev/null +++ b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive2.tf @@ -0,0 +1,25 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "incomplete_gateway_rule" { + display_name = "IncompleteGatewayRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + # FAIL: Solo tiene Internet Gateway, faltan los otros 12 events + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createinternetgateway", + "com.oraclecloud.virtualnetwork.updateinternetgateway", + "com.oraclecloud.virtualnetwork.deleteinternetgateway" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive3.tf b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive3.tf new file mode 100644 index 00000000000..82360d3d5b3 --- /dev/null +++ b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive3.tf @@ -0,0 +1,38 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "disabled_gateway_rule" { + display_name = "DisabledGatewayRule" + compartment_id = "ocid1.tenancy..." + description = "Has all events but is disabled" + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createinternetgateway", + "com.oraclecloud.virtualnetwork.updateinternetgateway", + "com.oraclecloud.virtualnetwork.deleteinternetgateway", + "com.oraclecloud.virtualnetwork.createnatgateway", + "com.oraclecloud.virtualnetwork.updatenatgateway", + "com.oraclecloud.virtualnetwork.deletenatgateway", + "com.oraclecloud.virtualnetwork.createservicegateway", + "com.oraclecloud.virtualnetwork.updateservicegateway", + "com.oraclecloud.virtualnetwork.deleteservicegateway", + "com.oraclecloud.virtualnetwork.createdrg", + "com.oraclecloud.virtualnetwork.updatedrg", + "com.oraclecloud.virtualnetwork.deletedrg", + "com.oraclecloud.virtualnetwork.createlocalpeeringgateway", + "com.oraclecloud.virtualnetwork.updatelocalpeeringgateway", + "com.oraclecloud.virtualnetwork.deletelocalpeeringgateway" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } + + is_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..acc708ee10b --- /dev/null +++ b/assets/queries/terraform/oci/oci_network_gateway_change_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Event Rule for Network Gateway Changes is Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for Network Gateway Changes is Missing", + "severity": "MEDIUM", + "line": 11, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Event Rule for Network Gateway Changes is Missing", + "severity": "MEDIUM", + "line": 37, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_notification_topic_without_subscription/metadata.json b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/metadata.json new file mode 100644 index 00000000000..0d8b32d31f7 --- /dev/null +++ b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "8e61709a-7ba2-4d51-a8ef-796a45e7c82c", + "queryName": "Beta - Notification Topic Without Subscription", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that OCI notification topics have at least one active subscription to deliver alerts. A topic without a subscription receives messages but does not send them to any destination, rendering monitoring alerts ineffective.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/ons_subscription", + "platform": "Terraform", + "descriptionID": "8e61709a", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_notification_topic_without_subscription/query.rego b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/query.rego new file mode 100644 index 00000000000..9e8568d9ae0 --- /dev/null +++ b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/query.rego @@ -0,0 +1,46 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: No oci_ons_notification_topic resource exists in the configuration. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + all_topics := [topic | + topic := input.document[_].resource.oci_ons_notification_topic[_] + ] + + count(all_topics) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "At least one 'oci_ons_notification_topic' resource should exist", + "keyActualValue": "No 'oci_ons_notification_topic' resource was found", + } +} + +# RULE 2: A notification topic exists but has no subscription pointing to it. +CxPolicy[result] { + doc := input.document[i] + _ := doc.resource.oci_ons_notification_topic[topic_name] + + matching_subscriptions := [sub | + sub := input.document[_].resource.oci_ons_subscription[_] + contains(sub.topic_id, topic_name) + ] + + count(matching_subscriptions) == 0 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_ons_notification_topic.%s", [topic_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_ons_notification_topic", topic_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'oci_ons_notification_topic' should have at least one associated 'oci_ons_subscription'", + "keyActualValue": "'oci_ons_notification_topic' has no subscriptions", + } +} diff --git a/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/negative1.tf b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/negative1.tf new file mode 100644 index 00000000000..85ed7f78d30 --- /dev/null +++ b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/negative1.tf @@ -0,0 +1,16 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_ons_notification_topic" "linked_topic" { + compartment_id = "ocid1.compartment..." + name = "linked-topic" +} + +# Esta suscripción apunta correctamente al tópico de arriba +resource "oci_ons_subscription" "sub_for_linked" { + compartment_id = "ocid1.compartment..." + topic_id = oci_ons_notification_topic.linked_topic.id + protocol = "EMAIL" + endpoint = "test@example.com" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/positive1.tf b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/positive1.tf new file mode 100644 index 00000000000..d4d1b09e80c --- /dev/null +++ b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/positive1.tf @@ -0,0 +1,8 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_vcn" "example_vcn" { + compartment_id = "ocid1.compartment..." + cidr_block = "10.0.0.0/16" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/positive2.tf b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/positive2.tf new file mode 100644 index 00000000000..4264e303256 --- /dev/null +++ b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/positive2.tf @@ -0,0 +1,9 @@ +provider "oci" { + region = "us-ashburn-1" +} + +# Caso: Tópico Exists but no tiene suscripción que lo apunte +resource "oci_ons_notification_topic" "orphan_topic" { + compartment_id = "ocid1.compartment..." + name = "orphan-topic" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/positive_expected_result.json new file mode 100644 index 00000000000..19040b914e2 --- /dev/null +++ b/assets/queries/terraform/oci/oci_notification_topic_without_subscription/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Notification Topic Without Subscription", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Notification Topic Without Subscription", + "severity": "MEDIUM", + "line": 6, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/metadata.json new file mode 100644 index 00000000000..be7f05cf865 --- /dev/null +++ b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "a1b080a7-8d58-4018-a14d-17beb72f5cef", + "queryName": "Beta - Event Rule for NSG Changes is Missing", + "severity": "MEDIUM", + "category": "Networking and Firewall", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on changes to Network Security Groups (NSGs). Auditing changes to NSGs is critical for detecting unauthorized modifications to network micro-segmentation rules that could expose services.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "a1b080a7", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/query.rego new file mode 100644 index 00000000000..da62046d3b6 --- /dev/null +++ b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/query.rego @@ -0,0 +1,78 @@ +package Cx + +import data.generic.common as common_lib + +expected_event_types := [ + "com.oraclecloud.virtualnetwork.createnetworksecuritygroup", + "com.oraclecloud.virtualnetwork.updatenetworksecuritygroup", + "com.oraclecloud.virtualnetwork.deletenetworksecuritygroup" +] + +# RULE 1: No rule exists in the project monitoring Network Security Group change events. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(any_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for Network Security Group changes should exist", + "keyActualValue": "No 'oci_events_rule' found for Network Security Group changes", + } +} + +# RULE 2: A rule exists but is missing some of the required events. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(matches) > 0 + count(matches) < count(expected_event_types) + + missing_count := count(expected_event_types) - count(matches) + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.condition", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "condition"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "The rule condition should include all required Network Security Group events", + "keyActualValue": sprintf("The rule is missing %d Network Security Group event(s)", [missing_count]), + } +} + +# RULE 3: A relevant rule exists but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + count(matches) > 0 + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..520332272d4 --- /dev/null +++ b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/negative1.tf @@ -0,0 +1,25 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "correct_nsg_rule" { + display_name = "CorrectNSGRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createnetworksecuritygroup", + "com.oraclecloud.virtualnetwork.updatenetworksecuritygroup", + "com.oraclecloud.virtualnetwork.deletenetworksecuritygroup" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + is_enabled = true + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..7df264f33a7 --- /dev/null +++ b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive1.tf @@ -0,0 +1,9 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_network_security_group" "test_nsg" { + compartment_id = "ocid1.compartment..." + display_name = "test-nsg" + vcn_id = "ocid1.vcn..." +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..c9d898513ee --- /dev/null +++ b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive2.tf @@ -0,0 +1,24 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "incomplete_nsg_rule" { + display_name = "IncompleteNSGRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + # FAIL: Missing "deletenetworksecuritygroup" + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createnetworksecuritygroup", + "com.oraclecloud.virtualnetwork.updatenetworksecuritygroup" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive3.tf b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive3.tf new file mode 100644 index 00000000000..2923fcaf250 --- /dev/null +++ b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive3.tf @@ -0,0 +1,26 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "disabled_nsg_rule" { + display_name = "DisabledNSGRule" + compartment_id = "ocid1.tenancy..." + description = "Has all events but is disabled" + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createnetworksecuritygroup", + "com.oraclecloud.virtualnetwork.updatenetworksecuritygroup", + "com.oraclecloud.virtualnetwork.deletenetworksecuritygroup" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } + + is_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..41ea867293b --- /dev/null +++ b/assets/queries/terraform/oci/oci_nsg_change_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Event Rule for NSG Changes is Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for NSG Changes is Missing", + "severity": "MEDIUM", + "line": 11, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Event Rule for NSG Changes is Missing", + "severity": "MEDIUM", + "line": 25, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/metadata.json b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/metadata.json new file mode 100644 index 00000000000..1ec8ee194bb --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "5ecf7b51-8c4b-4911-9d59-1f7f6b9a2143", + "queryName": "Beta - Object Storage Bucket Logging Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Ensures that OCI Object Storage buckets have object event emission enabled, which is crucial for write-level logging and security auditing.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/objectstorage_bucket#object_events_enabled", + "platform": "Terraform", + "descriptionID": "5ecf7b51", + "cloudProvider": "oci", + "cwe": "223", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/query.rego b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/query.rego new file mode 100644 index 00000000000..a5b0caa273e --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/query.rego @@ -0,0 +1,36 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: The 'object_events_enabled' attribute is missing (MissingAttribute). +# Default in OCI Terraform is false, así que su ausencia es un riesgo. +CxPolicy[result] { + bucket := input.document[i].resource.oci_objectstorage_bucket[bucket_name] + + object.get(bucket, "object_events_enabled", null) == null + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_objectstorage_bucket.%s", [bucket_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_objectstorage_bucket", bucket_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'object_events_enabled' should be present and set to 'true'", + "keyActualValue": "'object_events_enabled' is missing and defaults to 'false'", + } +} + +# RULE 2: The attribute 'object_events_enabled' está explícitamente en 'false' (IncorrectValue). +CxPolicy[result] { + bucket := input.document[i].resource.oci_objectstorage_bucket[bucket_name] + + bucket.object_events_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_objectstorage_bucket.%s.object_events_enabled", [bucket_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_objectstorage_bucket", bucket_name, "object_events_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'object_events_enabled' attribute should be 'true'", + "keyActualValue": "'object_events_enabled' attribute is 'false'", + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/negative1.tf b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/negative1.tf new file mode 100644 index 00000000000..ae49e43d76b --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/negative1.tf @@ -0,0 +1,12 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_objectstorage_bucket" "bucket_enabled_log" { + compartment_id = "ocid1.compartment.oc1..aaaa" + namespace = "my-namespace" + name = "bucket-secure" + + # PASS + object_events_enabled = true +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/positive1.tf b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/positive1.tf new file mode 100644 index 00000000000..74c9cd7b2cf --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/positive1.tf @@ -0,0 +1,11 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_objectstorage_bucket" "bucket_missing_log" { + compartment_id = "ocid1.compartment.oc1..aaaa" + namespace = "my-namespace" + name = "bucket-no-logs" + access_type = "NoPublicAccess" + # FAIL: Missing object_events_enabled +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/positive2.tf b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/positive2.tf new file mode 100644 index 00000000000..b9b5711d050 --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/positive2.tf @@ -0,0 +1,12 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_objectstorage_bucket" "bucket_disabled_log" { + compartment_id = "ocid1.compartment.oc1..aaaa" + namespace = "my-namespace" + name = "bucket-disabled-logs" + + # FAIL: Explícitamente false + object_events_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/positive_expected_result.json new file mode 100644 index 00000000000..71df47eb85d --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_logging_enabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Object Storage Bucket Logging Disabled", + "severity": "MEDIUM", + "line": 5, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Object Storage Bucket Logging Disabled", + "severity": "MEDIUM", + "line": 11, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/metadata.json b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/metadata.json new file mode 100644 index 00000000000..6f597340f2f --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "72b1c53c-619e-4e0a-937a-9af24cae69df", + "queryName": "Beta - Object Storage Bucket Versioning Disabled", + "severity": "MEDIUM", + "category": "Data Security", + "descriptionText": "Ensures that OCI Object Storage buckets have versioning enabled. Versioning protects against accidental deletion or overwriting of objects by keeping a history of all object versions.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/objectstorage_bucket#versioning", + "platform": "Terraform", + "descriptionID": "72b1c53c", + "cloudProvider": "oci", + "cwe": "668", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/query.rego b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/query.rego new file mode 100644 index 00000000000..5715e6650ca --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/query.rego @@ -0,0 +1,38 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: The 'versioning' attribute is missing en el bucket. +# Por defecto, si no se especifica, el versionado is disabled. +CxPolicy[result] { + bucket := input.document[i].resource.oci_objectstorage_bucket[bucket_name] + + object.get(bucket, "versioning", null) == null + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_objectstorage_bucket.%s", [bucket_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_objectstorage_bucket", bucket_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "'versioning' attribute should be present and set to 'Enabled'", + "keyActualValue": "'versioning' attribute is missing, disabling versioning", + } +} + +# RULE 2: The attribute 'versioning' Exists but no es 'Enabled'. +# Puede ser 'Disabled' o 'Suspended'. +CxPolicy[result] { + bucket := input.document[i].resource.oci_objectstorage_bucket[bucket_name] + + object.get(bucket, "versioning", null) != null + bucket.versioning != "Enabled" + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_objectstorage_bucket.%s.versioning", [bucket_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_objectstorage_bucket", bucket_name, "versioning"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'versioning' attribute should be 'Enabled'", + "keyActualValue": sprintf("'versioning' attribute is '%s'", [bucket.versioning]), + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/negative1.tf b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/negative1.tf new file mode 100644 index 00000000000..bd9f34d3613 --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/negative1.tf @@ -0,0 +1,12 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_objectstorage_bucket" "bucket_enabled_versioning" { + compartment_id = "ocid1.compartment.oc1..aaaa" + namespace = "my-namespace" + name = "bucket-secure" + + # PASS + versioning = "Enabled" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/positive1.tf b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/positive1.tf new file mode 100644 index 00000000000..563d15ca649 --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/positive1.tf @@ -0,0 +1,11 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_objectstorage_bucket" "bucket_missing_versioning" { + compartment_id = "ocid1.compartment.oc1..aaaa" + namespace = "my-namespace" + name = "bucket-no-ver" + access_type = "NoPublicAccess" + # FAIL: Missing The attribute versioning +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/positive2.tf b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/positive2.tf new file mode 100644 index 00000000000..2dfc0b7fd1b --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/positive2.tf @@ -0,0 +1,12 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_objectstorage_bucket" "bucket_suspended_versioning" { + compartment_id = "ocid1.compartment.oc1..aaaa" + namespace = "my-namespace" + name = "bucket-suspended" + + # FAIL: Está suspendido + versioning = "Suspended" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..1906578a1a0 --- /dev/null +++ b/assets/queries/terraform/oci/oci_objectstorage_bucket_versioning_disabled/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Object Storage Bucket Versioning Disabled", + "severity": "MEDIUM", + "line": 5, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Object Storage Bucket Versioning Disabled", + "severity": "MEDIUM", + "line": 11, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/metadata.json b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/metadata.json new file mode 100644 index 00000000000..274501f30ae --- /dev/null +++ b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "eb205044-d89b-40ac-9846-2e5993e091ab", + "queryName": "Beta - Resource Created In Root Compartment", + "severity": "HIGH", + "category": "Best Practices", + "descriptionText": "Ensures that resources are not created directly in the root compartment (tenancy). Following the principle of least privilege and separation of concerns, resources should be organized into dedicated child compartments.", + "descriptionUrl": "https://docs.oracle.com/en-us/iaas/Content/cloud-adoption-framework/iam-security-structure.htm", + "platform": "Terraform", + "descriptionID": "eb205044", + "cloudProvider": "oci", + "cwe": "284", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/query.rego b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/query.rego new file mode 100644 index 00000000000..4d7355c8cf7 --- /dev/null +++ b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/query.rego @@ -0,0 +1,54 @@ +package Cx + +import data.generic.common as common_lib +import future.keywords.in + +auditable_resource_types := { + "oci_core_instance", + "oci_core_vcn", + "oci_core_subnet", + "oci_core_security_list", + "oci_core_route_table", + "oci_core_internet_gateway", + "oci_core_nat_gateway", + "oci_core_service_gateway", + "oci_objectstorage_bucket", + "oci_database_db_system", + "oci_containerengine_cluster", + "oci_functions_application", + "oci_kms_vault", + "oci_ons_notification_topic" +} + +is_root_compartment(compartment_id) { + contains(lower(compartment_id), "var.tenancy_ocid") +} +is_root_compartment(compartment_id) { + contains(lower(compartment_id), "tenancy") +} + +CxPolicy[result] { + doc := input.document[i] + + some resource_type + resources := doc.resource[resource_type] + + resource_type in auditable_resource_types + + some resource_name + resource := resources[resource_name] + + compartment_id := object.get(resource, "compartment_id", "") + compartment_id != "" + + is_root_compartment(compartment_id) + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.%s.%s.compartment_id", [resource_type, resource_name]), + "searchLine": common_lib.build_search_line(["resource", resource_type, resource_name, "compartment_id"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("Resource '%s' should be created in a child compartment, not the root compartment", [resource_name]), + "keyActualValue": sprintf("Resource '%s' is created in the root compartment (tenancy)", [resource_name]), + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/negative1.tf b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/negative1.tf new file mode 100644 index 00000000000..94e3a98c1c7 --- /dev/null +++ b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/negative1.tf @@ -0,0 +1,17 @@ +provider "oci" { + region = "us-ashburn-1" +} + +variable "network_cmp_id" {} + +resource "oci_core_vcn" "child_vcn" { + cidr_block = "10.0.0.0/16" + compartment_id = var.network_cmp_id + display_name = "ChildVCN" +} + +resource "oci_objectstorage_bucket" "child_bucket" { + namespace = "ns" + name = "child-bucket" + compartment_id = "ocid1.compartment.oc1..bbbb..." +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/positive1.tf b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/positive1.tf new file mode 100644 index 00000000000..1baa92902ff --- /dev/null +++ b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/positive1.tf @@ -0,0 +1,12 @@ +provider "oci" { + region = "us-ashburn-1" +} + +variable "tenancy_ocid" {} + +# Caso: Uso explícito de var.tenancy_ocid +resource "oci_core_vcn" "root_vcn" { + cidr_block = "10.0.0.0/16" + compartment_id = var.tenancy_ocid + display_name = "RootVCN" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/positive2.tf b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/positive2.tf new file mode 100644 index 00000000000..37e4bc75290 --- /dev/null +++ b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/positive2.tf @@ -0,0 +1,10 @@ +provider "oci" { + region = "us-ashburn-1" +} + +# Caso: OCID hardcoded que contiene 'tenancy' +resource "oci_objectstorage_bucket" "root_bucket" { + namespace = "ns" + name = "root-bucket" + compartment_id = "ocid1.tenancy.oc1..aaaa..." +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/positive_expected_result.json new file mode 100644 index 00000000000..f1c9954b581 --- /dev/null +++ b/assets/queries/terraform/oci/oci_resource_created_in_root_compartment/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Resource Created In Root Compartment", + "severity": "HIGH", + "line": 10, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Resource Created In Root Compartment", + "severity": "HIGH", + "line": 9, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/metadata.json new file mode 100644 index 00000000000..b0bc83afbed --- /dev/null +++ b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "b3af1ce8-5711-4304-b1db-17b658629df2", + "queryName": "Beta - Event Rule for Route Table Changes is Missing", + "severity": "MEDIUM", + "category": "Networking and Firewall", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on changes to Route Tables. Auditing changes to route tables is critical for detecting unauthorized network path modifications that could lead to traffic interception or service disruption.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "b3af1ce8", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/query.rego new file mode 100644 index 00000000000..6b1ea134ea8 --- /dev/null +++ b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/query.rego @@ -0,0 +1,78 @@ +package Cx + +import data.generic.common as common_lib + +expected_event_types := [ + "com.oraclecloud.virtualnetwork.createroutetable", + "com.oraclecloud.virtualnetwork.updateroutetable", + "com.oraclecloud.virtualnetwork.deleteroutetable" +] + +# RULE 1: No rule exists in the project monitoring Route Table change events. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(any_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for Route Table changes should exist", + "keyActualValue": "No 'oci_events_rule' found for Route Table changes", + } +} + +# RULE 2: A rule exists but is missing some of the required events. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(matches) > 0 + count(matches) < count(expected_event_types) + + missing_count := count(expected_event_types) - count(matches) + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.condition", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "condition"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "The rule condition should include all required Route Table events", + "keyActualValue": sprintf("The rule is missing %d Route Table event(s)", [missing_count]), + } +} + +# RULE 3: A relevant rule exists but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + count(matches) > 0 + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..573e9e52a2a --- /dev/null +++ b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/negative1.tf @@ -0,0 +1,25 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "correct_rt_rule" { + display_name = "CorrectRTRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createroutetable", + "com.oraclecloud.virtualnetwork.updateroutetable", + "com.oraclecloud.virtualnetwork.deleteroutetable" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + is_enabled = true + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..ebf9915565f --- /dev/null +++ b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive1.tf @@ -0,0 +1,9 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_route_table" "test_rt" { + compartment_id = "ocid1.compartment..." + vcn_id = "ocid1.vcn..." + display_name = "test-rt" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..33a56a3fca4 --- /dev/null +++ b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive2.tf @@ -0,0 +1,24 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "incomplete_rt_rule" { + display_name = "IncompleteRTRule" + compartment_id = "ocid1.tenancy..." + is_enabled = true + + # FAIL: Missing "deleteroutetable" + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createroutetable", + "com.oraclecloud.virtualnetwork.updateroutetable" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive3.tf b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive3.tf new file mode 100644 index 00000000000..9e99b4f636b --- /dev/null +++ b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive3.tf @@ -0,0 +1,26 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "disabled_rt_rule" { + display_name = "DisabledRTRule" + compartment_id = "ocid1.tenancy..." + description = "Has all events but is disabled" + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createroutetable", + "com.oraclecloud.virtualnetwork.updateroutetable", + "com.oraclecloud.virtualnetwork.deleteroutetable" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic..." + } + } + + is_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..a263781a9bb --- /dev/null +++ b/assets/queries/terraform/oci/oci_route_table_change_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Event Rule for Route Table Changes is Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for Route Table Changes is Missing", + "severity": "MEDIUM", + "line": 11, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Event Rule for Route Table Changes is Missing", + "severity": "MEDIUM", + "line": 25, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/metadata.json new file mode 100644 index 00000000000..fbe51092bbe --- /dev/null +++ b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "a5dfa109-1f7d-4077-8d10-41247bfcf922", + "queryName": "Beta - Event Rule for Security List Changes is Missing", + "severity": "MEDIUM", + "category": "Networking and Firewall", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on changes to Security Lists. Auditing changes to these network ACLs is critical for detecting unauthorized modifications that could expose services to unintended traffic.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "a5dfa109", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/query.rego new file mode 100644 index 00000000000..261cb40ab8c --- /dev/null +++ b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/query.rego @@ -0,0 +1,78 @@ +package Cx + +import data.generic.common as common_lib + +expected_event_types := [ + "com.oraclecloud.virtualnetwork.createsecuritylist", + "com.oraclecloud.virtualnetwork.updatesecuritylist", + "com.oraclecloud.virtualnetwork.deletesecuritylist" +] + +# RULE 1: No rule exists in the project monitoring Security List change events. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(any_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for Security List changes should exist", + "keyActualValue": "No 'oci_events_rule' found for Security List changes", + } +} + +# RULE 2: A rule exists but is missing some of the required events. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(matches) > 0 + count(matches) < count(expected_event_types) + + missing_count := count(expected_event_types) - count(matches) + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.condition", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "condition"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "The rule condition should include all required Security List events", + "keyActualValue": sprintf("The rule is missing %d Security List event(s)", [missing_count]), + } +} + +# RULE 3: A relevant rule exists but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + count(matches) > 0 + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..f4b702e0533 --- /dev/null +++ b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/negative1.tf @@ -0,0 +1,24 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "compliant_sl_rule" { + display_name = "CompliantSLRule" + compartment_id = "ocid1.tenancy.oc1..aaaa" + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createsecuritylist", + "com.oraclecloud.virtualnetwork.updatesecuritylist", + "com.oraclecloud.virtualnetwork.deletesecuritylist" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic.oc1..aaaa" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..1183f90ad37 --- /dev/null +++ b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive1.tf @@ -0,0 +1,9 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_security_list" "test_sl" { + compartment_id = "ocid1.compartment.oc1..aaaa" + vcn_id = "ocid1.vcn.oc1..aaaa" + display_name = "vulnerable_sl" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..3af0e42cc84 --- /dev/null +++ b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive2.tf @@ -0,0 +1,23 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "incomplete_sl_rule" { + display_name = "IncompleteSLRule" + compartment_id = "ocid1.tenancy.oc1..aaaa" + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createsecuritylist", + "com.oraclecloud.virtualnetwork.updatesecuritylist" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic.oc1..aaaa" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive3.tf b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive3.tf new file mode 100644 index 00000000000..82e7e8390ac --- /dev/null +++ b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive3.tf @@ -0,0 +1,25 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "disabled_sl_rule" { + display_name = "DisabledSLRule" + compartment_id = "ocid1.tenancy.oc1..aaaa" + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createsecuritylist", + "com.oraclecloud.virtualnetwork.updatesecuritylist", + "com.oraclecloud.virtualnetwork.deletesecuritylist" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic.oc1..aaaa" + } + } + + is_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..039fbe2170b --- /dev/null +++ b/assets/queries/terraform/oci/oci_security_list_change_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Event Rule for Security List Changes is Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for Security List Changes is Missing", + "severity": "MEDIUM", + "line": 10, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Event Rule for Security List Changes is Missing", + "severity": "MEDIUM", + "line": 24, + "fileName": "positive3.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/metadata.json b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/metadata.json new file mode 100644 index 00000000000..e065cd7cada --- /dev/null +++ b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "8cd7b5db-800b-4c93-949b-bc215d63f7c3", + "queryName": "Beta - OCI Storage Admin with Delete Privileges (Manual)", + "severity": "INFO", + "category": "Identity and Access Management", + "descriptionText": "Storage service-level admins should ideally not have 'DELETE' permissions to prevent accidental or malicious data loss. The verb 'manage' includes DELETE permissions by default. Verify if a condition 'where request.permission != DELETE' is applied or if the 'manage' verb is necessary.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/identity_policy", + "platform": "Terraform", + "descriptionID": "8cd7b5db", + "cloudProvider": "oci", + "cwe": "269", + "riskScore": 0.0, + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/query.rego b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/query.rego new file mode 100644 index 00000000000..dc8d0167c53 --- /dev/null +++ b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/query.rego @@ -0,0 +1,32 @@ +package Cx + +import data.generic.common as common_lib + +storage_families := { + "object-family", + "volume-family", + "file-family", + "autonomous-database-family" +} + +CxPolicy[result] { + doc := input.document[i] + policy := doc.resource.oci_identity_policy[name] + + statement := policy.statements[_] + statement_lower := lower(statement) + + regex.match("(?i)\\bmanage\\b", statement) + + family := storage_families[_] + contains(statement_lower, family) + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_identity_policy.%s.statements", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_identity_policy", name, "statements"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("Statement granting 'manage' on '%s' should ideally exclude DELETE permissions via 'where request.permission != DELETE'", [family]), + "keyActualValue": sprintf("Statement grants full 'manage' access (including DELETE) on '%s': '%s'", [family, statement]), + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/negative1.tf b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/negative1.tf new file mode 100644 index 00000000000..e8f247c6e45 --- /dev/null +++ b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/negative1.tf @@ -0,0 +1,13 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_policy" "read_only_storage" { + name = "ReadOnlyStorage" + compartment_id = "ocid1.tenancy..." + + # PASS: El verbo es read, no incluye delete + statements = [ + "Allow group Auditors to read object-family in tenancy" + ] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/negative2.tf b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/negative2.tf new file mode 100644 index 00000000000..873d9452098 --- /dev/null +++ b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/negative2.tf @@ -0,0 +1,13 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_policy" "false_positive_check" { + name = "ManagerCheck" + compartment_id = "ocid1.tenancy..." + + # PASS: Aunque el grupo se llama Managers, el verbo es read. El regex \bmanage\b evita el fallo. + statements = [ + "Allow group ObjectManagers to read object-family in tenancy" + ] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/positive1.tf b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/positive1.tf new file mode 100644 index 00000000000..ad77ded208f --- /dev/null +++ b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/positive1.tf @@ -0,0 +1,13 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_policy" "unsafe_object_admin" { + name = "UnsafeObjectAdmin" + compartment_id = "ocid1.tenancy..." + + # FAIL: Otorga manage (incluye delete) en object-family + statements = [ + "Allow group StorageAdmins to manage object-family in tenancy" + ] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/positive2.tf b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/positive2.tf new file mode 100644 index 00000000000..7ae5cb0334d --- /dev/null +++ b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/positive2.tf @@ -0,0 +1,13 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_identity_policy" "unsafe_volume_admin" { + name = "UnsafeVolumeAdmin" + compartment_id = "ocid1.tenancy..." + + # FAIL: Otorga manage (incluye delete) en volume-family + statements = [ + "Allow group BackupAdmins to MANAGE volume-family in tenancy" + ] +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/positive_expected_result.json new file mode 100644 index 00000000000..0e46cd2dc21 --- /dev/null +++ b/assets/queries/terraform/oci/oci_storage_admin_no_delete_manual/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - OCI Storage Admin with Delete Privileges (Manual)", + "severity": "INFO", + "line": 10, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - OCI Storage Admin with Delete Privileges (Manual)", + "severity": "INFO", + "line": 10, + "fileName": "positive2.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/metadata.json b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/metadata.json new file mode 100644 index 00000000000..aa15094a4b0 --- /dev/null +++ b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "30d3bb83-f6aa-4302-a647-c404f12f8b8a", + "queryName": "Beta - VCN Subnet Without Flow Log", + "severity": "HIGH", + "category": "Observability", + "descriptionText": "VCN Subnet should have flow logging enabled to monitor traffic, as per CIS v3.0.0 recommendation 4.13.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/logging_log", + "platform": "Terraform", + "descriptionID": "30d3bb83", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "6.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/query.rego b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/query.rego new file mode 100644 index 00000000000..b558d9de386 --- /dev/null +++ b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/query.rego @@ -0,0 +1,79 @@ +package Cx + +import data.generic.common as common_lib + +# RULE 1: Does not exist ningún log asociado a la subred (Missing) +CxPolicy[result] { + doc := input.document[i] + _ := doc.resource.oci_core_subnet[subnet_name] + + associated_logs := [l | + l := input.document[_].resource.oci_logging_log[_] + contains(l.configuration.source.resource, subnet_name) + ] + count(associated_logs) == 0 + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_core_subnet.%s", [subnet_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_core_subnet", subnet_name], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "Subnet to have an associated VCN flow log", + "keyActualValue": "Subnet does not have an associated VCN flow log", + } +} + +# RULE 2: Un log de flujo is disabled +CxPolicy[result] { + doc := input.document[i] + log := doc.resource.oci_logging_log[log_name] + + log.log_type == "SERVICE" + object.get(log.configuration.source, "service", "") == "flowlogs" + log.is_enabled == false + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_logging_log.%s.is_enabled", [log_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_logging_log", log_name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be 'true'", + "keyActualValue": "'is_enabled' is 'false'", + } +} + +# RULE 3: Un log de flujo tiene el tipo incorrecto +CxPolicy[result] { + doc := input.document[i] + log := doc.resource.oci_logging_log[log_name] + + object.get(log.configuration.source, "service", "") == "flowlogs" + log.log_type != "SERVICE" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_logging_log.%s.log_type", [log_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_logging_log", log_name, "log_type"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'log_type' should be 'SERVICE'", + "keyActualValue": sprintf("'log_type' is '%s'", [log.log_type]), + } +} + +# RULE 4: Un log de flujo tiene el servicio incorrecto +CxPolicy[result] { + doc := input.document[i] + log := doc.resource.oci_logging_log[log_name] + + log.log_type == "SERVICE" + object.get(log.configuration.source, "service", "") != "flowlogs" + + result := { + "documentId": doc.id, + "searchKey": sprintf("resource.oci_logging_log.%s.configuration.source.service", [log_name]), + "searchLine": common_lib.build_search_line(["resource", "oci_logging_log", log_name, "configuration", "source", "service"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'service' should be 'flowlogs'", + "keyActualValue": sprintf("'service' is '%s'", [object.get(log.configuration.source, "service", "undefined")]), + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/negative1.tf b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..3ca9e0f1246 --- /dev/null +++ b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/negative1.tf @@ -0,0 +1,21 @@ +resource "oci_core_subnet" "secure_subnet" { + cidr_block = "10.0.1.0/24" + compartment_id = "ocid1.compartment.oc1..aaaa" + vcn_id = "ocid1.vcn.oc1..bbbb" + display_name = "secure_subnet" +} + +resource "oci_logging_log" "secure_flow_log" { + display_name = "secure_log" + log_group_id = "ocid1.loggroup.oc1..cccc" + log_type = "SERVICE" + is_enabled = true + + configuration { + source { + resource = oci_core_subnet.secure_subnet.id + service = "flowlogs" + category = "all" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive1.tf b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..aabb4d49dab --- /dev/null +++ b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive1.tf @@ -0,0 +1,6 @@ +resource "oci_core_subnet" "subnet_without_log" { + cidr_block = "10.0.1.0/24" + compartment_id = "ocid1.compartment.oc1..aaaa" + vcn_id = "ocid1.vcn.oc1..bbbb" + display_name = "orphan_subnet" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive2.tf b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive2.tf new file mode 100644 index 00000000000..e8a48a44290 --- /dev/null +++ b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive2.tf @@ -0,0 +1,15 @@ +resource "oci_logging_log" "disabled_flow_log" { + display_name = "disabled_log" + log_group_id = "ocid1.loggroup.oc1..cccc" + log_type = "SERVICE" + + # FAIL: Deshabilitado + is_enabled = false + + configuration { + source { + resource = "ocid1.subnet.oc1..aaaa" + service = "flowlogs" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive3.tf b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive3.tf new file mode 100644 index 00000000000..d436f63e1d8 --- /dev/null +++ b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive3.tf @@ -0,0 +1,15 @@ +resource "oci_logging_log" "wrong_type_log" { + display_name = "wrong_type" + log_group_id = "ocid1.loggroup.oc1..cccc" + + # FAIL: Debería ser SERVICE + log_type = "CUSTOM" + is_enabled = true + + configuration { + source { + resource = "ocid1.subnet.oc1..aaaa" + service = "flowlogs" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive4.tf b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive4.tf new file mode 100644 index 00000000000..326b7392d5e --- /dev/null +++ b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive4.tf @@ -0,0 +1,14 @@ +resource "oci_logging_log" "wrong_service_log" { + display_name = "wrong_service" + log_group_id = "ocid1.loggroup.oc1..cccc" + log_type = "SERVICE" + is_enabled = true + + configuration { + source { + resource = "ocid1.subnet.oc1..aaaa" + # FAIL: Debería ser flowlogs + service = "wronglogs" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..fc5152e402f --- /dev/null +++ b/assets/queries/terraform/oci/oci_subnet_flow_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,26 @@ +[ + { + "queryName": "Beta - VCN Subnet Without Flow Log", + "severity": "HIGH", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - VCN Subnet Without Flow Log", + "severity": "HIGH", + "line": 7, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - VCN Subnet Without Flow Log", + "severity": "HIGH", + "line": 6, + "fileName": "positive3.tf" + }, + { + "queryName": "Beta - VCN Subnet Without Flow Log", + "severity": "HIGH", + "line": 11, + "fileName": "positive4.tf" + } +] diff --git a/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/metadata.json b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/metadata.json new file mode 100644 index 00000000000..0d1a4009740 --- /dev/null +++ b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "419bb431-bf37-4e5d-bda6-af3750e808e4", + "queryName": "Beta - Event Rule for VCN Changes is Missing", + "severity": "MEDIUM", + "category": "Networking and Firewall", + "descriptionText": "Ensures that an OCI event rule is configured to monitor and alert on changes to Virtual Cloud Networks (VCNs). Auditing the creation, modification, and deletion of VCNs is important for detecting unauthorized network changes that could impact security posture.", + "descriptionUrl": "https://registry.terraform.io/providers/oracle/oci/latest/docs/resources/events_rule", + "platform": "Terraform", + "descriptionID": "419bb431", + "cloudProvider": "oci", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/query.rego b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/query.rego new file mode 100644 index 00000000000..55135d0d0c7 --- /dev/null +++ b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/query.rego @@ -0,0 +1,78 @@ +package Cx + +import data.generic.common as common_lib + +expected_event_types := [ + "com.oraclecloud.virtualnetwork.createvcn", + "com.oraclecloud.virtualnetwork.updatevcn", + "com.oraclecloud.virtualnetwork.deletevcn" +] + +# RULE 1: No rule exists in the project monitoring VCN change events. +CxPolicy[result] { + doc := input.document[i] + _ := doc.provider.oci + + any_rule := [rule | + rule := input.document[_].resource.oci_events_rule[_] + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(any_rule) == 0 + + result := { + "documentId": doc.id, + "searchKey": "provider.oci", + "searchLine": common_lib.build_search_line(["provider", "oci"], []), + "issueType": "MissingAttribute", + "keyExpectedValue": "An 'oci_events_rule' for VCN changes should exist", + "keyActualValue": "No 'oci_events_rule' found for VCN changes", + } +} + +# RULE 2: A rule exists but is missing some of the required events. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + + count(matches) > 0 + count(matches) < count(expected_event_types) + + missing_count := count(expected_event_types) - count(matches) + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.condition", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "condition"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "The rule condition should include all required VCN events", + "keyActualValue": sprintf("The rule is missing %d VCN event(s)", [missing_count]), + } +} + +# RULE 3: A relevant rule exists but is disabled. +CxPolicy[result] { + rule := input.document[i].resource.oci_events_rule[name] + + matches := [event | + event := expected_event_types[_] + contains(rule.condition, event) + ] + count(matches) > 0 + + rule.is_enabled == false + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("resource.oci_events_rule.%s.is_enabled", [name]), + "searchLine": common_lib.build_search_line(["resource", "oci_events_rule", name, "is_enabled"], []), + "issueType": "IncorrectValue", + "keyExpectedValue": "'is_enabled' should be true", + "keyActualValue": "'is_enabled' is false", + } +} diff --git a/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/negative1.tf b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/negative1.tf new file mode 100644 index 00000000000..a1a9b5dbc92 --- /dev/null +++ b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/negative1.tf @@ -0,0 +1,24 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "compliant_vcn_rule" { + display_name = "CompliantVCNRule" + compartment_id = "ocid1.tenancy.oc1..aaaa" + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createvcn", + "com.oraclecloud.virtualnetwork.updatevcn", + "com.oraclecloud.virtualnetwork.deletevcn" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic.oc1..aaaa" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive1.tf b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive1.tf new file mode 100644 index 00000000000..b6bc413c48c --- /dev/null +++ b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive1.tf @@ -0,0 +1,9 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_core_vcn" "test_vcn" { + compartment_id = "ocid1.compartment.oc1..aaaa" + cidr_block = "10.0.0.0/16" + display_name = "vulnerable_vcn" +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive2.tf b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive2.tf new file mode 100644 index 00000000000..cd99b64e5b6 --- /dev/null +++ b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive2.tf @@ -0,0 +1,23 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "incomplete_vcn_rule" { + display_name = "IncompleteVCNRule" + compartment_id = "ocid1.tenancy.oc1..aaaa" + is_enabled = true + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createvcn", + "com.oraclecloud.virtualnetwork.updatevcn" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic.oc1..aaaa" + } + } +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive3.tf b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive3.tf new file mode 100644 index 00000000000..52e7ca34bc9 --- /dev/null +++ b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive3.tf @@ -0,0 +1,25 @@ +provider "oci" { + region = "us-ashburn-1" +} + +resource "oci_events_rule" "disabled_vcn_rule" { + display_name = "DisabledVCNRule" + compartment_id = "ocid1.tenancy.oc1..aaaa" + + condition = jsonencode({ + "eventType": [ + "com.oraclecloud.virtualnetwork.createvcn", + "com.oraclecloud.virtualnetwork.updatevcn", + "com.oraclecloud.virtualnetwork.deletevcn" + ] + }) + + actions { + actions { + action_type = "ONS" + topic_id = "ocid1.onstopic.oc1..aaaa" + } + } + + is_enabled = false +} \ No newline at end of file diff --git a/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive_expected_result.json b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive_expected_result.json new file mode 100644 index 00000000000..e9d47b8ab38 --- /dev/null +++ b/assets/queries/terraform/oci/oci_vcn_change_event_rule_missing/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - Event Rule for VCN Changes is Missing", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Event Rule for VCN Changes is Missing", + "severity": "MEDIUM", + "line": 10, + "fileName": "positive2.tf" + }, + { + "queryName": "Beta - Event Rule for VCN Changes is Missing", + "severity": "MEDIUM", + "line": 24, + "fileName": "positive3.tf" + } +]