Sign in
Menu
Sign up
Docs
Check before applyctfkit

GitHub Actions

This page is for platform engineers who run Terraform or OpenTofu in GitHub Actions. By the end, every pull request gets ctfkit by compliance.tf findings in a sticky comment, the job summary and the Security tab, from the public composite action compliancetf/ctfkit-action. ctfkit is open source under Apache-2.0, and the action downloads it from the public releases of compliancetf/ctfkit with the job token, so running ctfkit needs no secret.

The action installs one pinned ctfkit release, checks it against the release checksums, and runs one ctfkit command over what your workflow already produced. It does not run init, plan or test; your workflow owns those steps. The two commands it runs, scan and report, open no network connection and read no cloud credentials. Only catalog: pull makes the action download the full control catalog first; see Loading the full catalog. The steps your workflow runs before it need their usual credentials.

Step 1: Pick a command

commandReadsYour workflow runs firstCredentials needed
scan with stage: pre_planconfiguration in chdircheckoutnone
report (default)the module cache, via pathinityour compliance.tf registry token, for init
scanplan JSON, via planinit, plan, show -jsonregistry token and cloud credentials, for plan

command defaults to report, which reads the module cache and so needs init to have run. Start with command: scan and stage: pre_plan when your workflow does not run init yet: it needs only the checkout. Use report where init already runs in CI, and a plan scan where plan does.

- uses: compliancetf/ctfkit-action@v0
  with:
    command: scan
    stage: pre_plan
    chdir: infra

Examples on this page use compliancetf/ctfkit-action@v0, which follows the newest v0 release of the action, and leave out version, so the action installs the newest ctfkit release. For a reproducible pipeline, pin both: the action to a release commit SHA, which Dependabot can bump, and version to the ctfkit release you reviewed, such as version: v1.2.3. The action's plan, sign and apply commands for a governed apply workflow are not available: they need a signed organization profile and floor, which compliance.tf does not issue.

The action checks the archive against the release checksums.txt. That file comes from the same release, so it shows the download is intact; it says nothing about who published it. To pin the archive itself, set version to the release you reviewed and sha256 to the digest of its archive, from the ctfkit_<version>_Linux_x86_64.tar.gz line of that release's checksums.txt. verify-attestation: 'true' also checks the GitHub build provenance attestation of the release; every ctfkit release carries one.

Step 2: Choose where results go

DestinationInputNeeds
Security tabsarif: 'true' (default)code scanning on the repository and security-events: write
Pull request commentpr-comment: 'true'pull-requests: write. One comment per command and directory, updated in place on every run
Job summaryalwaysnothing

sarif defaults to 'true'. On a repository without code scanning the upload fails and the job fails with it, even when ctfkit found nothing. Set sarif: 'false' there.

The job summary and the comment carry the same text. For report it is the report. For scan, which has no format, it is ctfkit's text output, HTML-escaped inside a <pre> block so that nothing from the checkout can inject markdown. The markdown output is the path to that file.

The action keys each comment by the command and the directory: chdir for scan, path for report, and . when neither is set. Two steps with the same command and directory update the same comment.

The comment uses comment-token, which defaults to the job token and is separate from github-token. A comment that cannot be written is a warning and does not fail the step.

Step 3: Set permissions

PermissionNeeded for
contents: readcheckout, and the release download with the job token
pull-requests: writepr-comment: 'true'
security-events: writesarif: 'true'

Step 4: Require the check to block a merge

The action fails its step when ctfkit exits 1 or 2, so the job shows a failing check. That check blocks a merge only when branch protection or a ruleset on the target branch requires it, so what blocks depends on the checks you require and the fail-on you set.

Complete workflow

ctfkit by compliance.tf in GitHub Actions GitHub Actions flow. A pull request starts two jobs. Job test checks out, runs tofu init and tofu test, then the ctfkit action runs report at post_init over the module cache and test results. Job ctfkit-scan checks out and runs the ctfkit action scan at pre_plan with no tofu and no registry token. The action downloads the ctfkit release from the public compliancetf/ctfkit releases with the job token. Results go to a sticky pull request comment, the job summary and SARIF in the Security tab when code scanning is available; the check passes, or fails on findings at or above fail-on. Job test Job ctfkit-scan PR opened Checkout, init, testtofu init · tofu test Checkoutno tofu, no registry token ctfkit reportpost_init · modules, tests ctfkit scanstage: pre_plan ctfkit releasepublic, job token Resultssticky PR commentjob summarySARIF, Security tabwith code scanning Checkfails on findingsat or above fail-on LegendTrigger or your workflow stepctfkit action stepPublic release

