Skip to content

Terraform HCL

Import raw Terraform .tf files for quick analysis of firewall rules without running terraform plan first. HCL import works in the browser and in the CLI:

npx @netbobr/cli analyze main.tf --cloud-format tf-hcl

# A module split over several files
cat *.tf | npx @netbobr/cli analyze - --cloud-format tf-hcl

The HCL parser reads literal values only. For modules, for_each, dynamic blocks or computed values, convert your Terraform code to plan JSON with terraform show -json and use the Terraform Plan JSON format instead (97.0% match rate in accuracy testing).

Example .tf file

resource "aws_security_group_rule" "allow-https" {
  type        = "ingress"
  protocol    = "tcp"
  from_port   = 443
  to_port     = 443
  cidr_blocks = ["10.0.0.0/8"]
}

resource "aws_security_group_rule" "allow-web-egress" {
  type        = "egress"
  protocol    = "tcp"
  from_port   = 80
  to_port     = 80
  cidr_blocks = ["0.0.0.0/0"]
}

resource "aws_security_group_rule" "trusted-ssh" {
  type        = "ingress"
  protocol    = "tcp"
  from_port   = 22
  to_port     = 22
  cidr_blocks = [var.trusted_cidrs]
}

Parser behavior

  • Uses a regex-based parser (not a full HCL parser) to extract resource blocks by pattern matching.
  • Supports seven resource types: aws_security_group_rule, aws_security_group (inline ingress/egress blocks), aws_vpc_security_group_ingress_rule, aws_vpc_security_group_egress_rule, azurerm_network_security_rule, azurerm_network_security_group (inline security_rule blocks), and google_compute_firewall.
  • Comments (#, // and /* ... */) are removed before parsing, so commented-out resources and attributes are ignored. Comment characters inside quoted strings (for example "arn:aws:s3:::bucket/*") are kept, and braces inside quoted strings do not end a block. Heredoc bodies (<<EOT ... EOT, <<-EOT ... EOT) are ignored: no firewall attribute is written as a heredoc, and their text never ends a block or becomes a resource.
  • A resource header must start its line (after optional indentation), as Terraform formats it.
  • Attributes are read line by line, so cidr_blocks is never confused with ipv6_cidr_blocks. Lists may span several lines and contain comments. Unquoted numbers work (protocol = -1, from_port = 22).
  • aws_security_group rules may use block syntax (ingress { ... }) or attribute syntax (ingress = [ { ... } ]) with literal objects. Attribute syntax built from an expression (ingress = var.rules, a for expression) is skipped with a warning.
  • Balanced brace matching is used to find resource block boundaries.

Terraform references

Any non-literal value cannot be resolved without running terraform plan: var.*, local.*, resource references such as aws_vpc.main.cidr_block, function calls such as concat(...), interpolated strings such as "${var.cidr}/32", and unbracketed lists such as cidr_blocks = var.admin_cidrs. netbobr never drops these silently:

  • An address becomes the 0.0.0.0/0 placeholder with a warning, so the rule is still analysed, conservatively: TF aws_security_group_rule.ssh: source Terraform reference var.office_cidr cannot be resolved to CIDR -- substituted 0.0.0.0/0. Literal values in the same list are kept.
  • A port expression is analysed as 1-65535 with a warning; a protocol expression as ANY with a warning.
  • dynamic "ingress", dynamic "egress", dynamic "allow" and dynamic "deny" blocks cannot be expanded by a regex parser. They are skipped with a warning (... dynamic "ingress" blocks cannot be evaluated by the HCL parser -- those rules were skipped (use terraform show -json)); the static blocks of the same resource are still parsed.

AWS rules

  • aws_security_group_rule, inline ingress/egress blocks and the aws_vpc_security_group_*_rule resources behave like the native AWS parser: cidr_blocks / cidr_ipv4 give the remote side, and the group's member instances are the 0.0.0.0/0 placeholder, reported in one summary warning per file.
  • from_port = 0 with to_port = 65535 becomes 1-65535; protocol -1 (quoted or not) is ANY 1-65535. Unsupported protocols (ESP, AH, GRE, SCTP, ICMPv6) are skipped with a warning.
  • Peers that are not IPv4 CIDRs are skipped with a warning: security_groups, source_security_group_id, referenced_security_group_id, self = true, ipv6_cidr_blocks / cidr_ipv6, and prefix_list_ids / prefix_list_id.

GCP rules

google_compute_firewall follows the GCP parser: source_tags, source_service_accounts, target_tags and target_service_accounts are substituted with a warning; source_ranges = [] is treated like an absent attribute; deny { } blocks are counted in one "deny rules were not evaluated" warning per file; disabled = true rules are skipped with a warning.

Azure rules

azurerm_network_security_rule resources and the inline security_rule { } blocks of azurerm_network_security_group follow the same rules as the native Azure NSG parser:

  • Singular and plural attributes are combined and expanded: source_address_prefix(es), destination_address_prefix(es), destination_port_range(s). Lists may span several lines and contain comments.
  • Source maps to source and destination to destination in both directions: outbound rules are not swapped.
  • * and Internet mean 0.0.0.0/0. Other service tags and source/destination_application_security_group_ids are replaced by the 0.0.0.0/0 placeholder with a warning.
  • Any non-literal address is substituted with 0.0.0.0/0 and always warned. That covers var.*, local.*, resource references such as azurerm_subnet.web.address_prefixes[0], and interpolated strings such as "${var.cidr}". Example warning: TF azurerm_network_security_rule.ssh: source Terraform reference var.office_cidr cannot be resolved to CIDR -- substituted 0.0.0.0/0.
  • A rule whose source and destination are both unresolvable is skipped with one warning instead of becoming an any-to-any flow.
  • A non-literal destination_port_range is analysed as 1-65535. A non-literal access is assumed Allow, and a non-literal protocol is assumed * (any). Each of these gets a warning.
  • access is case-insensitive. Deny rules are not evaluated and are counted in one summary warning per file. Esp/Ah rules are skipped with a warning.
  • dynamic "security_rule" blocks and the security_rule = [ ... ] attribute syntax cannot be evaluated by a regex parser. They are skipped with a warning; use terraform show -json for those NSGs.
  • Inline rules are reported per rule: ruleName is the rule name, ruleId is azurerm_network_security_group.<name>/<rule name>.

Limitations

LimitationDetail
Regex parserNot a full HCL parser. Complex expressions, locals, data sources, and conditional expressions are not evaluated.
Variable referencesvar.*, local.*, resource references and interpolated strings are substituted with 0.0.0.0/0 (addresses) or 1-65535 (ports), always with a warning.
for_each / countDynamic resource generation with for_each or count is not expanded.
dynamic blocksSkipped with a warning.
ModulesModule calls are not followed -- only resources defined in the scanned file(s) are parsed.

For comprehensive analysis, convert to plan JSON first:

terraform plan -out=tfplan
terraform show -json tfplan > plan.json

Then use the Terraform Plan JSON import, which supports the same resource types with every value resolved by Terraform.

Accuracy

Accuracy measured against a 400-rule CSV baseline (360 Tier 1 + 40 Tier 2).

MetricValue
CLI match rate11.0% (44/400)
Browser match rate11.0% (44/400)
Tier 1 accuracy12.6% (44/348)
Field accuracy100% for matched flows
Warnings generated8 (6 Terraform variable references, plus one AWS member-instance and one GCP target-instance summary)

The low match rate reflects the test fixture, not the parser: the HCL fixture is a small sample (45 flows) of the 400 baseline rules, so the other rows are expected missing. Flows that use variables are analysed with the 0.0.0.0/0 placeholder; they match the baseline only where it also has a 0.0.0.0/0 rule. For full coverage of real code, use the Terraform Plan JSON format (97.0% match rate).

Troubleshooting

"Very few rules parsed" Check the warnings: dynamic blocks, unsupported protocols and non-CIDR peers (security groups, IPv6, prefix lists) are skipped with a reason. Resources created with for_each/count or inside modules are only visible in plan JSON:

terraform plan -out=tfplan && terraform show -json tfplan > plan.json

"Variable warning" var.* references in CIDR blocks cannot be resolved without running terraform plan. The parser substitutes 0.0.0.0/0 for them and warns, so the rule is still analysed (conservatively, often with a higher risk score). Use plan JSON to analyse the real values.

"Complex expressions ignored" Locals, data sources, conditional expressions (condition ? a : b), and for_each are not evaluated. Only literal values are extracted.

"No resources found" Verify the file contains resource blocks with one of the supported types. The parser matches resource "type" "name" { patterns.

Browser: Drag and drop a .tf file onto the import area. HCL format is auto-detected by the presence of resource blocks with Terraform patterns.