Skip to content

Commit 6e42538

Browse files
authored
fix(materials): guard legacy tool annotations against empty values (#2881)
Signed-off-by: Jose I. Paris <jiparis@chainloop.dev>
1 parent 9c66450 commit 6e42538

11 files changed

Lines changed: 236 additions & 28 deletions

pkg/attestation/crafter/materials/csaf.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023-2025 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -146,10 +146,10 @@ func (i *CSAFCrafter) injectAnnotations(m *api.Attestation_Material, documentMap
146146
if tracking, ok := documentMap["tracking"].(map[string]any); ok {
147147
if generator, ok := tracking["generator"].(map[string]any); ok {
148148
if engine, ok := generator["engine"].(map[string]any); ok {
149-
if name, ok := engine["name"].(string); ok {
149+
if name, ok := engine["name"].(string); ok && name != "" {
150150
m.Annotations[AnnotationToolNameKey] = name
151151
}
152-
if version, ok := engine["version"].(string); ok {
152+
if version, ok := engine["version"].(string); ok && version != "" {
153153
m.Annotations[AnnotationToolVersionKey] = version
154154
}
155155
}

pkg/attestation/crafter/materials/csaf_test.go

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024-2025 The Chainloop Authors.
2+
// Copyright 2024-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -99,11 +99,13 @@ func TestNewCSAFCrafter(t *testing.T) {
9999

100100
func TestCSAFCraft(t *testing.T) {
101101
testCases := []struct {
102-
name string
103-
filePath string
104-
wantErr string
105-
digest string
106-
schema *contractAPI.CraftingSchema_Material
102+
name string
103+
filePath string
104+
wantErr string
105+
digest string
106+
schema *contractAPI.CraftingSchema_Material
107+
annotations map[string]string
108+
absentAnnotations []string
107109
}{
108110
{
109111
name: "non-expected json file",
@@ -167,6 +169,10 @@ func TestCSAFCraft(t *testing.T) {
167169
Name: "test",
168170
Type: contractAPI.CraftingSchema_Material_CSAF_VEX,
169171
},
172+
annotations: map[string]string{
173+
"chainloop.material.tool.name": "Secvisogram",
174+
"chainloop.material.tool.version": "1.11.0",
175+
},
170176
},
171177
{
172178
name: "2.0 security advisory",
@@ -252,6 +258,15 @@ func TestCSAFCraft(t *testing.T) {
252258
assert.Equal(&attestationApi.Attestation_Material_Artifact{
253259
Id: "test", Digest: tc.digest, Name: strings.Split(tc.filePath, "/")[2],
254260
}, got.GetArtifact())
261+
262+
for k, v := range tc.annotations {
263+
assert.Equal(v, got.Annotations[k])
264+
}
265+
266+
for _, k := range tc.absentAnnotations {
267+
_, exists := got.Annotations[k]
268+
assert.False(exists, "annotation %q should not be present", k)
269+
}
255270
})
256271
}
257272
}

pkg/attestation/crafter/materials/cyclonedxjson.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2024-2025 The Chainloop Authors.
2+
// Copyright 2024-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -214,8 +214,12 @@ func (i *CyclonedxJSONCrafter) extractMetadata(m *api.Attestation_Material, meta
214214

215215
// Maintain backward compatibility - keep legacy keys for the first tool
216216
if len(tools) > 0 {
217-
m.Annotations[AnnotationToolNameKey] = tools[0].Name
218-
m.Annotations[AnnotationToolVersionKey] = tools[0].Version
217+
if tools[0].Name != "" {
218+
m.Annotations[AnnotationToolNameKey] = tools[0].Name
219+
}
220+
if tools[0].Version != "" {
221+
m.Annotations[AnnotationToolVersionKey] = tools[0].Version
222+
}
219223
}
220224

221225
case *cyclonedxMetadataV15:
@@ -232,8 +236,12 @@ func (i *CyclonedxJSONCrafter) extractMetadata(m *api.Attestation_Material, meta
232236

233237
// Maintain backward compatibility - keep legacy keys for the first tool
234238
if len(tools) > 0 {
235-
m.Annotations[AnnotationToolNameKey] = tools[0].Name
236-
m.Annotations[AnnotationToolVersionKey] = tools[0].Version
239+
if tools[0].Name != "" {
240+
m.Annotations[AnnotationToolNameKey] = tools[0].Name
241+
}
242+
if tools[0].Version != "" {
243+
m.Annotations[AnnotationToolVersionKey] = tools[0].Version
244+
}
237245
}
238246

239247
default:

pkg/attestation/crafter/materials/cyclonedxjson_test.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023-2025 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -74,6 +74,7 @@ func TestCyclonedxJSONCraft(t *testing.T) {
7474
wantMainComponentKind string
7575
wantMainComponentVersion string
7676
annotations map[string]string
77+
absentAnnotations []string
7778
}{
7879
{
7980
name: "invalid path",
@@ -156,6 +157,34 @@ func TestCyclonedxJSONCraft(t *testing.T) {
156157
"chainloop.material.sbom.vulnerabilities_report": "true",
157158
},
158159
},
160+
{
161+
name: "1.4 version with empty tool version",
162+
filePath: "./testdata/sbom.cyclonedx-1.4-empty-tool-version.json",
163+
wantDigest: "sha256:f0b15f1ae6cadb9f14e0071f14888bcbae03686580f8fc8dc0c838e6e96aaf0e",
164+
wantFilename: "sbom.cyclonedx-1.4-empty-tool-version.json",
165+
wantMainComponent: "test-app",
166+
wantMainComponentKind: "application",
167+
wantMainComponentVersion: "1.0.0",
168+
annotations: map[string]string{
169+
"chainloop.material.tool.name": "Hub",
170+
"chainloop.material.tools": `["Hub"]`,
171+
},
172+
absentAnnotations: []string{"chainloop.material.tool.version"},
173+
},
174+
{
175+
name: "1.5 version with empty tool version",
176+
filePath: "./testdata/sbom.cyclonedx-1.5-empty-tool-version.json",
177+
wantDigest: "sha256:4091c3b42ec5d368365f5b6fba80dd30cc3ddf84f795096e738b02f8e2716ce1",
178+
wantFilename: "sbom.cyclonedx-1.5-empty-tool-version.json",
179+
wantMainComponent: "test-app",
180+
wantMainComponentKind: "application",
181+
wantMainComponentVersion: "1.0.0",
182+
annotations: map[string]string{
183+
"chainloop.material.tool.name": "Hub",
184+
"chainloop.material.tools": `["Hub"]`,
185+
},
186+
absentAnnotations: []string{"chainloop.material.tool.version"},
187+
},
159188
{
160189
name: "1.5 version with multiple tools",
161190
filePath: "./testdata/sbom.cyclonedx-1.5-multiple-tools.json",
@@ -221,6 +250,11 @@ func TestCyclonedxJSONCraft(t *testing.T) {
221250
ast.Equal(v, got.Annotations[k])
222251
}
223252
}
253+
254+
for _, k := range tc.absentAnnotations {
255+
_, exists := got.Annotations[k]
256+
ast.False(exists, "annotation %q should not be present", k)
257+
}
224258
})
225259
}
226260
}

pkg/attestation/crafter/materials/sarif.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023-2025 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -69,8 +69,10 @@ func (i *SARIFCrafter) injectAnnotations(m *api.Attestation_Material, doc *sarif
6969
if len(doc.Runs) > 0 {
7070
// assuming vendor from first run.
7171
m.Annotations = make(map[string]string)
72-
m.Annotations[AnnotationToolNameKey] = doc.Runs[0].Tool.Driver.Name
73-
if doc.Runs[0].Tool.Driver.Version != nil {
72+
if doc.Runs[0].Tool.Driver.Name != "" {
73+
m.Annotations[AnnotationToolNameKey] = doc.Runs[0].Tool.Driver.Name
74+
}
75+
if doc.Runs[0].Tool.Driver.Version != nil && *doc.Runs[0].Tool.Driver.Version != "" {
7476
m.Annotations[AnnotationToolVersionKey] = *doc.Runs[0].Tool.Driver.Version
7577
}
7678
}

pkg/attestation/crafter/materials/spdxjson.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023-2025 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -91,7 +91,11 @@ func (i *SPDXJSONCrafter) injectAnnotations(m *api.Attestation_Material, doc *sp
9191

9292
// Maintain backward compatibility - keep legacy keys for the first tool
9393
if len(tools) > 0 {
94-
m.Annotations[AnnotationToolNameKey] = tools[0].Name
95-
m.Annotations[AnnotationToolVersionKey] = tools[0].Version
94+
if tools[0].Name != "" {
95+
m.Annotations[AnnotationToolNameKey] = tools[0].Name
96+
}
97+
if tools[0].Version != "" {
98+
m.Annotations[AnnotationToolVersionKey] = tools[0].Version
99+
}
96100
}
97101
}

pkg/attestation/crafter/materials/spdxjson_test.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023-2025 The Chainloop Authors.
2+
// Copyright 2023-2026 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -66,12 +66,13 @@ func TestNewSPDXJSONCrafter(t *testing.T) {
6666

6767
func TestSPDXJSONCraft(t *testing.T) {
6868
testCases := []struct {
69-
name string
70-
filePath string
71-
wantErr string
72-
wantDigest string
73-
wantFilename string
74-
annotations map[string]string
69+
name string
70+
filePath string
71+
wantErr string
72+
wantDigest string
73+
wantFilename string
74+
annotations map[string]string
75+
absentAnnotations []string
7576
}{
7677
{
7778
name: "invalid sbom format",
@@ -99,6 +100,27 @@ func TestSPDXJSONCraft(t *testing.T) {
99100
"chainloop.material.tools": `["syft@0.73.0"]`,
100101
},
101102
},
103+
{
104+
name: "tool with empty name",
105+
filePath: "./testdata/sbom-spdx-empty-tool-name.json",
106+
wantDigest: "sha256:bbc5c2f78c7b05f2e80295e1bf827f24623394dafcd56a4a14f0591b835d28fa",
107+
wantFilename: "sbom-spdx-empty-tool-name.json",
108+
absentAnnotations: []string{
109+
"chainloop.material.tool.name",
110+
"chainloop.material.tool.version",
111+
},
112+
},
113+
{
114+
name: "tool without version",
115+
filePath: "./testdata/sbom-spdx-no-tool-version.json",
116+
wantDigest: "sha256:c502fcd2944cc7aadf718af910a242cca0702387c27e740229d6e35cd1ccd52e",
117+
wantFilename: "sbom-spdx-no-tool-version.json",
118+
annotations: map[string]string{
119+
"chainloop.material.tool.name": "spdxgen",
120+
"chainloop.material.tools": `["spdxgen"]`,
121+
},
122+
absentAnnotations: []string{"chainloop.material.tool.version"},
123+
},
102124
{
103125
name: "multiple tools",
104126
filePath: "./testdata/sbom-spdx-multiple-tools.json",
@@ -155,6 +177,11 @@ func TestSPDXJSONCraft(t *testing.T) {
155177
assert.Equal(v, got.Annotations[k])
156178
}
157179
}
180+
181+
for _, k := range tc.absentAnnotations {
182+
_, exists := got.Annotations[k]
183+
assert.False(exists, "annotation %q should not be present", k)
184+
}
158185
})
159186
}
160187
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"spdxVersion": "SPDX-2.3",
3+
"dataLicense": "CC0-1.0",
4+
"SPDXID": "SPDXRef-DOCUMENT",
5+
"name": "test-empty-tool-name",
6+
"documentNamespace": "https://example.com/test/empty-tool-name",
7+
"creationInfo": {
8+
"licenseListVersion": "3.20",
9+
"creators": [
10+
"Organization: Example Corp",
11+
"Tool: "
12+
],
13+
"created": "2024-01-01T10:00:00Z"
14+
},
15+
"packages": [
16+
{
17+
"name": "example-package",
18+
"SPDXID": "SPDXRef-Package-example",
19+
"versionInfo": "1.0.0",
20+
"downloadLocation": "NOASSERTION",
21+
"licenseConcluded": "MIT",
22+
"licenseDeclared": "MIT",
23+
"copyrightText": "NOASSERTION"
24+
}
25+
]
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"spdxVersion": "SPDX-2.3",
3+
"dataLicense": "CC0-1.0",
4+
"SPDXID": "SPDXRef-DOCUMENT",
5+
"name": "test-no-tool-version",
6+
"documentNamespace": "https://example.com/test/no-tool-version",
7+
"creationInfo": {
8+
"licenseListVersion": "3.20",
9+
"creators": [
10+
"Organization: Example Corp",
11+
"Tool: spdxgen"
12+
],
13+
"created": "2024-01-01T10:00:00Z"
14+
},
15+
"packages": [
16+
{
17+
"name": "example-package",
18+
"SPDXID": "SPDXRef-Package-example",
19+
"versionInfo": "1.0.0",
20+
"downloadLocation": "NOASSERTION",
21+
"licenseConcluded": "MIT",
22+
"licenseDeclared": "MIT",
23+
"copyrightText": "NOASSERTION"
24+
}
25+
]
26+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json",
3+
"bomFormat": "CycloneDX",
4+
"specVersion": "1.4",
5+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
6+
"version": 1,
7+
"metadata": {
8+
"timestamp": "2025-09-28T07:00:46Z",
9+
"tools": [
10+
{
11+
"name": "Hub",
12+
"version": ""
13+
}
14+
],
15+
"component": {
16+
"bom-ref": "test-component",
17+
"type": "application",
18+
"name": "test-app",
19+
"version": "1.0.0"
20+
}
21+
},
22+
"components": [
23+
{
24+
"bom-ref": "pkg:golang/example.com/test@v1.0.0",
25+
"type": "library",
26+
"name": "example.com/test",
27+
"version": "v1.0.0",
28+
"purl": "pkg:golang/example.com/test@v1.0.0"
29+
}
30+
]
31+
}

0 commit comments

Comments
 (0)