IaC Terraform Audit
ultrathink
Output path directive (canonical — overrides in-body references).
All file outputs from this skill MUST be written under .anthril/audits/iac-terraform-audit/.
Run mkdir -p .anthril/audits/iac-terraform-audit before the first Write call.
Primary artefact: .anthril/audits/iac-terraform-audit/<artefact>.
Do NOT write to the project root or to bare filenames at cwd.
Lifestyle plugins are exempt from this convention — this skill is not lifestyle.
When to use
Run this skill when the user mentions:
- Terraform review, IaC audit, infrastructure security
- Checkov, tfsec, OpenTofu review, Pulumi audit
- Pre-migration infra cleanup
- State file concerns, provider pinning, module design
Covers eight categories: state and backend configuration (remote state, encryption, locking, workspace separation), provider pinning (required_providers with version constraints, required_version), security (Checkov/tfsec taxonomy — public S3 ACLs, unencrypted RDS, open security groups, wildcard IAM, plaintext secrets in tfvars), module hygiene (variable validation, descriptions, types, outputs, sensitive flag), environment separation, drift risk, cost hotspots, and CI testing coverage.
Before You Start
- Determine operating mode.
--live runs terraform plan against each module (refresh-only, no apply). --apply writes HCL patches. --runtime is not applicable (Terraform has no safe runtime test).
- Enumerate modules. Run
scripts/list-modules.sh — groups .tf files by directory.
- Sub-agent budget. One agent per module. Warn above 10.
- Load
.iac-ignore for suppressions (pattern: <module_path>:<finding_id> or <module_path>:*).
- Live-mode requirements.
terraform init must succeed per module — warn the user if state backends require credentials and fall back to static audit for modules that can't initialise.
User Context
$ARGUMENTS
Module inventory: !bash "${CLAUDE_PLUGIN_ROOT}/skills/iac-terraform-audit/scripts/list-modules.sh"
Tools: !which terraform tofu 2>/dev/null | head -1 || echo "terraform:unavailable" · !which checkov tfsec 2>/dev/null | head -1 || echo "checkov:unavailable"
Audit Phases
Phase 1: Discovery & Mode Selection
- Parse module inventory. Identify Terraform / OpenTofu / Terragrunt / Pulumi.
- Confirm scope; warn if >10 modules.
- Verify live-mode tools. Fall back per-module if
terraform init fails.
Phase 2: Per-Module Snapshot
For each module, parse HCL (via hcl2json if available, else structural grep):
- Providers (
required_providers, version constraints, source)
- Required Terraform version (
required_version)
- Backend config (type, encryption, locking)
- Resources (by provider+type, name)
- Variables (with type, default, description, sensitive, validation)
- Outputs (with sensitive, description)
- Locals
- Data sources
- Module calls (local / registry / Git, with version)
- Lifecycle blocks
In --live mode, additionally run terraform plan -refresh-only and capture the summary (additions / changes / destructions predicted).
Phase 3: Cross-Module Topology
- Build a module-dependency graph (who calls whom via
module.<name> blocks, who reads whose state via terraform_remote_state).
- Detect shared backends (multiple modules writing to the same state key — usually a bug).
- Emit Mermaid graph for the report.
Phase 4: Parallel Sub-Agent Audit
Spawn one Agent(subagent_type=Explore) per module in a single assistant message. Each walks categories A–H in reference.md §1:
- A. State & Backend — remote backend, encryption at rest, state locking (DynamoDB / GCS / etc.), workspace separation, state file not in git
- B. Provider pinning —
required_providers with ~> or exact, required_version set, source pinned to a hash registry URL
- C. Security (Checkov/tfsec) — open SGs, public S3 ACLs, unencrypted RDS, IAM
*, KMS missing, plaintext secrets, EBS/EFS encryption
- D. Module hygiene — variables with
type and description, validation blocks for bounded inputs, sensitive = true on secrets, outputs documented
- E. Environment separation — workspace or directory per env, no cross-env refs, prod state isolated
- F. Drift risk —
lifecycle.ignore_changes abuse (should be targeted), null_resource for imperative work, create_before_destroy on resources that require it
- G. Cost hotspots — oversized default instance types, unbounded autoscaling max, NAT Gateways when cheaper egress suffices, attached-but-unused EIPs
- H. Testing & CI —
terraform fmt / validate / tflint integration in CI, terraform-docs generation, pre-commit hooks
Sub-agents MUST NOT run terraform apply / terraform destroy / state-mutating commands. Read-only plans only.
Phase 5: Cross-Module Analysis
- Duplicate resource patterns across modules → module extraction candidates
- Cyclic dependencies via
terraform_remote_state
- Provider version drift (module A uses AWS ~>5, module B uses ~>4)
- Missing
required_version consistency
Phase 6: Merge & Risk Register
Consolidate findings. Dedupe via .iac-ignore. Apply severity adjustments:
- Findings on production modules (determined via workspace name or directory convention) keep severity
- Findings on non-prod modules downgrade one tier
- Checkov/tfsec findings (if live tools available) cross-validated with sub-agent output
Assign IAC-001… IDs.
Phase 7: Remediation Drafting
Emit commented HCL blocks to iac-suggested.tf. Rules:
- Every FK-like resource relationship uses
depends_on = [] explicit when needed
- Every state change to sensitive resources is commented with
# MANUAL REVIEW — STATE RISK
- Provider pinning suggestions include both the current pin and the recommended
- Module extraction suggestions emit a minimal new module skeleton in a sub-block of the suggestion file
Phase 8: Apply Mode (opt-in)
When --apply, iterate findings with [a]pply / [s]kip / [A]ll / [q]uit. Any HCL change that would affect state (adding lifecycle { prevent_destroy = true }, changing a backend key) requires DESTROY confirmation.
Phase 9: Reporting
Write iac-terraform-audit.md + iac-terraform-audit.json + iac-suggested.tf (+ terraform-plan.txt in live mode).
Scoring
Weights: A=20, B=10, C=30, D=10, E=10, F=10, G=5, H=5 (sum 100). See reference.md §3.
| Total |
Verdict |
| 90+ |
PASS |
| 70–89 |
PASS WITH WARNINGS |
| 50–69 |
CONDITIONAL |
| <50 |
FAIL |
Important Principles
- State is the crown jewel. Remote, encrypted, locked, and never in git.
~> is a minor-version pin. ~>5.0 allows 5.x; ~>5.0.0 allows 5.0.x. Know which you want.
- Security findings that are true on paper may be false in context. A public S3 ACL on a static-site bucket is intentional. Flag but let the user suppress.
lifecycle.ignore_changes = all is almost always wrong. Target specific attributes.
null_resource + local-exec is imperative. Flag every occurrence.
- Cost findings are suggestions, never failures. Use INFO severity.
- Australian English. DD/MM/YYYY. Markdown-first.
Edge Cases
- Terragrunt. Parse
terragrunt.hcl blocks (include, dependency, generate). Findings on generated files go to the terragrunt.hcl that generated them.
- Pulumi. Parse
index.ts / __main__.py. Map to the same A–H taxonomy.
- CDK (TypeScript/Python). Skip — not in scope. Emit one finding: "CDK module detected; not covered by this skill."
- Remote module (
source = "git::..." or source = "terraform-aws-modules/..."). Audit the caller's reference and the version; do not recurse into the remote.
terraform init fails in live mode. Record as a module-level limitation; continue static audit.
- State encrypted with SSE-KMS but key is unmanaged. Flag MEDIUM — "encrypted but key lifecycle unclear".
- Mono-repo with many modules. Cluster by top-level directory; one sub-agent per cluster if counts are high.
- A module has only data sources, no resources. Audit the providers and backend; skip C/F.
1---2name: iac-terraform-audit3description: Audit Terraform, OpenTofu, Terragrunt, and Pulumi modules for state, provider pinning, security (Checkov/tfsec), module hygiene, environment separation, drift, and cost. One sub-agent per module. Static, live, and apply modes.4---5
6# IaC Terraform Audit
7
8ultrathink
9
10<!-- anthril-output-directive -->
11> **Output path directive (canonical — overrides in-body references).**
12> All file outputs from this skill MUST be written under `.anthril/audits/iac-terraform-audit/`.
13> Run `mkdir -p .anthril/audits/iac-terraform-audit` before the first `Write` call.
14> Primary artefact: `.anthril/audits/iac-terraform-audit/<artefact>`.
15> Do NOT write to the project root or to bare filenames at cwd.
16> Lifestyle plugins are exempt from this convention — this skill is not lifestyle.
17
18## When to use
19
20Run this skill when the user mentions:
21- Terraform review, IaC audit, infrastructure security
22- Checkov, tfsec, OpenTofu review, Pulumi audit
23- Pre-migration infra cleanup
24- State file concerns, provider pinning, module design
25
26Covers eight categories: state and backend configuration (remote state, encryption, locking, workspace separation), provider pinning (`required_providers` with version constraints, `required_version`), security (Checkov/tfsec taxonomy — public S3 ACLs, unencrypted RDS, open security groups, wildcard IAM, plaintext secrets in tfvars), module hygiene (variable validation, descriptions, types, outputs, sensitive flag), environment separation, drift risk, cost hotspots, and CI testing coverage.
27
28## Before You Start
29
301. **Determine operating mode.** `--live` runs `terraform plan` against each module (refresh-only, no apply). `--apply` writes HCL patches. `--runtime` is not applicable (Terraform has no safe runtime test).
312. **Enumerate modules.** Run `scripts/list-modules.sh` — groups `.tf` files by directory.
323. **Sub-agent budget.** One agent per module. Warn above 10.
334. **Load `.iac-ignore`** for suppressions (pattern: `<module_path>:<finding_id>` or `<module_path>:*`).
345. **Live-mode requirements.** `terraform init` must succeed per module — warn the user if state backends require credentials and fall back to static audit for modules that can't initialise.
35
36## User Context
37
38$ARGUMENTS
39
40Module inventory: !`bash "${CLAUDE_PLUGIN_ROOT}/skills/iac-terraform-audit/scripts/list-modules.sh"`
41
42Tools: !`which terraform tofu 2>/dev/null | head -1 || echo "terraform:unavailable"` · !`which checkov tfsec 2>/dev/null | head -1 || echo "checkov:unavailable"`
43
44---
45
46## Audit Phases
47
48### Phase 1: Discovery & Mode Selection
49
501. Parse module inventory. Identify Terraform / OpenTofu / Terragrunt / Pulumi.
512. Confirm scope; warn if >10 modules.
523. Verify live-mode tools. Fall back per-module if `terraform init` fails.
53
54### Phase 2: Per-Module Snapshot
55
56For each module, parse HCL (via `hcl2json` if available, else structural grep):
57
58- Providers (`required_providers`, version constraints, source)
59- Required Terraform version (`required_version`)
60- Backend config (type, encryption, locking)
61- Resources (by provider+type, name)
62- Variables (with type, default, description, sensitive, validation)
63- Outputs (with sensitive, description)
64- Locals
65- Data sources
66- Module calls (local / registry / Git, with version)
67- Lifecycle blocks
68
69In `--live` mode, additionally run `terraform plan -refresh-only` and capture the summary (additions / changes / destructions predicted).
70
71### Phase 3: Cross-Module Topology
72
731. Build a module-dependency graph (who calls whom via `module.<name>` blocks, who reads whose state via `terraform_remote_state`).
742. Detect shared backends (multiple modules writing to the same state key — usually a bug).
753. Emit Mermaid graph for the report.
76
77### Phase 4: Parallel Sub-Agent Audit
78
79Spawn one `Agent(subagent_type=Explore)` per module in a single assistant message. Each walks categories A–H in `reference.md` §1:
80
81- **A. State & Backend** — remote backend, encryption at rest, state locking (DynamoDB / GCS / etc.), workspace separation, state file not in git
82- **B. Provider pinning** — `required_providers` with `~>` or exact, `required_version` set, source pinned to a hash registry URL
83- **C. Security (Checkov/tfsec)** — open SGs, public S3 ACLs, unencrypted RDS, IAM `*`, KMS missing, plaintext secrets, EBS/EFS encryption
84- **D. Module hygiene** — variables with `type` and `description`, `validation` blocks for bounded inputs, `sensitive = true` on secrets, outputs documented
85- **E. Environment separation** — workspace or directory per env, no cross-env refs, prod state isolated
86- **F. Drift risk** — `lifecycle.ignore_changes` abuse (should be targeted), `null_resource` for imperative work, `create_before_destroy` on resources that require it
87- **G. Cost hotspots** — oversized default instance types, unbounded autoscaling max, NAT Gateways when cheaper egress suffices, attached-but-unused EIPs
88- **H. Testing & CI** — `terraform fmt` / `validate` / `tflint` integration in CI, `terraform-docs` generation, pre-commit hooks
89
90Sub-agents MUST NOT run `terraform apply` / `terraform destroy` / state-mutating commands. Read-only plans only.
91
92### Phase 5: Cross-Module Analysis
93
94- Duplicate resource patterns across modules → module extraction candidates
95- Cyclic dependencies via `terraform_remote_state`
96- Provider version drift (module A uses AWS ~>5, module B uses ~>4)
97- Missing `required_version` consistency
98
99### Phase 6: Merge & Risk Register
100
101Consolidate findings. Dedupe via `.iac-ignore`. Apply severity adjustments:
102- Findings on production modules (determined via workspace name or directory convention) keep severity
103- Findings on non-prod modules downgrade one tier
104- Checkov/tfsec findings (if live tools available) cross-validated with sub-agent output
105
106Assign `IAC-001…` IDs.
107
108### Phase 7: Remediation Drafting
109
110Emit commented HCL blocks to `iac-suggested.tf`. Rules:
111
112- **Every FK-like resource relationship** uses `depends_on = []` explicit when needed
113- **Every state change to sensitive resources** is commented with `# MANUAL REVIEW — STATE RISK`
114- **Provider pinning suggestions** include both the current pin and the recommended
115- **Module extraction suggestions** emit a minimal new module skeleton in a sub-block of the suggestion file
116
117### Phase 8: Apply Mode (opt-in)
118
119When `--apply`, iterate findings with `[a]pply / [s]kip / [A]ll / [q]uit`. Any HCL change that would affect state (adding `lifecycle { prevent_destroy = true }`, changing a backend key) requires `DESTROY` confirmation.
120
121### Phase 9: Reporting
122
123Write `iac-terraform-audit.md` + `iac-terraform-audit.json` + `iac-suggested.tf` (+ `terraform-plan.txt` in live mode).
124
125---
126
127## Scoring
128
129Weights: A=20, B=10, C=30, D=10, E=10, F=10, G=5, H=5 (sum 100). See `reference.md` §3.
130
131| Total | Verdict |
132|---|---|
133| 90+ | PASS |
134| 70–89 | PASS WITH WARNINGS |
135| 50–69 | CONDITIONAL |
136| <50 | FAIL |
137
138---
139
140## Important Principles
141
142- **State is the crown jewel.** Remote, encrypted, locked, and never in git.
143- **`~>` is a minor-version pin.** `~>5.0` allows 5.x; `~>5.0.0` allows 5.0.x. Know which you want.
144- **Security findings that are true on paper may be false in context.** A public S3 ACL on a static-site bucket is intentional. Flag but let the user suppress.
145- **`lifecycle.ignore_changes = all` is almost always wrong.** Target specific attributes.
146- **`null_resource` + `local-exec` is imperative.** Flag every occurrence.
147- **Cost findings are suggestions, never failures.** Use INFO severity.
148- **Australian English. DD/MM/YYYY. Markdown-first.**
149
150---
151
152## Edge Cases
153
1541. **Terragrunt.** Parse `terragrunt.hcl` blocks (`include`, `dependency`, `generate`). Findings on generated files go to the `terragrunt.hcl` that generated them.
1552. **Pulumi.** Parse `index.ts` / `__main__.py`. Map to the same A–H taxonomy.
1563. **CDK (TypeScript/Python).** Skip — not in scope. Emit one finding: "CDK module detected; not covered by this skill."
1574. **Remote module (`source = "git::..."` or `source = "terraform-aws-modules/..."`).** Audit the caller's reference and the version; do not recurse into the remote.
1585. **`terraform init` fails in live mode.** Record as a module-level limitation; continue static audit.
1596. **State encrypted with SSE-KMS but key is unmanaged.** Flag MEDIUM — "encrypted but key lifecycle unclear".
1607. **Mono-repo with many modules.** Cluster by top-level directory; one sub-agent per cluster if counts are high.
1618. **A module has only data sources, no resources.** Audit the providers and backend; skip C/F.