Skip to main content

Policies

Starting with Chainloop 0.93.8, operators can attach policies to contracts. These policies will be evaluated against the different materials and the statement metadata, if required. The result of the evaluation is informed as a list of possible violations and added to the attestation statement before signing and sending it to Chainloop.

Currently, policy violations won't block attestation push commands, but instead, we chose to include them in the attestation so that they can be used for building server side control gates.

Policy specification

A policy can be defined in a YAML document, like this:

cyclonedx-licenses.yaml
apiVersion: workflowcontract.chainloop.dev/v1
kind: Policy
metadata:
name: cyclonedx-licenses
description: Checks for components without licenses
annotations:
category: sbom
spec:
policies:
- kind: SBOM_CYCLONEDX_JSON
embedded: |
package main

import rego.v1

violations contains msg if {
count(without_license) > 0
msg := sprintf("Missing licenses for %s", [components_str])
}

components_str := concat(", ", [comp.purl | some comp in without_license])

without_license contains comp if {
some comp in input.components
not comp.licenses
}

In this particular example, we see:

  • policies have a name (cyclonedx-licenses)
  • they can be optionally applied to a specific type of material (check the documentation for the supported types). If no type is specified, a material name will need to be provided explicitly in the contract.
  • they have a policy script that it's evaluated against the material (in this case a CycloneDX SBOM report). Currently, only Rego policies are supported.
  • there can be multiple scripts, each associated with a different material type.

Policy scripts could also be specified in a detached form:

...
spec:
policies:
- kind: SBOM_CYCLONEDX_JSON
path: my-script.rego

Supporting multiple material types

Policies can accept multiple material types. This is specially useful when a material can be specified in multiple format types, but from the user perspective, we still want to maintain one single policy.

For example, this policy would check for vulnerabilities in SARIF, CycloneDX and CSAF formats:

...
apiVersion: workflowcontract.chainloop.dev/v1
kind: Policy
metadata:
name: cve-policy
spec:
policies:
- kind: SBOM_CYCLONEDX_JSON
path: cves-cyclonedx.rego
- kind: CSAF_SECURITY_ADVISORY
path: cves-csaf-sa.rego
- kind: SARIF
path: cves-sarif.rego

In these cases, Chainloop will choose the right script to execute, but externally it would be seen as a single policy. If more than one path is executed (because they might have the same kind), the evaluation result will be the sum of all evaluations.

Applying policies to contracts

When defining a contract, a new policies section can be specified. Policies can be applied to any material, but also to the attestation statement as a whole.

schemaVersion: v1
materials:
- name: sbom
type: SBOM_CYCLONEDX_JSON
- name: another-sbom
type: SBOM_CYCLONEDX_JSON
- name: my-image
type: CONTAINER_IMAGE
policies:
materials: # policies applied to materials
- ref: file://cyclonedx-licenses.yaml # (1)
# or optionally with the digest appended, see integrity checks below
# - ref: file://cyclonedx-licenses.yaml@sha256:5b40425cb7bcba16ac47e3d8a8d3af7288afeeb632096994e741decedd5d38b3
attestation: # policies applied to the whole attestation
- ref: https://raw.githubusercontent.com/chainloop-dev/chainloop/main/docs/examples/policies/chainloop-commit.yaml # (2)

Here we can see that:

  • (1) materials will be validated against cyclonedx-licenses.yaml policy. But, since that policy has a type property set to SBOM_CYCLONEDX_JSON, only SBOM materials (sbom and another-sbom in this case) will be evaluated.

    If we wanted to only evaluate the policy against the sbom material, and skip the other, we should filter them by name:

    policies:
    materials:
    - ref: file://cyclonedx-licenses.yaml
    selector: # (3)
    name: sbom

    Here, in (3), we are making explicit that only sbom material must be evaluated by the cyclonedx-licenses.yaml policy.

  • (2) the attestation in-toto statement as a whole will be evaluated against the remote policy chainloop-commit.yaml, which has a type property set to ATTESTATION. This brings the opportunity to validate global attestation properties, like annotations, the presence of a material, etc. You can see this policy and other examples in the examples folder.

