fix: correctly report supporting evidence for binary packages (#4558)

Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
Keith Zantow
2026-01-23 13:01:12 -05:00
committed by GitHub
parent 27b1219e98
commit 836f358cd4
12 changed files with 177 additions and 56 deletions
@@ -10,7 +10,7 @@ import (
)
func Test_BranchingMatcher(t *testing.T) {
matchingTest := FileContentsVersionMatcher("", `my-verison:(?<version>\d+\.\d+)`)
matchingTest := FileContentsVersionMatcher("", `my-version:(?<version>\d+\.\d+)`)
notMatchingTest := MatchPath("**/not-version*")
tests := []struct {
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"maps"
"path"
"regexp"
"strconv"
"strings"
@@ -273,9 +274,7 @@ func SharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher Evide
}
for _, p := range pkgs {
// set the source binary as the first location
locationSet := file.NewLocationSet(context.Location)
locationSet.Add(p.Locations.ToSlice()...)
p.Locations = locationSet
makePrimaryLocation(&p, context.Location)
meta, _ := p.Metadata.(pkg.BinarySignature)
p.Metadata = pkg.BinarySignature{
Matches: append([]pkg.ClassifierMatch{
@@ -309,6 +308,28 @@ func MatchPath(path string) EvidenceMatcher {
}
}
// SupportingEvidenceMatcher defines an evidence matcher that searches for secondary evidence with path globs
// relative to a primary file, for example: a VERSION file in the same or a parent directory to another binary
func SupportingEvidenceMatcher(relativePathGlob string, evidenceMatcher EvidenceMatcher) EvidenceMatcher {
return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) {
f := path.Dir(context.Location.RealPath)
f = path.Join(f, relativePathGlob)
f = path.Clean(f)
// this would ideally be RelativeFileByPath but with a glob search:
relativeFiles, err := context.Resolver.FilesByGlob(f)
if err != nil {
return nil, err
}
for _, relativeFile := range relativeFiles {
evidence, err := collectSupportingEvidence(classifier, context, relativeFile, evidenceMatcher)
if evidence != nil || err != nil {
return evidence, err
}
}
return nil, nil
}
}
func getReader(context MatcherContext) (unionreader.UnionReader, error) {
if context.GetReader != nil {
return context.GetReader(context)
@@ -368,3 +389,43 @@ func sharedLibraries(context MatcherContext) ([]string, error) {
return nil, nil
}
func makePrimaryLocation(p *pkg.Package, primaryLocation file.Location) {
locationSet := file.NewLocationSet(primaryLocation.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation))
for _, l := range p.Locations.ToSlice() {
if locationSet.Contains(l) { // no need for duplicate locations
continue
}
locationSet.Add(l.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.SupportingEvidenceAnnotation))
}
p.Locations = locationSet
}
func collectSupportingEvidence(classifier Classifier, context MatcherContext, relativeFile file.Location, evidenceMatcher EvidenceMatcher) ([]pkg.Package, error) {
rdr, err := context.Resolver.FileContentsByLocation(relativeFile)
if err != nil {
return nil, err
}
defer internal.CloseAndLogError(rdr, relativeFile.Path())
ur, err := unionreader.GetUnionReader(rdr)
if err != nil {
return nil, err
}
newContext := MatcherContext{
Resolver: context.Resolver,
Location: relativeFile,
GetReader: func(_ MatcherContext) (unionreader.UnionReader, error) {
return ur, nil
},
}
packages, err := evidenceMatcher(classifier, newContext)
if err != nil {
return nil, err
}
for i := range packages {
p := &(packages[i])
// relative files are supporting evidence, like a VERSION file near a go binary, mark the results as supporting
makePrimaryLocation(p, context.Location)
}
return packages, nil
}
@@ -12,6 +12,8 @@ import (
"github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/internal/unionreader"
"github.com/anchore/syft/syft/source"
"github.com/anchore/syft/syft/source/directorysource"
)
func Test_ClassifierCPEs(t *testing.T) {
@@ -27,7 +29,7 @@ func Test_ClassifierCPEs(t *testing.T) {
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version.txt",
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-verison:(?P<version>[0-9.]+)`),
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`),
CPEs: []cpe.CPE{},
},
cpes: nil,
@@ -38,7 +40,7 @@ func Test_ClassifierCPEs(t *testing.T) {
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version.txt",
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-verison:(?P<version>[0-9.]+)`),
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
},
@@ -53,7 +55,7 @@ func Test_ClassifierCPEs(t *testing.T) {
classifier: Classifier{
Package: "some-app",
FileGlob: "**/version.txt",
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-verison:(?P<version>[0-9.]+)`),
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`),
CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
@@ -183,3 +185,61 @@ func TestFileContentsVersionMatcher(t *testing.T) {
})
}
}
func Test_SupportingEvidenceMatcher(t *testing.T) {
tests := []struct {
name string
classifier Classifier
expected string
}{
{
name: "simple version string regexp",
classifier: Classifier{
FileGlob: "**/some-binary",
EvidenceMatcher: SupportingEvidenceMatcher("../version.txt",
FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>[0-9.]+)`)),
Package: "some-binary",
},
expected: "1.8",
},
{
name: "not matching version string regexp",
classifier: Classifier{
FileGlob: "**/some-binary",
EvidenceMatcher: SupportingEvidenceMatcher("../version.txt",
FileContentsVersionMatcher("cataloger-name", `(?m)my-version:(?P<version>abdd)`)),
Package: "some-binary",
},
expected: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s, err := directorysource.NewFromPath("test-fixtures")
require.NoError(t, err)
r, err := s.FileResolver(source.AllLayersScope)
require.NoError(t, err)
results, err := r.FilesByGlob(tt.classifier.FileGlob)
require.NoError(t, err)
for _, result := range results {
got, err := tt.classifier.EvidenceMatcher(tt.classifier, MatcherContext{
Resolver: r,
Location: result,
GetReader: func(ctx MatcherContext) (unionreader.UnionReader, error) {
return getReader(ctx)
},
})
require.NoError(t, err)
if tt.expected != "" {
require.NotEmpty(t, got)
require.Equal(t, tt.expected, got[0].Version)
} else {
require.Empty(t, got)
}
}
})
}
}
@@ -0,0 +1 @@
a binary
@@ -1 +1 @@
my-verison:1.8
my-version:1.8