Quickstart
This page takes ctfkit by compliance.tf from install to two scans and one waived finding: a scan over a configuration before init, a scan over a plan JSON, and an exception that suppresses one finding. The first scan needs only the files. The second needs a root you can already init and plan, with your registry token and cloud credentials.
It is for platform engineers trying ctfkit on one root before wiring it into CI.
Step 1: Install ctfkit
On macOS or Linux with Homebrew, install the newest release:
brew install compliancetf/tap/ctfkit
ctfkit versionbrew upgrade ctfkit moves to the next release. The compliancetf/homebrew-tap formula pins each archive only after checking its build provenance attestation.
Without Homebrew, download a release archive. Releases are public on GitHub Releases and download without an account or a token. The commands below install the newest release and check the archive against checksums.txt before you unpack it. To pin a release, as you should in CI and in images you build, replace latest/download with download/v<version>, such as download/v1.2.3. On Linux x86_64:
base=https://github.com/compliancetf/ctfkit/releases/latest/download
curl -fsSLO "$base/checksums.txt"
tarball="$(awk '$2 ~ /_Linux_x86_64\.tar\.gz$/ {print $2}' checksums.txt)"
curl -fsSLO "$base/$tarball"
awk -v a="$tarball" '$2==a' checksums.txt | sha256sum -c -
tar -xzf "$tarball" ctfkit
sudo install -m 0755 ctfkit /usr/local/bin/ctfkit
ctfkit versionOn Windows, in PowerShell (on Windows on Arm, use arm64 in place of x86_64):
$ErrorActionPreference = "Stop"
$base = "https://github.com/compliancetf/ctfkit/releases/latest/download"
Invoke-WebRequest "$base/checksums.txt" -OutFile checksums.txt -UseBasicParsing
$line = Select-String -Path checksums.txt -Pattern "ctfkit_.*_Windows_x86_64\.zip$"
$hash, $zip = $line.Line.Trim() -split "\s+"
Invoke-WebRequest "$base/$zip" -OutFile $zip -UseBasicParsing
if ((Get-FileHash $zip -Algorithm SHA256).Hash -ne $hash) { throw "checksum mismatch: $zip" }
Expand-Archive $zip -DestinationPath "$env:LOCALAPPDATA\ctfkit" -Force
& "$env:LOCALAPPDATA\ctfkit\ctfkit.exe" versionAdd $env:LOCALAPPDATA\ctfkit to your PATH to run ctfkit from any directory.
ctfkit version prints the release, the commit and build date of the binary, the Go version it was built with and your platform:
ctfkit version <version>
commit: <commit>
built: <build date>
go: <Go version>
platform: darwin/arm64For other platforms, the .deb, .rpm and .apk packages, the pre-commit hook and checking which workflow built a release with gh attestation verify, follow Install in the ctfkit repository.
Step 2: Scan a configuration before plan
A pre_plan scan reads .tf and .tofu files as written. It needs no init, no credentials and no network:
ctfkit scan --stage pre_plan ./infraIt checks module sources, pinning, disabled controls and providers, and it reports provisioners and external data sources, code that would run on the machine that plans or applies the configuration. A clean run prints:
All checks passed: no findings.
stage pre_plan, catalog 2026.11.1 (embedded)A run with findings lists each one, then a summary line. Here the configuration uses the upstream terraform-aws-modules/s3-bucket/aws module with version = "~> 3.0", a third-party registry module and a Git module source with no ref:
ERROR ctf.coverage.module_source
module.s3_bucket (non_ctf_module.tf:1)
Upstream terraform-aws-modules/s3-bucket/aws is a migration candidate; the same path is served under this organisation's framework host
fix: use soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws ~> 3.0
NOTE ctf.coverage.module_source
module.vpc (non_ctf_module.tf:9)
Third-party module registry.terraform.io/hashicorp/aws/vpc has no compliance.tf equivalent; it is listed for inventory only
WARNING ctf.policy.floating_ref
module.vpc_from_git (non_ctf_module.tf:20)
Module source git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git has no ref, so the code that runs can change without a configuration change
3 finding(s): 1 error, 1 warning, 1 note, 0 suppressed
stage pre_plan, catalog 2026.11.1 (embedded)
ctfkit: 1 finding(s) at or above errorThe error names the same module path on soc2.compliance.tf, the framework host that serves the SOC 2 build. The fix always names a framework host, picked with --remediation-host; Hosts and registry tokens explains framework hosts and your organization's host. In a pre_plan scan the fix keeps the version constraint your configuration already has, here ~> 3.0. Compliance.tf serves s3-bucket from 5.0.0 (Version compatibility), so applying this fix as written fails terraform init: move the constraint to ~> 5.0 and handle the upstream 3.x-to-5.x upgrade in the same change.
Step 3: Scan a plan
A post_plan scan reads the plan as JSON. scan never runs terraform or tofu, so produce the JSON first. init needs your compliance.tf registry token when the root uses compliance.tf modules, and plan needs the cloud credentials the root normally uses:
cd infra
terraform init # or: tofu init
terraform plan -out=tfplan # or: tofu plan -out=tfplan
terraform show -json tfplan > plan.json
ctfkit scan plan.json--stage auto, the default, picks post_plan because a plan was given. At this stage Terraform has already resolved variables, count, for_each and nested modules, so ctfkit also checks raw resources that a compliance.tf module covers, required tags and sensitive changes. Here a raw aws_s3_bucket has no module version to keep, so the fix gives the version the catalog recommends, ~> 5.0:
ERROR ctf.coverage.module_source
aws_s3_bucket.logs (main.tf:1)
Raw aws_s3_bucket where the compliance.tf module soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws exists
fix: use soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws ~> 5.0
1 finding(s): 1 error, 0 warning, 0 note, 0 suppressed
stage post_plan, catalog 2026.11.1 (embedded)
ctfkit: 1 finding(s) at or above errorWhen the plan file sits somewhere other than the configuration, name the configuration with --chdir so file and line anchors resolve: ctfkit scan --plan out/plan.json --chdir ./infra.
Step 4: Read the output
Each finding has up to four parts. The fix: line appears only when a compliance.tf module closes the finding, so the NOTE and the WARNING above have none.
| Line | Meaning |
|---|---|
ERROR, WARNING or NOTE, then the rule ID | the level and the reference analyzer rule that fired |
address and file:line | the resource or module, and where it is declared |
| message | what was found |
fix: | the compliance.tf module source and version that close it, when one applies |
The exit code tells your pipeline what happened:
| Exit | Meaning |
|---|---|
0 | nothing at or above --fail-on (default error) |
1 | unsuppressed findings at or above --fail-on |
2 | could not run, or refused: an unreadable or invalid input, settings, catalog or exceptions file, or an invalid flag |
To hand the findings to another tool, pick a format:
ctfkit scan plan.json --format sarif --out findings.sarif # code scanning
ctfkit scan plan.json --format json --fail-on never > findings.json # OPA or Conftest
ctfkit scan plan.json --format sarif,json,junit --out-dir out/ # several at onceStep 5: Waive one finding
An exception waives a finding for one resource, with an approver and a review date. Save this as .ctf-exceptions.yaml in infra, the directory you scanned in Step 3, to waive the raw bucket finding there:
schema_version: "1.0"
exceptions:
- rule: ctf.coverage.module_source
resource: aws_s3_bucket.logs
justification: "Why this bucket stays a raw resource for now."
approved_by: platform@bobthecorp.example
review_date: "2027-06-01"A rule entry reaches only findings with no control id. If the finding lists control ids, which the json and sarif outputs show in ctf.controlIds, replace rule: with one control: entry for each of them. A control: entry matches only control ids the loaded catalog holds, which for the embedded catalog means the CIS AWS Benchmark v6.0 controls.
Run the same scan again. The finding still appears, marked SUPPRESSED with the exception's justification, and the summary line counts it as suppressed. A suppressed finding does not affect the exit code, so this run exits 0:
SUPPRESSED ctf.coverage.module_source
aws_s3_bucket.logs (main.tf:1)
Raw aws_s3_bucket where the compliance.tf module soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws exists
fix: use soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws ~> 5.0
exception: allowed=true Why this bucket stays a raw resource for now.
1 finding(s): 0 error, 0 warning, 0 note, 1 suppressed
stage post_plan, catalog 2026.11.1 (embedded)The json and sarif outputs carry the same finding with its suppressions entry.
An entry without an approver or without a review_date suppresses nothing, and an entry stops suppressing the day after its review date. Every key and the errors that exit 2 are in Exceptions.
Next steps
- GitHub Actions - run the same scans on every pull request.
- Spacelift - run them as stack hooks with a plan policy.
- Commands - every flag, the settings file and exceptions.
- Control catalog - load the full control catalog with
ctfkit catalog pullwhen the CIS AWS Benchmark v6.0 controls in the embedded catalog are not enough. - Reference analyzers - what each rule checks and where it stops.