skip to content

A Trivy 0.74 config scan reports nothing on a Terraform security group whose CIDR comes from prod.tfvars — why, and what does --tf-vars change?

level: middleimportance: should knowfreq 15%

answer

  1. static evaluation, no plan
  2. where variable values come from
  3. unknown is not a finding
  4. the warning in the log
  5. one tfvars file per environment

basics

~20 s

Trivy evaluates HCL without running Terraform and reads root variable values only from defaults, TF_VAR_ variables and files passed with --tf-vars. Unset variables become unknown, so a check cannot see 0.0.0.0/0. Passing --tf-vars prod.tfvars supplies the real value.

solid answer

~40 s

Trivy evaluates Terraform statically: it resolves variables, locals, functions and module inputs, but it never runs `terraform plan` or calls a provider. In 0.74 a root-module variable's value comes from its `default`, from `TF_VAR_` environment variables, or from files you name with `--tf-vars`; Trivy does not pick up `terraform.tfvars` or `*.auto.tfvars` on its own. A variable with no default and no supplied value becomes *unknown*, Trivy logs a warning that variable values were not found, and the security-group check sees an unknown CIDR rather than `0.0.0.0/0`, so it stays quiet. `trivy config --tf-vars prod.tfvars ./infra` gives it the value production uses, and the open ingress appears. Run one scan per environment, since each tfvars file can produce different findings. Values resolved from `data` sources or computed attributes stay unknown even then.

code

hcl · 12 lines
hcl
variable "ingress_cidr" {
  type = string
}

resource "aws_security_group_rule" "https_in" {
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  cidr_blocks       = [var.ingress_cidr]
  security_group_id = aws_security_group.web.id
}

go deeper

for a junior

Remember that Trivy reads Terraform files without running Terraform, so it only knows variable values you give it.

for a middle

Explain the three value sources, the unknown fallback and its log warning, and why an unknown CIDR produces no finding rather than a failure.

for a senior

Show you would scan per environment with the deployed tfvars, read the warning as a coverage gap, and know which values stay unknown regardless.

for a principal

Weigh HCL scans with tfvars against scanning plans in the pipeline: speed and early feedback versus resolved values, and who owns keeping inputs aligned.

## How Trivy reads Terraform Trivy's Terraform scanner is a **static evaluator**. It parses the HCL, builds the module tree, and evaluates expressions (variables, locals, functions such as `cidrsubnet()`, `for_each`, module inputs) into concrete attribute values. Checks from the trivy-checks bundle then inspect those values: is `cidr_blocks` `0.0.0.0/0`, is encryption enabled, is logging configured. What it does **not** do is run Terraform. There is no `terraform plan`, no provider call, no read of remote state. Anything Terraform would only learn at plan or apply time is invisible to it. ## Where variable values come from In Trivy 0.74 the Terraform parser fills the root module's input variables from three places (a child module gets its inputs from the calling `module` block): | Source | How Trivy gets it | |---|---| | `default` in the `variable` block | read from the HCL itself | | `TF_VAR_<name>` environment variables | read from the scanning process's environment | | tfvars files | only the files named with `--tf-vars` (repeatable; `.tfvars` or `.tfvars.json`) | Two details matter in practice: - Trivy does **not** auto-load `terraform.tfvars` or `*.auto.tfvars` the way `terraform plan` does. If the value lives there, you must name the file. - Files are applied in the order given, after the environment variables, so with `--tf-vars common.tfvars --tf-vars prod.tfvars` a key set in both takes the `prod.tfvars` value. ## What happens to a variable nobody set If a variable has no `default` and no value from the environment or a named file, Trivy marks it **unknown** (typed if the block declares a `type`) and logs a warning: *"Variable values were not found in the environment or variable files. Evaluating may not work correctly."* The scan continues. An unknown value is not a violation. A check that asks "is this CIDR `0.0.0.0/0`?" cannot answer yes for an unknown, so the usual outcome is silence: a **false negative**. Trivy's own documentation warns that unknowns, and fallbacks such as `try()` defaults, can produce false positives as well as false negatives, depending on how a check is written. ## The scenario, step by step 1. `variables.tf` declares `variable "ingress_cidr" { type = string }` with no default. 2. `prod.tfvars` sets `ingress_cidr = "0.0.0.0/0"`. 3. CI runs `trivy config ./infra`. The variable is unknown, the warning is logged, the security-group check passes. 4. Production is applied with `prod.tfvars` and the port is open to the internet. The fix is to scan what you deploy: `trivy config --tf-vars prod.tfvars ./infra`. Run one scan per environment's tfvars file, because staging and production can legitimately differ. ## Related inputs and limits - **Downloaded modules** under `.terraform` are scanned by default; `--tf-exclude-downloaded-modules` drops their findings when a third-party module's noise drowns yours. - **CloudFormation** has the equivalent `--cf-params` for parameter files; **Helm** has `--helm-values` and `--helm-set`. - **Data sources and computed attributes** (IDs, IP addresses, DNS names known only after apply) stay unknown whatever you pass. Feeding the scanner a plan instead of HCL closes that gap at a cost, and is a separate Terraform workflow decision. - **File functions** such as `file()` can only read inside the scan root; scanning a subdirectory that references `../shared/` leaves those values unresolved. ## What to watch in CI - Treat the "Variable values were not found" warning as a coverage signal, not log noise; it names the variables Trivy could not resolve. - Keep the tfvars file the pipeline applies and the one it scans the same file, ideally the same path variable in the job. - Never put real secrets in the tfvars you hand Trivy just to make the scan resolve; the checks only need the security-relevant values. ## Confirming a value actually resolved A silent pass and a genuine pass look the same in a default report, so make the difference visible: - `--include-non-failures` lists the checks that passed, so you can see the security-group check was evaluated against that resource at all. - For checks that do fire, `--render-cause terraform` prints the rendered cause in the table, showing the value Trivy computed rather than the raw `var.ingress_cidr` expression. - Plant the bad value deliberately once (a throwaway tfvars with `0.0.0.0/0`) and confirm the scan fails; that proves the variable path works before you rely on it. If the warning names variables you consider irrelevant, give them harmless defaults in a scan-only tfvars rather than ignoring the line, so the next genuinely missing value stands out.

  • The same module is applied with dev.tfvars and prod.tfvars. One scan or two?
    Two. Each `--tf-vars` file can resolve variables to different values, so a check may pass for dev and fail for prod. Scanning once without either file evaluates neither environment; scanning with both merges them, and the later file's values win where keys overlap.
  • Would setting TF_VAR_ingress_cidr in the CI job work instead of --tf-vars?
    Yes for that variable: Trivy reads `TF_VAR_` environment variables into its inputs. It does not scale well, though, and a `--tf-vars` file passed on the same run overrides the environment value for any key both define.

saying these in an interview costs you the question

  • Trivy picks up terraform.tfvars automatically like terraform plan does
  • An unresolved variable makes the Terraform scan fail outright
  • Trivy runs terraform plan under the hood to get real values
  • With --tf-vars set, data sources and computed IDs are resolved too
  • A clean scan without tfvars means every environment is clean