🏗️

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 Trivy1,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 apply in 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


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:

  1. Go to Settings → API Keys.
  2. Click Create API Key, give it a name (e.g. ci-iac-scan), select the scan scope, and choose an expiry.
  3. Copy the key — it's shown once and starts with ek_.
  4. 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 — when true, a FAIL verdict returns HTTP 422 so your pipeline step fails. When false, 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; fi

GitLab 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

  1. Developer git push opens a pull request.
  2. The CI runner calls POST /api/v1/scan/iac with your ek_ token and the changed IaC files.
  3. EchelonGraph parses the HCL2, runs all three engines (native + Trivy + KICS), and correlates each finding to your live cloud asset graph.
  4. The response carries the findings plus a gate verdict — 200 PASS → ✅ merge, 422 FAIL → ⛔ block the PR.

The gate verdict

VerdictTriggered byHTTP (gate=true)Pipeline
FAIL≥ 1 CRITICAL finding422⛔ build fails
WARN≥ 1 HIGH (no critical)200⚠️ reported, doesn't block
PASSonly medium/low, or clean200✅ 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:

FieldMeaning
severityCRITICAL / HIGH / MEDIUM / LOW
rule_idstable per-cloud ID (TF-/GCP-/AZ-)
resource + file:linejump straight to the offending line
description / remediationwhat's wrong and how to fix it
auto_fixa ready-to-paste corrected HCL snippet
cis_benchmarkCIS benchmark reference
complianceSOC 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:

CloudNative-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 & KubernetesS3/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