Write Custom Checkov Policies in Python

Checkov ships hundreds of built-in checks, but your organisation has rules no vendor knows about. This guide, part of security and compliance basics under Python IaC fundamentals and strategy, writes a custom Checkov policy in Python and runs it against your infrastructure alongside the built-ins covered in scanning Python IaC with Checkov.

Context

A built-in scan enforces community baselines, but internal rules — an approved AMI list, a mandatory logging bucket, a naming convention — need custom checks. Checkov lets you write these as Python classes that plug into the same scan, so one command enforces both community and in-house policy.

Custom check Custom check: Checkov policy with 4 facets. Checkov policy Resource which type Category e.g. logging scan() pass/fail Guideline fix link
A custom check declares the resource it targets and returns a pass or fail from scan_resource_conf.

Prerequisites

Prerequisites Prerequisites: layered from checkov down to JSON. checkov Python Terraform CDKTF JSON
Prerequisites: the building blocks this section assembles.
  • Python 3.9+ with checkov installed
  • A directory of Terraform/CDKTF-synthesized JSON to scan
  • A rule expressed concretely: which resource, which attribute, what value is compliant
# CLI: confirm Checkov and point it at a synthesized config
checkov --version && ls cdktf.out/stacks/*/cdk.tf.json

Implementation

Subclass BaseResourceCheck, declare the resource types it applies to, and implement scan_resource_conf returning PASSED or FAILED. This check requires S3 buckets to have access logging configured.

Custom scan Custom scan: synth config then load checks then scan resources then report CKV_ACME synth config load checks scan resources report CKV_ACME
Checkov loads your external checks and evaluates them alongside the built-ins.
# checks/s3_logging.py — require S3 access logging
# CLI: checkov -d . --external-checks-dir checks
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories

class S3AccessLogging(BaseResourceCheck):
    def __init__(self):
        super().__init__(
            name="Ensure S3 buckets enable access logging",
            id="CKV_ACME_1",
            categories=[CheckCategories.LOGGING],
            supported_resources=["aws_s3_bucket"])

    def scan_resource_conf(self, conf: dict) -> CheckResult:
        # conf holds the resource block; 'logging' must be present and non-empty.
        logging = conf.get("logging")
        return CheckResult.PASSED if logging else CheckResult.FAILED

check = S3AccessLogging()

Verification

Run Checkov with your external checks directory and confirm your custom id appears in the results, failing a non-compliant bucket.

Verification Verification: Test → Program → Mock/Cloud. Test Program Mock/Cloud invoke declare resolve assert
Verification: the test drives the program and asserts on resolved values.
# CLI: expect CKV_ACME_1 in the output, FAILED for an unlogged bucket
checkov -d cdktf.out --external-checks-dir checks --check CKV_ACME_1

Gotchas & Edge Cases

Gotchas & Edge Cases Gotchas & Edge Cases: Where it breaks with 4 facets. Where it breaks conf.get watch this boundary conf watch this boundary Edge Cases watch this boundary Terraform watch this boundary
Gotchas & Edge Cases: the boundaries where things break and what to check.

Attribute shape varies. Terraform wraps block attributes in lists; conf.get('logging') may be [{...}], so inspect the real conf before asserting on it.

Unique ids matter. Custom ids must not collide with built-ins; prefix them (CKV_ACME_*) so upgrades never clash.

Wire it into CI. A custom check only protects you if it runs on every change; add the --external-checks-dir flag to the same pipeline stage that runs the built-in scan.

FAQ

Can I write checks for CDKTF output?

Yes — Checkov scans the synthesized cdk.tf.json, so a check on aws_s3_bucket works whether the JSON came from HCL or CDKTF.

How do I suppress a check on one resource?

Add a checkov:skip comment or use a Checkov config file; document why, so suppressions stay auditable.

Is this a replacement for CrossGuard?

They overlap but differ: Checkov scans static config, while Pulumi CrossGuard evaluates Pulumi's planned graph at preview.