Files
syft/syft/pkg/cataloger/generic/cataloger.go
T
William Murphy 93a7d2ee27 fix: better clean up of file handles (#2823)
* always close ELF cataloger file handles

The elf-binary-package-cataloger does its own file IO to account for the
possibility of a logical ELF package being broken across multiple
physical files. However, this casued it to skip the normal invocation
pattern in the generic cataloger code that prevented file leaks. Ensure
this cataloger always closes its file handles.

Signed-off-by: Will Murphy <will.murphy@anchore.com>

* defer closing of generic cataloger file handles

Otherwise, a panicking cataloger could leak file handles.

Signed-off-by: Will Murphy <will.murphy@anchore.com>

* add unit test for file closed on panic parser

Signed-off-by: Will Murphy <will.murphy@anchore.com>

* make invoke parser a static function

Signed-off-by: Will Murphy <will.murphy@anchore.com>

* push error logging down into invoke parser

Signed-off-by: Will Murphy <will.murphy@anchore.com>

---------

Signed-off-by: Will Murphy <will.murphy@anchore.com>
2024-05-01 12:58:17 -04:00

166 lines
5.2 KiB
Go

package generic
import (
"context"
"github.com/anchore/go-logger"
"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/linux"
"github.com/anchore/syft/syft/pkg"
)
type processor func(resolver file.Resolver, env Environment) []request
type request struct {
file.Location
Parser
}
// Cataloger implements the Catalog interface and is responsible for dispatching the proper parser function for
// a given path or glob pattern. This is intended to be reusable across many package cataloger types.
type Cataloger struct {
processor []processor
upstreamCataloger string
}
func (c *Cataloger) WithParserByGlobs(parser Parser, globs ...string) *Cataloger {
c.processor = append(c.processor,
func(resolver file.Resolver, _ Environment) []request {
var requests []request
for _, g := range globs {
log.WithFields("glob", g).Trace("searching for paths matching glob")
matches, err := resolver.FilesByGlob(g)
if err != nil {
log.Warnf("unable to process glob=%q: %+v", g, err)
continue
}
requests = append(requests, makeRequests(parser, matches)...)
}
return requests
},
)
return c
}
func (c *Cataloger) WithParserByMimeTypes(parser Parser, types ...string) *Cataloger {
c.processor = append(c.processor,
func(resolver file.Resolver, _ Environment) []request {
var requests []request
log.WithFields("mimetypes", types).Trace("searching for paths matching mimetype")
matches, err := resolver.FilesByMIMEType(types...)
if err != nil {
log.Warnf("unable to process mimetypes=%+v: %+v", types, err)
return nil
}
requests = append(requests, makeRequests(parser, matches)...)
return requests
},
)
return c
}
func (c *Cataloger) WithParserByPath(parser Parser, paths ...string) *Cataloger {
c.processor = append(c.processor,
func(resolver file.Resolver, _ Environment) []request {
var requests []request
for _, p := range paths {
log.WithFields("path", p).Trace("searching for path")
matches, err := resolver.FilesByPath(p)
if err != nil {
log.Warnf("unable to process path=%q: %+v", p, err)
continue
}
requests = append(requests, makeRequests(parser, matches)...)
}
return requests
},
)
return c
}
func makeRequests(parser Parser, locations []file.Location) []request {
var requests []request
for _, l := range locations {
requests = append(requests, request{
Location: l,
Parser: parser,
})
}
return requests
}
// NewCataloger if provided path-to-parser-function and glob-to-parser-function lookups creates a Cataloger
func NewCataloger(upstreamCataloger string) *Cataloger {
return &Cataloger{
upstreamCataloger: upstreamCataloger,
}
}
// Name returns a string that uniquely describes the upstream cataloger that this Generic Cataloger represents.
func (c *Cataloger) Name() string {
return c.upstreamCataloger
}
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source.
func (c *Cataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
var packages []pkg.Package
var relationships []artifact.Relationship
logger := log.Nested("cataloger", c.upstreamCataloger)
env := Environment{
// TODO: consider passing into the cataloger, this would affect the cataloger interface (and all implementations). This can be deferred until later.
LinuxRelease: linux.IdentifyRelease(resolver),
}
for _, req := range c.selectFiles(resolver) {
location, parser := req.Location, req.Parser
log.WithFields("path", location.RealPath).Trace("parsing file contents")
discoveredPackages, discoveredRelationships, err := invokeParser(ctx, resolver, location, logger, parser, &env)
if err != nil {
continue // logging is handled within invokeParser
}
for _, p := range discoveredPackages {
p.FoundBy = c.upstreamCataloger
packages = append(packages, p)
}
relationships = append(relationships, discoveredRelationships...)
}
return packages, relationships, nil
}
func invokeParser(ctx context.Context, resolver file.Resolver, location file.Location, logger logger.Logger, parser Parser, env *Environment) ([]pkg.Package, []artifact.Relationship, error) {
contentReader, err := resolver.FileContentsByLocation(location)
if err != nil {
logger.WithFields("location", location.RealPath, "error", err).Warn("unable to fetch contents")
return nil, nil, err
}
defer internal.CloseAndLogError(contentReader, location.AccessPath)
discoveredPackages, discoveredRelationships, err := parser(ctx, resolver, env, file.NewLocationReadCloser(location, contentReader))
if err != nil {
logger.WithFields("location", location.RealPath, "error", err).Warnf("cataloger failed")
return nil, nil, err
}
return discoveredPackages, discoveredRelationships, nil
}
// selectFiles takes a set of file trees and resolves and file references of interest for future cataloging
func (c *Cataloger) selectFiles(resolver file.Resolver) []request {
var requests []request
for _, proc := range c.processor {
requests = append(requests, proc(resolver, Environment{})...)
}
return requests
}