Skip to content

Commit 892f68c

Browse files
authored
feat: add SARIF format support (#362)
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent 0f0f818 commit 892f68c

File tree

10 files changed

+544
-15
lines changed

10 files changed

+544
-15
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Chainloop supports the collection of the following pieces of evidence types:
128128
- [SPDX SBOM](https://spdx.dev/specifications/)
129129
- [CSAF VEX](https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html)
130130
- [OpenVEX](https://github.com/openvex)
131+
- [SARIF](https://docs.oasis-open.org/sarif/sarif/v2.1.0/)
131132
- [JUnit](https://www.ibm.com/docs/en/developer-for-zos/14.1?topic=formats-junit-xml-format)
132133
- Generic Artifact Types
133134
- Key-Value metadata pairs

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: 21 additions & 15 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ message CraftingSchema {
7171
OPENVEX = 7;
7272
// https://docs.oasis-open.org/csaf/csaf/v2.0/cs03/csaf-v2.0-cs03.html
7373
CSAF_VEX = 8;
74+
// Static analysis output format
75+
// https://github.com/microsoft/sarif-tutorials/blob/main/docs/1-Introduction.md
76+
SARIF = 9;
7477
}
7578
}
7679
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ require (
214214
github.com/opencontainers/go-digest v1.0.0 // indirect
215215
github.com/opencontainers/runc v1.1.9 // indirect
216216
github.com/opentracing/opentracing-go v1.2.0 // indirect
217+
github.com/owenrumney/go-sarif v1.1.1
217218
github.com/pelletier/go-toml/v2 v2.0.9 // indirect
218219
github.com/pkg/errors v0.9.1 // indirect
219220
github.com/pmezard/go-difflib v1.0.0 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -957,6 +957,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS
957957
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
958958
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
959959
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
960+
github.com/owenrumney/go-sarif v1.1.1 h1:QNObu6YX1igyFKhdzd7vgzmw7XsWN3/6NMGuDzBgXmE=
961+
github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U=
960962
github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU=
961963
github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c=
962964
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
@@ -1205,6 +1207,7 @@ github.com/zalando/go-keyring v0.2.2 h1:f0xmpYiSrHtSNAVgwip93Cg8tuF45HJM6rHq/A5R
12051207
github.com/zalando/go-keyring v0.2.2/go.mod h1:sI3evg9Wvpw3+n4SqplGSJUMwtDeROfD4nsFz4z9PG0=
12061208
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
12071209
github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
1210+
github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk=
12081211
github.com/zclconf/go-cty v1.13.3 h1:m+b9q3YDbg6Bec5rr+KGy1MzEVzY/jC2X+YX4yqKtHI=
12091212
github.com/zclconf/go-cty v1.13.3/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0=
12101213
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=

internal/attestation/crafter/materials/materials.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ func Craft(ctx context.Context, materialSchema *schemaapi.CraftingSchema_Materia
160160
crafter, err = NewOpenVEXCrafter(materialSchema, casBackend, logger)
161161
case schemaapi.CraftingSchema_Material_CSAF_VEX:
162162
crafter, err = NewCSAFVEXCrafter(materialSchema, casBackend, logger)
163+
case schemaapi.CraftingSchema_Material_SARIF:
164+
crafter, err = NewSARIFCrafter(materialSchema, casBackend, logger)
163165
default:
164166
return nil, fmt.Errorf("material of type %q not supported yet", materialSchema.Type)
165167
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//
2+
// Copyright 2023 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+
api "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1"
23+
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
24+
"github.com/chainloop-dev/chainloop/internal/casclient"
25+
"github.com/owenrumney/go-sarif/sarif"
26+
"github.com/rs/zerolog"
27+
)
28+
29+
type SARIFCrafter struct {
30+
backend *casclient.CASBackend
31+
*crafterCommon
32+
}
33+
34+
func NewSARIFCrafter(materialSchema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*SARIFCrafter, error) {
35+
if materialSchema.Type != schemaapi.CraftingSchema_Material_SARIF {
36+
return nil, fmt.Errorf("material type is not SARIF format")
37+
}
38+
39+
return &SARIFCrafter{
40+
backend: backend,
41+
crafterCommon: &crafterCommon{logger: l, input: materialSchema},
42+
}, nil
43+
}
44+
45+
func (i *SARIFCrafter) Craft(ctx context.Context, filepath string) (*api.Attestation_Material, error) {
46+
i.logger.Debug().Str("path", filepath).Msg("decoding SARIF file")
47+
doc, err := sarif.Open(filepath)
48+
// parse doesn't fail if the provided file is a valid JSON, but not a valid CSAF VEX file
49+
if err != nil || doc.Schema == "" {
50+
if err != nil {
51+
i.logger.Debug().Err(err).Msg("error decoding file")
52+
}
53+
54+
return nil, fmt.Errorf("invalid SARIF file: %w", ErrInvalidMaterialType)
55+
}
56+
57+
return uploadAndCraft(ctx, i.input, i.backend, filepath, i.logger)
58+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//
2+
// Copyright 2023 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+
attestationApi "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1"
24+
contractAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
25+
"github.com/chainloop-dev/chainloop/internal/attestation/crafter/materials"
26+
"github.com/chainloop-dev/chainloop/internal/casclient"
27+
mUploader "github.com/chainloop-dev/chainloop/internal/casclient/mocks"
28+
"github.com/rs/zerolog"
29+
"github.com/stretchr/testify/assert"
30+
"github.com/stretchr/testify/require"
31+
)
32+
33+
func TestNewSARIFCrafter(t *testing.T) {
34+
testCases := []struct {
35+
name string
36+
input *contractAPI.CraftingSchema_Material
37+
wantErr bool
38+
}{
39+
{
40+
name: "happy path",
41+
input: &contractAPI.CraftingSchema_Material{
42+
Type: contractAPI.CraftingSchema_Material_SARIF,
43+
},
44+
},
45+
{
46+
name: "wrong type",
47+
input: &contractAPI.CraftingSchema_Material{
48+
Type: contractAPI.CraftingSchema_Material_CONTAINER_IMAGE,
49+
},
50+
wantErr: true,
51+
},
52+
}
53+
54+
for _, tc := range testCases {
55+
t.Run(tc.name, func(t *testing.T) {
56+
_, err := materials.NewSARIFCrafter(tc.input, nil, nil)
57+
if tc.wantErr {
58+
assert.Error(t, err)
59+
return
60+
}
61+
62+
assert.NoError(t, err)
63+
})
64+
}
65+
}
66+
67+
func TestSARIFCraft(t *testing.T) {
68+
testCases := []struct {
69+
name string
70+
filePath string
71+
wantErr string
72+
}{
73+
{
74+
name: "non-expected json file",
75+
filePath: "./testdata/sbom.cyclonedx.json",
76+
wantErr: "unexpected material type",
77+
},
78+
{
79+
name: "invalid path",
80+
filePath: "./testdata/non-existing.json",
81+
wantErr: "unexpected material type",
82+
},
83+
{
84+
name: "invalid artifact type",
85+
filePath: "./testdata/simple.txt",
86+
wantErr: "unexpected material type",
87+
},
88+
{
89+
name: "valid artifact type",
90+
filePath: "./testdata/report.sarif",
91+
},
92+
}
93+
94+
assert := assert.New(t)
95+
schema := &contractAPI.CraftingSchema_Material{
96+
Name: "test",
97+
Type: contractAPI.CraftingSchema_Material_SARIF,
98+
}
99+
l := zerolog.Nop()
100+
for _, tc := range testCases {
101+
t.Run(tc.name, func(t *testing.T) {
102+
// Mock uploader
103+
uploader := mUploader.NewUploader(t)
104+
if tc.wantErr == "" {
105+
uploader.On("UploadFile", context.TODO(), tc.filePath).
106+
Return(&casclient.UpDownStatus{
107+
Digest: "deadbeef",
108+
Filename: "report.sarif",
109+
}, nil)
110+
}
111+
112+
backend := &casclient.CASBackend{Uploader: uploader}
113+
crafter, err := materials.NewSARIFCrafter(schema, backend, &l)
114+
require.NoError(t, err)
115+
116+
got, err := crafter.Craft(context.TODO(), tc.filePath)
117+
if tc.wantErr != "" {
118+
assert.ErrorContains(err, tc.wantErr)
119+
return
120+
}
121+
require.NoError(t, err)
122+
123+
assert.Equal(contractAPI.CraftingSchema_Material_SARIF.String(), got.MaterialType.String())
124+
assert.True(got.UploadedToCas)
125+
126+
// // The result includes the digest reference
127+
assert.Equal(&attestationApi.Attestation_Material_Artifact{
128+
Id: "test", Digest: "sha256:c4a63494f9289dd9fd44f841efb4f5b52765c2de6332f2d86e5f6c0340b40a95", Name: "report.sarif",
129+
}, got.GetArtifact())
130+
})
131+
}
132+
}

0 commit comments

Comments
 (0)