Skip to main content
Policies

Implement control gates and security checks in your attestations.

Daniel avatar
Written by Daniel
Updated over 3 weeks ago

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.

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:

Policy evaluations

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:

Writing custom policies

Writing policies in the Chainloop platform

In the Policies section, click "Create Policy". A new form will be presented where you can write your logic in Rego language. But don't panic, find all your answers in our Rego introduction.

Policies can be applied to any material. That why you need to specify a material type in the drop down. It will be used as a filter: the policy will be applied to every piece of evidence in the attestation that fits in that material type (a CycloneDX report, for example).

You can add as many "Specification items" as you need to support multiple material types in the same policy.

Hosting policies in your source repository

A policy can also 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) that will be used for referencing them.

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

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.

Finally, note that material policies are evaluated during chainloop attestation add commands, while attestation policies are evaluated continuously (in chainloop attestation add and chainloop attestation push commands).

Embedding or referencing policies in contracts

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

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

How to write a Chainloop policy in Rego

Check this how-to to know how you can write Chainloop policies in Rego language.

How to enforce policies

Refer to this guide on how to enforce policies in your CI

Policy Groups

This feature allow operators to group related policies into one single entity that can be reused across the organization. With Policy Groups, materials and policies can be enforced in Chainloop contracts with little or no effort.

For example, they might want to create a "SBOM quality" group with some SBOM-related policies. The policy groups can be defined this way:

# sbom-quality.yaml
apiVersion: workflowcontract.chainloop.dev/v1
kind: PolicyGroup
metadata:
name: sbom-quality
description: This policy group applies a number of SBOM-related policies
annotations:
category: SBOM
spec:
inputs:
- name: bannedLicenses
description: comma separated list of licenses to ban
required: true
- name: bannedComponents
description: comma separated list of components to ban
required: true
policies:
materials:
- name: sbom
type: SBOM_CYCLONEDX_JSON
policies:
- ref: sbom-banned-licenses
with:
licenses: {{ inputs.bannedLicenses }}
- ref: sbom-banned-components
with:
components: {{ inputs.bannedComponents }}

Using Policy Groups

This policy group could be applied to any contract:

schemaVersion: v1
materials: []
policyGroups:
- ref: file://groups/sbom-quality-group.yaml
with:
bannedComponents: [email protected]
bannedLicenses: AGPL-1.0-only, AGPL-1.0-or-later, AGPL-3.0-only, AGPL-3.0-or-later

As we introduced earlier, policy groups define both materials and policies applied to them. Once they are included to a contract, they become part of the contract. From this point of view, they can be seen as subcontracts, as they can also be used to enforce materials to be present in the attestation. For example, after applying the above contract, doing an attestation push would fail until the required material is provided:

$ chainloop att init --workflow mywf --project myproject
INF Attestation initialized! now you can check its status or add materials to it
┌────────────────┬──────────────────────────────────────┐
│ Initialized At │ 30 Dec 24 12:26 UTC │
├────────────────┼──────────────────────────────────────┤
│ Attestation ID │ a52f082c-ca4a-4952-bb07-fa9117c04a90 │
│ Organization │ my-org │
│ Name │ mywf │
│ Project. │ myproject │
│ Version │ v0.148.0 (prerelease) │
│ Contract │ myproject-mywf (revision 76) │
└────────────────┴──────────────────────────────────────┘
┌────────────────────────────────┐
│ Materials │
├──────────┬─────────────────────┤
│ Name │ sbom │
│ Type │ SBOM_CYCLONEDX_JSON │
│ Set │ No │
│ Required │ Yes │
└──────────┴─────────────────────┘

$ chainloop att push
ERR some materials have not been crafted yet: sbom
exit status 1

Policy group parameters

In the same way as policies, groups can accept arguments, which are specified in the inputs section. Then those inputs can be passed down to policies using interpolation.

In the example above, bannedComponents input parameter (which is mandatory) is passed to the underlying policy with the expression {{ inputs.bannedComponents }}

- ref: sbom-banned-components
with:
components: {{ inputs.bannedComponents }}

Using placeholders in material names

In the previous example, our policy group enforces a sbom material. But what if our contract requires multiple SBOMs (because we are building several images in the same pipeline, for example)? By using parameters and placeholders in material names, we can add as many instances of the same policy group as we need:

apiVersion: workflowcontract.chainloop.dev/v1
kind: PolicyGroup
metadata:
name: sbom-quality
spec:
inputs:
- name: container_name
description: name of the enforced material
required: true
policies:
materials:
- name: "{{ inputs.container_name }}" # This will materialize when doing an `attestation init`
type: SBOM_CYCLONEDX_JSON

In our contract:

schemaVersion: v1
materials: []
policyGroups:
- ref: file://groups/sbom-quality-group.yaml
with:
container_name: "sbom-from-image-1"
- ref: file://groups/sbom-quality-group.yaml
with:
container_name: "sbom-from-image-2"

And finally, in our attestation, we can see the new configuration applied:

$ chainloop att init --workflow mywf --project myproject
INF Attestation initialized! now you can check its status or add materials to it
┌────────────────┬──────────────────────────────────────┐
│ Initialized At │ 30 Dec 24 13:21 UTC │
├────────────────┼──────────────────────────────────────┤
│ Attestation ID │ 0837859e-0abf-414e-b205-d455da6dfe7e │
│ Organization. │ my-org │
│ Name │ mywf │
│ Project. │ myproject │
│ Version. │ v0.148.0 (prerelease) │
│ Contract. │ myproject-mywf (revision 77) │
└────────────────┴──────────────────────────────────────┘
┌────────────────────────────────┐
│ Materials │
├──────────┬─────────────────────┤
│ Name │ sbom-from-image-1 │
│ Type │ SBOM_CYCLONEDX_JSON │
│ Set │ No │
│ Required │ Yes │
├──────────┼─────────────────────┤
│ Name. │ sbom-from-image-2 │
│ Type │ SBOM_CYCLONEDX_JSON │
│ Set │ No │
│ Required │ Yes │
└──────────┴─────────────────────┘
Did this answer your question?