Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add abstraction for adding relationships from package cataloger results #2853

Merged
merged 11 commits into from
May 14, 2024
6 changes: 5 additions & 1 deletion internal/string_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,9 @@ func SplitAny(s string, seps string) []string {
splitter := func(r rune) bool {
return strings.ContainsRune(seps, r)
}
return strings.FieldsFunc(s, splitter)
result := strings.FieldsFunc(s, splitter)
if len(result) == 0 {
return []string{s}
}
return result
}
2 changes: 1 addition & 1 deletion internal/string_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func TestSplitAny(t *testing.T) {
name: "empty",
input: "",
fields: ",",
want: []string{},
want: []string{""},
},
{
name: "multiple separators",
Expand Down
4 changes: 3 additions & 1 deletion syft/pkg/cataloger/alpine/cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ package alpine
import (
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/pkg/cataloger/internal/dependency"
)

// NewDBCataloger returns a new cataloger object initialized for Alpine package DB flat-file stores.
func NewDBCataloger() pkg.Cataloger {
return generic.NewCataloger("apk-db-cataloger").
WithParserByGlobs(parseApkDB, pkg.ApkDBGlob)
WithParserByGlobs(parseApkDB, pkg.ApkDBGlob).
WithProcessors(dependency.Processor(dbEntryDependencySpecifier))
}
230 changes: 230 additions & 0 deletions syft/pkg/cataloger/alpine/cataloger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,239 @@ package alpine
import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"

"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/pkgtest"
)

func TestApkDBCataloger(t *testing.T) {
dbLocation := file.NewLocation("lib/apk/db/installed")

bashPkg := pkg.Package{
Name: "bash",
Version: "5.2.21-r0",
Type: pkg.ApkPkg,
FoundBy: "apk-db-cataloger",
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("GPL-3.0-or-later", dbLocation),
),
Locations: file.NewLocationSet(dbLocation),
Metadata: pkg.ApkDBEntry{
Package: "bash",
OriginPackage: "bash",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
Version: "5.2.21-r0",
Architecture: "x86_64",
URL: "https://www.gnu.org/software/bash/bash.html",
Description: "The GNU Bourne Again shell",
Size: 448728,
InstalledSize: 1396736,
Dependencies: []string{
"/bin/sh", "so:libc.musl-x86_64.so.1", "so:libreadline.so.8",
},
Provides: []string{
"cmd:bash=5.2.21-r0",
},
// note: files not provided and not under test
},
}

busyboxBinshPkg := pkg.Package{
Name: "busybox-binsh",
Version: "1.36.1-r15",
Type: pkg.ApkPkg,
FoundBy: "apk-db-cataloger",
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("GPL-2.0-only", dbLocation),
),
Locations: file.NewLocationSet(dbLocation),
Metadata: pkg.ApkDBEntry{
Package: "busybox-binsh",
OriginPackage: "busybox",
Maintainer: "Sören Tempel <soeren+alpine@soeren-tempel.net>",
Version: "1.36.1-r15",
Architecture: "x86_64",
URL: "https://busybox.net/",
Description: "busybox ash /bin/sh",
Size: 1543,
InstalledSize: 8192,
Dependencies: []string{
"busybox=1.36.1-r15",
},
Provides: []string{
"/bin/sh", "cmd:sh=1.36.1-r15",
},
// note: files not provided and not under test
},
}

muslPkg := pkg.Package{
Name: "musl",
Version: "1.2.4_git20230717-r4",
Type: pkg.ApkPkg,
FoundBy: "apk-db-cataloger",
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("MIT", dbLocation),
),
Locations: file.NewLocationSet(dbLocation),
Metadata: pkg.ApkDBEntry{
Package: "musl",
OriginPackage: "musl",
Maintainer: "Timo Teräs <timo.teras@iki.fi>",
Version: "1.2.4_git20230717-r4",
Architecture: "x86_64",
URL: "https://musl.libc.org/",
Description: "the musl c library (libc) implementation",
Size: 407278,
InstalledSize: 667648,
Dependencies: []string{},
Provides: []string{
"so:libc.musl-x86_64.so.1=1",
},
// note: files not provided and not under test
},
}

readlinePkg := pkg.Package{
Name: "readline",
Version: "8.2.1-r2",
Type: pkg.ApkPkg,
FoundBy: "apk-db-cataloger",
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("GPL-2.0-or-later", dbLocation),
),
Locations: file.NewLocationSet(dbLocation),
Metadata: pkg.ApkDBEntry{
Package: "readline",
OriginPackage: "readline",
Maintainer: "Natanael Copa <ncopa@alpinelinux.org>",
Version: "8.2.1-r2",
Architecture: "x86_64",
URL: "https://tiswww.cwru.edu/php/chet/readline/rltop.html",
Description: "GNU readline library",
Size: 119878,
InstalledSize: 303104,
Dependencies: []string{
"so:libc.musl-x86_64.so.1", "so:libncursesw.so.6",
},
Provides: []string{
"so:libreadline.so.8=8.2",
},
// note: files not provided and not under test
},
}

