From 0296719091ad79d9382ef18be5ef20159970f9be Mon Sep 17 00:00:00 2001 From: Eljees <3.14hell@gmail.com> Date: Tue, 28 Jul 2026 11:54:07 +0300 Subject: [PATCH] fix(cyclonedx): decode evidence occurrence locations Signed-off-by: Eljees <3.14hell@gmail.com> --- syft/format/cyclonedxjson/decoder_test.go | 32 +++++++++ .../cyclonedxutil/helpers/component.go | 18 ++++- .../cyclonedxutil/helpers/component_test.go | 66 +++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) diff --git a/syft/format/cyclonedxjson/decoder_test.go b/syft/format/cyclonedxjson/decoder_test.go index 53a57b453b0..e8c9e7af526 100644 --- a/syft/format/cyclonedxjson/decoder_test.go +++ b/syft/format/cyclonedxjson/decoder_test.go @@ -88,6 +88,38 @@ func TestDecoder_Decode(t *testing.T) { } } +func TestDecoder_DecodeEvidenceOccurrenceLocation(t *testing.T) { + reader := strings.NewReader(`{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "components": [ + { + "type": "library", + "bom-ref": "example@1.0.0", + "name": "example", + "version": "1.0.0", + "evidence": { + "occurrences": [ + {"location": "/usr/bin/example"} + ] + } + } + ] + }`) + + bom, _, _, err := NewFormatDecoder().Decode(reader) + require.NoError(t, err) + + packages := bom.Artifacts.Packages.Sorted() + require.Len(t, packages, 1) + + locations := packages[0].Locations.ToSlice() + require.Len(t, locations, 1) + assert.Equal(t, "/usr/bin/example", locations[0].RealPath) + assert.Equal(t, "/usr/bin/example", locations[0].AccessPath) +} + func TestDecoder_Identify(t *testing.T) { type testCase struct { name string diff --git a/syft/format/internal/cyclonedxutil/helpers/component.go b/syft/format/internal/cyclonedxutil/helpers/component.go index 2e289fc8461..12a20cb7ad4 100644 --- a/syft/format/internal/cyclonedxutil/helpers/component.go +++ b/syft/format/internal/cyclonedxutil/helpers/component.go @@ -102,7 +102,7 @@ func decodeComponent(c *cyclonedx.Component) *pkg.Package { p := &pkg.Package{ Version: c.Version, - Locations: decodeLocations(values), + Locations: decodeLocations(values, c.Evidence), Licenses: pkg.NewLicenseSet(decodeLicenses(c)...), CPEs: decodeCPEs(c), } @@ -204,13 +204,25 @@ func setPackageName(p *pkg.Package, c *cyclonedx.Component) { p.Name = name } -func decodeLocations(vals map[string]string) file.LocationSet { +func decodeLocations(vals map[string]string, evidence *cyclonedx.Evidence) file.LocationSet { v := Decode(reflect.TypeFor[[]file.Location](), vals, "syft:location", CycloneDXFields) out, ok := v.([]file.Location) if !ok { out = nil } - return file.NewLocationSet(out...) + + locations := file.NewLocationSet(out...) + if evidence == nil || evidence.Occurrences == nil { + return locations + } + + for _, occurrence := range *evidence.Occurrences { + if occurrence.Location != "" { + locations.Add(file.NewLocation(occurrence.Location)) + } + } + + return locations } func decodePackageMetadata(vals map[string]string, c *cyclonedx.Component, typeName string) any { diff --git a/syft/format/internal/cyclonedxutil/helpers/component_test.go b/syft/format/internal/cyclonedxutil/helpers/component_test.go index 2b1f44421a6..2bc007ff20a 100644 --- a/syft/format/internal/cyclonedxutil/helpers/component_test.go +++ b/syft/format/internal/cyclonedxutil/helpers/component_test.go @@ -385,6 +385,72 @@ func Test_decodeComponent(t *testing.T) { } } +func Test_decodeComponentLocations(t *testing.T) { + tests := []struct { + name string + component cyclonedx.Component + wantPaths []string + }{ + { + name: "decode CycloneDX evidence occurrences", + component: cyclonedx.Component{ + Name: "example", + Evidence: evidenceWithOccurrences( + "/usr/bin/example", + "/opt/example", + ), + }, + wantPaths: []string{"/opt/example", "/usr/bin/example"}, + }, + { + name: "combine Syft properties and evidence occurrences", + component: cyclonedx.Component{ + Name: "example", + Properties: &[]cyclonedx.Property{ + { + Name: "syft:location:0:path", + Value: "/usr/lib/example", + }, + }, + Evidence: evidenceWithOccurrences("/usr/bin/example"), + }, + wantPaths: []string{"/usr/bin/example", "/usr/lib/example"}, + }, + { + name: "ignore empty and duplicate evidence locations", + component: cyclonedx.Component{ + Name: "example", + Evidence: evidenceWithOccurrences( + "/usr/bin/example", + "", + "/usr/bin/example", + ), + }, + wantPaths: []string{"/usr/bin/example"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := decodeComponent(&tt.component) + locations := p.Locations.ToSlice() + paths := make([]string, len(locations)) + for i, location := range locations { + paths[i] = location.RealPath + } + assert.ElementsMatch(t, tt.wantPaths, paths) + }) + } +} + +func evidenceWithOccurrences(locations ...string) *cyclonedx.Evidence { + occurrences := make([]cyclonedx.EvidenceOccurrence, len(locations)) + for i, location := range locations { + occurrences[i] = cyclonedx.EvidenceOccurrence{Location: location} + } + return &cyclonedx.Evidence{Occurrences: &occurrences} +} + func Test_setPackageName(t *testing.T) { tests := []struct { name string