skip to content

In Checkov, what are the two ways to write a custom check, and when do you need the Python form?

level: juniorimportance: should knowfreq 48%

answer

  1. two authoring surfaces, one is not code
  2. data conditions versus a class you implement
  3. attribute and connection conditions
  4. metadata block plus definition block
  5. scan_resource_conf returns a CheckResult

basics

~20 s

Checkov custom checks come in two forms: a declarative YAML policy made of attribute or connection conditions, and a Python class implementing a scan method. YAML covers straightforward attribute comparisons; Python is for logic YAML operators cannot express.

solid answer

~50 s

Checkov loads custom checks from a directory you point it at, and they can be written two ways. The declarative form is a YAML file with a `metadata` block (name, id, category) and a `definition` block: a condition naming `resource_types`, an `attribute`, an `operator` such as `equals`, `regex_match` or `within`, and a `value` — and these compose with `and`/`or`. An approved-AMI allowlist or an allowed instance-type list is exactly that shape, so YAML is the right tool. The Python form subclasses a base check class, declares the same metadata in its constructor plus `supported_resources`, and implements `scan_resource_conf(conf)` returning `CheckResult.PASSED` or `CheckResult.FAILED`. Reach for Python when the decision needs real code: parsing an AMI name, comparing two attributes to each other, walking a nested block, or an allowlist you build at load time. YAML is the default because anyone on the team can read and review it.

go deeper

for a junior

Be ready to say that there are two forms — a declarative YAML policy and a Python class — and to sketch the YAML one: metadata with a name and id, then a condition naming a resource type, an attribute, an operator and a value.

for a middle

Explain the mechanics of both: what the constructor declares in the Python form, what scan_resource_conf returns, and how YAML conditions compose with and/or. Know that a connection condition exists and asserts a relationship between resources.

for a senior

Show the judgment about which form to use and why YAML is the default in a team setting: it reviews in a pull request, it does not need a Python reviewer, and it keeps the rule readable by the people it will block. Escalate to Python only for logic operators cannot express.

for a principal

Own the conventions around custom rules: an id namespace of your own, a name written for the blocked developer, a guideline link, and one rule per intent so exceptions and rollout can be granted per rule rather than per bundle.

## Why a custom check exists at all An infrastructure-as-code scanner ships a catalog of generic misconfiguration rules — encryption off, a wide-open security group, public storage. What it cannot ship is *your* organisation's rules: only these AMIs are approved, only these instance families and sizes may be launched, only these regions, every resource carries an owner tag. Those rules exist only once somebody writes them, and a custom check is where they get written. Checkov gives you two authoring surfaces for that, and choosing between them is the first decision. ## Form 1 — the declarative YAML policy A YAML custom policy is data, not code. It has two top-level blocks: ```yaml metadata: name: "EC2 instances must use an approved AMI" id: "CKV_ACME_1" category: "GENERAL_SECURITY" definition: cond_type: attribute resource_types: - aws_instance attribute: ami operator: within value: - ami-0aaa1111 - ami-0bbb2222 ``` `metadata` carries the identity of the rule — the id that will appear in every report, the human-readable name, the category. `definition` carries the condition. A condition of `cond_type: attribute` names the resource types it applies to, the attribute path to inspect, an operator (`equals`, `not_equals`, `exists`, `not_exists`, `contains`, `regex_match`, `within`, and comparison operators), and the value to compare against. Conditions nest under `and:` / `or:` lists, so "instance type is in this list AND the owner tag exists" is a single YAML file. There is a second condition type that is easy to miss and is the main reason to know the YAML form exists: `cond_type: connection`. Checkov builds a graph of the resources it parsed, so a connection condition can assert that a resource of one type is (or is not) connected to a resource of another type — an instance attached to a security group, a bucket attached to a policy. That is a cross-resource assertion, and it is not something you get from looking at one resource block in isolation. ## Form 2 — the Python check The Python form is a class: ```python class ApprovedAmi(BaseResourceCheck): def __init__(self): super().__init__(name="EC2 instances must use an approved AMI", id="CKV_ACME_1", categories=[CheckCategories.GENERAL_SECURITY], supported_resources=["aws_instance"]) def scan_resource_conf(self, conf): ... ``` The constructor declares the same identity metadata the YAML `metadata` block does, plus `supported_resources` — the resource types this check is invoked for. `scan_resource_conf` receives the parsed resource block and returns a `CheckResult`. Instantiating the class at module scope is what registers it. There is also a value-oriented base class where you only supply the attribute path to inspect and the expected value, which is a middle ground between the two forms. Choose Python when the rule needs behaviour a fixed operator set cannot express: comparing two attributes of the same resource to each other, deriving the allowlist from something computed at load time, string-parsing an image or AMI name, or walking a nested block whose shape varies. ## What both forms must carry Whichever form you pick, the rule is not just its logic. It carries an **id** — the string that shows up in reports, in dashboards, in baselines, and in whatever suppression mechanism your team uses. Pick a namespace of your own (`CKV_ACME_1`, not a number in the vendor's range) so a custom check never shadows a built-in one and so anyone reading a report can tell at a glance that this rule came from your team. It carries a **name** that has to make sense to the developer who is blocked by it at 5pm, and a **category**. Rules that also let you attach a guideline link should have one: a finding with a URL that explains what to do instead is a very different experience from a bare rule id. ## The practical default Start in YAML. It reviews well in a pull request, a non-Python engineer can read it, and the majority of organisational rules really are "this attribute must be one of these values". Escalate to Python only when you hit a wall — and when you do, expect the parsed configuration to be less convenient than it looks, because the scanner hands you its own parsed representation rather than the source text you wrote.

  • What can a Checkov YAML custom policy express that a single-resource attribute check cannot?
    A condition of `cond_type: connection` asserts a relationship between two resource types in the parsed graph — for example that an instance is connected to a security group, or that a bucket has a policy attached. Attribute conditions only ever look inside one resource block, so anything cross-resource needs the connection form (or Python that walks the graph).
  • Where does the id you declare on a custom check actually show up?
    Everywhere the finding is consumed: the console and JSON/SARIF output, any baseline file, dashboards, and the suppression mechanism your team uses to silence a specific rule. That is why you namespace it — an id that collides with a built-in makes reports ambiguous and lets one suppression silence two different rules.
  • Your rule needs to allow only certain instance types AND require an owner tag. One check or two?
    Two, in most cases. One id per intent means a developer sees exactly what they violated and a team can adopt one rule before the other. Combine them into one check only when neither half is meaningful alone — a single id that can fail for two unrelated reasons is hard to triage and hard to grant an exception to.

The YAML form is a filled-in form; the Python form is writing the clerk's job description. Most rules only need the form.

saying these in an interview costs you the question

  • Says custom checks must always be Python
  • Thinks the YAML form can only compare one attribute
  • Cannot name any metadata a check must declare
  • Reuses a vendor rule id for a custom check
  • Believes an attribute condition can see another resource

context