Tools Used in DevSecOps · Checkov

Checkov

Checkov is an open-source static analysis tool for infrastructure as code, built by the startup Bridgecrew and now developed under Palo Alto Networks, where it also serves as the open-source engine behind Prisma Cloud's IaC scanning. Its whole reason for existing is that "does this Terraform module, this CloudFormation stack, this Kubernetes manifest, or this Dockerfile look safe to apply" used to mean maintaining a different scanner, a different policy format, and a different suppression syntax for each language separately. Checkov answers all four from one Python engine, one shared policy library, and one command — because it doesn't scan text, it parses each source language into a resolved graph of resources first, which is also what lets it write a single check once and evaluate it against the same underlying resource however it was declared. By the end of this page you should know how that graph gets built, what a built-in policy ID actually checks, how to write your own check in Python or in YAML with no Python at all, and — the part teams get wrong most often — the real difference between suppressing a finding as an accepted risk and actually fixing it.

☺ Explain it like I'm 10

Imagine four house blueprints drawn in four different styles — one in pencil, one in blue ink, one as a 3D model, one as a list of instructions — for four different houses. A building inspector who only knows how to read blue-ink blueprints has to hire three more inspectors for the other three styles. Checkov is an inspector who translates every style into the same internal 3D model first — walls, doors, wiring, all connected the way they'd actually be connected in the finished house — and then checks that one model against the same rulebook, no matter which style the blueprint arrived in. That's also why it can catch something a page-by-page inspector would miss: a door with a broken lock that only matters because of which room it connects to.

🤖Your host for this topic: Recon the Robot — Checkov is Recon's own reconciliation partner for infrastructure-as-code: before anything gets applied, Recon wants the whole resource graph checked against policy, not just each file read one at a time.

What Checkov is, and the problem it solves

☺ Like you're 10: It started as a Terraform scanner for one small startup's own use, and grew into the policy engine a much bigger security company now ships as its own product.