This is the workflow in the diagram. Job ctfkit-scan runs a pre_plan scan over each root with no cloud or registry credentials. Job test runs init and tofu test over each root, then reports module coverage with the test results. Both comment on the pull request. It is written for a private repository without code scanning; the notes after it show the change for code scanning.

name: ctfkit

on:
  pull_request:

permissions:
  contents: read
  pull-requests: write

jobs:
  ctfkit-scan:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        root: [infra/network, infra/storage]
    steps:
      - uses: actions/checkout@v4

      - uses: compliancetf/ctfkit-action@v0
        with:
          command: scan
          stage: pre_plan
          chdir: ${{ matrix.root }}
          sarif: 'false'
          pr-comment: 'true'

  test:
    # Pull requests from forks get no secrets, so init could not read the registry.
    if: github.event.pull_request.head.repo.full_name == github.repository
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        root: [infra/network, infra/storage]
    steps:
      - uses: actions/checkout@v4

      - uses: opentofu/setup-opentofu@v2

      - name: Initialize
        working-directory: ${{ matrix.root }}
        env:
          # Your compliance.tf registry token, for init; ctfkit never reads it.
          # One variable per compliance.tf host in your module sources.
          TF_TOKEN_bobthecorp_compliance_tf: ${{ secrets.CTF_TOKEN }}
        run: tofu init -input=false

      - name: Run the tests
        working-directory: ${{ matrix.root }}
        run: tofu test -json > tests.json
        continue-on-error: true

      - uses: compliancetf/ctfkit-action@v0
        with:
          command: report
          path: ${{ matrix.root }}
          tests-json: ${{ matrix.root }}/tests.json
          sarif: 'false'
          pr-comment: 'true'

Terraform reads a registry token from TF_TOKEN_<host>, with the dots in the host name replaced by underscores. For the host bobthecorp.compliance.tf that is TF_TOKEN_bobthecorp_compliance_tf.

With code scanning on the repository, change two things in each job: add security-events: write to permissions, and set sarif: ${{ github.event.pull_request.head.repo.full_name == github.repository }} so the upload runs except on pull requests from forks, where it would fail the step. A root without tests drops the test step and the tests-json input. Terraform works the same way: use hashicorp/setup-terraform, terraform init and terraform test -json.

To scan a plan as well, add tofu plan -out=tfplan and tofu show -json tfplan > plan.json in the root after init, then a step with command: scan, plan: ${{ matrix.root }}/plan.json and chdir: ${{ matrix.root }}. That job needs the same cloud credentials your plan job already has.

Scanning one root of several

Without chdir, a scan reads configuration from the whole checkout. In a repository with several roots, set chdir to one root per step or matrix entry, as in the workflow above. ctfkit writes each finding's file relative to chdir, and the copy uploaded to the Security tab gets the chdir prefix back, so code scanning resolves infra/network/main.tf against the repository root. plan stays relative to the checkout.

Settings the repository cannot change

The action passes fail-on, config, exceptions and catalog to ctfkit on every run, so ctfkit never reads a .ctfkit.yaml or .ctf-exceptions.yaml committed to the repository it analyzes. A pull request cannot lower the threshold, widen the endpoint allowlist or grant itself an exception by committing a file.

Unless you set the config and exceptions inputs, the action uses the files bundled with it. The bundled settings leave every key at the ctfkit default, so no tag policy runs. The bundled exception registry is empty.

InputDefaultMeaning
fail-onerrorlowest level that fails the step: error, warning, note, never
configbundledsettings file; the bundled one leaves every key at the ctfkit default
exceptionsbundledexception registry; the bundled one is empty, so nothing is suppressed
catalogembeddedembedded pins the catalog compiled into the release you pinned with version, which holds the CIS AWS Benchmark v6.0 controls; pull downloads the full catalog; a path replaces the catalog with that file

To use your own settings or exceptions, keep ctfkit.yaml and exceptions.yaml on the default branch, or in a separate repository, and read them from a second checkout of the pull request's base commit. A pull request can change its own head, but not the base commit's copy:

      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.base.sha }}
          path: .ctfkit-base
          sparse-checkout: .ctfkit

      - uses: compliancetf/ctfkit-action@v0
        with:
          command: scan
          stage: pre_plan
          chdir: infra
          config: ${{ github.workspace }}/.ctfkit-base/.ctfkit/ctfkit.yaml
          exceptions: ${{ github.workspace }}/.ctfkit-base/.ctfkit/exceptions.yaml

