Skip to content
Infrastructure as Code iac testing 5 min read

Static Security Scanning

A configuration can pass terraform validate and tflint and still ship a publicly readable S3 bucket, an unencrypted database, or a security group open to 0.0.0.0/0. Those are not syntax errors — they are insecure defaults, and the only way to catch them before apply is a scanner that knows what “good” looks like. Static security scanners read your HCL (or the JSON plan) and match it against a library of security and compliance policies. This page covers the three you will actually reach for: Checkov, tfsec, and Trivy. All three work with Terraform and OpenTofu.

What the scanners catch

These tools encode hundreds of rules drawn from CIS benchmarks, cloud provider best practices, and compliance frameworks (PCI-DSS, HIPAA, SOC 2). Each rule maps to a concrete misconfiguration: missing encryption, overly permissive IAM, public network exposure, disabled logging. Crucially, they understand resource defaults — if encryption is off unless you explicitly enable it, the scanner flags the resource even though your HCL never mentions encryption at all.

Consider this configuration. It is syntactically valid and would apply cleanly:

resource "aws_s3_bucket" "logs" {
  bucket = "devcraftly-app-logs"
}

resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Web tier"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Two serious problems hide here: the bucket has no encryption, versioning, or public-access block, and SSH is open to the entire internet. Run Checkov against the directory:

checkov --directory . --quiet --compact

Output:

       _               _
   ___| |__   ___  ___| | _______   __
  / __| '_ \ / _ \/ __| |/ / _ \ \ / /
 | (__| | | |  __/ (__|   < (_) \ V /
  \___|_| |_|\___|\___|_|\_\___/ \_/

terraform scan results:

Passed checks: 2, Failed checks: 4, Skipped checks: 0

Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
	FAILED for resource: aws_s3_bucket.logs
	File: /main.tf:1-3

Check: CKV_AWS_19: "Ensure all data stored in the S3 bucket is securely encrypted at rest"
	FAILED for resource: aws_s3_bucket.logs
	File: /main.tf:1-3

Check: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22"
	FAILED for resource: aws_security_group.web
	File: /main.tf:5-15

Check: CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled"
	FAILED for resource: aws_s3_bucket.logs
	File: /main.tf:1-3

Each finding cites the rule ID, a human description, the resource, and the exact line range — enough to fix it without guesswork.

Choosing a tool

The three overlap heavily. The practical differences are about scope and where each is heading.

ToolMaintainerScopeNotes
CheckovPrisma Cloud / BridgecrewTerraform, CloudFormation, K8s, Helm, Dockerfile, ARMLargest policy library; supports custom Python and YAML policies
tfsecAqua SecurityTerraform onlyFast; now in maintenance mode, folded into Trivy
TrivyAqua SecurityIaC, containers, filesystems, SBOM, secretsThe successor to tfsec; one binary for misconfig + vuln + secret scanning

For greenfield work, Trivy is the forward-looking default — trivy config . runs the former tfsec engine plus a unified scanner. Checkov remains the richest choice when you need broad coverage across many file types or custom organizational policies.

trivy config --severity HIGH,CRITICAL .

Output:

main.tf (terraform)
===================
Tests: 6 (SUCCESSES: 4, FAILURES: 2)
Failures: 2 (HIGH: 1, CRITICAL: 1)

CRITICAL: Security group rule allows ingress from public internet.
  AVD-AWS-0107 — Public ingress to port 22 (SSH)
  main.tf:8-13

HIGH: Bucket does not have encryption enabled.
  AVD-AWS-0088
  main.tf:1-3

Suppressing false positives

Not every finding is actionable — a public-facing load balancer bucket may legitimately need broad access. Suppress individual rules inline with a comment so the exemption lives next to the code and survives review. Checkov reads checkov:skip comments:

resource "aws_s3_bucket" "public_assets" {
  #checkov:skip=CKV_AWS_20:Static website assets are intentionally public
  bucket = "devcraftly-public-assets"
}

Trivy uses the same idea with a trivy:ignore annotation, and supports a central .trivyignore file for blanket exemptions. Always include a justification after the colon — a skip without a reason is an unreviewed risk.

Suppress narrowly, by rule ID and resource, and never disable a whole rule class globally to silence one finding. A blanket --skip-check in CI hides every future instance of that misconfiguration, not just the one you reviewed.

Custom policy packs

The built-in checks enforce industry baselines; your organization will have rules of its own — mandatory tags, an approved AMI list, a banned region. Checkov supports custom policies in YAML for simple attribute matches and Python for complex logic. This YAML policy fails any resource missing a CostCenter tag:

metadata:
  id: "CKV_DEVCRAFTLY_1"
  name: "Every resource must carry a CostCenter tag"
  category: "GENERAL_SECURITY"
definition:
  cond_type: "attribute"
  resource_types:
    - "aws_s3_bucket"
    - "aws_instance"
  attribute: "tags.CostCenter"
  operator: "exists"

Point Checkov at the directory holding your policies with --external-checks-dir, and they run alongside the built-ins.

CI integration

Scanners are single binaries and exit non-zero on a failed check, so they gate merges naturally. Emit SARIF so findings appear as inline annotations in the pull-request diff and the GitHub Security tab.

name: security-scan
on: [pull_request]

jobs:
  checkov:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: bridgecrewio/checkov-action@v12
        with:
          directory: .
          output_format: sarif
          output_file_path: results.sarif
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with:
          sarif_file: results.sarif

Security scanning sits one rung above linting on the quality ladder — after validate and tflint, before plan-based policy as code. Run it on raw HCL for fast feedback, and optionally against the JSON plan (checkov -f plan.json) to catch misconfigurations that only appear after variables and modules resolve.

Best Practices

  • Pin the scanner version in CI so a new rule release never breaks a pipeline you did not review.
  • Scan the JSON plan, not just the HCL, when modules or variables hide the final resource shape.
  • Suppress findings inline by specific rule ID with a written justification — never disable a rule globally to silence one case.
  • Standardize on Trivy for new projects (it absorbs tfsec); keep Checkov where you need broad file-type or custom-policy coverage.
  • Codify organization-specific rules (tags, regions, AMIs) as custom policies rather than relying on manual review.
  • Emit SARIF in CI to surface findings as inline pull-request annotations and feed the Security tab.
  • Run the scan early enough that an insecure default blocks the merge, not the deploy.
Last updated June 14, 2026
Was this helpful?