Skip to main content

Writing Chainloop policies

Chainloop policies consists of a YAML document with some metadata and a Rego script which holds the policy logic. You can check this document for a quick reference on policies. Read the following sections for a better understanding on how to write Rego code for your Chainloop policies.

Rego language

Rego language, from Open Policy Agent initiative, has become the de-facto standard for writing software supply chain policies. It's a rule-oriented language, suitable for non-programmers that want to communicate and enforce business and security requirements in their pipelines.

Chainloop Rego implementation

A typical Chainloop Rego policy looks like this:

policy-template.rego
package main

import rego.v1

# (1)
################################
# Common section do NOT change #
################################

# (2)
result := {
"skipped": skipped,
"violations": violations,
"skip_reason": skip_reason,
}

default skip_reason := ""

skip_reason := m if {
not valid_input
m := "invalid input"
}

default skipped := true

skipped := false if valid_input

########################################
# EO Common section, custom code below #
########################################

# Validates if the input is valid and can be understood by this policy (3)
valid_input if {
# insert code here
}

# If the input is valid, check for any policy violation here (4)
violations contains msg if {
valid_input
# insert code here
}

In the above template we can see there is a common section (1). Chainloop will look for the main rule result, if present. Older versions of Chainloop will only check for a violations rule. result object has essentially three fields:

  • skipped: whether the policy evaluation was skipped. This property would be set to true when the input, for whatever reason, cannot be evaluated (unexpected format, etc.). This property is useful to avoid false positives.
  • skip_reason: if the policy evaluation was skipped, this property will contain some informative explanation of why this policy wasn't evaluated.
  • violations: will hold the list of policy violations for a given input. Note that in this case, skipped will be set false, denoting that the input was evaluated against the policy, and it didn't pass.

Note that there is no need to modify the common section. Policy developers will only need to fill in the valid_input and violations rules:

  • valid_input would fail if some preconditions were not met, like the input format.

Example

Let's say we want to write a policy that checks our SBOM in CycloneDX format to match a specific version. A valid_input rule would look like this:

# It's a valid input if format is CycloneDX and has specVersion field that we can check later
valid_input if {
input.bomFormat == "CycloneDX"
input.specVersion
}

violations rule would return the list of policy violations, given that valid_input evaluates to true. If we wanted the CycloneDX report to be version 1.5:

violations contains msg if {
valid_input
input.specVersion != "1.5"
msg := sprintf("wrong CycloneDX version. Expected 1.5, but it was %s", [input.specVersion])
}

When evaluated against an attestation, The policy will generate an output similar to this:

{
"result": {
"skipped": false,
"violations": [
"wrong CycloneDX version. Expected 1.5, but it was 1.4"
]
}
}

Make sure you test your policies in the Rego Playground.

Chainloop policy

Once we have our Rego logic for our policy, we can create a Chainloop policy like this:

# cyclonedx-version.yaml
apiVersion: workflowcontract.chainloop.dev/v1
kind: Policy
metadata:
name: cyclonedx-version
spec:
policies:
- kind: SBOM_CYCLONEDX_JSON
name: cyclonedx-version.rego

and finally attach it to a contract:

schemaVersion: v1
policies:
materials:
- ref: file://cyclonedx-version.yaml

Check our policies reference for more information on how to attach policies to contracts.

Policy inputs

As we can see in the above examples, Rego policies will receive and inputs variable with all the payload to be evaluated. Chainloop will inject the evidence payload into that variable, for example a CycloneDX JSON document. This way, input.specVersion will denote the version of the CycloneDX document.

