Snyk IaC scans Terraform files without ever running terraform plan, without terraform init, and without touching a single cloud credential. That trick depends on a static analysis pipeline that converts raw HCL syntax into a JSON document a policy engine can reason about, then checks that document against a library of more than 400 pre-built rules covering AWS, Azure, GCP, and Kubernetes. None of this is guesswork on Snyk's part or ours: Snyk has open-sourced the Go library that does the actual parsing (snyk-iac-parsers), documented the Open Policy Agent-based rule engine it evaluates against, and published an engineering post explaining how it maps a policy violation back to an exact line number in your source file. This post walks through each stage of that pipeline — parse, normalize, evaluate, and locate — using only what Snyk has publicly documented.
What actually happens when Snyk IaC scans a Terraform file?
Snyk IaC reads the .tf files off disk and parses them entirely locally, without uploading the raw configuration to Snyk's servers and without needing a live connection to a cloud provider. Since Snyk IaC launched in August 2020 with coverage for Kubernetes, Helm charts, and Terraform AWS resources, its default mode has targeted static configuration rather than a live plan: the CLI locates whatever .tf files sit in the target directory, hands them to a bundled parser, and evaluates the result — no provider plugins, no .tfstate file, and no AWS, Azure, or GCP API keys required for the base scan. Snyk added a second input mode in an April 22, 2021 public beta that instead consumes the JSON output of terraform plan (via terraform show -json), letting the identical rule set assess proposed infrastructure changes rather than only what's already committed. Both paths — static source and plan JSON — feed into the same downstream parsing and evaluation logic.
How does Snyk turn HCL's block syntax into something a policy engine can query?
It converts HCL into plain JSON with a dedicated parser, then hands that JSON to Open Policy Agent (OPA) as a structured input document. Terraform's HCL2 is a block-structured, domain-specific syntax — not JSON — so a general-purpose policy engine that only understands structured documents can't query it directly. Snyk closes that gap with a small Go library it publishes publicly as snyk-iac-parsers on GitHub (17 releases as of this writing, most recently v1.3.1), which bundles three parsers: one for HCL2 source, one for Terraform plan JSON (it extracts the resource_changes array), and one for YAML, used for Kubernetes manifests. Whatever the input format, the output shape is identical: JSON. Resource blocks become JSON objects keyed by resource type and name, block attributes become nested fields, and references between resources — a security group ID pointed to by an EC2 instance, for example — persist as plain string values a policy can follow across the document. That normalized JSON becomes the input document every Rego rule evaluates against.
How are the actual security checks written and run against that JSON model?
They're written as Rego policies, evaluated by Open Policy Agent, and shipped as versioned, Wasm-compiled rule bundles rather than a bespoke Snyk rule language. Every one of Snyk's built-in checks — more than 400 in its current catalog spanning AWS, Azure, GCP, and Kubernetes — is expressed in Rego, OPA's declarative query language for JSON documents. Mechanically, a rule walks the input looking for a resource shape it considers unsafe — an aws_s3_bucket with no corresponding aws_s3_bucket_public_access_block, or a security group ingress rule whose cidr_blocks includes 0.0.0.0/0 — and emits a deny with a message when it finds a match. Snyk exposes this same mechanism to customers through the snyk-iac-rules SDK: teams scaffold a new rule with snyk-iac-rules template --rule <name> --format hcl2|json|yaml|tf-plan, write it and its unit tests in Rego, build it into a bundle, and push that bundle to an OCI-compliant container registry so the Snyk CLI can pull it down the way it would any other dependency. Because rules compile to WebAssembly modules before execution, custom rule authors are limited to whichever OPA built-in functions have Wasm support — a real, documented constraint rather than an incidental one.
How does a violation in the JSON model get mapped back to a specific line in your .tf file?
Snyk instruments OPA's own execution trace rather than tracking line and column numbers while parsing. Snyk's engineering team described this directly in its "Automatic source locations with Rego" post: as the parsed document is loaded, every term in it is recursively annotated with metadata recording its path within the input structure — reusing a metadata field OPA already supports on terms. When a policy actually runs, Snyk attaches to OPA's Tracer interface, which emits events describing what the interpreter is doing at each step, and uses those events to record which specific paths a rule touched — whether through a unification, a built-in function argument, or a bare expression. Once a rule denies, Snyk has the exact attribute path that triggered it; resolving that path to a line and column is then a matter of walking the original YAML or HCL node tree until it lands on the node matching that path. The result is that source-location reporting works automatically for both built-in and custom rules, without a rule author having to do anything special to make it happen.
What can't this static model see, and why does plan-file scanning exist alongside it?
It can't see anything Terraform only learns by talking to a provider — computed attributes, data source lookups, or the real state of resources already running in the cloud — because the parser only ever reads the literal text of the configuration on disk. A count or for_each expression whose length depends on a variable set in a .tfvars file resolves fine, since that value is still just text in the repository. But a value that depends on a data "aws_ami" "latest" lookup, or a resource attribute the provider only fills in after a real API call — an auto-generated ARN, for instance — simply doesn't exist anywhere in the static JSON, so no rule can evaluate it. That gap is the documented reason Snyk shipped Terraform plan scanning in its April 2021 beta: feeding the engine terraform show -json output instead of raw .tf source lets the same 400-plus rule set evaluate the fuller, provider-resolved resource_changes list, at the cost of needing an actual plan run to exist first. In Snyk's own framing, static source scanning and plan-JSON scanning are complementary stages of one pipeline rather than two competing modes — one runs on every commit with zero setup, the other runs later, once a plan is already being generated as part of the deployment workflow.
How Safeguard Helps
Everything above happens inside the boundary of a single Terraform repository or plan file — the parser only ever reasons about the HCL or JSON it's handed. It has no visibility into where that HCL came from: which registry a third-party module was pulled from, whether the provider binary running in your pipeline matches what was published upstream, or whether the plan JSON being scanned is actually the artifact your CI system produced and not something altered in between. That's the layer Safeguard focuses on. Rather than re-implementing HCL parsing or misconfiguration rules, Safeguard tracks the supply chain around the infrastructure code itself — verifying the provenance of Terraform modules and providers pulled into a build, generating SBOMs that cover infrastructure dependencies alongside application ones, and attesting that a plan or build artifact reaching a security gate is the one your pipeline actually generated. Static configuration analysis tells you a resource is misconfigured; supply chain attestation tells you whether you can trust the input that analysis ran against in the first place. Used together, they cover more of the path from source code to running infrastructure than either does alone.