compliance.tf

Run checks in GitHub Actions

This page puts ctfkit by compliance.tf findings on every pull request, in a sticky comment, the job summary and the Security tab, from one public action. ctfkit itself opens no network connection and reads no cloud credentials. The action needs the release token to download ctfkit, and the steps your workflow runs before it need their usual credentials.

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 private repository with the release 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 releasevia release token Results sticky PR comment job summary SARIF, Security tab with code scanning Checkfails on findingsat or above fail-on LEGENDTriggerYour workflow stepctfkit action stepPrivate releaseOutput

The public composite action compliancetf/ctfkit-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. Status: preview.

Get access

ctfkit is available to compliance.tf customers on request: see Get access.

Two defaults need attention on first use:

  • command defaults to report, which reads the module cache and so needs init to have run. For a first run without init, set command: scan and stage: pre_plan.
  • 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.

1. Create the release token

The action downloads ctfkit from the releases of the private compliancetf/ctfkit repository. The job token cannot read another private repository, so every workflow needs this token. Create a fine-grained personal access token with an account that has been granted access:

  • Resource owner: compliancetf
  • Repository access: only compliancetf/ctfkit
  • Repository permissions: Contents: read

Store it as an Actions secret, for example CTFKIT_RELEASE_TOKEN, at the repository or organization level, and pass it as github-token. The action uses it for the release download only.

A fine-grained token expires on the date you set. When it expires, the download fails and every ctfkit step fails with it, so note the date and rotate the secret before then. To keep the token independent of one person's account, create it with a dedicated machine user that has been granted access.

2. Pick a command

commandReadsYour workflow runs firstCredentials needed
scan with stage: pre_planconfiguration in chdircheckoutnone beyond the release token
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

Start with command: scan and stage: pre_plan when your workflow does not run init yet: it needs only the checkout and the release token. Use report, the default, where init already runs in CI, and a plan scan where plan does.

- uses: compliancetf/ctfkit-action@v0.2.1
  with:
    command: scan
    stage: pre_plan
    chdir: infra
    version: v0.2.1
    github-token: ${{ secrets.CTFKIT_RELEASE_TOKEN }}

3. 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

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.

Blocking 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.

4. Set permissions

PermissionNeeded for
contents: readcheckout
pull-requests: writepr-comment: 'true'
security-events: writesarif: 'true'

Complete workflow

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:
    # Pull requests from forks get no secrets, so the release download would fail.
    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: compliancetf/ctfkit-action@v0.2.1
        with:
          command: scan
          stage: pre_plan
          chdir: ${{ matrix.root }}
          version: v0.2.1
          github-token: ${{ secrets.CTFKIT_RELEASE_TOKEN }}
          sarif: 'false'
          pr-comment: 'true'

  test:
    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.2.1
        with:
          command: report
          path: ${{ matrix.root }}
          tests-json: ${{ matrix.root }}/tests.json
          version: v0.2.1
          github-token: ${{ secrets.CTFKIT_RELEASE_TOKEN }}
          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 drop sarif: 'false' so the default upload runs. 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
catalogembeddedpins the catalog compiled into the release you pinned with version

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.2.1
        with:
          command: scan
          stage: pre_plan
          chdir: infra
          version: v0.2.1
          github-token: ${{ secrets.CTFKIT_RELEASE_TOKEN }}
          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.

Pull requests from forks

  • A pull_request from a fork runs without repository secrets, so CTFKIT_RELEASE_TOKEN is empty and the release download fails. The if: condition in the workflow above skips those runs.
  • The job token on a fork pull request is read-only, so a pr-comment there ends with a warning and no comment.
  • 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
versionrequiredrelease to install, such as v0.2.1
github-tokenjob tokentoken for the release download only. The job token cannot read the private compliancetf/ctfkit repository, so set this
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, exceptions, catalogsee abovepinned policy inputs
repositorycompliancetf/ctfkitrepository whose release holds the archive

Outputs

OutputMeaning
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. It is unsigned; signing is planned.

On this page

Ask AI about this

Help improve this page