mirror of
https://github.com/anchore/syft.git
synced 2026-11-03 16:06:36 +01:00
fix: align binary java detection with jvm cataloger + support IBM (#4046)
Signed-off-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package binutils
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// BranchingEvidenceMatcher loads the contents of the reader into memory, assuming that all
|
||||
// classifier matchers are going to be reading the same file, e.g. a "java" binary, continuing to
|
||||
// try classifier EvidenceMatcher until the first set of packages is found
|
||||
func BranchingEvidenceMatcher(classifiers ...Classifier) EvidenceMatcher {
|
||||
return func(_ Classifier, context MatcherContext) ([]pkg.Package, error) {
|
||||
// we are scanning the same contents multiple times, so read it into memory for a reader that can reset
|
||||
rdr, err := getReader(context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer internal.CloseAndLogError(rdr, context.Location.RealPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, c := range classifiers {
|
||||
pkgs, err := c.EvidenceMatcher(c, MatcherContext{
|
||||
Resolver: context.Resolver,
|
||||
Location: context.Location,
|
||||
GetReader: func(_ MatcherContext) (unionreader.UnionReader, error) {
|
||||
_, err := rdr.Seek(0, io.SeekStart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rdr, nil
|
||||
},
|
||||
})
|
||||
if len(pkgs) > 0 || err != nil {
|
||||
return pkgs, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package binutils
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/unionreader"
|
||||
)
|
||||
|
||||
func Test_BranchingMatcher(t *testing.T) {
|
||||
matchingTest := FileContentsVersionMatcher("", `my-verison:(?<version>\d+\.\d+)`)
|
||||
notMatchingTest := MatchPath("**/not-version*")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
matcher EvidenceMatcher
|
||||
expectedPackageNames []string
|
||||
}{
|
||||
{
|
||||
name: "not matching",
|
||||
matcher: BranchingEvidenceMatcher(
|
||||
Classifier{
|
||||
EvidenceMatcher: MatchAll(
|
||||
notMatchingTest,
|
||||
matchingTest,
|
||||
),
|
||||
Package: "a-pkg",
|
||||
},
|
||||
),
|
||||
expectedPackageNames: nil,
|
||||
},
|
||||
{
|
||||
name: "both match",
|
||||
matcher: BranchingEvidenceMatcher(
|
||||
Classifier{
|
||||
EvidenceMatcher: matchingTest,
|
||||
Package: "a-pkg",
|
||||
},
|
||||
Classifier{
|
||||
EvidenceMatcher: matchingTest,
|
||||
Package: "b-pkg",
|
||||
},
|
||||
),
|
||||
expectedPackageNames: []string{"a-pkg"},
|
||||
},
|
||||
{
|
||||
name: "first-does-not-match",
|
||||
matcher: BranchingEvidenceMatcher(
|
||||
Classifier{
|
||||
EvidenceMatcher: MatchAll(
|
||||
notMatchingTest,
|
||||
matchingTest,
|
||||
),
|
||||
Package: "b-pkg",
|
||||
},
|
||||
Classifier{
|
||||
EvidenceMatcher: matchingTest,
|
||||
Package: "c-pkg",
|
||||
},
|
||||
),
|
||||
expectedPackageNames: []string{"c-pkg"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
resolver := file.NewMockResolverForPaths("test-fixtures/version.txt", "test-fixtures/version-parts.txt")
|
||||
locs, err := resolver.FilesByGlob("**/version.txt")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, locs, 1)
|
||||
loc := locs[0]
|
||||
rdr, err := resolver.FileContentsByLocation(loc)
|
||||
require.NoError(t, err)
|
||||
urdr, err := unionreader.GetUnionReader(rdr)
|
||||
require.NoError(t, err)
|
||||
pkgs, err := test.matcher(Classifier{
|
||||
Package: "a-pkg",
|
||||
}, MatcherContext{
|
||||
Resolver: resolver,
|
||||
Location: loc,
|
||||
GetReader: func(resolver MatcherContext) (unionreader.UnionReader, error) {
|
||||
return urdr, nil
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
var got []string
|
||||
for i := range pkgs {
|
||||
got = append(got, pkgs[i].Name)
|
||||
}
|
||||
require.EqualValues(t, test.expectedPackageNames, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,14 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/bmatcuk/doublestar/v4"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -71,7 +74,8 @@ func (cfg Classifier) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// EvidenceMatcher is a function called to catalog Packages that match some sort of evidence
|
||||
// EvidenceMatcher is a function called to identify based on some sort of evidence in the filesystem contents.
|
||||
// A non-nil return value indicates a successful match, regardless of packages being returned.
|
||||
type EvidenceMatcher func(classifier Classifier, context MatcherContext) ([]pkg.Package, error)
|
||||
|
||||
type MatcherContext struct {
|
||||
@@ -80,13 +84,16 @@ type MatcherContext struct {
|
||||
GetReader func(resolver MatcherContext) (unionreader.UnionReader, error)
|
||||
}
|
||||
|
||||
func EvidenceMatchers(matchers ...EvidenceMatcher) EvidenceMatcher {
|
||||
// MatchAny returns a combined evidence matcher that returns results from the first
|
||||
// matcher that returns results
|
||||
func MatchAny(matchers ...EvidenceMatcher) EvidenceMatcher {
|
||||
return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) {
|
||||
for _, matcher := range matchers {
|
||||
match, err := matcher(classifier, context)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// only return when results
|
||||
if match != nil {
|
||||
return match, nil
|
||||
}
|
||||
@@ -95,6 +102,23 @@ func EvidenceMatchers(matchers ...EvidenceMatcher) EvidenceMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
// MatchAll executes all matchers until one returns nil results, only returning the final results
|
||||
func MatchAll(matchers ...EvidenceMatcher) EvidenceMatcher {
|
||||
return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) {
|
||||
var out []pkg.Package
|
||||
for _, matcher := range matchers {
|
||||
match, err := matcher(classifier, context)
|
||||
if match == nil || err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(match) > 0 {
|
||||
out = match
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
}
|
||||
|
||||
type ContextualEvidenceMatchers struct {
|
||||
CatalogerName string
|
||||
}
|
||||
@@ -103,8 +127,8 @@ func (c ContextualEvidenceMatchers) FileNameTemplateVersionMatcher(fileNamePatte
|
||||
return FileNameTemplateVersionMatcher(fileNamePattern, contentTemplate, c.CatalogerName)
|
||||
}
|
||||
|
||||
func (c ContextualEvidenceMatchers) FileContentsVersionMatcher(pattern string) EvidenceMatcher {
|
||||
return FileContentsVersionMatcher(pattern, c.CatalogerName)
|
||||
func (c ContextualEvidenceMatchers) FileContentsVersionMatcher(patterns ...string) EvidenceMatcher {
|
||||
return FileContentsVersionMatcher(c.CatalogerName, patterns...)
|
||||
}
|
||||
|
||||
func FileNameTemplateVersionMatcher(fileNamePattern, contentTemplate, catalogerName string) EvidenceMatcher {
|
||||
@@ -156,17 +180,36 @@ func FileNameTemplateVersionMatcher(fileNamePattern, contentTemplate, catalogerN
|
||||
}
|
||||
}
|
||||
|
||||
func FileContentsVersionMatcher(pattern, catalogerName string) EvidenceMatcher {
|
||||
pat := regexp.MustCompile(pattern)
|
||||
// FileContentsVersionMatcher will match all provided patterns, extracting named capture groups from each pattern, overwriting earlier results
|
||||
func FileContentsVersionMatcher(catalogerName string, patterns ...string) EvidenceMatcher {
|
||||
if len(patterns) == 0 {
|
||||
panic("must specify at least one pattern")
|
||||
}
|
||||
var pats []*regexp.Regexp
|
||||
for _, pattern := range patterns {
|
||||
pats = append(pats, regexp.MustCompile(pattern))
|
||||
}
|
||||
return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) {
|
||||
contents, err := getReader(context)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get read contents for file: %w", err)
|
||||
}
|
||||
var matchMetadata map[string]string
|
||||
|
||||
matchMetadata, err := internal.MatchNamedCaptureGroupsFromReader(pat, contents)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to match version: %w", err)
|
||||
for _, pat := range pats {
|
||||
contents, err := getReader(context)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get read contents for file: %w", err)
|
||||
}
|
||||
|
||||
match, err := internal.MatchNamedCaptureGroupsFromReader(pat, contents)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to match version: %w", err)
|
||||
}
|
||||
if match == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if matchMetadata == nil {
|
||||
matchMetadata = match
|
||||
} else {
|
||||
maps.Copy(matchMetadata, match)
|
||||
}
|
||||
}
|
||||
|
||||
// Convert {major: 1, minor: 2, patch: 3} to "1.2.3"
|
||||
@@ -187,6 +230,10 @@ func FileContentsVersionMatcher(pattern, catalogerName string) EvidenceMatcher {
|
||||
|
||||
p := NewClassifierPackage(classifier, context.Location, matchMetadata, catalogerName)
|
||||
if p == nil {
|
||||
if matchMetadata != nil {
|
||||
// if we had a successful metadata match, but no packages, return a successful match result
|
||||
return []pkg.Package{}, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -194,29 +241,6 @@ func FileContentsVersionMatcher(pattern, catalogerName string) EvidenceMatcher {
|
||||
}
|
||||
}
|
||||
|
||||
// MatchExcluding tests the provided regular expressions against the file, and if matched, DOES NOT return
|
||||
// anything that the matcher would otherwise return
|
||||
func MatchExcluding(matcher EvidenceMatcher, contentPatternsToExclude ...string) EvidenceMatcher {
|
||||
var nonMatchPatterns []*regexp.Regexp
|
||||
for _, p := range contentPatternsToExclude {
|
||||
nonMatchPatterns = append(nonMatchPatterns, regexp.MustCompile(p))
|
||||
}
|
||||
return func(classifier Classifier, context MatcherContext) ([]pkg.Package, error) {
|
||||
contents, err := getReader(context)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get read contents for file: %w", err)
|
||||
}
|
||||
matches, err := internal.MatchAnyFromReader(contents, nonMatchPatterns...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to match content: %w", err)
|
||||
}
|
||||
if matches {
|
||||
return nil, nil
|
||||
}
|
||||
return matcher(classifier, context)
|
||||
}
|
||||
}
|
||||
|
||||
func SharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher EvidenceMatcher) EvidenceMatcher {
|
||||
pat := regexp.MustCompile(sharedLibraryPattern)
|
||||
return func(classifier Classifier, context MatcherContext) (packages []pkg.Package, _ error) {
|
||||
@@ -234,16 +258,19 @@ func SharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher Evide
|
||||
return nil, err
|
||||
}
|
||||
for _, libraryLocation := range locations {
|
||||
// create a new resolver without the cached context lookup -- this is decidedly a different file
|
||||
newResolver := MatcherContext{
|
||||
Resolver: context.Resolver,
|
||||
Location: libraryLocation,
|
||||
GetReader: context.GetReader,
|
||||
Resolver: context.Resolver,
|
||||
Location: libraryLocation,
|
||||
}
|
||||
newResolver.Location = libraryLocation
|
||||
pkgs, err := sharedLibraryMatcher(classifier, newResolver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// not a successful match
|
||||
if pkgs == nil {
|
||||
continue
|
||||
}
|
||||
for _, p := range pkgs {
|
||||
// set the source binary as the first location
|
||||
locationSet := file.NewLocationSet(context.Location)
|
||||
@@ -260,12 +287,28 @@ func SharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher Evide
|
||||
}
|
||||
packages = append(packages, p)
|
||||
}
|
||||
// return non-nil package results as a successful match indication if the evidence matcher returned a successful match indication
|
||||
if packages == nil {
|
||||
packages = pkgs
|
||||
}
|
||||
}
|
||||
}
|
||||
return packages, nil
|
||||
}
|
||||
}
|
||||
|
||||
func MatchPath(path string) EvidenceMatcher {
|
||||
if !doublestar.ValidatePattern(path) {
|
||||
panic("invalid pattern")
|
||||
}
|
||||
return func(_ Classifier, context MatcherContext) ([]pkg.Package, error) {
|
||||
if doublestar.MatchUnvalidated(path, context.Location.RealPath) {
|
||||
return []pkg.Package{}, nil // return non-nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func getReader(context MatcherContext) (unionreader.UnionReader, error) {
|
||||
if context.GetReader != nil {
|
||||
return context.GetReader(context)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package binutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"text/template"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/cpe"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -21,8 +24,8 @@ func NewClassifierPackage(classifier Classifier, location file.Location, matchMe
|
||||
|
||||
var cpes []cpe.CPE
|
||||
for _, c := range classifier.CPEs {
|
||||
c.Attributes.Version = version
|
||||
c.Attributes.Update = update
|
||||
c.Attributes.Version = templatedUpdate(c.Attributes.Version, matchMetadata, version)
|
||||
c.Attributes.Update = templatedUpdate(c.Attributes.Update, matchMetadata, update)
|
||||
cpes = append(cpes, c)
|
||||
}
|
||||
|
||||
@@ -55,3 +58,28 @@ func NewClassifierPackage(classifier Classifier, location file.Location, matchMe
|
||||
|
||||
return &p
|
||||
}
|
||||
|
||||
func templatedUpdate(providedValue string, matchMetadata map[string]string, defaultValue string) string {
|
||||
// if no template provided, just use the value directly
|
||||
if providedValue == "" {
|
||||
return defaultValue
|
||||
}
|
||||
// support templated updates
|
||||
t, err := template.New("").Option("missingkey=zero").Parse(providedValue)
|
||||
if err != nil {
|
||||
log.Debugf("unable to parse classifier template=%q : %w", providedValue, err)
|
||||
} else {
|
||||
update := bytes.Buffer{}
|
||||
err = t.Execute(&update, matchMetadata)
|
||||
if err != nil {
|
||||
log.Debugf("unable to render template: %w", err)
|
||||
} else {
|
||||
// only use the template result if it's non-empty
|
||||
providedValue = update.String()
|
||||
if providedValue != "" {
|
||||
return providedValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func Test_ClassifierCPEs(t *testing.T) {
|
||||
classifier: Classifier{
|
||||
Package: "some-app",
|
||||
FileGlob: "**/version.txt",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`, "cataloger-name"),
|
||||
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-verison:(?P<version>[0-9.]+)`),
|
||||
CPEs: []cpe.CPE{},
|
||||
},
|
||||
cpes: nil,
|
||||
@@ -38,7 +38,7 @@ func Test_ClassifierCPEs(t *testing.T) {
|
||||
classifier: Classifier{
|
||||
Package: "some-app",
|
||||
FileGlob: "**/version.txt",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`, "cataloger-name"),
|
||||
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-verison:(?P<version>[0-9.]+)`),
|
||||
CPEs: []cpe.CPE{
|
||||
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*", cpe.GeneratedSource),
|
||||
},
|
||||
@@ -53,7 +53,7 @@ func Test_ClassifierCPEs(t *testing.T) {
|
||||
classifier: Classifier{
|
||||
Package: "some-app",
|
||||
FileGlob: "**/version.txt",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`, "cataloger-name"),
|
||||
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)my-verison:(?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),
|
||||
@@ -70,7 +70,7 @@ func Test_ClassifierCPEs(t *testing.T) {
|
||||
classifier: Classifier{
|
||||
Package: "some-app",
|
||||
FileGlob: "**/version-parts.txt",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(`(?m)\x00(?P<major>[0-9.]+)\x00(?P<minor>[0-9.]+)\x00(?P<patch>[0-9.]+)\x00`, "cataloger-name"),
|
||||
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", `(?m)\x00(?P<major>[0-9.]+)\x00(?P<minor>[0-9.]+)\x00(?P<patch>[0-9.]+)\x00`),
|
||||
CPEs: []cpe.CPE{},
|
||||
},
|
||||
cpes: nil,
|
||||
@@ -113,7 +113,7 @@ func TestClassifier_MarshalJSON(t *testing.T) {
|
||||
classifier: Classifier{
|
||||
Class: "class",
|
||||
FileGlob: "glob",
|
||||
EvidenceMatcher: FileContentsVersionMatcher(".thing", "cataloger-name"),
|
||||
EvidenceMatcher: FileContentsVersionMatcher("cataloger-name", ".thing"),
|
||||
Package: "pkg",
|
||||
PURL: packageurl.PackageURL{
|
||||
Type: "type",
|
||||
@@ -168,7 +168,7 @@ func TestFileContentsVersionMatcher(t *testing.T) {
|
||||
mockGetContent := func(context MatcherContext) (unionreader.UnionReader, error) {
|
||||
return unionreader.GetUnionReader(io.NopCloser(bytes.NewBufferString(tt.data)))
|
||||
}
|
||||
fn := FileContentsVersionMatcher(tt.pattern, "cataloger-name")
|
||||
fn := FileContentsVersionMatcher("cataloger-name", tt.pattern)
|
||||
p, err := fn(Classifier{}, MatcherContext{
|
||||
GetReader: mockGetContent,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user