feat: Generic Binary Cataloger (#1336)

This commit is contained in:
Keith Zantow
2022-11-29 18:28:10 -05:00
committed by GitHub
parent 7a69e2129b
commit 4f39287216
36 changed files with 579 additions and 740 deletions
-100
View File
@@ -1,100 +0,0 @@
package generic
import (
"fmt"
"io"
"path"
"regexp"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/unionreader"
"github.com/anchore/syft/syft/source"
)
// 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 EvidencePatternTemplates.
type Classifier struct {
Package string
// FilepathPatterns is a list of regular expressions that will be used to match against the file path of a given
// source location. If any of the patterns match, the file will be considered a candidate for parsing.
// If no patterns are provided, the reader is automatically considered a candidate.
FilepathPatterns []*regexp.Regexp
// EvidencePatterns is a list of regular expressions that will be used to match against the file contents of a
// given file in the source location. If any of the patterns match, the file will be considered a candidate for parsing.
EvidencePatterns []*regexp.Regexp
// CPEs are the specific CPEs we want to include for this binary with updated version information
CPEs []pkg.CPE
}
func (c Classifier) Examine(reader source.LocationReadCloser) (p *pkg.Package, r *artifact.Relationship, err error) {
doesFilepathMatch := true
if len(c.FilepathPatterns) > 0 {
doesFilepathMatch, _ = file.FilepathMatches(c.FilepathPatterns, reader.Location)
}
if !doesFilepathMatch {
return nil, nil, fmt.Errorf("location: %s did not match any patterns for package=%q", reader.Location, c.Package)
}
contents, err := getContents(reader)
if err != nil {
return nil, nil, fmt.Errorf("unable to get read contents for file: %w", err)
}
var classifiedPackage *pkg.Package
for _, evidencePattern := range c.EvidencePatterns {
if !evidencePattern.Match(contents) {
continue
}
matchMetadata := internal.MatchNamedCaptureGroups(evidencePattern, string(contents))
version, ok := matchMetadata["version"]
if !ok {
log.Debugf("no version found in binary from pattern %v", evidencePattern)
continue
}
var cpes []pkg.CPE
for _, cpe := range c.CPEs {
cpe.Version = version
if err == nil {
cpes = append(cpes, cpe)
}
}
classifiedPackage = &pkg.Package{
Name: path.Base(reader.VirtualPath),
Version: version,
Language: pkg.Binary,
Locations: source.NewLocationSet(reader.Location),
Type: pkg.BinaryPkg,
CPEs: cpes,
MetadataType: pkg.BinaryMetadataType,
Metadata: pkg.BinaryMetadata{
Classifier: c.Package,
RealPath: reader.RealPath,
VirtualPath: reader.VirtualPath,
},
}
break
}
return classifiedPackage, nil, nil
}
func getContents(reader source.LocationReadCloser) ([]byte, error) {
unionReader, err := unionreader.GetUnionReader(reader.ReadCloser)
if err != nil {
return nil, fmt.Errorf("unable to get union reader for file: %w", err)
}
contents, err := io.ReadAll(unionReader)
if err != nil {
return nil, fmt.Errorf("unable to get contents for file: %w", err)
}
return contents, nil
}
@@ -1,96 +0,0 @@
package generic
import (
"regexp"
"testing"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func Test_ClassifierCPEs(t *testing.T) {
tests := []struct {
name string
fixture string
classifier Classifier
cpes []string
}{
{
name: "no CPEs",
fixture: "test-fixtures/version.txt",
classifier: Classifier{
Package: "some-app",
FilepathPatterns: []*regexp.Regexp{
regexp.MustCompile(".*/version.txt"),
},
EvidencePatterns: []*regexp.Regexp{
regexp.MustCompile(`(?m)my-verison:(?P<version>[0-9.]+)`),
},
CPEs: []pkg.CPE{},
},
cpes: nil,
},
{
name: "one CPE",
fixture: "test-fixtures/version.txt",
classifier: Classifier{
Package: "some-app",
FilepathPatterns: []*regexp.Regexp{
regexp.MustCompile(".*/version.txt"),
},
EvidencePatterns: []*regexp.Regexp{
regexp.MustCompile(`(?m)my-verison:(?P<version>[0-9.]+)`),
},
CPEs: []pkg.CPE{
pkg.MustCPE("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"),
},
},
cpes: []string{
"cpe:2.3:a:some:app:1.8:*:*:*:*:*:*:*",
},
},
{
name: "multiple CPEs",
fixture: "test-fixtures/version.txt",
classifier: Classifier{
Package: "some-app",
FilepathPatterns: []*regexp.Regexp{
regexp.MustCompile(".*/version.txt"),
},
EvidencePatterns: []*regexp.Regexp{
regexp.MustCompile(`(?m)my-verison:(?P<version>[0-9.]+)`),
},
CPEs: []pkg.CPE{
pkg.MustCPE("cpe:2.3:a:some:app:*:*:*:*:*:*:*:*"),
pkg.MustCPE("cpe:2.3:a:some:apps:*:*:*:*:*:*:*:*"),
},
},
cpes: []string{
"cpe:2.3:a:some:app:1.8:*:*:*:*:*:*:*",
"cpe:2.3:a:some:apps:1.8:*:*:*:*:*:*:*",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
resolver := source.NewMockResolverForPaths(test.fixture)
locations, err := resolver.FilesByPath(test.fixture)
require.NoError(t, err)
require.Len(t, locations, 1)
location := locations[0]
readCloser, err := resolver.FileContentsByLocation(location)
require.NoError(t, err)
p, _, err := test.classifier.Examine(source.NewLocationReadCloser(location, readCloser))
require.NoError(t, err)
var cpes []string
for _, c := range p.CPEs {
cpes = append(cpes, pkg.CPEString(c))
}
require.Equal(t, test.cpes, cpes)
})
}
}
@@ -1 +0,0 @@
my-verison:1.8