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 |
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.tfmodules/mod_a/main.tfmodules/mod_c/main.tfRun
checkov --directory . --check CKV_AZURE_35 --framework terraformExpected result
default_action = "Deny"is explicitly passed to module"c". The variable should resolve to"Deny".Actual result
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:
"a"(the same-source sibling)refargument from module"c"module.b.result.some_attrwith a literal string"a"→"z"(consumer now sorts after producer)