CoursesCheckov & IaC scanningInstall & first scan

Install & first scan

checkov -d and reading results.

Intermediate10 min · lesson 2 of 12

A spell-checker never runs your document. It reads the words on the page and flags the ones that break rules it already knows. Checkov does that to infrastructure code, meaning the Terraform files, templates and manifests that describe your buckets, servers and clusters and live in git alongside your application source. It reads Terraform, CloudFormation, Kubernetes manifests and their cousins, walks every resource it finds, and compares each one against a catalogue of built-in policies. Nothing gets deployed. No cloud credentials are touched. No plan is applied. You point it at a folder, it reads, and it tells you which resources failed which checks.

Here is the failure that makes people stop trusting scanners. Your pipeline goes red overnight. Forty new failures, none of them in code anyone edited, because the runner image quietly floated Checkov up a minor version and that release brought new checks along with it. A pinned, reproducible install is how you keep that fake incident off the on-call channel, and how you make today's scan comparable to yesterday's.

So this lesson stays short and practical. Install Checkov with pipx (a tool that installs Python command line programs into their own private environment) or run the official Docker image. Pin the version in CI (continuous integration, the automated build that runs on every push) so results stay reproducible. Then run your first command, checkov -d ., point it at a folder and read what comes back.

Getting Checkov installed

Checkov is a Python package, so the install question is really a Python packaging question. pipx is the tidiest answer on a laptop. It builds an isolated environment for Checkov, keeps its dependencies away from everything else on the machine, and still puts the checkov command on your PATH (the list of folders your shell searches when you type a command). Plain pip works fine if you are already inside a virtualenv. For build runners that should not be installing Python packages at all, there is an official Docker image. Whichever route you take, run the binary once and write the version down. The check catalogue grows with every release, so pinning that version in CI is what keeps two scans of the same code returning the same answer.

terminal
$ pipx install checkov
$ checkov --version
output
checkov, version 3.2.451

Your first scan with -d

The flag you will type most is -d, short for --directory. Hand it a path and Checkov walks the whole tree underneath it, finds every supported file, and scans the lot. Use . for the folder you are standing in. Use -f when you want a single file instead. You never have to announce what kind of files these are; Checkov works out the frameworks for itself from what it finds. For a first run, aim it at a directory holding real Terraform or real Kubernetes YAML (YAML is the indentation-sensitive text format Kubernetes config is written in) and let it go. Running the Docker image? You mount the repository into the container as a volume, then give -d the path as it exists inside the container, not the one on your laptop.

