Skip to content

Commit 3ca0d61

Browse files
authored
feat(policies): document policies (#1106)
Signed-off-by: Jose I. Paris <[email protected]>
1 parent 5dfd3a4 commit 3ca0d61

File tree

9 files changed

+412
-0
lines changed

9 files changed

+412
-0
lines changed

docs/docs/reference/policies.md

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
---
2+
title: Policies
3+
---
4+
5+
Starting with Chainloop [0.93.8](https://github.com/chainloop-dev/chainloop/releases/tag/v0.93.8), operators can attach policies to contracts.
6+
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
7+
before signing and sending it to Chainloop.
8+
9+
Currently, policy violations won't block `attestation push` commands, but instead, we chose to include them in the attestation so that they can
10+
be used for building server side control gates.
11+
12+
### Policy specification
13+
A policy can be defined in a YAML document, like this:
14+
```yaml
15+
# cyclonedx-licenses.yaml
16+
apiVersion: workflowcontract.chainloop.dev/v1
17+
kind: Policy
18+
metadata:
19+
name: cyclonedx-licenses # (1)
20+
spec:
21+
type: SBOM_CYCLONEDX_JSON # (2)
22+
embedded: | # (3)
23+
package main
24+
25+
deny[msg] {
26+
count(without_license) > 0
27+
msg := "SBOM has components without licenses"
28+
}
29+
30+
without_license = {comp.purl |
31+
some i
32+
comp := input.components[i]
33+
not comp.licenses
34+
}
35+
```
36+
In this particular example, we see:
37+
* (1) policies have a name
38+
* (2) 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.
39+
* (3) 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.
40+
41+
Policy scripts could also be specified in a detached form:
42+
```yaml
43+
...
44+
spec:
45+
type: SBOM_CYCLONEDX_JSON
46+
path: my-script.rego
47+
```
48+
49+
### Applying policies to contracts
50+
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.
51+
```yaml
52+
schemaVersion: v1
53+
materials:
54+
- name: sbom
55+
type: SBOM_CYCLONEDX_JSON
56+
- name: another-sbom
57+
type: SBOM_CYCLONEDX_JSON
58+
- name: my-image
59+
type: CONTAINER_IMAGE
60+
policies:
61+
materials: # policies applied to materials
62+
- ref: cyclonedx-licenses.yaml # (1)
63+
attestation: # policies applied to the whole attestation
64+
- ref: https://github.com/chainloop/chainloop-dev/blob/main/docs/examples/policies/chainloop-commit.yaml # (2)
65+
```
66+
Here we can see that:
67+
- (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.
68+
69+
If we wanted to only evaluate the policy against the `sbom` material, and skip the other, we should filter them by name:
70+
```yaml
71+
policies:
72+
materials:
73+
- ref: cyclonedx-licenses.yaml
74+
selector: # (3)
75+
name: sbom
76+
```
77+
Here, in (3), we are making explicit that only `sbom` material must be evaluated by the `cyclonedx-licenses.yaml` policy.
78+
- (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`.
79+
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](https://github.com/chainloop-dev/chainloop/tree/main/docs/examples/policies).
80+
81+
Finally, note that material policies are evaluated during `chainloop attestation add` commands, while attestation policies are evaluated in `chainloop attestation push` command.
82+
83+
### Embedding or referencing policies
84+
There are two ways to attach a policy to a contract:
85+
* **By referencing it**, as it can be seen in the examples above. `ref` property admits a local (filesystem) or remote reference (HTTPS). For example:
86+
```yaml
87+
policies:
88+
materials:
89+
- ref: cyclonedx-licenses.yaml # local reference
90+
```
91+
and
92+
```yaml
93+
policies:
94+
materials:
95+
- ref: https://github.com/chainloop/chainloop-dev/blob/main/docs/examples/policies/cyclonedx-licenses.yaml
96+
```
97+
are both equivalent. The advantage of having remote policies is that they can be easily reused, allowing organizations to create policy catalogs.
98+
99+
* 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:
100+
```yaml
101+
schemaVersion: v1
102+
materials:
103+
- name: sbom
104+
type: SBOM_CYCLONEDX_JSON
105+
policies:
106+
materials:
107+
- policy: # (1)
108+
apiVersion: workflowcontract.chainloop.dev/v1
109+
kind: Policy
110+
metadata:
111+
name: sbom-licenses
112+
spec:
113+
type: SBOM_CYCLONEDX_JSON
114+
embedded: |
115+
package main
116+
117+
deny[msg] {
118+
count(without_license) > 0
119+
msg := "SBOM has components without licenses"
120+
}
121+
122+
without_license = {comp.purl |
123+
some i
124+
comp := input.components[i]
125+
not comp.licenses
126+
}
127+
```
128+
In the example above, we can see that, when referenced by the `policy` attribute (1), a full policy can be embedded in the contract.
129+
130+
### Rego scripts
131+
Currently, policy scripts are assumed to be written in [Rego language](https://www.openpolicyagent.org/docs/latest/policy-language/#learning-rego). Other policy engines might be implemented in the future.
132+
The only requirement of the policy is the existence of one or multiple `deny` rules, which evaluate to a **list of violation strings**.
133+
For example, this policy script:
134+
```yaml
135+
package main
136+
137+
deny[msg] {
138+
not is_approved
139+
140+
msg:= "Container image is not approved"
141+
}
142+
143+
is_approved {
144+
input.predicate.materials[_].annotations["chainloop.material.type"] == "CONTAINER_IMAGE"
145+
146+
input.predicate.annotations.approval == "true"
147+
}
148+
```
149+
when evaluated against an attestation, will generate the following output if the expected annotation is not present:
150+
```json
151+
{
152+
"deny": [
153+
"Container image is not approved"
154+
]
155+
}
156+
```
157+
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](https://www.openpolicyagent.org/docs/latest/opa-1/).
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2024 The Chainloop Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
apiVersion: workflowcontract.chainloop.dev/v1
16+
kind: Policy
17+
metadata:
18+
name: chainloop-commit
19+
spec:
20+
type: ATTESTATION
21+
embedded: |
22+
package main
23+
24+
deny[msg] {
25+
not has_commit
26+
msg := "missing commit in attestation material"
27+
}
28+
29+
has_commit {
30+
some i
31+
input.subject[i].name == "git.head"
32+
input.subject[i].digest.sha1
33+
}
34+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2024 The Chainloop Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Checks that there are is a container material with a custom annotation "chainloop-qa-approval=true"
16+
# chainloop att push
17+
# This can be used as a control gate to allow e.g. a production deployment
18+
apiVersion: workflowcontract.chainloop.dev/v1
19+
kind: Policy
20+
metadata:
21+
name: sarif-errors
22+
spec:
23+
type: ATTESTATION
24+
embedded: |
25+
package main
26+
27+
deny[msg] {
28+
not is_approved
29+
30+
msg:= "Container image is not approved"
31+
}
32+
33+
is_approved {
34+
input.predicate.materials[_].annotations["chainloop.material.type"] == "CONTAINER_IMAGE"
35+
36+
input.predicate.annotations.approval == "true"
37+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright 2024 The Chainloop Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Checks that all components have a license
16+
apiVersion: workflowcontract.chainloop.dev/v1
17+
kind: Policy
18+
metadata:
19+
name: cyclonedx-licenses
20+
spec:
21+
type: SBOM_CYCLONEDX_JSON
22+
embedded: |
23+
package main
24+
25+
deny[msg] {
26+
count(without_license) > 0
27+
msg := "SBOM has components without licenses"
28+
}
29+
30+
without_license = {comp.purl |
31+
some i
32+
comp := input.components[i]
33+
not comp.licenses
34+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright 2024 The Chainloop Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# Checks that there are no errors in the SARIF report
16+
apiVersion: workflowcontract.chainloop.dev/v1
17+
kind: Policy
18+
metadata:
19+
name: sarif-errors
20+
spec:
21+
embedded: |
22+
package main
23+
24+
deny[msg] {
25+
has_errors
26+
msg := "There are errors in the SARIF report"
27+
}
28+
29+
has_errors {
30+
input.runs[_].results[_].level == "error"
31+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Copyright 2024 The Chainloop Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
apiVersion: workflowcontract.chainloop.dev/v1
16+
kind: Policy
17+
metadata:
18+
name: sbom-present
19+
spec:
20+
type: ATTESTATION
21+
embedded: |
22+
package main
23+
24+
# Verifies there is a SBOM material, even if not enforced by contract
25+
26+
import future.keywords.contains
27+
import future.keywords.in
28+
29+
deny[msg] {
30+
not has_sbom
31+
msg := "missing SBOM material"
32+
}
33+
34+
# Collect all material types
35+
kinds contains kind {
36+
some material in input.predicate.materials
37+
kind := material.annotations["chainloop.material.type"]
38+
}
39+
40+
has_sbom {
41+
values := ["SBOM_SPDX_JSON","SBOM_CYCLONEDX_JSON"]
42+
kinds[_] == values[_]
43+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2024 The Chainloop Authors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
apiVersion: workflowcontract.chainloop.dev/v1
16+
kind: Policy
17+
metadata:
18+
name: made-with-syft
19+
spec:
20+
type: SBOM_SPDX_JSON
21+
embedded: |
22+
package main
23+
24+
import future.keywords.in
25+
26+
# Verifies that the SPDX was created with Syft
27+
28+
deny[msg] {
29+
not made_with_syft
30+
31+
msg := "Not made with syft"
32+
}
33+
34+
made_with_syft {
35+
some creator in input.creationInfo.creators
36+
contains(creator, "syft")
37+
}

0 commit comments

Comments
 (0)