diff --git a/pkg/attestation/crafter/materials/csaf.go b/pkg/attestation/crafter/materials/csaf.go index a19cff11f..212eca421 100644 --- a/pkg/attestation/crafter/materials/csaf.go +++ b/pkg/attestation/crafter/materials/csaf.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // 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 if tracking, ok := documentMap["tracking"].(map[string]any); ok { if generator, ok := tracking["generator"].(map[string]any); ok { if engine, ok := generator["engine"].(map[string]any); ok { - if name, ok := engine["name"].(string); ok { + if name, ok := engine["name"].(string); ok && name != "" { m.Annotations[AnnotationToolNameKey] = name } - if version, ok := engine["version"].(string); ok { + if version, ok := engine["version"].(string); ok && version != "" { m.Annotations[AnnotationToolVersionKey] = version } } diff --git a/pkg/attestation/crafter/materials/csaf_test.go b/pkg/attestation/crafter/materials/csaf_test.go index 7a67693d3..cb7126adc 100644 --- a/pkg/attestation/crafter/materials/csaf_test.go +++ b/pkg/attestation/crafter/materials/csaf_test.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -99,11 +99,13 @@ func TestNewCSAFCrafter(t *testing.T) { func TestCSAFCraft(t *testing.T) { testCases := []struct { - name string - filePath string - wantErr string - digest string - schema *contractAPI.CraftingSchema_Material + name string + filePath string + wantErr string + digest string + schema *contractAPI.CraftingSchema_Material + annotations map[string]string + absentAnnotations []string }{ { name: "non-expected json file", @@ -167,6 +169,10 @@ func TestCSAFCraft(t *testing.T) { Name: "test", Type: contractAPI.CraftingSchema_Material_CSAF_VEX, }, + annotations: map[string]string{ + "chainloop.material.tool.name": "Secvisogram", + "chainloop.material.tool.version": "1.11.0", + }, }, { name: "2.0 security advisory", @@ -252,6 +258,15 @@ func TestCSAFCraft(t *testing.T) { assert.Equal(&attestationApi.Attestation_Material_Artifact{ Id: "test", Digest: tc.digest, Name: strings.Split(tc.filePath, "/")[2], }, got.GetArtifact()) + + for k, v := range tc.annotations { + assert.Equal(v, got.Annotations[k]) + } + + for _, k := range tc.absentAnnotations { + _, exists := got.Annotations[k] + assert.False(exists, "annotation %q should not be present", k) + } }) } } diff --git a/pkg/attestation/crafter/materials/cyclonedxjson.go b/pkg/attestation/crafter/materials/cyclonedxjson.go index 102f062f1..17fbc38d7 100644 --- a/pkg/attestation/crafter/materials/cyclonedxjson.go +++ b/pkg/attestation/crafter/materials/cyclonedxjson.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // 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 // Maintain backward compatibility - keep legacy keys for the first tool if len(tools) > 0 { - m.Annotations[AnnotationToolNameKey] = tools[0].Name - m.Annotations[AnnotationToolVersionKey] = tools[0].Version + if tools[0].Name != "" { + m.Annotations[AnnotationToolNameKey] = tools[0].Name + } + if tools[0].Version != "" { + m.Annotations[AnnotationToolVersionKey] = tools[0].Version + } } case *cyclonedxMetadataV15: @@ -232,8 +236,12 @@ func (i *CyclonedxJSONCrafter) extractMetadata(m *api.Attestation_Material, meta // Maintain backward compatibility - keep legacy keys for the first tool if len(tools) > 0 { - m.Annotations[AnnotationToolNameKey] = tools[0].Name - m.Annotations[AnnotationToolVersionKey] = tools[0].Version + if tools[0].Name != "" { + m.Annotations[AnnotationToolNameKey] = tools[0].Name + } + if tools[0].Version != "" { + m.Annotations[AnnotationToolVersionKey] = tools[0].Version + } } default: diff --git a/pkg/attestation/crafter/materials/cyclonedxjson_test.go b/pkg/attestation/crafter/materials/cyclonedxjson_test.go index 5112407ea..63223663e 100644 --- a/pkg/attestation/crafter/materials/cyclonedxjson_test.go +++ b/pkg/attestation/crafter/materials/cyclonedxjson_test.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -74,6 +74,7 @@ func TestCyclonedxJSONCraft(t *testing.T) { wantMainComponentKind string wantMainComponentVersion string annotations map[string]string + absentAnnotations []string }{ { name: "invalid path", @@ -156,6 +157,34 @@ func TestCyclonedxJSONCraft(t *testing.T) { "chainloop.material.sbom.vulnerabilities_report": "true", }, }, + { + name: "1.4 version with empty tool version", + filePath: "./testdata/sbom.cyclonedx-1.4-empty-tool-version.json", + wantDigest: "sha256:f0b15f1ae6cadb9f14e0071f14888bcbae03686580f8fc8dc0c838e6e96aaf0e", + wantFilename: "sbom.cyclonedx-1.4-empty-tool-version.json", + wantMainComponent: "test-app", + wantMainComponentKind: "application", + wantMainComponentVersion: "1.0.0", + annotations: map[string]string{ + "chainloop.material.tool.name": "Hub", + "chainloop.material.tools": `["Hub"]`, + }, + absentAnnotations: []string{"chainloop.material.tool.version"}, + }, + { + name: "1.5 version with empty tool version", + filePath: "./testdata/sbom.cyclonedx-1.5-empty-tool-version.json", + wantDigest: "sha256:4091c3b42ec5d368365f5b6fba80dd30cc3ddf84f795096e738b02f8e2716ce1", + wantFilename: "sbom.cyclonedx-1.5-empty-tool-version.json", + wantMainComponent: "test-app", + wantMainComponentKind: "application", + wantMainComponentVersion: "1.0.0", + annotations: map[string]string{ + "chainloop.material.tool.name": "Hub", + "chainloop.material.tools": `["Hub"]`, + }, + absentAnnotations: []string{"chainloop.material.tool.version"}, + }, { name: "1.5 version with multiple tools", filePath: "./testdata/sbom.cyclonedx-1.5-multiple-tools.json", @@ -221,6 +250,11 @@ func TestCyclonedxJSONCraft(t *testing.T) { ast.Equal(v, got.Annotations[k]) } } + + for _, k := range tc.absentAnnotations { + _, exists := got.Annotations[k] + ast.False(exists, "annotation %q should not be present", k) + } }) } } diff --git a/pkg/attestation/crafter/materials/sarif.go b/pkg/attestation/crafter/materials/sarif.go index 0c64cfe15..cb6918b10 100644 --- a/pkg/attestation/crafter/materials/sarif.go +++ b/pkg/attestation/crafter/materials/sarif.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // 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 if len(doc.Runs) > 0 { // assuming vendor from first run. m.Annotations = make(map[string]string) - m.Annotations[AnnotationToolNameKey] = doc.Runs[0].Tool.Driver.Name - if doc.Runs[0].Tool.Driver.Version != nil { + if doc.Runs[0].Tool.Driver.Name != "" { + m.Annotations[AnnotationToolNameKey] = doc.Runs[0].Tool.Driver.Name + } + if doc.Runs[0].Tool.Driver.Version != nil && *doc.Runs[0].Tool.Driver.Version != "" { m.Annotations[AnnotationToolVersionKey] = *doc.Runs[0].Tool.Driver.Version } } diff --git a/pkg/attestation/crafter/materials/spdxjson.go b/pkg/attestation/crafter/materials/spdxjson.go index c359b4ce5..885ad2cdd 100644 --- a/pkg/attestation/crafter/materials/spdxjson.go +++ b/pkg/attestation/crafter/materials/spdxjson.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // 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 // Maintain backward compatibility - keep legacy keys for the first tool if len(tools) > 0 { - m.Annotations[AnnotationToolNameKey] = tools[0].Name - m.Annotations[AnnotationToolVersionKey] = tools[0].Version + if tools[0].Name != "" { + m.Annotations[AnnotationToolNameKey] = tools[0].Name + } + if tools[0].Version != "" { + m.Annotations[AnnotationToolVersionKey] = tools[0].Version + } } } diff --git a/pkg/attestation/crafter/materials/spdxjson_test.go b/pkg/attestation/crafter/materials/spdxjson_test.go index 66c55ebae..3b82cc763 100644 --- a/pkg/attestation/crafter/materials/spdxjson_test.go +++ b/pkg/attestation/crafter/materials/spdxjson_test.go @@ -1,5 +1,5 @@ // -// Copyright 2023-2025 The Chainloop Authors. +// Copyright 2023-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -66,12 +66,13 @@ func TestNewSPDXJSONCrafter(t *testing.T) { func TestSPDXJSONCraft(t *testing.T) { testCases := []struct { - name string - filePath string - wantErr string - wantDigest string - wantFilename string - annotations map[string]string + name string + filePath string + wantErr string + wantDigest string + wantFilename string + annotations map[string]string + absentAnnotations []string }{ { name: "invalid sbom format", @@ -99,6 +100,27 @@ func TestSPDXJSONCraft(t *testing.T) { "chainloop.material.tools": `["syft@0.73.0"]`, }, }, + { + name: "tool with empty name", + filePath: "./testdata/sbom-spdx-empty-tool-name.json", + wantDigest: "sha256:bbc5c2f78c7b05f2e80295e1bf827f24623394dafcd56a4a14f0591b835d28fa", + wantFilename: "sbom-spdx-empty-tool-name.json", + absentAnnotations: []string{ + "chainloop.material.tool.name", + "chainloop.material.tool.version", + }, + }, + { + name: "tool without version", + filePath: "./testdata/sbom-spdx-no-tool-version.json", + wantDigest: "sha256:c502fcd2944cc7aadf718af910a242cca0702387c27e740229d6e35cd1ccd52e", + wantFilename: "sbom-spdx-no-tool-version.json", + annotations: map[string]string{ + "chainloop.material.tool.name": "spdxgen", + "chainloop.material.tools": `["spdxgen"]`, + }, + absentAnnotations: []string{"chainloop.material.tool.version"}, + }, { name: "multiple tools", filePath: "./testdata/sbom-spdx-multiple-tools.json", @@ -155,6 +177,11 @@ func TestSPDXJSONCraft(t *testing.T) { assert.Equal(v, got.Annotations[k]) } } + + for _, k := range tc.absentAnnotations { + _, exists := got.Annotations[k] + assert.False(exists, "annotation %q should not be present", k) + } }) } } diff --git a/pkg/attestation/crafter/materials/testdata/sbom-spdx-empty-tool-name.json b/pkg/attestation/crafter/materials/testdata/sbom-spdx-empty-tool-name.json new file mode 100644 index 000000000..6e70f74ce --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/sbom-spdx-empty-tool-name.json @@ -0,0 +1,26 @@ +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "test-empty-tool-name", + "documentNamespace": "https://example.com/test/empty-tool-name", + "creationInfo": { + "licenseListVersion": "3.20", + "creators": [ + "Organization: Example Corp", + "Tool: " + ], + "created": "2024-01-01T10:00:00Z" + }, + "packages": [ + { + "name": "example-package", + "SPDXID": "SPDXRef-Package-example", + "versionInfo": "1.0.0", + "downloadLocation": "NOASSERTION", + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "copyrightText": "NOASSERTION" + } + ] +} diff --git a/pkg/attestation/crafter/materials/testdata/sbom-spdx-no-tool-version.json b/pkg/attestation/crafter/materials/testdata/sbom-spdx-no-tool-version.json new file mode 100644 index 000000000..e6c2c0d41 --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/sbom-spdx-no-tool-version.json @@ -0,0 +1,26 @@ +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "test-no-tool-version", + "documentNamespace": "https://example.com/test/no-tool-version", + "creationInfo": { + "licenseListVersion": "3.20", + "creators": [ + "Organization: Example Corp", + "Tool: spdxgen" + ], + "created": "2024-01-01T10:00:00Z" + }, + "packages": [ + { + "name": "example-package", + "SPDXID": "SPDXRef-Package-example", + "versionInfo": "1.0.0", + "downloadLocation": "NOASSERTION", + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "copyrightText": "NOASSERTION" + } + ] +} diff --git a/pkg/attestation/crafter/materials/testdata/sbom.cyclonedx-1.4-empty-tool-version.json b/pkg/attestation/crafter/materials/testdata/sbom.cyclonedx-1.4-empty-tool-version.json new file mode 100644 index 000000000..15408e998 --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/sbom.cyclonedx-1.4-empty-tool-version.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2025-09-28T07:00:46Z", + "tools": [ + { + "name": "Hub", + "version": "" + } + ], + "component": { + "bom-ref": "test-component", + "type": "application", + "name": "test-app", + "version": "1.0.0" + } + }, + "components": [ + { + "bom-ref": "pkg:golang/example.com/test@v1.0.0", + "type": "library", + "name": "example.com/test", + "version": "v1.0.0", + "purl": "pkg:golang/example.com/test@v1.0.0" + } + ] +} diff --git a/pkg/attestation/crafter/materials/testdata/sbom.cyclonedx-1.5-empty-tool-version.json b/pkg/attestation/crafter/materials/testdata/sbom.cyclonedx-1.5-empty-tool-version.json new file mode 100644 index 000000000..f8261f1d6 --- /dev/null +++ b/pkg/attestation/crafter/materials/testdata/sbom.cyclonedx-1.5-empty-tool-version.json @@ -0,0 +1,35 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79", + "version": 1, + "metadata": { + "timestamp": "2025-09-28T07:00:46Z", + "tools": { + "components": [ + { + "type": "application", + "author": "Black Duck", + "name": "Hub", + "version": "" + } + ] + }, + "component": { + "bom-ref": "test-component", + "type": "application", + "name": "test-app", + "version": "1.0.0" + } + }, + "components": [ + { + "bom-ref": "pkg:golang/example.com/test@v1.0.0", + "type": "library", + "name": "example.com/test", + "version": "v1.0.0", + "purl": "pkg:golang/example.com/test@v1.0.0" + } + ] +}