terminal
$ checkov -d .
output
_ _
___| |__ ___ ___| | _______ __
/ __| '_ \ / _ \/ __| |/ / _ \ \ / /
| (__| | | | __/ (__| < (_) \ V /
\___|_| |_|\___|\___|_|\_\___/ \_/
By Prisma Cloud | version: 3.2.451
terraform scan results:
Passed checks: 41, Failed checks: 3, 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:12-18
Guide: https://docs.prismacloud.io/...
What happens on checkov -d
1Point at folder
checkov -d . discovers IaC files
2Parse resources
reads Terraform/YAML statically
3Evaluate policies
each resource vs built-in checks
4Print pass/fail
per-check results + summary
No cloud calls and no state. Checkov only reads files on disk.

Reading the default output

Every result is one check against one resource, and it always has the same shape: a check ID such as CKV_AWS_21, a plain-English description of what the check wants, the verdict PASSED or FAILED, the address of the resource, the file with its line numbers, and a Guide link. The footer tallies passed, failed and skipped. When anything failed, Checkov exits with a non-zero status code, and that number is what lets it break a build. Two flags make the first read far easier. --compact drops the inlined code snippets. --quiet hides the PASSED lines so only the failures are left on screen.

terminal
$ checkov -d . --compact --quiet
output
Passed checks: 41, Failed checks: 3, Skipped checks: 0
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:12-18
Guide: https://docs.prismacloud.io/.../s3_14

Output formats and exit codes

Checkov can print in several shapes. CLI (command line interface) output is the one for humans. JSON (JavaScript Object Notation, the format machines parse) feeds your own scripts. SARIF (Static Analysis Results Interchange Format) is what GitHub code scanning eats, so picking it drops findings straight into the Security tab of a pull request. There is also JUnit for test-report viewers, plus CycloneDX and SPDX (Software Package Data Exchange) for inventory tooling. Exit code 1 means at least one check failed, and that is the number your pipeline should act on. --output-file-path writes the artefact next to the repo so dashboards and trend charts have something to read.

terminal
$ checkov -d . -o sarif --output-file-path results
$ echo exit=$?
output
Wrote SARIF output to results/results_sarif.sarif
exit=1

Docker and CI-friendly installs

On a build runner, pin the image tag or the pip version. A silent upgrade is exactly how new checks arrive at two in the morning on code nobody has opened. The official bridgecrew/checkov image takes the same flags as the local binary, so nothing you learned on your laptop goes to waste. Pre-commit hooks are a third place Checkov can live, handy for catching problems before a push, though the copy wired into your pipeline is the one that decides what merges.

terminal
$ docker run --rm -v "$PWD:/tf" bridgecrew/checkov:3.2.451 -d /tf --compact --quiet
output
Passed checks: 41, Failed checks: 3, Skipped checks: 0
Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
FAILED for resource: aws_s3_bucket.logs
A spotless scan usually means Checkov found nothing to read
Passed: 0, Failed: 0 is almost never good news. It normally means there were no parseable resources at all: wrong directory, resources that live in modules only referenced from here, or files that failed to parse. Run it again without --quiet and look for PASSED lines naming real resources before you believe the zero.

Pin versions in CI

New checks ship in ordinary Checkov releases, which is good for your security and awful for your Tuesday. An unpinned pip install checkov, or a floating bridgecrew/checkov:latest tag, can add failures overnight on code nobody has touched in months. It bites hardest after you have taken a baseline, because checks that did not exist when the baseline was recorded slide past your accepted debt and land in the build. So pin it: a fixed version in requirements.txt, pipx pin checkov on a laptop, an immutable tag in Docker. Then upgrade on purpose. Read the release notes, regenerate the baseline, and look at the diff of new findings before merging the version bump.

terminal
$ pipx install checkov==3.2.451
$ pipx pin checkov
$ checkov --version
output
checkov is pinned to 3.2.451
checkov, version 3.2.451

Confirm files were parsed

Before you trust a green summary, prove Checkov actually looked at something. Run it once with --compact and without --quiet, then read the PASSED lines. If they name resources you recognise, an aws_s3_bucket or a Deployment, the scan was real. After that, check parsing_errors in the JSON output. Any number above zero means files failed to parse and were dropped quietly, and nothing inside those files appears anywhere in the pass or fail counts you were about to celebrate.

terminal
$ checkov -d . -o json | jq 'if type=="array" then .[].summary else .summary end'
output
{
"passed": 812,
"failed": 37,
"skipped": 9,
"parsing_errors": 0,
"resource_count": 214,
"checkov_version": "3.2.451"
}

Pre-commit can run checkov -f over staged files, which gives whoever wrote the change a fast local loop. The gate that counts is still the one in CI, because that is the gate deciding what merges. Keep both on the same pinned release. Then a green laptop next to a red pipeline tells you something real about configuration drift, rather than sending you hunting for a difference that turns out to be two versions of the same scanner.

Before wiring Checkov into a shared pipeline, smoke-test it against a tiny fixture repo holding one resource you know fails and one you know passes. That single run proves three things at once: the binary is on PATH, the version is the one you asked for, and framework detection works on that runner image. GitHub-hosted runners start clean every job, so pip or pipx reinstalls each time unless you cache it. Pulling bridgecrew/checkov at a fixed tag is often faster, and it is more reproducible.

Learn the shape of a result line before you start tuning flags. One check ID, one resource, one verdict. And do not skip past the Guide link at the end of a failure. That link is the remediation page your developers will be reading for the next twenty minutes, so it is the first thing to point them at. The progression worth memorising: install once, scan with -d, read failures with --compact --quiet, then graduate to JSON and SARIF when CI enters the picture in cv-cicd.

Locked-down runners with no route to the public internet need a package index mirror, or the Docker image copied into an internal registry. The flags do not change; only the delivery does. Write the pinned version into the repo README beside the terraform required_version, so whoever bumps one is staring straight at the other.

Which format you pick is really a question about who reads the results. SARIF puts findings in the pull request Security tab, a small preview of what cv-cicd builds out properly. JUnit XML (Extensible Markup Language) slots into the test views in GitLab and Jenkins, so failures turn up where your team already looks. JSON feeds a dashboard you build yourself. The install and the scan are identical in every case; the format only changes what happens to the results downstream.

Try this

Pin a version, scan a directory with real infrastructure code in it, then read the JSON summary. Zero failures is not the goal here. The goal is proof: the binary runs, the path is right, and resource_count came back above zero.

terminal
$ pipx install checkov==3.2.451
$ pipx pin checkov
$ checkov --version
$ checkov -d . --compact --quiet
$ checkov -d . -o json | jq 'if type=="array" then .[].summary else .summary end'
output
checkov is pinned to 3.2.451
checkov, version 3.2.451
Passed checks: 41, Failed checks: 3, Skipped checks: 0
{
"passed": 41,
"failed": 3,
"parsing_errors": 0,
"resource_count": 12
}

Takeaway

Installing Checkov takes a minute. Installing it reproducibly is the habit worth building. Unpinned pip installs and :latest Docker tags add checks while you sleep and quietly break a baseline you believed was frozen. Pin the laptop and the pipeline to the same version, then upgrade deliberately, with the release notes open and a fresh baseline diff in front of you.

And if your very first scan comes back Passed: 0, Failed: 0, treat that as a parse or path problem until you have proof otherwise. Next you learn to read a single FAILED line properly in cv-results, and to decide whether that finding gets fixed, suppressed with a written reason, or parked in a baseline with the rest of the legacy debt.

Quick check
01You want Checkov to walk a whole repository and scan every infrastructure file it finds. Which flag do you reach for?
Incorrect — -f scans one named file. Handy in a pre-commit hook on staged files, no use for a whole tree.
Correct — -d, or --directory, walks the path you give it and works out the frameworks on its own.
Incorrect — -o picks the output format, json or sarif and the rest. It says nothing about what gets scanned.
Incorrect — --baseline compares a scan against a snapshot of findings you already accepted. It filters results rather than choosing the target.
02A scan finishes with three failed checks and returns exit code 1. Why is that the designed behaviour?
Incorrect — No. Files that fail to parse are counted separately under parsing_errors in the JSON summary. An ordinary failed check exits 1 by design, with nothing broken. HCL is HashiCorp Configuration Language, the syntax Terraform is written in.
Correct — The non-zero exit is the gate signal, so no wrapper script has to decide whether the build stops.
Incorrect — No. Exiting 1 on any failed check is the default behaviour, unless you ask for --soft-fail.
Incorrect — No. The output format changes what gets written, never the exit-code rules.
03Your first checkov -d . on a new repo prints Passed checks: 0, Failed checks: 0, Skipped checks: 0 and exits 0. What do you do next?
Incorrect — No. A zero-zero summary nearly always means Checkov found nothing to read, so you would be freezing an empty scan and gating on nothing.
Correct — PASSED lines prove real resources were evaluated, and parsing_errors above zero tells you files were dropped for failing to parse.
Incorrect — No. --compact only removes the inlined code snippets from the output. It cannot make results appear that were never produced.
Incorrect — No. Floating to :latest is the exact habit this lesson warns against, and a version bump does not repair an empty scan. It only changes which checks would have run.

Related