Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 41 additions & 6 deletions docs/docs/reference/policies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,39 @@ 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](./operator/contract#material-schema) 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](https://www.openpolicyagent.org/docs/latest/policy-language/#learning-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:
```yaml
...
spec:
type: SBOM_CYCLONEDX_JSON
path: my-script.rego
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:
```yaml
...
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.
```yaml
Expand Down Expand Up @@ -86,11 +110,22 @@ There are two ways to attach a policy to a contract:

* 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:

<CodeBlock language="yaml" title="cyclonedx-licenses.yaml" showLineNumbers>
{PolicyYAML}
</CodeBlock>
```yaml
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 `policy` attribute (1), a full policy can be embedded in the contract.
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:
Expand Down
33 changes: 17 additions & 16 deletions docs/examples/policies/sbom/cyclonedx-banned-licenses.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,20 @@ metadata:
annotations:
category: sbom
spec:
type: SBOM_CYCLONEDX_JSON
embedded: |
package main

import rego.v1

banned_licenses := ["GPL-2.0", "GPL-3.0"]

violations contains ref if {
some i
comp := input.components[i]
some j
license := comp.licenses[j].license
license.name == banned_licenses[_]
ref := sprintf("Forbidden license %v for %v (%v)", [license.name, comp.name, comp["bom-ref"]])
}
policies:
- kind: SBOM_CYCLONEDX_JSON
embedded: |
package main

import rego.v1

banned_licenses := ["GPL-2.0", "GPL-3.0"]

violations contains ref if {
some i
comp := input.components[i]
some j
license := comp.licenses[j].license
license.name == banned_licenses[_]
ref := sprintf("Forbidden license %v for %v (%v)", [license.name, comp.name, comp["bom-ref"]])
}
63 changes: 32 additions & 31 deletions docs/examples/policies/sbom/cyclonedx-banned-packages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,37 @@ metadata:
annotations:
category: sbom
spec:
type: SBOM_CYCLONEDX_JSON
embedded: |
package main
policies:
- kind: SBOM_CYCLONEDX_JSON
embedded: |
package main

import rego.v1

# It supports packages with version. When specified, requires it to be semver, and would also fail when version is lower
banned_packages := ["log4j@2.14.1"]

import rego.v1
# all versions
violations contains ref if {
some i
comp := input.components[i]
some j
banned := banned_packages[j]
nv := split(banned, "@")
not nv[1]
comp.name == nv[0]
ref := sprintf("Banned package: %v", [comp.name])
}

# It supports packages with version. When specified, requires it to be semver, and would also fail when version is lower
banned_packages := ["log4j@2.14.1"]

# all versions
violations contains ref if {
some i
comp := input.components[i]
some j
banned := banned_packages[j]
nv := split(banned, "@")
not nv[1]
comp.name == nv[0]
ref := sprintf("Banned package: %v", [comp.name])
}

# specific versions
violations contains ref if {
some i
comp := input.components[i]
some j
banned := banned_packages[j]
nv := split(banned, "@")
comp.name == nv[0]
result := semver.compare(comp.version, nv[1])
result <= 0
ref := sprintf("Banned package: %v %v", [comp.name, comp.version])
}
# specific versions
violations contains ref if {
some i
comp := input.components[i]
some j
banned := banned_packages[j]
nv := split(banned, "@")
comp.name == nv[0]
result := semver.compare(comp.version, nv[1])
result <= 0
ref := sprintf("Banned package: %v %v", [comp.name, comp.version])
}
41 changes: 21 additions & 20 deletions docs/examples/policies/sbom/cyclonedx-freshness.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,24 @@ metadata:
annotations:
category: sbom
spec:
type: SBOM_CYCLONEDX_JSON
embedded: |
package main

import rego.v1

limit := 30

nanosecs_per_second = (1000 * 1000) * 1000

nanosecs_per_day = ((24 * 60) * 60) * nanosecs_per_second

maximum_age = limit * nanosecs_per_day

violations contains msg if {
sbom_ns = time.parse_rfc3339_ns(input.metadata.timestamp)
exceeding = time.now_ns() - (sbom_ns + maximum_age)
exceeding > 0
msg := sprintf("SBOM created at: %s which is too old (freshness limit set to %d days)", [input.metadata.timestamp, limit])
}
policies:
- kind: SBOM_CYCLONEDX_JSON
embedded: |
package main

import rego.v1

limit := 30

nanosecs_per_second = (1000 * 1000) * 1000

nanosecs_per_day = ((24 * 60) * 60) * nanosecs_per_second

maximum_age = limit * nanosecs_per_day

violations contains msg if {
sbom_ns = time.parse_rfc3339_ns(input.metadata.timestamp)
exceeding = time.now_ns() - (sbom_ns + maximum_age)
exceeding > 0
msg := sprintf("SBOM created at: %s which is too old (freshness limit set to %d days)", [input.metadata.timestamp, limit])
}
35 changes: 18 additions & 17 deletions docs/examples/policies/sbom/cyclonedx-licenses.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ metadata:
annotations:
category: sbom
spec:
type: 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
}
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
}

