mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
port deb/dpkg cataloger to new generic cataloger pattern (#1288)
Signed-off-by: Alex Goodman <alex.goodman@anchore.com> Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
bd5adbc9b3
commit
52cb7269bf
@ -19,3 +19,12 @@ func TruncateMiddleEllipsis(input string, maxLen int) string {
|
|||||||
}
|
}
|
||||||
return input[:maxLen/2] + "..." + input[len(input)-(maxLen/2):]
|
return input[:maxLen/2] + "..." + input[len(input)-(maxLen/2):]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StringInSlice(a string, list []string) bool {
|
||||||
|
for _, b := range list {
|
||||||
|
if b == a {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ func CatalogPackages(src *source.Source, cfg cataloger.Config) (*pkg.Catalog, []
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if the catalogers have been configured, use them regardless of input type
|
// if the catalogers have been configured, use them regardless of input type
|
||||||
var catalogers []cataloger.Cataloger
|
var catalogers []pkg.Cataloger
|
||||||
if len(cfg.Catalogers) > 0 {
|
if len(cfg.Catalogers) > 0 {
|
||||||
catalogers = cataloger.AllCatalogers(cfg)
|
catalogers = cataloger.AllCatalogers(cfg)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
16
syft/pkg/cataloger.go
Normal file
16
syft/pkg/cataloger.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cataloger describes behavior for an object to participate in parsing container image or file system
|
||||||
|
// contents for the purpose of discovering Packages. Each concrete implementation should focus on discovering Packages
|
||||||
|
// for a specific Package Type or ecosystem.
|
||||||
|
type Cataloger interface {
|
||||||
|
// Name returns a string that uniquely describes a cataloger
|
||||||
|
Name() string
|
||||||
|
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source.
|
||||||
|
Catalog(resolver source.FileResolver) ([]Package, []artifact.Relationship, error)
|
||||||
|
}
|
||||||
@ -754,7 +754,7 @@ func TestMultiplePackages(t *testing.T) {
|
|||||||
VersionID: "3.12",
|
VersionID: "3.12",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
pkgtest.TestGenericParserWithEnv(t, fixture, parseApkDB, &env, expected, expectedRelationships)
|
pkgtest.TestFileParserWithEnv(t, fixture, parseApkDB, &env, expected, expectedRelationships)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@ func newMonitor() (*progress.Manual, *progress.Manual) {
|
|||||||
// In order to efficiently retrieve contents from a underlying container image the content fetch requests are
|
// In order to efficiently retrieve contents from a underlying container image the content fetch requests are
|
||||||
// done in bulk. Specifically, all files of interest are collected from each catalogers and accumulated into a single
|
// done in bulk. Specifically, all files of interest are collected from each catalogers and accumulated into a single
|
||||||
// request.
|
// request.
|
||||||
func Catalog(resolver source.FileResolver, release *linux.Release, catalogers ...Cataloger) (*pkg.Catalog, []artifact.Relationship, error) {
|
func Catalog(resolver source.FileResolver, release *linux.Release, catalogers ...pkg.Cataloger) (*pkg.Catalog, []artifact.Relationship, error) {
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
var allRelationships []artifact.Relationship
|
var allRelationships []artifact.Relationship
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/alpm"
|
"github.com/anchore/syft/syft/pkg/cataloger/alpm"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/apkdb"
|
"github.com/anchore/syft/syft/pkg/cataloger/apkdb"
|
||||||
@ -28,24 +27,13 @@ import (
|
|||||||
"github.com/anchore/syft/syft/pkg/cataloger/ruby"
|
"github.com/anchore/syft/syft/pkg/cataloger/ruby"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/rust"
|
"github.com/anchore/syft/syft/pkg/cataloger/rust"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/swift"
|
"github.com/anchore/syft/syft/pkg/cataloger/swift"
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const AllCatalogersPattern = "all"
|
const AllCatalogersPattern = "all"
|
||||||
|
|
||||||
// Cataloger describes behavior for an object to participate in parsing container image or file system
|
|
||||||
// contents for the purpose of discovering Packages. Each concrete implementation should focus on discovering Packages
|
|
||||||
// for a specific Package Type or ecosystem.
|
|
||||||
type Cataloger interface {
|
|
||||||
// Name returns a string that uniquely describes a cataloger
|
|
||||||
Name() string
|
|
||||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing the catalog source.
|
|
||||||
Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImageCatalogers returns a slice of locally implemented catalogers that are fit for detecting installations of packages.
|
// ImageCatalogers returns a slice of locally implemented catalogers that are fit for detecting installations of packages.
|
||||||
func ImageCatalogers(cfg Config) []Cataloger {
|
func ImageCatalogers(cfg Config) []pkg.Cataloger {
|
||||||
return filterCatalogers([]Cataloger{
|
return filterCatalogers([]pkg.Cataloger{
|
||||||
alpm.NewAlpmdbCataloger(),
|
alpm.NewAlpmdbCataloger(),
|
||||||
ruby.NewGemSpecCataloger(),
|
ruby.NewGemSpecCataloger(),
|
||||||
python.NewPythonPackageCataloger(),
|
python.NewPythonPackageCataloger(),
|
||||||
@ -62,8 +50,8 @@ func ImageCatalogers(cfg Config) []Cataloger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DirectoryCatalogers returns a slice of locally implemented catalogers that are fit for detecting packages from index files (and select installations)
|
// DirectoryCatalogers returns a slice of locally implemented catalogers that are fit for detecting packages from index files (and select installations)
|
||||||
func DirectoryCatalogers(cfg Config) []Cataloger {
|
func DirectoryCatalogers(cfg Config) []pkg.Cataloger {
|
||||||
return filterCatalogers([]Cataloger{
|
return filterCatalogers([]pkg.Cataloger{
|
||||||
alpm.NewAlpmdbCataloger(),
|
alpm.NewAlpmdbCataloger(),
|
||||||
ruby.NewGemFileLockCataloger(),
|
ruby.NewGemFileLockCataloger(),
|
||||||
python.NewPythonIndexCataloger(),
|
python.NewPythonIndexCataloger(),
|
||||||
@ -89,8 +77,8 @@ func DirectoryCatalogers(cfg Config) []Cataloger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// AllCatalogers returns all implemented catalogers
|
// AllCatalogers returns all implemented catalogers
|
||||||
func AllCatalogers(cfg Config) []Cataloger {
|
func AllCatalogers(cfg Config) []pkg.Cataloger {
|
||||||
return filterCatalogers([]Cataloger{
|
return filterCatalogers([]pkg.Cataloger{
|
||||||
alpm.NewAlpmdbCataloger(),
|
alpm.NewAlpmdbCataloger(),
|
||||||
ruby.NewGemFileLockCataloger(),
|
ruby.NewGemFileLockCataloger(),
|
||||||
ruby.NewGemSpecCataloger(),
|
ruby.NewGemSpecCataloger(),
|
||||||
@ -128,7 +116,7 @@ func RequestedAllCatalogers(cfg Config) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterCatalogers(catalogers []Cataloger, enabledCatalogerPatterns []string) []Cataloger {
|
func filterCatalogers(catalogers []pkg.Cataloger, enabledCatalogerPatterns []string) []pkg.Cataloger {
|
||||||
// if cataloger is not set, all applicable catalogers are enabled by default
|
// if cataloger is not set, all applicable catalogers are enabled by default
|
||||||
if len(enabledCatalogerPatterns) == 0 {
|
if len(enabledCatalogerPatterns) == 0 {
|
||||||
return catalogers
|
return catalogers
|
||||||
@ -138,7 +126,7 @@ func filterCatalogers(catalogers []Cataloger, enabledCatalogerPatterns []string)
|
|||||||
return catalogers
|
return catalogers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var keepCatalogers []Cataloger
|
var keepCatalogers []pkg.Cataloger
|
||||||
for _, cataloger := range catalogers {
|
for _, cataloger := range catalogers {
|
||||||
if contains(enabledCatalogerPatterns, cataloger.Name()) {
|
if contains(enabledCatalogerPatterns, cataloger.Name()) {
|
||||||
keepCatalogers = append(keepCatalogers, cataloger)
|
keepCatalogers = append(keepCatalogers, cataloger)
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import (
|
|||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ Cataloger = (*dummy)(nil)
|
var _ pkg.Cataloger = (*dummy)(nil)
|
||||||
|
|
||||||
type dummy struct {
|
type dummy struct {
|
||||||
name string
|
name string
|
||||||
@ -147,7 +147,7 @@ func Test_filterCatalogers(t *testing.T) {
|
|||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
var catalogers []Cataloger
|
var catalogers []pkg.Cataloger
|
||||||
for _, n := range tt.catalogers {
|
for _, n := range tt.catalogers {
|
||||||
catalogers = append(catalogers, dummy{name: n})
|
catalogers = append(catalogers, dummy{name: n})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,5 +90,5 @@ func TestParseConanfile(t *testing.T) {
|
|||||||
// TODO: relationships are not under test
|
// TODO: relationships are not under test
|
||||||
var expectedRelationships []artifact.Relationship
|
var expectedRelationships []artifact.Relationship
|
||||||
|
|
||||||
pkgtest.TestGenericParser(t, fixture, parseConanfile, expected, expectedRelationships)
|
pkgtest.TestFileParser(t, fixture, parseConanfile, expected, expectedRelationships)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,5 +35,5 @@ func TestParseConanlock(t *testing.T) {
|
|||||||
// TODO: relationships are not under test
|
// TODO: relationships are not under test
|
||||||
var expectedRelationships []artifact.Relationship
|
var expectedRelationships []artifact.Relationship
|
||||||
|
|
||||||
pkgtest.TestGenericParser(t, fixture, parseConanlock, expected, expectedRelationships)
|
pkgtest.TestFileParser(t, fixture, parseConanlock, expected, expectedRelationships)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,5 +98,5 @@ func TestParsePubspecLock(t *testing.T) {
|
|||||||
// TODO: relationships are not under test
|
// TODO: relationships are not under test
|
||||||
var expectedRelationships []artifact.Relationship
|
var expectedRelationships []artifact.Relationship
|
||||||
|
|
||||||
pkgtest.TestGenericParser(t, fixture, parsePubspecLock, expected, expectedRelationships)
|
pkgtest.TestFileParser(t, fixture, parsePubspecLock, expected, expectedRelationships)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,230 +4,14 @@ Package dpkg provides a concrete Cataloger implementation for Debian package DB
|
|||||||
package deb
|
package deb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
"github.com/anchore/syft/internal/log"
|
|
||||||
"github.com/anchore/syft/syft/artifact"
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const catalogerName = "dpkgdb-cataloger"
|
||||||
md5sumsExt = ".md5sums"
|
|
||||||
conffilesExt = ".conffiles"
|
|
||||||
docsPath = "/usr/share/doc"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Cataloger struct{}
|
// NewDpkgdbCataloger returns a new Deb package cataloger capable of parsing DPKG status DB files.
|
||||||
|
func NewDpkgdbCataloger() *generic.Cataloger {
|
||||||
// NewDpkgdbCataloger returns a new Deb package cataloger object.
|
return generic.NewCataloger(catalogerName).
|
||||||
func NewDpkgdbCataloger() *Cataloger {
|
WithParserByGlobs(parseDpkgDB, pkg.DpkgDBGlob)
|
||||||
return &Cataloger{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name returns a string that uniquely describes a cataloger
|
|
||||||
func (c *Cataloger) Name() string {
|
|
||||||
return "dpkgdb-cataloger"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing dpkg support files.
|
|
||||||
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
|
|
||||||
dbFileMatches, err := resolver.FilesByGlob(pkg.DpkgDBGlob)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("failed to find dpkg status files's by glob: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var allPackages []pkg.Package
|
|
||||||
for _, dbLocation := range dbFileMatches {
|
|
||||||
dbContents, err := resolver.FileContentsByLocation(dbLocation)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
pkgs, err := parseDpkgStatus(dbContents)
|
|
||||||
internal.CloseAndLogError(dbContents, dbLocation.VirtualPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("dpkg cataloger: unable to catalog package=%+v: %w", dbLocation.RealPath, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range pkgs {
|
|
||||||
p := &pkgs[i]
|
|
||||||
p.FoundBy = c.Name()
|
|
||||||
p.Locations.Add(dbLocation)
|
|
||||||
|
|
||||||
// the current entry only has what may have been listed in the status file, however, there are additional
|
|
||||||
// files that are listed in multiple other locations. We should retrieve them all and merge the file lists
|
|
||||||
// together.
|
|
||||||
mergeFileListing(resolver, dbLocation, p)
|
|
||||||
|
|
||||||
// fetch additional data from the copyright file to derive the license information
|
|
||||||
addLicenses(resolver, dbLocation, p)
|
|
||||||
|
|
||||||
p.SetID()
|
|
||||||
}
|
|
||||||
|
|
||||||
allPackages = append(allPackages, pkgs...)
|
|
||||||
}
|
|
||||||
return allPackages, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
|
|
||||||
// get license information from the copyright file
|
|
||||||
copyrightReader, copyrightLocation := fetchCopyrightContents(resolver, dbLocation, p)
|
|
||||||
|
|
||||||
if copyrightReader != nil && copyrightLocation != nil {
|
|
||||||
defer internal.CloseAndLogError(copyrightReader, copyrightLocation.VirtualPath)
|
|
||||||
// attach the licenses
|
|
||||||
p.Licenses = parseLicensesFromCopyright(copyrightReader)
|
|
||||||
|
|
||||||
// keep a record of the file where this was discovered
|
|
||||||
p.Locations.Add(*copyrightLocation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeFileListing(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
|
|
||||||
metadata := p.Metadata.(pkg.DpkgMetadata)
|
|
||||||
|
|
||||||
// get file listing (package files + additional config files)
|
|
||||||
files, infoLocations := getAdditionalFileListing(resolver, dbLocation, p)
|
|
||||||
loopNewFiles:
|
|
||||||
for _, newFile := range files {
|
|
||||||
for _, existingFile := range metadata.Files {
|
|
||||||
if existingFile.Path == newFile.Path {
|
|
||||||
// skip adding this file since it already exists
|
|
||||||
continue loopNewFiles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
metadata.Files = append(metadata.Files, newFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort files by path
|
|
||||||
sort.SliceStable(metadata.Files, func(i, j int) bool {
|
|
||||||
return metadata.Files[i].Path < metadata.Files[j].Path
|
|
||||||
})
|
|
||||||
|
|
||||||
// persist alterations
|
|
||||||
p.Metadata = metadata
|
|
||||||
|
|
||||||
// persist location information from each new source of information
|
|
||||||
p.Locations.Add(infoLocations...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAdditionalFileListing(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) ([]pkg.DpkgFileRecord, []source.Location) {
|
|
||||||
// ensure the default value for a collection is never nil since this may be shown as JSON
|
|
||||||
var files = make([]pkg.DpkgFileRecord, 0)
|
|
||||||
var locations []source.Location
|
|
||||||
|
|
||||||
md5Reader, md5Location := fetchMd5Contents(resolver, dbLocation, p)
|
|
||||||
|
|
||||||
if md5Reader != nil && md5Location != nil {
|
|
||||||
defer internal.CloseAndLogError(md5Reader, md5Location.VirtualPath)
|
|
||||||
// attach the file list
|
|
||||||
files = append(files, parseDpkgMD5Info(md5Reader)...)
|
|
||||||
|
|
||||||
// keep a record of the file where this was discovered
|
|
||||||
locations = append(locations, *md5Location)
|
|
||||||
}
|
|
||||||
|
|
||||||
conffilesReader, conffilesLocation := fetchConffileContents(resolver, dbLocation, p)
|
|
||||||
|
|
||||||
if conffilesReader != nil && conffilesLocation != nil {
|
|
||||||
defer internal.CloseAndLogError(conffilesReader, conffilesLocation.VirtualPath)
|
|
||||||
// attach the file list
|
|
||||||
files = append(files, parseDpkgConffileInfo(conffilesReader)...)
|
|
||||||
|
|
||||||
// keep a record of the file where this was discovered
|
|
||||||
locations = append(locations, *conffilesLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
return files, locations
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchMd5Contents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.ReadCloser, *source.Location) {
|
|
||||||
var md5Reader io.ReadCloser
|
|
||||||
var err error
|
|
||||||
|
|
||||||
parentPath := filepath.Dir(dbLocation.RealPath)
|
|
||||||
|
|
||||||
// look for /var/lib/dpkg/info/NAME:ARCH.md5sums
|
|
||||||
name := md5Key(p)
|
|
||||||
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+md5sumsExt))
|
|
||||||
|
|
||||||
if location == nil {
|
|
||||||
// the most specific key did not work, fallback to just the name
|
|
||||||
// look for /var/lib/dpkg/info/NAME.md5sums
|
|
||||||
location = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", p.Name+md5sumsExt))
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is unexpected, but not a show-stopper
|
|
||||||
if location != nil {
|
|
||||||
md5Reader, err = resolver.FileContentsByLocation(*location)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to fetch deb md5 contents (package=%s): %+v", p.Name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return md5Reader, location
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchConffileContents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.ReadCloser, *source.Location) {
|
|
||||||
var reader io.ReadCloser
|
|
||||||
var err error
|
|
||||||
|
|
||||||
parentPath := filepath.Dir(dbLocation.RealPath)
|
|
||||||
|
|
||||||
// look for /var/lib/dpkg/info/NAME:ARCH.conffiles
|
|
||||||
name := md5Key(p)
|
|
||||||
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+conffilesExt))
|
|
||||||
|
|
||||||
if location == nil {
|
|
||||||
// the most specific key did not work, fallback to just the name
|
|
||||||
// look for /var/lib/dpkg/info/NAME.conffiles
|
|
||||||
location = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", p.Name+conffilesExt))
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is unexpected, but not a show-stopper
|
|
||||||
if location != nil {
|
|
||||||
reader, err = resolver.FileContentsByLocation(*location)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to fetch deb conffiles contents (package=%s): %+v", p.Name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader, location
|
|
||||||
}
|
|
||||||
|
|
||||||
func fetchCopyrightContents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.ReadCloser, *source.Location) {
|
|
||||||
// look for /usr/share/docs/NAME/copyright files
|
|
||||||
name := p.Name
|
|
||||||
copyrightPath := path.Join(docsPath, name, "copyright")
|
|
||||||
location := resolver.RelativeFileByPath(dbLocation, copyrightPath)
|
|
||||||
|
|
||||||
// we may not have a copyright file for each package, ignore missing files
|
|
||||||
if location == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
reader, err := resolver.FileContentsByLocation(*location)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("failed to fetch deb copyright contents (package=%s): %w", p.Name, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return reader, location
|
|
||||||
}
|
|
||||||
|
|
||||||
func md5Key(p *pkg.Package) string {
|
|
||||||
metadata := p.Metadata.(pkg.DpkgMetadata)
|
|
||||||
|
|
||||||
contentKey := p.Name
|
|
||||||
if metadata.Architecture != "" && metadata.Architecture != "all" {
|
|
||||||
contentKey = contentKey + ":" + metadata.Architecture
|
|
||||||
}
|
|
||||||
return contentKey
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,37 +3,25 @@ package deb
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDpkgCataloger(t *testing.T) {
|
func TestDpkgCataloger(t *testing.T) {
|
||||||
tests := []struct {
|
expected := []pkg.Package{
|
||||||
name string
|
|
||||||
sources map[string][]string
|
|
||||||
expected []pkg.Package
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "go-case",
|
|
||||||
sources: map[string][]string{
|
|
||||||
"libpam-runtime": {
|
|
||||||
"/var/lib/dpkg/status",
|
|
||||||
"/var/lib/dpkg/info/libpam-runtime.md5sums",
|
|
||||||
"/var/lib/dpkg/info/libpam-runtime.conffiles",
|
|
||||||
"/usr/share/doc/libpam-runtime/copyright",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: []pkg.Package{
|
|
||||||
{
|
{
|
||||||
Name: "libpam-runtime",
|
Name: "libpam-runtime",
|
||||||
Version: "1.1.8-3.6",
|
Version: "1.1.8-3.6",
|
||||||
FoundBy: "dpkgdb-cataloger",
|
FoundBy: "dpkgdb-cataloger",
|
||||||
Licenses: []string{"GPL-1", "GPL-2", "LGPL-2.1"},
|
Licenses: []string{"GPL-1", "GPL-2", "LGPL-2.1"},
|
||||||
|
Locations: source.NewLocationSet(
|
||||||
|
source.NewVirtualLocation("/var/lib/dpkg/status", "/var/lib/dpkg/status"),
|
||||||
|
source.NewVirtualLocation("/var/lib/dpkg/info/libpam-runtime.md5sums", "/var/lib/dpkg/info/libpam-runtime.md5sums"),
|
||||||
|
source.NewVirtualLocation("/var/lib/dpkg/info/libpam-runtime.conffiles", "/var/lib/dpkg/info/libpam-runtime.conffiles"),
|
||||||
|
source.NewVirtualLocation("/usr/share/doc/libpam-runtime/copyright", "/usr/share/doc/libpam-runtime/copyright"),
|
||||||
|
),
|
||||||
Type: pkg.DebPkg,
|
Type: pkg.DebPkg,
|
||||||
MetadataType: pkg.DpkgMetadataType,
|
MetadataType: pkg.DpkgMetadataType,
|
||||||
Metadata: pkg.DpkgMetadata{
|
Metadata: pkg.DpkgMetadata{
|
||||||
@ -83,58 +71,13 @@ func TestDpkgCataloger(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
|
|
||||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-dpkg")
|
|
||||||
|
|
||||||
s, err := source.NewFromImage(img, "")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c := NewDpkgdbCataloger()
|
c := NewDpkgdbCataloger()
|
||||||
|
|
||||||
resolver, err := s.FileResolver(source.SquashedScope)
|
pkgtest.NewCatalogTester().
|
||||||
if err != nil {
|
WithImageResolver(t, "image-dpkg").
|
||||||
t.Errorf("could not get resolver error: %+v", err)
|
IgnoreLocationLayer(). // this fixture can be rebuilt, thus the layer ID will change
|
||||||
}
|
Expects(expected, nil).
|
||||||
|
TestCataloger(t, c)
|
||||||
actual, _, err := c.Catalog(resolver)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("failed to catalog: %+v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(actual) != len(test.expected) {
|
|
||||||
for _, a := range actual {
|
|
||||||
t.Logf(" %+v", a)
|
|
||||||
}
|
|
||||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
// test sources...
|
|
||||||
for idx := range actual {
|
|
||||||
a := &actual[idx]
|
|
||||||
// we will test the sources separately
|
|
||||||
var sourcesList = make([]string, len(a.Locations.ToSlice()))
|
|
||||||
for i, s := range a.Locations.ToSlice() {
|
|
||||||
sourcesList[i] = s.RealPath
|
|
||||||
}
|
|
||||||
a.Locations = source.NewLocationSet()
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, sourcesList, test.sources[a.Name])
|
|
||||||
}
|
|
||||||
|
|
||||||
// test remaining fields...
|
|
||||||
for _, d := range deep.Equal(actual, test.expected) {
|
|
||||||
t.Errorf("diff: %+v", d)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
247
syft/pkg/cataloger/deb/package.go
Normal file
247
syft/pkg/cataloger/deb/package.go
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
package deb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/anchore/packageurl-go"
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/linux"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
md5sumsExt = ".md5sums"
|
||||||
|
conffilesExt = ".conffiles"
|
||||||
|
docsPath = "/usr/share/doc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newDpkgPackage(d pkg.DpkgMetadata, dbLocation source.Location, resolver source.FileResolver, release *linux.Release) pkg.Package {
|
||||||
|
p := pkg.Package{
|
||||||
|
Name: d.Package,
|
||||||
|
Version: d.Version,
|
||||||
|
Locations: source.NewLocationSet(dbLocation),
|
||||||
|
PURL: packageURL(d, release),
|
||||||
|
Type: pkg.DebPkg,
|
||||||
|
MetadataType: pkg.DpkgMetadataType,
|
||||||
|
Metadata: d,
|
||||||
|
}
|
||||||
|
|
||||||
|
// the current entry only has what may have been listed in the status file, however, there are additional
|
||||||
|
// files that are listed in multiple other locations. We should retrieve them all and merge the file lists
|
||||||
|
// together.
|
||||||
|
mergeFileListing(resolver, dbLocation, &p)
|
||||||
|
|
||||||
|
// fetch additional data from the copyright file to derive the license information
|
||||||
|
addLicenses(resolver, dbLocation, &p)
|
||||||
|
|
||||||
|
p.SetID()
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec)
|
||||||
|
func packageURL(m pkg.DpkgMetadata, distro *linux.Release) string {
|
||||||
|
if distro == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if distro.ID != "debian" && !internal.StringInSlice("debian", distro.IDLike) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
qualifiers := map[string]string{
|
||||||
|
pkg.PURLQualifierArch: m.Architecture,
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Source != "" {
|
||||||
|
if m.SourceVersion != "" {
|
||||||
|
qualifiers[pkg.PURLQualifierUpstream] = fmt.Sprintf("%s@%s", m.Source, m.SourceVersion)
|
||||||
|
} else {
|
||||||
|
qualifiers[pkg.PURLQualifierUpstream] = m.Source
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageurl.NewPackageURL(
|
||||||
|
packageurl.TypeDebian,
|
||||||
|
distro.ID,
|
||||||
|
m.Package,
|
||||||
|
m.Version,
|
||||||
|
pkg.PURLQualifiers(
|
||||||
|
qualifiers,
|
||||||
|
distro,
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
).ToString()
|
||||||
|
}
|
||||||
|
|
||||||
|
func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
|
||||||
|
metadata := p.Metadata.(pkg.DpkgMetadata)
|
||||||
|
|
||||||
|
// get license information from the copyright file
|
||||||
|
copyrightReader, copyrightLocation := fetchCopyrightContents(resolver, dbLocation, metadata)
|
||||||
|
|
||||||
|
if copyrightReader != nil && copyrightLocation != nil {
|
||||||
|
defer internal.CloseAndLogError(copyrightReader, copyrightLocation.VirtualPath)
|
||||||
|
// attach the licenses
|
||||||
|
p.Licenses = parseLicensesFromCopyright(copyrightReader)
|
||||||
|
|
||||||
|
// keep a record of the file where this was discovered
|
||||||
|
p.Locations.Add(*copyrightLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeFileListing(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
|
||||||
|
metadata := p.Metadata.(pkg.DpkgMetadata)
|
||||||
|
|
||||||
|
// get file listing (package files + additional config files)
|
||||||
|
files, infoLocations := getAdditionalFileListing(resolver, dbLocation, metadata)
|
||||||
|
loopNewFiles:
|
||||||
|
for _, newFile := range files {
|
||||||
|
for _, existingFile := range metadata.Files {
|
||||||
|
if existingFile.Path == newFile.Path {
|
||||||
|
// skip adding this file since it already exists
|
||||||
|
continue loopNewFiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
metadata.Files = append(metadata.Files, newFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort files by path
|
||||||
|
sort.SliceStable(metadata.Files, func(i, j int) bool {
|
||||||
|
return metadata.Files[i].Path < metadata.Files[j].Path
|
||||||
|
})
|
||||||
|
|
||||||
|
// persist alterations
|
||||||
|
p.Metadata = metadata
|
||||||
|
|
||||||
|
// persist location information from each new source of information
|
||||||
|
p.Locations.Add(infoLocations...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAdditionalFileListing(resolver source.FileResolver, dbLocation source.Location, m pkg.DpkgMetadata) ([]pkg.DpkgFileRecord, []source.Location) {
|
||||||
|
// ensure the default value for a collection is never nil since this may be shown as JSON
|
||||||
|
var files = make([]pkg.DpkgFileRecord, 0)
|
||||||
|
var locations []source.Location
|
||||||
|
|
||||||
|
md5Reader, md5Location := fetchMd5Contents(resolver, dbLocation, m)
|
||||||
|
|
||||||
|
if md5Reader != nil && md5Location != nil {
|
||||||
|
defer internal.CloseAndLogError(md5Reader, md5Location.VirtualPath)
|
||||||
|
// attach the file list
|
||||||
|
files = append(files, parseDpkgMD5Info(md5Reader)...)
|
||||||
|
|
||||||
|
// keep a record of the file where this was discovered
|
||||||
|
locations = append(locations, *md5Location)
|
||||||
|
}
|
||||||
|
|
||||||
|
conffilesReader, conffilesLocation := fetchConffileContents(resolver, dbLocation, m)
|
||||||
|
|
||||||
|
if conffilesReader != nil && conffilesLocation != nil {
|
||||||
|
defer internal.CloseAndLogError(conffilesReader, conffilesLocation.VirtualPath)
|
||||||
|
// attach the file list
|
||||||
|
files = append(files, parseDpkgConffileInfo(conffilesReader)...)
|
||||||
|
|
||||||
|
// keep a record of the file where this was discovered
|
||||||
|
locations = append(locations, *conffilesLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, locations
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchMd5Contents(resolver source.FileResolver, dbLocation source.Location, m pkg.DpkgMetadata) (io.ReadCloser, *source.Location) {
|
||||||
|
var md5Reader io.ReadCloser
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if resolver == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parentPath := filepath.Dir(dbLocation.RealPath)
|
||||||
|
|
||||||
|
// look for /var/lib/dpkg/info/NAME:ARCH.md5sums
|
||||||
|
name := md5Key(m)
|
||||||
|
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+md5sumsExt))
|
||||||
|
|
||||||
|
if location == nil {
|
||||||
|
// the most specific key did not work, fallback to just the name
|
||||||
|
// look for /var/lib/dpkg/info/NAME.md5sums
|
||||||
|
location = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", m.Package+md5sumsExt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is unexpected, but not a show-stopper
|
||||||
|
if location != nil {
|
||||||
|
md5Reader, err = resolver.FileContentsByLocation(*location)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to fetch deb md5 contents (package=%s): %+v", m.Package, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return md5Reader, location
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchConffileContents(resolver source.FileResolver, dbLocation source.Location, m pkg.DpkgMetadata) (io.ReadCloser, *source.Location) {
|
||||||
|
var reader io.ReadCloser
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if resolver == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parentPath := filepath.Dir(dbLocation.RealPath)
|
||||||
|
|
||||||
|
// look for /var/lib/dpkg/info/NAME:ARCH.conffiles
|
||||||
|
name := md5Key(m)
|
||||||
|
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+conffilesExt))
|
||||||
|
|
||||||
|
if location == nil {
|
||||||
|
// the most specific key did not work, fallback to just the name
|
||||||
|
// look for /var/lib/dpkg/info/NAME.conffiles
|
||||||
|
location = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", m.Package+conffilesExt))
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is unexpected, but not a show-stopper
|
||||||
|
if location != nil {
|
||||||
|
reader, err = resolver.FileContentsByLocation(*location)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to fetch deb conffiles contents (package=%s): %+v", m.Package, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader, location
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchCopyrightContents(resolver source.FileResolver, dbLocation source.Location, m pkg.DpkgMetadata) (io.ReadCloser, *source.Location) {
|
||||||
|
if resolver == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// look for /usr/share/docs/NAME/copyright files
|
||||||
|
copyrightPath := path.Join(docsPath, m.Package, "copyright")
|
||||||
|
location := resolver.RelativeFileByPath(dbLocation, copyrightPath)
|
||||||
|
|
||||||
|
// we may not have a copyright file for each package, ignore missing files
|
||||||
|
if location == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reader, err := resolver.FileContentsByLocation(*location)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("failed to fetch deb copyright contents (package=%s): %w", m.Package, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader, location
|
||||||
|
}
|
||||||
|
|
||||||
|
func md5Key(metadata pkg.DpkgMetadata) string {
|
||||||
|
contentKey := metadata.Package
|
||||||
|
if metadata.Architecture != "" && metadata.Architecture != "all" {
|
||||||
|
contentKey = contentKey + ":" + metadata.Architecture
|
||||||
|
}
|
||||||
|
return contentKey
|
||||||
|
}
|
||||||
@ -1,20 +1,19 @@
|
|||||||
package pkg
|
package deb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/linux"
|
"github.com/anchore/syft/syft/linux"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDpkgMetadata_pURL(t *testing.T) {
|
func Test_packageURL(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
distro *linux.Release
|
distro *linux.Release
|
||||||
metadata DpkgMetadata
|
metadata pkg.DpkgMetadata
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -22,8 +21,23 @@ func TestDpkgMetadata_pURL(t *testing.T) {
|
|||||||
distro: &linux.Release{
|
distro: &linux.Release{
|
||||||
ID: "debian",
|
ID: "debian",
|
||||||
VersionID: "11",
|
VersionID: "11",
|
||||||
|
IDLike: []string{
|
||||||
|
"debian",
|
||||||
},
|
},
|
||||||
metadata: DpkgMetadata{
|
},
|
||||||
|
metadata: pkg.DpkgMetadata{
|
||||||
|
Package: "p",
|
||||||
|
Version: "v",
|
||||||
|
},
|
||||||
|
expected: "pkg:deb/debian/p@v?distro=debian-11",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing ID_LIKE",
|
||||||
|
distro: &linux.Release{
|
||||||
|
ID: "debian",
|
||||||
|
VersionID: "11",
|
||||||
|
},
|
||||||
|
metadata: pkg.DpkgMetadata{
|
||||||
Package: "p",
|
Package: "p",
|
||||||
Version: "v",
|
Version: "v",
|
||||||
},
|
},
|
||||||
@ -33,9 +47,12 @@ func TestDpkgMetadata_pURL(t *testing.T) {
|
|||||||
name: "with arch info",
|
name: "with arch info",
|
||||||
distro: &linux.Release{
|
distro: &linux.Release{
|
||||||
ID: "ubuntu",
|
ID: "ubuntu",
|
||||||
|
IDLike: []string{
|
||||||
|
"debian",
|
||||||
|
},
|
||||||
VersionID: "16.04",
|
VersionID: "16.04",
|
||||||
},
|
},
|
||||||
metadata: DpkgMetadata{
|
metadata: pkg.DpkgMetadata{
|
||||||
Package: "p",
|
Package: "p",
|
||||||
Version: "v",
|
Version: "v",
|
||||||
Architecture: "a",
|
Architecture: "a",
|
||||||
@ -44,19 +61,22 @@ func TestDpkgMetadata_pURL(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "missing distro",
|
name: "missing distro",
|
||||||
metadata: DpkgMetadata{
|
metadata: pkg.DpkgMetadata{
|
||||||
Package: "p",
|
Package: "p",
|
||||||
Version: "v",
|
Version: "v",
|
||||||
},
|
},
|
||||||
expected: "pkg:deb/p@v",
|
expected: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with upstream qualifier with source pkg name info",
|
name: "with upstream qualifier with source pkg name info",
|
||||||
distro: &linux.Release{
|
distro: &linux.Release{
|
||||||
ID: "debian",
|
ID: "debian",
|
||||||
VersionID: "11",
|
VersionID: "11",
|
||||||
|
IDLike: []string{
|
||||||
|
"debian",
|
||||||
},
|
},
|
||||||
metadata: DpkgMetadata{
|
},
|
||||||
|
metadata: pkg.DpkgMetadata{
|
||||||
Package: "p",
|
Package: "p",
|
||||||
Source: "s",
|
Source: "s",
|
||||||
Version: "v",
|
Version: "v",
|
||||||
@ -68,8 +88,11 @@ func TestDpkgMetadata_pURL(t *testing.T) {
|
|||||||
distro: &linux.Release{
|
distro: &linux.Release{
|
||||||
ID: "debian",
|
ID: "debian",
|
||||||
VersionID: "11",
|
VersionID: "11",
|
||||||
|
IDLike: []string{
|
||||||
|
"debian",
|
||||||
},
|
},
|
||||||
metadata: DpkgMetadata{
|
},
|
||||||
|
metadata: pkg.DpkgMetadata{
|
||||||
Package: "p",
|
Package: "p",
|
||||||
Source: "s",
|
Source: "s",
|
||||||
Version: "v",
|
Version: "v",
|
||||||
@ -81,51 +104,9 @@ func TestDpkgMetadata_pURL(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) {
|
||||||
actual := test.metadata.PackageURL(test.distro)
|
actual := packageURL(test.metadata, test.distro)
|
||||||
if actual != test.expected {
|
if diff := cmp.Diff(test.expected, actual); diff != "" {
|
||||||
dmp := diffmatchpatch.New()
|
t.Errorf("unexpected packageURL (-want +got):\n%s", diff)
|
||||||
diffs := dmp.DiffMain(test.expected, actual, true)
|
|
||||||
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDpkgMetadata_FileOwner(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
metadata DpkgMetadata
|
|
||||||
expected []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
metadata: DpkgMetadata{
|
|
||||||
Files: []DpkgFileRecord{
|
|
||||||
{Path: "/somewhere"},
|
|
||||||
{Path: "/else"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: []string{
|
|
||||||
"/else",
|
|
||||||
"/somewhere",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
metadata: DpkgMetadata{
|
|
||||||
Files: []DpkgFileRecord{
|
|
||||||
{Path: "/somewhere"},
|
|
||||||
{Path: ""},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: []string{
|
|
||||||
"/somewhere",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(strings.Join(test.expected, ","), func(t *testing.T) {
|
|
||||||
actual := test.metadata.OwnedFiles()
|
|
||||||
for _, d := range deep.Equal(test.expected, actual) {
|
|
||||||
t.Errorf("diff: %+v", d)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -4,7 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseLicensesFromCopyright(t *testing.T) {
|
func TestParseLicensesFromCopyright(t *testing.T) {
|
||||||
@ -38,31 +39,15 @@ func TestParseLicensesFromCopyright(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.fixture, func(t *testing.T) {
|
t.Run(test.fixture, func(t *testing.T) {
|
||||||
file, err := os.Open(test.fixture)
|
f, err := os.Open(test.fixture)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal("Unable to read: ", err)
|
t.Cleanup(func() { require.NoError(t, f.Close()) })
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := file.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("closing file failed:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
actual := parseLicensesFromCopyright(file)
|
actual := parseLicensesFromCopyright(f)
|
||||||
|
|
||||||
if len(actual) != len(test.expected) {
|
if diff := cmp.Diff(test.expected, actual); diff != "" {
|
||||||
for _, a := range actual {
|
t.Errorf("unexpected package licenses (-want +got):\n%s", diff)
|
||||||
t.Logf(" %+v", a)
|
|
||||||
}
|
}
|
||||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
diffs := deep.Equal(actual, test.expected)
|
|
||||||
for _, d := range diffs {
|
|
||||||
t.Errorf("diff: %+v", d)
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,10 @@ import (
|
|||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/artifact"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -21,20 +24,24 @@ var (
|
|||||||
sourceRegexp = regexp.MustCompile(`(?P<name>\S+)( \((?P<version>.*)\))?`)
|
sourceRegexp = regexp.MustCompile(`(?P<name>\S+)( \((?P<version>.*)\))?`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func newDpkgPackage(d pkg.DpkgMetadata) *pkg.Package {
|
func parseDpkgDB(resolver source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||||
return &pkg.Package{
|
metadata, err := parseDpkgStatus(reader)
|
||||||
Name: d.Package,
|
if err != nil {
|
||||||
Version: d.Version,
|
return nil, nil, fmt.Errorf("unable to catalog dpkg DB=%q: %w", reader.RealPath, err)
|
||||||
Type: pkg.DebPkg,
|
|
||||||
MetadataType: pkg.DpkgMetadataType,
|
|
||||||
Metadata: d,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pkgs []pkg.Package
|
||||||
|
for _, m := range metadata {
|
||||||
|
pkgs = append(pkgs, newDpkgPackage(m, reader.Location, resolver, env.LinuxRelease))
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkgs, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
|
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
|
||||||
func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
|
func parseDpkgStatus(reader io.Reader) ([]pkg.DpkgMetadata, error) {
|
||||||
buffedReader := bufio.NewReader(reader)
|
buffedReader := bufio.NewReader(reader)
|
||||||
var packages []pkg.Package
|
var metadata []pkg.DpkgMetadata
|
||||||
|
|
||||||
continueProcessing := true
|
continueProcessing := true
|
||||||
for continueProcessing {
|
for continueProcessing {
|
||||||
@ -46,40 +53,44 @@ func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if entry == nil {
|
||||||
p := newDpkgPackage(entry)
|
continue
|
||||||
if pkg.IsValid(p) {
|
|
||||||
packages = append(packages, *p)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return packages, nil
|
metadata = append(metadata, *entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader.
|
// parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader.
|
||||||
func parseDpkgStatusEntry(reader *bufio.Reader) (pkg.DpkgMetadata, error) {
|
func parseDpkgStatusEntry(reader *bufio.Reader) (*pkg.DpkgMetadata, error) {
|
||||||
var retErr error
|
var retErr error
|
||||||
dpkgFields, err := extractAllFields(reader)
|
dpkgFields, err := extractAllFields(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, errEndOfPackages) {
|
if !errors.Is(err, errEndOfPackages) {
|
||||||
return pkg.DpkgMetadata{}, err
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(dpkgFields) == 0 {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
retErr = err
|
retErr = err
|
||||||
}
|
}
|
||||||
|
|
||||||
entry := pkg.DpkgMetadata{
|
entry := pkg.DpkgMetadata{}
|
||||||
// ensure the default value for a collection is never nil since this may be shown as JSON
|
|
||||||
Files: make([]pkg.DpkgFileRecord, 0),
|
|
||||||
}
|
|
||||||
err = mapstructure.Decode(dpkgFields, &entry)
|
err = mapstructure.Decode(dpkgFields, &entry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return pkg.DpkgMetadata{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
name, version := extractSourceVersion(entry.Source)
|
sourceName, sourceVersion := extractSourceVersion(entry.Source)
|
||||||
if version != "" {
|
if sourceVersion != "" {
|
||||||
entry.SourceVersion = version
|
entry.SourceVersion = sourceVersion
|
||||||
entry.Source = name
|
entry.Source = sourceName
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Package == "" {
|
||||||
|
return nil, retErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// there may be an optional conffiles section that we should persist as files
|
// there may be an optional conffiles section that we should persist as files
|
||||||
@ -89,7 +100,12 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (pkg.DpkgMetadata, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry, retErr
|
if entry.Files == nil {
|
||||||
|
// ensure the default value for a collection is never nil since this may be shown as JSON
|
||||||
|
entry.Files = make([]pkg.DpkgFileRecord, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &entry, retErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractAllFields(reader *bufio.Reader) (map[string]interface{}, error) {
|
func extractAllFields(reader *bufio.Reader) (map[string]interface{}, error) {
|
||||||
@ -5,34 +5,30 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
|
"github.com/anchore/syft/syft/linux"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
func compareEntries(t *testing.T, left, right pkg.DpkgMetadata) {
|
func Test_parseDpkgStatus(t *testing.T) {
|
||||||
t.Helper()
|
|
||||||
if diff := deep.Equal(left, right); diff != nil {
|
|
||||||
t.Error(diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSinglePackage(t *testing.T) {
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
expected pkg.DpkgMetadata
|
expected []pkg.DpkgMetadata
|
||||||
fixturePath string
|
fixturePath string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "Test Single Package",
|
name: "single package",
|
||||||
fixturePath: filepath.Join("test-fixtures", "status", "single"),
|
fixturePath: "test-fixtures/status/single",
|
||||||
expected: pkg.DpkgMetadata{
|
expected: []pkg.DpkgMetadata{
|
||||||
|
{
|
||||||
Package: "apt",
|
Package: "apt",
|
||||||
Source: "apt-dev",
|
Source: "apt-dev",
|
||||||
Version: "1.8.2",
|
Version: "1.8.2",
|
||||||
@ -89,10 +85,12 @@ func TestSinglePackage(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single package with installed size",
|
||||||
|
fixturePath: "test-fixtures/status/installed-size-4KB",
|
||||||
|
expected: []pkg.DpkgMetadata{
|
||||||
{
|
{
|
||||||
name: "parse storage notation",
|
|
||||||
fixturePath: filepath.Join("test-fixtures", "status", "installed-size-4KB"),
|
|
||||||
expected: pkg.DpkgMetadata{
|
|
||||||
Package: "apt",
|
Package: "apt",
|
||||||
Source: "apt-dev",
|
Source: "apt-dev",
|
||||||
Version: "1.8.2",
|
Version: "1.8.2",
|
||||||
@ -113,41 +111,13 @@ func TestSinglePackage(t *testing.T) {
|
|||||||
* apt-cdrom to use removable media as a source for packages
|
* apt-cdrom to use removable media as a source for packages
|
||||||
* apt-config as an interface to the configuration settings
|
* apt-config as an interface to the configuration settings
|
||||||
* apt-key as an interface to manage authentication keys`,
|
* apt-key as an interface to manage authentication keys`,
|
||||||
|
Files: []pkg.DpkgFileRecord{},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
|
||||||
|
|
||||||
for _, test := range tests {
|
|
||||||
t.Run(test.name, func(t *testing.T) {
|
|
||||||
file, err := os.Open(test.fixturePath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to read test_fixtures/single: ", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := file.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("closing file failed:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
reader := bufio.NewReader(file)
|
|
||||||
|
|
||||||
entry, err := parseDpkgStatusEntry(reader)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to read file contents: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
compareEntries(t, entry, test.expected)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMultiplePackages(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
expected []pkg.DpkgMetadata
|
|
||||||
}{
|
|
||||||
{
|
{
|
||||||
name: "Test Multiple Package",
|
name: "multiple entries",
|
||||||
|
fixturePath: "test-fixtures/status/multiple",
|
||||||
expected: []pkg.DpkgMetadata{
|
expected: []pkg.DpkgMetadata{
|
||||||
{
|
{
|
||||||
Package: "no-version",
|
Package: "no-version",
|
||||||
@ -237,30 +207,18 @@ func TestMultiplePackages(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) {
|
||||||
file, err := os.Open("test-fixtures/status/multiple")
|
f, err := os.Open(test.fixturePath)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal("Unable to read: ", err)
|
t.Cleanup(func() { require.NoError(t, f.Close()) })
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := file.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("closing file failed:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
pkgs, err := parseDpkgStatus(file)
|
reader := bufio.NewReader(f)
|
||||||
if err != nil {
|
|
||||||
t.Fatal("Unable to read file contents: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(pkgs) != 3 {
|
entries, err := parseDpkgStatus(reader)
|
||||||
t.Fatalf("unexpected number of entries: %d", len(pkgs))
|
require.NoError(t, err)
|
||||||
}
|
|
||||||
|
|
||||||
for idx, entry := range pkgs {
|
if diff := cmp.Diff(test.expected, entries); diff != "" {
|
||||||
compareEntries(t, entry.Metadata.(pkg.DpkgMetadata), test.expected[idx])
|
t.Errorf("unexpected entry (-want +got):\n%s", diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -304,23 +262,23 @@ func TestSourceVersionExtract(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertAs(expected error) assert.ErrorAssertionFunc {
|
func requireAs(expected error) require.ErrorAssertionFunc {
|
||||||
return func(t assert.TestingT, err error, i ...interface{}) bool {
|
return func(t require.TestingT, err error, i ...interface{}) {
|
||||||
return assert.ErrorAs(t, err, &expected)
|
require.ErrorAs(t, err, &expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_parseDpkgStatus(t *testing.T) {
|
func Test_parseDpkgStatus_negativeCases(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
input string
|
input string
|
||||||
want []pkg.Package
|
want []pkg.Package
|
||||||
wantErr assert.ErrorAssertionFunc
|
wantErr require.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "no more packages",
|
name: "no more packages",
|
||||||
input: `Package: apt`,
|
input: `Package: apt`,
|
||||||
wantErr: assert.NoError,
|
wantErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "duplicated key",
|
name: "duplicated key",
|
||||||
@ -328,14 +286,14 @@ func Test_parseDpkgStatus(t *testing.T) {
|
|||||||
Package: apt-get
|
Package: apt-get
|
||||||
|
|
||||||
`,
|
`,
|
||||||
wantErr: assertAs(errors.New("duplicate key discovered: Package")),
|
wantErr: requireAs(errors.New("duplicate key discovered: Package")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no match for continuation",
|
name: "no match for continuation",
|
||||||
input: ` Package: apt
|
input: ` Package: apt
|
||||||
|
|
||||||
`,
|
`,
|
||||||
wantErr: assertAs(errors.New("no match for continuation: line: ' Package: apt'")),
|
wantErr: requireAs(errors.New("no match for continuation: line: ' Package: apt'")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "find keys",
|
name: "find keys",
|
||||||
@ -348,6 +306,8 @@ Installed-Size: 10kib
|
|||||||
{
|
{
|
||||||
Name: "apt",
|
Name: "apt",
|
||||||
Type: "deb",
|
Type: "deb",
|
||||||
|
PURL: "pkg:deb/debian/apt?distro=debian-10",
|
||||||
|
Locations: source.NewLocationSet(source.NewLocation("place")),
|
||||||
MetadataType: "DpkgMetadata",
|
MetadataType: "DpkgMetadata",
|
||||||
Metadata: pkg.DpkgMetadata{
|
Metadata: pkg.DpkgMetadata{
|
||||||
Package: "apt",
|
Package: "apt",
|
||||||
@ -356,16 +316,18 @@ Installed-Size: 10kib
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantErr: assert.NoError,
|
wantErr: require.NoError,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
r := bufio.NewReader(strings.NewReader(tt.input))
|
pkgtest.NewCatalogTester().
|
||||||
got, err := parseDpkgStatus(r)
|
FromString("place", tt.input).
|
||||||
tt.wantErr(t, err, fmt.Sprintf("parseDpkgStatus"))
|
WithErrorAssertion(tt.wantErr).
|
||||||
assert.Equal(t, tt.want, got)
|
WithLinuxRelease(linux.Release{ID: "debian", VersionID: "10"}).
|
||||||
|
Expects(tt.want, nil).
|
||||||
|
TestParser(t, parseDpkgDB)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -376,53 +338,53 @@ func Test_handleNewKeyValue(t *testing.T) {
|
|||||||
line string
|
line string
|
||||||
wantKey string
|
wantKey string
|
||||||
wantVal interface{}
|
wantVal interface{}
|
||||||
wantErr assert.ErrorAssertionFunc
|
wantErr require.ErrorAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "cannot parse field",
|
name: "cannot parse field",
|
||||||
line: "blabla",
|
line: "blabla",
|
||||||
wantErr: assertAs(errors.New("cannot parse field from line: 'blabla'")),
|
wantErr: requireAs(errors.New("cannot parse field from line: 'blabla'")),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "parse field",
|
name: "parse field",
|
||||||
line: "key: val",
|
line: "key: val",
|
||||||
wantKey: "key",
|
wantKey: "key",
|
||||||
wantVal: "val",
|
wantVal: "val",
|
||||||
wantErr: assert.NoError,
|
wantErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "parse installed size",
|
name: "parse installed size",
|
||||||
line: "InstalledSize: 128",
|
line: "InstalledSize: 128",
|
||||||
wantKey: "InstalledSize",
|
wantKey: "InstalledSize",
|
||||||
wantVal: 128,
|
wantVal: 128,
|
||||||
wantErr: assert.NoError,
|
wantErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "parse installed kib size",
|
name: "parse installed kib size",
|
||||||
line: "InstalledSize: 1kib",
|
line: "InstalledSize: 1kib",
|
||||||
wantKey: "InstalledSize",
|
wantKey: "InstalledSize",
|
||||||
wantVal: 1024,
|
wantVal: 1024,
|
||||||
wantErr: assert.NoError,
|
wantErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "parse installed kb size",
|
name: "parse installed kb size",
|
||||||
line: "InstalledSize: 1kb",
|
line: "InstalledSize: 1kb",
|
||||||
wantKey: "InstalledSize",
|
wantKey: "InstalledSize",
|
||||||
wantVal: 1000,
|
wantVal: 1000,
|
||||||
wantErr: assert.NoError,
|
wantErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "parse installed-size mb",
|
name: "parse installed-size mb",
|
||||||
line: "Installed-Size: 1 mb",
|
line: "Installed-Size: 1 mb",
|
||||||
wantKey: "InstalledSize",
|
wantKey: "InstalledSize",
|
||||||
wantVal: 1000000,
|
wantVal: 1000000,
|
||||||
wantErr: assert.NoError,
|
wantErr: require.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "fail parsing installed-size",
|
name: "fail parsing installed-size",
|
||||||
line: "Installed-Size: 1bla",
|
line: "Installed-Size: 1bla",
|
||||||
wantKey: "",
|
wantKey: "",
|
||||||
wantErr: assertAs(fmt.Errorf("unhandled size name: %s", "bla")),
|
wantErr: requireAs(fmt.Errorf("unhandled size name: %s", "bla")),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@ -4,7 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
@ -40,29 +41,14 @@ func TestMD5SumInfoParsing(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.fixture, func(t *testing.T) {
|
t.Run(test.fixture, func(t *testing.T) {
|
||||||
file, err := os.Open(test.fixture)
|
f, err := os.Open(test.fixture)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal("Unable to read: ", err)
|
t.Cleanup(func() { require.NoError(t, f.Close()) })
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := file.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("closing file failed:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
actual := parseDpkgMD5Info(file)
|
actual := parseDpkgMD5Info(f)
|
||||||
|
|
||||||
if len(actual) != len(test.expected) {
|
if diff := cmp.Diff(test.expected, actual); diff != "" {
|
||||||
for _, a := range actual {
|
t.Errorf("unexpected md5 files (-want +got):\n%s", diff)
|
||||||
t.Logf(" %+v", a)
|
|
||||||
}
|
|
||||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
diffs := deep.Equal(actual, test.expected)
|
|
||||||
for _, d := range diffs {
|
|
||||||
t.Errorf("diff: %+v", d)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
@ -89,29 +75,14 @@ func TestConffileInfoParsing(t *testing.T) {
|
|||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.fixture, func(t *testing.T) {
|
t.Run(test.fixture, func(t *testing.T) {
|
||||||
file, err := os.Open(test.fixture)
|
f, err := os.Open(test.fixture)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal("Unable to read: ", err)
|
t.Cleanup(func() { require.NoError(t, f.Close()) })
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := file.Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal("closing file failed:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
actual := parseDpkgConffileInfo(file)
|
actual := parseDpkgConffileInfo(f)
|
||||||
|
|
||||||
if len(actual) != len(test.expected) {
|
if diff := cmp.Diff(test.expected, actual); diff != "" {
|
||||||
for _, a := range actual {
|
t.Errorf("unexpected md5 files (-want +got):\n%s", diff)
|
||||||
t.Logf(" %+v", a)
|
|
||||||
}
|
|
||||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
|
|
||||||
}
|
|
||||||
|
|
||||||
diffs := deep.Equal(actual, test.expected)
|
|
||||||
for _, d := range diffs {
|
|
||||||
t.Errorf("diff: %+v", d)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@ -209,5 +209,5 @@ func TestParseDotnetDeps(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var expectedRelationships []artifact.Relationship
|
var expectedRelationships []artifact.Relationship
|
||||||
pkgtest.TestGenericParser(t, fixture, parseDotnetDeps, expected, expectedRelationships)
|
pkgtest.TestFileParser(t, fixture, parseDotnetDeps, expected, expectedRelationships)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
package pkgtest
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
|
||||||
"github.com/google/go-cmp/cmp/cmpopts"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AssertPackagesEqual(t testing.TB, expected, actual []pkg.Package) {
|
|
||||||
if diff := cmp.Diff(expected, actual,
|
|
||||||
cmpopts.IgnoreFields(pkg.Package{}, "id"), // note: ID is not deterministic for test purposes
|
|
||||||
cmp.Comparer(
|
|
||||||
func(x, y source.LocationSet) bool {
|
|
||||||
xs := x.ToSlice()
|
|
||||||
ys := y.ToSlice()
|
|
||||||
|
|
||||||
if len(xs) != len(ys) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for i, xe := range xs {
|
|
||||||
ye := ys[i]
|
|
||||||
if !(cmp.Equal(xe.Coordinates, ye.Coordinates) && cmp.Equal(xe.VirtualPath, ye.VirtualPath)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
),
|
|
||||||
); diff != "" {
|
|
||||||
t.Errorf("unexpected packages from parsing (-expected +actual)\n%s", diff)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,37 +1,175 @@
|
|||||||
package pkgtest
|
package pkgtest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
|
"github.com/google/go-cmp/cmp/cmpopts"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||||
"github.com/anchore/syft/syft/artifact"
|
"github.com/anchore/syft/syft/artifact"
|
||||||
|
"github.com/anchore/syft/syft/linux"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGenericParser(t *testing.T, fixturePath string, parser generic.Parser, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) {
|
type locationComparer func(x, y source.Location) bool
|
||||||
t.Helper()
|
|
||||||
TestGenericParserWithEnv(t, fixturePath, parser, nil, expectedPkgs, expectedRelationships)
|
type CatalogTester struct {
|
||||||
|
expectedPkgs []pkg.Package
|
||||||
|
expectedRelationships []artifact.Relationship
|
||||||
|
env *generic.Environment
|
||||||
|
reader source.LocationReadCloser
|
||||||
|
resolver source.FileResolver
|
||||||
|
wantErr require.ErrorAssertionFunc
|
||||||
|
compareOptions []cmp.Option
|
||||||
|
locationComparer locationComparer
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenericParserWithEnv(t *testing.T, fixturePath string, parser generic.Parser, env *generic.Environment, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) {
|
func NewCatalogTester() *CatalogTester {
|
||||||
|
return &CatalogTester{
|
||||||
|
wantErr: require.NoError,
|
||||||
|
locationComparer: func(x, y source.Location) bool {
|
||||||
|
return cmp.Equal(x.Coordinates, y.Coordinates) && cmp.Equal(x.VirtualPath, y.VirtualPath)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) FromFile(t *testing.T, path string) *CatalogTester {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
fixture, err := os.Open(fixturePath)
|
|
||||||
|
fixture, err := os.Open(path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
actualPkgs, actualRelationships, err := parser(nil, env, source.LocationReadCloser{
|
p.reader = source.LocationReadCloser{
|
||||||
Location: source.NewLocation(fixture.Name()),
|
Location: source.NewLocation(fixture.Name()),
|
||||||
ReadCloser: fixture,
|
ReadCloser: fixture,
|
||||||
})
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) FromString(location, data string) *CatalogTester {
|
||||||
|
p.reader = source.LocationReadCloser{
|
||||||
|
Location: source.NewLocation(location),
|
||||||
|
ReadCloser: io.NopCloser(strings.NewReader(data)),
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) WithLinuxRelease(r linux.Release) *CatalogTester {
|
||||||
|
if p.env == nil {
|
||||||
|
p.env = &generic.Environment{}
|
||||||
|
}
|
||||||
|
p.env.LinuxRelease = &r
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) WithEnv(env *generic.Environment) *CatalogTester {
|
||||||
|
p.env = env
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) WithError() *CatalogTester {
|
||||||
|
p.wantErr = require.Error
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) WithErrorAssertion(a require.ErrorAssertionFunc) *CatalogTester {
|
||||||
|
p.wantErr = a
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) WithResolver(r source.FileResolver) *CatalogTester {
|
||||||
|
p.resolver = r
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) WithImageResolver(t *testing.T, fixtureName string) *CatalogTester {
|
||||||
|
t.Helper()
|
||||||
|
img := imagetest.GetFixtureImage(t, "docker-archive", fixtureName)
|
||||||
|
|
||||||
|
s, err := source.NewFromImage(img, fixtureName)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
AssertPackagesEqual(t, expectedPkgs, actualPkgs)
|
r, err := s.FileResolver(source.SquashedScope)
|
||||||
|
require.NoError(t, err)
|
||||||
|
p.resolver = r
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
if diff := cmp.Diff(expectedRelationships, actualRelationships); diff != "" {
|
func (p *CatalogTester) IgnoreLocationLayer() *CatalogTester {
|
||||||
|
p.locationComparer = func(x, y source.Location) bool {
|
||||||
|
return cmp.Equal(x.Coordinates.RealPath, y.Coordinates.RealPath) && cmp.Equal(x.VirtualPath, y.VirtualPath)
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) Expects(pkgs []pkg.Package, relationships []artifact.Relationship) *CatalogTester {
|
||||||
|
p.expectedPkgs = pkgs
|
||||||
|
p.expectedRelationships = relationships
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) TestParser(t *testing.T, parser generic.Parser) {
|
||||||
|
t.Helper()
|
||||||
|
pkgs, relationships, err := parser(p.resolver, p.env, p.reader)
|
||||||
|
p.wantErr(t, err)
|
||||||
|
p.assertPkgs(t, pkgs, relationships)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) {
|
||||||
|
t.Helper()
|
||||||
|
pkgs, relationships, err := cataloger.Catalog(p.resolver)
|
||||||
|
p.wantErr(t, err)
|
||||||
|
p.assertPkgs(t, pkgs, relationships)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *CatalogTester) assertPkgs(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
p.compareOptions = append(p.compareOptions,
|
||||||
|
cmpopts.IgnoreFields(pkg.Package{}, "id"), // note: ID is not deterministic for test purposes
|
||||||
|
cmp.Comparer(
|
||||||
|
func(x, y source.LocationSet) bool {
|
||||||
|
xs := x.ToSlice()
|
||||||
|
ys := y.ToSlice()
|
||||||
|
|
||||||
|
if len(xs) != len(ys) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i, xe := range xs {
|
||||||
|
ye := ys[i]
|
||||||
|
if !p.locationComparer(xe, ye) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if diff := cmp.Diff(p.expectedPkgs, pkgs, p.compareOptions...); diff != "" {
|
||||||
|
t.Errorf("unexpected packages from parsing (-expected +actual)\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
if diff := cmp.Diff(p.expectedRelationships, relationships, p.compareOptions...); diff != "" {
|
||||||
t.Errorf("unexpected relationships from parsing (-expected +actual)\n%s", diff)
|
t.Errorf("unexpected relationships from parsing (-expected +actual)\n%s", diff)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFileParser(t *testing.T, fixturePath string, parser generic.Parser, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) {
|
||||||
|
t.Helper()
|
||||||
|
NewCatalogTester().FromFile(t, fixturePath).Expects(expectedPkgs, expectedRelationships).TestParser(t, parser)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFileParserWithEnv(t *testing.T, fixturePath string, parser generic.Parser, env *generic.Environment, expectedPkgs []pkg.Package, expectedRelationships []artifact.Relationship) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
NewCatalogTester().FromFile(t, fixturePath).WithEnv(env).Expects(expectedPkgs, expectedRelationships).TestParser(t, parser)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,22 +1,16 @@
|
|||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/scylladb/go-set/strset"
|
"github.com/scylladb/go-set/strset"
|
||||||
|
|
||||||
"github.com/anchore/packageurl-go"
|
|
||||||
"github.com/anchore/syft/syft/file"
|
"github.com/anchore/syft/syft/file"
|
||||||
"github.com/anchore/syft/syft/linux"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const DpkgDBGlob = "**/var/lib/dpkg/{status,status.d/**}"
|
const DpkgDBGlob = "**/var/lib/dpkg/{status,status.d/**}"
|
||||||
|
|
||||||
var (
|
var _ FileOwner = (*DpkgMetadata)(nil)
|
||||||
_ FileOwner = (*DpkgMetadata)(nil)
|
|
||||||
_ urlIdentifier = (*DpkgMetadata)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described
|
// DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described
|
||||||
// at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section.
|
// at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section.
|
||||||
@ -39,40 +33,6 @@ type DpkgFileRecord struct {
|
|||||||
IsConfigFile bool `json:"isConfigFile"`
|
IsConfigFile bool `json:"isConfigFile"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec)
|
|
||||||
func (m DpkgMetadata) PackageURL(distro *linux.Release) string {
|
|
||||||
var namespace string
|
|
||||||
if distro != nil {
|
|
||||||
namespace = distro.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
qualifiers := map[string]string{
|
|
||||||
PURLQualifierArch: m.Architecture,
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.Source != "" {
|
|
||||||
if m.SourceVersion != "" {
|
|
||||||
qualifiers[PURLQualifierUpstream] = fmt.Sprintf("%s@%s", m.Source, m.SourceVersion)
|
|
||||||
} else {
|
|
||||||
qualifiers[PURLQualifierUpstream] = m.Source
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return packageurl.NewPackageURL(
|
|
||||||
// TODO: replace with `packageurl.TypeDebian` upon merge of https://github.com/package-url/packageurl-go/pull/21
|
|
||||||
// TODO: or, since we're now using an Anchore fork of this module, we could do this sooner.
|
|
||||||
"deb",
|
|
||||||
namespace,
|
|
||||||
m.Package,
|
|
||||||
m.Version,
|
|
||||||
PURLQualifiers(
|
|
||||||
qualifiers,
|
|
||||||
distro,
|
|
||||||
),
|
|
||||||
"",
|
|
||||||
).ToString()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m DpkgMetadata) OwnedFiles() (result []string) {
|
func (m DpkgMetadata) OwnedFiles() (result []string) {
|
||||||
s := strset.New()
|
s := strset.New()
|
||||||
for _, f := range m.Files {
|
for _, f := range m.Files {
|
||||||
|
|||||||
@ -66,24 +66,6 @@ func TestPackageURL(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expected: "pkg:npm/name@v0.1.0",
|
expected: "pkg:npm/name@v0.1.0",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "deb",
|
|
||||||
distro: &linux.Release{
|
|
||||||
ID: "ubuntu",
|
|
||||||
VersionID: "20.04",
|
|
||||||
},
|
|
||||||
pkg: Package{
|
|
||||||
Name: "bad-name",
|
|
||||||
Version: "bad-v0.1.0",
|
|
||||||
Type: DebPkg,
|
|
||||||
Metadata: DpkgMetadata{
|
|
||||||
Package: "name",
|
|
||||||
Version: "v0.1.0",
|
|
||||||
Architecture: "amd64",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: "pkg:deb/ubuntu/name@v0.1.0?arch=amd64&distro=ubuntu-20.04",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "rpm",
|
name: "rpm",
|
||||||
distro: &linux.Release{
|
distro: &linux.Release{
|
||||||
@ -199,6 +181,7 @@ func TestPackageURL(t *testing.T) {
|
|||||||
expectedTypes.Remove(string(ConanPkg))
|
expectedTypes.Remove(string(ConanPkg))
|
||||||
expectedTypes.Remove(string(DartPubPkg))
|
expectedTypes.Remove(string(DartPubPkg))
|
||||||
expectedTypes.Remove(string(DotnetPkg))
|
expectedTypes.Remove(string(DotnetPkg))
|
||||||
|
expectedTypes.Remove(string(DebPkg))
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user