From 595c14cbd7448c71f75580dc217368b810a7c559 Mon Sep 17 00:00:00 2001 From: Antero Silva Date: Mon, 9 Mar 2026 22:33:30 +0000 Subject: [PATCH 1/2] feat(queries): add 20 Beta security queries from IaC scanner gap analysis Gap analysis comparing KICS against Trivy/tfsec, Terrascan, Snyk IaC and Prowler identified security checks present in multiple tools but absent from KICS. All queries marked experimental: true. AWS Terraform (10 queries): - Beta - EMR Cluster Local Disk Encryption Disabled (HIGH, CWE-311) - Beta - CloudFormation Stack Termination Protection Disabled (MEDIUM, CWE-693) - Beta - API Gateway V2 CORS Wildcard Origin Allowed (HIGH, CWE-942) - Beta - Step Functions State Machine Logging Disabled (MEDIUM, CWE-778) - Beta - WAFv2 Web ACL Logging Disabled (MEDIUM, CWE-778) - Beta - Cognito User Pool Advanced Security Disabled (MEDIUM, CWE-307) - Beta - Route53 Zone Query Logging Disabled (LOW, CWE-778) - Beta - EventBridge Bus Allows Cross-Account Access (HIGH, CWE-284) - Beta - ECS Task Definition Without Read-Only Root Filesystem (HIGH, CWE-732) - Beta - Kinesis Firehose Delivery Stream SSE Disabled (HIGH, CWE-311) GCP Terraform (5 queries): - Beta - Cloud SQL PostgreSQL log_checkpoints Not Enabled (MEDIUM, CWE-778) - Beta - Cloud SQL PostgreSQL log_lock_waits Not Enabled (MEDIUM, CWE-778) - Beta - GKE Node Pool Auto-Repair Disabled (MEDIUM, CWE-693) - Beta - GKE Node Pool Auto-Upgrade Disabled (MEDIUM, CWE-1188) - Beta - GKE Cluster Stackdriver Logging Disabled (MEDIUM, CWE-778) Azure Terraform (5 queries): - Beta - Key Vault Key Expiry Not Set (MEDIUM, CWE-324) - Beta - Key Vault Secret Expiry Not Set (MEDIUM, CWE-324) - Beta - Storage Account Queue Logging Disabled (MEDIUM, CWE-778) - Beta - Container Registry Admin Account Enabled (HIGH, CWE-269) - Beta - Redis Cache Publicly Accessible (HIGH, CWE-668) Each query includes query.rego, metadata.json, and positive/negative test fixtures. Gap analysis sources: Trivy/tfsec, Terrascan, Snyk IaC, Prowler. Co-Authored-By: Claude Sonnet 4.6 --- .../metadata.json | 14 ++++ .../query.rego | 21 ++++++ .../test/negative1.tf | 9 +++ .../test/positive1.tf | 9 +++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 34 ++++++++++ .../test/negative1.tf | 5 ++ .../test/positive1.tf | 5 ++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 34 ++++++++++ .../test/negative1.tf | 7 ++ .../test/positive1.tf | 7 ++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 38 +++++++++++ .../test/negative1.tf | 9 +++ .../test/positive1.tf | 9 +++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 39 +++++++++++ .../test/negative1.tf | 15 +++++ .../test/positive1.tf | 9 +++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 24 +++++++ .../test/negative1.tf | 15 +++++ .../test/positive1.tf | 13 ++++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 34 ++++++++++ .../test/negative1.tf | 15 +++++ .../test/positive1.tf | 13 ++++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 25 +++++++ .../test/negative1.tf | 8 +++ .../test/positive1.tf | 3 + .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 34 ++++++++++ .../test/negative1.tf | 15 +++++ .../test/positive1.tf | 13 ++++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../wafv2_web_acl_logging_disabled/query.rego | 27 ++++++++ .../test/negative1.tf | 19 ++++++ .../test/positive1.tf | 14 ++++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 19 ++++++ .../test/negative1.tf | 7 ++ .../test/positive1.tf | 7 ++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../key_vault_key_expiry_not_set/query.rego | 19 ++++++ .../test/negative1.tf | 8 +++ .../test/positive1.tf | 7 ++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 19 ++++++ .../test/negative1.tf | 6 ++ .../test/positive1.tf | 5 ++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 34 ++++++++++ .../test/negative1.tf | 9 +++ .../test/positive1.tf | 9 +++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 67 +++++++++++++++++++ .../test/negative1.tf | 17 +++++ .../test/positive1.tf | 17 +++++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 19 ++++++ .../test/negative1.tf | 5 ++ .../test/positive1.tf | 5 ++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 35 ++++++++++ .../test/negative1.tf | 10 +++ .../test/positive1.tf | 10 +++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 20 ++++++ .../test/negative1.tf | 10 +++ .../test/positive1.tf | 10 +++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 29 ++++++++ .../test/negative1.tf | 17 +++++ .../test/positive1.tf | 13 ++++ .../test/positive_expected_result.json | 8 +++ .../metadata.json | 14 ++++ .../query.rego | 29 ++++++++ .../test/negative1.tf | 13 ++++ .../test/positive1.tf | 13 ++++ .../test/positive_expected_result.json | 8 +++ 100 files changed, 1450 insertions(+) create mode 100644 assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/metadata.json create mode 100644 assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/query.rego create mode 100644 assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/negative1.tf create mode 100644 assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/positive1.tf create mode 100644 assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/positive_expected_result.json create mode 100644 assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/metadata.json create mode 100644 assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/query.rego create mode 100644 assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/metadata.json create mode 100644 assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/query.rego create mode 100644 assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/metadata.json create mode 100644 assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/query.rego create mode 100644 assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/negative1.tf create mode 100644 assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/positive1.tf create mode 100644 assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/positive_expected_result.json create mode 100644 assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/metadata.json create mode 100644 assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/query.rego create mode 100644 assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/metadata.json create mode 100644 assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/query.rego create mode 100644 assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/negative1.tf create mode 100644 assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/positive1.tf create mode 100644 assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/positive_expected_result.json create mode 100644 assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/metadata.json create mode 100644 assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/query.rego create mode 100644 assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/aws/route53_zone_query_logging_disabled/metadata.json create mode 100644 assets/queries/terraform/aws/route53_zone_query_logging_disabled/query.rego create mode 100644 assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/metadata.json create mode 100644 assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/query.rego create mode 100644 assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/metadata.json create mode 100644 assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/query.rego create mode 100644 assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/azure/container_registry_admin_account_enabled/metadata.json create mode 100644 assets/queries/terraform/azure/container_registry_admin_account_enabled/query.rego create mode 100644 assets/queries/terraform/azure/container_registry_admin_account_enabled/test/negative1.tf create mode 100644 assets/queries/terraform/azure/container_registry_admin_account_enabled/test/positive1.tf create mode 100644 assets/queries/terraform/azure/container_registry_admin_account_enabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/azure/key_vault_key_expiry_not_set/metadata.json create mode 100644 assets/queries/terraform/azure/key_vault_key_expiry_not_set/query.rego create mode 100644 assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/negative1.tf create mode 100644 assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/positive1.tf create mode 100644 assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/positive_expected_result.json create mode 100644 assets/queries/terraform/azure/key_vault_secret_expiry_not_set/metadata.json create mode 100644 assets/queries/terraform/azure/key_vault_secret_expiry_not_set/query.rego create mode 100644 assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/negative1.tf create mode 100644 assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/positive1.tf create mode 100644 assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/positive_expected_result.json create mode 100644 assets/queries/terraform/azure/redis_cache_publicly_accessible/metadata.json create mode 100644 assets/queries/terraform/azure/redis_cache_publicly_accessible/query.rego create mode 100644 assets/queries/terraform/azure/redis_cache_publicly_accessible/test/negative1.tf create mode 100644 assets/queries/terraform/azure/redis_cache_publicly_accessible/test/positive1.tf create mode 100644 assets/queries/terraform/azure/redis_cache_publicly_accessible/test/positive_expected_result.json create mode 100644 assets/queries/terraform/azure/storage_account_queue_logging_disabled/metadata.json create mode 100644 assets/queries/terraform/azure/storage_account_queue_logging_disabled/query.rego create mode 100644 assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/metadata.json create mode 100644 assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/query.rego create mode 100644 assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/metadata.json create mode 100644 assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/query.rego create mode 100644 assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/metadata.json create mode 100644 assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/query.rego create mode 100644 assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/negative1.tf create mode 100644 assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/positive1.tf create mode 100644 assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/positive_expected_result.json create mode 100644 assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/metadata.json create mode 100644 assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/query.rego create mode 100644 assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/negative1.tf create mode 100644 assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/positive1.tf create mode 100644 assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/positive_expected_result.json create mode 100644 assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/metadata.json create mode 100644 assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/query.rego create mode 100644 assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/negative1.tf create mode 100644 assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/positive1.tf create mode 100644 assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/positive_expected_result.json diff --git a/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/metadata.json b/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/metadata.json new file mode 100644 index 00000000000..f0c3e7e67e2 --- /dev/null +++ b/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "8af4c38a-e1d2-405b-ac41-995db44b5536", + "queryName": "Beta - API Gateway V2 CORS Wildcard Origin Allowed", + "severity": "HIGH", + "category": "Insecure Configurations", + "descriptionText": "API Gateway V2 CORS configuration should not allow all origins ('*'). Wildcard origins allow any website to make cross-origin requests, enabling CSRF attacks against authenticated API endpoints.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_api#allow_origins", + "platform": "Terraform", + "descriptionID": "c3d4e5f6", + "cloudProvider": "aws", + "cwe": "942", + "riskScore": "7.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/query.rego b/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/query.rego new file mode 100644 index 00000000000..8b7bfc00247 --- /dev/null +++ b/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/query.rego @@ -0,0 +1,21 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_apigatewayv2_api[name] + cors := resource.cors_configuration + origins := cors.allow_origins + origins[_] == "*" + result := { + "documentId": input.document[i].id, + "resourceType": "aws_apigatewayv2_api", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_apigatewayv2_api[%s].cors_configuration.allow_origins", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_apigatewayv2_api[%s].cors_configuration.allow_origins should not contain wildcard '*'", [name]), + "keyActualValue": sprintf("aws_apigatewayv2_api[%s].cors_configuration.allow_origins contains wildcard '*'", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_apigatewayv2_api", name, "cors_configuration", "allow_origins"], []), + } +} diff --git a/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/negative1.tf b/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/negative1.tf new file mode 100644 index 00000000000..3e2cf896480 --- /dev/null +++ b/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/negative1.tf @@ -0,0 +1,9 @@ +resource "aws_apigatewayv2_api" "pass" { + name = "my-api" + protocol_type = "HTTP" + + cors_configuration { + allow_origins = ["https://app.example.com"] + allow_methods = ["GET", "POST"] + } +} diff --git a/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/positive1.tf b/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/positive1.tf new file mode 100644 index 00000000000..d0fe89c4631 --- /dev/null +++ b/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/positive1.tf @@ -0,0 +1,9 @@ +resource "aws_apigatewayv2_api" "fail" { + name = "my-api" + protocol_type = "HTTP" + + cors_configuration { + allow_origins = ["*"] + allow_methods = ["GET", "POST"] + } +} diff --git a/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/positive_expected_result.json b/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/positive_expected_result.json new file mode 100644 index 00000000000..299d6e9a925 --- /dev/null +++ b/assets/queries/terraform/aws/api_gateway_v2_cors_wildcard_origin/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - API Gateway V2 CORS Wildcard Origin Allowed", + "severity": "HIGH", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/metadata.json b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/metadata.json new file mode 100644 index 00000000000..5adf1473bd0 --- /dev/null +++ b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "c1c08f98-c7f0-4b28-8a6b-f9e99763c73a", + "queryName": "Beta - CloudFormation Stack Termination Protection Disabled", + "severity": "MEDIUM", + "category": "Insecure Configurations", + "descriptionText": "CloudFormation stacks should have termination protection enabled to prevent accidental or unauthorized stack deletion, which could cause irreversible infrastructure loss.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudformation_stack#termination_protection", + "platform": "Terraform", + "descriptionID": "b2c3d4e5", + "cloudProvider": "aws", + "cwe": "693", + "riskScore": "5.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/query.rego b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/query.rego new file mode 100644 index 00000000000..6a87c4d327e --- /dev/null +++ b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/query.rego @@ -0,0 +1,34 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_cloudformation_stack[name] + not common_lib.valid_key(resource, "termination_protection") + result := { + "documentId": input.document[i].id, + "resourceType": "aws_cloudformation_stack", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_cloudformation_stack[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("aws_cloudformation_stack[%s].termination_protection should be true", [name]), + "keyActualValue": sprintf("aws_cloudformation_stack[%s].termination_protection is not defined", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_cloudformation_stack", name], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.aws_cloudformation_stack[name] + resource.termination_protection == false + result := { + "documentId": input.document[i].id, + "resourceType": "aws_cloudformation_stack", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_cloudformation_stack[%s].termination_protection", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_cloudformation_stack[%s].termination_protection should be true", [name]), + "keyActualValue": sprintf("aws_cloudformation_stack[%s].termination_protection is false", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_cloudformation_stack", name, "termination_protection"], []), + } +} diff --git a/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/negative1.tf b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/negative1.tf new file mode 100644 index 00000000000..931d1d583e7 --- /dev/null +++ b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/negative1.tf @@ -0,0 +1,5 @@ +resource "aws_cloudformation_stack" "pass" { + name = "my-stack" + template_body = file("template.yaml") + termination_protection = true +} diff --git a/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/positive1.tf b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/positive1.tf new file mode 100644 index 00000000000..fcdbe2f5628 --- /dev/null +++ b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/positive1.tf @@ -0,0 +1,5 @@ +resource "aws_cloudformation_stack" "fail" { + name = "my-stack" + template_body = file("template.yaml") + termination_protection = false +} diff --git a/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/positive_expected_result.json b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..c81495b15a3 --- /dev/null +++ b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - CloudFormation Stack Termination Protection Disabled", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/metadata.json b/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/metadata.json new file mode 100644 index 00000000000..6c29e65de27 --- /dev/null +++ b/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "ff2128f0-7084-4fec-81a6-dbd95b19a7ab", + "queryName": "Beta - Cognito User Pool Advanced Security Disabled", + "severity": "MEDIUM", + "category": "Access Control", + "descriptionText": "Amazon Cognito User Pools should enable advanced security features (AUDIT or ENFORCED mode) to detect and act on compromised credentials, anomalous sign-in attempts, and account takeover risks.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cognito_user_pool#advanced_security_mode", + "platform": "Terraform", + "descriptionID": "f6a7b8c9", + "cloudProvider": "aws", + "cwe": "307", + "riskScore": "5.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/query.rego b/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/query.rego new file mode 100644 index 00000000000..7427e873789 --- /dev/null +++ b/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/query.rego @@ -0,0 +1,34 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_cognito_user_pool[name] + not common_lib.valid_key(resource, "user_pool_add_ons") + result := { + "documentId": input.document[i].id, + "resourceType": "aws_cognito_user_pool", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_cognito_user_pool[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("aws_cognito_user_pool[%s].user_pool_add_ons.advanced_security_mode should be ENFORCED or AUDIT", [name]), + "keyActualValue": sprintf("aws_cognito_user_pool[%s].user_pool_add_ons is not defined", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_cognito_user_pool", name], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.aws_cognito_user_pool[name] + resource.user_pool_add_ons.advanced_security_mode == "OFF" + result := { + "documentId": input.document[i].id, + "resourceType": "aws_cognito_user_pool", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_cognito_user_pool[%s].user_pool_add_ons.advanced_security_mode", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_cognito_user_pool[%s].user_pool_add_ons.advanced_security_mode should be ENFORCED or AUDIT", [name]), + "keyActualValue": sprintf("aws_cognito_user_pool[%s].user_pool_add_ons.advanced_security_mode is OFF", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_cognito_user_pool", name, "user_pool_add_ons", "advanced_security_mode"], []), + } +} diff --git a/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/negative1.tf b/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/negative1.tf new file mode 100644 index 00000000000..e2f54a5ca6d --- /dev/null +++ b/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/negative1.tf @@ -0,0 +1,7 @@ +resource "aws_cognito_user_pool" "pass" { + name = "my-user-pool" + + user_pool_add_ons { + advanced_security_mode = "ENFORCED" + } +} diff --git a/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/positive1.tf b/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/positive1.tf new file mode 100644 index 00000000000..86d464d80e8 --- /dev/null +++ b/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/positive1.tf @@ -0,0 +1,7 @@ +resource "aws_cognito_user_pool" "fail" { + name = "my-user-pool" + + user_pool_add_ons { + advanced_security_mode = "OFF" + } +} diff --git a/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/positive_expected_result.json b/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..1d9c08f39b5 --- /dev/null +++ b/assets/queries/terraform/aws/cognito_user_pool_advanced_security_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Cognito User Pool Advanced Security Disabled", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/metadata.json b/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/metadata.json new file mode 100644 index 00000000000..768215b9607 --- /dev/null +++ b/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "2fb51f09-ac2b-4424-a394-a20c61fdde77", + "queryName": "Beta - ECS Task Definition Without Read-Only Root Filesystem", + "severity": "HIGH", + "category": "Insecure Configurations", + "descriptionText": "ECS container definitions should set readonlyRootFilesystem to true to prevent containers from writing to the root filesystem, limiting the blast radius of a container compromise and preventing persistent malware installation.", + "descriptionUrl": "https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#container_definition_security", + "platform": "Terraform", + "descriptionID": "c9d0e1f2", + "cloudProvider": "aws", + "cwe": "732", + "riskScore": "7.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/query.rego b/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/query.rego new file mode 100644 index 00000000000..de3ab8d5419 --- /dev/null +++ b/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/query.rego @@ -0,0 +1,38 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_ecs_task_definition[name] + defs := json.unmarshal(resource.container_definitions) + container := defs[_] + not common_lib.valid_key(container, "readonlyRootFilesystem") + result := { + "documentId": input.document[i].id, + "resourceType": "aws_ecs_task_definition", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_ecs_task_definition[%s].container_definitions", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("aws_ecs_task_definition[%s] container_definitions should have readonlyRootFilesystem: true", [name]), + "keyActualValue": sprintf("aws_ecs_task_definition[%s] has a container without readonlyRootFilesystem defined", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_ecs_task_definition", name, "container_definitions"], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.aws_ecs_task_definition[name] + defs := json.unmarshal(resource.container_definitions) + container := defs[_] + container.readonlyRootFilesystem == false + result := { + "documentId": input.document[i].id, + "resourceType": "aws_ecs_task_definition", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_ecs_task_definition[%s].container_definitions", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_ecs_task_definition[%s] containers should have readonlyRootFilesystem: true", [name]), + "keyActualValue": sprintf("aws_ecs_task_definition[%s] has a container with readonlyRootFilesystem: false", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_ecs_task_definition", name, "container_definitions"], []), + } +} diff --git a/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/negative1.tf b/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/negative1.tf new file mode 100644 index 00000000000..abc7d3d1dfe --- /dev/null +++ b/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/negative1.tf @@ -0,0 +1,9 @@ +resource "aws_ecs_task_definition" "pass" { + family = "my-task" + container_definitions = jsonencode([{ + name = "my-container" + image = "nginx:latest" + essential = true + readonlyRootFilesystem = true + }]) +} diff --git a/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/positive1.tf b/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/positive1.tf new file mode 100644 index 00000000000..5ada1e95ba8 --- /dev/null +++ b/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/positive1.tf @@ -0,0 +1,9 @@ +resource "aws_ecs_task_definition" "fail" { + family = "my-task" + container_definitions = jsonencode([{ + name = "my-container" + image = "nginx:latest" + essential = true + readonlyRootFilesystem = false + }]) +} diff --git a/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/positive_expected_result.json b/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/positive_expected_result.json new file mode 100644 index 00000000000..8856645e52b --- /dev/null +++ b/assets/queries/terraform/aws/ecs_task_definition_without_read_only_root_filesystem/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - ECS Task Definition Without Read-Only Root Filesystem", + "severity": "HIGH", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/metadata.json b/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/metadata.json new file mode 100644 index 00000000000..b3171b5dd67 --- /dev/null +++ b/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "97d14cd6-5d27-466f-ad96-cc2fac23e8be", + "queryName": "Beta - EMR Cluster Local Disk Encryption Disabled", + "severity": "HIGH", + "category": "Encryption", + "descriptionText": "Amazon EMR security configurations should enable local disk encryption for cluster nodes to protect data stored on locally attached disks against unauthorized access.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/emr_security_configuration", + "platform": "Terraform", + "descriptionID": "a1b2c3d4", + "cloudProvider": "aws", + "cwe": "311", + "riskScore": "7.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/query.rego b/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/query.rego new file mode 100644 index 00000000000..2dc9249ba7c --- /dev/null +++ b/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/query.rego @@ -0,0 +1,39 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_emr_security_configuration[name] + cfg := json.unmarshal(resource.configuration) + enc := cfg.EncryptionConfiguration + not common_lib.valid_key(enc, "AtRestEncryptionConfiguration") + result := { + "documentId": input.document[i].id, + "resourceType": "aws_emr_security_configuration", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_emr_security_configuration[%s].configuration", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("aws_emr_security_configuration[%s] should define AtRestEncryptionConfiguration with LocalDiskEncryptionConfiguration", [name]), + "keyActualValue": sprintf("aws_emr_security_configuration[%s] has no AtRestEncryptionConfiguration", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_emr_security_configuration", name, "configuration"], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.aws_emr_security_configuration[name] + cfg := json.unmarshal(resource.configuration) + enc := cfg.EncryptionConfiguration + at_rest := enc.AtRestEncryptionConfiguration + not common_lib.valid_key(at_rest, "LocalDiskEncryptionConfiguration") + result := { + "documentId": input.document[i].id, + "resourceType": "aws_emr_security_configuration", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_emr_security_configuration[%s].configuration", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("aws_emr_security_configuration[%s] should define LocalDiskEncryptionConfiguration", [name]), + "keyActualValue": sprintf("aws_emr_security_configuration[%s].AtRestEncryptionConfiguration is missing LocalDiskEncryptionConfiguration", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_emr_security_configuration", name, "configuration"], []), + } +} diff --git a/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/negative1.tf b/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/negative1.tf new file mode 100644 index 00000000000..64a32fa19ff --- /dev/null +++ b/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/negative1.tf @@ -0,0 +1,15 @@ +resource "aws_emr_security_configuration" "pass" { + name = "my-emr-sec-config" + configuration = jsonencode({ + EncryptionConfiguration = { + EnableInTransitEncryption = true + EnableAtRestEncryption = true + AtRestEncryptionConfiguration = { + LocalDiskEncryptionConfiguration = { + EncryptionKeyProviderType = "AwsKms" + AwsKmsKey = "arn:aws:kms:us-east-1:123456789012:key/example" + } + } + } + }) +} diff --git a/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/positive1.tf b/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/positive1.tf new file mode 100644 index 00000000000..bd0b8c38c42 --- /dev/null +++ b/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/positive1.tf @@ -0,0 +1,9 @@ +resource "aws_emr_security_configuration" "fail" { + name = "my-emr-sec-config" + configuration = jsonencode({ + EncryptionConfiguration = { + EnableInTransitEncryption = true + EnableAtRestEncryption = false + } + }) +} diff --git a/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/positive_expected_result.json b/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..220ef9f2d5c --- /dev/null +++ b/assets/queries/terraform/aws/emr_cluster_local_disk_encryption_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - EMR Cluster Local Disk Encryption Disabled", + "severity": "HIGH", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/metadata.json b/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/metadata.json new file mode 100644 index 00000000000..fbaeccd241c --- /dev/null +++ b/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "22383f22-a900-4d5d-b0fe-e2b28cc5d1a6", + "queryName": "Beta - EventBridge Bus Allows Cross-Account Access", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "Amazon EventBridge event bus resource policies should not allow unrestricted cross-account access (Principal: '*'). Overly permissive policies allow any AWS account to publish events, potentially enabling data injection attacks.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_bus_policy", + "platform": "Terraform", + "descriptionID": "b8c9d0e1", + "cloudProvider": "aws", + "cwe": "284", + "riskScore": "7.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/query.rego b/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/query.rego new file mode 100644 index 00000000000..b5e27c32414 --- /dev/null +++ b/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/query.rego @@ -0,0 +1,24 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_cloudwatch_event_bus_policy[name] + doc := json.unmarshal(resource.policy) + stmt := doc.Statement[_] + principal := stmt.Principal + is_string(principal) + principal == "*" + stmt.Effect == "Allow" + result := { + "documentId": input.document[i].id, + "resourceType": "aws_cloudwatch_event_bus_policy", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_cloudwatch_event_bus_policy[%s].policy", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_cloudwatch_event_bus_policy[%s].policy should restrict Principal to specific accounts", [name]), + "keyActualValue": sprintf("aws_cloudwatch_event_bus_policy[%s].policy allows Principal '*' (any account)", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_cloudwatch_event_bus_policy", name, "policy"], []), + } +} diff --git a/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/negative1.tf b/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/negative1.tf new file mode 100644 index 00000000000..297d8cd3178 --- /dev/null +++ b/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/negative1.tf @@ -0,0 +1,15 @@ +resource "aws_cloudwatch_event_bus_policy" "pass" { + event_bus_name = aws_cloudwatch_event_bus.main.name + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "AllowSpecificAccount" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::123456789012:root" + } + Action = "events:PutEvents" + Resource = aws_cloudwatch_event_bus.main.arn + }] + }) +} diff --git a/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/positive1.tf b/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/positive1.tf new file mode 100644 index 00000000000..37948b8afa1 --- /dev/null +++ b/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/positive1.tf @@ -0,0 +1,13 @@ +resource "aws_cloudwatch_event_bus_policy" "fail" { + event_bus_name = aws_cloudwatch_event_bus.main.name + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "AllowAll" + Effect = "Allow" + Principal = "*" + Action = "events:PutEvents" + Resource = aws_cloudwatch_event_bus.main.arn + }] + }) +} diff --git a/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/positive_expected_result.json b/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/positive_expected_result.json new file mode 100644 index 00000000000..510b316b8c2 --- /dev/null +++ b/assets/queries/terraform/aws/eventbridge_bus_allows_cross_account_access/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - EventBridge Bus Allows Cross-Account Access", + "severity": "HIGH", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/metadata.json b/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/metadata.json new file mode 100644 index 00000000000..5f3f666fd6e --- /dev/null +++ b/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "59f7a480-7ad5-49af-b5f2-d3d20e43217e", + "queryName": "Beta - Kinesis Firehose Delivery Stream SSE Disabled", + "severity": "HIGH", + "category": "Encryption", + "descriptionText": "Amazon Kinesis Firehose delivery streams should have server-side encryption (SSE) enabled to protect streaming data at rest using KMS keys.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kinesis_firehose_delivery_stream#server_side_encryption", + "platform": "Terraform", + "descriptionID": "d0e1f2a3", + "cloudProvider": "aws", + "cwe": "311", + "riskScore": "7.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/query.rego b/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/query.rego new file mode 100644 index 00000000000..bc7989f3eba --- /dev/null +++ b/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/query.rego @@ -0,0 +1,34 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_kinesis_firehose_delivery_stream[name] + not common_lib.valid_key(resource, "server_side_encryption") + result := { + "documentId": input.document[i].id, + "resourceType": "aws_kinesis_firehose_delivery_stream", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_kinesis_firehose_delivery_stream[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("aws_kinesis_firehose_delivery_stream[%s].server_side_encryption should be enabled", [name]), + "keyActualValue": sprintf("aws_kinesis_firehose_delivery_stream[%s].server_side_encryption is not defined", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_kinesis_firehose_delivery_stream", name], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.aws_kinesis_firehose_delivery_stream[name] + resource.server_side_encryption.enabled == false + result := { + "documentId": input.document[i].id, + "resourceType": "aws_kinesis_firehose_delivery_stream", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_kinesis_firehose_delivery_stream[%s].server_side_encryption.enabled", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_kinesis_firehose_delivery_stream[%s].server_side_encryption.enabled should be true", [name]), + "keyActualValue": sprintf("aws_kinesis_firehose_delivery_stream[%s].server_side_encryption.enabled is false", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_kinesis_firehose_delivery_stream", name, "server_side_encryption", "enabled"], []), + } +} diff --git a/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/negative1.tf b/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/negative1.tf new file mode 100644 index 00000000000..ffdd5543f7e --- /dev/null +++ b/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/negative1.tf @@ -0,0 +1,15 @@ +resource "aws_kinesis_firehose_delivery_stream" "pass" { + name = "my-firehose" + destination = "s3" + + s3_configuration { + role_arn = aws_iam_role.firehose.arn + bucket_arn = aws_s3_bucket.dest.arn + } + + server_side_encryption { + enabled = true + key_type = "CUSTOMER_MANAGED_CMK" + key_arn = aws_kms_key.firehose.arn + } +} diff --git a/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/positive1.tf b/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/positive1.tf new file mode 100644 index 00000000000..4b29001381e --- /dev/null +++ b/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/positive1.tf @@ -0,0 +1,13 @@ +resource "aws_kinesis_firehose_delivery_stream" "fail" { + name = "my-firehose" + destination = "s3" + + s3_configuration { + role_arn = aws_iam_role.firehose.arn + bucket_arn = aws_s3_bucket.dest.arn + } + + server_side_encryption { + enabled = false + } +} diff --git a/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/positive_expected_result.json b/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..7b5ff79f0c5 --- /dev/null +++ b/assets/queries/terraform/aws/kinesis_firehose_delivery_stream_sse_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Kinesis Firehose Delivery Stream SSE Disabled", + "severity": "HIGH", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/route53_zone_query_logging_disabled/metadata.json b/assets/queries/terraform/aws/route53_zone_query_logging_disabled/metadata.json new file mode 100644 index 00000000000..d146e6bf3ab --- /dev/null +++ b/assets/queries/terraform/aws/route53_zone_query_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "339cb652-abbf-485c-b18e-2ff865c26fc0", + "queryName": "Beta - Route53 Zone Query Logging Disabled", + "severity": "LOW", + "category": "Observability", + "descriptionText": "Amazon Route 53 hosted zones should have query logging enabled to track DNS queries for security analysis, troubleshooting, and compliance with audit requirements.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_query_log", + "platform": "Terraform", + "descriptionID": "a7b8c9d0", + "cloudProvider": "aws", + "cwe": "778", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/route53_zone_query_logging_disabled/query.rego b/assets/queries/terraform/aws/route53_zone_query_logging_disabled/query.rego new file mode 100644 index 00000000000..4beae06bddc --- /dev/null +++ b/assets/queries/terraform/aws/route53_zone_query_logging_disabled/query.rego @@ -0,0 +1,25 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_route53_zone[name] + zone_id := resource.zone_id + not any_query_log_for_zone(zone_id) + + result := { + "documentId": input.document[i].id, + "resourceType": "aws_route53_zone", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_route53_zone[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("aws_route53_zone[%s] should have an associated aws_route53_query_log resource", [name]), + "keyActualValue": sprintf("aws_route53_zone[%s] has no query logging configured", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_route53_zone", name], []), + } +} + +any_query_log_for_zone(zone_id) { + _ := input.document[_].resource.aws_route53_query_log[_] +} diff --git a/assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/negative1.tf b/assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..617d8330d27 --- /dev/null +++ b/assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/negative1.tf @@ -0,0 +1,8 @@ +resource "aws_route53_zone" "pass" { + name = "example.com" +} + +resource "aws_route53_query_log" "pass" { + cloudwatch_log_group_arn = aws_cloudwatch_log_group.dns.arn + zone_id = aws_route53_zone.pass.zone_id +} diff --git a/assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/positive1.tf b/assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..063ec94081c --- /dev/null +++ b/assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/positive1.tf @@ -0,0 +1,3 @@ +resource "aws_route53_zone" "fail" { + name = "example.com" +} diff --git a/assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..8a22b6cd312 --- /dev/null +++ b/assets/queries/terraform/aws/route53_zone_query_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Route53 Zone Query Logging Disabled", + "severity": "LOW", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/metadata.json b/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/metadata.json new file mode 100644 index 00000000000..af5a072a473 --- /dev/null +++ b/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "fb07e5be-c561-4d33-afb1-fb6632bdc885", + "queryName": "Beta - Step Functions State Machine Logging Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "AWS Step Functions state machines should have logging enabled to CloudWatch Logs to provide execution history, error details, and audit trails for workflow debugging and compliance.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sfn_state_machine#logging_configuration", + "platform": "Terraform", + "descriptionID": "d4e5f6a7", + "cloudProvider": "aws", + "cwe": "778", + "riskScore": "4.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/query.rego b/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/query.rego new file mode 100644 index 00000000000..5fdc4e3b389 --- /dev/null +++ b/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/query.rego @@ -0,0 +1,34 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_sfn_state_machine[name] + not common_lib.valid_key(resource, "logging_configuration") + result := { + "documentId": input.document[i].id, + "resourceType": "aws_sfn_state_machine", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_sfn_state_machine[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("aws_sfn_state_machine[%s].logging_configuration should be defined with level != OFF", [name]), + "keyActualValue": sprintf("aws_sfn_state_machine[%s].logging_configuration is not defined", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_sfn_state_machine", name], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.aws_sfn_state_machine[name] + resource.logging_configuration.level == "OFF" + result := { + "documentId": input.document[i].id, + "resourceType": "aws_sfn_state_machine", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_sfn_state_machine[%s].logging_configuration.level", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("aws_sfn_state_machine[%s].logging_configuration.level should be ALL, ERROR, or FATAL", [name]), + "keyActualValue": sprintf("aws_sfn_state_machine[%s].logging_configuration.level is OFF", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_sfn_state_machine", name, "logging_configuration", "level"], []), + } +} diff --git a/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/negative1.tf b/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..773469167bf --- /dev/null +++ b/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/negative1.tf @@ -0,0 +1,15 @@ +resource "aws_sfn_state_machine" "pass" { + name = "my-state-machine" + role_arn = aws_iam_role.sfn_role.arn + definition = jsonencode({ + Comment = "My state machine" + StartAt = "HelloWorld" + States = {} + }) + + logging_configuration { + level = "ALL" + include_execution_data = true + log_destination = "${aws_cloudwatch_log_group.sfn.arn}:*" + } +} diff --git a/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/positive1.tf b/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..53d6e5621f7 --- /dev/null +++ b/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/positive1.tf @@ -0,0 +1,13 @@ +resource "aws_sfn_state_machine" "fail" { + name = "my-state-machine" + role_arn = aws_iam_role.sfn_role.arn + definition = jsonencode({ + Comment = "My state machine" + StartAt = "HelloWorld" + States = {} + }) + + logging_configuration { + level = "OFF" + } +} diff --git a/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..a636a9ad58a --- /dev/null +++ b/assets/queries/terraform/aws/step_functions_state_machine_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Step Functions State Machine Logging Disabled", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/metadata.json b/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/metadata.json new file mode 100644 index 00000000000..d234a990005 --- /dev/null +++ b/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "3795666c-7407-44b1-8983-f1bf8f2460ac", + "queryName": "Beta - WAFv2 Web ACL Logging Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "AWS WAFv2 Web ACLs should have logging enabled to track web requests, rule evaluations, and blocked traffic. WAF logs are essential for incident response and security analysis.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/wafv2_web_acl_logging_configuration", + "platform": "Terraform", + "descriptionID": "e5f6a7b8", + "cloudProvider": "aws", + "cwe": "778", + "riskScore": "5.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/query.rego b/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/query.rego new file mode 100644 index 00000000000..ea9adf2316a --- /dev/null +++ b/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/query.rego @@ -0,0 +1,27 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_wafv2_web_acl[name] + acl_name := terra_lib.get_resource_name(resource, name) + + # Check there is no corresponding aws_wafv2_web_acl_logging_configuration + not any_logging_config_exists(acl_name) + + result := { + "documentId": input.document[i].id, + "resourceType": "aws_wafv2_web_acl", + "resourceName": acl_name, + "searchKey": sprintf("aws_wafv2_web_acl[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("aws_wafv2_web_acl[%s] should have an associated aws_wafv2_web_acl_logging_configuration", [name]), + "keyActualValue": sprintf("aws_wafv2_web_acl[%s] has no logging configuration", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_wafv2_web_acl", name], []), + } +} + +any_logging_config_exists(acl_name) { + _ := input.document[_].resource.aws_wafv2_web_acl_logging_configuration[_] +} diff --git a/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/negative1.tf b/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..37d17c5a0d0 --- /dev/null +++ b/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/negative1.tf @@ -0,0 +1,19 @@ +resource "aws_wafv2_web_acl" "pass" { + name = "my-waf-acl" + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "my-waf-metric" + sampled_requests_enabled = true + } +} + +resource "aws_wafv2_web_acl_logging_configuration" "pass_log" { + log_destination_configs = [aws_kinesis_firehose_delivery_stream.waf.arn] + resource_arn = aws_wafv2_web_acl.pass.arn +} diff --git a/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/positive1.tf b/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..d8b84a3677c --- /dev/null +++ b/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/positive1.tf @@ -0,0 +1,14 @@ +resource "aws_wafv2_web_acl" "fail" { + name = "my-waf-acl" + scope = "REGIONAL" + + default_action { + allow {} + } + + visibility_config { + cloudwatch_metrics_enabled = true + metric_name = "my-waf-metric" + sampled_requests_enabled = true + } +} diff --git a/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..dc417682bdf --- /dev/null +++ b/assets/queries/terraform/aws/wafv2_web_acl_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - WAFv2 Web ACL Logging Disabled", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/azure/container_registry_admin_account_enabled/metadata.json b/assets/queries/terraform/azure/container_registry_admin_account_enabled/metadata.json new file mode 100644 index 00000000000..30437249228 --- /dev/null +++ b/assets/queries/terraform/azure/container_registry_admin_account_enabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "bd252825-fc09-4737-bbab-755d6096af19", + "queryName": "Beta - Container Registry Admin Account Enabled", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "Azure Container Registry admin account should be disabled. The admin account uses a shared, long-lived password that cannot be scoped to specific operations or individuals, violating least-privilege principles.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_registry#admin_enabled", + "platform": "Terraform", + "descriptionID": "a9b0c1d2", + "cloudProvider": "azure", + "cwe": "269", + "riskScore": "7.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/container_registry_admin_account_enabled/query.rego b/assets/queries/terraform/azure/container_registry_admin_account_enabled/query.rego new file mode 100644 index 00000000000..b27b2a3a8d7 --- /dev/null +++ b/assets/queries/terraform/azure/container_registry_admin_account_enabled/query.rego @@ -0,0 +1,19 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_container_registry[name] + resource.admin_enabled == true + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_container_registry", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_container_registry[%s].admin_enabled", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("azurerm_container_registry[%s].admin_enabled should be false", [name]), + "keyActualValue": sprintf("azurerm_container_registry[%s].admin_enabled is true", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_container_registry", name, "admin_enabled"], []), + } +} diff --git a/assets/queries/terraform/azure/container_registry_admin_account_enabled/test/negative1.tf b/assets/queries/terraform/azure/container_registry_admin_account_enabled/test/negative1.tf new file mode 100644 index 00000000000..634706e433f --- /dev/null +++ b/assets/queries/terraform/azure/container_registry_admin_account_enabled/test/negative1.tf @@ -0,0 +1,7 @@ +resource "azurerm_container_registry" "pass" { + name = "myContainerRegistry" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + sku = "Standard" + admin_enabled = false +} diff --git a/assets/queries/terraform/azure/container_registry_admin_account_enabled/test/positive1.tf b/assets/queries/terraform/azure/container_registry_admin_account_enabled/test/positive1.tf new file mode 100644 index 00000000000..06d36fee9df --- /dev/null +++ b/assets/queries/terraform/azure/container_registry_admin_account_enabled/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_container_registry" "fail" { + name = "myContainerRegistry" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + sku = "Standard" + admin_enabled = true +} diff --git a/assets/queries/terraform/azure/container_registry_admin_account_enabled/test/positive_expected_result.json b/assets/queries/terraform/azure/container_registry_admin_account_enabled/test/positive_expected_result.json new file mode 100644 index 00000000000..1a3695a9ffc --- /dev/null +++ b/assets/queries/terraform/azure/container_registry_admin_account_enabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Container Registry Admin Account Enabled", + "severity": "HIGH", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/azure/key_vault_key_expiry_not_set/metadata.json b/assets/queries/terraform/azure/key_vault_key_expiry_not_set/metadata.json new file mode 100644 index 00000000000..be3dd1b9ba9 --- /dev/null +++ b/assets/queries/terraform/azure/key_vault_key_expiry_not_set/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "32d4621f-f5c5-4630-a0a1-3f850b1030e4", + "queryName": "Beta - Key Vault Key Expiry Not Set", + "severity": "MEDIUM", + "category": "Insecure Configurations", + "descriptionText": "Azure Key Vault cryptographic keys should have an expiration date set to enforce key rotation and reduce the risk associated with long-lived keys. Keys without expiry remain valid indefinitely.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_key#expiration_date", + "platform": "Terraform", + "descriptionID": "d6e7f8a9", + "cloudProvider": "azure", + "cwe": "324", + "riskScore": "5.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/key_vault_key_expiry_not_set/query.rego b/assets/queries/terraform/azure/key_vault_key_expiry_not_set/query.rego new file mode 100644 index 00000000000..472a3ecb8af --- /dev/null +++ b/assets/queries/terraform/azure/key_vault_key_expiry_not_set/query.rego @@ -0,0 +1,19 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_key_vault_key[name] + not common_lib.valid_key(resource, "expiration_date") + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_key_vault_key", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_key_vault_key[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("azurerm_key_vault_key[%s].expiration_date should be set", [name]), + "keyActualValue": sprintf("azurerm_key_vault_key[%s].expiration_date is not defined", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_key_vault_key", name], []), + } +} diff --git a/assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/negative1.tf b/assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/negative1.tf new file mode 100644 index 00000000000..25be21d1ba1 --- /dev/null +++ b/assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/negative1.tf @@ -0,0 +1,8 @@ +resource "azurerm_key_vault_key" "pass" { + name = "my-key" + key_vault_id = azurerm_key_vault.kv.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "verify"] + expiration_date = "2027-01-01T00:00:00Z" +} diff --git a/assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/positive1.tf b/assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/positive1.tf new file mode 100644 index 00000000000..4f43dad9938 --- /dev/null +++ b/assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/positive1.tf @@ -0,0 +1,7 @@ +resource "azurerm_key_vault_key" "fail" { + name = "my-key" + key_vault_id = azurerm_key_vault.kv.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "verify"] +} diff --git a/assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/positive_expected_result.json b/assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/positive_expected_result.json new file mode 100644 index 00000000000..e513ecef0b5 --- /dev/null +++ b/assets/queries/terraform/azure/key_vault_key_expiry_not_set/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Key Vault Key Expiry Not Set", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/metadata.json b/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/metadata.json new file mode 100644 index 00000000000..926a9b13736 --- /dev/null +++ b/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "8bdaa22b-84f0-4547-9842-dd4d636fa0c9", + "queryName": "Beta - Key Vault Secret Expiry Not Set", + "severity": "MEDIUM", + "category": "Insecure Configurations", + "descriptionText": "Azure Key Vault secrets should have an expiration date set to enforce regular rotation of credentials and API keys, limiting the window of exposure if a secret is compromised.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_secret#expiration_date", + "platform": "Terraform", + "descriptionID": "e7f8a9b0", + "cloudProvider": "azure", + "cwe": "324", + "riskScore": "5.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/query.rego b/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/query.rego new file mode 100644 index 00000000000..14171b48d99 --- /dev/null +++ b/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/query.rego @@ -0,0 +1,19 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_key_vault_secret[name] + not common_lib.valid_key(resource, "expiration_date") + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_key_vault_secret", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_key_vault_secret[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("azurerm_key_vault_secret[%s].expiration_date should be set", [name]), + "keyActualValue": sprintf("azurerm_key_vault_secret[%s].expiration_date is not defined", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_key_vault_secret", name], []), + } +} diff --git a/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/negative1.tf b/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/negative1.tf new file mode 100644 index 00000000000..57bd2767a38 --- /dev/null +++ b/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/negative1.tf @@ -0,0 +1,6 @@ +resource "azurerm_key_vault_secret" "pass" { + name = "my-secret" + value = "super-secret-value" + key_vault_id = azurerm_key_vault.kv.id + expiration_date = "2027-01-01T00:00:00Z" +} diff --git a/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/positive1.tf b/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/positive1.tf new file mode 100644 index 00000000000..370ab58cd0d --- /dev/null +++ b/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/positive1.tf @@ -0,0 +1,5 @@ +resource "azurerm_key_vault_secret" "fail" { + name = "my-secret" + value = "super-secret-value" + key_vault_id = azurerm_key_vault.kv.id +} diff --git a/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/positive_expected_result.json b/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/positive_expected_result.json new file mode 100644 index 00000000000..cedcdb74ae4 --- /dev/null +++ b/assets/queries/terraform/azure/key_vault_secret_expiry_not_set/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Key Vault Secret Expiry Not Set", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/azure/redis_cache_publicly_accessible/metadata.json b/assets/queries/terraform/azure/redis_cache_publicly_accessible/metadata.json new file mode 100644 index 00000000000..b69f0f588be --- /dev/null +++ b/assets/queries/terraform/azure/redis_cache_publicly_accessible/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "5c1a5d5f-5c87-4e13-b3fe-1822a3612dfb", + "queryName": "Beta - Redis Cache Publicly Accessible", + "severity": "HIGH", + "category": "Networking and Firewall", + "descriptionText": "Azure Redis Cache instances should disable public network access so they are only reachable through private endpoints, preventing direct internet access to cached data.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/redis_cache#public_network_access_enabled", + "platform": "Terraform", + "descriptionID": "c1d2e3f4", + "cloudProvider": "azure", + "cwe": "668", + "riskScore": "7.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/redis_cache_publicly_accessible/query.rego b/assets/queries/terraform/azure/redis_cache_publicly_accessible/query.rego new file mode 100644 index 00000000000..78b6a404be1 --- /dev/null +++ b/assets/queries/terraform/azure/redis_cache_publicly_accessible/query.rego @@ -0,0 +1,34 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_redis_cache[name] + not common_lib.valid_key(resource, "public_network_access_enabled") + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_redis_cache", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_redis_cache[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("azurerm_redis_cache[%s].public_network_access_enabled should be false", [name]), + "keyActualValue": sprintf("azurerm_redis_cache[%s].public_network_access_enabled is not defined (defaults to true)", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_redis_cache", name], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_redis_cache[name] + resource.public_network_access_enabled == true + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_redis_cache", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_redis_cache[%s].public_network_access_enabled", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("azurerm_redis_cache[%s].public_network_access_enabled should be false", [name]), + "keyActualValue": sprintf("azurerm_redis_cache[%s].public_network_access_enabled is true", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_redis_cache", name, "public_network_access_enabled"], []), + } +} diff --git a/assets/queries/terraform/azure/redis_cache_publicly_accessible/test/negative1.tf b/assets/queries/terraform/azure/redis_cache_publicly_accessible/test/negative1.tf new file mode 100644 index 00000000000..26f02c6b4f2 --- /dev/null +++ b/assets/queries/terraform/azure/redis_cache_publicly_accessible/test/negative1.tf @@ -0,0 +1,9 @@ +resource "azurerm_redis_cache" "pass" { + name = "my-redis" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + capacity = 1 + family = "C" + sku_name = "Standard" + public_network_access_enabled = false +} diff --git a/assets/queries/terraform/azure/redis_cache_publicly_accessible/test/positive1.tf b/assets/queries/terraform/azure/redis_cache_publicly_accessible/test/positive1.tf new file mode 100644 index 00000000000..9035e058115 --- /dev/null +++ b/assets/queries/terraform/azure/redis_cache_publicly_accessible/test/positive1.tf @@ -0,0 +1,9 @@ +resource "azurerm_redis_cache" "fail" { + name = "my-redis" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + capacity = 1 + family = "C" + sku_name = "Standard" + public_network_access_enabled = true +} diff --git a/assets/queries/terraform/azure/redis_cache_publicly_accessible/test/positive_expected_result.json b/assets/queries/terraform/azure/redis_cache_publicly_accessible/test/positive_expected_result.json new file mode 100644 index 00000000000..c8d9e8b6ebc --- /dev/null +++ b/assets/queries/terraform/azure/redis_cache_publicly_accessible/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Redis Cache Publicly Accessible", + "severity": "HIGH", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/azure/storage_account_queue_logging_disabled/metadata.json b/assets/queries/terraform/azure/storage_account_queue_logging_disabled/metadata.json new file mode 100644 index 00000000000..150ef50f685 --- /dev/null +++ b/assets/queries/terraform/azure/storage_account_queue_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "ba046489-38ab-4bd2-9189-2b5028ce9e52", + "queryName": "Beta - Storage Account Queue Logging Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Azure Storage Account queue storage logging should be enabled for read, write, and delete operations to maintain an audit trail of all queue transactions (CIS Azure 3.3).", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account#logging", + "platform": "Terraform", + "descriptionID": "f8a9b0c1", + "cloudProvider": "azure", + "cwe": "778", + "riskScore": "4.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/storage_account_queue_logging_disabled/query.rego b/assets/queries/terraform/azure/storage_account_queue_logging_disabled/query.rego new file mode 100644 index 00000000000..73fb4510dd1 --- /dev/null +++ b/assets/queries/terraform/azure/storage_account_queue_logging_disabled/query.rego @@ -0,0 +1,67 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_storage_account[name] + not common_lib.valid_key(resource, "queue_properties") + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_storage_account", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_storage_account[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("azurerm_storage_account[%s].queue_properties.logging should be defined with delete, read, and write enabled", [name]), + "keyActualValue": sprintf("azurerm_storage_account[%s].queue_properties is not defined", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_storage_account[name] + logging := resource.queue_properties.logging + logging.delete == false + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_storage_account", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_storage_account[%s].queue_properties.logging.delete", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("azurerm_storage_account[%s].queue_properties.logging.delete should be true", [name]), + "keyActualValue": sprintf("azurerm_storage_account[%s].queue_properties.logging.delete is false", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name, "queue_properties", "logging", "delete"], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_storage_account[name] + logging := resource.queue_properties.logging + logging.read == false + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_storage_account", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_storage_account[%s].queue_properties.logging.read", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("azurerm_storage_account[%s].queue_properties.logging.read should be true", [name]), + "keyActualValue": sprintf("azurerm_storage_account[%s].queue_properties.logging.read is false", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name, "queue_properties", "logging", "read"], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_storage_account[name] + logging := resource.queue_properties.logging + logging.write == false + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_storage_account", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_storage_account[%s].queue_properties.logging.write", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("azurerm_storage_account[%s].queue_properties.logging.write should be true", [name]), + "keyActualValue": sprintf("azurerm_storage_account[%s].queue_properties.logging.write is false", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_storage_account", name, "queue_properties", "logging", "write"], []), + } +} diff --git a/assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/negative1.tf b/assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..f645b76a79b --- /dev/null +++ b/assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/negative1.tf @@ -0,0 +1,17 @@ +resource "azurerm_storage_account" "pass" { + name = "mystorageaccount" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + account_tier = "Standard" + account_replication_type = "LRS" + + queue_properties { + logging { + delete = true + read = true + write = true + version = "1.0" + retention_policy_days = 10 + } + } +} diff --git a/assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/positive1.tf b/assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..11a39922cf7 --- /dev/null +++ b/assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/positive1.tf @@ -0,0 +1,17 @@ +resource "azurerm_storage_account" "fail" { + name = "mystorageaccount" + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + account_tier = "Standard" + account_replication_type = "LRS" + + queue_properties { + logging { + delete = false + read = false + write = true + version = "1.0" + retention_policy_days = 10 + } + } +} diff --git a/assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..f5beeaa4d41 --- /dev/null +++ b/assets/queries/terraform/azure/storage_account_queue_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Storage Account Queue Logging Disabled", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/metadata.json b/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/metadata.json new file mode 100644 index 00000000000..9635eedc454 --- /dev/null +++ b/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "f28c98bd-1552-44d9-bc4b-499990bb33b4", + "queryName": "Beta - GKE Cluster Stackdriver Logging Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "GKE clusters should use Stackdriver (Cloud Logging) for logging. Disabling cluster logging removes visibility into container and system logs, making incident response and compliance auditing impossible.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster#logging_service", + "platform": "Terraform", + "descriptionID": "c5d6e7f8", + "cloudProvider": "gcp", + "cwe": "778", + "riskScore": "5.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/query.rego b/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/query.rego new file mode 100644 index 00000000000..b1c67d11da7 --- /dev/null +++ b/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/query.rego @@ -0,0 +1,19 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.google_container_cluster[name] + resource.logging_service == "none" + result := { + "documentId": input.document[i].id, + "resourceType": "google_container_cluster", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_cluster[%s].logging_service", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("google_container_cluster[%s].logging_service should be logging.googleapis.com/kubernetes", [name]), + "keyActualValue": sprintf("google_container_cluster[%s].logging_service is none", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_cluster", name, "logging_service"], []), + } +} diff --git a/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/negative1.tf b/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/negative1.tf new file mode 100644 index 00000000000..637f51c7899 --- /dev/null +++ b/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/negative1.tf @@ -0,0 +1,5 @@ +resource "google_container_cluster" "pass" { + name = "my-cluster" + location = "us-central1" + logging_service = "logging.googleapis.com/kubernetes" +} diff --git a/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/positive1.tf b/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/positive1.tf new file mode 100644 index 00000000000..6a396bce375 --- /dev/null +++ b/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/positive1.tf @@ -0,0 +1,5 @@ +resource "google_container_cluster" "fail" { + name = "my-cluster" + location = "us-central1" + logging_service = "none" +} diff --git a/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/positive_expected_result.json b/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..97a0ece78df --- /dev/null +++ b/assets/queries/terraform/gcp/gke_cluster_stackdriver_logging_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - GKE Cluster Stackdriver Logging Disabled", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/metadata.json b/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/metadata.json new file mode 100644 index 00000000000..c6e3050ab29 --- /dev/null +++ b/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "679f7707-a487-4a66-b54b-d44ae88f363e", + "queryName": "Beta - GKE Node Pool Auto-Repair Disabled", + "severity": "MEDIUM", + "category": "Availability", + "descriptionText": "GKE node pools should have auto-repair enabled so that Google automatically repairs unhealthy nodes, maintaining cluster stability and reducing the risk of running compromised or degraded nodes.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_node_pool#auto_repair", + "platform": "Terraform", + "descriptionID": "a3b4c5d6", + "cloudProvider": "gcp", + "cwe": "693", + "riskScore": "4.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/query.rego b/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/query.rego new file mode 100644 index 00000000000..33317e54138 --- /dev/null +++ b/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/query.rego @@ -0,0 +1,35 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.google_container_node_pool[name] + mgmt := resource.management + mgmt.auto_repair == false + result := { + "documentId": input.document[i].id, + "resourceType": "google_container_node_pool", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_node_pool[%s].management.auto_repair", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("google_container_node_pool[%s].management.auto_repair should be true", [name]), + "keyActualValue": sprintf("google_container_node_pool[%s].management.auto_repair is false", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_node_pool", name, "management", "auto_repair"], []), + } +} + +CxPolicy[result] { + resource := input.document[i].resource.google_container_node_pool[name] + not common_lib.valid_key(resource, "management") + result := { + "documentId": input.document[i].id, + "resourceType": "google_container_node_pool", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_node_pool[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("google_container_node_pool[%s].management.auto_repair should be true", [name]), + "keyActualValue": sprintf("google_container_node_pool[%s].management is not defined", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_node_pool", name], []), + } +} diff --git a/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/negative1.tf b/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/negative1.tf new file mode 100644 index 00000000000..364ed32f004 --- /dev/null +++ b/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/negative1.tf @@ -0,0 +1,10 @@ +resource "google_container_node_pool" "pass" { + name = "my-node-pool" + cluster = google_container_cluster.primary.id + node_count = 1 + + management { + auto_repair = true + auto_upgrade = true + } +} diff --git a/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/positive1.tf b/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/positive1.tf new file mode 100644 index 00000000000..1992b9cc8d8 --- /dev/null +++ b/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/positive1.tf @@ -0,0 +1,10 @@ +resource "google_container_node_pool" "fail" { + name = "my-node-pool" + cluster = google_container_cluster.primary.id + node_count = 1 + + management { + auto_repair = false + auto_upgrade = true + } +} diff --git a/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/positive_expected_result.json b/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..e0acf7894dc --- /dev/null +++ b/assets/queries/terraform/gcp/gke_node_pool_auto_repair_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - GKE Node Pool Auto-Repair Disabled", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/metadata.json b/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/metadata.json new file mode 100644 index 00000000000..c52c34a07bb --- /dev/null +++ b/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "2178b485-6015-4d16-9487-c5fdfc869138", + "queryName": "Beta - GKE Node Pool Auto-Upgrade Disabled", + "severity": "MEDIUM", + "category": "Insecure Configurations", + "descriptionText": "GKE node pools should have auto-upgrade enabled so nodes are automatically updated to the latest stable Kubernetes version, ensuring security patches are applied without manual intervention.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_node_pool#auto_upgrade", + "platform": "Terraform", + "descriptionID": "b4c5d6e7", + "cloudProvider": "gcp", + "cwe": "1188", + "riskScore": "4.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/query.rego b/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/query.rego new file mode 100644 index 00000000000..ef4ba881ec0 --- /dev/null +++ b/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/query.rego @@ -0,0 +1,20 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.google_container_node_pool[name] + mgmt := resource.management + mgmt.auto_upgrade == false + result := { + "documentId": input.document[i].id, + "resourceType": "google_container_node_pool", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_container_node_pool[%s].management.auto_upgrade", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("google_container_node_pool[%s].management.auto_upgrade should be true", [name]), + "keyActualValue": sprintf("google_container_node_pool[%s].management.auto_upgrade is false", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_container_node_pool", name, "management", "auto_upgrade"], []), + } +} diff --git a/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/negative1.tf b/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/negative1.tf new file mode 100644 index 00000000000..364ed32f004 --- /dev/null +++ b/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/negative1.tf @@ -0,0 +1,10 @@ +resource "google_container_node_pool" "pass" { + name = "my-node-pool" + cluster = google_container_cluster.primary.id + node_count = 1 + + management { + auto_repair = true + auto_upgrade = true + } +} diff --git a/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/positive1.tf b/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/positive1.tf new file mode 100644 index 00000000000..f3f3075bd7c --- /dev/null +++ b/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/positive1.tf @@ -0,0 +1,10 @@ +resource "google_container_node_pool" "fail" { + name = "my-node-pool" + cluster = google_container_cluster.primary.id + node_count = 1 + + management { + auto_repair = true + auto_upgrade = false + } +} diff --git a/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/positive_expected_result.json b/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..84105566edc --- /dev/null +++ b/assets/queries/terraform/gcp/gke_node_pool_auto_upgrade_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - GKE Node Pool Auto-Upgrade Disabled", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/metadata.json b/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/metadata.json new file mode 100644 index 00000000000..d9044e02b89 --- /dev/null +++ b/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "c15e35e0-62f3-4f7e-8718-b1ce1fab31aa", + "queryName": "Beta - Cloud SQL PostgreSQL log_checkpoints Not Enabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Google Cloud SQL PostgreSQL instances should enable the log_checkpoints database flag to log each checkpoint, supporting performance analysis and forensic investigation (CIS GCP 6.2.1).", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database_instance#database_flags", + "platform": "Terraform", + "descriptionID": "e1f2a3b4", + "cloudProvider": "gcp", + "cwe": "778", + "riskScore": "4.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/query.rego b/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/query.rego new file mode 100644 index 00000000000..71f4de34dac --- /dev/null +++ b/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/query.rego @@ -0,0 +1,29 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.google_sql_database_instance[name] + settings := resource.settings + db_version := resource.database_version + startswith(db_version, "POSTGRES") + flags := settings.database_flags + not has_flag_enabled(flags, "log_checkpoints") + result := { + "documentId": input.document[i].id, + "resourceType": "google_sql_database_instance", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_sql_database_instance[%s].settings.database_flags", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("google_sql_database_instance[%s] should have database_flag log_checkpoints set to on", [name]), + "keyActualValue": sprintf("google_sql_database_instance[%s] is missing log_checkpoints flag", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_sql_database_instance", name, "settings", "database_flags"], []), + } +} + +has_flag_enabled(flags, flag_name) { + flag := flags[_] + flag.name == flag_name + flag.value == "on" +} diff --git a/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/negative1.tf b/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/negative1.tf new file mode 100644 index 00000000000..c663c280570 --- /dev/null +++ b/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/negative1.tf @@ -0,0 +1,17 @@ +resource "google_sql_database_instance" "pass" { + name = "my-postgres" + database_version = "POSTGRES_14" + region = "us-central1" + + settings { + tier = "db-f1-micro" + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_connections" + value = "on" + } + } +} diff --git a/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/positive1.tf b/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/positive1.tf new file mode 100644 index 00000000000..d14c613110e --- /dev/null +++ b/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/positive1.tf @@ -0,0 +1,13 @@ +resource "google_sql_database_instance" "fail" { + name = "my-postgres" + database_version = "POSTGRES_14" + region = "us-central1" + + settings { + tier = "db-f1-micro" + database_flags { + name = "log_connections" + value = "on" + } + } +} diff --git a/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/positive_expected_result.json b/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/positive_expected_result.json new file mode 100644 index 00000000000..f8f5555d8a3 --- /dev/null +++ b/assets/queries/terraform/gcp/sql_db_instance_without_log_checkpoints/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Cloud SQL PostgreSQL log_checkpoints Not Enabled", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/metadata.json b/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/metadata.json new file mode 100644 index 00000000000..e0cfd0ca0f0 --- /dev/null +++ b/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "3f533062-c04b-4f50-aab8-67cb3e5f898e", + "queryName": "Beta - Cloud SQL PostgreSQL log_lock_waits Not Enabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Google Cloud SQL PostgreSQL instances should enable the log_lock_waits flag to record lock wait events, helping identify deadlocks, performance issues, and potential denial-of-service conditions (CIS GCP 6.2.4).", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database_instance#database_flags", + "platform": "Terraform", + "descriptionID": "f2a3b4c5", + "cloudProvider": "gcp", + "cwe": "778", + "riskScore": "4.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/query.rego b/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/query.rego new file mode 100644 index 00000000000..1745257d125 --- /dev/null +++ b/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/query.rego @@ -0,0 +1,29 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as terra_lib + +CxPolicy[result] { + resource := input.document[i].resource.google_sql_database_instance[name] + settings := resource.settings + db_version := resource.database_version + startswith(db_version, "POSTGRES") + flags := settings.database_flags + not has_flag_enabled(flags, "log_lock_waits") + result := { + "documentId": input.document[i].id, + "resourceType": "google_sql_database_instance", + "resourceName": terra_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_sql_database_instance[%s].settings.database_flags", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("google_sql_database_instance[%s] should have database_flag log_lock_waits set to on", [name]), + "keyActualValue": sprintf("google_sql_database_instance[%s] is missing log_lock_waits flag", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_sql_database_instance", name, "settings", "database_flags"], []), + } +} + +has_flag_enabled(flags, flag_name) { + flag := flags[_] + flag.name == flag_name + flag.value == "on" +} diff --git a/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/negative1.tf b/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/negative1.tf new file mode 100644 index 00000000000..1374f008b21 --- /dev/null +++ b/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/negative1.tf @@ -0,0 +1,13 @@ +resource "google_sql_database_instance" "pass" { + name = "my-postgres" + database_version = "POSTGRES_14" + region = "us-central1" + + settings { + tier = "db-f1-micro" + database_flags { + name = "log_lock_waits" + value = "on" + } + } +} diff --git a/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/positive1.tf b/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/positive1.tf new file mode 100644 index 00000000000..5a5b2e0bea0 --- /dev/null +++ b/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/positive1.tf @@ -0,0 +1,13 @@ +resource "google_sql_database_instance" "fail" { + name = "my-postgres" + database_version = "POSTGRES_14" + region = "us-central1" + + settings { + tier = "db-f1-micro" + database_flags { + name = "log_checkpoints" + value = "on" + } + } +} diff --git a/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/positive_expected_result.json b/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/positive_expected_result.json new file mode 100644 index 00000000000..9a9b96e49e6 --- /dev/null +++ b/assets/queries/terraform/gcp/sql_db_instance_without_log_lock_waits/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Cloud SQL PostgreSQL log_lock_waits Not Enabled", + "severity": "MEDIUM", + "line": 1, + "filename": "positive1.tf" + } +] \ No newline at end of file From 52e05c93f8c3ee8fd74f6aa19614e00def7dc86b Mon Sep 17 00:00:00 2001 From: Antero Silva Date: Wed, 11 Mar 2026 19:54:22 +0000 Subject: [PATCH 2/2] feat(queries): add auto-remediation support to 5 Terraform AWS queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add remediation and remediationType fields to the following queries that previously lacked auto-remediation capability: - amazon_dms_replication_instance_is_publicly_accessible: replace true→false - cloudformation_stack_termination_protection_disabled: add true / replace false→true - ecs_services_assigned_with_public_ip_address: replace true→false (resource + module) - mq_broker_logging_disabled: replace false→true, add missing keys, add missing logs block - rds_cluster_with_backup_disabled: add backup_retention_period = 7 Co-Authored-By: Claude Sonnet 4.6 --- .../query.rego | 5 +++++ .../query.rego | 7 +++++++ .../query.rego | 10 ++++++++++ .../aws/mq_broker_logging_disabled/query.rego | 9 +++++++++ .../aws/rds_cluster_with_backup_disabled/query.rego | 2 ++ 5 files changed, 33 insertions(+) diff --git a/assets/queries/terraform/aws/amazon_dms_replication_instance_is_publicly_accessible/query.rego b/assets/queries/terraform/aws/amazon_dms_replication_instance_is_publicly_accessible/query.rego index b5d42201a6d..326fa49e018 100644 --- a/assets/queries/terraform/aws/amazon_dms_replication_instance_is_publicly_accessible/query.rego +++ b/assets/queries/terraform/aws/amazon_dms_replication_instance_is_publicly_accessible/query.rego @@ -16,5 +16,10 @@ CxPolicy[result] { "issueType": "IncorrectValue", "keyExpectedValue": sprintf("aws_dms_replication_instance[%s].publicly_accessible should be set to false", [name]), "keyActualValue": sprintf("aws_dms_replication_instance[%s].publicly_accessible is set to true", [name]), + "remediation": json.marshal({ + "before": "true", + "after": "false", + }), + "remediationType": "replacement", } } \ No newline at end of file diff --git a/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/query.rego b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/query.rego index 6a87c4d327e..42cd60da4ad 100644 --- a/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/query.rego +++ b/assets/queries/terraform/aws/cloudformation_stack_termination_protection_disabled/query.rego @@ -15,6 +15,8 @@ CxPolicy[result] { "keyExpectedValue": sprintf("aws_cloudformation_stack[%s].termination_protection should be true", [name]), "keyActualValue": sprintf("aws_cloudformation_stack[%s].termination_protection is not defined", [name]), "searchLine": common_lib.build_search_line(["resource", "aws_cloudformation_stack", name], []), + "remediation": "termination_protection = true", + "remediationType": "addition", } } @@ -30,5 +32,10 @@ CxPolicy[result] { "keyExpectedValue": sprintf("aws_cloudformation_stack[%s].termination_protection should be true", [name]), "keyActualValue": sprintf("aws_cloudformation_stack[%s].termination_protection is false", [name]), "searchLine": common_lib.build_search_line(["resource", "aws_cloudformation_stack", name, "termination_protection"], []), + "remediation": json.marshal({ + "before": "false", + "after": "true", + }), + "remediationType": "replacement", } } diff --git a/assets/queries/terraform/aws/ecs_services_assigned_with_public_ip_address/query.rego b/assets/queries/terraform/aws/ecs_services_assigned_with_public_ip_address/query.rego index dbe5bd65b78..956cd216470 100644 --- a/assets/queries/terraform/aws/ecs_services_assigned_with_public_ip_address/query.rego +++ b/assets/queries/terraform/aws/ecs_services_assigned_with_public_ip_address/query.rego @@ -17,6 +17,11 @@ CxPolicy[result] { "keyExpectedValue": sprintf("'aws_ecs_service[%s].network_configuration.assign_public_ip' should be set to 'false'(default value is 'false')", [name]), "keyActualValue": sprintf("'aws_ecs_service[%s].network_configuration.assign_public_ip' is set to true", [name]), "searchLine": common_lib.build_search_line(["resource", "aws_ecs_service", name, "network_configuration", "assign_public_ip"], []), + "remediation": json.marshal({ + "before": "true", + "after": "false", + }), + "remediationType": "replacement", } } @@ -37,5 +42,10 @@ CxPolicy[result] { "keyExpectedValue": sprintf("'module[%s].%s.%s.assign_public_ip' should be set to 'false'(default value is 'false')", [name,block,service]), "keyActualValue": sprintf("'module[%s].%s.%s.assign_public_ip' is set to true", [name,block,service]), "searchLine": common_lib.build_search_line(["module", name, block, service, "assign_public_ip"], []), + "remediation": json.marshal({ + "before": "true", + "after": "false", + }), + "remediationType": "replacement", } } \ No newline at end of file diff --git a/assets/queries/terraform/aws/mq_broker_logging_disabled/query.rego b/assets/queries/terraform/aws/mq_broker_logging_disabled/query.rego index 69d0802df91..ce8ca2c41c5 100644 --- a/assets/queries/terraform/aws/mq_broker_logging_disabled/query.rego +++ b/assets/queries/terraform/aws/mq_broker_logging_disabled/query.rego @@ -20,6 +20,11 @@ CxPolicy[result] { "issueType": "IncorrectValue", "keyExpectedValue": "'general' and 'audit' logging should be set to true", "keyActualValue": sprintf("'%s' is set to false", [type]), + "remediation": json.marshal({ + "before": "false", + "after": "true", + }), + "remediationType": "replacement", } } @@ -41,6 +46,8 @@ CxPolicy[result] { "issueType": "MissingAttribute", "keyExpectedValue": "'general' and 'audit' logging should be set to true", "keyActualValue": "'general' and/or 'audit' is undefined", + "remediation": sprintf("%s = true", [type]), + "remediationType": "addition", } } @@ -57,6 +64,8 @@ CxPolicy[result] { "issueType": "MissingAttribute", "keyExpectedValue": "'logs' should be set and enabling general AND audit logging", "keyActualValue": "'logs' is undefined", + "remediation": "logs {\n\t\tgeneral = true\n\t\taudit = true\n\t}", + "remediationType": "addition", } } diff --git a/assets/queries/terraform/aws/rds_cluster_with_backup_disabled/query.rego b/assets/queries/terraform/aws/rds_cluster_with_backup_disabled/query.rego index 9a40f45d478..97aba1f42c1 100644 --- a/assets/queries/terraform/aws/rds_cluster_with_backup_disabled/query.rego +++ b/assets/queries/terraform/aws/rds_cluster_with_backup_disabled/query.rego @@ -15,5 +15,7 @@ CxPolicy[result] { "issueType": "MissingAttribute", "keyExpectedValue": "aws_rds_cluster.backup_retention_period should be defined and not null", "keyActualValue": "aws_rds_cluster.backup_retention_period is undefined or null", + "remediation": "backup_retention_period = 7", + "remediationType": "addition", } }