compliance.tf

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_plan over .tf files, post_plan over plan JSON.
  • ctfkit report (preview): post_init over .terraform/modules.
  • Exit 0 nothing at or above --fail-on, 1 findings at or above it, 2 environment error.
  • Settings come from --config, else .ctfkit.yaml in the discovery root, else built-in defaults.
  • ctfkit never runs terraform or tofu and 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.

Where ctfkit by compliance.tf runs in a Terraform workflow Timeline of a Terraform or OpenTofu run. Before init, the pre_plan stage runs ctfkit scan over the .tf files in a pre-commit hook or the GitHub Action. After init, post_init runs ctfkit report over the module cache in the GitHub Action or a Spacelift after_init hook. After plan and show -json, post_plan runs ctfkit scan --plan plan.json in the GitHub Action or a Spacelift after_plan hook. pre_apply and post_apply are reserved and run no analyzers. Edit .tf files initterraform or tofu plan + show -jsonwrites plan.json apply pre_planctfkit scan <dir>SEES.tf files as written: no init, no credentialsRUNS INpre-commit hook · GitHub ActionSTART HERE post_initctfkit reportSEESmodule cache that init downloadedRUNS INGitHub Action · Spacelift after_init hook post_planctfkit scan --plan plan.jsonSEESresolved plan values: tags, exposure, changesRUNS INGitHub Action · Spacelift after_plan hook pre_applyreserved: the same plan JSON, right before apply post_applyreserved: would read state LEGENDTerraform or OpenTofu stepctfkit stageStart hereReserved, no analyzers
StageCommandSeesCannot see
pre_planscan.tf and .tofu files as written: module sources, version pins, provider blocks and literal arguments. Needs no init and no credentialsanything evaluated. HCL is parsed and not evaluated, so var.*, local.*, functions, count, for_each and module outputs do not resolve
post_initreportthe module cache in .terraform/modules after init, including the manifest each compliance.tf module carriesevaluated values or provider data. scan --stage post_init exits 2 and points to report
post_planscanthe plan JSON, with variables, count, for_each and nested modules resolved. Needs init, credentials and a successful planvalues 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
FlagMeaning
--stageauto (default), pre_plan or post_plan. auto picks post_plan when a plan is given, pre_plan otherwise
--planplan JSON from terraform show -json or tofu show -json; a positional .json file means the same
--chdirTerraform configuration directory (default .); a directory positional means the same
--format, -fcomma-separated list of sarif, json, text, pretty (default), junit, spacelift
--outoutput file for exactly one format; - is stdout
--out-dirdirectory for several formats
--fail-onerror (default), warning, note or never
--configsettings file; see Settings file
--exceptionsexception registry (default: .ctf-exceptions.yaml in the configuration directory)
--catalogcontrol catalog YAML, or the literal embedded to pin the catalog compiled into the binary
--remediation-hostframework 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, --platformrun metadata; detected from the CI environment when not given
--watermarkoptional banner line added to every output
--no-color, --verboseterminal 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
FlagMeaning
--pathmodule root or example directory, repeatable (default .)
--formatcomma-separated list of markdown (default), receipt, sarif, spacelift
--outoutput file for a single format; - is stdout
--out-dirdirectory for several formats
--readmeinject the section between the markers in this file
--markersmarker pair START,END (default <!-- CTFKIT_REPORT_START --> and <!-- CTFKIT_REPORT_END -->)
--tests-jsonoutput of tofu test -json or terraform test -json for this root, written by an earlier step
--fail-onerror (default), warning, note or never
--config, --exceptions, --catalogas for scan, except that --exceptions defaults to .ctf-exceptions.yaml in the working directory, when present
--run-id, --commit, --platform, --watermarkas 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

CommandDoes
ctfkit versionprints the release, the Go version and the platform
ctfkit config initwrites a starter .ctfkit.yaml in the working directory
ctfkit config validate [path]checks a settings file

Output formats

Format--out-dir file nameUse
sariffindings.sarifGitHub code scanning and any SARIF 2.1.0 reader
jsonfindings.jsonOPA, Conftest and your own scripts
textfindings.txtlogs and job summaries
prettyfindings.pretty.txta terminal (the default)
junitfindings.junit.xmlCI test report views
spaceliftalways in the working directory: ctfkit-scan.custom.spacelift.json from scan, ctfkit.custom.spacelift.json from reportthe 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

CodeMeaning
0nothing at or above --fail-on
1unsuppressed findings at or above --fail-on
2environment 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:

  1. --config FILE, if given.
  2. .ctfkit.yaml in the discovery root. For scan that is the configuration directory: --chdir, or the directory argument, else the working directory. For report it is the working directory.
  3. 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: error

ctfkit 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"
KeyRule
control or ruleexactly 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
resourcerequired; the address of the resource the exception covers
approved_by, approvalsat least one approver across the two; approvals is a list
review_daterequired, YYYY-MM-DD in UTC. The exception holds through the end of that day
identry id; falls back to approval_ticket
justificationfree text, kept with the entry
schema_versionoptional; 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.

On this page

Ask AI about this

Help improve this page