Overview

Policies are rules evaluated against materials and/or the whole attestation document. They represent acceptance criteria for the system to ingest those materials. Even in failure cases (when there are policy violations), attestations are still sent to Chainloop but marked as “not compliant.” This way, Chainloop keeps track of everything happening in the system, even if it’s unacceptable. Policies are written in Rego language.

Policies are also a fundamental piece in Chainloop’s compliance platform, as they are directly related to framework requirements. With the assistance of SecOps and developers, Requirements are matched to specific logic in Policies (programmed) written in the OPA Rego language. Rego is a rule engine and language used to automate the analysis of different reports and inputs to the system.

Policies can also be grouped into Policy Groups to facilitate reuse across different products. For instance, a SAST (static application security testing) policy group can consist of a “no-vulnerabilities” policy, a “CWE” policy for common weaknesses, and a “secrets detection” policy.

Policy types

Policies can be of two types:

  • Built-in: policies that are part of the Chainloop platform
  • Custom: policies that are created and stored in the Chainloop platform

Built-in policies

Chainloop provides a curated set of policies tailored to common compliance controls, like:

  • SBOM sanity checks,
  • Artifact signature verification,
  • Licenses and component versions ban policies
  • SAST, linters result checks, code quality and coverage
  • CVE scans,
  • etc.

They can be found in the “Policies” section in Chainloop platform:

If none of the built-in policies fit your needs, you can create your own, more on that later.

Custom policies

Custom policies are policies created by the user that can be stored in the Chainloop platform or in a local or remote file.

Using Policies

Attaching policies to a contract

Policies are attached to a contract via the policies section. Policies can be applied to any material, but also to the attestation statement as a whole.

Option 1 - By reference

You can reference policies via three methods:

  • If it’s a policy stored in the Chainloop platform (either built-in and custom), you can reference it by name
  • If it’s a custom policy, you can reference it by URL or by path
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: sbom-banned-licenses # (1) built-in policy
       # or optionally with the digest appended, see integrity checks below
       # - ref: sbom-banned-licenses@sha256:5b40425cb7bcba16ac47e3d8a8d3af7288afeeb632096994e741decedd5d38b3
       with:
         licenses: "AGPL-10, AGPL-3.0"
  attestation: # policies applied to the whole attestation
    - ref: https://my-org.chainloop.dev/policies/my-custom-remotepolicy.yaml # (2) custom remote policy

Here we can see that:

  • (1) is a built-in policy referenced by name. Since that policy is compatible with SBOM_CYCLONEDX_JSON, only SBOM materials (sbom and another-sbom in this case) will be evaluated against it.

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

    policies:
      materials:
        - ref: sbom-banned-licenses # (1)
          selector:
            name: sbom
    

    Here, we are making explicit that only sbom material must be evaluated by the sbom-banned-licenses policy.

  • (2) the attestation in-toto statement as a whole will be evaluated against the remote policy my-custom-remotepolicy.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.

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 policies stored in the Chainloop platform, you can find the sha256 hash in the policy details page:

Option 2 - Embedded

As an alternative to referencing policies, you can also embed them in your contract. This is useful if you want to ensure that the policy source cannot be changed, as it’s stored and versioned within the contract.

  policies:
    materials:
      - embedded:
          apiVersion: workflowcontract.chainloop.dev/v1
          kind: Policy
          metadata:
            name: cve-policy
          spec:
            policies:
            - kind: SBOM_CYCLONEDX_JSON
              path: cves-cyclonedx.rego

Understanding policy evaluation results

Once configured, Policies are evaluated against inputs (inputs, materials, and artifacts, will be explained in the following sections). Evaluation results will include:

  • Overall result: success, failure, skipped
  • Policy violations: if the evaluation failed, what were the failures. For example: “component x, version y has critical vulnerabilities”
  • Skip reason: if the policy couldn’t be evaluated, what’s the reason (input might have the wrong format, for example)

The results of policy evaluations are stored in Chainloop on every attestation and can be queried through the user interface:

Configuring Enforcement

Security and Compliance teams can set policy evaluation strategies to ENFORCED by default in their organization settings.

This means that during any attestation process that contains policy violations, the CLI will return with an error code in addition to recording the attestation result and the fact that the pipeline was blocked.

But I can hear what you are saying. Breaking pipelines might not be a good idea! We hear you. That’s why developers can provide the flag —exception-bypass-policy-check as an exception path when unblocking their pipelines.

This will make their CI pipeline run as expected, but the exception has been recorded and exposed to the compliance team for verification.

Writing policies

Writing a policy in Chainloop usually involves

1

Write the Chainloop policy in a YAML document

2

Write the associated rego policy script

1 - Chainloop Policy YAML

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
      
      # Global result object
      result := {
        "skipped": skipped,
        "violations": violations,
        "skip_reason": skip_reason,
      }
  
      default skip_reason := ""
      
      skip_reason := m if {
        not valid_input
        m := "the file content is not recognized"
      }
      
      default skipped := true
      
      skipped := false if valid_input
      
      valid_input if {
        # expect at least 1 component in the SBOM
        count(input.components) > 0
      }
  
      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 explicitly set in the contract, through selectors.
  • they have a policy script that it’s evaluated against the material (in this case a CycloneDX SBOM report). Currently, only Rego language is 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.

Policy arguments

Policies may accept arguments to customize its behavior. If defined, the inputs section, will be used by Chainloop to know with inputs arguments are supported by the policy

For example, this policy 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:
  inputs: #(1)
    - name: threshold
      description: quality threshold
      required: true
  policies:
    - kind: SBOM_CYCLONEDX_JSON
      embedded: |
        package main

        import rego.v1

        result := {
          "skipped": false,
          "violations": violations,
        }

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

        violations contains msg if {
          input.score < threshold
          msg := sprintf("quality threshold not met %d < %d", [input.score, threshold])
        }
  • (1) the input section tells Chainloop which parameters should be expected. If missing, the argument will be ignored (an no value will be passed to the policy)
  • (2) input parametes are available in the input.args rego input field.

The above example 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

2 - Write the associated policy script

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.

Using Chainloop Template

Chainloop expects the rego scripts to expose a predefined set of rules so a good starting point is to use the following template:

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.

Writing the policy logic

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.

Attaching it to the policy YAML spec

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

Give it a Try

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.

Configuring 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.