Reading & triaging findings
Checks, severity, guides.
A Checkov run does not hand you a to-do list. It hands you a lab report. Hundreds of lines, green and red, each one rule checked against one piece of infrastructure, most of them harmless and a few genuinely dangerous. A doctor reading your bloodwork does not panic at every value outside the reference range. She reads the whole page, separates the incidental flags from the ones that need treating this week, and notes what to watch. Doing that to scanner output is called triage.
Every finding comes with three things: a stable check ID (the rule's permanent name, like CKV_AWS_18), the resource that tripped it, and a file and line number. Most also print a Guide link to a page describing the fix, but a check with no guideline mapped to it prints no Guide line at all. The ID is the part you carry everywhere else, into skip comments, baselines and your CI (continuous integration) config. This lesson walks the whole loop. Read one finding properly, cut the noise, group by how often a rule fires and how much damage it could do, then close each row out as fixed or as a written-down skip with a reason attached.
Read a single finding
Learn one record before you drown in five hundred. The first line gives you the check ID, CKV_AWS_18, then a one-sentence name and either PASSED or FAILED. The lines below it name the resource that failed (aws_s3_bucket.logs, a storage bucket), the file and line range (/s3.tf:1-4), and the Guide URL. The prefix carries meaning. A CKV_ check looks at one resource on its own. A CKV2_ check is a graph check: it reasons about how resources connect to each other. Read the ID before you read the name. The name is a summary you will forget by lunchtime. The ID is the word you type into every suppression and baseline file you will ever write.
$ checkov -d . --compact --quiet
terraform scan results:Passed checks: 12, Failed checks: 3, Skipped checks: 0Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"FAILED for resource: aws_s3_bucket.logsFile: /s3.tf:1-4Guide: https://docs.prismacloud.io/.../s3_13Check: CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled"FAILED for resource: aws_s3_bucket.logsFile: /s3.tf:1-4
Narrow the noise
A first scan on a real repository throws hundreds of failures at you, so triage starts by making the list shorter. --quiet hides everything that passed. --compact drops the code snippets. --check keeps only the IDs you name, and --skip-check drops the ones you have already ruled on. The heavy lifting happens with -o json, which prints results as JSON (JavaScript Object Notation, a machine-readable format), piped into jq, a small command-line tool for slicing JSON apart. Group by check_id and the picture changes shape: "CKV_AWS_18 fires forty times, and all forty come out of one module." Sort by frequency and by how exposed the resource is, never by the order things scrolled past.
$ checkov -d . --framework terraform -o json \| jq -r '.results.failed_checks[].check_id' \| sort | uniq -c | sort -rn | head
40 CKV_AWS_1812 CKV_AWS_213 CKV_AWS_791 CKV2_AWS_6
Pull remediation from JSON
Most failed checks in the JSON output carry a guideline field, the same advice that sits behind the Guide link. Working through a batch of forty, you want that text on screen beside the resource name rather than opening one browser tab at a time. Where a check has no guideline mapped to it the field comes back null, and the filter prints the word null on that line instead of advice. Scope the run to a single framework before you write the filter. One framework gives you a single object with .results at the top. Ask for several and Checkov hands back a list of those objects instead, and your jq path quietly stops matching anything.
$ checkov -d . --framework terraform -o json \| jq -r '.results.failed_checks[]| select(.check_id == "CKV_AWS_18")| "\(.resource) \(.check_name)\n \(.guideline)"'
aws_s3_bucket.logs Ensure the S3 bucket has access logging enabledS3 bucket should have access logging enabled. Add aws_s3_bucket_logging...
From finding to fix
Once the list is ranked, close the loop. When the finding prints a Guide link, open it before you edit anything, because some checks want one named attribute set and will keep failing if you tighten something nearby instead. Often the check name plus the resource address tells you the fix outright. When a finding is real but cannot be fixed yet, write a documented suppression with a reason (cv-suppress). Deleting the failing line to turn the output green does not count as a fix. Someone reads that file in six months and believes it.
$ checkov -d . --check CKV_AWS_18,CKV_AWS_21 --compact --quiet
Passed checks: 0, Failed checks: 2, Skipped checks: 0Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"FAILED for resource: aws_s3_bucket.logs
CKV vs CKV2 graph checks
A house alarm that only complains when the back door is unlocked and the side gate is open at the same time is not reporting on a door. It is reporting on a combination. CKV2_ graph checks work the same way: an instance attached to a wide-open security group, a bucket wired to a public access control list (ACL, the rules saying who may read a stored object). So fixing the one resource named in the output may not clear the row. Read the Guide slowly on these, because the fix often lives in two files. Counting CKV2_ failures per module still pays, though; it points you at the templates whose wiring needs changing rather than one attribute.
$ checkov -d . --framework terraform -o json | jq -r '.results.failed_checks[] | select(.check_id | startswith("CKV2_")) | .check_id' | sort | uniq -c
8 CKV2_AWS_63 CKV2_AWS_12
Rank by exposure
How do you rank a list with no severity column to sort on? On exposure, or blast radius: how far the damage spreads if a stranger finds that row before you do. A CKV_AWS_20 finding about a publicly readable S3 bucket outranks a missing description tag every single time, whatever order they printed in. Count still matters, just second: those forty CKV_AWS_18 rows are one module edit, an afternoon rather than a sprint. One production database security group open to the whole internet beats fifty cosmetic findings.
If you are running this as a programme rather than a one-off cleanup, keep the JSON from every scan. Failed counts per check_id plotted across commits show whether remediation is landing or whether you are treading water while new code adds rows at the same rate. The metrics side of that is cv-program. This lesson is the part where you decide what gets fixed this sprint, what goes into a baseline, and what gets a skip.
Watch someone new to Checkov read a failing scan and you will see them scroll until their own resource appears. Wrong sort order. Forty CKV_AWS_18 rows caused by one shared module that forgot logging is one fix, not forty. A lone CKV2_AWS_6 on a public-facing bucket, sitting at the very bottom of the output, might be the only line that matters this week. Counting by check_id in JSON is how experienced engineers read a wall of red: count, cluster, rank by exposure, then open the Guide for the top three. Not for every line.
The skipped tally deserves a look on every scan too. A skip count that creeps up release after release means suppressions are piling on faster than fixes. SKIPPED with a ticket reference beside it is healthy; someone made that call on purpose and wrote it down. SKIPPED with an empty reason is debt hiding in plain sight. When the number jumps after a noisy release, run the grep audit from cv-suppress before you trust the rest of the report.
Export for dashboards
Once triage has named the top offenders, get the results out of your terminal. JSON feeds scripts and ticket automation. SARIF (Static Analysis Results Interchange Format, the shared file format code-scanning dashboards read) feeds the GitHub Security tab or Azure DevOps. One run can write both, with --output json --output sarif --output-file-path results. Archive the summary for each commit and cv-program turns those files into trend lines. Triage is the human step between scanner output and a ticket backlog. Skip it and every FAILED line looks equally urgent, which is the same as none of them being urgent.
$ checkov -d . --framework terraform -o json --output-file-path results$ jq '.results.failed_checks | length' results/results_json.json
37
When findings leave your team, send the four fields that travel well: check_id, resource, file_line and check_name. A raw JSON dump full of build-runner paths means nothing to a product manager, and it buries the one line you wanted read. The ID is the stable key that survives the trip through Slack, into JIRA, and back into a skip comment months later.
Try this
Do this: run a compact quiet scan, count the failures by check ID with jq, then re-run scoped to the two IDs at the top of that count. That is the whole triage loop in three commands. Keep --framework terraform on the JSON run, because a repo that also holds a Dockerfile hands jq a list and .results matches nothing. Group before you scroll, open one Guide, pick the fix that clears the most rows.
$ checkov -d . --compact --quiet$ checkov -d . --framework terraform -o json | jq -r '.results.failed_checks[].check_id' | sort | uniq -c | sort -nr | head$ checkov -d . --check CKV_AWS_18,CKV_AWS_21 --compact --quiet
Passed checks: 812, Failed checks: 37, Skipped checks: 014 CKV_AWS_189 CKV_AWS_214 CKV_AWS_792 CKV2_AWS_6Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"FAILED for resource: aws_s3_bucket.logs
Takeaway
Remember: triage is what turns a scanner into a backlog you can actually work. Rank by exposure and by how many rows one edit clears, because in open-source mode there is no severity column to sort on. Fix the module fanning out fourteen identical CKV_AWS_18 hits before you chase a one-off edge case, and never turn a finding green by deleting the line that failed.
When a finding is real but not fixable today, write the skip with a ticket behind it (cv-suppress), or park legacy bulk debt in a baseline (cv-baseline). Next up: point Checkov at the frameworks you actually own (cv-frameworks), so third-party Helm charts nobody on your team can edit never reach this worklist in the first place.
checkov -d . --compact --quiet reports 37 failed checks, but checkov -d . -o json | jq -r '.results.failed_checks[].check_id' prints nothing at all. What fixes the pipeline?