Commands
ctfkit by compliance.tf has two analysis commands. scan checks a configuration or a plan. report reads the module cache after init. Both write the same finding format and follow the same exit code contract.
It is for platform engineers wiring ctfkit into CI who need every flag, file name and exit code.
Get access
ctfkit is available to compliance.tf customers on request: see Get access.
ctfkit scan(available):pre_planover.tffiles,post_planover plan JSON.ctfkit report(preview):post_initover.terraform/modules.- Exit
0nothing at or above--fail-on,1findings at or above it,2environment error. - Settings come from
--config, else.ctfkit.yamlin the discovery root, else built-in defaults. - ctfkit never runs
terraformortofuand never opens a network connection.
Stages
A stage is the point in the Terraform or OpenTofu workflow where an analyzer runs. The diagram shows where each stage runs. The table shows what each stage can and cannot see, which depends on what Terraform has produced by then.
| Stage | Command | Sees | Cannot see |
|---|---|---|---|
pre_plan | scan | .tf and .tofu files as written: module sources, version pins, provider blocks and literal arguments. Needs no init and no credentials | anything evaluated. HCL is parsed and not evaluated, so var.*, local.*, functions, count, for_each and module outputs do not resolve |
post_init | report | the module cache in .terraform/modules after init, including the manifest each compliance.tf module carries | evaluated values or provider data. scan --stage post_init exits 2 and points to report |
post_plan | scan | the plan JSON, with variables, count, for_each and nested modules resolved. Needs init, credentials and a successful plan | values unknown until apply; a rule that depends on one stays silent |
pre_apply and post_apply are reserved names with no analyzers; scan --stage post_apply exits 2.
scan
ctfkit scan [flags] [DIR | PLAN_FILE | FILE.tf ...]Positional arguments: a directory is the configuration directory; .tf, .tf.json, .tofu and .tofu.json files are an explicit pre_plan file list (what the pre-commit hook passes); any other .json file is the plan.
ctfkit scan plan.json # post_plan
ctfkit scan --plan plan.json --chdir ./infra # the same, findings anchored to ./infra
ctfkit scan --stage pre_plan ./terraform/ # pre_plan over a directory
ctfkit scan --stage pre_plan main.tf vpc.tf # pre_plan over a file list
ctfkit scan plan.json --format sarif --out findings.sarif
ctfkit scan plan.json --format sarif,json,junit --out-dir out/
ctfkit scan plan.json --format spacelift # writes ctfkit-scan.custom.spacelift.json| Flag | Meaning |
|---|---|
--stage | auto (default), pre_plan or post_plan. auto picks post_plan when a plan is given, pre_plan otherwise |
--plan | plan JSON from terraform show -json or tofu show -json; a positional .json file means the same |
--chdir | Terraform configuration directory (default .); a directory positional means the same |
--format, -f | comma-separated list of sarif, json, text, pretty (default), junit, spacelift |
--out | output file for exactly one format; - is stdout |
--out-dir | directory for several formats |
--fail-on | error (default), warning, note or never |
--config | settings file; see Settings file |
--exceptions | exception registry (default: .ctf-exceptions.yaml in the configuration directory) |
--catalog | control catalog YAML, or the literal embedded to pin the catalog compiled into the binary |
--remediation-host | framework id whose registry host goes into the suggested module source, for example pci_dss_v40. Default: soc_2, which gives soc2.compliance.tf |
--run-id, --commit, --platform | run metadata; detected from the CI environment when not given |
--watermark | optional banner line added to every output |
--no-color, --verbose | terminal output only |
Every single-value flag refuses a second use and an empty value, with exit 2. An adapter that pins --config or --fail-on therefore cannot be overridden by a later flag on the same command line.
ctfkit validate is a hidden alias of scan --stage pre_plan, kept for one minor release for existing pre-commit configurations.
report
Preview. report reads .terraform/modules after init and reports which controls the compliance.tf modules in the configuration enforce, what the configuration declares itself, and what the report does not prove.
terraform init
ctfkit report # to stdout
ctfkit report --format receipt,sarif,markdown --out-dir out/
ctfkit report --format --readme README.md # inject into README.md| Flag | Meaning |
|---|---|
--path | module root or example directory, repeatable (default .) |
--format | comma-separated list of markdown (default), receipt, sarif, spacelift |
--out | output file for a single format; - is stdout |
--out-dir | directory for several formats |
--readme | inject the section between the markers in this file |
--markers | marker pair START,END (default <!-- CTFKIT_REPORT_START --> and <!-- CTFKIT_REPORT_END -->) |
--tests-json | output of tofu test -json or terraform test -json for this root, written by an earlier step |
--fail-on | error (default), warning, note or never |
--config, --exceptions, --catalog | as for scan, except that --exceptions defaults to .ctf-exceptions.yaml in the working directory, when present |
--run-id, --commit, --platform, --watermark | as for scan |
--readme and --markers need markdown in --format; without it the run exits 2. The receipt records which controls the compliance.tf modules enforce at module build time, as read from the manifests written at init. It describes the configuration and says nothing about live resources. It is unsigned; signing is planned.
Other commands
| Command | Does |
|---|---|
ctfkit version | prints the release, the Go version and the platform |
ctfkit config init | writes a starter .ctfkit.yaml in the working directory |
ctfkit config validate [path] | checks a settings file |
Output formats
| Format | --out-dir file name | Use |
|---|---|---|
sarif | findings.sarif | GitHub code scanning and any SARIF 2.1.0 reader |
json | findings.json | OPA, Conftest and your own scripts |
text | findings.txt | logs and job summaries |
pretty | findings.pretty.txt | a terminal (the default) |
junit | findings.junit.xml | CI test report views |
spacelift | always in the working directory: ctfkit-scan.custom.spacelift.json from scan, ctfkit.custom.spacelift.json from report | the Spacelift plan policy |
report writes report.md, receipt.json and findings.sarif into --out-dir.
One format goes to stdout or to --out. Several need --out-dir. The SARIF output is a profile of SARIF 2.1.0: a normal SARIF log plus a ctf.* property bag on the run and on each result, carrying control ids, frameworks and the compliance.tf module that closes the gap. Any SARIF reader loads it as ordinary SARIF. The profile is a draft and its field names can still change.
The JSON output carries catalogVersion and findings, where each finding is a SARIF result: ruleId, level, message.text, locations and, when an exception covers it, suppressions.
Exit codes and --fail-on
| Code | Meaning |
|---|---|
0 | nothing at or above --fail-on |
1 | unsuppressed findings at or above --fail-on |
2 | environment error: unreadable or invalid plan, configuration, catalog, settings or exceptions file, or an invalid flag |
--fail-on takes error (default), warning, note or never. The flag wins over the CTFKIT_FAIL_ON environment variable, which wins over fail_on in the settings file. never keeps the exit code at 0 on findings, which suits a step whose output another tool judges, such as OPA.
A scan whose format list is exactly spacelift returns 0 on findings, because the Spacelift plan policy reads the verdict in the file. It still returns 2 on an environment error.
Hosts and registry tokens
compliance.tf serves modules from two kinds of host. Your organization's host, such as bobthecorp.compliance.tf, serves the builds your organization configured. Framework hosts, such as soc2.compliance.tf or hipaa.compliance.tf, serve the build for one framework, which also applies your organization's Baseline rules when they are enforced; Module request resolution has the details.
--remediation-host picks the framework host that suggested fixes point to. It takes a framework id and defaults to soc_2, so fixes name soc2.compliance.tf; pci_dss_v40 gives pcidss.compliance.tf. Fixes always name a framework host: --remediation-host cannot point them at your organization's host, so replace the host yourself if your modules come from there. An unknown id exits 2 and lists the valid ones. The embedded catalog in v0.2.1 accepts acsc_essential_eight, acsc_ism_2023, aws_control_tower, aws_genai_v2, aws_well_architected_v10, cccs_medium, cfr_part_11, cis_v120, cis_v130, cis_v140, cis_v500, cis_v600, cis_v71_ig1, cis_v80_ig1, cisa_cyber_essentials, eu_gmp_annex_11, fedramp_low_rev_4, fedramp_moderate_rev_4, ffiec, gdpr, hipaa_final_omnibus_2013, hipaa_security_2003, iso_27001_2013, iso_27001_2022, nis2, nist_800_171_rev_2, nist_800_53_rev_4, nist_800_53_rev_5, nist_csf_v11, nist_csf_v2, nydfs_23, pci_dss_v321, pci_dss_v40, rbi_cyber_security, rbi_itf_nbfc, soc_2.
init downloads these modules with your compliance.tf registry token, which you create on the Registry tokens page of the compliance.tf platform. Terraform reads it from TF_TOKEN_<host>, with the dots in the host name replaced by underscores, so set one variable for every compliance.tf host in your module sources: TF_TOKEN_bobthecorp_compliance_tf for bobthecorp.compliance.tf, TF_TOKEN_soc2_compliance_tf for soc2.compliance.tf. An organization token carries your organization's plan on every host, so the same token value can go in each variable. ctfkit never reads the token.
Settings file
ctfkit looks for settings in this order:
--config FILE, if given..ctfkit.yamlin the discovery root. Forscanthat is the configuration directory:--chdir, or the directory argument, else the working directory. Forreportit is the working directory.- Built-in defaults.
When scan falls back to the working directory, it names that directory in one stderr line after the run. The settings file is part of the scanned repository, so anyone who can open a pull request can edit it. A run that has to hold regardless of repository content passes --config, --exceptions, --catalog and --fail-on from outside the repository. The GitHub Action and the Spacelift hooks do this.
# Hosts that count as compliance.tf. Omit the key and any host ending in .compliance.tf
# counts, including your organization's host. A list counts only the hosts it names.
# An empty list (allowed_endpoints: []) is a configuration error, exit 2.
allowed_endpoints:
- "bobthecorp.compliance.tf"
- "soc2.compliance.tf"
# Optional allowlists. Empty means everything is allowed.
allowed_resources: []
allowed_providers:
- registry.terraform.io/hashicorp/aws
# Tag policy for planned resources.
tags:
- key: Owner
required: true
resource_types: [aws_s3_bucket] # empty means every taggable resource
- key: CostCentre
pattern: "^CC-[0-9]{4}$" # RE2; allowed_values is the alternative
required_tags: [Environment] # shorthand for required tags entries
# Exit-1 threshold when neither --fail-on nor CTFKIT_FAIL_ON is set.
fail_on: errorctfkit config init writes a starter file; ctfkit config validate .ctfkit.yaml checks one.
Exceptions
An exception waives a finding for one resource, with an approver and a review date. The registry is a YAML file: --exceptions FILE, or else .ctf-exceptions.yaml in the same discovery root as the settings file. For scan that is the configuration directory, and for report the working directory. No file means no exceptions, which is not an error.
schema_version: "1.0"
exceptions:
- control: s3_bucket_object_lock_enabled
resource: aws_s3_bucket.logs
justification: "Scratch bucket for ETL output. Objects are deleted within 24 hours, which object lock retention would block."
approved_by: compliance@bobthecorp.example
approval_ticket: SEC-4711
review_date: "2027-06-01"| Key | Rule |
|---|---|
control or rule | exactly one per entry: a control id, or a rule ID such as ctf.policy.floating_ref. Findings with no control id, such as the provider and pinning checks, need a rule entry |
resource | required; the address of the resource the exception covers |
approved_by, approvals | at least one approver across the two; approvals is a list |
review_date | required, YYYY-MM-DD in UTC. The exception holds through the end of that day |
id | entry id; falls back to approval_ticket |
justification | free text, kept with the entry |
schema_version | optional; when present its major version must be 1 |
An entry without an approver or without a review date never suppresses anything. An expired entry stops suppressing the day after its review date, and the finding comes back. Two entries with the same id and resource, or an entry that names both control and rule, are an error, exit 2. Keys ctfkit does not read, such as organization or compensating_controls, are allowed and kept for your own records.
A suppressed finding still appears in the output with its suppressions entry and counts as suppressed in the summary line; it no longer affects the exit code or the Spacelift verdict.