MonPhare
Your Terraform modules are drifting. This tool catches it before prod does.
Terraform and OpenTofu module constraint analyzer — catches version drift, deprecated modules, and risky constraints across all your repos before production does.
mon-far — French for “my lighthouse”. Because someone has to shine a light on your Terraform constraints.
I built this after spending a weekend debugging a production incident that traced back to a module version constraint nobody had noticed was wrong. The module had no upper bound, a breaking major version dropped, and terraform init on a fresh CI runner pulled it in silently. No warning. No error. Just broken infra.
The fix took twenty minutes. Finding the cause took four hours.
The Problem
Across a large platform team’s repos, this is happening silently right now:
# team-a/main.tf — no pin, pulls latest on every initmodule "vpc" { source = "terraform-aws-modules/vpc/aws"}
# team-b/main.tf — accepts anything, including breaking changesmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = ">= 0.0.0"}
# team-c/main.tf — frozen, no security patchesmodule "vpc" { source = "terraform-aws-modules/vpc/aws" version = "= 2.44.0"}terraform init is non-deterministic for team-a. Team-b silently pulls a breaking major on the next run. Team-c misses CVE patches for months. None of this surfaces in code review. It surfaces in a 2am incident.
The frustrating part is that these aren’t hard problems to catch — you just need something that actually looks. That’s what MonPhare does.
What It Catches
| Issue | Risk | Example |
|---|---|---|
| Missing version | Non-deterministic inits | source = "aws-modules/vpc/aws" (no version) |
| Too broad | Accepts breaking changes | version = ">= 0.0.0" |
| Wildcard | Same risk, different syntax | version = "*" |
| No upper bound | Major version creep | version = ">= 3.0" |
| Exact pin | Frozen — no security patches | version = "= 2.44.0" |
| Pre-release in prod | Instability risk | version = "~> 2.0.0-beta" |
| Cross-repo conflict | Incompatible ranges across teams | repo-a: >= 5.0 vs repo-b: < 5.0 |
| Deprecated module | Known CVE, retired version | vpc/aws at 1.0.1 |
| Deprecated provider | Security team-flagged range | azurerm < 3.50.0 |
| Deprecated runtime | Terraform/OpenTofu too old | terraform < 0.13.0 |
How It Works
Static analysis of HCL across one or more repositories. No Terraform binary, no terraform init, no credentials required to get a full constraint picture.
Three passes under the hood:
- HCL parsing — walk the file tree, extract all
module,provider, andterraformblocks, collectsourceandversionfields with file location - Graph construction — build a directed dependency graph from root modules down through all transitive references; track which repo each constraint comes from for cross-repo conflict detection
- Constraint resolution — for each module, collect all version constraints from all callers across all scanned repos, run semver range intersection, flag anything problematic
The deprecation rules come from a monphare.yaml config you define yourself — specific module versions with CVE references, provider version ranges your security team has flagged, runtime version floors. The tool doesn’t know what “deprecated” means for your environment; you tell it.
Real Output
MonPhare v0.3.0 [FAILED] 3 errors, 3 warningsScanned: 1 files, 4 modules, 3 providers
+------+-----------------------+----------------+----------+-----------+| Sev | Resource | Issue | Current | File |+------+-----------------------+----------------+----------+-----------+| ERR | module.vpc_no_version | No version | - | main.tf:0 || ERR | module.git_module | No version | - | main.tf:0 || ERR | provider.aws | No version | - | main.tf:0 || WARN | resource.google | No upper bound | - | main.tf:0 || WARN | resource.azurerm | No upper bound | - | main.tf:0 || WARN | provider.google | Too broad | >= 0.0.0 | main.tf:0 || INFO | resource.eks_exact | Exact version | - | main.tf:0 |+------+-----------------------+----------------+----------+-----------+
Fix errors to pass.CLI Usage
# scan a local directorymonphare scan ./terraform
# scan a remote repo directly (public repos, no token needed)monphare scan https://github.com/terraform-aws-modules/terraform-aws-vpc
# scan an entire GitHub orgexport MONPHARE_GIT_TOKEN=ghp_xxxmonphare scan --github my-org
# output JSON for CI integrationmonphare scan ./terraform --format json --output report.json
# strict mode: exit code 1 on warnings (useful in CI)monphare scan ./terraform --strict
# visualize the dependency graph as Mermaid (renders in GitHub)monphare graph ./terraform --format mermaidIt also supports GitLab, Azure DevOps, and Bitbucket for the remote scanning path.
CI Integration
# GitHub Actions- name: Analyze Terraform constraints run: monphare scan ./terraform --strict --format json --output report.jsonExit codes: 0 = clean, 1 = warnings (with --strict) or runtime error, 2 = constraint errors.
Installation
# Homebrewbrew tap tanguc/tap && brew install monphare
# Dockerdocker run --rm -v "$(pwd):/workspace" ghcr.io/tanguc/monphare scan /workspace
# From sourcecargo install --path .Pre-built binaries for Linux (x86_64, ARM64), macOS (Intel, Apple Silicon), and Windows on the releases page.
What I Learned Building It
HCL is a surprisingly irregular format. The grammar documentation has real gaps, and production Terraform code uses patterns the spec doesn’t clearly define — dynamic source values, for_each on modules, workspace-specific overrides. Building a tolerant parser that handles these edge cases took longer than all the analysis logic combined.
The constraint arithmetic was the other hard part. Semver range intersection sounds straightforward until you’re mixing ~>, >=, !=, and exact pins in the same constraint set. The cross-repo conflict case adds another layer: collect constraints from N repos, check whether any combination of them produces an empty intersection. Get the intersection logic wrong and you either miss real conflicts or flag constraints that are actually fine.
Rust was the right choice here. The type system forces you to make the semver representation explicit and handle every edge case at compile time rather than discovering it in a test at runtime. It also meant the binary is fast enough to scan a large org’s repos without anyone complaining.