tforphans

command module
v0.1.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: MIT Imports: 27 Imported by: 0

README

tforphans

Release CI

Finds live infrastructure that no Terraform state manages: resources created by hand, left behind by deleted configurations or made by other tools. Then writes the import blocks to bring them under Terraform.

How It Works in 30 Seconds

  1. List what is live. Every provider with list resources lists its objects through terraform query (Terraform 1.14+), with the root module's own provider configuration. Providers without them are listed through their plural data sources (-scan) or a plugin.
  2. Read every state. The roots' own states, every root under -state-roots, and any -state file or URL, for every workspace.
  3. Match. Each live object is looked up in the states by its resource identity (or by attributes for older state).
  4. Report. What matches nothing is unmanaged: a table, JSON, or import blocks with tforphans adopt. The last line says whether the check was complete.
$ tforphans -state-roots ~/work/infra
SCOPE                    TYPE           NAME             IDENTITY                                               SOURCE
account_id 123456789012  aws_instance   debug-box        account_id=123456789012, id=i-0abc, region=eu-north-1  .
account_id 123456789012  aws_s3_bucket  tmp-export-2024  account_id=123456789012, bucket=tmp-export-2024, ...   .

512 live, 498 managed, 0 uncertain, 12 ignored, 2 unmanaged; 41 states read, 187 resource types queried, 58 skipped (-v to list)
coverage: complete

(Illustrative output.)

Supported Providers

Provider How it is listed What is covered Used on real infrastructure
AWS (hashicorp/aws) list resources 247 resource types in 6.68 not yet
Google Cloud (hashicorp/google, google-beta) list resources 152 resource types in 8.6 yes
Azure (hashicorp/azurerm) list resources 118 resource types in 5.9 not yet
Any other provider with list resources list resources whatever the installed version lists
Cloudflare (cloudflare/cloudflare 5.x) -scan cloudflare, plural data sources about 95 of 271 resource types; the rest are mostly settings yes
Proxmox VE (bpg/proxmox; telmate/proxmox in state) -scan proxmox, plural data sources VMs, containers, pools, users, groups, roles, HA, replication, SDN vnets, backup jobs, ACME accounts yes
Vault (hashicorp/vault) plugin vault.sh auth methods, roles, secrets engines, policies
Junos (jeremmfr/junos) plugin junos.py interfaces, BGP, policies, prefix lists, security zones, applications new
Anything else your own plugin: one JSON object per line

List resource coverage grows with every provider release, without changes here: tforphans -v lists the types a run skipped and why. Planned next: GitHub (-scan github), then plugins for Helm and Kubernetes.

Quick Start

  1. Install (macOS, Linux):

    brew install 5xxeu/tap/tforphans
    

    Other ways: Install.

  2. Go to an initialized root module, with read-only credentials for its provider in the environment, and run it:

    cd infra/aws/prod && terraform init
    tforphans
    
  3. Give it the states of every other root that manages the same accounts, or their objects show as unmanaged:

    tforphans -state-roots ~/work/infra
    
  4. Adopt what should be in Terraform:

    tforphans adopt -state-roots ~/work/infra -o imports.tf
    terraform plan -generate-config-out=generated.tf
    

For Cloudflare or Proxmox, no root module is needed:

export CLOUDFLARE_API_TOKEN=...   # "Read all resources" template
tforphans -scan cloudflare -state-roots ~/work/cloudflare

Contents

Why

Infrastructure created outside Terraform skipped code review, policy checks (checkov, tfsec, OPA) and cost estimates (Infracost), because all of those look at code. tforphans finds it, which helps with:

  • security: hand-made access policies, identity providers, tokens, members, tunnels, DNS records that may dangle (subdomain takeover);
  • cost: forgotten instances, disks, IPs and other resources nobody owns;
  • IaC coverage: knowing what still has to be brought under Terraform, and doing it with tforphans adopt.

It does not judge how a resource is configured (that is what CSPM tools such as Prowler do) and knows nothing about prices; -format json hands the findings to tools that do.

Features

  • 🔎 No Provider Knowledge to Maintain: Resource types come from the providers' own list resources, so coverage follows provider releases.
  • 🗂 Every State Counts: -state-roots finds and reads every root module and workspace in a tree, through any backend Terraform supports; Terragrunt units too.
  • 🪪 Exact Matching: Resource identity from state (Terraform 1.12+), checked against the provider that listed the object; older state is matched by attributes, and a match that cannot confirm the account or cluster is reported as uncertain, not as managed.
  • ✅ Honest Coverage: The last line says whether "nothing unmanaged" can be trusted, with every failed listing, unread state and skipped type counted; -strict turns gaps into a failed CI job.
  • 📥 Adoption: tforphans adopt writes import blocks for terraform plan -generate-config-out, with addresses that never collide with existing ones.
  • 🧹 Little Noise: Objects the cloud creates on its own (default security groups, GKE nodes, managed rulesets) are hidden by default presets that go only by marks users cannot set; an ignore file covers the rest.
  • 🌍 All Regions and Zones: -arg region=... fans list queries out; -arg zone=auto reads every Google Cloud zone.
  • ☁️ Beyond List Resources: Plural data sources for Cloudflare and Proxmox VE, with crash isolation; plugins for anything else.
  • 🔐 Read Only: Never applies your configuration and plans it only with -refresh, which writes nothing; read-only credentials are enough; no telemetry.
  • 📄 Scriptable: Versioned JSON output, counts per type and per scope, a config file per audit.