Additionally, Chainloop will inject the following fields:

  • input.args: the list of arguments passed to the policy from the contract or the policy group. Each argument becomes a field in the args input:

      // input.args
    {
    "severity": "MEDIUM",
    "foo": "bar",
    "licenses": ["AGPL-1.0-only", "AGPL-1.0-or-later"]
    }

    All arguments are passed as String type. So if you expect a numeric value you'll need to convert it with the to_number Rego builtin.

    Also, for convenience, comma-separated values are parsed and injected as arrays, as in the above example.

  • input.chainloop_metadata: This is an In-toto descriptor JSON representation of the evidence, which Chainloop generates and stores in the attestation. Developers can create policies that check for specific fields in this payload.

    A typical chainloop_metadata field will look like this:

    {
    "chainloop_metadata" : {
    "name" : "registry-1.docker.io/bitnamicharts/chainloop",
    "digest" : {
    "sha256" : "2af5745f843476bd781663eea84d3bd6bcd7a9cb9fcd54ce10cf48142bed2151"
    },
    "annotations" : {
    "chainloop.material.image.tag" : "2.0.21",
    "chainloop.material.name" : "material-1731339792439159000",
    "chainloop.material.signature" : "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQub2NpLmltYWdlLm1hbmlmZXN0LnYxK2pzb24iLCJjb25maWciOnsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnNpZ25hdHVyZSIsImRpZ2VzdCI6InNoYTI1Njo0NDEzNmZhMzU1YjM2NzhhMTE0NmFkMTZmN2U4NjQ5ZTk0ZmI0ZmMyMWZlNzdlODMxMGMwNjBmNjFjYWFmZjhhIiwic2l6ZSI6Mn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vam9zZStqc29uIiwiZGlnZXN0Ijoic2hhMjU2OmMwYWFlMzc5ODE4Zjk2NDQ5Nzk1OGMzNGM4NWZhYzU0MWFiZjgyZDlhMTUxZDBlZDg2MmM4ODE0OWE3ZjQxNmUiLCJzaXplIjo3OTQ3fV0sInN1YmplY3QiOnsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLm9jaS5pbWFnZS5tYW5pZmVzdC52MStqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjJhZjU3NDVmODQzNDc2YmQ3ODE2NjNlZWE4NGQzYmQ2YmNkN2E5Y2I5ZmNkNTRjZTEwY2Y0ODE0MmJlZDIxNTEiLCJzaXplIjo0ODV9LCJhbm5vdGF0aW9ucyI6eyJpby5jbmNmLm5vdGFyeS54NTA5Y2hhaW4udGh1bWJwcmludCNTMjU2IjoiW1wiODM0NDQ2Y2E1ZDk5Mzg2NTYxYjc0OWQ3MjdlNTI1ODU3ZjU3ZDlhNjY3NDRhZjYzZmMxY2I3YzcyNzYyZTA4ZlwiLFwiNzBhMzlkMWQ1Y2Y4ZDVhMWVkNzBiYmM1YWM1NjA5M2JhZDEzYzUyOTdiMzdkOTZiNTFkZDkxZThjYzZiM2IxNlwiLFwiYzQ0MWYzMzBiMzNhYzI2ODc0NWUzYzFkZTcwZjRiYTRjNzY1OTEzNGUwODQyNWY0N2JjOTQ2ZmZiNDgxMjc2NlwiXSIsIm9yZy5vcGVuY29udGFpbmVycy5pbWFnZS5jcmVhdGVkIjoiMjAyNC0xMS0wOFQxMTo0MzoxNVoifX0=",
    "chainloop.material.signature.digest" : "sha256:2e3aded29ba4266d4c682694c5b45585fa0a3d92bd1ea9bfd52448528c7eb6f5",
    "chainloop.material.signature.provider" : "notary",
    "chainloop.material.type" : "HELM_CHART"
    }
    }
    }

    Besides the basic information (name, digest) of the evidence, the annotations field will contain some useful metadata gathered by Chainloop during the attestation process. The example above corresponds to an OCI HELM_CHART evidence, for which Chainloop is able to detect the notary signature. You can write, for example, a policy that validates that your assets are properly signed, like this:

    violations contains msg if {
    not input.chainloop_metadata.annotations["chainloop.material.signature"]
    msg := sprintf("Signature not found for material '%s'", [input.chainloop_metadata.name])
    }

Policy engine constraints (Rego)

To ensure the policy engine work as pure and as fast as possible, we have deactivated some of the OPA built-in functions. The following functions are not allowed in the policy scripts:

  • opa.runtime
  • rego.parse_module
  • trace

Also http.send has been isolated so only requests to the following domains are allowed:

  • chainloop.dev
  • cisa.gov

This prevents unexpected behavior and potential remote exploits, particularly since these policies are evaluated client-side.