For a separate repository, set repository on the second checkout instead of ref, with a token that can read it.

Loading the full catalog

The embedded catalog resolves the compliance.tf module source that closes each gap, and control ids and clauses for the CIS AWS Benchmark v6.0 controls. For every control and framework, set catalog: pull. The action then runs ctfkit catalog pull before scan or report. It needs action v0.5.0 or later, ctfkit 0.7.0 or later, a paid plan, and your compliance.tf registry token in TF_TOKEN_registry_compliance_tf on the step:

- uses: compliancetf/ctfkit-action@v0
  env:
    TF_TOKEN_registry_compliance_tf: ${{ secrets.COMPLIANCETF_REGISTRY_TOKEN }}
  with:
    command: scan
    stage: pre_plan
    chdir: infra
    catalog: pull

ctfkit sends that token to registry.compliance.tf only. The catalog goes to a directory of its own under RUNNER_TEMP, outside the output directory the summary, upload and comment steps read, and is deleted when the step ends, since the full catalog is licensed to your organization. Findings still carry the control ids and frameworks it resolves. A failed pull fails the step with exit code 2, and ctfkit does not run. scan and report stay offline; only the pull uses the network. A pull request from a fork gets no secrets, so keep catalog: pull on jobs that skip fork pull requests, as the test job above does.

A path in catalog replaces the embedded catalog wholesale rather than adding to it, so point it only at a full catalog file you maintain.

Pull requests from forks

  • A pull_request from a fork gets a read-only job token. The release download still works, since the job token reads public releases. The comment and the Security tab upload do not: a pr-comment there ends with a warning and no comment, and a SARIF upload fails the step, so keep sarif off for fork pull requests.
  • A fork pull request also runs without repository secrets, so init cannot read your compliance.tf registry. The if: condition on the test job in the workflow above skips those runs; the ctfkit-scan job needs no secret and runs.
  • pull_request_target has a write token and secrets and runs the base branch workflow. Under it, never check out the pull request head, and never run terraform or tofu with secrets available: configuration from a fork can run code during init and plan.

Inputs

InputDefaultMeaning
commandreportreport or scan; plan, sign and apply run a governed apply, which is not available: it needs a signed organization profile, and compliance.tf does not issue them
versionlatestctfkit release to install: latest, the newest release when the job runs, or a release such as v1.2.3
github-tokenjob tokentoken for the release download only. The job token reads the public compliancetf/ctfkit releases; set this only for a private copy named in repository
sha256noneSHA-256 of the release archive for the runner; a different archive fails the step. Needs a pinned version
verify-attestationfalsecheck the build provenance attestation of the release
path.module root for report
tests-jsonnonetofu test -json or terraform test -json output for the same root, for report
plannoneplan JSON for scan; refused with stage: pre_plan
chdirnoneconfiguration directory for scan
stageemptyscan: auto, pre_plan or post_plan; empty means auto. report: empty means post_init
formatreceipt,sarif,markdownreport formats; spacelift is refused. scan always renders sarif and text
sariftrueupload to the Security tab; set 'false' on a repository without code scanning
pr-commentfalsekeep one comment on the pull request
comment-tokenjob tokentoken for the pull request comment only
fail-on, config, exceptionssee abovepinned policy inputs
catalogembeddedembedded, pull or a path; see Loading the full catalog
repositorycompliancetf/ctfkitrepository whose release holds the archive

Outputs

OutputMeaning
ctfkit-versionthe ctfkit release that was installed, with latest resolved; pass it to later jobs that must run the same release
exit-code0 ok, 1 findings at or above fail-on, 2 environment error
sarifpath to the SARIF findings file
markdownpath to the text written to the job summary: the report section for report, the escaped text findings for scan
receiptpath to the evidence receipt; empty for scan
pr-commentcreated, updated, skipped or failed; empty when pr-comment is off

The action records the ctfkit exit code, runs the SARIF upload, the job summary and the comment, and applies the exit code last. A run with findings still fills the Security tab and the comment before the job goes red. To read exit-code in a later step, set continue-on-error: true on the action step.

The receipt records which controls the compliance.tf modules in the configuration enforce at module build time, as read from the manifests written at init. It describes the configuration and says nothing about live resources. The receipt is not signed; receipt signing is planned.

Next steps

  • Commands - every flag, the settings file and the exception registry the pinned inputs point at.
  • Reference analyzers - what each rule ID checks, to decide which findings to enforce.
  • Spacelift - the same checks as Spacelift stack hooks and a plan policy.

On this page

Ask AI about this

Help improve this page