diff --git a/assets/queries/dockerfile/env_instruction_has_secret/metadata.json b/assets/queries/dockerfile/env_instruction_has_secret/metadata.json new file mode 100644 index 00000000000..e6d01cb3d7b --- /dev/null +++ b/assets/queries/dockerfile/env_instruction_has_secret/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "781e75f7-bf43-4ab7-bae7-a08ad6607af3", + "queryName": "Beta - ENV Instruction Contains Secret", + "severity": "HIGH", + "category": "Secret Management", + "descriptionText": "Dockerfile ENV instructions should not store secrets, passwords, API keys or tokens as plaintext values. These values are embedded in every image layer and visible in 'docker inspect' output, risking credential exposure. Use build-time secrets (BuildKit) or a secrets manager instead.", + "descriptionUrl": "https://docs.docker.com/build/building/secrets/", + "platform": "Dockerfile", + "descriptionID": "7abccf53", + "cloudProvider": "common", + "cwe": "798", + "riskScore": "8.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/dockerfile/env_instruction_has_secret/query.rego b/assets/queries/dockerfile/env_instruction_has_secret/query.rego new file mode 100644 index 00000000000..8ba27a3a8f7 --- /dev/null +++ b/assets/queries/dockerfile/env_instruction_has_secret/query.rego @@ -0,0 +1,34 @@ +package Cx + +secret_env_patterns := { + "password", "passwd", "pwd", "secret", "api_key", "apikey", + "token", "private_key", "auth_key", "access_key", "secret_key", + "encryption_key", "db_pass", "database_password", "app_secret" +} + +CxPolicy[result] { + resource := input.document[i].command[name][_] + resource.Cmd == "env" + + env_entry := resource.Value[_] + parts := split(env_entry, "=") + count(parts) >= 2 + + env_key := lower(parts[0]) + env_val := concat("=", array.slice(parts, 1, count(parts))) + + contains(env_key, secret_env_patterns[_]) + env_val != "" + not startswith(env_val, "$") + not startswith(env_val, "\${") + not env_val == "changeme" + not env_val == "placeholder" + + result := { + "documentId": input.document[i].id, + "searchKey": sprintf("FROM={{%s}}.ENV {{%s}}", [name, parts[0]]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("ENV '%s' should not contain a hardcoded secret value; use BuildKit secrets or a runtime secrets manager", [parts[0]]), + "keyActualValue": sprintf("ENV '%s' appears to contain a hardcoded secret value", [parts[0]]), + } +} diff --git a/assets/queries/dockerfile/env_instruction_has_secret/test/negative.dockerfile b/assets/queries/dockerfile/env_instruction_has_secret/test/negative.dockerfile new file mode 100644 index 00000000000..da77762a8ea --- /dev/null +++ b/assets/queries/dockerfile/env_instruction_has_secret/test/negative.dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-slim +ENV APP_SECRET=${APP_SECRET} +ENV DATABASE_PASSWORD=$DB_PASS +ENV LOG_LEVEL=info +ENV PORT=8080 +RUN pip install flask diff --git a/assets/queries/dockerfile/env_instruction_has_secret/test/positive.dockerfile b/assets/queries/dockerfile/env_instruction_has_secret/test/positive.dockerfile new file mode 100644 index 00000000000..afeb54f42bf --- /dev/null +++ b/assets/queries/dockerfile/env_instruction_has_secret/test/positive.dockerfile @@ -0,0 +1,5 @@ +FROM python:3.11-slim +ENV APP_SECRET=my_super_secret_value +ENV DATABASE_PASSWORD=s3cr3t123 +ENV API_KEY=AKIAIOSFODNN7EXAMPLE +RUN pip install flask diff --git a/assets/queries/dockerfile/env_instruction_has_secret/test/positive_expected_result.json b/assets/queries/dockerfile/env_instruction_has_secret/test/positive_expected_result.json new file mode 100644 index 00000000000..7165086eb9f --- /dev/null +++ b/assets/queries/dockerfile/env_instruction_has_secret/test/positive_expected_result.json @@ -0,0 +1,20 @@ +[ + { + "queryName": "Beta - ENV Instruction Contains Secret", + "severity": "HIGH", + "line": 2, + "fileName": "positive.dockerfile" + }, + { + "queryName": "Beta - ENV Instruction Contains Secret", + "severity": "HIGH", + "line": 3, + "fileName": "positive.dockerfile" + }, + { + "queryName": "Beta - ENV Instruction Contains Secret", + "severity": "HIGH", + "line": 4, + "fileName": "positive.dockerfile" + } +] \ No newline at end of file diff --git a/assets/queries/k8s/container_seccomp_profile_not_set/metadata.json b/assets/queries/k8s/container_seccomp_profile_not_set/metadata.json new file mode 100644 index 00000000000..1f696f0ceae --- /dev/null +++ b/assets/queries/k8s/container_seccomp_profile_not_set/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "ab2bf277-cd73-451c-8a34-70ea5c7ee258", + "queryName": "Beta - Container Seccomp Profile Not Set", + "severity": "MEDIUM", + "category": "Insecure Configurations", + "descriptionText": "Containers should have a seccomp profile set to 'RuntimeDefault' or 'Localhost' in their security context. Without it, the container has an unconfined syscall profile, increasing the attack surface if a container breakout occurs.", + "descriptionUrl": "https://kubernetes.io/docs/tutorials/security/seccomp/", + "platform": "Kubernetes", + "descriptionID": "30a7f7cc", + "cloudProvider": "common", + "cwe": "250", + "riskScore": "5.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/k8s/container_seccomp_profile_not_set/query.rego b/assets/queries/k8s/container_seccomp_profile_not_set/query.rego new file mode 100644 index 00000000000..667ac9d0f13 --- /dev/null +++ b/assets/queries/k8s/container_seccomp_profile_not_set/query.rego @@ -0,0 +1,37 @@ +package Cx + +import data.generic.k8s as k8sLib +import data.generic.common as common_lib + +valid_types := {"RuntimeDefault", "Localhost"} + +CxPolicy[result] { + document := input.document[i] + specInfo := k8sLib.getSpecInfo(document) + metadata := document.metadata + types := {"initContainers", "containers"} + container := specInfo.spec[types[x]][j] + + not has_valid_seccomp(container, specInfo.spec) + + result := { + "documentId": document.id, + "resourceType": document.kind, + "resourceName": metadata.name, + "searchKey": sprintf("metadata.name={{%s}}.%s.%s.name={{%s}}.securityContext", [metadata.name, specInfo.path, types[x], container.name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("Container '%s' should have seccompProfile.type set to 'RuntimeDefault' or 'Localhost'", [container.name]), + "keyActualValue": sprintf("Container '%s' does not have a valid seccompProfile configured", [container.name]), + "searchLine": common_lib.build_search_line(split(specInfo.path, "."), [types[x], j, "securityContext"]), + } +} + +# seccomp defined at container level +has_valid_seccomp(container, _) { + container.securityContext.seccompProfile.type == valid_types[_] +} + +# seccomp defined at pod spec level +has_valid_seccomp(_, spec) { + spec.securityContext.seccompProfile.type == valid_types[_] +} diff --git a/assets/queries/k8s/container_seccomp_profile_not_set/test/negative1.yaml b/assets/queries/k8s/container_seccomp_profile_not_set/test/negative1.yaml new file mode 100644 index 00000000000..26891bd9d43 --- /dev/null +++ b/assets/queries/k8s/container_seccomp_profile_not_set/test/negative1.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: negative-pod-container-level +spec: + containers: + - name: app + image: nginx:1.21 + securityContext: + seccompProfile: + type: RuntimeDefault + runAsNonRoot: true diff --git a/assets/queries/k8s/container_seccomp_profile_not_set/test/negative2.yaml b/assets/queries/k8s/container_seccomp_profile_not_set/test/negative2.yaml new file mode 100644 index 00000000000..220a2c2ed19 --- /dev/null +++ b/assets/queries/k8s/container_seccomp_profile_not_set/test/negative2.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Pod +metadata: + name: negative-pod-pod-level +spec: + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: app + image: nginx:1.21 diff --git a/assets/queries/k8s/container_seccomp_profile_not_set/test/positive1.yaml b/assets/queries/k8s/container_seccomp_profile_not_set/test/positive1.yaml new file mode 100644 index 00000000000..76d9c9ecda7 --- /dev/null +++ b/assets/queries/k8s/container_seccomp_profile_not_set/test/positive1.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Pod +metadata: + name: positive-pod +spec: + containers: + - name: app + image: nginx:1.21 + securityContext: + runAsNonRoot: true diff --git a/assets/queries/k8s/container_seccomp_profile_not_set/test/positive_expected_result.json b/assets/queries/k8s/container_seccomp_profile_not_set/test/positive_expected_result.json new file mode 100644 index 00000000000..a02c9575500 --- /dev/null +++ b/assets/queries/k8s/container_seccomp_profile_not_set/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Container Seccomp Profile Not Set", + "severity": "MEDIUM", + "line": 7, + "fileName": "positive1.yaml" + } +] \ No newline at end of file diff --git a/assets/queries/k8s/deployment_has_no_pod_anti_affinity/test/negative.yaml b/assets/queries/k8s/deployment_has_no_pod_anti_affinity/test/negative.yaml index 806b28bab5a..13681533471 100644 --- a/assets/queries/k8s/deployment_has_no_pod_anti_affinity/test/negative.yaml +++ b/assets/queries/k8s/deployment_has_no_pod_anti_affinity/test/negative.yaml @@ -3,10 +3,10 @@ kind: Deployment metadata: name: web-server spec: + replicas: 3 selector: matchLabels: app: web-store - replicas: 3 template: metadata: labels: @@ -21,7 +21,7 @@ spec: operator: In values: - web-store - topologyKey: "kubernetes.io/hostname" + topologyKey: kubernetes.io/hostname containers: - - name: web-app - image: nginx:1.16-alpine \ No newline at end of file + - image: nginx:1.16-alpine + name: web-app diff --git a/assets/queries/k8s/deployment_has_no_pod_anti_affinity/test/positive.yaml b/assets/queries/k8s/deployment_has_no_pod_anti_affinity/test/positive.yaml index 4188b2e53d4..f3b12fac6e6 100644 --- a/assets/queries/k8s/deployment_has_no_pod_anti_affinity/test/positive.yaml +++ b/assets/queries/k8s/deployment_has_no_pod_anti_affinity/test/positive.yaml @@ -1,12 +1,13 @@ +--- apiVersion: apps/v1 kind: Deployment metadata: name: label-mismatch spec: + replicas: 3 selector: matchLabels: app: web-store - replicas: 3 template: metadata: labels: @@ -18,25 +19,25 @@ spec: - labelSelector: matchLabels: app: web-store - topologyKey: "kubernetes.io/hostname" + topologyKey: kubernetes.io/hostname containers: - - name: web-app - image: nginx:1.16-alpine + - image: nginx:1.16-alpine + name: web-app --- apiVersion: apps/v1 kind: Deployment metadata: name: no-affinity spec: + replicas: 3 selector: matchLabels: app: web-store - replicas: 3 template: metadata: labels: app: web-store spec: containers: - - name: web-app - image: nginx:1.16-alpine + - image: nginx:1.16-alpine + name: web-app diff --git a/assets/queries/k8s/ingress_without_tls/metadata.json b/assets/queries/k8s/ingress_without_tls/metadata.json new file mode 100644 index 00000000000..bf88ab507af --- /dev/null +++ b/assets/queries/k8s/ingress_without_tls/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "c81c6c42-6c17-48c4-bde6-ea242e76f275", + "queryName": "Beta - Ingress Without TLS", + "severity": "HIGH", + "category": "Networking and Firewall", + "descriptionText": "Kubernetes Ingress resources should configure TLS to encrypt traffic between clients and the ingress controller. Without a 'tls' block, traffic is served over plain HTTP, exposing sensitive data in transit.", + "descriptionUrl": "https://kubernetes.io/docs/concepts/services-networking/ingress/#tls", + "platform": "Kubernetes", + "descriptionID": "0dcbf4a2", + "cloudProvider": "common", + "cwe": "319", + "riskScore": "7.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/k8s/ingress_without_tls/query.rego b/assets/queries/k8s/ingress_without_tls/query.rego new file mode 100644 index 00000000000..c62669402d6 --- /dev/null +++ b/assets/queries/k8s/ingress_without_tls/query.rego @@ -0,0 +1,40 @@ +package Cx + +import data.generic.common as common_lib + +CxPolicy[result] { + document := input.document[i] + document.kind == "Ingress" + metadata := document.metadata + not common_lib.valid_key(document.spec, "tls") + + result := { + "documentId": document.id, + "resourceType": document.kind, + "resourceName": metadata.name, + "searchKey": sprintf("metadata.name={{%s}}.spec", [metadata.name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("Ingress '%s' should have 'spec.tls' configured", [metadata.name]), + "keyActualValue": sprintf("Ingress '%s' does not have 'spec.tls' configured; traffic is served over HTTP", [metadata.name]), + "searchLine": common_lib.build_search_line(["spec"], []), + } +} + +CxPolicy[result] { + document := input.document[i] + document.kind == "Ingress" + metadata := document.metadata + tls_entries := document.spec.tls + count(tls_entries) == 0 + + result := { + "documentId": document.id, + "resourceType": document.kind, + "resourceName": metadata.name, + "searchKey": sprintf("metadata.name={{%s}}.spec.tls", [metadata.name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("Ingress '%s' should have at least one TLS entry in 'spec.tls'", [metadata.name]), + "keyActualValue": sprintf("Ingress '%s' has an empty 'spec.tls' list", [metadata.name]), + "searchLine": common_lib.build_search_line(["spec", "tls"], []), + } +} diff --git a/assets/queries/k8s/ingress_without_tls/test/negative1.yaml b/assets/queries/k8s/ingress_without_tls/test/negative1.yaml new file mode 100644 index 00000000000..e7618d4a41a --- /dev/null +++ b/assets/queries/k8s/ingress_without_tls/test/negative1.yaml @@ -0,0 +1,20 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: negative-ingress +spec: + tls: + - hosts: + - example.com + secretName: example-tls + rules: + - host: example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: my-service + port: + number: 80 diff --git a/assets/queries/k8s/ingress_without_tls/test/positive1.yaml b/assets/queries/k8s/ingress_without_tls/test/positive1.yaml new file mode 100644 index 00000000000..d204c9d6c94 --- /dev/null +++ b/assets/queries/k8s/ingress_without_tls/test/positive1.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: positive-ingress +spec: + rules: + - host: example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: my-service + port: + number: 80 diff --git a/assets/queries/k8s/ingress_without_tls/test/positive_expected_result.json b/assets/queries/k8s/ingress_without_tls/test/positive_expected_result.json new file mode 100644 index 00000000000..386e92aba3f --- /dev/null +++ b/assets/queries/k8s/ingress_without_tls/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Ingress Without TLS", + "severity": "HIGH", + "line": 5, + "fileName": "positive1.yaml" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/backup_vault_without_kms_key/metadata.json b/assets/queries/terraform/aws/backup_vault_without_kms_key/metadata.json new file mode 100644 index 00000000000..fa257ecf6bb --- /dev/null +++ b/assets/queries/terraform/aws/backup_vault_without_kms_key/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "f979bf87-34fd-4bab-a5b1-530268a581d5", + "queryName": "Beta - Backup Vault Without KMS Key", + "severity": "MEDIUM", + "category": "Encryption", + "descriptionText": "AWS Backup Vaults should be encrypted using a customer-managed KMS key. Without a 'kms_key_arn', backups are protected only by the default AWS-managed key, offering no additional control over key rotation or access.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_vault#kms_key_arn", + "platform": "Terraform", + "descriptionID": "6df8a84c", + "cloudProvider": "aws", + "cwe": "311", + "riskScore": "5.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/backup_vault_without_kms_key/query.rego b/assets/queries/terraform/aws/backup_vault_without_kms_key/query.rego new file mode 100644 index 00000000000..09d60fa3fcf --- /dev/null +++ b/assets/queries/terraform/aws/backup_vault_without_kms_key/query.rego @@ -0,0 +1,22 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_backup_vault[name] + not common_lib.valid_key(resource, "kms_key_arn") + + result := { + "documentId": input.document[i].id, + "resourceType": "aws_backup_vault", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_backup_vault[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'aws_backup_vault[%s].kms_key_arn' should be defined", [name]), + "keyActualValue": sprintf("'aws_backup_vault[%s].kms_key_arn' is not defined; vault uses AWS-managed key", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_backup_vault", name], []), + "remediation": "kms_key_arn = aws_kms_key.example.arn", + "remediationType": "addition", + } +} diff --git a/assets/queries/terraform/aws/backup_vault_without_kms_key/test/negative1.tf b/assets/queries/terraform/aws/backup_vault_without_kms_key/test/negative1.tf new file mode 100644 index 00000000000..20b595131c4 --- /dev/null +++ b/assets/queries/terraform/aws/backup_vault_without_kms_key/test/negative1.tf @@ -0,0 +1,4 @@ +resource "aws_backup_vault" "negative1" { + name = "example_vault" + kms_key_arn = aws_kms_key.example.arn +} diff --git a/assets/queries/terraform/aws/backup_vault_without_kms_key/test/positive1.tf b/assets/queries/terraform/aws/backup_vault_without_kms_key/test/positive1.tf new file mode 100644 index 00000000000..a94ff9f997e --- /dev/null +++ b/assets/queries/terraform/aws/backup_vault_without_kms_key/test/positive1.tf @@ -0,0 +1,3 @@ +resource "aws_backup_vault" "positive1" { + name = "example_vault" +} diff --git a/assets/queries/terraform/aws/backup_vault_without_kms_key/test/positive_expected_result.json b/assets/queries/terraform/aws/backup_vault_without_kms_key/test/positive_expected_result.json new file mode 100644 index 00000000000..c9cdd590efe --- /dev/null +++ b/assets/queries/terraform/aws/backup_vault_without_kms_key/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Backup Vault Without KMS Key", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/lambda_function_url_without_authentication/metadata.json b/assets/queries/terraform/aws/lambda_function_url_without_authentication/metadata.json new file mode 100644 index 00000000000..951bcf7bc3e --- /dev/null +++ b/assets/queries/terraform/aws/lambda_function_url_without_authentication/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "0cbdf8fb-83cd-418f-918f-de86df0621b6", + "queryName": "Beta - Lambda Function URL Without Authentication", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "AWS Lambda Function URLs should require authentication. Setting 'authorization_type' to 'NONE' allows unauthenticated public invocation of the function, exposing it to abuse or data exfiltration.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function_url#authorization_type", + "platform": "Terraform", + "descriptionID": "1c4bf6dc", + "cloudProvider": "aws", + "cwe": "306", + "riskScore": "8.1", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/lambda_function_url_without_authentication/query.rego b/assets/queries/terraform/aws/lambda_function_url_without_authentication/query.rego new file mode 100644 index 00000000000..cc4db8b5dd7 --- /dev/null +++ b/assets/queries/terraform/aws/lambda_function_url_without_authentication/query.rego @@ -0,0 +1,22 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_lambda_function_url[name] + resource.authorization_type == "NONE" + + result := { + "documentId": input.document[i].id, + "resourceType": "aws_lambda_function_url", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_lambda_function_url[%s].authorization_type", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'aws_lambda_function_url[%s].authorization_type' should not be 'NONE'", [name]), + "keyActualValue": sprintf("'aws_lambda_function_url[%s].authorization_type' is 'NONE', allowing unauthenticated invocations", [name]), + "searchLine": common_lib.build_search_line(["resource", "aws_lambda_function_url", name, "authorization_type"], []), + "remediation": "authorization_type = \"AWS_IAM\"", + "remediationType": "replacement", + } +} diff --git a/assets/queries/terraform/aws/lambda_function_url_without_authentication/test/negative1.tf b/assets/queries/terraform/aws/lambda_function_url_without_authentication/test/negative1.tf new file mode 100644 index 00000000000..9577d0039e0 --- /dev/null +++ b/assets/queries/terraform/aws/lambda_function_url_without_authentication/test/negative1.tf @@ -0,0 +1,4 @@ +resource "aws_lambda_function_url" "negative1" { + function_name = aws_lambda_function.example.function_name + authorization_type = "AWS_IAM" +} diff --git a/assets/queries/terraform/aws/lambda_function_url_without_authentication/test/positive1.tf b/assets/queries/terraform/aws/lambda_function_url_without_authentication/test/positive1.tf new file mode 100644 index 00000000000..095e2667520 --- /dev/null +++ b/assets/queries/terraform/aws/lambda_function_url_without_authentication/test/positive1.tf @@ -0,0 +1,4 @@ +resource "aws_lambda_function_url" "positive1" { + function_name = aws_lambda_function.example.function_name + authorization_type = "NONE" +} diff --git a/assets/queries/terraform/aws/lambda_function_url_without_authentication/test/positive_expected_result.json b/assets/queries/terraform/aws/lambda_function_url_without_authentication/test/positive_expected_result.json new file mode 100644 index 00000000000..c51760d4f23 --- /dev/null +++ b/assets/queries/terraform/aws/lambda_function_url_without_authentication/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Lambda Function URL Without Authentication", + "severity": "HIGH", + "line": 3, + "fileName": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/metadata.json b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/metadata.json new file mode 100644 index 00000000000..a19da0c694d --- /dev/null +++ b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "ef088173-6ce6-46b6-af2e-708ab73737c1", + "queryName": "Beta - Transfer Family Server Publicly Accessible", + "severity": "HIGH", + "category": "Networking and Firewall", + "descriptionText": "AWS Transfer Family servers should use VPC endpoint type instead of PUBLIC to restrict access and reduce the attack surface. Public endpoints expose the file transfer service directly to the internet.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/transfer_server#endpoint_type", + "platform": "Terraform", + "descriptionID": "1d9081ec", + "cloudProvider": "aws", + "cwe": "668", + "riskScore": "7.5", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/query.rego b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/query.rego new file mode 100644 index 00000000000..e5aba02eb10 --- /dev/null +++ b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/query.rego @@ -0,0 +1,23 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +CxPolicy[result] { + resource := input.document[i].resource.aws_transfer_server[name] + endpoint := object.get(resource, "endpoint_type", "PUBLIC") + endpoint != "VPC" + + result := { + "documentId": input.document[i].id, + "resourceType": "aws_transfer_server", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("aws_transfer_server[%s].endpoint_type", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'aws_transfer_server[%s].endpoint_type' should be 'VPC'", [name]), + "keyActualValue": sprintf("'aws_transfer_server[%s].endpoint_type' is '%s'", [name, endpoint]), + "searchLine": common_lib.build_search_line(["resource", "aws_transfer_server", name, "endpoint_type"], []), + "remediation": "endpoint_type = \"VPC\"", + "remediationType": "replacement", + } +} diff --git a/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/negative1.tf b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/negative1.tf new file mode 100644 index 00000000000..f9a413622d3 --- /dev/null +++ b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/negative1.tf @@ -0,0 +1,7 @@ +resource "aws_transfer_server" "negative1" { + identity_provider_type = "SERVICE_MANAGED" + endpoint_type = "VPC" + endpoint_details { + vpc_id = aws_vpc.example.id + } +} diff --git a/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/positive1.tf b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/positive1.tf new file mode 100644 index 00000000000..2728be2f100 --- /dev/null +++ b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/positive1.tf @@ -0,0 +1,4 @@ +resource "aws_transfer_server" "positive1" { + identity_provider_type = "SERVICE_MANAGED" + endpoint_type = "PUBLIC" +} diff --git a/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/positive2.tf b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/positive2.tf new file mode 100644 index 00000000000..7a01ae4b32e --- /dev/null +++ b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/positive2.tf @@ -0,0 +1,3 @@ +resource "aws_transfer_server" "positive2" { + identity_provider_type = "SERVICE_MANAGED" +} diff --git a/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/positive_expected_result.json b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/positive_expected_result.json new file mode 100644 index 00000000000..95c458d90f7 --- /dev/null +++ b/assets/queries/terraform/aws/transfer_family_server_publicly_accessible/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Transfer Family Server Publicly Accessible", + "severity": "HIGH", + "line": 3, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Transfer Family Server Publicly Accessible", + "severity": "HIGH", + "line": 1, + "fileName": "positive2.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/azure/aks_defender_profile_disabled/metadata.json b/assets/queries/terraform/azure/aks_defender_profile_disabled/metadata.json new file mode 100644 index 00000000000..aa5a36a631c --- /dev/null +++ b/assets/queries/terraform/azure/aks_defender_profile_disabled/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "beb57138-c9ee-421a-8332-6224ab3a2a6a", + "queryName": "Beta - AKS Defender Profile Disabled", + "severity": "MEDIUM", + "category": "Observability", + "descriptionText": "Azure Kubernetes Service clusters should have Microsoft Defender enabled via the 'microsoft_defender' block. Without it, threat detection for the cluster workloads is not active, leaving security incidents undetected.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster#microsoft_defender", + "platform": "Terraform", + "descriptionID": "f8f4f141", + "cloudProvider": "azure", + "cwe": "778", + "riskScore": "5.3", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/aks_defender_profile_disabled/query.rego b/assets/queries/terraform/azure/aks_defender_profile_disabled/query.rego new file mode 100644 index 00000000000..884771f88a8 --- /dev/null +++ b/assets/queries/terraform/azure/aks_defender_profile_disabled/query.rego @@ -0,0 +1,22 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_kubernetes_cluster[name] + not common_lib.valid_key(resource, "microsoft_defender") + + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_kubernetes_cluster", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_kubernetes_cluster[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_kubernetes_cluster[%s].microsoft_defender' should be configured", [name]), + "keyActualValue": sprintf("'azurerm_kubernetes_cluster[%s].microsoft_defender' is not defined", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_kubernetes_cluster", name], []), + "remediation": "microsoft_defender {\n log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id\n }", + "remediationType": "addition", + } +} diff --git a/assets/queries/terraform/azure/aks_defender_profile_disabled/test/negative1.tf b/assets/queries/terraform/azure/aks_defender_profile_disabled/test/negative1.tf new file mode 100644 index 00000000000..45757fd915d --- /dev/null +++ b/assets/queries/terraform/azure/aks_defender_profile_disabled/test/negative1.tf @@ -0,0 +1,17 @@ +resource "azurerm_kubernetes_cluster" "negative1" { + name = "example-aks" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + dns_prefix = "exampleaks" + default_node_pool { + name = "default" + node_count = 1 + vm_size = "Standard_D2_v2" + } + identity { + type = "SystemAssigned" + } + microsoft_defender { + log_analytics_workspace_id = azurerm_log_analytics_workspace.example.id + } +} diff --git a/assets/queries/terraform/azure/aks_defender_profile_disabled/test/positive1.tf b/assets/queries/terraform/azure/aks_defender_profile_disabled/test/positive1.tf new file mode 100644 index 00000000000..04157c1e49d --- /dev/null +++ b/assets/queries/terraform/azure/aks_defender_profile_disabled/test/positive1.tf @@ -0,0 +1,14 @@ +resource "azurerm_kubernetes_cluster" "positive1" { + name = "example-aks" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + dns_prefix = "exampleaks" + default_node_pool { + name = "default" + node_count = 1 + vm_size = "Standard_D2_v2" + } + identity { + type = "SystemAssigned" + } +} diff --git a/assets/queries/terraform/azure/aks_defender_profile_disabled/test/positive_expected_result.json b/assets/queries/terraform/azure/aks_defender_profile_disabled/test/positive_expected_result.json new file mode 100644 index 00000000000..398ac164f1e --- /dev/null +++ b/assets/queries/terraform/azure/aks_defender_profile_disabled/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - AKS Defender Profile Disabled", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/metadata.json b/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/metadata.json new file mode 100644 index 00000000000..7d5b42849c5 --- /dev/null +++ b/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "34d7d523-ed2c-43f4-9c20-ca22d07a0f56", + "queryName": "Beta - Machine Learning Workspace Without CMK", + "severity": "MEDIUM", + "category": "Encryption", + "descriptionText": "Azure Machine Learning workspaces store model artifacts, datasets, and experiment logs. These should be encrypted with a customer-managed key using the 'encryption' block to meet data residency and compliance requirements.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/machine_learning_workspace#encryption", + "platform": "Terraform", + "descriptionID": "b489594d", + "cloudProvider": "azure", + "cwe": "311", + "riskScore": "5.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/query.rego b/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/query.rego new file mode 100644 index 00000000000..fda7e6e583d --- /dev/null +++ b/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/query.rego @@ -0,0 +1,22 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_machine_learning_workspace[name] + not common_lib.valid_key(resource, "encryption") + + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_machine_learning_workspace", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_machine_learning_workspace[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_machine_learning_workspace[%s].encryption' should be defined with a customer-managed key", [name]), + "keyActualValue": sprintf("'azurerm_machine_learning_workspace[%s].encryption' is not defined; using Microsoft-managed key", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_machine_learning_workspace", name], []), + "remediation": "encryption {\n key_vault_id = azurerm_key_vault.example.id\n key_id = azurerm_key_vault_key.example.id\n }", + "remediationType": "addition", + } +} diff --git a/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/test/negative1.tf b/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/test/negative1.tf new file mode 100644 index 00000000000..1786dcab8ad --- /dev/null +++ b/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/test/negative1.tf @@ -0,0 +1,15 @@ +resource "azurerm_machine_learning_workspace" "negative1" { + name = "example-mlworkspace" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + application_insights_id = azurerm_application_insights.example.id + key_vault_id = azurerm_key_vault.example.id + storage_account_id = azurerm_storage_account.example.id + identity { + type = "SystemAssigned" + } + encryption { + key_vault_id = azurerm_key_vault.example.id + key_id = azurerm_key_vault_key.example.id + } +} diff --git a/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/test/positive1.tf b/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/test/positive1.tf new file mode 100644 index 00000000000..10c248ff30e --- /dev/null +++ b/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/test/positive1.tf @@ -0,0 +1,11 @@ +resource "azurerm_machine_learning_workspace" "positive1" { + name = "example-mlworkspace" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + application_insights_id = azurerm_application_insights.example.id + key_vault_id = azurerm_key_vault.example.id + storage_account_id = azurerm_storage_account.example.id + identity { + type = "SystemAssigned" + } +} diff --git a/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/test/positive_expected_result.json b/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/test/positive_expected_result.json new file mode 100644 index 00000000000..18cc9f05af5 --- /dev/null +++ b/assets/queries/terraform/azure/machine_learning_workspace_without_cmk/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Machine Learning Workspace Without CMK", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/azure/service_bus_namespace_without_cmk/metadata.json b/assets/queries/terraform/azure/service_bus_namespace_without_cmk/metadata.json new file mode 100644 index 00000000000..c3894c8e6db --- /dev/null +++ b/assets/queries/terraform/azure/service_bus_namespace_without_cmk/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "8a800912-0bcb-441c-aa95-306d3d9a7bdd", + "queryName": "Beta - Service Bus Namespace Without CMK", + "severity": "LOW", + "category": "Encryption", + "descriptionText": "Azure Service Bus namespaces should be encrypted with a customer-managed key (CMK) for enhanced control over encryption keys. Without a 'customer_managed_key' block, Microsoft-managed keys are used with no customer key management.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/servicebus_namespace#customer_managed_key", + "platform": "Terraform", + "descriptionID": "39e5eb1f", + "cloudProvider": "azure", + "cwe": "311", + "riskScore": "3.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/azure/service_bus_namespace_without_cmk/query.rego b/assets/queries/terraform/azure/service_bus_namespace_without_cmk/query.rego new file mode 100644 index 00000000000..2068a9ebab8 --- /dev/null +++ b/assets/queries/terraform/azure/service_bus_namespace_without_cmk/query.rego @@ -0,0 +1,22 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +CxPolicy[result] { + resource := input.document[i].resource.azurerm_servicebus_namespace[name] + not common_lib.valid_key(resource, "customer_managed_key") + + result := { + "documentId": input.document[i].id, + "resourceType": "azurerm_servicebus_namespace", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("azurerm_servicebus_namespace[%s]", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'azurerm_servicebus_namespace[%s].customer_managed_key' should be defined", [name]), + "keyActualValue": sprintf("'azurerm_servicebus_namespace[%s].customer_managed_key' is not defined; using Microsoft-managed key", [name]), + "searchLine": common_lib.build_search_line(["resource", "azurerm_servicebus_namespace", name], []), + "remediation": "customer_managed_key {\n key_vault_key_id = azurerm_key_vault_key.example.id\n identity_id = azurerm_user_assigned_identity.example.id\n }", + "remediationType": "addition", + } +} diff --git a/assets/queries/terraform/azure/service_bus_namespace_without_cmk/test/negative1.tf b/assets/queries/terraform/azure/service_bus_namespace_without_cmk/test/negative1.tf new file mode 100644 index 00000000000..2c535a3a434 --- /dev/null +++ b/assets/queries/terraform/azure/service_bus_namespace_without_cmk/test/negative1.tf @@ -0,0 +1,10 @@ +resource "azurerm_servicebus_namespace" "negative1" { + name = "example-sbnamespace" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + sku = "Premium" + customer_managed_key { + key_vault_key_id = azurerm_key_vault_key.example.id + identity_id = azurerm_user_assigned_identity.example.id + } +} diff --git a/assets/queries/terraform/azure/service_bus_namespace_without_cmk/test/positive1.tf b/assets/queries/terraform/azure/service_bus_namespace_without_cmk/test/positive1.tf new file mode 100644 index 00000000000..b3824d1445c --- /dev/null +++ b/assets/queries/terraform/azure/service_bus_namespace_without_cmk/test/positive1.tf @@ -0,0 +1,6 @@ +resource "azurerm_servicebus_namespace" "positive1" { + name = "example-sbnamespace" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + sku = "Premium" +} diff --git a/assets/queries/terraform/azure/service_bus_namespace_without_cmk/test/positive_expected_result.json b/assets/queries/terraform/azure/service_bus_namespace_without_cmk/test/positive_expected_result.json new file mode 100644 index 00000000000..c0ffa818ee0 --- /dev/null +++ b/assets/queries/terraform/azure/service_bus_namespace_without_cmk/test/positive_expected_result.json @@ -0,0 +1,8 @@ +[ + { + "queryName": "Beta - Service Bus Namespace Without CMK", + "severity": "LOW", + "line": 1, + "fileName": "positive1.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/gcp/artifact_registry_repository_public_access/metadata.json b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/metadata.json new file mode 100644 index 00000000000..cc85c77d0df --- /dev/null +++ b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "664c673f-71f2-4a7f-a54a-2efa25e8567f", + "queryName": "Beta - Artifact Registry Repository With Public Access", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "GCP Artifact Registry repositories should not grant access to 'allUsers' or 'allAuthenticatedUsers'. Public access allows anyone to read or push container images and packages, risking supply chain attacks or data leakage.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/artifact_registry_repository_iam_member", + "platform": "Terraform", + "descriptionID": "3116634b", + "cloudProvider": "gcp", + "cwe": "284", + "riskScore": "8.2", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/artifact_registry_repository_public_access/query.rego b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/query.rego new file mode 100644 index 00000000000..535e8340eb0 --- /dev/null +++ b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/query.rego @@ -0,0 +1,43 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +public_members := {"allUsers", "allAuthenticatedUsers"} + +CxPolicy[result] { + resource := input.document[i].resource.google_artifact_registry_repository_iam_member[name] + resource.member == public_members[_] + + result := { + "documentId": input.document[i].id, + "resourceType": "google_artifact_registry_repository_iam_member", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_artifact_registry_repository_iam_member[%s].member", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'google_artifact_registry_repository_iam_member[%s].member' should not be 'allUsers' or 'allAuthenticatedUsers'", [name]), + "keyActualValue": sprintf("'google_artifact_registry_repository_iam_member[%s].member' is '%s'", [name, resource.member]), + "searchLine": common_lib.build_search_line(["resource", "google_artifact_registry_repository_iam_member", name, "member"], []), + "remediation": "Replace public member with a specific service account or group identity", + "remediationType": "replacement", + } +} + +CxPolicy[result] { + resource := input.document[i].resource.google_artifact_registry_repository_iam_binding[name] + member := resource.members[_] + member == public_members[_] + + result := { + "documentId": input.document[i].id, + "resourceType": "google_artifact_registry_repository_iam_binding", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_artifact_registry_repository_iam_binding[%s].members", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'google_artifact_registry_repository_iam_binding[%s].members' should not include public identities", [name]), + "keyActualValue": sprintf("'google_artifact_registry_repository_iam_binding[%s].members' contains '%s'", [name, member]), + "searchLine": common_lib.build_search_line(["resource", "google_artifact_registry_repository_iam_binding", name, "members"], []), + "remediation": "Remove public identities from members", + "remediationType": "removal", + } +} diff --git a/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/negative1.tf b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/negative1.tf new file mode 100644 index 00000000000..44d93ecdab4 --- /dev/null +++ b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/negative1.tf @@ -0,0 +1,6 @@ +resource "google_artifact_registry_repository_iam_member" "negative1" { + repository = google_artifact_registry_repository.my_repo.name + location = google_artifact_registry_repository.my_repo.location + role = "roles/artifactregistry.reader" + member = "serviceAccount:ci-runner@project.iam.gserviceaccount.com" +} diff --git a/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/positive1.tf b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/positive1.tf new file mode 100644 index 00000000000..07aad5b33cd --- /dev/null +++ b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/positive1.tf @@ -0,0 +1,6 @@ +resource "google_artifact_registry_repository_iam_member" "positive1" { + repository = google_artifact_registry_repository.my_repo.name + location = google_artifact_registry_repository.my_repo.location + role = "roles/artifactregistry.reader" + member = "allUsers" +} diff --git a/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/positive2.tf b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/positive2.tf new file mode 100644 index 00000000000..212d0476fed --- /dev/null +++ b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/positive2.tf @@ -0,0 +1,6 @@ +resource "google_artifact_registry_repository_iam_binding" "positive2" { + repository = google_artifact_registry_repository.my_repo.name + location = google_artifact_registry_repository.my_repo.location + role = "roles/artifactregistry.reader" + members = ["allAuthenticatedUsers"] +} diff --git a/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/positive_expected_result.json b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/positive_expected_result.json new file mode 100644 index 00000000000..83452caeb90 --- /dev/null +++ b/assets/queries/terraform/gcp/artifact_registry_repository_public_access/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Artifact Registry Repository With Public Access", + "severity": "HIGH", + "line": 5, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Artifact Registry Repository With Public Access", + "severity": "HIGH", + "line": 5, + "fileName": "positive2.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/metadata.json b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/metadata.json new file mode 100644 index 00000000000..ad6bf39b261 --- /dev/null +++ b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "07a27a3b-46e9-4172-bc68-25e3277d7998", + "queryName": "Beta - Cloud Run Service Allows Unauthenticated Access", + "severity": "HIGH", + "category": "Access Control", + "descriptionText": "GCP Cloud Run services should not allow unauthenticated invocations. Granting 'roles/run.invoker' to 'allUsers' exposes the service publicly without authentication, enabling potential abuse or data exposure.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloud_run_service_iam_member", + "platform": "Terraform", + "descriptionID": "b89d8324", + "cloudProvider": "gcp", + "cwe": "306", + "riskScore": "8.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/query.rego b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/query.rego new file mode 100644 index 00000000000..a7e0b7686c0 --- /dev/null +++ b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/query.rego @@ -0,0 +1,40 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +CxPolicy[result] { + resource := input.document[i].resource.google_cloud_run_service_iam_member[name] + resource.member == "allUsers" + + result := { + "documentId": input.document[i].id, + "resourceType": "google_cloud_run_service_iam_member", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_cloud_run_service_iam_member[%s].member", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'google_cloud_run_service_iam_member[%s].member' should not be 'allUsers'", [name]), + "keyActualValue": sprintf("'google_cloud_run_service_iam_member[%s].member' is 'allUsers', allowing unauthenticated access", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_cloud_run_service_iam_member", name, "member"], []), + "remediation": "Remove 'allUsers' member and use specific service accounts or authenticated identities", + "remediationType": "removal", + } +} + +CxPolicy[result] { + resource := input.document[i].resource.google_cloud_run_service_iam_binding[name] + resource.members[_] == "allUsers" + + result := { + "documentId": input.document[i].id, + "resourceType": "google_cloud_run_service_iam_binding", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_cloud_run_service_iam_binding[%s].members", [name]), + "issueType": "IncorrectValue", + "keyExpectedValue": sprintf("'google_cloud_run_service_iam_binding[%s].members' should not contain 'allUsers'", [name]), + "keyActualValue": sprintf("'google_cloud_run_service_iam_binding[%s].members' contains 'allUsers', allowing unauthenticated access", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_cloud_run_service_iam_binding", name, "members"], []), + "remediation": "Remove 'allUsers' from members and use specific authenticated identities", + "remediationType": "removal", + } +} diff --git a/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/negative1.tf b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/negative1.tf new file mode 100644 index 00000000000..731733d3dc4 --- /dev/null +++ b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/negative1.tf @@ -0,0 +1,6 @@ +resource "google_cloud_run_service_iam_member" "negative1" { + service = google_cloud_run_service.default.name + location = google_cloud_run_service.default.location + role = "roles/run.invoker" + member = "serviceAccount:my-service-account@project.iam.gserviceaccount.com" +} diff --git a/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/positive1.tf b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/positive1.tf new file mode 100644 index 00000000000..dc21bd32ae7 --- /dev/null +++ b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/positive1.tf @@ -0,0 +1,6 @@ +resource "google_cloud_run_service_iam_member" "positive1" { + service = google_cloud_run_service.default.name + location = google_cloud_run_service.default.location + role = "roles/run.invoker" + member = "allUsers" +} diff --git a/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/positive2.tf b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/positive2.tf new file mode 100644 index 00000000000..5a9bc9b34f1 --- /dev/null +++ b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/positive2.tf @@ -0,0 +1,6 @@ +resource "google_cloud_run_service_iam_binding" "positive2" { + service = google_cloud_run_service.default.name + location = google_cloud_run_service.default.location + role = "roles/run.invoker" + members = ["allUsers", "serviceAccount:my-sa@project.iam.gserviceaccount.com"] +} diff --git a/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/positive_expected_result.json b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/positive_expected_result.json new file mode 100644 index 00000000000..fb483861be0 --- /dev/null +++ b/assets/queries/terraform/gcp/cloud_run_service_allows_unauthenticated/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Cloud Run Service Allows Unauthenticated Access", + "severity": "HIGH", + "line": 5, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Cloud Run Service Allows Unauthenticated Access", + "severity": "HIGH", + "line": 5, + "fileName": "positive2.tf" + } +] \ No newline at end of file diff --git a/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/metadata.json b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/metadata.json new file mode 100644 index 00000000000..e6f9b47e501 --- /dev/null +++ b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "fe0ff686-f344-4d06-ba0c-84321161aebb", + "queryName": "Beta - Secret Manager Secret Without CMEK", + "severity": "MEDIUM", + "category": "Encryption", + "descriptionText": "GCP Secret Manager secrets should use customer-managed encryption keys (CMEK) for enhanced key control. Without a 'customer_managed_encryption.kms_key_name' in the replication config, secrets are encrypted with Google-managed keys.", + "descriptionUrl": "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret#customer_managed_encryption", + "platform": "Terraform", + "descriptionID": "9be17ab9", + "cloudProvider": "gcp", + "cwe": "311", + "riskScore": "5.0", + "experimental": "true" +} \ No newline at end of file diff --git a/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/query.rego b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/query.rego new file mode 100644 index 00000000000..ab38ccba04a --- /dev/null +++ b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/query.rego @@ -0,0 +1,33 @@ +package Cx + +import data.generic.common as common_lib +import data.generic.terraform as tf_lib + +CxPolicy[result] { + resource := input.document[i].resource.google_secret_manager_secret[name] + replication := object.get(resource, "replication", {}) + not has_cmek(replication) + + result := { + "documentId": input.document[i].id, + "resourceType": "google_secret_manager_secret", + "resourceName": tf_lib.get_resource_name(resource, name), + "searchKey": sprintf("google_secret_manager_secret[%s].replication", [name]), + "issueType": "MissingAttribute", + "keyExpectedValue": sprintf("'google_secret_manager_secret[%s]' should configure customer_managed_encryption with a KMS key", [name]), + "keyActualValue": sprintf("'google_secret_manager_secret[%s]' uses Google-managed encryption keys", [name]), + "searchLine": common_lib.build_search_line(["resource", "google_secret_manager_secret", name, "replication"], []), + "remediation": "replication {\n user_managed {\n replicas {\n location = \"us-central1\"\n customer_managed_encryption {\n kms_key_name = google_kms_crypto_key.example.id\n }\n }\n }\n }", + "remediationType": "replacement", + } +} + +has_cmek(replication) { + is_object(replication.user_managed) + replication.user_managed.replicas[_].customer_managed_encryption.kms_key_name != "" +} + +has_cmek(replication) { + is_array(replication.user_managed.replicas) + replication.user_managed.replicas[_].customer_managed_encryption.kms_key_name != "" +} diff --git a/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/negative1.tf b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/negative1.tf new file mode 100644 index 00000000000..4a112a2123e --- /dev/null +++ b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/negative1.tf @@ -0,0 +1,13 @@ +resource "google_secret_manager_secret" "negative1" { + secret_id = "my-secret" + replication { + user_managed { + replicas { + location = "us-central1" + customer_managed_encryption { + kms_key_name = google_kms_crypto_key.example.id + } + } + } + } +} diff --git a/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/positive1.tf b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/positive1.tf new file mode 100644 index 00000000000..0fe27f8127a --- /dev/null +++ b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/positive1.tf @@ -0,0 +1,6 @@ +resource "google_secret_manager_secret" "positive1" { + secret_id = "my-secret" + replication { + auto {} + } +} diff --git a/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/positive2.tf b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/positive2.tf new file mode 100644 index 00000000000..a8fc758bf5a --- /dev/null +++ b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/positive2.tf @@ -0,0 +1,10 @@ +resource "google_secret_manager_secret" "positive2" { + secret_id = "my-secret" + replication { + user_managed { + replicas { + location = "us-central1" + } + } + } +} diff --git a/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/positive_expected_result.json b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/positive_expected_result.json new file mode 100644 index 00000000000..ae905454f1f --- /dev/null +++ b/assets/queries/terraform/gcp/secret_manager_secret_without_cmek/test/positive_expected_result.json @@ -0,0 +1,14 @@ +[ + { + "queryName": "Beta - Secret Manager Secret Without CMEK", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive1.tf" + }, + { + "queryName": "Beta - Secret Manager Secret Without CMEK", + "severity": "MEDIUM", + "line": 1, + "fileName": "positive2.tf" + } +] \ No newline at end of file