Skip to content

Commit daaa968

Browse files
jiparisjavirlnmigmartri
authored
feat(chainloop): add EVIDENCE material type (#702)
Signed-off-by: Jose I. Paris <[email protected]> Signed-off-by: Javier Rodriguez <[email protected]> Signed-off-by: Miguel Martinez Trivino <[email protected]> Co-authored-by: Javier Rodriguez <[email protected]> Co-authored-by: Miguel Martinez Trivino <[email protected]>
1 parent 6915d7f commit daaa968

File tree

9 files changed

+245
-16
lines changed

9 files changed

+245
-16
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,8 @@ Chainloop supports the collection of the following pieces of evidence types:
131131
- [SARIF](https://docs.oasis-open.org/sarif/sarif/v2.1.0/)
132132
- [JUnit](https://www.ibm.com/docs/en/developer-for-zos/14.1?topic=formats-junit-xml-format)
133133
- [Helm Chart](https://helm.sh/docs/topics/charts/)
134-
- Generic Artifact Types
134+
- Artifact Type: It represents a software artifact.
135+
- Custom Evidence Type: Custom piece of evidence that doesn't fit in any other category, for instance, an approval report in json format, etc.
135136
- Key-Value metadata pairs
136137

137138
During the attestation process, these pieces of evidence will get uploaded to the [Content Addressable Storage](https://docs.chainloop.dev/reference/operator/cas-backend/) (if applicable) and referenced in a [SLSA](https://slsa.dev) attestation.

app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go

Lines changed: 20 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/workflowcontract/v1/crafting_schema.proto

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ message CraftingSchema {
8484
// https://github.com/microsoft/sarif-tutorials/blob/main/docs/1-Introduction.md
8585
SARIF = 9;
8686
HELM_CHART = 10;
87+
88+
// Pieces of evidences represent generic, additional context that don't fit
89+
// into one of the well known material types. For example, a custom approval report (in json), ...
90+
EVIDENCE = 11;
8791
}
8892
}
8993
}

docs/docs/reference/operator/contract.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ Chainloop supports the collection of the following pieces of evidence types:
5656
- [SARIF](https://docs.oasis-open.org/sarif/sarif/v2.1.0/)
5757
- [JUnit](https://www.ibm.com/docs/en/developer-for-zos/14.1?topic=formats-junit-xml-format)
5858
- [Helm Chart](https://helm.sh/docs/topics/charts/)
59-
- Generic Artifact Types
59+
- Artifact Type: It represents a software artifact.
60+
- Custom Evidence Type: Custom piece of evidence that doesn't fit in any other category, for instance, an approval report in json format, etc.
6061
- Key-Value metadata pairs
6162

6263
To learn more on how to add them to your contract, refer to the `type` section below.

internal/attestation/crafter/materials/artifact_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
//nolint:dupl
1617
package materials_test
1718

1819
import (
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package materials
17+
18+
import (
19+
"context"
20+
"fmt"
21+
22+
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
23+
api "github.com/chainloop-dev/chainloop/internal/attestation/crafter/api/attestation/v1"
24+
"github.com/chainloop-dev/chainloop/internal/casclient"
25+
"github.com/rs/zerolog"
26+
)
27+
28+
type EvidenceCrafter struct {
29+
*crafterCommon
30+
backend *casclient.CASBackend
31+
}
32+
33+
// NewEvidenceCrafter generates a new Evidence material.
34+
// Pieces of evidences represent generic, additional context that don't fit
35+
// into one of the well known material types. For example, a custom approval report (in json), ...
36+
func NewEvidenceCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*EvidenceCrafter, error) {
37+
if schema.Type != schemaapi.CraftingSchema_Material_EVIDENCE {
38+
return nil, fmt.Errorf("material type is not evidence")
39+
}
40+
41+
craftCommon := &crafterCommon{logger: l, input: schema}
42+
return &EvidenceCrafter{backend: backend, crafterCommon: craftCommon}, nil
43+
}
44+
45+
// Craft will calculate the digest of the artifact, simulate an upload and return the material definition
46+
func (i *EvidenceCrafter) Craft(ctx context.Context, artifactPath string) (*api.Attestation_Material, error) {
47+
return uploadAndCraft(ctx, i.input, i.backend, artifactPath, i.logger)
48+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
//nolint:dupl
17+
package materials_test
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
"code.cloudfoundry.org/bytefmt"
24+
contractAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
25+
attestationApi "github.com/chainloop-dev/chainloop/internal/attestation/crafter/api/attestation/v1"
26+
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/materials"
27+
"github.com/chainloop-dev/chainloop/internal/casclient"
28+
mUploader "github.com/chainloop-dev/chainloop/internal/casclient/mocks"
29+
"github.com/rs/zerolog"
30+
"github.com/stretchr/testify/assert"
31+
"github.com/stretchr/testify/require"
32+
)
33+
34+
func TestNewEvidenceCrafter(t *testing.T) {
35+
testCases := []struct {
36+
name string
37+
input *contractAPI.CraftingSchema_Material
38+
wantErr bool
39+
}{
40+
{
41+
name: "happy path",
42+
input: &contractAPI.CraftingSchema_Material{
43+
Type: contractAPI.CraftingSchema_Material_EVIDENCE,
44+
},
45+
},
46+
{
47+
name: "wrong type",
48+
input: &contractAPI.CraftingSchema_Material{
49+
Type: contractAPI.CraftingSchema_Material_CONTAINER_IMAGE,
50+
},
51+
wantErr: true,
52+
},
53+
}
54+
55+
for _, tc := range testCases {
56+
t.Run(tc.name, func(t *testing.T) {
57+
_, err := materials.NewEvidenceCrafter(tc.input, nil, nil)
58+
if tc.wantErr {
59+
assert.Error(t, err)
60+
return
61+
}
62+
63+
assert.NoError(t, err)
64+
})
65+
}
66+
}
67+
68+
func TestEvidenceCraft(t *testing.T) {
69+
assert := assert.New(t)
70+
schema := &contractAPI.CraftingSchema_Material{
71+
Name: "test",
72+
Type: contractAPI.CraftingSchema_Material_EVIDENCE,
73+
}
74+
75+
l := zerolog.Nop()
76+
77+
// Mock uploader
78+
uploader := mUploader.NewUploader(t)
79+
uploader.On("UploadFile", context.TODO(), "./testdata/simple.txt").
80+
Return(&casclient.UpDownStatus{
81+
Digest: "deadbeef",
82+
Filename: "simple.txt",
83+
}, nil)
84+
85+
backend := &casclient.CASBackend{Uploader: uploader}
86+
87+
crafter, err := materials.NewEvidenceCrafter(schema, backend, &l)
88+
require.NoError(t, err)
89+
90+
got, err := crafter.Craft(context.TODO(), "./testdata/simple.txt")
91+
assert.NoError(err)
92+
assert.Equal(contractAPI.CraftingSchema_Material_EVIDENCE.String(), got.MaterialType.String())
93+
assert.True(got.UploadedToCas)
94+
95+
// The result includes the digest reference
96+
assert.Equal(got.GetArtifact(), &attestationApi.Attestation_Material_Artifact{
97+
Id: "test", Digest: "sha256:54181dfe59340b318253e59f7695f547c5c10d071cb75001170a389061349918", Name: "simple.txt",
98+
})
99+
}
100+
101+
func TestEvidenceCraftInline(t *testing.T) {
102+
assert := assert.New(t)
103+
schema := &contractAPI.CraftingSchema_Material{
104+
Name: "test",
105+
Type: contractAPI.CraftingSchema_Material_EVIDENCE,
106+
}
107+
l := zerolog.Nop()
108+
109+
t.Run("inline without size limit", func(t *testing.T) {
110+
backend := &casclient.CASBackend{}
111+
112+
crafter, err := materials.NewEvidenceCrafter(schema, backend, &l)
113+
require.NoError(t, err)
114+
115+
got, err := crafter.Craft(context.TODO(), "./testdata/simple.txt")
116+
assert.NoError(err)
117+
assertEvidenceMaterial(t, got)
118+
})
119+
120+
t.Run("backend with size limit", func(t *testing.T) {
121+
backend := &casclient.CASBackend{
122+
MaxSize: 100 * bytefmt.BYTE,
123+
}
124+
125+
crafter, err := materials.NewEvidenceCrafter(schema, backend, &l)
126+
require.NoError(t, err)
127+
128+
got, err := crafter.Craft(context.TODO(), "./testdata/simple.txt")
129+
assert.NoError(err)
130+
assertEvidenceMaterial(t, got)
131+
})
132+
133+
t.Run("backend with size limit too small", func(t *testing.T) {
134+
backend := &casclient.CASBackend{
135+
MaxSize: bytefmt.BYTE,
136+
}
137+
138+
crafter, err := materials.NewEvidenceCrafter(schema, backend, &l)
139+
require.NoError(t, err)
140+
141+
_, err = crafter.Craft(context.TODO(), "./testdata/simple.txt")
142+
assert.Error(err)
143+
})
144+
}
145+
146+
func assertEvidenceMaterial(t *testing.T, got *attestationApi.Attestation_Material) {
147+
assert := assert.New(t)
148+
// Not uploaded to CAS
149+
assert.False(got.UploadedToCas)
150+
// The result includes the digest and inline content
151+
assert.Equal(got.GetArtifact(), &attestationApi.Attestation_Material_Artifact{
152+
Id: "test", Digest: "sha256:54181dfe59340b318253e59f7695f547c5c10d071cb75001170a389061349918", Name: "simple.txt",
153+
// Inline content
154+
Content: []byte("txt file"),
155+
})
156+
}

internal/attestation/crafter/materials/materials.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia
171171
crafter, err = NewSARIFCrafter(materialSchema, casBackend, logger)
172172
case schemaapi.CraftingSchema_Material_HELM_CHART:
173173
crafter, err = NewHelmChartCrafter(materialSchema, casBackend, logger)
174+
case schemaapi.CraftingSchema_Material_EVIDENCE:
175+
crafter, err = NewEvidenceCrafter(materialSchema, casBackend, logger)
174176
default:
175177
return nil, fmt.Errorf("material of type %q not supported yet", materialSchema.Type)
176178
}

0 commit comments

Comments
 (0)