mirror of
https://github.com/anchore/syft.git
synced 2026-11-03 16:06:36 +01:00
fix: normalize decoded FullPath
Nothing checked that a decoded `FullPath` actually extends `RealPath` with an archive chain. The SPDX
decoder sets `FullPath` to the file name even when the name doesn't end with the comment's chain, and
syft-json takes whatever is in the document. `{RealPath: "/x", FullPath: "/x"}` and `{RealPath: "/x"}`
share an ID but are different map keys, so decoded files, unknowns and locations could fail to line
up with the same coordinates built by a resolver.
The syft-json and SPDX decoders now clear a `FullPath` that equals `RealPath` or doesn't start with
`RealPath` + `!/`.
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -549,7 +549,7 @@ func coordinatesFromFile(name, comment, id string) file.Coordinates {
|
||||
if v, ok := strings.CutPrefix(id, layerIDCommentPrefix); ok {
|
||||
c.FileSystemID = v
|
||||
}
|
||||
return c
|
||||
return internal.NormalizeCoordinates(c)
|
||||
}
|
||||
|
||||
func toSyftLocation(f *spdx.File) file.Location {
|
||||
|
||||
@@ -789,3 +789,12 @@ func Test_skipsPackagesWithGeneratedFromRelationship(t *testing.T) {
|
||||
assert.NotNil(t, s.Artifacts.Packages.Package("1"))
|
||||
assert.Nil(t, s.Artifacts.Packages.Package("1-src"))
|
||||
}
|
||||
|
||||
func Test_coordinatesFromFile_nonCanonicalChainIsDropped(t *testing.T) {
|
||||
// a chain the name doesn't end with leaves FullPath equal to RealPath, which must decode as a plain file
|
||||
got := coordinatesFromFile("/app.jar", "archivePath: other.jar!/x", "")
|
||||
assert.Equal(t, file.NewCoordinates("/app.jar", ""), got)
|
||||
|
||||
nested := coordinatesFromFile("/app.war!/lib/a.jar", "archivePath: lib/a.jar", "")
|
||||
assert.Equal(t, file.Coordinates{RealPath: "/app.war", FullPath: "/app.war!/lib/a.jar"}, nested)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
)
|
||||
|
||||
// NormalizeCoordinates clears a decoded FullPath that adds no archive chain to RealPath (one equal to
|
||||
// RealPath, or one not starting with RealPath and "!/"), so decoded coordinates compare equal to the
|
||||
// ones a resolver builds.
|
||||
func NormalizeCoordinates(c file.Coordinates) file.Coordinates {
|
||||
if !strings.HasPrefix(c.FullPath, c.RealPath+file.ArchivePathSeparator) {
|
||||
c.FullPath = ""
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// NormalizeLocations applies NormalizeCoordinates to each location in place.
|
||||
func NormalizeLocations(locations []file.Location) []file.Location {
|
||||
for i := range locations {
|
||||
locations[i].Coordinates = NormalizeCoordinates(locations[i].Coordinates)
|
||||
}
|
||||
return locations
|
||||
}
|
||||
@@ -80,7 +80,7 @@ func toSyftFiles(files []model.File) sbom.Artifacts {
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
coord := f.Location
|
||||
coord := internal.NormalizeCoordinates(f.Location)
|
||||
if f.Metadata != nil {
|
||||
fm, err := safeFileModeConvert(f.Metadata.Mode)
|
||||
if err != nil {
|
||||
@@ -163,7 +163,7 @@ func toSyftLicenses(m []model.License) (p []pkg.License) {
|
||||
SPDXExpression: l.SPDXExpression,
|
||||
Type: l.Type,
|
||||
URLs: l.URLs,
|
||||
Locations: file.NewLocationSet(l.Locations...),
|
||||
Locations: file.NewLocationSet(internal.NormalizeLocations(l.Locations)...),
|
||||
Contents: l.Contents,
|
||||
})
|
||||
}
|
||||
@@ -239,7 +239,7 @@ func toSyftRelationships(doc *model.Document, catalog *pkg.Collection, relations
|
||||
idMap[doc.Source.ID] = toSyftSource(doc.Source)
|
||||
|
||||
for _, f := range doc.Files {
|
||||
idMap[f.ID] = f.Location
|
||||
idMap[f.ID] = internal.NormalizeCoordinates(f.Location)
|
||||
}
|
||||
|
||||
var out []artifact.Relationship
|
||||
@@ -346,7 +346,7 @@ func toSyftPackage(p model.Package, idAliases map[string]string) pkg.Package {
|
||||
Name: p.Name,
|
||||
Version: p.Version,
|
||||
FoundBy: p.FoundBy,
|
||||
Locations: file.NewLocationSet(p.Locations...),
|
||||
Locations: file.NewLocationSet(internal.NormalizeLocations(p.Locations)...),
|
||||
Licenses: pkg.NewLicenseSet(toSyftLicenses(p.Licenses)...),
|
||||
Language: p.Language,
|
||||
Type: p.Type,
|
||||
|
||||
@@ -599,3 +599,35 @@ func Test_safeFileModeConvert(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_toSyftModel_fullPathIsNormalized(t *testing.T) {
|
||||
plain := file.NewCoordinates("/app.jar", "")
|
||||
nested := file.Coordinates{RealPath: "/app.war", FullPath: "/app.war!/lib/a.jar"}
|
||||
decoded := []file.Coordinates{
|
||||
{RealPath: "/app.jar", FullPath: "/app.jar"},
|
||||
{RealPath: "/app.jar", FullPath: "/elsewhere!/app.jar"},
|
||||
nested,
|
||||
}
|
||||
want := []file.Coordinates{plain, plain, nested}
|
||||
|
||||
var files []model.File
|
||||
for _, c := range decoded {
|
||||
files = append(files, model.File{ID: string(c.ID()), Location: c, Unknowns: []string{"x"}})
|
||||
}
|
||||
got := toSyftFiles(files)
|
||||
for _, c := range want {
|
||||
assert.Contains(t, got.Unknowns, c)
|
||||
}
|
||||
assert.Len(t, got.Unknowns, 2)
|
||||
|
||||
var locations []file.Location
|
||||
for _, c := range decoded {
|
||||
locations = append(locations, file.NewLocationFromCoordinates(c))
|
||||
}
|
||||
p := toSyftPackage(model.Package{PackageBasicData: model.PackageBasicData{Locations: locations}}, nil)
|
||||
var gotCoordinates []file.Coordinates
|
||||
for _, l := range p.Locations.ToSlice() {
|
||||
gotCoordinates = append(gotCoordinates, l.Coordinates)
|
||||
}
|
||||
assert.ElementsMatch(t, []file.Coordinates{plain, plain, nested}, gotCoordinates)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user