43 changes: 22 additions & 21 deletions docs/examples/policies/sbom/cyclonedx-required-packages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,25 @@ metadata:
annotations:
category: sbom
spec:
type: SBOM_CYCLONEDX_JSON
embedded: |
package main

import rego.v1

required_packages := {"glibc", "libcrypto3"}

violations contains msg if {
count(all_matches) != count(required_packages)
missing := required_packages - all_matches
some i
msg := sprintf("missing package: %v", [missing[i]])
}

all_matches contains name if {
some i
comp := input.components[i]
comp.name == required_packages[_]
name := comp.name
}
policies:
- kind: SBOM_CYCLONEDX_JSON
embedded: |
package main

import rego.v1

required_packages := {"glibc", "libcrypto3"}

violations contains msg if {
count(all_matches) != count(required_packages)
missing := required_packages - all_matches
some i
msg := sprintf("missing package: %v", [missing[i]])
}

all_matches contains name if {
some i
comp := input.components[i]
comp.name == required_packages[_]
name := comp.name
}
39 changes: 20 additions & 19 deletions docs/examples/policies/sbom/sbom-present.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,26 @@ metadata:
annotations:
category: sbom
spec:
type: ATTESTATION
embedded: |
package main

# Verifies there is a SBOM material, even if not enforced by contract

import future.keywords.contains
import future.keywords.in

violations[msg] {
not has_sbom
msg := "missing SBOM material"
}

# Collect all material types
kinds contains kind {
some material in input.predicate.materials
kind := material.annotations["chainloop.material.type"]
}
policies:
- kind: ATTESTATION
embedded: |
package main

# Verifies there is a SBOM material, even if not enforced by contract

import future.keywords.contains
import future.keywords.in

violations[msg] {
not has_sbom
msg := "missing SBOM material"
}

# Collect all material types
kinds contains kind {
some material in input.predicate.materials
kind := material.annotations["chainloop.material.type"]
}

has_sbom {
values := ["SBOM_SPDX_JSON","SBOM_CYCLONEDX_JSON"]
Expand Down
33 changes: 17 additions & 16 deletions docs/examples/policies/sbom/spdx-sbom-syft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,20 @@ metadata:
annotations:
category: sbom
spec:
type: SBOM_SPDX_JSON
embedded: |
package main

import future.keywords.in

violations[msg] {
not made_with_syft

msg := "Not made with syft"
}

made_with_syft {
some creator in input.creationInfo.creators
contains(creator, "syft")
}
policies:
- kind: SBOM_SPDX_JSON
embedded: |
package main

import future.keywords.in

violations[msg] {
not made_with_syft

msg := "Not made with syft"
}

made_with_syft {
some creator in input.creationInfo.creators
contains(creator, "syft")
}