expectedPkgs := []pkg.Package{
bashPkg,
busyboxBinshPkg,
muslPkg,
readlinePkg,
}

// # apk info --depends bash
// bash-5.2.21-r0 depends on:
// /bin/sh
// so:libc.musl-x86_64.so.1
// so:libreadline.so.8
//
// # apk info --who-owns /bin/sh
// /bin/sh is owned by busybox-binsh-1.36.1-r15
//
// # find / | grep musl
// /lib/ld-musl-x86_64.so.1
// /lib/libc.musl-x86_64.so.1
//
// # apk info --who-owns '/lib/libc.musl-x86_64.so.1'
// /lib/libc.musl-x86_64.so.1 is owned by musl-1.2.4_git20230717-r4
//
// # find / | grep libreadline
// /usr/lib/libreadline.so.8.2
// /usr/lib/libreadline.so.8
//
// # apk info --who-owns '/usr/lib/libreadline.so.8'
// /usr/lib/libreadline.so.8 is owned by readline-8.2.1-r2

expectedRelationships := []artifact.Relationship{
{
From: busyboxBinshPkg,
To: bashPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: readlinePkg,
To: bashPkg,
Type: artifact.DependencyOfRelationship,
},
{
From: muslPkg,
To: readlinePkg,
Type: artifact.DependencyOfRelationship,
},
{
From: muslPkg,
To: bashPkg,
Type: artifact.DependencyOfRelationship,
},
}

pkgtest.NewCatalogTester().
FromDirectory(t, "test-fixtures/multiple-1").
WithCompareOptions(cmpopts.IgnoreFields(pkg.ApkDBEntry{}, "Files", "GitCommit", "Checksum")).
Expects(expectedPkgs, expectedRelationships).
TestCataloger(t, NewDBCataloger())

}

func TestCatalogerDependencyTree(t *testing.T) {
assertion := func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) {
expected := map[string][]string{
"alpine-baselayout": {"busybox", "alpine-baselayout-data", "musl"},
"apk-tools": {"ca-certificates-bundle", "musl", "libcrypto1.1", "libssl1.1", "zlib"},
"busybox": {"musl"},
"libc-utils": {"musl-utils"},
"libcrypto1.1": {"musl"},
"libssl1.1": {"musl", "libcrypto1.1"},
"musl-utils": {"scanelf", "musl"},
"scanelf": {"musl"},
"ssl_client": {"musl", "libcrypto1.1", "libssl1.1"},
"zlib": {"musl"},
}
pkgsByID := make(map[artifact.ID]pkg.Package)
for _, p := range pkgs {
p.SetID()
pkgsByID[p.ID()] = p
}

actualDependencies := make(map[string][]string)

for _, r := range relationships {
switch r.Type {
case artifact.DependencyOfRelationship:
to := pkgsByID[r.To.ID()]
from := pkgsByID[r.From.ID()]
actualDependencies[to.Name] = append(actualDependencies[to.Name], from.Name)
default:
t.Fatalf("unexpected relationship type: %+v", r.Type)
}
}

if d := cmp.Diff(expected, actualDependencies); d != "" {
t.Fail()
t.Log(d)
}
}

pkgtest.NewCatalogTester().
FromDirectory(t, "test-fixtures/multiple-2").
ExpectsAssertion(assertion).
TestCataloger(t, NewDBCataloger())

}

func TestCataloger_Globs(t *testing.T) {
tests := []struct {
name string
Expand Down
48 changes: 48 additions & 0 deletions syft/pkg/cataloger/alpine/dependency.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package alpine

import (
"strings"

"github.com/anchore/syft/internal"
"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.ApkDBEntry)
if !ok {
log.Tracef("cataloger failed to extract apk metadata for package %+v", p.Name)
return dependency.Specification{}
}

provides := []string{p.Name}
provides = append(provides, stripVersionSpecifiers(meta.Provides)...)

return dependency.Specification{
Provides: provides,
Requires: stripVersionSpecifiers(meta.Dependencies),
}
}

func stripVersionSpecifiers(given []string) []string {
var keys []string
for _, key := range given {
key = stripVersionSpecifier(key)
if key == "" {
continue
}
keys = append(keys, key)
}
return keys
}

func stripVersionSpecifier(s string) string {
// examples:
// musl>=1 --> musl
// cmd:scanelf=1.3.4-r0 --> cmd:scanelf

return strings.TrimSpace(internal.SplitAny(s, "<>=")[0])
}