Finally, note that material policies are evaluated during chainloop attestation add commands, while attestation policies are evaluated in chainloop attestation push command.

Embedding or referencing policies

There are two ways to attach a policy to a contract:

  • By referencing it, as it can be seen in the examples above. ref property admits a local file:// (filesystem) or remote reference https://. For example:

    policies:
    materials:
    - ref: file://cyclonedx-licenses.yaml # local reference

    and

    policies:
    materials:
    - ref: https://raw.githubusercontent.com/chainloop-dev/chainloop/main/docs/examples/policies/sbom/cyclonedx-banned-licenses.yaml

    are both equivalent. The advantage of having remote policies is that they can be easily reused, allowing organizations to create policy catalogs.

  • If preferred, authors could create self-contained contracts embedding policy specifications. The main advantage of this method is that it ensures that the policy source cannot be changed, as it's stored and versioned within the contract:

      policies:
    materials:
    - embedded: # (1)
    # Put full policy spec here
    apiVersion: workflowcontract.chainloop.dev/v1
    kind: Policy
    metadata:
    name: cve-policy
    spec:
    policies:
    - kind: SBOM_CYCLONEDX_JSON
    path: cves-cyclonedx.rego

In the example above, we can see that, when referenced by the embedded attribute (1), a full policy can be embedded in the contract.

Policy arguments

Policies may accept arguments to customize its behaviour. See this policy that matches a "quality" score against a "threshold" argument:

# quality.yaml
apiVersion: workflowcontract.chainloop.dev/v1
kind: Policy
metadata:
name: quality
description: Checks for components without licenses
annotations:
category: sbom
spec:
type: SBOM_CYCLONEDX_JSON
embedded: |
package main

import rego.v1

default threshold := 5
threshold := to_number(input.args.threshold) # (1)

violations contains msg if {
input.score < threshold
msg := sprintf("quality threshold not met %d < %d", [input.score, threshold])
}

It can be instantiated with a custom threshold parameter, by adding a with property in the policy attachment in the contract:

policies:
materials:
- ref: file://quality.yaml
with:
threshold: 6 (1)

(1) This is interpreted as a string, that's why we need to add to_number in the policy script

Integrity Checks

Optionally, you can append the sha256 hash of the policy file content to your policy attachment reference. By doing so, the policy engine will make sure the resolved policy matches the expected hash in the contract reference.

For example

policies:
materials:
# append digest to optionally check the integrity of the policy file during evaluation
- ref: file://cyclonedx-banned-licenses.yaml@sha256:5b40425cb7bcba16ac47e3d8a8d3af7288afeeb632096994e741decedd5d38b3
# It also works for http references
- ref: https://raw.githubusercontent.com/chainloop-dev/chainloop/main/docs/examples/policies/sbom/cyclonedx-banned-licenses.yaml@sha256:5b40425cb7bcba16ac47e3d8a8d3af7288afeeb632096994e741decedd5d38b3

Policy scripts format (REGO)

Currently, policy scripts are assumed to be written in Rego language. Other policy engines might be implemented in the future. The only requirement of the policy is the existence of one or multiple violations rules, which evaluate to a set of violation messages. For example, this policy script:

package main

import rego.v1

violations contains msg if {
not is_approved

msg:= "Container image is not approved"
}

is_approved if {
some material in input.predicate.materials
material.annotations["chainloop.material.type"] == "CONTAINER_IMAGE"

input.predicate.annotations.approval == "true"
}

when evaluated against an attestation, will generate the following output if the expected annotation is not present:

{
"violations": [
"Container image is not approved"
]
}

Make sure you test your policies in https://play.openpolicyagent.org/, since you might get different results when using Rego V1 syntax, as there are some breaking changes.