Checkov is the open source infrastructure as code (IaC) scanner maintained by Prisma Cloud (Palo Alto Networks), under the Apache 2.0 licence. It reads your Terraform, Kubernetes, Dockerfile, Ansible or CloudFormation files and flags misconfigurations before they are deployed: public bucket, SSH open to the world, privileged container. This lesson is for a DevOps engineer who wants to plug it into CI without drowning in its results.
Everything is measured on Checkov 3.3.22: 25 failures on four Terraform resources, 22 on a single Deployment, and plain-text secrets that slip through without an alert in each of the four example files. Knowing what Checkov does not see matters as much as knowing how to read what it finds.
What you will learn
- Install Checkov in a pinned version and run a first scan
- Read a result and separate real risks from comfort rules
- Spot the blind spots, secrets included, before relying on it
- Make a CI job fail on the chosen rules, not on severity
- Handle false positives and write a custom rule that loads
- Produce the evidence for SOCLE-INT-VER-2 (in French), the IaC configuration analysis
What is Checkov?
Checkov is a static analyser (SAST)
specialised in infrastructure as code: it reads the code, without deploying
anything or querying a cloud. Each rule (a "check") carries a stable
identifier, such as CKV_AWS_24, and verifies one precise good practice. It
can also link resources together through graph analysis: rules prefixed
CKV2_ check, for example, whether an S3 bucket has a public access block
declared in another resource.
How many rules, and for which formats?
The checkov --list command of version 3.3.22 counts 1361 distinct rule
identifiers. The "more than 800" figure still going around dates from
version 2. The breakdown shows where the effort goes: Terraform dwarfs
everything else, and a more recent framework such as GitHub Actions only
has a handful of rules.
| Framework | Rules (Checkov 3.3.22) |
|---|---|
| Terraform | 1144 |
| ARM / Bicep (Azure) | 173 / 167 |
| CloudFormation | 139 |
| Kubernetes | 119 |
| Ansible | 29 |
| Dockerfile | 28 |
| OpenAPI | 21 |
| Secrets | 18 |
| GitHub Actions | 8 |
| GitLab CI | 3 |
The same rule can serve several formats, hence a sum greater than the total. For CI pipelines, these figures say it all: a GitHub Actions workflow is better checked with dedicated tools such as zizmor, seen in the lesson on implicit trust in pipelines.
What Checkov does not prove
A green Checkov report does not prove that an infrastructure is secure. It proves that the code contains none of the configurations its rules know about. Three limits come back throughout the lesson. It only sees the code, not the real state of the cloud nor what an operator changed by hand. It does not read the vulnerabilities of an image's packages, which is the job of Trivy. And its secret detection, measured below, lets ordinary passwords through.
Installing Checkov in a pinned version
Checkov is a Python package: you install it with pipx to isolate it from
the rest of the system, pinning the version. The official Docker image
avoids installing Python, and is pinned by digest: since the Trivy incident
of 2026, a scanner is treated like any dependency, never as latest.
# Isolated installation, pinned versionpipx install "checkov==3.3.22"checkov --version# 3.3.22
# Or without Python, with the image pinned by digestdocker run --rm -v "$PWD":/src -w /src \ bridgecrew/checkov:3.3.22@sha256:617c76e3f9b1f7907ebca9abb6b9d746844edcb48e9bd775e4692c69c1c6ac47 \ -d . --compact --quietThe output of checkov --version must show exactly the requested version.
The Homebrew package also exists on macOS, but it follows the latest
version published, which does not suit a reproducible CI.
First scan: a deliberately wrong Terraform file
The simplest way to understand Checkov is to give it a file whose mistakes you
know. This one is valid for Terraform: terraform validate accepts it. It
nevertheless contains four classic mistakes, marked in comments, and a
plain-text password.
# ❌ S3 bucket with public accessresource "aws_s3_bucket" "bad_bucket" { bucket = "factures-clients-prod" acl = "public-read" # Problem: bucket publicly accessible}
# ❌ EC2 instance without IMDSv2resource "aws_instance" "bad_instance" { ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.micro" # No metadata_options: IMDSv1 stays active, exploitable through SSRF}
# ❌ Security Group with SSH open to the worldresource "aws_security_group" "bad_sg" { name = "allow-all-ssh" ingress { from_port = 22 to_port = 22 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] # Problem: SSH open to the Internet }}
# ❌ Publicly accessible RDS with a plain-text passwordresource "aws_db_instance" "bad_rds" { identifier = "facturation" engine = "mysql" instance_class = "db.t3.micro" username = "admin" password = "SuperSecret123!" # Problem: secret in the code publicly_accessible = true # Problem: database exposed to the Internet}Running the scan and reducing the output
Two options make the output readable in a terminal. --compact removes the
code excerpt shown under each result, and --quiet hides the passed
rules: without it, Checkov also lists every check that passed. The scan
exits with code 1 as soon as a rule fails, which is enough to make a CI job
fail.
$ checkov -d terraform --compact --quietterraform scan results:
Passed checks: 14, Failed checks: 25, Skipped checks: 0
Check: CKV_AWS_79: "Ensure Instance Metadata Service Version 1 is not enabled" FAILED for resource: aws_instance.bad_instance File: /main.tf:8-12Check: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22" FAILED for resource: aws_security_group.bad_sg File: /main.tf:15-23Check: CKV_AWS_17: "Ensure all data stored in RDS is not publicly accessible" FAILED for resource: aws_db_instance.bad_rds File: /main.tf:26-33Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access." FAILED for resource: aws_s3_bucket.bad_bucket File: /main.tf:2-5...$ echo $?1The four targeted mistakes are found, each by a named rule. Each block also
carries a Guide: line pointing to Prisma Cloud's remediation page,
removed here for readability.
Reading a result: real risk or comfort rule?
Out of 25 failures, only four match the targeted mistakes. The other 21 are
good practices that this file does not address: cross-region replication
of the bucket (CKV_AWS_144), Multi-AZ for the database (CKV_AWS_157),
logging, lifecycle. They are not equal: a public bucket exposes data today, a
missing replication only costs you in a regional disaster. The reading work is
to classify, not to fix everything.
| Rule | What it checks | Risk if ignored |
|---|---|---|
CKV_AWS_20 | public-read ACL on a bucket | Data readable by the whole Internet |
CKV_AWS_24 | SSH open from 0.0.0.0/0 | Brute force, unauthorised access |
CKV_AWS_17 | publicly_accessible = true on RDS | Database directly exposed |
CKV_AWS_79 | IMDSv1 active on EC2 | Theft of the instance credentials through SSRF |
CKV_AWS_144 | Cross-region replication | Loss in case of a regional outage, no exposure |
The database's plain-text password, however, triggers no rule. Secret
detection looks for known formats and high-entropy strings:
SuperSecret123! resembles neither. This is the first blind spot to
remember.
Kubernetes, Dockerfile, Ansible: what the rules see
The other formats follow the same principle, with a common trap: a high number of passed rules says nothing about the security of the file. Checkov applies its whole rule set to every resource, including the Kubernetes control plane checks that make no sense for a Deployment.
A Deployment that gives access to the node
This manifest piles up the configurations that give a container full access to its node: shared host network and processes, privileged mode, root of the file system mounted. It also carries a secret in an environment variable.
apiVersion: apps/v1kind: Deploymentmetadata: name: insecure-app namespace: default # ❌ Uses the default namespacespec: replicas: 1 selector: matchLabels: app: insecure-app template: metadata: labels: app: insecure-app spec: hostNetwork: true # ❌ Shares the host network hostPID: true # ❌ Sees the host processes containers: - name: app image: nginx:latest # ❌ "latest" tag, not reproducible securityContext: privileged: true # ❌ Privileged container = root on the host env: - name: DB_PASSWORD value: "secret123" # ❌ Secret in the env variables volumeMounts: - mountPath: /host name: host-volume volumes: - name: host-volume hostPath: path: / # ❌ Mounts the host rootThe scan reports 22 failures for 67 passes, and the most serious ones are
there. Watch the identifiers: hostPID triggers CKV_K8S_17, not
CKV_K8S_1, which carries the same title but only targets the old
PodSecurityPolicy, removed from Kubernetes.
$ checkov -f k8s/deployment.yaml --compact --quietkubernetes scan results:
Passed checks: 67, Failed checks: 22, Skipped checks: 0
Check: CKV_K8S_17: "Containers should not share the host process ID namespace"Check: CKV_K8S_19: "Containers should not share the host network namespace"Check: CKV_K8S_16: "Container should not be privileged"Check: CKV_K8S_14: "Image Tag should be fixed - not latest or blank"Check: CKV_K8S_43: "Image should use digest"...Two mistakes in the manifest nevertheless trigger no rule. The hostPath
mount of the root / goes through: the only related rule, CKV_K8S_27,
only targets the Docker socket. And DB_PASSWORD in clear text in env.value
goes through too: CKV_K8S_35 only concerns Kubernetes secrets injected as
variables. These two blind spots belong to an admission controller and to
the Pod Security Standards (the three levels of restriction that Kubernetes
can impose on pods), covered in the module on containers.
A badly built Dockerfile
Dockerfile rules cover build choices: floating base image, ADD
instruction, no non-root user. This file combines several of them, and hides
an API key in an ENV, readable by anyone who pulls the image.
# ❌ "latest" tag: non-reproducible versionFROM python:latest
# ❌ Deprecated MAINTAINER (use LABEL)MAINTAINER dev@example.com
WORKDIR /app
# ❌ ADD can extract archives and download from the InternetADD https://example.com/config.tar.gz /app/ADD . /app/
RUN pip install -r requirements.txt
# ❌ Exposes the SSH portEXPOSE 22 80
# ❌ Secret in an environment variable (visible in the image)ENV API_KEY=sk-secret-12345
# ❌ No USER and no HEALTHCHECKCMD ["python", "app.py"]The scan gives 7 failures. Absence rules, such as CKV_DOCKER_3 which
requires a USER, target the whole file (Dockerfile. without a suffix),
for lack of a line to point to.
$ checkov -f docker/Dockerfile --compact --quietdockerfile scan results:
Passed checks: 26, Failed checks: 7, Skipped checks: 0
Check: CKV_DOCKER_7: "Ensure the base image uses a non latest version tag"Check: CKV_DOCKER_6: "Ensure that LABEL maintainer is used instead of MAINTAINER (deprecated)"Check: CKV_DOCKER_4: "Ensure that COPY is used instead of ADD in Dockerfiles"Check: CKV_DOCKER_4: "Ensure that COPY is used instead of ADD in Dockerfiles"Check: CKV_DOCKER_1: "Ensure port 22 is not exposed"Check: CKV_DOCKER_2: "Ensure that HEALTHCHECK instructions have been added to container images"Check: CKV_DOCKER_3: "Ensure that a user for the container has been created"The API_KEY=sk-secret-12345 key appears nowhere. CKV_DOCKER_7 only
triggers on latest: a versioned tag without a digest, such as
python:3.14, satisfies it. The lesson on
Dockerfile best practices
covers what these rules leave aside.
An Ansible playbook
The Ansible rule set is short, 29 rules, focused on the options that
disable a verification. This playbook contains two of them, force and
validate_certs, plus a mode: "0777" and two secrets in variables.
---- name: Configuration serveur web hosts: webservers become: true vars: # ❌ Plain-text password in the code db_password: "SuperSecret123!" api_key: "sk-1234567890abcdef"
tasks: - name: Install packages with force ansible.builtin.apt: name: nginx state: present force: true # ❌ Bypasses signature verification
- name: Download file ansible.builtin.get_url: url: https://example.com/config.tar.gz dest: /tmp/config.tar.gz validate_certs: false # ❌ Disables TLS validation
- name: Set insecure permissions ansible.builtin.file: path: /etc/app/config mode: "0777" # ❌ Overly permissive permissionsThe scan produces two reports for a single file: ansible for the modules
and secrets for the text. Secret detection runs on any file it can read,
whatever the framework.
$ checkov -f ansible/playbook.yml --compact --quietsecrets scan results:
Passed checks: 0, Failed checks: 1, Skipped checks: 0
Check: CKV_SECRET_6: "Base64 High Entropy String" File: /ansible/playbook.yml:8-9
ansible scan results:
Passed checks: 2, Failed checks: 2, Skipped checks: 0
Check: CKV_ANSIBLE_6: "Ensure that the force parameter is not used, as it disables signature validation ..."Check: CKV_ANSIBLE_2: "Ensure that certificate validation isn't disabled with get_url"Only the api_key, a random string, is found. The db_password goes
through, and so does the mode: "0777": none of the 29 Ansible rules reads
the value of a permission. This check is left to code review, or to a
custom rule like those at the end of this lesson.
Secret detection has blind spots
Checkov ships 18 secret rules: known formats (CKV_SECRET_2 for an AWS
AKIA… key, CKV_SECRET_13 for a private key, CKV_SECRET_14 for Slack) and
two entropy rules, in base64 (CKV_SECRET_6) and in hexadecimal
(CKV_SECRET_19). This mechanism misses two categories, measured on
version 3.3.22.
The first is the human password: SuperSecret123!, secret123 or
sk-secret-12345 have neither a known format nor enough entropy, and none of
the four previous files flagged them. The second is more surprising: by
default, Checkov only reads secrets in certain file types. A real AWS key
in a credentials.ini, or a GitHub token in a .env, goes through without
an alert.
$ checkov -d sec --framework secrets --compact --quiet# no result: .ini and .env are not read
$ checkov -d sec --framework secrets --compact --quiet --enable-secret-scan-all-filesPassed checks: 0, Failed checks: 3, Skipped checks: 0Check: CKV_SECRET_6: "Base64 High Entropy String" File: /.env:1-2Check: CKV_SECRET_2: "AWS Access Key" File: /credentials.ini:1-2Check: CKV_SECRET_2: "AWS Access Key" File: /credentials.ini:2-3The same content renamed config.yaml is found without the option. The
practical conclusion: Checkov does not replace a dedicated secret detector
such as gitleaks, run in pre-commit and in CI, covered in the lesson on
secrets in CI/CD.
Making CI fail on the right rules
Checkov exits with code 1 as soon as a rule fails and with code 0 otherwise: on an old project, the first job is therefore bound to break. Three options adjust this behaviour, and they were measured on the previous Dockerfile.
| Option | Effect | Measured exit code |
|---|---|---|
| none | Fails as soon as one rule is at fault | 1 |
--soft-fail | Full report, never fails | 0 |
--soft-fail-on CKV_DOCKER_2,CKV_DOCKER_3 | These two rules no longer block, the others do | 1 (5 other failures) |
--soft-fail --hard-fail-on CKV_DOCKER_8 | Only the listed rule blocks | 0 (it passes) |
The pair --soft-fail and --hard-fail-on is the most useful to get
started: you block on a short list of serious rules, such as CKV_AWS_20,
CKV_AWS_24 or CKV_K8S_16, and leave the rest as a report. You lengthen the
list as the debt goes down.
A GitHub Actions example
The official bridgecrewio/checkov-action action runs Checkov in its
official image. It is pinned to the SHA of its commit, like any action: its
version v12.3128.0 runs Checkov 3.3.22. The SARIF report (a standard format
for analysis results) goes to the repository's Code Scanning tab, even when the
scan fails, thanks to if: always().
name: checkov
on: pull_request: paths: - "infra/**"
permissions: {}
jobs: checkov: runs-on: ubuntu-24.04 permissions: contents: read security-events: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false
- name: Scan the IaC uses: bridgecrewio/checkov-action@cc0e17d456ef1713bbfd963ebac19dc776775cb2 # v12.3128.0 with: directory: infra/ framework: terraform output_format: cli,sarif output_file_path: console,results.sarif
- name: Publish to Code Scanning if: always() uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: sarif_file: results.sarifThis workflow passes actionlint, zizmor 1.30.0 and poutine 1.1.6
with no finding at all. One limit remains to know: the action designates its
image by tag (ghcr.io/bridgecrewio/checkov:3.3.22), not by digest. For a
complete guarantee, run the image pinned by digest in a run step.
Managing false positives
A rule can be wrong in your context: a bucket that hosts a public website must be public. Checkov offers three mechanisms, from the most targeted to the broadest. The first, inline suppression, is the only one that stays attached to the code and goes through review with it.
Suppressing a rule in the code
Each format has its own comment syntax, always followed by a
justification. The three forms below were verified: each scan counts the
rule as Skipped checks: 1 or 2 instead of a failure.
resource "aws_s3_bucket" "site" { # checkov:skip=CKV_AWS_20: static website, public by design bucket = "site-vitrine" acl = "public-read"}apiVersion: v1kind: Podmetadata: name: test-pod namespace: recette annotations: checkov.io/skip1: CKV_K8S_40=UID imposed by the test imagespec: containers: - name: app image: nginx:1.29.1@sha256:8adbdcb969e2676478ee2c7ad333956f0c8e0e4c5a7463f4611d7a2e7a7ff5dc# checkov:skip=CKV_DOCKER_2: the health probe is carried by the Kubernetes manifestFROM ubuntu:24.04@sha256:008173c23f95b170204355c12626cb5a965d779a7e1283b09e9cffbb1bf33ca3# checkov:skip=CKV_DOCKER_4: the archive must be extracted on copyADD app.tar.gz /opt/app/RUN useradd --uid 1000 --create-home appuserUSER appuserOn Kubernetes, the annotation is numbered (skip1, skip2…) to set
several of them. On the command line, --skip-check CKV_AWS_20,CKV_AWS_21
removes rules for the whole scan, without a trace in the code: keep it for the
rules that the team considers irrelevant everywhere.
Starting with a baseline
On an existing project, --create-baseline records the current failures in a
.checkov.baseline file, placed in the scanned folder. Subsequent scans
with --baseline only report the new failures. Measured: after adding an
open RDP Security Group to the starting Terraform file, the scan with the
baseline only reports its 3 failures, instead of 28.
checkov -d terraform --create-baselinecheckov -d terraform --baseline terraform/.checkov.baseline --compact --quietThe baseline freezes a debt, it does not pay it off. You version it, and regenerate it only when fixes have reduced it.
A configuration file for the team
Rather than repeating the options, a .checkov.yaml file placed in the
current folder is loaded automatically, without any option. Measured: with
skip-check: [CKV_DOCKER_2, CKV_DOCKER_3] in this file, a plain
checkov -d . drops from 7 to 5 failures. It is both handy and treacherous,
like the trivy.yaml of the previous lesson: a file forgotten in a repository
silently filters the report.
directory: - infraframework: - terraform - kubernetes - dockerfileskip-check: - CKV_AWS_144 # cross-region replication: out of budget, decision of 2026-10-01compact: truequiet: trueEach key takes a command-line option without the double dash. The command
checkov -d . --skip-check CKV_DOCKER_7 --create-config .checkov.yaml writes
this file from the current options, and --show-config displays the
configuration actually applied. Avoid the check: key: when set, it limits
the scan to the listed rules only.
Exporting the results
Checkov writes several formats in a single pass: cli, json, sarif,
junitxml, csv and cyclonedx, among others. The behaviour of
--output-file-path changes depending on what you give it, and it was
measured: a list of paths, one per format, writes files; a single
path for several formats is treated as a folder, filled with
results_json.json and results_sarif.sarif.
# One file per format: console for cli, checkov.xml for JUnitcheckov -d infra -o cli -o junitxml --output-file-path console,checkov.xml
# One folder for several formatscheckov -d infra -o sarif -o json --output-file-path rapportsThe summary block of the JSON is enough to follow a project over time.
Its parsing_errors field deserves attention: a file that Checkov could
not read is not analysed, and does not count as a failure.
{ "passed": 26, "failed": 7, "skipped": 0, "parsing_errors": 0, "resource_count": 1, "checkov_version": "3.3.22"}Writing a custom rule
The built-in rules ignore your internal conventions: mandatory cost centre
tag, allowed instance types. Checkov accepts rules in YAML for an attribute
to check, and in Python for richer logic. Both are loaded with
--external-checks-dir.
In YAML: a mandatory tag
The YAML format describes a condition on an attribute. This one requires a
cost-center tag on every S3 bucket; on the starting Terraform file, it
fails on aws_s3_bucket.bad_bucket, which has none.
metadata: id: "CKV2_ACME_1" name: "Ensure S3 buckets carry a cost-center tag" category: "GENERAL_SECURITY"
definition: cond_type: "attribute" resource_types: - "aws_s3_bucket" attribute: "tags.cost-center" operator: "exists"checkov -d terraform --external-checks-dir policies --check CKV2_ACME_1In Python, and the __init__.py trap
A Python rule inherits from a base class and returns PASSED or FAILED.
This one refuses instance types outside a list. One detail decides
everything, measured on version 3.3.22: without an __init__.py file in
the folder, the rule is not loaded, and Checkov does not say so. The scan
then returns zero results, which is easily mistaken for a success.
from checkov.common.models.enums import CheckCategories, CheckResultfrom checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
class EC2InstanceTypeCheck(BaseResourceCheck): def __init__(self): super().__init__( name="Ensure EC2 instances use approved instance types", id="CKV_ACME_1", categories=[CheckCategories.GENERAL_SECURITY], supported_resources=["aws_instance"], )
def scan_resource_conf(self, conf): # Types allowed in your organisation autorises = ["t3.micro", "t3.small", "t3.medium"] instance_type = conf.get("instance_type", [None])[0] if instance_type in autorises: return CheckResult.PASSED return CheckResult.FAILED
check = EC2InstanceTypeCheck()touch checks/__init__.py # without it, the rule is silently ignoredcheckov -d terraform --external-checks-dir checks --check CKV_ACME_1# Passed checks: 1 (t3.micro); an m5.4xlarge instance gives FAILEDPrefix the identifiers with the name of the organisation (CKV_ACME_) to
avoid any collision with a future official rule. Test each rule on a
compliant file and a faulty file: a rule that never sees a failure may
never have been loaded.
Key points
- Checkov reads infrastructure code without deploying anything: 1361 rules in 3.3.22, of which 1144 for Terraform.
- You install it pinned:
pipx install "checkov==3.3.22"or the image pinned by digest. --compactremoves the code,--quietremoves the passes; the scan exits with code 1 at the first failure.- A failure is not a risk: you classify the results before fixing.
- Ordinary secrets go through, and
.inior.envfiles are only read with--enable-secret-scan-all-files. - Without an API key, no severity: you block with
--soft-failand--hard-fail-onon named rules. - A
.checkov.yamlis loaded automatically, and a Python rule without__init__.pyis silently ignored. - The evidence expected by SOCLE-INT-VER-2 is an IaC configuration analysis report: the SARIF or the JSON of the CI job.
Knowledge check
Check that you have the essentials of this lesson. The quiz only covers what has just been explained here, from Checkov results to custom rules.
Knowledge check
Check what you have learned with this interactive quiz
How it works
- The timer starts when you click Start
- Multiple choice, true/false and short answer questions
- You can move between questions
- Detailed results are shown at the end
Starts the quiz and the timer
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: Reading Vault secrets from Terraform without freezing them in state
A new full quiz with random questions
Work again only on the questions you missed
FAQ: frequently asked questions
The questions below cover the most common doubts about Checkov: comparison with Trivy, integration into CI, false positives and custom rules, with the same pinned version as the lesson.
CKV_AWS_24, CKV_K8S_16…).What it covers
Version 3.3.22 counts 1361 rule identifiers (checkov --list), of which 1144 for Terraform, 139 for CloudFormation, 119 for Kubernetes, 29 for Ansible and 28 for Dockerfiles.What it does not do
- It does not see the real state of the cloud, only the code.
- It does not look for the CVEs of an image's packages: that is the role of Trivy or Grype.
- Its secret detection misses ordinary passwords.
checkov -d terraform --compact --quiet
With pipx, pinned version
pipx install "checkov==3.3.22"
checkov --version
# 3.3.22
pipx isolates Checkov in its own Python environment, without touching the system.With Docker, image pinned by digest
docker run --rm -v "$PWD":/src -w /src \
bridgecrew/checkov:3.3.22@sha256:617c76e3f9b1f7907ebca9abb6b9d746844edcb48e9bd775e4692c69c1c6ac47 \
-d . --compact --quiet
Why pin
A scanner runs with access to the code and often to the CI secrets. Since the compromise of Trivy in 2026, it is treated like any dependency: exact version, and digest for an image. Homebrew installs the latest published version, which suits a workstation but not a reproducible CI.Basic commands
# A folder, readable output
checkov -d terraform --compact --quiet
# A single file
checkov -f terraform/main.tf
# Limit to the Terraform framework
checkov -d . --framework terraform
Two options not to confuse
| Option | Effect |
|---|---|
--compact |
Removes the code excerpt under each result |
--quiet |
Hides the passed rules |
What you get
On a file of four deliberately faulty resources, Checkov 3.3.22 returns 25 failures: the four targeted mistakes (CKV_AWS_20, CKV_AWS_24, CKV_AWS_17, CKV_AWS_79) and 21 good practices such as cross-region replication. You must classify before fixing. The database's plain-text password, however, is not flagged.Comparison
| Criterion | Checkov | Trivy |
|---|---|---|
| Vendor | Prisma Cloud (Palo Alto) | Aqua Security |
| IaC configurations | Core of the tool, 1361 rules | Yes, engine inherited from tfsec |
| CVEs of images and dependencies | Not in open source | Yes |
| SBOM | CycloneDX export of the results | Generation and rescan |
| Custom rules | Python or YAML | Rego |
When to use which
- Checkov when infrastructure as code is the main subject and you want to write your own rules simply.
- Trivy for a single tool covering images, dependencies, secrets and configuration.
- Both to cross-check: two engines do not miss the same things.
In the code, with a justification
resource "aws_s3_bucket" "site" {
# checkov:skip=CKV_AWS_20: static website, public by design
bucket = "site-vitrine"
acl = "public-read"
}
metadata:
annotations:
checkov.io/skip1: CKV_K8S_40=UID imposed by the test image
# checkov:skip=CKV_DOCKER_2: the health probe is carried by Kubernetes
The rule then moves to Skipped checks instead of a failure.On the command line
checkov -d . --skip-check CKV_AWS_20,CKV_AWS_21
Keep this form for the rules judged irrelevant everywhere: it leaves no trace in the code.On an existing project
--create-baseline records the current failures in .checkov.baseline, and --baseline then only reports the new ones.In YAML
metadata:
id: "CKV2_ACME_1"
name: "Ensure S3 buckets carry a cost-center tag"
category: "GENERAL_SECURITY"
definition:
cond_type: "attribute"
resource_types:
- "aws_s3_bucket"
attribute: "tags.cost-center"
operator: "exists"
checkov -d terraform --external-checks-dir policies --check CKV2_ACME_1
In Python
A class inherits fromBaseResourceCheck and returns CheckResult.PASSED or FAILED. The folder must contain an __init__.py: measured on version 3.3.22, without it the rule is not loaded and the scan returns no result, without an error.Good practice
Test each rule on a compliant file and a faulty file. A rule that never sees a failure may never have been loaded.Workflow
permissions: {}
jobs:
checkov:
runs-on: ubuntu-24.04
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: bridgecrewio/checkov-action@cc0e17d456ef1713bbfd963ebac19dc776775cb2 # v12.3128.0
with:
directory: infra/
output_format: cli,sarif
output_file_path: console,results.sarif
- if: always()
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
sarif_file: results.sarif
Good to know
- Version v12.3128.0 of the action runs Checkov 3.3.22, designated by tag and not by digest.
- This workflow passes actionlint, zizmor and poutine without a finding.
- The results appear in the Security tab, under Code scanning.
GitLab CI job
checkov:
stage: test
image:
name: bridgecrew/checkov:3.3.22@sha256:617c76e3f9b1f7907ebca9abb6b9d746844edcb48e9bd775e4692c69c1c6ac47
entrypoint: [""]
script:
- checkov -d infra -o cli -o junitxml --output-file-path console,checkov.xml --compact --quiet
artifacts:
when: always
reports:
junit: checkov.xml
Key points
entrypoint: [""]lets GitLab start its shell instead of the image'scheckovcommand.- With a list of paths,
--output-file-pathwrites one file per format; with a single path for several formats, it creates a folder. - The job fails at the first failure. To block only on serious rules:
--soft-fail --hard-fail-on CKV_AWS_20,CKV_AWS_24.
What was measured
On a Dockerfile that fails 7 rules,checkov --skip-check LOW keeps its 7 failures: without a Prisma Cloud API key, Checkov does not know the severity of its rules.What works without a key
| Option | Effect |
|---|---|
--soft-fail |
Full report, exit code 0 |
--soft-fail-on ID,ID |
These rules no longer block |
--hard-fail-on ID,ID |
With --soft-fail, only these rules block |
Recommended method
Start with a short list of serious rules in--hard-fail-on (public bucket, open SSH, privileged container), and lengthen it as the debt goes down.What it finds
Known formats (CKV_SECRET_2 for an AWS key, CKV_SECRET_13 for a private key) and high-entropy strings (CKV_SECRET_6, CKV_SECRET_19).What it misses, measured on version 3.3.22
- Human passwords:
SuperSecret123!in a Terraform file,secret123in a manifest,API_KEY=sk-secret-12345in a Dockerfile are not flagged. .iniand.envfiles: an AWS key incredentials.inigoes through, except with the following option.
checkov -d . --framework secrets --enable-secret-scan-all-files
Consequence
Checkov complements a dedicated detector such as gitleaks, run in pre-commit and in CI, it does not replace it.Example
# .checkov.yaml
directory:
- infra
framework:
- terraform
- kubernetes
skip-check:
- CKV_AWS_144 # cross-region replication: out of budget
compact: true
quiet: true
Useful commands
# Write the file from the current options
checkov -d . --skip-check CKV_AWS_144 --create-config .checkov.yaml
# See what is really applied, and where it comes from
checkov -d . --show-config
The trap
The file is loaded without any option: a.checkov.yaml forgotten in a repository can silently remove rules. Measured: two lines of skip-check take a Dockerfile from 7 to 5 failures. Also avoid the check: key, which limits the scan to the listed rules only.Where the volume comes from
On a Terraform file of four resources, 25 failures for four real mistakes; on a single Kubernetes Deployment, 22 failures. Part of them concern security (public bucket, privileged container), another part operational comfort (Multi-AZ, probes, resource limits).How to reduce the noise
| Situation | Answer |
|---|---|
| Rule wrong in your context | checkov:skip= with a justification |
| Rule irrelevant everywhere | skip-check in .checkov.yaml, with the decision as a comment |
| Old project | --create-baseline, then --baseline |
| File not read | Check parsing_errors in the JSON |
What not to do
Switching everything to--soft-fail without a --hard-fail-on list: the scan then blocks nothing any more, and nobody reads the report any more.