IaC / Terraform Scanning
Infrastructure-as-Code Security Scanning
Catch cloud misconfigurations in 15+ Infrastructure-as-Code formats *before* they ship — and keep tracking them *after* they ship. Three engines in one service, so you get the widest coverage of any IaC scanner *plus* something none of them have:
- Native HCL2 engine — a real HashiCorp syntax tree (not whitespace-brittle regex) with compliance-rich typed rules, auto-fix snippets and CIS mapping across AWS, GCP and Azure.
- Aqua Trivy — 1,000+ checks across Terraform, CloudFormation, Kubernetes, Helm, Dockerfile and Azure ARM.
- Checkmarx KICS — the long-tail formats no one else covers: Ansible, Pulumi, Google Deployment Manager, Crossplane, OpenAPI, Knative, gRPC, CI/CD pipelines, Docker Compose and serverless.
Exposed as two surfaces:
- Shift-left CI/CD gate — block a risky
terraform applyin your pipeline, with a pass/fail verdict. - In-platform posture — IaC findings appear on your Findings dashboard, right next to your live cloud findings.
The differentiator — code-to-cloud correlation. Unlike a CLI-only scanner (tfsec / checkov / trivy / KICS), every IaC misconfiguration is matched to your live cloud asset graph: you instantly see whether the bad config is *actually deployed*, and whether that asset is internet-facing or holds sensitive data. A hypothetical config issue becomes a confirmed live exposure that outranks purely-static findings — the link pure scanners structurally cannot make.
How it fits together
POST /api/v1/scan/iac (Bearer ek_…). Verdict blocks a bad terraform apply before deploy.Part A — The CI/CD gate (shift-left)
Step 1 — Create an API key
The endpoint is authenticated with a per-tenant API key — the same kind you'd use for any EchelonGraph API call. You generate it in the app:
- Go to Settings → API Keys.
- Click Create API Key, give it a name (e.g.
ci-iac-scan), select thescanscope, and choose an expiry. - Copy the key — it's shown once and starts with
ek_. - Store it in your CI as a secret named
ECHELONGRAPH_API_TOKEN.
> The key is scoped to your workspace only — every scan it runs is isolated to your tenant. There is no GCP login, no gcloud, and no shared endpoint; you never touch our internal infrastructure.
Step 2 — Call the endpoint
POST https://app.echelongraph.io/api/v1/scan/iac
Authorization: Bearer ek_…
Content-Type: application/json
{ "files": { "main.tf": "<file contents>" }, "gate": true }files— a map of filename → contents (Terraform.tf/.tfvars, CloudFormation YAML/JSON, or Kubernetes manifests). Send your whole module together so cross-file rules work.gate— whentrue, a FAIL verdict returns HTTP 422 so your pipeline step fails. Whenfalse, you always get 200 with the findings (report-only).
Step 3 — Wire it into your pipeline
GitHub Actions — .github/workflows/iac-security.yml:
name: IaC Security Gate
on: [pull_request]
jobs:
iac-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: EchelonGraph IaC scan
env:
ECHELONGRAPH_API_TOKEN: ${{ secrets.ECHELONGRAPH_API_TOKEN }}
run: |
# Collect every .tf in the repo into a {files:{...}} body
python3 - <<'PY' > body.json
import json, glob
files = {f: open(f).read() for f in glob.glob("**/*.tf", recursive=True)}
print(json.dumps({"files": files, "gate": True}))
PY
code=$(curl -s -o resp.json -w "%{http_code}" \
-X POST https://app.echelongraph.io/api/v1/scan/iac \
-H "Authorization: Bearer $ECHELONGRAPH_API_TOKEN" \
-H "Content-Type: application/json" --data-binary @body.json)
jq -r '.findings[] | "[\(.severity)] \(.rule_id) \(.file):\(.line) — \(.description)"' resp.json
echo "gate: $(jq -r .gate_result resp.json) · risk $(jq -r .summary.risk_score resp.json)/100"
if [ "$code" = "422" ]; then echo "::error::IaC gate FAILED — critical misconfiguration"; exit 1; fiGitLab CI — .gitlab-ci.yml:
iac-scan:
image: alpine
before_script: [ "apk add --no-cache curl jq python3" ]
script:
- python3 -c "import json,glob;print(json.dumps({'files':{f:open(f).read() for f in glob.glob('**/*.tf',recursive=True)},'gate':True}))" > body.json
- code=$(curl -s -o resp.json -w "%{http_code}" -X POST https://app.echelongraph.io/api/v1/scan/iac -H "Authorization: Bearer $ECHELONGRAPH_API_TOKEN" -H "Content-Type: application/json" --data-binary @body.json)
- jq -r '.findings[] | "[\(.severity)] \(.rule_id) \(.file):\(.line)"' resp.json
- test "$code" != "422"What happens, end to end
- Developer
git pushopens a pull request. - The CI runner calls
POST /api/v1/scan/iacwith yourek_token and the changed IaC files. - EchelonGraph parses the HCL2, runs all three engines (native + Trivy + KICS), and correlates each finding to your live cloud asset graph.
- The response carries the findings plus a gate verdict — 200 PASS → ✅ merge, 422 FAIL → ⛔ block the PR.
The gate verdict
| Verdict | Triggered by | HTTP (gate=true) | Pipeline |
|---|---|---|---|
| FAIL | ≥ 1 CRITICAL finding | 422 | ⛔ build fails |
| WARN | ≥ 1 HIGH (no critical) | 200 | ⚠️ reported, doesn't block |
| PASS | only medium/low, or clean | 200 | ✅ build passes |
By default only CRITICAL fails the gate. To also block on HIGH, check the summary yourself in CI — e.g. [ "$(jq -r .summary.high resp.json)" = "0" ].
What a result looks like
{
"summary": {
"total_findings": 6, "critical": 3, "high": 3, "medium": 0, "low": 0,
"risk_score": 100, "risk_grade": "F"
},
"gate_result": "FAIL",
"findings": [
{
"rule_id": "TF-001",
"severity": "CRITICAL",
"resource": "aws_s3_bucket_acl.data",
"file": "s3.tf",
"line": 5,
"description": "S3 bucket has public ACL (public-read)",
"remediation": "Set acl = \"private\" or use a bucket policy with restricted access",
"cis_benchmark": "CIS AWS 2.1.5",
"category": "access",
"auto_fix": "# Fix: Change ACL to private\nresource \"aws_s3_bucket_acl\" \"data\" {\n acl = \"private\"\n}",
"compliance": ["SOC2-CC6.3", "HIPAA-164.312(a)(1)", "PCI-DSS-7.1"]
}
]
}Every finding carries:
| Field | Meaning |
|---|---|
severity | CRITICAL / HIGH / MEDIUM / LOW |
rule_id | stable per-cloud ID (TF-/GCP-/AZ-) |
resource + file:line | jump straight to the offending line |
description / remediation | what's wrong and how to fix it |
auto_fix | a ready-to-paste corrected HCL snippet |
cis_benchmark | CIS benchmark reference |
compliance | SOC 2 / HIPAA / PCI-DSS / GDPR mapping |
Part B — In-platform posture
Your Terraform is also scanned during your regular Tier-1 scan, and the findings land on your Findings dashboard tagged *IaC Misconfiguration* — with the same severity, file:line, remediation, and compliance mapping. Because it's the same platform as your cloud scan, an IaC finding sits next to the live cloud finding for the same resource: "public in the *code* and confirmed public in the *running account*."
> Setup: today the Terraform source the platform scans is configured with your team during onboarding. A self-serve "Connect repository / upload Terraform" experience is on the near-term roadmap.
Coverage
Breadth comes from Trivy (1,000+ checks across Terraform/CloudFormation/Kubernetes/Helm/Dockerfile/Azure ARM) and KICS (Ansible, Pulumi, Google Deployment Manager, Crossplane, OpenAPI, Knative, gRPC, CI/CD, Compose, serverless). Depth comes from our native typed rules below — curated, compliance-mapped, with auto-fix snippets — which win on overlap so the basics are never double-reported:
| Cloud | Native-rule examples (depth layer) |
|---|---|
| AWS (18 rules) | public S3 ACL/PAB, unencrypted RDS/EBS, open security groups, IAM wildcard admin, IMDSv2 not enforced, public AMI, CloudTrail logging/KMS |
| GCP (13 rules) | public Storage/IAM, open firewall (SSH/RDP), public Cloud SQL, GKE legacy ABAC, KMS rotation, public BigQuery |
| Azure (12 rules) | public Storage/weak TLS, open NSG, public SQL + firewall, no-SSL Postgres, Key Vault purge-protection, AKS RBAC |
| + CloudFormation & Kubernetes | S3/RDS, privileged containers, hostPath, :latest, missing limits/probes |
The engine reads typed attributes from a real syntax tree, so detection is immune to formatting — storage_encrypted=false is caught whether it's tab-indented, comment-trailed, or fmt-aligned. It is read-only: it parses files and never touches your cloud.
FAQ
Where does the token come from? Settings → API Keys, with the scan scope. It's tenant-scoped, so findings are isolated to your workspace.
Is my code stored? The CI endpoint is stateless — files are parsed in memory and discarded; only the findings come back. (The in-platform surface stores the resulting findings, not your source.)
What are the limits? Up to 100 files / 5 MB per request.
Which formats? 15+: Terraform (.tf/.tfvars, real HCL2), CloudFormation (YAML/JSON), Kubernetes, Helm, Dockerfile, Azure ARM, Ansible, Pulumi, Google Deployment Manager, Crossplane, OpenAPI/Swagger, Knative, gRPC, CI/CD pipelines, Docker Compose and serverless frameworks.
Related: API Reference · Scanning Tiers · Compliance