Overview
Checkov's 1000+ built-in checks cover common misconfigurations, but every organization has unique standards. Custom checks let you enforce company-specific naming conventions, tagging requirements, resource configurations, and security baselines.
How It Works
YAML Custom Check (Simple Attribute Validation)
# custom-checks/require-encryption.yaml
metadata:
id: "CKV2_CUSTOM_1"
name: "Ensure all EBS volumes use customer-managed KMS keys"
category: "ENCRYPTION"
guideline: "https://wiki.company.com/security/encryption-standard"
definition:
and:
- cond_type: "attribute"
resource_types:
- "aws_ebs_volume"
attribute: "kms_key_id"
operator: "exists"
- cond_type: "attribute"
resource_types:
- "aws_ebs_volume"
attribute: "encrypted"
operator: "equals"
value: truePython Custom Check (Complex Logic)
# custom-checks/require_tags.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories
class RequireMandatoryTags(BaseResourceCheck):
def __init__(self):
name = "Ensure all resources have mandatory tags"
id = "CKV2_CUSTOM_2"
supported_resources = [
"aws_instance", "aws_s3_bucket", "aws_rds_instance",
"aws_lambda_function", "aws_ecs_service",
]
categories = [CheckCategories.GENERAL_SECURITY]
super().__init__(name=name, id=id, categories=categories,
supported_resources=supported_resources)
def scan_resource_conf(self, conf):
required_tags = ["Environment", "Team", "CostCenter"]
tags = conf.get("tags", [{}])
if isinstance(tags, list):
tags = tags[0] if tags else {}
for tag in required_tags:
if tag not in tags:
return CheckResult.FAILED
return CheckResult.PASSED
check = RequireMandatoryTags()Naming Convention Check
# custom-checks/naming_convention.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckResult, CheckCategories
import re
class ResourceNamingConvention(BaseResourceCheck):
def __init__(self):
name = "Ensure resources follow naming convention: {env}-{service}-{resource}"
id = "CKV2_CUSTOM_3"
supported_resources = ["aws_s3_bucket", "aws_sqs_queue", "aws_sns_topic"]
categories = [CheckCategories.CONVENTION]
super().__init__(name=name, id=id, categories=categories,
supported_resources=supported_resources)
def scan_resource_conf(self, conf):
pattern = r'^(dev|staging|prod)-[a-z]+-[a-z]+$'
name_fields = ["bucket", "name"]
for field in name_fields:
values = conf.get(field, [])
if values and isinstance(values, list) and values[0]:
if re.match(pattern, str(values[0])):
return CheckResult.PASSED
return CheckResult.FAILED
check = ResourceNamingConvention()Running Custom Checks
# Specify custom checks directory
checkov -d ./terraform/ --external-checks-dir ./custom-checks/
# Combine with built-in checks
checkov -d ./terraform/ --external-checks-dir ./custom-checks/ --check CKV_AWS_18,CKV2_CUSTOM_1
Best Practices
- -Use YAML checks for simple attribute validation (faster to write, easier to maintain)
- -Use Python checks for complex logic (regex, cross-resource relationships)
- -Follow Checkov ID convention: CKV2_CUSTOM_{number}
- -Include guideline URLs pointing to internal documentation
- -Test custom checks against known-good and known-bad Terraform files
- -Version custom checks alongside IaC code in the same repository
Common Mistakes
- -Reimplementing built-in checks as custom checks (check the registry first)
- -Not testing custom checks (they can have false positives/negatives)
- -Hardcoding values that should be configurable (use environment variables)
- -Writing checks that are too specific to one project (make them reusable)