Terraform security scanning in GitHub Actions
There are three ways to put audytx's cross-resource AWS Terraform review in front of a pull request, in rising order of effort. Most teams need only the first. The second gives you a hard build gate inside GitHub Actions; the third adds resolved-value precision. All three run the same engine and suppress the same false positives.
GitHub App
Zero config. A summary comment on the PR, SARIF to Code Scanning. No workflow file.
Actions + MCP
A short workflow fails the build on High/Critical findings. Free Client ID.
Plan enrichment
Upload the resolved terraform plan over OIDC for variable-accurate findings.
The zero-config path: the GitHub App
Before you write a workflow, know that you may not need one. The audytx GitHub App reviews every pull request on install — no Actions file, no token to store. It reads your .tf/.tfvars files, runs the whole engine, posts one comment with the findings and their suppression rationales, adds inline annotations, and uploads a SARIF document to GitHub Code Scanning so findings show in the Security tab.
A hard gate in GitHub Actions
To block a merge on a High or Critical finding, add a workflow that calls the audytx MCP API and fails the job on the result. This needs a free Client ID — store it as the repository secret AUDYTX_CLIENT_ID. The workflow bundles your Terraform, POSTs one JSON-RPC call, and exits non-zero when the severity summary crosses your threshold:
name: audytx Terraform security gate
on: [pull_request]
permissions:
contents: read
jobs:
audytx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Scan Terraform with audytx
env:
AUDYTX_CLIENT_ID: ${{ secrets.AUDYTX_CLIENT_ID }}
run: |
# Bundle every .tf/.tfvars file into one JSON-RPC request.
# Pass the whole tree — cross-resource reasoning needs it.
req=$(find . \( -name '*.tf' -o -name '*.tfvars' \) -print0 \
| xargs -0 -I{} jq -n --arg p "{}" --rawfile c "{}" \
'{path:$p, content:$c}' \
| jq -s '{jsonrpc:"2.0", id:1, method:"tools/call",
params:{name:"scan_terraform", arguments:{files:.}}}')
resp=$(curl -sf https://audytx.com/mcp \
-H "Content-Type: application/json" \
-H "X-Client-ID: $AUDYTX_CLIENT_ID" --data "$req")
# A refused call comes back as a tool error, not as findings.
if [ "$(echo "$resp" | jq -r '.result.isError // false')" = "true" ]; then
echo "::error::audytx refused the scan: $(echo "$resp" | jq -r '.result.content[0].text')"
exit 1
fi
# List the High/Critical findings for the log …
echo "$resp" | jq -r '.result.structuredContent.findings[]
| select(.severity=="CRITICAL" or .severity=="HIGH")
| "\(.severity) \(.rule_id) \(.file):\(.line_start) \(.title)"'
# … then fail the build if any exist.
gate=$(echo "$resp" \
| jq '.result.structuredContent.summary | .critical + .high')
if [ "$gate" -gt 0 ]; then
echo "::error::audytx found $gate High/Critical AWS security findings"
exit 1
fiThe response carries a summary block (critical/high/medium/low counts) and a findings array with rule_id, severity, file, line_start, and remediation — the same contract the docs describe. Move the gate to .critical alone for a looser policy, or add .medium for a stricter one.
Resolved-value precision with a plan upload
Static parsing can't resolve every value: variables, count/for_each expansion and module internals are only partly known without a plan. If you want findings computed against the resolved plan, add the plan-upload workflow. It authenticates with the run's GitHub OIDC token — no secret to store — runs terraform show -json, and POSTs the plan so audytx re-scans with final values folded in. It's strictly additive: the normal scan keeps working with or without it.
The full workflow and its permissions: id-token: write block are documented on the plan-scanning page. Pair it with either level above.
The same gate for CloudFormation
Building CloudFormation instead of Terraform? The MCP gate works the same way and returns the same response shape. Send templates only: point find at the directory that holds your templates and call scan_cloudformation. If the call includes a file that is not a CloudFormation template (a workflow file or a package.json, for example), the whole call is refused and no scan credit is used. Replace infra/cloudformation with your template directory:
req=$(find infra/cloudformation -type f \( -name '*.yaml' -o -name '*.yml' -o -name '*.json' -o -name '*.template' \) -print0 \
| xargs -0 -I{} jq -n --arg p "{}" --rawfile c "{}" \
'{path:$p, content:$c}' \
| jq -s '{jsonrpc:"2.0", id:1, method:"tools/call",
params:{name:"scan_cloudformation", arguments:{files:.}}}')supported_iac).Common questions
Do I need a GitHub Actions workflow at all?
No. The GitHub App reviews every pull request on install with no workflow file — a summary comment plus SARIF to Code Scanning. You only need a workflow if you want to fail the build on a finding, which the App doesn't do. Many teams run the App alone.
Is there a prebuilt CLI or Marketplace Action I can drop in?
No. The blocking gate is the MCP call shown above, which any repository can run with a free Client ID. It is the same engine the App and coding agents use, so every surface reports the same findings.
Does the workflow send my Terraform anywhere it's stored?
The MCP call sends file contents to audytx, which parses them in memory and discards them when the response returns. audytx does keep a record of each finding (rule, message, resource, file and lines) for 90 days, then deletes it. File contents are never part of that record.
How do the findings reach the GitHub Security tab?
Through the GitHub App, which uploads a SARIF v2.1.0 document to Code Scanning automatically — including the context-suppressed findings as SARIF suppressions with their rationale. The Level 2 workflow is a build gate, not a SARIF uploader; run both to get the gate and the Security-tab surface.
Put the gate in front of your next PR
Install the GitHub App for zero-config review, or drop in the workflow to fail the build on High/Critical findings. The Free plan includes 100 scans a month.