mirror of
https://github.com/anchore/syft.git
synced 2026-11-03 07:56:36 +01:00
Add abstraction for adding relationships from package cataloger results (#2853)
* add internal dependency resolver Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * refactor dependency relationship resolution to common object Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * replace cataloger decorator with generic processor Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * refactor resolver to be a single function Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * use common dependency specifier for debian Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * use common dependency specifier for arch Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * use common dependency specifier for alpine Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * allow for generic pkg and rel assertions in testpkg helper Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * do not allow for empty results Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * move stable deduplicate comment Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> * remove relationship resolver type Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com> --------- Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
@@ -4,96 +4,14 @@ Package arch provides a concrete Cataloger implementations for packages relating
|
||||
package arch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"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/generic"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
|
||||
)
|
||||
|
||||
type cataloger struct {
|
||||
*generic.Cataloger
|
||||
}
|
||||
|
||||
// NewDBCataloger returns a new cataloger object initialized for arch linux pacman database flat-file stores.
|
||||
func NewDBCataloger() pkg.Cataloger {
|
||||
return cataloger{
|
||||
Cataloger: generic.NewCataloger("alpm-db-cataloger").
|
||||
WithParserByGlobs(parseAlpmDB, pkg.AlpmDBGlob),
|
||||
}
|
||||
}
|
||||
|
||||
func (c cataloger) Catalog(ctx context.Context, resolver file.Resolver) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
pkgs, rels, err := c.Cataloger.Catalog(ctx, resolver)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
rels = append(rels, associateRelationships(pkgs)...)
|
||||
|
||||
return pkgs, rels, nil
|
||||
}
|
||||
|
||||
// associateRelationships will create relationships between packages based on the "Depends" and "Provides"
|
||||
// fields for installed packages. If there is an installed package that has a dependency that is (somehow) not installed,
|
||||
// then that relationship (between the installed and uninstalled package) will NOT be created.
|
||||
func associateRelationships(pkgs []pkg.Package) (relationships []artifact.Relationship) {
|
||||
// map["provides" + "package"] -> packages that provide that package
|
||||
lookup := make(map[string][]pkg.Package)
|
||||
|
||||
// read providers and add lookup keys as needed
|
||||
for _, p := range pkgs {
|
||||
meta, ok := p.Metadata.(pkg.AlpmDBEntry)
|
||||
if !ok {
|
||||
log.Warnf("cataloger failed to extract alpm 'provides' metadata for package %+v", p.Name)
|
||||
continue
|
||||
}
|
||||
// allow for lookup by package name
|
||||
lookup[p.Name] = append(lookup[p.Name], p)
|
||||
|
||||
for _, provides := range meta.Provides {
|
||||
// allow for lookup by exact specification
|
||||
lookup[provides] = append(lookup[provides], p)
|
||||
|
||||
// allow for lookup by library name only
|
||||
k := stripVersionSpecifier(provides)
|
||||
lookup[k] = append(lookup[k], p)
|
||||
}
|
||||
}
|
||||
|
||||
// read "Depends" and match with provider keys
|
||||
for _, p := range pkgs {
|
||||
meta, ok := p.Metadata.(pkg.AlpmDBEntry)
|
||||
if !ok {
|
||||
log.Warnf("cataloger failed to extract alpm 'dependency' metadata for package %+v", p.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, dep := range meta.Depends {
|
||||
for _, depPkg := range lookup[dep] {
|
||||
relationships = append(relationships, artifact.Relationship{
|
||||
From: depPkg,
|
||||
To: p,
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return relationships
|
||||
}
|
||||
|
||||
func stripVersionSpecifier(s string) string {
|
||||
// examples:
|
||||
// gcc-libs --> gcc-libs
|
||||
// libtree-sitter.so=0-64 --> libtree-sitter.so
|
||||
|
||||
items := strings.Split(s, "=")
|
||||
if len(items) == 0 {
|
||||
return s
|
||||
}
|
||||
|
||||
return strings.TrimSpace(items[0])
|
||||
return generic.NewCataloger("alpm-db-cataloger").
|
||||
WithParserByGlobs(parseAlpmDB, pkg.AlpmDBGlob).
|
||||
WithProcessors(dependency.Processor(dbEntryDependencySpecifier))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package arch
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
|
||||
)
|
||||
|
||||
var _ dependency.Specifier = dbEntryDependencySpecifier
|
||||
|
||||
func dbEntryDependencySpecifier(p pkg.Package) dependency.Specification {
|
||||
meta, ok := p.Metadata.(pkg.AlpmDBEntry)
|
||||
if !ok {
|
||||
log.Tracef("cataloger failed to extract alpm metadata for package %+v", p.Name)
|
||||
return dependency.Specification{}
|
||||
}
|
||||
|
||||
provides := []string{p.Name}
|
||||
for _, key := range meta.Provides {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
provides = append(provides, key, stripVersionSpecifier(key))
|
||||
}
|
||||
|
||||
var requires []string
|
||||
for _, depSpecifier := range meta.Depends {
|
||||
if depSpecifier == "" {
|
||||
continue
|
||||
}
|
||||
requires = append(requires, depSpecifier)
|
||||
}
|
||||
|
||||
return dependency.Specification{
|
||||
Provides: provides,
|
||||
Requires: requires,
|
||||
}
|
||||
}
|
||||
|
||||
func stripVersionSpecifier(s string) string {
|
||||
// examples:
|
||||
// gcc-libs --> gcc-libs
|
||||
// libtree-sitter.so=0-64 --> libtree-sitter.so
|
||||
|
||||
return strings.TrimSpace(strings.Split(s, "=")[0])
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package arch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
|
||||
)
|
||||
|
||||
func Test_dbEntryDependencySpecifier(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
p pkg.Package
|
||||
want dependency.Specification
|
||||
}{
|
||||
{
|
||||
name: "keeps given values + package name",
|
||||
p: pkg.Package{
|
||||
Name: "package-c",
|
||||
Metadata: pkg.AlpmDBEntry{
|
||||
Provides: []string{"a-thing"},
|
||||
Depends: []string{"b-thing"},
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-c", "a-thing", "a-thing"}, // note: gets deduplicated downstream
|
||||
Requires: []string{"b-thing"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "strip version specifiers",
|
||||
p: pkg.Package{
|
||||
Name: "package-a",
|
||||
Metadata: pkg.AlpmDBEntry{
|
||||
Provides: []string{"libtree-sitter.so.me=1-64"},
|
||||
Depends: []string{"libtree-sitter.so.thing=2-64"},
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-a", "libtree-sitter.so.me=1-64", "libtree-sitter.so.me"},
|
||||
Requires: []string{"libtree-sitter.so.thing=2-64"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty dependency data entries",
|
||||
p: pkg.Package{
|
||||
Name: "package-a",
|
||||
Metadata: pkg.AlpmDBEntry{
|
||||
Provides: []string{""},
|
||||
Depends: []string{""},
|
||||
},
|
||||
},
|
||||
want: dependency.Specification{
|
||||
Provides: []string{"package-a"},
|
||||
Requires: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, dbEntryDependencySpecifier(tt.p))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_stripVersionSpecifier(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
version string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty expression",
|
||||
version: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "no expression",
|
||||
version: "gcc-libs",
|
||||
want: "gcc-libs",
|
||||
},
|
||||
{
|
||||
name: "=",
|
||||
version: "libtree-sitter.so=0-64",
|
||||
want: "libtree-sitter.so",
|
||||
},
|
||||
{
|
||||
name: "ignores file paths",
|
||||
version: "/bin/sh",
|
||||
want: "/bin/sh",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, stripVersionSpecifier(tt.version))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user