Skip to content

Variable resolution fails when two module instances share the same source and a third module references one's output #7470

@pszypowicz

Description

@pszypowicz

Description

When two module calls share the same source, and a third module (different source) references one of their outputs, Checkov fails to resolve variables in the third module. Explicitly-passed argument values remain as unresolved var.* references, causing false positives on any check that inspects those attributes.

The bug is ordering-dependent: it only triggers when the sibling module's name sorts alphabetically before the producer's name (e.g., module "a" before module "b").

Checkov version: 3.2.508

Minimal reproduction

Directory layout

.
├── main.tf
└── modules/
    ├── mod_a/
    │   └── main.tf
    └── mod_c/
        └── main.tf

main.tf

module "b" {
  source = "./modules/mod_a"
}

module "a" {
  source = "./modules/mod_a"
  result = module.b.result
}

module "c" {
  source = "./modules/mod_c"

  default_action = "Deny"

  ref = module.b.result.some_attr
}

modules/mod_a/main.tf

variable "result" {
  default = null
}

output "result" {
  value = var.result
}

modules/mod_c/main.tf

variable "default_action" {
  type    = string
  default = "Deny"
}

variable "ref" {
  default = null
}

resource "azurerm_storage_account" "sa" {
  network_rules {
    default_action = var.default_action
  }
}

Run

checkov --directory . --check CKV_AZURE_35 --framework terraform

Expected result

Passed checks: 1, Failed checks: 0

default_action = "Deny" is explicitly passed to module "c". The variable should resolve to "Deny".

Actual result

Passed checks: 0, Failed checks: 1

Check: CKV_AZURE_35: "Ensure default network access rule for Storage Accounts is set to deny"
        FAILED for resource: module.c.azurerm_storage_account.sa
        File: /modules/mod_c/main.tf
        Calling File: /main.tf

The resource config shows default_action = var.default_action — the variable reference was never resolved.

Root cause

In TerraformLocalGraph._should_add_edge(), the second condition uses a loose path-only comparison (self.get_abspath(vertex.source_module_object.path) == self.get_abspath(module_node.path)) to decide which output vertex to connect an edge to. When two modules share the same source, their outputs live at the same path, so the edge connects to whichever output is encountered first (alphabetically by module name) rather than the correct module instance.

The fix is to replace the path comparison with get_vertex_as_tf_module(module_node), which compares the full module identity (name + path + nested modules) instead of just the path.

Elimination matrix

Each row changes one thing from the failing reproduction:

Change Result
Remove module "a" (the same-source sibling) PASS
Remove ref argument from module "c" PASS
Replace module.b.result.some_attr with a literal string PASS
Rename "a""z" (consumer now sorts after producer) PASS

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions