Mondoo Policy Checks for Terraform (HCL, Plan, and State) with MQL
Learn how to write Mondoo MQL checks for Terraform HCL, Plan, and State. Use the cnspec shell, Mondoo Terraform provider, filters, and variants to build production-grade Policy as Code across your IaC and runtime.
As Terraform usage grows, Policy as Code becomes essential to enforce security, compliance, and operational best practices before changes reach production. MQL provides a consistent way to write checks against all three stages of the Terraform lifecycle:
- Terraform HCL (your source code)
- Terraform Plan JSON (proposed changes)
- Terraform State JSON (what currently exists)
In this guide you'll:
- Install and use the Mondoo Terraform provider in the
cnspec shell. - Explore Terraform resources with MQL and write checks using
.where()and.all(). - Package checks into Mondoo policies with filters and variants so a single check runs consistently against HCL, Plan, and State.
Mondoo Terraform provider overview
Mondoo providers are plugins for cnspec and cnquery that connect to different platforms and data sources. Each provider translates an external system's data into objects you can query with MQL. Think of them as connectors for cloud APIs, config files, Terraform plans, and more.
The cnspec Terraform provider
The Mondoo Terraform provider is a cnspec plugin (not a HashiCorp Terraform provider). It parses Terraform HCL, Plan, and State data and makes the contents available as MQL resources.
With it you can:
- Scan Terraform code and outputs including HCL files, Plan JSON, and State JSON.
- Query resources consistently using
terraform.resources(HCL),terraform.plan.resourceChanges(Plan), orterraform.state.resources(State), so the same MQL patterns work across all stages. - Use policy variants to define a check once and run it against HCL, Plan, State, and even runtime cloud APIs.
Install the provider
cnspec providers install terraformUse the interactive shell
Use cnspec shell to prototype queries interactively before codifying them in policy YAML.
Connect to HCL
Point the shell at a directory containing .tf files:
cnspec shell terraform ./path/to/terraform/dirConnect to a Plan
First, export the plan to JSON:
terraform show -json tfplan.binary > tfplan.jsonThen open it in the shell:
cnspec shell terraform ./path/to/tfplan.jsonConnect to State
First, export the state to JSON:
terraform show -json > tfstate.jsonThen open it in the shell:
cnspec shell terraform ./path/to/terraform.tfstateWriting checks for Terraform HCL
This section walks through a typical workflow: discover resources, inspect their arguments, and build a check. All examples use HCL; the Plan and State sections below follow the same pattern with different resource paths.
1) List resources
Start by listing all resources Mondoo parsed from the HCL:
terraform.resources { nameLabel }This returns all parsed resource blocks:
cnspec> terraform.resources
terraform.resources: [
0: terraform.block type="resource" labels=[
0: "random_string"
1: "random"
]
1: terraform.block type="resource" labels=[
0: "google_cloud_run_v2_service"
1: "hello_service"
]
2: terraform.block type="resource" labels=[
0: "google_cloud_run_v2_service_iam_member"
1: "public_invoker"
]
3: terraform.block type="resource" labels=[
0: "google_redis_instance"
1: "dev_memstore"
]
4: terraform.block type="resource" labels=[
0: "google_storage_bucket"
1: "example"
]
]2) Filter to a specific resource type
Use .where() to narrow down to the resources you care about:
terraform.resources.where( nameLabel == "google_storage_bucket" ) { * }3) Inspect specific arguments
Once you know what resources exist, drill into their arguments:
terraform.resources.where( nameLabel == "google_storage_bucket" ) { arguments }This returns the Terraform arguments found in the HCL:
cnspec> terraform.resources.where( nameLabel == "google_storage_bucket" ) { arguments }
terraform.resources.where: [
0: {
arguments: {
labels: {
environment: "dev"
}
location: "us-central1"
name: [
0: "lunalectric-bucket-"
1: "random_string.random.id"
]
uniform_bucket_level_access: true
}
}
]4) Assert a condition with .all()
Use .all() to write a pass/fail check that must hold for every matching resource:
terraform.resources.where( nameLabel == "google_storage_bucket" ).all( arguments.uniform_bucket_level_access == true )Using filters to control when checks run
When you package a check into a Mondoo policy, filters ensure it only runs on the right platform and when the relevant resources are present. This prevents false positives and unnecessary noise.
Example: Only run on Terraform HCL, and only when GCS buckets are present:
filters: asset.platform == "terraform-hcl" && terraform.resources.contains(nameLabel == "aws_s3_bucket")Terraform Plan (JSON)
Plan checks catch misconfigurations before they land in State or production, making them ideal for CI gating on pull requests. The workflow mirrors the HCL steps above, but uses terraform.plan.resourceChanges instead of terraform.resources.
Open a cnspec shell against a Plan file
First, produce a JSON plan:
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.jsonThen open the shell on the JSON:
cnspec shell terraform ./path/to/tfplan.json1) List resource changes
See what resources the plan will create, update, or destroy:
terraform.plan.resourceChanges { * }2) Filter and expand fields
terraform.plan.resourceChanges.where( type == "google_storage_bucket" ) { * }3) Inspect planned values
Check what a specific argument will be after terraform apply:
terraform.plan.resourceChanges.where( type == "google_storage_bucket" ) { change.after.uniform_bucket_level_access }4) Assert with .all()
Returns true only if every planned GCS bucket enables uniform bucket-level access:
terraform.plan.resourceChanges.where( type == "google_storage_bucket" ).all( change.after.uniform_bucket_level_access == true )5) Add a filter for policy use
Restrict this check to Plan files that contain the relevant resource type:
filters: asset.platform == "terraform-plan" && terraform.plan.resourceChanges.contains( type == "google_storage_bucket" )Terraform State (JSON)
State checks verify what actually exists in your infrastructure, closing the loop after HCL and Plan. The workflow is the same again, using terraform.state.resources.
Open a cnspec shell against a State file
Export the current state to JSON:
terraform show -json > tfstate.jsonThen open it in the shell:
cnspec shell terraform ./tfstate.json1) List resources in State
See what resources exist:
terraform.state.resources { type }2) Filter and expand fields
terraform.state.resources.where(type == "google_storage_bucket") { values }3) Inspect specific values
Check the current uniform_bucket_level_access setting for each GCS bucket:
terraform.state.resources.where( type == "google_storage_bucket" ) { values.uniform_bucket_level_access }4) Assert with .all()
Returns true only if every GCS bucket in State has uniform bucket-level access enabled:
terraform.state.resources.where( type == "google_storage_bucket" ).all( values.uniform_bucket_level_access == true )5) Add a filter for policy use
Restrict this check to State files that contain the relevant resource type:
filters: asset.platform == "terraform-state" && terraform.state.resources.contains(type == "google_storage_bucket")Putting it all together with policy variants
The real power of this approach is authoring one logical check that spans all three Terraform stages (and optionally runtime cloud APIs) using policy variants. A single policy definition can include:
- A title and numeric impact score
- Documentation with description and rationale
- Multiple remediation paths (Terraform code, console steps, CLI commands)
- Variants that adapt the same check to different platforms
For example, the "Ensure that Cloud Storage buckets have uniform bucket-level access enabled" check defines variants for Runtime, Terraform HCL, Terraform Plan, and Terraform State. See the full open source example.
Why this pattern works
- One intent, many surfaces: The top-level check captures the security rule once; variants adapt it to each stage.
- Noise-free filtering: Each variant runs only when the target platform and resource type are present.
- Clear ownership: Title, impact score, and rich docs make the check understandable and actionable for SRE, SecOps, and platform teams.
- CI to production continuity: HCL/Plan variants prevent bad changes; State/Runtime variants ensure reality stays compliant.