Checkov was created by Bridgecrew, a cloud-security startup, and released as free, open-source software under the Apache-2.0 license. Palo Alto Networks acquired Bridgecrew in 2021 and folded its technology into Prisma Cloud as the "Code Security" module — but Checkov itself stayed open source, is still hosted at github.com/bridgecrewio/checkov, and is still actively released with no Prisma Cloud account required to run it. That split matters operationally: everything on this page — the graph engine, the built-in policy library, custom checks, suppression — works completely offline with the open-source binary. Connecting a Prisma Cloud account layers on a hosted dashboard, additional proprietary policies (identified by a BC_* ID prefix instead of Checkov's own CKV_*), and org-wide policy management — a paid tier on top of a genuinely complete free tool, not a crippled trial of one.

The problem Checkov was built to solve is coverage plus consistency at once. A platform team's infrastructure is rarely written in one language: Terraform provisions the cloud resources, a Kubernetes manifest or Helm chart describes what runs on top of them, a Dockerfile builds the image those workloads run, and a CloudFormation stack might sit alongside all of it because one team standardized differently than another. Scanning each language with a different specialist tool means four different policy libraries that can drift out of sync with each other — a rule enforced for Terraform-provisioned S3 buckets but never written for the equivalent CloudFormation resource, simply because nobody built that second tool's version of the same check. Checkov's answer is one engine and one policy library that already understands all of those languages, so a policy written once has a real chance of applying everywhere the same underlying resource can appear.

◆ Key idea

Checkov's built-in checks are written against a resource's logical configuration, not against one language's syntax for expressing it. Many of its AWS checks declare support for both the Terraform resource type (aws_s3_bucket) and the equivalent CloudFormation resource type (AWS::S3::Bucket) in the same Python check class — one policy ID, evaluated against the same real-world risk, regardless of which IaC language a team happened to choose. That's the concrete payoff of "one tool, one library" from the content brief this page opened with, and it's the reason a security team can write one exception process instead of four.

Architecture: parsing IaC into a graph, not scanning it as text

☺ Like you're 10: Checkov doesn't just read each file on its own — it first draws the whole map of how every resource, variable, and module connects to every other one, then checks the finished map.

The detail that separates Checkov from a simple regex-over-text scanner is that it builds a resource graph before running a single check. For each supported framework, a dedicated parser reads the source — HCL2 for Terraform, YAML/JSON for CloudFormation and Kubernetes, a Dockerfile-specific parser for images, and similar parsers for Helm, Kustomize, Serverless Framework, ARM, and Bicep — and produces a graph (built on the NetworkX library under the hood) where each resource is a node, and edges connect a resource to the variables, modules, and other resources it actually depends on. Terraform variables get resolved through that graph: a var.bucket_name reference, a value passed down into a module, a count or for_each expansion — Checkov walks the graph to substitute the real value before a check ever runs, rather than a check having to parse raw interpolation syntax itself. Checks then run against fully resolved resource configurations, and — because the graph also captures relationships between resources — some checks can inspect a connection rather than a single resource in isolation: whether an EC2 instance is reachable through a security group that itself allows unrestricted ingress, for example, which no per-file, per-resource scan can see on its own.

IaC source Terraform (.tf) CloudFormation Kubernetes YAML Dockerfile + Helm, ARM, more checkov — one Python engine Parse → build resource graph variables + modules resolved, cross-resource edges kept (NetworkX graph, one per framework parser) Built-in checks CKV_* Python classes 1,000s, shipped in the binary, no network needed Custom checks Python (BaseResourceCheck) or YAML — attribute and connection conditions Suppression filter #checkov:skip inline · --skip-check · baseline file every skip should carry a reason on record One report CLI table · JSON SARIF · JUnit XML one exit code Four languages, one graph, one policy library, one suppression convention.

Two more architectural details are worth knowing precisely. First, resolving a Terraform module fully sometimes requires the module's own source — Checkov can fetch remote modules from a registry or a git URL to complete the graph (controlled with --download-external-modules), which means graph accuracy for anything pulled from a remote module quietly depends on that fetch succeeding; in an air-gapped environment, disable it and pre-vendor the modules instead. Second, not everything in a graph can be resolved to a concrete value — a value that only exists once cloud infrastructure is actually provisioned (an AWS-generated ARN, an unresolved data source lookup) is represented internally as an "unknown," and checks that need a concrete value to evaluate are written to handle that case explicitly rather than guessing.

The policy library: one scanner across Terraform, CloudFormation, Kubernetes, and Dockerfiles

☺ Like you're 10: Every rule has an ID and a plain-English name, and the same rulebook covers your cloud resources, your cluster manifests, and the image your app runs in.

Checkov ships with well over a thousand built-in checks, each carrying a stable ID with a prefix that names its framework family — CKV_AWS_*, CKV_AZURE_*, and CKV_GCP_* for cloud-provider resources (evaluated against both Terraform and CloudFormation, per provider), CKV_K8S_* for Kubernetes manifests and Helm-rendered output, and CKV_DOCKER_* for Dockerfile instructions — plus additional families for Serverless Framework, ARM/Bicep, OpenAPI, and several CI-config formats. A handful of checks that show up constantly in practice, exactly the kind you should recognize on sight in a scan's output:

Check IDWhat it catchesFramework
CKV_AWS_20An S3 bucket allowing public READ accessTerraform / CloudFormation
CKV_AWS_21An S3 bucket with versioning not enabledTerraform / CloudFormation
CKV_AWS_19An S3 bucket without server-side encryptionTerraform / CloudFormation
CKV_AWS_24A security group allowing SSH ingress from 0.0.0.0/0Terraform / CloudFormation
CKV_K8S_8A container with no liveness probe configuredKubernetes / Helm
CKV_K8S_23A container not enforcing runAsNonRootKubernetes / Helm
CKV_DOCKER_2No HEALTHCHECK instruction in the imageDockerfile
CKV_DOCKER_3No non-root USER instruction before the entrypointDockerfile

Treat those IDs as illustrative rather than a fixed table to memorize — new checks ship every release, and IDs are never reused once assigned, so the exact catalogue for your installed version is always one command away: checkov --list prints every check Checkov knows, with its ID, name, and framework. Checks also map to published compliance frameworks — --compliance-framework cis_aws and similar flags filter a run down to just the checks that correspond to a specific CIS Benchmark section, which is the mechanism compliance as code at scale covers from the compliance-reporting side rather than the scanning side.

# auto-detect every framework Checkov finds under this directory in one pass
$ checkov -d infra/

# scan just the frameworks that are actually present, explicitly
$ checkov -d infra/ --framework terraform,cloudformation,kubernetes,dockerfile

# see exactly what a specific check ID means before you decide to suppress or fix it
$ checkov --list | grep CKV_AWS_20

Custom policies: a check written in Python, or the same idea in YAML with no Python at all

☺ Like you're 10: If none of the built-in rules cover something your team specifically cares about, you can write your own rule two ways — as real code, or by filling in a template that needs no code.

Every organization eventually has a rule the built-in library doesn't ship, because it's specific to that organization: every S3 bucket must carry a managed-by tag naming the team that owns it, every Deployment must reference an image from the internal registry, no security group may reference a specific legacy CIDR block that's known to be a shared, poorly governed subnet. Checkov supports two ways to write that rule, and the choice mostly comes down to who's writing it.

A Python check, for anything a YAML condition can't express

A custom Python check subclasses BaseResourceCheck, declares which resource types it applies to, and implements scan_resource_conf to return a pass or fail result against that resource's parsed configuration:

# custom_checks/EnsureBucketManagedByTag.py
from checkov.common.models.enums import CheckCategories, CheckResult
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck

class EnsureBucketManagedByTag(BaseResourceCheck):
    def __init__(self):
        super().__init__(
            name="Ensure S3 buckets carry a managed-by tag",
            id="CKV_ACME_1",                         # pick an unused, org-namespaced prefix
            categories=[CheckCategories.GENERAL_SECURITY],
            supported_resources=["aws_s3_bucket"],
        )

    def scan_resource_conf(self, conf):
        tags = conf.get("tags")
        # the HCL2 parser wraps a single block's attributes in a one-item list —
        # conf.get("tags") returns [{"managed-by": ["platform-team"]}], not a bare dict
        if tags and isinstance(tags, list):
            tags = tags[0]
        if tags and tags.get("managed-by") == ["platform-team"]:
            return CheckResult.PASSED
        return CheckResult.FAILED

check = EnsureBucketManagedByTag()   # Checkov discovers checks by the module-level instance
# point Checkov at a directory of custom checks alongside the built-in library
$ checkov -d infra/ --external-checks-dir ./custom_checks

A YAML custom policy, for a team that would rather not write Python

For conditions that boil down to "does this attribute equal/exist/match this value," Checkov's YAML policy format expresses the same idea declaratively, no Python required — and because it's built on the same graph, a YAML policy can express a connection check, not just a single resource's attributes:

# custom_checks/bucket-tag-policy.yaml
metadata:
  id: "CKV2_ACME_1"
  name: "Ensure S3 buckets carry a managed-by tag"
  category: "GENERAL_SECURITY"
scope:
  provider: "aws"
definition:
  cond_type: "attribute"
  resource_types:
    - "aws_s3_bucket"
  attribute: "tags.managed-by"
  operator: "equals"
  value: "platform-team"

The cond_type: attribute shape checks one resource's own configuration; cond_type: connection instead walks the graph's edges to check a relationship between two resource types — the shape that lets a custom policy flag "an EC2 instance whose security group allows ingress from anywhere," which no single-resource check, built-in or custom, can express. Checkov's YAML operator vocabulary and condition types have grown across releases, so treat the shape above as the pattern rather than a guaranteed copy-paste — check the current schema in Checkov's own documentation before authoring one for real, the same way you'd check checkov --list before assuming a built-in ID still means what an older blog post says it means.

🤖 Recon's view

"I don't care whether a policy is Python or YAML — I care whether it's checked into the same repository as the infrastructure it governs, reviewed the same way a Terraform module change is reviewed, and run identically in every pipeline. A policy that lives in one engineer's head, or in a wiki page nobody enforces, isn't a policy. It's a preference that happens to be written down somewhere."

Suppression: an accepted-risk finding versus an actual fix

☺ Like you're 10: Turning off a smoke alarm because you're cooking bacon is fine for five minutes — leaving it off forever because you got used to the beeping is a different decision entirely, and Checkov makes you write down which one you meant.

Not every finding should block a merge. A bucket that intentionally has public read access because it serves a static website, a security group rule that's genuinely restricted further by a second rule Checkov's per-resource check can't see, a legacy resource mid-migration that will be deleted next sprint — these are real cases where the right move is to acknowledge the finding and move on, not to force a change that makes the infrastructure worse to satisfy a scanner. Checkov gives you three ways to do that, and they're not interchangeable.

Inline suppression: scoped to one resource, with a reason on record

# infra/static_site.tf
resource "aws_s3_bucket" "site" {
  bucket = "acme-marketing-site"
}

resource "aws_s3_bucket_public_access_block" "site" {
  bucket = aws_s3_bucket.site.id
  # checkov:skip=CKV_AWS_20:Public READ is intentional — this bucket serves a static
  # website behind CloudFront; see runbook RB-114 for the origin-access-identity setup
  block_public_acls = false
}

A #checkov:skip=<check id>:<reason> comment placed on or directly above the resource block suppresses exactly that check, for exactly that resource, and nothing else. The reason after the colon isn't enforced by the tool — Checkov will happily accept a skip with no explanation at all — but treating it as mandatory in code review is the entire difference between an accepted risk and a silently disabled gate. A skip comment with a real reason is reviewable in the same pull request as the change that needed it; a bare skip comment is a scanner that's been switched off with no trace of why, which is functionally worse than not scanning that resource at all, because it looks green in every report from then on.

--skip-check: broader, and worth treating as a policy exception, not a convenience flag

# skip a check across an entire run — every resource, every file
$ checkov -d infra/ --skip-check CKV_AWS_130

# the inverse: run only a named allowlist of checks, ignoring everything else in the library
$ checkov -d infra/ --check CKV_AWS_20,CKV_AWS_21,CKV_AWS_24

--skip-check disables a rule everywhere it would otherwise fire, for the whole run, with no per-resource reason attached at all — appropriate for a genuinely inapplicable check (a rule scoped to a cloud provider or service the pipeline never touches), and a poor substitute for the inline comment when the real intent is "this one resource, for this one reason."

A baseline file: freezing today's findings so new ones are the only ones that block

# capture every current finding as accepted, without fixing or suppressing any of it individually
$ checkov -d infra/ --create-baseline
# writes .checkov.baseline — a fingerprinted snapshot of today's findings

# subsequent runs only fail on findings NOT already in the baseline
$ checkov -d infra/ --baseline .checkov.baseline

A baseline exists for exactly one situation: adopting Checkov against infrastructure that's been running unscanned for years, where failing the build on every pre-existing finding on day one would block all work indefinitely rather than improving anything. It's meant to be a debt inventory a team pays down over time, not a permanent amnesty — nothing about --baseline distinguishes "we're actively fixing these" from "we generated this once and forgot it existed."

⚠ Watch out — a suppression with no expiry is a suppression forever

None of Checkov's three suppression mechanisms come with a built-in reminder to revisit them. An inline skip written for a temporary migration can outlive the migration by years; a baseline generated during onboarding can quietly absorb new legitimate findings if nobody notices it's stopped shrinking. Suppression debt behaves exactly like any other kind: it's invisible from a passing pipeline, and it only becomes visible during an incident or an audit, when someone finally asks why a known-bad configuration was accepted risk two years ago and nobody can find the reasoning. Treat a periodic review of every #checkov:skip comment and the baseline file's size, the same discipline compliance & governance already applies to any other tracked exception, as part of actually running this tool rather than an optional nice-to-have.

Day-to-day commands

☺ Like you're 10: One command scans everything by default, one flag decides whether a finding actually stops the build, and a handful of output formats feed whatever's reading the results next.

# the default: scan a directory, auto-detect every framework present, human-readable CLI output
$ checkov -d infra/

# scan a single file, or force a specific framework instead of auto-detection
$ checkov -f main.tf --framework terraform

# scan an already-rendered Terraform plan, not just source — catches values only known at plan time
$ terraform show -json tfplan.binary > tfplan.json
$ checkov -f tfplan.json --framework terraform_plan

# machine-readable output for CI annotations, a dashboard, or DefectDojo import
$ checkov -d infra/ -o json --output-file-path results.json
$ checkov -d infra/ -o sarif --output-file-path results.sarif   # GitHub code scanning
$ checkov -d infra/ -o junitxml --output-file-path results.xml  # most CI test-result viewers

# quieter output for a pipeline log, showing only failures
$ checkov -d infra/ --compact --quiet

# report only, never fail the build — the flag that flips Checkov's DEFAULT-FAIL behavior
$ checkov -d infra/ --soft-fail

# list every built-in check this installed version knows about
$ checkov --list

Checkov also ships as a Docker image (bridgecrew/checkov) for a pipeline that would rather not manage a Python environment, a pre-commit hook for a local, pre-push gate, and an official GitHub Action — all wrapping the exact same CLI and exit-code behavior described above, so nothing about the commands changes based on which wrapper is running them.

◆ Key idea — Checkov's default is the opposite of a silent pipeline

Unlike scanners that default to a zero exit code regardless of findings, Checkov fails the run by default the moment any check reports FAILED — no severity flag, no --exit-code argument needed to make it a real gate. --soft-fail is the flag that turns it into report-only mode. That default is exactly backwards from Trivy's, covered on the Trivy tool page — worth remembering the direction each tool defaults to before wiring either into a pipeline you expect to actually block a bad merge.

Gotchas and failure modes

☺ Like you're 10: Most surprises trace back to the graph needing something it couldn't reach, or a custom check assuming the parsed config looks simpler than it actually does.

Where Checkov sits against tfsec, Trivy, Terrascan, and Snyk IaC

☺ Like you're 10: Every alternative is trading Checkov's breadth and graph-based depth for something else — a smaller, faster single-purpose binary, or a bundle deal with a tool that already does three other jobs.

The real question, as with most tool comparisons on this page's shelf, is rarely "which one is objectively better" — it's which trade-off matches the pipeline stage you're building.

ToolScopeStrengthTrade-off
CheckovTerraform, CloudFormation, Kubernetes, Helm, Dockerfile, Serverless, ARM/Bicep, and more — one shared graph engineLargest built-in policy count of any open-source IaC scanner; graph-based checks can inspect cross-resource connections, not just single resources; custom checks in Python or YAMLA Python engine on a large monorepo is slower than a single static Go binary; the graph makes some findings harder to reason about than a flat text match
tfsecTerraform onlyFast, single-purpose Go binary; simple mental model, one file in, one set of findings outTerraform-only by design; the standalone project's engine has been absorbed into Trivy's config scanner, so its own future as a separate binary is worth checking against its current README
Trivy (trivy config)Terraform, CloudFormation, Kubernetes, Dockerfile, Helm — via the absorbed tfsec engine, alongside vulnerability, secret, and license scanning in the same binaryOne install covers IaC misconfiguration plus three other scanner categories entirelySmaller built-in IaC policy count than Checkov ships specifically for that job — breadth across categories, not depth in this one
Terrascan-style Rego scannersMulti-cloud, multi-IaC, policies written in RegoShares a policy language with any other Open Policy Agent investment a platform already has, including Kubernetes admission controlA second policy language to maintain if a team hasn't already standardized on Rego elsewhere; smaller default policy library than Checkov's out of the box
Snyk IaCTerraform, CloudFormation, Kubernetes — part of a commercial SCA/container/IaC platformOne vendor, one dashboard, across IaC and application dependencies; computed fix suggestions and automatic pull requestsRequires an account and network access to run; deeper prioritization features sit behind paid tiers — see the Snyk tool page

A pipeline that's already committed to Checkov as its IaC gate rarely needs a second tool in the same category — the built-in library and graph-based checks cover more ground out of the box than most teams write custom policies for in their first year. Where a second, narrower tool earns its place is usually a specific format Checkov doesn't reach as deeply, or a team that's already standardized on Rego for Kubernetes admission control and wants one policy language rather than two. See vulnerability management & triage for how Checkov's findings get deduplicated alongside every other scanner's output once a pipeline is running more than one at a time, and platform engineering for security guardrails for why a platform team usually runs Checkov centrally against every plan rather than trusting each application team to opt in on their own.

🎬 At the Shift-Left Squad
🤖

Recon the Robot: New Terraform plan, checked against the graph. One failure: CKV_AWS_20, public read on the marketing bucket.

🐢

Timmy the Turtle: Before this merges — is that finding getting fixed, or suppressed?

🦫

Benny the Beaver: Suppressed. It's the static site bucket, public read is the whole point — it's sitting behind CloudFront with an origin access identity.

🐢

Timmy the Turtle: Then the skip comment needs the reason in it, not just the ID. "Fine, it's fine" isn't a reason I can check later.

🦝

Rocky the Raccoon: I'll take that bait — is the origin access identity actually configured, or is this bucket just public with an extra sentence explaining why that's okay?

🦫

Benny the Beaver: ...it's configured. Also now I'm going to go double-check it, thanks.

🐿️

Nutty the Squirrel: While you're in there — file the runbook link in the skip comment. I catalogue every accepted risk quarterly, and "it's fine, ask Benny" doesn't survive Benny going on vacation.

✓ Checkpoint

1. What does Checkov build before it runs a single check, and what does that make possible that a plain text scan can't do? 2. Name the four IaC languages this page's content brief specifically calls out, and explain what "one policy library" concretely buys a team scanning all four. 3. What's the difference between writing a custom check in Python versus in YAML, and what capability does a YAML "connection" condition have that a single-resource attribute check doesn't? 4. Contrast the three suppression mechanisms — inline comment, --skip-check, and a baseline file — and say which one is right for "this one resource, for this one reason." 5. What does --soft-fail actually change, and how does Checkov's default behavior compare to Trivy's on that exact point?

Check your answers
  1. A resource graph — each resource as a node, connected to the variables, modules, and other resources it depends on, with values resolved. That makes cross-resource "connection" checks possible (for example, an EC2 instance reachable through a security group with open ingress), which a scanner reading one file or one resource block at a time can't see.
  2. Terraform, CloudFormation, Kubernetes, and Dockerfiles. One policy library means a rule written once — many of Checkov's AWS checks declare support for both the Terraform and CloudFormation resource type in the same check class — has a real chance of applying consistently everywhere the underlying resource can appear, instead of a security team maintaining separate, potentially drifting rule sets per language.
  3. A Python check subclasses BaseResourceCheck and can express arbitrary logic in scan_resource_conf; a YAML custom policy declares a condition declaratively with no code, using cond_type: attribute for a single resource's configuration or cond_type: connection to check a relationship between two resource types across the graph — the connection type is the one capability a plain per-resource attribute check (Python or YAML) doesn't have on its own.
  4. An inline #checkov:skip=<id>:<reason> comment suppresses exactly one check on exactly one resource, with a reason ideally on record in the same pull request — the right tool for "this one resource, for this one reason." --skip-check disables a check across an entire run with no per-resource reason at all, appropriate for a genuinely inapplicable rule. A baseline file freezes every current finding as accepted in one shot, meant for onboarding legacy infrastructure gradually rather than for an individual, reasoned exception.
  5. --soft-fail switches Checkov from its default — failing the run (non-zero exit) the moment any check reports FAILED — into report-only mode with no build failure. That default is the opposite of Trivy's, which exits 0 regardless of findings unless --exit-code is explicitly set; the two tools default to opposite gating behavior, which is worth confirming explicitly before wiring either into a pipeline meant to actually block a bad merge.