Skip to content

Custom checks

Zora offers a declarative way to create your own checks using the CustomCheck API, introduced in version 0.6.

Custom checks use the Common Expression Language (CEL) to declare the validation rules and are performed by the Marvin plugin, which should be enabled in your cluster scans.


Marvin is already a default plugin and enabled by default in cluster scans since Zora 0.5.0.

CustomCheck API

The example below demonstrates a custom check that requires the labels and to be present on Pods, Deployments and Services.


kind: CustomCheck
  name: mycheck
  message: "Required labels"
  severity: Low
  category: Custom
      - group: ""
        version: v1
        resource: pods
      - group: apps
        version: v1
        resource: deployments
      - group: ""
        version: v1
        resource: services
    - expression: >
        has(object.metadata.labels) &&
            req, req != label
      message: "Resource without required labels"

The spec.match.resources defines which resources are checked by the expressions defined in spec.validations.expression using Common Expression Language (CEL).

If an expression evaluates to false, the check fails, and a ClusterIssue is reported.

CEL Playground

To quickly test CEL expressions directly from your browser, check out CEL Playground.


The variables available in CEL expressions:

Variable Description
object The object being scanned.
params The parameter defined in spec.params field.

If the object matches a PodSpec, the following useful variables are available:

Variable Description
allContainers A list of all containers, including initContainers and ephemeralContainers.
podMeta The Pod metadata.
podSpec The Pod spec.

The following resources matches a PodSpec:

  • v1/pods
  • v1/replicationcontrollers
  • apps/v1/replicasets
  • apps/v1/deployments
  • apps/v1/statefulsets
  • apps/v1/daemonsets
  • batch/v1/jobs
  • batch/v1/cronjobs

Applying custom checks

Since you have a CustomCheck on a file, you can apply it with the following command.

kubectl apply -f check.yaml -n zora-system

Listing custom checks

Once created, list the custom checks to see if they are ready.

kubectl get customchecks -n zora-system
mycheck   Required labels   Low        True

The READY column indicates when the check has successfully compiled and is ready to be used in the next Marvin scan.

ClusterIssues reported by a custom check are labeled custom=true and can be filtered by the following command:

kubectl get clusterissues -l custom=true
NAME                             CLUSTER     ID        MESSAGE           SEVERITY   CATEGORY   AGE
mycluster-mycheck-4edd75cb85a4   mycluster   mycheck   Required labels   Low        Custom     25s


All Marvin checks are similar to the CustomCheck API. You can see them in the internal/builtins folder for examples.

Here are some examples of Marvin built-in checks expressions:

Marvin's checks and Zora's CustomCheck API are inspired in Kubernetes ValidatingAdmissionPolicy API, introduced in version 1.26 as an alpha feature. Below, the table of validation expression examples from Kubernetes documentation.

Expression Purpose
object.minReplicas <= object.replicas && object.replicas <= object.maxReplicas Validate that the three fields defining replicas are ordered appropriately
'Available' in object.stateCounts Validate that an entry with the 'Available' key exists in a map
(size(object.list1) == 0) != (size(object.list2) == 0) Validate that one of two lists is non-empty, but not both
!('MY_KEY' in object.map1) || object['MY_KEY'].matches('^[a-zA-Z]*$') Validate the value of a map for a specific key, if it is in the map
object.envars.filter(e, == 'MY_ENV').all(e, e.value.matches('^[a-zA-Z]*$') Validate the 'value' field of a listMap entry where key field 'name' is 'MY_ENV'
has(object.expired) && object.created + object.ttl < object.expired Validate that 'expired' date is after a 'create' date plus a 'ttl' duration'ok') Validate a 'health' string field has the prefix 'ok'
object.widgets.exists(w, w.key == 'x' && < 10) Validate that the 'foo' property of a listMap item with a key 'x' is less than 10
type(object) == string ? object == '100%' : object == 1000 Validate an int-or-string field for both the int and string cases Validate that an object's name has the prefix of another field value
object.set1.all(e, !(e in object.set2)) Validate that two listSets are disjoint
size(object.names) == size(object.details) && object.names.all(n, n in object.details) Validate the 'details' map is keyed by the items in the 'names' listSet
size(object.clusters.filter(c, == object.primary)) == 1 Validate that the 'primary' property has one and only one occurrence in the 'clusters' listMap