Computer Pathshalaकंप्यूटर पाठशाला
videoLesson 02· 25 minFree previewContent ready

IaC scanning that actually catches real bugs

Video coming soon. The lesson script and lab are ready; recording is in production.voiceover script available ↓

IaC scanning that actually catches real bugs

Infrastructure-as-Code scanning is the easiest win in DevSecOps. The tools are mature, the false-positive rate is manageable, and the findings are concrete: "your S3 bucket allows public access" is unambiguous. Yet most teams that adopt an IaC scanner end up turning it off within a quarter because the signal-to-noise ratio kills adoption.

This lesson walks through the three popular open-source scanners — Checkov, tfsec, Terrascan — on a real Terraform codebase. We'll look at what each catches, what each misses, and the muting / waiver mechanisms that let you keep a scanner running long-term without going insane.

The three scanners — quick orientation

ToolLanguage coverageStrengthWeakness
CheckovTerraform, CloudFormation, Kubernetes, Dockerfile, ARM, Bicep, HelmBest policy coverage; strong custom-policy DSL (YAML); growing graph-based checksSlow on large codebases (~30s for 500-file repo); noisier defaults
tfsecTerraformFast (~3s on the same repo); cleanest outputSmaller ruleset; project effectively merged into Trivy IaC
TerrascanTerraform, K8s, Helm, Dockerfile, ARMSolid CIS-benchmark coverage; OPA Rego policy DSLLess actively developed in 2026; weaker ergonomics

In practice, pick one and run it well rather than three poorly. Our recommendation in 2026: Checkov for the breadth, with --quiet --compact flags for sane output. tfsec is the close second if you only care about Terraform.

Running Checkov on a real codebase

Let's run Checkov on a Terraform module that provisions an S3 bucket and an EC2 instance. The module starts intentionally sloppy:

# s3.tf
resource "aws_s3_bucket" "data" {
  bucket = "metalkartpay-prod-logs"
}

resource "aws_s3_bucket_acl" "data" {
  bucket = aws_s3_bucket.data.id
  acl    = "public-read"
}

# ec2.tf
resource "aws_security_group" "web" {
  name = "web-sg"
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web" {
  ami           = "ami-0123456789"
  instance_type = "t3.micro"
  user_data     = <<EOF
#!/bin/bash
echo "DB_PASSWORD=hunter2" > /etc/myapp/env
EOF
}

Run Checkov:

pip install checkov
checkov -d . --compact --quiet

You'll get something like this (abbreviated):

Check: CKV_AWS_19 — S3 bucket should have access logging enabled
  s3.tf:1-3  aws_s3_bucket.data  FAILED

Check: CKV_AWS_20 — S3 bucket should not be public
  s3.tf:1-3  aws_s3_bucket.data  FAILED

Check: CKV_AWS_57 — S3 bucket should have versioning enabled
  s3.tf:1-3  aws_s3_bucket.data  FAILED

Check: CKV_AWS_24 — Security group should restrict ingress on port 22
  ec2.tf:1-9  aws_security_group.web  FAILED

Check: CKV_AWS_8  — EBS volume encryption should be enabled
  ec2.tf:11   aws_instance.web  FAILED

Check: CKV_AWS_46 — Ensure no hard-coded secrets in EC2 user data
  ec2.tf:11   aws_instance.web  FAILED

Six failures. Each one is a real issue. Let's classify them:

Critical (block the PR):

High (warn loudly):

Medium / informational (log only):

This is the classification you have to do for every Checkov rule your codebase triggers. Without this triage step, your team gets six blocking failures the first time the scanner runs, panics, and turns the scanner off.

The muting / waiver mechanism

