Terraform Security Scanning: Checkov vs Terrascan vs tfsec Compared
A storage account with allow_nested_items_to_be_public = true slipped through a tfsec scan because a developer had suppressed the check three months earlier without removing the annotation after the risk was resolved. This guide compares Checkov, Trivy (the tfsec successor), and Terrascan across rule coverage, false positive rate, custom rule authoring, and CI/CD integration to help you build a pipeline that actually catches misconfigurations before they reach production.
The Suppressed Check That Let a Public Storage Account Through
A security team ran tfsec on their Azure Terraform repository, got 12 findings, resolved them, and merged. Two weeks later, a Defender for Cloud recommendation flagged an Azure Storage account allowing anonymous blob read access. The Terraform resource had allow_nested_items_to_be_public = true set explicitly — but the tfsec check for that attribute had been suppressed with a comment annotation three months earlier by a developer who did not understand what the check blocked. Nobody removed the suppression after the risk was addressed elsewhere.
Three tools, three different detection approaches, all with suppressible findings. None of them is a substitute for understanding what you are suppressing.
This guide compares Checkov, tfsec (now merged into Trivy), and Terrascan across the dimensions that matter for a production security pipeline: rule coverage, false positive rate, custom rule development, CI/CD integration, and maintenance trajectory.
---
The State of tfsec in 2026
Before comparing tools, you need to know: tfsec is no longer independently developed. Aqua Security, which maintained tfsec, merged its functionality into Trivy in 2023. The tfsec CLI is a thin wrapper that calls trivy config internally. When you run tfsec ., you are running trivy config . with tfsec-compatible output formatting.
This matters for three reasons:
- The tfsec GitHub repository (
aquasecurity/tfsec) has been in maintenance-only mode since late 2023. New rules go into Trivy, not tfsec. - Trivy has significantly broader coverage: Dockerfile, Helm charts, Kubernetes manifests, CloudFormation, CDK, Azure ARM templates, and Terraform — all under one binary.
- tfsec-specific suppression annotations (
#tfsec:ignore:) still work in the wrapper but the Trivy equivalent (#trivy:ignore:) is preferred for new pipelines.
If you are starting a new project, use Trivy directly. If you are maintaining an existing tfsec pipeline, plan migration within the next 12 months. The rest of this article discusses tfsec functionality through the lens of Trivy, noting when behavior differs.
---
Checkov
Checkov is maintained by Bridgecrew, now part of Palo Alto Networks. It has the largest check library of the three tools: approximately 2,500 checks across Terraform, CloudFormation, ARM templates, Bicep, Helm, Dockerfiles, Kubernetes manifests, GitHub Actions, and Ansible playbooks.
Core Capabilities
Install and run against a Terraform directory:
pip install checkov# Run against the current directory, output SARIF for CI integration
checkov -d . --framework terraform --output sarif --output-file results/checkov.sarif
# Run with a specific check ID allowlist
checkov -d . --check CKV_AZURE_3,CKV_AZURE_33,CKV_AZURE_36
# Skip a check with justification documented in code
checkov -d . --skip-check CKV_AZURE_18 --skip-check CKV_AZURE_131
Checkov's Azure coverage hits the most commonly misconfigured resources: Storage Accounts (25+ checks), Key Vault (10+ checks), Virtual Networks and NSGs (15+ checks), AKS (20+ checks), App Services (10+ checks), and SQL/PostgreSQL/Cosmos DB (10+ checks each).
Check IDs follow a CKV_AZURE_ pattern for built-in checks, plus CKV2_AZURE_ for v2 framework checks. The v2 checks include composite evaluations — for example, verifying that a storage account both has a private endpoint AND has public access disabled, rather than checking either condition independently.
Custom Checks
Checkov supports custom checks in Python or YAML. The YAML approach covers simple attribute-level checks:
# custom_checks/azure_storage_lifecycle.yaml
metadata:
name: "Ensure Azure Storage accounts have lifecycle management policies configured"
id: "CKV_CUSTOM_AZURE_1"
category: "GENERAL_SECURITY"
scope:
provider: azure
definition:
cond_type: attribute
resource_types:
<ul class="list-disc pl-6 mb-4 space-y-2">
<li class="text-gray-600 ml-4">azurerm_storage_account</li>
</ul>
attribute: lifecycle_rule
operator: existsFor cross-resource evaluation — verifying that a storage account's private endpoint is in the same VNet as its connected workload — you need a Python check:
# custom_checks/azure_storage_pe_vnet_check.py
from checkov.common.models.enums import CheckResult, CheckCategories
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheckclass StoragePrivateEndpointVNetCheck(BaseResourceCheck):
def __init__(self):
name = "Ensure storage private endpoint is in approved VNet"
id = "CKV_CUSTOM_AZURE_2"
categories = [CheckCategories.NETWORKING]
supported_resources = ["azurerm_private_endpoint"]
super().__init__(name=name, id=id, categories=categories,
supported_resources=supported_resources)
def scan_resource_conf(self, conf):
subnet_id = conf.get("subnet_id", [""])[0]
if "approved-vnet" not in str(subnet_id):
return CheckResult.FAILED
return CheckResult.PASSED
scanner = StoragePrivateEndpointVNetCheck()Run with custom checks using the --external-checks-dir flag:
checkov -d . --external-checks-dir custom_checks/
Strengths and Weaknesses
Checkov's breadth is its main advantage. If your pipeline covers multiple IaC types, Checkov handles all of them with consistent output and a single configuration file. Palo Alto's Prisma Cloud version adds graph-based checks that trace network connectivity across resources — flagging a storage account reachable from the internet through a chain of permissive NSG rules and VNet peering. The open-source version does not include graph-based analysis.
The weakness is false positive rate. Checkov checks tend toward strictness. CKV_AZURE_3 (ensure SQL Server audit retention is greater than 90 days) fails if you set retention to exactly 89 days, even if your policy is 90-day minimum and you round down in configuration. Teams that have not tuned the tool see enough noise to erode trust in the scanner output.
---
tfsec (Trivy config)
Trivy's IaC scanning (trivy config) succeeds tfsec and uses the same check library, now identified by AVD IDs in the format AVD-AZU-.
# Install Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin# Scan Terraform directory, output SARIF
trivy config . --format sarif --output results/trivy.sarif
# Show only HIGH and CRITICAL severity findings
trivy config . --severity HIGH,CRITICAL
# Scan with a project configuration file
trivy config . --config trivy.yaml
A minimal trivy.yaml for a project baseline:
# trivy.yaml
scan:
skip-dirs:
<ul class="list-disc pl-6 mb-4 space-y-2">
<li class="text-gray-600 ml-4">.terraform</li>
<li class="text-gray-600 ml-4">tests</li>
</ul>
severity:
<ul class="list-disc pl-6 mb-4 space-y-2">
<li class="text-gray-600 ml-2">CRITICAL</li>
<li class="text-gray-600 ml-2">HIGH</li>
<li class="text-gray-600 ml-2">MEDIUM</li>
</ul>
ignorefile: .trivyignoreSuppress a finding with an inline annotation:
resource "azurerm_storage_account" "example" {
#trivy:ignore:AVD-AZU-0008
allow_nested_items_to_be_public = false
# ...
}Or add to a centralized .trivyignore file with justification:
# .trivyignore
AVD-AZU-0008 # Accepted risk: blob public access blocked at org level by Azure Policy
Strengths and Weaknesses
Trivy's strength is single-binary breadth. You scan Terraform, Dockerfiles, container images, Kubernetes manifests, and SBOMs with the same tool and the same CI step. If you are already using Trivy for container vulnerability scanning, adding trivy config costs one extra CI step with no new tool to manage.
Azure check coverage is slightly shallower than Checkov — approximately 600 Terraform checks vs Checkov's 2,500-plus — though the critical misconfigurations (public access, encryption at rest, diagnostic logging, RBAC over access keys) are well-covered in both tools.
The primary weakness is migration friction from tfsec. AVD check IDs do not map directly to old tfsec IDs. If you have tfsec suppression annotations in existing Terraform files, they still work with the tfsec wrapper but not with trivy config directly. Migrating suppressions is a manual per-file process with no automated tooling.
---
Terrascan
Terrascan is maintained by Tenable (which acquired Accurics). Its distinctive feature is a Rego-based policy engine — the same policy language as Open Policy Agent. Teams that have invested in OPA for Kubernetes admission control can reuse their policy language expertise for Terraform scanning.
# Install Terrascan
curl -L "https://github.com/tenable/terrascan/releases/latest/download/terrascan_linux_amd64.tar.gz" | tar -xz
sudo mv terrascan /usr/local/bin/# Scan Terraform with Azure policies
terrascan scan -i terraform -t azure --output sarif > results/terrascan.sarif
# List available Azure policy checks
terrascan scan -l -t azure | grep -i storage
# Scan with a custom policy directory
terrascan scan -i terraform -t azure --policy-path custom_policies/
A custom Rego policy:
# custom_policies/azure/storage_secure_transfer.rego
package accurics.azure.storage.azurermStorageAccountSecureTransferimport input as tfplan
deny[msg] {
resource := tfplan.resource.azurerm_storage_account[_]
resource.config.enable_https_traffic_only != true
msg := sprintf("Storage account '%v' does not enforce HTTPS-only traffic", [resource.name])
}
Strengths and Weaknesses
Terrascan's Rego engine is genuinely powerful for organizations with existing OPA infrastructure. Policy reuse across Kubernetes (Gatekeeper or OPA), Terraform (Terrascan), and CI gates (Conftest) creates a unified policy language — one skill set covering your entire platform's policy enforcement points.
The weaknesses are real. Terrascan's Azure check library is substantially smaller than Checkov's: approximately 200 Azure-specific checks vs Checkov's 800-plus. Development velocity has slowed since the Tenable acquisition. If coverage breadth is the priority, Terrascan is the weakest of the three.
---
Tool Comparison
| Feature | Checkov | Trivy (tfsec successor) | Terrascan |
|---|---|---|---|
| Azure Terraform checks | ~800 | ~600 | ~200 |
| Custom rule language | Python or YAML | Rego or Go | Rego |
| Container image scanning | No (open source) | Yes | No |
| Dockerfile scanning | Yes | Yes | Yes |
| Kubernetes manifest scanning | Yes | Yes | Yes |
| GitHub Actions scanning | Yes | No | No |
| SARIF output | Yes | Yes | Yes |
| Active development | Very active | Active | Slowing |
| Commercial upgrade path | Prisma Cloud | Aqua Platform | Tenable Cloud Security |
| False positive rate | Medium-High | Medium | Low-Medium |
| OPA/Rego native policies | No | Partial | Yes |
CI/CD Integration
GitHub Actions Pipeline
For most Azure and Terraform shops, running Checkov with SARIF output integrated into GitHub Advanced Security gives the best developer experience: findings appear directly on PR diffs with line-level annotations.
# .github/workflows/terraform-security.yml
name: Terraform Security Scanon:
pull_request:
paths:
<ul class="list-disc pl-6 mb-4 space-y-2">
<li class="text-gray-600 ml-6">'**.tf'</li>
<li class="text-gray-600 ml-6">'**.tfvars'</li>
</ul>
jobs:
checkov:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
<ul class="list-disc pl-6 mb-4 space-y-2">
<li class="text-gray-600 ml-6">uses: actions/checkout@v4</li>
</ul>
- name: Run Checkov
<ul class="list-disc pl-6 mb-4 space-y-2">
<li class="text-gray-600 ml-6">name: Upload SARIF to GitHub Advanced Security</li>
</ul>
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results/checkov.sarif
category: checkov trivy:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
<ul class="list-disc pl-6 mb-4 space-y-2">
<li class="text-gray-600 ml-6">uses: actions/checkout@v4</li>
</ul>
- name: Run Trivy config scan
<ul class="list-disc pl-6 mb-4 space-y-2">
<li class="text-gray-600 ml-6">name: Upload Trivy SARIF</li>
</ul>
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results/trivy.sarif
category: trivy-configRunning both tools in parallel catches findings each misses individually. Checkov's v2 composite checks catch multi-resource misconfigurations that Trivy misses. Trivy's Dockerfile scanning catches base image issues that Checkov only partially covers.
Hard-Fail vs Soft-Fail Rollout
The soft_fail: true in the example above is intentional for initial rollout. A scanner that blocks every PR on day one gets disabled by engineering leads within a week. The recommended sequence:
Weeks 1-4: Soft-fail mode. Gather baseline findings. Triage which are genuine risks vs noise.
Weeks 5-8: Hard-fail on CRITICAL findings only. Fix or suppress with documented justification for each.
Month 3 onward: Expand hard-fail to HIGH findings. Establish a suppression review process.
Never set soft_fail: false without triaging the baseline first. One team I worked with did this and had 340 findings blocking the pipeline on launch day. Engineers started annotating everything with #tfsec:ignore without reading what the checks caught — creating exactly the suppression debt the scanner was supposed to prevent.
---
Managing False Positives
False positive management is where most teams under-invest and where scanner trust erodes fastest. A three-level framework: Level 1: Inline suppression for resource-specific exceptions
resource "azurerm_storage_account" "public_assets" {
# CKV_AZURE_59: Public blob access intentionally enabled for static website hosting
# Approved by: security-team on 2026-03-15 | Review date: 2026-09-15
#checkov:skip=CKV_AZURE_59:Public assets storage - approved for static website hosting
allow_nested_items_to_be_public = true
# ...
}
Level 2: Project-wide baseline suppressionsMaintain a .checkov.yaml file documenting org-wide accepted risks with written justifications. Require security team PR review before any modification to this file.
Level 3: Check-level disablement
For checks structurally incompatible with your architecture — for example, a check requiring explicit encryption-at-rest configuration on a resource type where Azure manages encryption by default and does not expose the setting in Terraform — disable them at the config level and document the reasoning in a comment.
Every suppression needs an owner, an expiration date, and a justification. Without all three, suppression debt accumulates until a real finding is hidden behind a stale annotation and nobody notices.
---
Which Tool to Choose
Choose Checkov if you need broad coverage across multiple IaC types, want the largest Azure rule library, or are in the Prisma Cloud ecosystem. Choose Trivy (replacing tfsec) if you are already using Trivy for container image scanning, want a single binary for both IaC and containers, or are migrating from tfsec. Consider Terrascan only if your team has existing OPA or Rego investment and wants a shared policy language across Kubernetes admission control and Terraform scanning.For most Azure shops: run Checkov for IaC breadth and Trivy for container images. Treat them as complementary tools, not competitors. The CI overhead is one extra step per pipeline, and the combined coverage closes meaningfully more gaps than either tool alone. Pair both with Defender for Cloud for runtime drift detection post-deploy — see the CSPM comparison guide for how IaC scanning fits into a full posture management stack.
---
IaC Security Pipeline Hardening Checklist
- [ ] Checkov or Trivy integrated into pull request checks with SARIF output uploaded to GitHub Advanced Security or equivalent
- [ ] Baseline triage completed before setting hard-fail mode: all pre-existing findings resolved or suppressed with owner, justification, and expiry date
- [ ] CRITICAL findings set to block merge in CI: PRs cannot merge with unresolved CRITICAL IaC security findings
- [ ] Every inline suppression annotation has three elements: check ID, business justification, and review date
- [ ] Suppression baseline files require security team PR review before modification
- [ ] Custom checks written for organization-specific controls not covered by built-in rules
- [ ] tfsec suppression annotations audited if migrating to Trivy:
#tfsec:ignoreannotations must be converted to#trivy:ignoreequivalents - [ ]
.checkov.yamlortrivy.yamlcommitted to the repository with skip-dirs excluding.terraform/and test fixture directories - [ ] Scan results retained for 90 days minimum to support compliance audit trails
- [ ] Tool version pinned in CI to prevent silent check changes from invalidating established baselines
- [ ] At least one IaC finding reviewed per security sprint to maintain team familiarity with scanner output
- [ ] Defender for Cloud CSPM enabled as a complementary runtime check: IaC scanning catches misconfigurations before deploy; Defender catches drift after deploy
Get weekly security insights
Cloud security, zero trust, and identity guides — straight to your inbox.
Microsoft Cloud Solution Architect
Cloud Solution Architect with deep expertise in Microsoft Azure and a strong background in systems and IT infrastructure. Passionate about cloud technologies, security best practices, and helping organizations modernize their infrastructure.
Questions & Answers
Related Articles
Need Help with Your Security?
Our team of security experts can help you implement the strategies discussed in this article.
Contact Us