Add ability to extend the binaries cataloguers (#2469)

* Add ability to extend the binaries cataloguers

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>

* restrict binary classifier package attributes

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Laurent Goderre <laurent.goderre@docker.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Laurent Goderre 2024-01-05 15:32:07 -05:00 committed by GitHub
parent 3174a17efb
commit a16a4ad6c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 523 additions and 390 deletions

View File

@ -14,6 +14,7 @@ import (
"github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/pkg/cataloger" "github.com/anchore/syft/syft/pkg/cataloger"
binaryCataloger "github.com/anchore/syft/syft/pkg/cataloger/binary"
golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang" golangCataloger "github.com/anchore/syft/syft/pkg/cataloger/golang"
javaCataloger "github.com/anchore/syft/syft/pkg/cataloger/java" javaCataloger "github.com/anchore/syft/syft/pkg/cataloger/java"
javascriptCataloger "github.com/anchore/syft/syft/pkg/cataloger/javascript" javascriptCataloger "github.com/anchore/syft/syft/pkg/cataloger/javascript"
@ -150,6 +151,7 @@ func (cfg Catalog) ToCatalogerConfig() cataloger.Config {
Javascript: javascriptCataloger.DefaultCatalogerConfig(). Javascript: javascriptCataloger.DefaultCatalogerConfig().
WithSearchRemoteLicenses(cfg.Javascript.SearchRemoteLicenses). WithSearchRemoteLicenses(cfg.Javascript.SearchRemoteLicenses).
WithNpmBaseURL(cfg.Javascript.NpmBaseURL), WithNpmBaseURL(cfg.Javascript.NpmBaseURL),
Binary: binaryCataloger.DefaultCatalogerConfig(),
Python: pythonCataloger.CatalogerConfig{ Python: pythonCataloger.CatalogerConfig{
GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements, GuessUnpinnedRequirements: cfg.Python.GuessUnpinnedRequirements,
}, },

View File

@ -12,8 +12,20 @@ import (
const catalogerName = "binary-cataloger" const catalogerName = "binary-cataloger"
func NewCataloger() pkg.Cataloger { type CatalogerConfig struct {
return &Cataloger{} Classifiers []Classifier
}
func DefaultCatalogerConfig() CatalogerConfig {
return CatalogerConfig{
Classifiers: DefaultClassifiers(),
}
}
func NewCataloger(cfg CatalogerConfig) pkg.Cataloger {
return &Cataloger{
classifiers: cfg.Classifiers,
}
} }
// Cataloger is the cataloger responsible for surfacing evidence of a very limited set of binary files, // Cataloger is the cataloger responsible for surfacing evidence of a very limited set of binary files,
@ -21,7 +33,9 @@ func NewCataloger() pkg.Cataloger {
// binary, but rather the specific set that has been curated to be important, predominantly related to toolchain- // binary, but rather the specific set that has been curated to be important, predominantly related to toolchain-
// related runtimes like Python, Go, Java, or Node. Some exceptions can be made for widely-used binaries such // related runtimes like Python, Go, Java, or Node. Some exceptions can be made for widely-used binaries such
// as busybox. // as busybox.
type Cataloger struct{} type Cataloger struct {
classifiers []Classifier
}
// Name returns a string that uniquely describes the Cataloger // Name returns a string that uniquely describes the Cataloger
func (c Cataloger) Name() string { func (c Cataloger) Name() string {
@ -34,7 +48,7 @@ func (c Cataloger) Catalog(resolver file.Resolver) ([]pkg.Package, []artifact.Re
var packages []pkg.Package var packages []pkg.Package
var relationships []artifact.Relationship var relationships []artifact.Relationship
for _, cls := range defaultClassifiers { for _, cls := range c.classifiers {
log.WithFields("classifier", cls.Class).Trace("cataloging binaries") log.WithFields("classifier", cls.Class).Trace("cataloging binaries")
newPkgs, err := catalog(resolver, cls) newPkgs, err := catalog(resolver, cls)
if err != nil { if err != nil {
@ -71,7 +85,7 @@ func mergePackages(target *pkg.Package, extra *pkg.Package) {
target.Metadata = meta target.Metadata = meta
} }
func catalog(resolver file.Resolver, cls classifier) (packages []pkg.Package, err error) { func catalog(resolver file.Resolver, cls Classifier) (packages []pkg.Package, err error) {
locations, err := resolver.FilesByGlob(cls.FileGlob) locations, err := resolver.FilesByGlob(cls.FileGlob)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -780,7 +780,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
c := NewCataloger() c := NewCataloger(DefaultCatalogerConfig())
src, err := source.NewFromDirectoryPath(test.fixtureDir) src, err := source.NewFromDirectoryPath(test.fixtureDir)
require.NoError(t, err) require.NoError(t, err)
@ -819,7 +819,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
c := NewCataloger() c := NewCataloger(DefaultCatalogerConfig())
img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage) img := imagetest.GetFixtureImage(t, "docker-archive", test.fixtureImage)
src, err := source.NewFromStereoscopeImageObject(img, test.fixtureImage, nil) src, err := source.NewFromStereoscopeImageObject(img, test.fixtureImage, nil)
@ -850,7 +850,7 @@ func Test_Cataloger_DefaultClassifiers_PositiveCases_Image(t *testing.T) {
} }
func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) { func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
c := NewCataloger() c := NewCataloger(DefaultCatalogerConfig())
src, err := source.NewFromDirectoryPath("test-fixtures/classifiers/negative") src, err := source.NewFromDirectoryPath("test-fixtures/classifiers/negative")
assert.NoError(t, err) assert.NoError(t, err)
@ -863,6 +863,129 @@ func TestClassifierCataloger_DefaultClassifiers_NegativeCases(t *testing.T) {
assert.Equal(t, 0, len(actualResults)) assert.Equal(t, 0, len(actualResults))
} }
func Test_Cataloger_CustomClassifiers(t *testing.T) {
defaultClassifers := DefaultClassifiers()
golangExpected := pkg.Package{
Name: "go",
Version: "1.14",
PURL: "pkg:generic/go@1.14",
Locations: locations("go"),
Metadata: metadata("go-binary"),
}
customExpected := pkg.Package{
Name: "foo",
Version: "1.2.3",
PURL: "pkg:generic/foo@1.2.3",
Locations: locations("foo"),
Metadata: metadata("foo-binary"),
}
fooClassifier := Classifier{
Class: "foo-binary",
FileGlob: "**/foo",
EvidenceMatcher: FileContentsVersionMatcher(
`(?m)foobar\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "foo",
PURL: mustPURL("pkg:generic/foo@version"),
CPEs: singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"),
}
tests := []struct {
name string
config CatalogerConfig
fixtureDir string
expected *pkg.Package
}{
{
name: "empty-negative",
config: CatalogerConfig{
Classifiers: []Classifier{},
},
fixtureDir: "test-fixtures/classifiers/positive/go-1.14",
expected: nil,
},
{
name: "default-positive",
config: CatalogerConfig{
Classifiers: defaultClassifers,
},
fixtureDir: "test-fixtures/classifiers/positive/go-1.14",
expected: &golangExpected,
},
{
name: "nodefault-negative",
config: CatalogerConfig{
Classifiers: []Classifier{fooClassifier},
},
fixtureDir: "test-fixtures/classifiers/positive/go-1.14",
expected: nil,
},
{
name: "default-extended-positive",
config: CatalogerConfig{
Classifiers: append(
append([]Classifier{}, defaultClassifers...),
fooClassifier,
),
},
fixtureDir: "test-fixtures/classifiers/positive/go-1.14",
expected: &golangExpected,
},
{
name: "default-cutsom-negative",
config: CatalogerConfig{
Classifiers: append(
append([]Classifier{}, defaultClassifers...),
Classifier{
Class: "foo-binary",
FileGlob: "**/foo",
EvidenceMatcher: FileContentsVersionMatcher(`(?m)not there`),
Package: "foo",
PURL: mustPURL("pkg:generic/foo@version"),
CPEs: singleCPE("cpe:2.3:a:foo:foo:*:*:*:*:*:*:*:*"),
},
),
},
fixtureDir: "test-fixtures/classifiers/positive/custom",
expected: nil,
},
{
name: "default-cutsom-positive",
config: CatalogerConfig{
Classifiers: append(
append([]Classifier{}, defaultClassifers...),
fooClassifier,
),
},
fixtureDir: "test-fixtures/classifiers/positive/custom",
expected: &customExpected,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c := NewCataloger(test.config)
src, err := source.NewFromDirectoryPath(test.fixtureDir)
require.NoError(t, err)
resolver, err := src.FileResolver(source.SquashedScope)
require.NoError(t, err)
packages, _, err := c.Catalog(resolver)
require.NoError(t, err)
if test.expected == nil {
assert.Equal(t, 0, len(packages))
} else {
require.Len(t, packages, 1)
assertPackagesAreEqual(t, *test.expected, packages[0])
}
})
}
}
func locations(locations ...string) file.LocationSet { func locations(locations ...string) file.LocationSet {
var locs []file.Location var locs []file.Location
for _, s := range locations { for _, s := range locations {
@ -1019,7 +1142,7 @@ func (p *panicyResolver) FileMetadataByLocation(_ file.Location) (file.Metadata,
var _ file.Resolver = (*panicyResolver)(nil) var _ file.Resolver = (*panicyResolver)(nil)
func Test_Cataloger_ResilientToErrors(t *testing.T) { func Test_Cataloger_ResilientToErrors(t *testing.T) {
c := NewCataloger() c := NewCataloger(DefaultCatalogerConfig())
resolver := &panicyResolver{} resolver := &panicyResolver{}
_, _, err := c.Catalog(resolver) _, _, err := c.Catalog(resolver)

View File

@ -20,11 +20,9 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader" "github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
) )
var emptyPURL = packageurl.PackageURL{} // Classifier is a generic package classifier that can be used to match a package definition
// to a file that meets the given content criteria of the EvidenceMatcher.
// classifier is a generic package classifier that can be used to match a package definition type Classifier struct {
// to a file that meets the given content criteria of the evidenceMatcher.
type classifier struct {
Class string Class string
// FileGlob is a selector to narrow down file inspection using the **/glob* syntax // FileGlob is a selector to narrow down file inspection using the **/glob* syntax
@ -32,19 +30,13 @@ type classifier struct {
// EvidenceMatcher is what will be used to match against the file in the source // EvidenceMatcher is what will be used to match against the file in the source
// location. If the matcher returns a package, the file will be considered a candidate. // location. If the matcher returns a package, the file will be considered a candidate.
EvidenceMatcher evidenceMatcher EvidenceMatcher EvidenceMatcher
// Information below is used to specify the Package information when returned // Information below is used to specify the Package information when returned
// Package is the name to use for the package // Package is the name to use for the package
Package string Package string
// Language is the language to classify this package as
Language pkg.Language
// Type is the package type to use for the package
Type pkg.Type
// PURL is the Package URL to use when generating a package // PURL is the Package URL to use when generating a package
PURL packageurl.PackageURL PURL packageurl.PackageURL
@ -52,11 +44,11 @@ type classifier struct {
CPEs []cpe.CPE CPEs []cpe.CPE
} }
// evidenceMatcher is a function called to catalog Packages that match some sort of evidence // EvidenceMatcher is a function called to catalog Packages that match some sort of evidence
type evidenceMatcher func(resolver file.Resolver, classifier classifier, location file.Location) ([]pkg.Package, error) type EvidenceMatcher func(resolver file.Resolver, classifier Classifier, location file.Location) ([]pkg.Package, error)
func evidenceMatchers(matchers ...evidenceMatcher) evidenceMatcher { func evidenceMatchers(matchers ...EvidenceMatcher) EvidenceMatcher {
return func(resolver file.Resolver, classifier classifier, location file.Location) ([]pkg.Package, error) { return func(resolver file.Resolver, classifier Classifier, location file.Location) ([]pkg.Package, error) {
for _, matcher := range matchers { for _, matcher := range matchers {
match, err := matcher(resolver, classifier, location) match, err := matcher(resolver, classifier, location)
if err != nil { if err != nil {
@ -70,9 +62,9 @@ func evidenceMatchers(matchers ...evidenceMatcher) evidenceMatcher {
} }
} }
func fileNameTemplateVersionMatcher(fileNamePattern string, contentTemplate string) evidenceMatcher { func fileNameTemplateVersionMatcher(fileNamePattern string, contentTemplate string) EvidenceMatcher {
pat := regexp.MustCompile(fileNamePattern) pat := regexp.MustCompile(fileNamePattern)
return func(resolver file.Resolver, classifier classifier, location file.Location) ([]pkg.Package, error) { return func(resolver file.Resolver, classifier Classifier, location file.Location) ([]pkg.Package, error) {
if !pat.MatchString(location.RealPath) { if !pat.MatchString(location.RealPath) {
return nil, nil return nil, nil
} }
@ -116,9 +108,9 @@ func fileNameTemplateVersionMatcher(fileNamePattern string, contentTemplate stri
} }
} }
func fileContentsVersionMatcher(pattern string) evidenceMatcher { func FileContentsVersionMatcher(pattern string) EvidenceMatcher {
pat := regexp.MustCompile(pattern) pat := regexp.MustCompile(pattern)
return func(resolver file.Resolver, classifier classifier, location file.Location) ([]pkg.Package, error) { return func(resolver file.Resolver, classifier Classifier, location file.Location) ([]pkg.Package, error) {
contents, err := getContents(resolver, location) contents, err := getContents(resolver, location)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to get read contents for file: %w", err) return nil, fmt.Errorf("unable to get read contents for file: %w", err)
@ -136,9 +128,9 @@ func fileContentsVersionMatcher(pattern string) evidenceMatcher {
} }
//nolint:gocognit //nolint:gocognit
func sharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher evidenceMatcher) evidenceMatcher { func sharedLibraryLookup(sharedLibraryPattern string, sharedLibraryMatcher EvidenceMatcher) EvidenceMatcher {
pat := regexp.MustCompile(sharedLibraryPattern) pat := regexp.MustCompile(sharedLibraryPattern)
return func(resolver file.Resolver, classifier classifier, location file.Location) (packages []pkg.Package, _ error) { return func(resolver file.Resolver, classifier Classifier, location file.Location) (packages []pkg.Package, _ error) {
libs, err := sharedLibraries(resolver, location) libs, err := sharedLibraries(resolver, location)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -13,16 +13,16 @@ func Test_ClassifierCPEs(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
fixture string fixture string
classifier classifier classifier Classifier
cpes []string cpes []string
}{ }{
{ {
name: "no CPEs", name: "no CPEs",
fixture: "test-fixtures/version.txt", fixture: "test-fixtures/version.txt",
classifier: classifier{ classifier: Classifier{
Package: "some-app", Package: "some-app",
FileGlob: "**/version.txt", FileGlob: "**/version.txt",
EvidenceMatcher: fileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`), EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
CPEs: []cpe.CPE{}, CPEs: []cpe.CPE{},
}, },
cpes: nil, cpes: nil,
@ -30,10 +30,10 @@ func Test_ClassifierCPEs(t *testing.T) {
{ {
name: "one CPE", name: "one CPE",
fixture: "test-fixtures/version.txt", fixture: "test-fixtures/version.txt",
classifier: classifier{ classifier: Classifier{
Package: "some-app", Package: "some-app",
FileGlob: "**/version.txt", FileGlob: "**/version.txt",
EvidenceMatcher: fileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`), EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
CPEs: []cpe.CPE{ CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"), cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"),
}, },
@ -45,10 +45,10 @@ func Test_ClassifierCPEs(t *testing.T) {
{ {
name: "multiple CPEs", name: "multiple CPEs",
fixture: "test-fixtures/version.txt", fixture: "test-fixtures/version.txt",
classifier: classifier{ classifier: Classifier{
Package: "some-app", Package: "some-app",
FileGlob: "**/version.txt", FileGlob: "**/version.txt",
EvidenceMatcher: fileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`), EvidenceMatcher: FileContentsVersionMatcher(`(?m)my-verison:(?P<version>[0-9.]+)`),
CPEs: []cpe.CPE{ CPEs: []cpe.CPE{
cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"), cpe.Must("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"),
cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*"), cpe.Must("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*"),

View File

@ -2,10 +2,11 @@ package binary
import ( import (
"github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/pkg"
) )
var defaultClassifiers = []classifier{ //nolint:funlen
func DefaultClassifiers() []Classifier {
return []Classifier{
{ {
Class: "python-binary", Class: "python-binary",
FileGlob: "**/python*", FileGlob: "**/python*",
@ -40,7 +41,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "go-binary", Class: "go-binary",
FileGlob: "**/go", FileGlob: "**/go",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)\x00`), `(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)\x00`),
Package: "go", Package: "go",
PURL: mustPURL("pkg:generic/go@version"), PURL: mustPURL("pkg:generic/go@version"),
@ -49,7 +50,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "julia-binary", Class: "julia-binary",
FileGlob: "**/libjulia-internal.so", FileGlob: "**/libjulia-internal.so",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)__init__\x00(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00verify`), `(?m)__init__\x00(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00verify`),
Package: "julia", Package: "julia",
PURL: mustPURL("pkg:generic/julia@version"), PURL: mustPURL("pkg:generic/julia@version"),
@ -58,7 +59,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "helm", Class: "helm",
FileGlob: "**/helm", FileGlob: "**/helm",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)\x00v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`), `(?m)\x00v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)\x00`),
Package: "helm", Package: "helm",
PURL: mustPURL("pkg:golang/helm.sh/helm@version"), PURL: mustPURL("pkg:golang/helm.sh/helm@version"),
@ -68,8 +69,8 @@ var defaultClassifiers = []classifier{
Class: "redis-binary", Class: "redis-binary",
FileGlob: "**/redis-server", FileGlob: "**/redis-server",
EvidenceMatcher: evidenceMatchers( EvidenceMatcher: evidenceMatchers(
fileContentsVersionMatcher(`(?s)payload %5.*?(?P<version>\d.\d\.\d\d*)[a-z0-9]{12,15}-[0-9]{19}`), FileContentsVersionMatcher(`(?s)payload %5.*?(?P<version>\d.\d\.\d\d*)[a-z0-9]{12,15}-[0-9]{19}`),
fileContentsVersionMatcher(`(?s)\x00(?P<version>\d.\d\.\d\d*)[a-z0-9]{12}-[0-9]{19}\x00.*?payload %5`), FileContentsVersionMatcher(`(?s)\x00(?P<version>\d.\d\.\d\d*)[a-z0-9]{12}-[0-9]{19}\x00.*?payload %5`),
), ),
Package: "redis", Package: "redis",
PURL: mustPURL("pkg:generic/redis@version"), PURL: mustPURL("pkg:generic/redis@version"),
@ -78,7 +79,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "java-binary-openjdk", Class: "java-binary-openjdk",
FileGlob: "**/java", FileGlob: "**/java",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// [NUL]openjdk[NUL]java[NUL]0.0[NUL]11.0.17+8-LTS[NUL] // [NUL]openjdk[NUL]java[NUL]0.0[NUL]11.0.17+8-LTS[NUL]
// [NUL]openjdk[NUL]java[NUL]1.8[NUL]1.8.0_352-b08[NUL] // [NUL]openjdk[NUL]java[NUL]1.8[NUL]1.8.0_352-b08[NUL]
`(?m)\x00openjdk\x00java\x00(?P<release>[0-9]+[.0-9]*)\x00(?P<version>[0-9]+[^\x00]+)\x00`), `(?m)\x00openjdk\x00java\x00(?P<release>[0-9]+[.0-9]*)\x00(?P<version>[0-9]+[^\x00]+)\x00`),
@ -90,7 +91,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "java-binary-ibm", Class: "java-binary-ibm",
FileGlob: "**/java", FileGlob: "**/java",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// [NUL]java[NUL]1.8[NUL][NUL][NUL][NUL]1.8.0-foreman_2022_09_22_15_30-b00[NUL] // [NUL]java[NUL]1.8[NUL][NUL][NUL][NUL]1.8.0-foreman_2022_09_22_15_30-b00[NUL]
`(?m)\x00java\x00(?P<release>[0-9]+[.0-9]+)\x00{4}(?P<version>[0-9]+[-._a-zA-Z0-9]+)\x00`), `(?m)\x00java\x00(?P<release>[0-9]+[.0-9]+)\x00{4}(?P<version>[0-9]+[-._a-zA-Z0-9]+)\x00`),
Package: "java", Package: "java",
@ -100,7 +101,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "java-binary-oracle", Class: "java-binary-oracle",
FileGlob: "**/java", FileGlob: "**/java",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// [NUL]19.0.1+10-21[NUL] // [NUL]19.0.1+10-21[NUL]
`(?m)\x00(?P<version>[0-9]+[.0-9]+[+][-0-9]+)\x00`), `(?m)\x00(?P<version>[0-9]+[.0-9]+[+][-0-9]+)\x00`),
Package: "java", Package: "java",
@ -110,17 +111,16 @@ var defaultClassifiers = []classifier{
{ {
Class: "nodejs-binary", Class: "nodejs-binary",
FileGlob: "**/node", FileGlob: "**/node",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)node\.js\/v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`), `(?m)node\.js\/v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "node", Package: "node",
Language: pkg.JavaScript,
PURL: mustPURL("pkg:generic/node@version"), PURL: mustPURL("pkg:generic/node@version"),
CPEs: singleCPE("cpe:2.3:a:nodejs:node.js:*:*:*:*:*:*:*:*"), CPEs: singleCPE("cpe:2.3:a:nodejs:node.js:*:*:*:*:*:*:*:*"),
}, },
{ {
Class: "go-binary-hint", Class: "go-binary-hint",
FileGlob: "**/VERSION", FileGlob: "**/VERSION",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)`), `(?m)go(?P<version>[0-9]+\.[0-9]+(\.[0-9]+|beta[0-9]+|alpha[0-9]+|rc[0-9]+)?)`),
Package: "go", Package: "go",
PURL: mustPURL("pkg:generic/go@version"), PURL: mustPURL("pkg:generic/go@version"),
@ -128,7 +128,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "busybox-binary", Class: "busybox-binary",
FileGlob: "**/busybox", FileGlob: "**/busybox",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)BusyBox\s+v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`), `(?m)BusyBox\s+v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "busybox", Package: "busybox",
PURL: mustPURL("pkg:generic/busybox@version"), PURL: mustPURL("pkg:generic/busybox@version"),
@ -138,8 +138,8 @@ var defaultClassifiers = []classifier{
Class: "haproxy-binary", Class: "haproxy-binary",
FileGlob: "**/haproxy", FileGlob: "**/haproxy",
EvidenceMatcher: evidenceMatchers( EvidenceMatcher: evidenceMatchers(
fileContentsVersionMatcher(`(?m)HA-Proxy version (?P<version>[0-9]+\.[0-9]+\.[0-9]+)`), FileContentsVersionMatcher(`(?m)HA-Proxy version (?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
fileContentsVersionMatcher(`(?m)(?P<version>[0-9]+\.[0-9]+\.[0-9]+)-[0-9a-zA-Z]{7}.+HAProxy version`), FileContentsVersionMatcher(`(?m)(?P<version>[0-9]+\.[0-9]+\.[0-9]+)-[0-9a-zA-Z]{7}.+HAProxy version`),
), ),
Package: "haproxy", Package: "haproxy",
PURL: mustPURL("pkg:generic/haproxy@version"), PURL: mustPURL("pkg:generic/haproxy@version"),
@ -148,7 +148,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "perl-binary", Class: "perl-binary",
FileGlob: "**/perl", FileGlob: "**/perl",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)\/usr\/local\/lib\/perl\d\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`), `(?m)\/usr\/local\/lib\/perl\d\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "perl", Package: "perl",
PURL: mustPURL("pkg:generic/perl@version"), PURL: mustPURL("pkg:generic/perl@version"),
@ -167,7 +167,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "php-fpm-binary", Class: "php-fpm-binary",
FileGlob: "**/php-fpm*", FileGlob: "**/php-fpm*",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`), `(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
Package: "php-fpm", Package: "php-fpm",
PURL: mustPURL("pkg:generic/php-fpm@version"), PURL: mustPURL("pkg:generic/php-fpm@version"),
@ -176,7 +176,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "php-apache-binary", Class: "php-apache-binary",
FileGlob: "**/libphp*.so", FileGlob: "**/libphp*.so",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`), `(?m)X-Powered-By: PHP\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)`),
Package: "libphp", Package: "libphp",
PURL: mustPURL("pkg:generic/php@version"), PURL: mustPURL("pkg:generic/php@version"),
@ -185,7 +185,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "php-composer-binary", Class: "php-composer-binary",
FileGlob: "**/composer*", FileGlob: "**/composer*",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)'pretty_version'\s*=>\s*'(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)'`), `(?m)'pretty_version'\s*=>\s*'(?P<version>[0-9]+\.[0-9]+\.[0-9]+(beta[0-9]+|alpha[0-9]+|RC[0-9]+)?)'`),
Package: "composer", Package: "composer",
PURL: mustPURL("pkg:generic/composer@version"), PURL: mustPURL("pkg:generic/composer@version"),
@ -194,7 +194,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "httpd-binary", Class: "httpd-binary",
FileGlob: "**/httpd", FileGlob: "**/httpd",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)Apache\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`), `(?m)Apache\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "httpd", Package: "httpd",
PURL: mustPURL("pkg:generic/httpd@version"), PURL: mustPURL("pkg:generic/httpd@version"),
@ -203,7 +203,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "memcached-binary", Class: "memcached-binary",
FileGlob: "**/memcached", FileGlob: "**/memcached",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
`(?m)memcached\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`), `(?m)memcached\s(?P<version>[0-9]+\.[0-9]+\.[0-9]+)`),
Package: "memcached", Package: "memcached",
PURL: mustPURL("pkg:generic/memcached@version"), PURL: mustPURL("pkg:generic/memcached@version"),
@ -211,7 +211,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "traefik-binary", Class: "traefik-binary",
FileGlob: "**/traefik", FileGlob: "**/traefik",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// [NUL]v1.7.34[NUL] // [NUL]v1.7.34[NUL]
// [NUL]2.9.6[NUL] // [NUL]2.9.6[NUL]
`(?m)\x00v?(?P<version>[0-9]+\.[0-9]+\.[0-9]+(-alpha[0-9]|-beta[0-9]|-rc[0-9])?)\x00`), `(?m)\x00v?(?P<version>[0-9]+\.[0-9]+\.[0-9]+(-alpha[0-9]|-beta[0-9]|-rc[0-9])?)\x00`),
@ -221,7 +221,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "postgresql-binary", Class: "postgresql-binary",
FileGlob: "**/postgres", FileGlob: "**/postgres",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// [NUL]PostgreSQL 15beta4 // [NUL]PostgreSQL 15beta4
// [NUL]PostgreSQL 15.1 // [NUL]PostgreSQL 15.1
// [NUL]PostgreSQL 9.6.24 // [NUL]PostgreSQL 9.6.24
@ -233,7 +233,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "mysql-binary", Class: "mysql-binary",
FileGlob: "**/mysql", FileGlob: "**/mysql",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// ../../mysql-8.0.34 // ../../mysql-8.0.34
// /mysql-5.6.51/bld/client // /mysql-5.6.51/bld/client
`(?m).*/mysql-(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`), `(?m).*/mysql-(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)`),
@ -244,7 +244,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "mariadb-binary", Class: "mariadb-binary",
FileGlob: "**/mariadb", FileGlob: "**/mariadb",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// 10.6.15-MariaDB // 10.6.15-MariaDB
`(?m)(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)-MariaDB`), `(?m)(?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)?(alpha[0-9]|beta[0-9]|rc[0-9])?)-MariaDB`),
Package: "mariadb", Package: "mariadb",
@ -253,7 +253,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "rust-standard-library-linux", Class: "rust-standard-library-linux",
FileGlob: "**/libstd-????????????????.so", FileGlob: "**/libstd-????????????????.so",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// clang LLVM (rustc version 1.48.0 (7eac88abb 2020-11-16)) // clang LLVM (rustc version 1.48.0 (7eac88abb 2020-11-16))
`(?m)(\x00)clang LLVM \(rustc version (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)) \(\w+ \d{4}\-\d{2}\-\d{2}\)`), `(?m)(\x00)clang LLVM \(rustc version (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)) \(\w+ \d{4}\-\d{2}\-\d{2}\)`),
Package: "rust", Package: "rust",
@ -263,7 +263,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "rust-standard-library-macos", Class: "rust-standard-library-macos",
FileGlob: "**/libstd-????????????????.dylib", FileGlob: "**/libstd-????????????????.dylib",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// c 1.48.0 (7eac88abb 2020-11-16) // c 1.48.0 (7eac88abb 2020-11-16)
`(?m)c (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)) \(\w+ \d{4}\-\d{2}\-\d{2}\)`), `(?m)c (?P<version>[0-9]+(\.[0-9]+)?(\.[0-9]+)) \(\w+ \d{4}\-\d{2}\-\d{2}\)`),
Package: "rust", Package: "rust",
@ -287,7 +287,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "erlang-binary", Class: "erlang-binary",
FileGlob: "**/erlexec", FileGlob: "**/erlexec",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// <artificial>[NUL]/usr/local/src/otp-25.3.2.7/erts/ // <artificial>[NUL]/usr/local/src/otp-25.3.2.7/erts/
`(?m)\<artificial\>\x00/usr/local/src/otp-(?P<version>[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+?)/erts/`, `(?m)\<artificial\>\x00/usr/local/src/otp-(?P<version>[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+?)/erts/`,
), ),
@ -298,7 +298,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "consul-binary", Class: "consul-binary",
FileGlob: "**/consul", FileGlob: "**/consul",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// NOTE: This is brittle and may not work for past or future versions // NOTE: This is brittle and may not work for past or future versions
`CONSUL_VERSION: (?P<version>\d+\.\d+\.\d+)`, `CONSUL_VERSION: (?P<version>\d+\.\d+\.\d+)`,
), ),
@ -309,7 +309,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "nginx-binary", Class: "nginx-binary",
FileGlob: "**/nginx", FileGlob: "**/nginx",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// [NUL]nginx version: nginx/1.25.1 - fetches '1.25.1' // [NUL]nginx version: nginx/1.25.1 - fetches '1.25.1'
// [NUL]nginx version: openresty/1.21.4.1 - fetches '1.21.4' as this is the nginx version part // [NUL]nginx version: openresty/1.21.4.1 - fetches '1.21.4' as this is the nginx version part
`(?m)(\x00|\?)nginx version: [^\/]+\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(?:\+\d+)?(?:-\d+)?)`, `(?m)(\x00|\?)nginx version: [^\/]+\/(?P<version>[0-9]+\.[0-9]+\.[0-9]+(?:\+\d+)?(?:-\d+)?)`,
@ -324,7 +324,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "bash-binary", Class: "bash-binary",
FileGlob: "**/bash", FileGlob: "**/bash",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// @(#)Bash version 5.2.15(1) release GNU // @(#)Bash version 5.2.15(1) release GNU
// @(#)Bash version 5.2.0(1) alpha GNU // @(#)Bash version 5.2.0(1) alpha GNU
// @(#)Bash version 5.2.0(1) beta GNU // @(#)Bash version 5.2.0(1) beta GNU
@ -338,7 +338,7 @@ var defaultClassifiers = []classifier{
{ {
Class: "openssl-binary", Class: "openssl-binary",
FileGlob: "**/openssl", FileGlob: "**/openssl",
EvidenceMatcher: fileContentsVersionMatcher( EvidenceMatcher: FileContentsVersionMatcher(
// [NUL]OpenSSL 3.1.4' // [NUL]OpenSSL 3.1.4'
`\x00OpenSSL (?P<version>[0-9]+\.[0-9]+\.[0-9]+(-alpha[0-9]|-beta[0-9]|-rc[0-9])?)`, `\x00OpenSSL (?P<version>[0-9]+\.[0-9]+\.[0-9]+(-alpha[0-9]|-beta[0-9]|-rc[0-9])?)`,
), ),
@ -346,6 +346,7 @@ var defaultClassifiers = []classifier{
PURL: mustPURL("pkg:generic/openssl@version"), PURL: mustPURL("pkg:generic/openssl@version"),
CPEs: singleCPE("cpe:2.3:a:openssl:openssl:*:*:*:*:*:*:*:*"), CPEs: singleCPE("cpe:2.3:a:openssl:openssl:*:*:*:*:*:*:*:*"),
}, },
}
} }
// in both binaries and shared libraries, the version pattern is [NUL]3.11.2[NUL] // in both binaries and shared libraries, the version pattern is [NUL]3.11.2[NUL]
@ -356,7 +357,7 @@ var libpythonMatcher = fileNameTemplateVersionMatcher(
pythonVersionTemplate, pythonVersionTemplate,
) )
var rubyMatcher = fileContentsVersionMatcher( var rubyMatcher = FileContentsVersionMatcher(
// ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-linux] // ruby 3.2.1 (2023-02-08 revision 31819e82c8) [x86_64-linux]
// ruby 2.7.7p221 (2022-11-24 revision 168ec2b1e5) [x86_64-linux] // ruby 2.7.7p221 (2022-11-24 revision 168ec2b1e5) [x86_64-linux]
`(?m)ruby (?P<version>[0-9]+\.[0-9]+\.[0-9]+(p[0-9]+)?) `) `(?m)ruby (?P<version>[0-9]+\.[0-9]+\.[0-9]+(p[0-9]+)?) `)

View File

@ -3,12 +3,15 @@ package binary
import ( import (
"reflect" "reflect"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/cpe" "github.com/anchore/syft/syft/cpe"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
func newPackage(classifier classifier, location file.Location, matchMetadata map[string]string) *pkg.Package { var emptyPURL = packageurl.PackageURL{}
func newPackage(classifier Classifier, location file.Location, matchMetadata map[string]string) *pkg.Package {
version, ok := matchMetadata["version"] version, ok := matchMetadata["version"]
if !ok { if !ok {
return nil return nil
@ -42,20 +45,12 @@ func newPackage(classifier classifier, location file.Location, matchMetadata map
}, },
} }
if classifier.Type != "" {
p.Type = classifier.Type
}
if !reflect.DeepEqual(classifier.PURL, emptyPURL) { if !reflect.DeepEqual(classifier.PURL, emptyPURL) {
purl := classifier.PURL purl := classifier.PURL
purl.Version = version purl.Version = version
p.PURL = purl.ToString() p.PURL = purl.ToString()
} }
if classifier.Language != "" {
p.Language = classifier.Language
}
p.SetID() p.SetID()
return &p return &p

View File

@ -0,0 +1,3 @@
blah
foobar 1.2.3
baz

View File

@ -44,7 +44,7 @@ func ImageCatalogers(cfg Config) []pkg.Cataloger {
return filterCatalogers([]pkg.Cataloger{ return filterCatalogers([]pkg.Cataloger{
arch.NewDBCataloger(), arch.NewDBCataloger(),
alpine.NewDBCataloger(), alpine.NewDBCataloger(),
binary.NewCataloger(), binary.NewCataloger(cfg.Binary),
cpp.NewConanInfoCataloger(), cpp.NewConanInfoCataloger(),
debian.NewDBCataloger(), debian.NewDBCataloger(),
dotnet.NewDotnetPortableExecutableCataloger(), dotnet.NewDotnetPortableExecutableCataloger(),
@ -68,7 +68,7 @@ func DirectoryCatalogers(cfg Config) []pkg.Cataloger {
return filterCatalogers([]pkg.Cataloger{ return filterCatalogers([]pkg.Cataloger{
arch.NewDBCataloger(), arch.NewDBCataloger(),
alpine.NewDBCataloger(), alpine.NewDBCataloger(),
binary.NewCataloger(), binary.NewCataloger(cfg.Binary),
cpp.NewConanCataloger(), cpp.NewConanCataloger(),
dart.NewPubspecLockCataloger(), dart.NewPubspecLockCataloger(),
debian.NewDBCataloger(), debian.NewDBCataloger(),
@ -107,7 +107,7 @@ func AllCatalogers(cfg Config) []pkg.Cataloger {
return filterCatalogers([]pkg.Cataloger{ return filterCatalogers([]pkg.Cataloger{
arch.NewDBCataloger(), arch.NewDBCataloger(),
alpine.NewDBCataloger(), alpine.NewDBCataloger(),
binary.NewCataloger(), binary.NewCataloger(cfg.Binary),
cpp.NewConanCataloger(), cpp.NewConanCataloger(),
dart.NewPubspecLockCataloger(), dart.NewPubspecLockCataloger(),
debian.NewDBCataloger(), debian.NewDBCataloger(),

View File

@ -2,6 +2,7 @@ package cataloger
import ( import (
"github.com/anchore/syft/syft/cataloging" "github.com/anchore/syft/syft/cataloging"
"github.com/anchore/syft/syft/pkg/cataloger/binary"
"github.com/anchore/syft/syft/pkg/cataloger/golang" "github.com/anchore/syft/syft/pkg/cataloger/golang"
"github.com/anchore/syft/syft/pkg/cataloger/java" "github.com/anchore/syft/syft/pkg/cataloger/java"
"github.com/anchore/syft/syft/pkg/cataloger/javascript" "github.com/anchore/syft/syft/pkg/cataloger/javascript"
@ -17,6 +18,7 @@ type Config struct {
Python python.CatalogerConfig Python python.CatalogerConfig
Java java.ArchiveCatalogerConfig Java java.ArchiveCatalogerConfig
Javascript javascript.CatalogerConfig Javascript javascript.CatalogerConfig
Binary binary.CatalogerConfig
Catalogers []string Catalogers []string
Parallelism int Parallelism int
ExcludeBinaryOverlapByOwnership bool ExcludeBinaryOverlapByOwnership bool
@ -30,6 +32,7 @@ func DefaultConfig() Config {
Python: python.DefaultCatalogerConfig(), Python: python.DefaultCatalogerConfig(),
Java: java.DefaultArchiveCatalogerConfig(), Java: java.DefaultArchiveCatalogerConfig(),
Javascript: javascript.DefaultCatalogerConfig(), Javascript: javascript.DefaultCatalogerConfig(),
Binary: binary.DefaultCatalogerConfig(),
ExcludeBinaryOverlapByOwnership: true, ExcludeBinaryOverlapByOwnership: true,
} }
} }