When Not to Use It

  • The objects belong to another tool by design (Argo CD, Helm, Kubernetes operators, CloudFormation): they show as unmanaged, because to Terraform they are. Ignore them, or use that tool's own drift detection.
  • You want to know how resources are configured (public buckets, open security groups): that is a CSPM tool's job; feed it -format json if you want both.
  • The provider has neither list resources nor plural data sources: only a plugin can list it.
  • OpenTofu without terraform: OpenTofu has no query command yet; only -no-query with -scan and plugins can work (not tested).

Install

Binaries for Linux, macOS and Windows are on the releases page, with checksums, an SBOM and a build provenance attestation (gh attestation verify tforphans_*.tar.gz -R 5xxeu/tforphans). With Homebrew (macOS, Linux):

brew install 5xxeu/tap/tforphans

Or build it (Go 1.26 or newer):

go install github.com/5xxeu/tforphans@latest

Requirements

Terraform in PATH (or -terraform /path/to/bin):

What Terraform
Listing with terraform query (the default) 1.14 or newer; tforphans checks and says so
Matching by resource identity state written by 1.12 or newer with a provider that records identities; older state is matched by attributes (less certain, see -refresh)
adopt import blocks with identity 1.12; with id 1.5
-no-query with -state, -state-roots, -scan, -plugin no query needed; tested only with 1.15 and 1.16 so far

The tests run against 1.15 and 1.16. Roots that are queried must allow 1.14 in their required_version (-ignore-required-version overrides it for the run); roots whose state is only read need not.

Credentials: whatever the providers and backends need; read-only is enough (AWS ReadOnlyAccess, GCP Viewer, Azure Reader, a Cloudflare "Read all resources" token, a Proxmox PVEAuditor token).

Exit status: 0 nothing found, 2 unmanaged resources found, 1 an error or, with -strict, incomplete coverage. See Output, exit status and coverage.

  • driftctl solved the same problem with its own scanners for every resource type; it is in maintenance mode. tforphans keeps no provider knowledge of its own and relies on terraform query, so coverage follows the providers.
  • tfimport works in the opposite direction: you write the configuration for existing infrastructure and it works out the import IDs. It does not look for unknown resources.

Development

go test ./...
go test -tags integration .   # needs terraform; builds testdata/fakeprovider

CI (.github/workflows/ci.yml) runs gofmt, go vet, the unit tests with the race detector, golangci-lint, govulncheck and the integration tests against Terraform 1.15 and 1.16. The integration tests use a fake provider (things are files in a temporary directory) that implements list resources and resource identity, and run the whole flow including the import round trip; the Proxmox test runs the real bpg/proxmox against a fake Proxmox VE API.

Some tests run the binary itself (this test binary as main): exit codes, stdout and stderr, SIGTERM and SIGINT with plugin children, and (with -tags integration) a stop during terraform query that must leave no temporary file in the root. testdata/golden holds JSON reports of a fixed state and plugin output; after an intended change of the format, rewrite them with TFORPHANS_UPDATE_GOLDEN=1 go test -run TestGoldenJSON . and review the diff. Every key of these reports must be described in docs/json.md.

Fuzz targets check that plugin output always becomes import blocks that parse as HCL with every id and identity value intact (FuzzImportHCL, FuzzHCLString), that no state crashes the parsers and every identity matches itself (FuzzParse), and that terraform query output gives the same result however it is split (FuzzParseQuery). The unit tests run their seeds; CI fuzzes each for a minute every night. Locally:

go test -run '^$' -fuzz '^FuzzImportHCL$' -fuzztime 5m ./internal/report/

A v* tag builds the release with GoReleaser (.goreleaser.yaml) as a draft, with checksums, SBOMs and a build provenance attestation, and pushes the Homebrew cask to 5xxeu/homebrew-tap (secret HOMEBREW_TAP_TOKEN: a fine-grained token with contents write on that repository only). The cask points at the release files, so publish the draft soon after.

License

MIT, see LICENSE.

Documentation

Overview

Command tforphans finds live infrastructure that no Terraform state manages.

For every root module it lists live resources of each list-capable resource type with `terraform query` (Terraform 1.14+), using the module's own provider configuration, and compares their resource identities with the module's state plus any extra states. Providers without list support are covered by plugins: commands that print the same data as JSON lines.

Directories

Path Synopsis
internal
dsource
Package dsource lists live objects for providers without list resources through their plural data sources (cloudflare_dns_records for cloudflare_dns_record, and so on), so the provider's own API code and authentication do the work.
Package dsource lists live objects for providers without list resources through their plural data sources (cloudflare_dns_records for cloudflare_dns_record, and so on), so the provider's own API code and authentication do the work.
glob
Package glob implements shell-style patterns where "*" matches any run of characters (including "/" and ".") and "?" matches one character.
Package glob implements shell-style patterns where "*" matches any run of characters (including "/" and ".") and "?" matches one character.
model
Package model holds the types shared by discovery sources and the report.
Package model holds the types shared by discovery sources and the report.
plugin
Package plugin runs external discovery commands for providers that do not implement list resources.
Package plugin runs external discovery commands for providers that do not implement list resources.
proc
Package proc runs child commands so that cancelling them stops everything they started.
Package proc runs child commands so that cancelling them stops everything they started.
report
Package report filters and prints unmanaged resources.
Package report filters and prints unmanaged resources.
state
Package state reads Terraform state and answers whether a live resource is already managed by it.
Package state reads Terraform state and answers whether a live resource is already managed by it.
tf
Package tf drives the terraform CLI: it reads state and provider schemas from a root module and lists live resources with `terraform query`.
Package tf drives the terraform CLI: it reads state and provider schemas from a root module and lists live resources with `terraform query`.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL