A misconfigured security group doesn't have to originate in a .tf file to be dangerous — sometimes it's introduced by a console click that Terraform never sees until the next plan. Snyk IaC's approach to this problem is to scan the machine-readable JSON representation of a Terraform plan, not just the source HCL, so that whatever Terraform is actually about to create, update, or leave untouched gets evaluated against a security rule set before terraform apply runs. Because a plan JSON reflects resolved values — variables interpolated, modules expanded, data sources looked up, and (when a refresh runs) real provider state reconciled — it captures configuration drift and computed attributes that static analysis of raw .tf files can miss entirely. This post walks through the mechanics: how the plan JSON is generated, what Snyk actually reads out of it, how that connects to drift detection, and where the technique's limits are.
What is a Terraform plan JSON file, and why does Snyk scan it instead of raw HCL?
A Terraform plan JSON is the structured, machine-readable output of terraform show -json run against a saved plan file, and Snyk scans it because it represents the resolved infrastructure state rather than the authored configuration. The typical workflow is:
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
snyk iac test tfplan.json
Raw HCL files can contain variables, count/for_each loops, dynamic blocks, module calls, and references to remote state or data sources — none of which have concrete values until Terraform actually evaluates them. Scanning main.tf in isolation means guessing what a var.allowed_cidr or a for_each over a map will ultimately produce. The plan JSON has already done that evaluation. Every resource that will exist after apply appears with its final, resolved attribute values, which is why Snyk added plan JSON as a first-class input format alongside HCL, CloudFormation, Kubernetes manifests, and ARM templates.
What does the plan JSON actually contain, and what does Snyk read out of it?
The plan JSON is organized around a resource_changes array, and each entry is what Snyk's rule engine evaluates. A resource_changes entry includes the resource address (e.g., aws_security_group.web), its type and provider_name, and a change object with two fields that matter most: actions and after. The actions array tells you what Terraform intends to do — ["no-op"], ["create"], ["update"], ["delete"], or ["delete","create"] for a forced replacement — and after holds the planned attribute values post-apply. Snyk runs its resource-type-specific checks against after (and, for updates, compares it with before) to look for things like an aws_security_group ingress rule with cidr_blocks: ["0.0.0.0/0"] on port 22, an aws_s3_bucket without a server_side_encryption_configuration block, or an aws_db_instance with publicly_accessible: true. Because after is the fully resolved value, this catches misconfigurations assembled dynamically — through a variable default, a module output, or a for_each — that never appear as a literal string anywhere in the source tree.
How does scanning the plan JSON specifically catch drift before deployment?
It catches drift because terraform plan itself performs a refresh against the real provider APIs before it computes the diff, so the JSON it produces already encodes any gap between the last-known state and the actual live resource. If an engineer opens port 3389 directly in the AWS console outside of Terraform, the next terraform plan refresh detects that the live security group no longer matches state, and the resulting plan reflects it — either as an update action that will silently re-apply the (now-drifted) configuration back to its intended value, or, if the .tf source was never updated to match, as a change that leaves the drifted rule in place. Snyk evaluating the after values in that plan surfaces the security-relevant result of that reconciliation before anyone runs apply again, rather than after the fact when the drifted resource has already been live and exposed. This is meaningfully different from scanning committed HCL, which only reflects what's declared in source control and has no visibility into what has quietly changed in the actual cloud account since the last apply.
Why did Snyk's coverage of drift-aware scanning expand after 2023?
It expanded because Snyk acquired Fugue in November 2023, a cloud security posture management vendor whose core technology — the open-source Regula policy engine — was purpose-built to evaluate Terraform plans and live cloud configuration with Open Policy Agent (OPA) Rego rules, including reconciling desired-state-versus-actual-state drift. Prior to that acquisition, Snyk IaC's plan-scanning capability was primarily a pre-deployment gate: run it in CI against a generated plan JSON, fail the pipeline on critical findings, block the merge. The Fugue technology brought continuous, live-environment posture checks into the same product family, so drift that occurs after a successful, clean deployment — not just drift caught by a subsequent plan — could also be flagged. The practical upshot for a team using snyk iac test on a plan JSON in CI is the same core mechanism described above; the acquisition mainly extended what happens to drift findings after they're detected, connecting them to a broader cloud posture view rather than a one-off pipeline check.
What can't the plan JSON tell Snyk, and where does this approach fall short?
The plan JSON can't tell Snyk about values Terraform itself doesn't know yet, and those gaps show up as unknown_values (in the plan's after_unknown map) for attributes such as auto-generated ARNs, resource IDs, or IPs assigned at creation time — fields a rule might need but that simply aren't populated until after apply actually runs. Sensitive values are also redacted: the plan JSON marks them in before_sensitive/after_sensitive maps rather than exposing the real value, which is correct for security hygiene but means a rule that needs to inspect, say, a secret's format can't. There's also a scope limit: plan JSON only covers resources Terraform manages. Anything created out-of-band — a manually provisioned IAM user, a security group edited directly in a cloud console that Terraform's state has no record of at all (as opposed to drift on a resource it does manage) — never enters a resource_changes array because Terraform doesn't know it exists. And because a plan is generated fresh at scan time, the safety of this entire approach depends on scanning happening on every pipeline run against a current plan, not a cached one from a prior commit.
How Safeguard Helps
Snyk IaC's plan-scanning model is a strong pre-deployment control, but it's only as trustworthy as the pipeline that produces the plan JSON and acts on the scan result — and that's the layer Safeguard is built to secure. A tfplan.json file is just another build artifact: if the CI job that generates it can be tampered with, if the scan step can be skipped on a specific branch, or if someone can push a plan straight to apply without the gate ever running, the scan's findings are moot regardless of how accurate they are. Safeguard focuses on making sure the infrastructure-as-code pipeline itself is verifiable — enforcing that IaC scan steps (including tools like snyk iac test) actually execute and pass before a deployment workflow is allowed to proceed, that the plan artifact scanned is the same one applied, and that changes to Terraform configuration and CI workflow files go through reviewed, signed commits rather than direct pushes. We also correlate IaC findings with the rest of the software supply chain — SBOM data, dependency provenance, and build attestations — so a drifted, publicly exposed database isn't a finding that lives only in a Snyk dashboard disconnected from everything else that touched the release. The scanning mechanics described above are sound engineering; Safeguard's role is making sure the process wrapped around them can't be quietly bypassed.