Inevitably, a finding will fire on code that's correct for a reason the scanner doesn't see. The S3 bucket really does need to be public because it serves the marketing site. The SSH ingress really is restricted to the office IP range (and cidr_blocks is set via a variable Checkov can't statically evaluate).

You have three options:

  1. Inline waiver — put a comment on the offending line:
    resource "aws_s3_bucket_acl" "data" {
      bucket = aws_s3_bucket.data.id
      acl    = "public-read"   # checkov:skip=CKV_AWS_20: marketing static site, public is intentional
    }
    The format is checkov:skip=<CHECK_ID>: <justification>. The justification is required by team policy (not by Checkov), so that the next developer reading the code knows why the waiver exists.
  2. Module-level waiver — a .checkov.yaml file at the directory root that excludes specific checks for specific files:
    skip-check:
      - CKV_AWS_20
    Use only when the waiver applies to every resource in that directory.
  3. Global waiver — same .checkov.yaml at the repo root with no path scoping. Avoid unless the check is genuinely useless to your team.

The discipline: every waiver has a justification comment. Reviews flag missing justifications. After three months, periodically audit waivers — the reason they were added may no longer apply, and the finding might now be a real issue.

Custom policies — what your team actually cares about

Checkov ships ~1,000 rules. Most of them are useful. A few are aspirational for your codebase, and a few are specific to your environment in a way Checkov can't anticipate.

Custom policies in Checkov can be written as YAML:

# .checkov-custom/no-public-rds.yaml
metadata:
  name: "RDS instances must not be publicly accessible"
  id: "CKV_CUSTOM_001"
  category: "GENERAL_SECURITY"
definition:
  cond_type: "attribute"
  resource_types: ["aws_db_instance"]
  attribute: "publicly_accessible"
  operator: "equals"
  value: false

Run with:

checkov -d . --external-checks-dir .checkov-custom --compact --quiet

Custom policies are how you encode the lessons your team learned the hard way. "We had an incident where someone made an RDS instance publicly accessible" → "We now have a custom check that blocks it."

The CI pipeline runs both built-in and custom checks together. Over time the custom check directory becomes your team's institutional memory of what not to do.

What Checkov misses

Three classes of bug Checkov won't catch:

  1. Logical bugs. "This IAM policy is over-permissioned for what this app actually does." Checkov can flag *:* (wildcards), but it can't tell whether s3:GetObject on prod-pii-bucket/* is appropriate for this application. Tools that try — IAM Access Analyzer, Policy Simulator — are run at level 6-7, not level 1-3.
  2. Cross-resource issues. "This S3 bucket policy and that IAM role together create an unintended path to the data." Each piece in isolation looks fine. The combination is the bug. Graph-based tools like Checkov's graph mode help here but are slow.
  3. Runtime configuration drift. "This was provisioned with the secure config, but someone changed it via the AWS Console three months later." This is what cloud-runtime security tools (Wiz, Lacework, Prisma) are for. Out of scope for IaC scanning.

The right frame: Checkov is your first line of defence. It catches the easy bugs cheaply. The other lines (level 5-7 tooling) catch the rest. Don't expect any single tool to catch everything; expect the stack to.

Wiring Checkov into CI

A minimal GitHub Actions job:

name: IaC security scan
on:
  pull_request:
    paths: ['**.tf', '**.tfvars', 'modules/**']

jobs:
  checkov:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: bridgecrewio/checkov-action@v12
        with:
          directory: .
          framework: terraform
          quiet: true
          soft_fail: false      # block PR on critical findings
          # soft_fail_on flips this per check, see below
          skip_check: CKV_AWS_19,CKV_AWS_57   # informational only — don't block
          external_checks_dirs: ".checkov-custom"

soft_fail controls the BLOCK vs WARN decision globally. For finer-grained control, use soft_fail_on to flip specific checks to warn, and skip_check to flip specific checks to log-only.

Key takeaways

What's next

Lesson 03 unpacks the block / warn / log decision in detail. We've used the framework loosely so far; Lesson 03 makes it a tool you can apply consistently to any new scanner you adopt — Semgrep, Trivy, Snyk, your custom rule, anything. After that, Lesson 04 on false-positive triage (the discipline that keeps the whole thing alive), and the end-of-week lab where you wire severity gates on a real PR pipeline.