port apk cataloger to new generic cataloger pattern (#1283)

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman
2022-10-24 13:51:09 -04:00
committed by GitHub
parent 28cadfdb5d
commit f36c0ca971
7 changed files with 136 additions and 117 deletions
+6 -7
View File
@@ -5,14 +5,13 @@ package apkdb
import (
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// NewApkdbCataloger returns a new Alpine DB cataloger object.
func NewApkdbCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
pkg.ApkDBGlob: parseApkDB,
}
const catalogerName = "apkdb-cataloger"
return common.NewGenericCataloger(nil, globParsers, "apkdb-cataloger")
// NewApkdbCataloger returns a new Alpine DB cataloger object.
func NewApkdbCataloger() *generic.Cataloger {
return generic.NewCataloger(catalogerName).
WithParserByGlobs(parseApkDB, pkg.ApkDBGlob)
}
+57
View File
@@ -0,0 +1,57 @@
package apkdb
import (
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func newPackage(d pkg.ApkMetadata, release *linux.Release, locations ...source.Location) pkg.Package {
p := pkg.Package{
Name: d.Package,
Version: d.Version,
Locations: source.NewLocationSet(locations...),
Licenses: strings.Split(d.License, " "),
PURL: packageURL(d, release),
Type: pkg.ApkPkg,
MetadataType: pkg.ApkMetadataType,
Metadata: d,
}
p.SetID()
return p
}
// packageURL returns the PURL for the specific Alpine package (see https://github.com/package-url/purl-spec)
func packageURL(m pkg.ApkMetadata, distro *linux.Release) string {
if distro == nil || distro.ID != "alpine" {
// note: there is no namespace variation (like with debian ID_LIKE for ubuntu ID, for example)
return ""
}
qualifiers := map[string]string{
pkg.PURLQualifierArch: m.Architecture,
}
if m.OriginPackage != "" {
qualifiers[pkg.PURLQualifierUpstream] = m.OriginPackage
}
return packageurl.NewPackageURL(
// note: this is currently a candidate and not technically within spec
// see https://github.com/package-url/purl-spec#other-candidate-types-to-define
"alpine",
"",
m.Package,
m.Version,
pkg.PURLQualifiers(
qualifiers,
distro,
),
"",
).ToString()
}
+176
View File
@@ -0,0 +1,176 @@
package apkdb
import (
"strings"
"testing"
"github.com/go-test/deep"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
)
func Test_PackageURL(t *testing.T) {
tests := []struct {
name string
metadata pkg.ApkMetadata
distro linux.Release
expected string
}{
{
name: "bad distro",
metadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
},
distro: linux.Release{
ID: "something else",
VersionID: "3.4.6",
},
expected: "",
},
{
name: "gocase",
metadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
},
distro: linux.Release{
ID: "alpine",
VersionID: "3.4.6",
},
expected: "pkg:alpine/p@v?arch=a&distro=alpine-3.4.6",
},
{
name: "missing architecture",
metadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
},
distro: linux.Release{
ID: "alpine",
VersionID: "3.4.6",
},
expected: "pkg:alpine/p@v?distro=alpine-3.4.6",
},
// verify #351
{
metadata: pkg.ApkMetadata{
Package: "g++",
Version: "v84",
Architecture: "am86",
},
distro: linux.Release{
ID: "alpine",
VersionID: "3.4.6",
},
expected: "pkg:alpine/g++@v84?arch=am86&distro=alpine-3.4.6",
},
{
metadata: pkg.ApkMetadata{
Package: "g plus plus",
Version: "v84",
Architecture: "am86",
},
distro: linux.Release{
ID: "alpine",
VersionID: "3.15.0",
},
expected: "pkg:alpine/g%20plus%20plus@v84?arch=am86&distro=alpine-3.15.0",
},
{
name: "add source information as qualifier",
metadata: pkg.ApkMetadata{
Package: "p",
Version: "v",
Architecture: "a",
OriginPackage: "origin",
},
distro: linux.Release{
ID: "alpine",
VersionID: "3.4.6",
},
expected: "pkg:alpine/p@v?arch=a&upstream=origin&distro=alpine-3.4.6",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := packageURL(test.metadata, &test.distro)
if actual != test.expected {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.expected, actual, true)
t.Errorf("diff: %s", dmp.DiffPrettyText(diffs))
}
if test.expected == "" {
return
}
// verify packageurl can parse
purl, err := packageurl.FromString(actual)
if err != nil {
t.Errorf("cannot re-parse purl: %s", actual)
}
if purl.Name != test.metadata.Package {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.metadata.Package, purl.Name, true)
t.Errorf("invalid purl name: %s", dmp.DiffPrettyText(diffs))
}
if purl.Version != test.metadata.Version {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.metadata.Version, purl.Version, true)
t.Errorf("invalid purl version: %s", dmp.DiffPrettyText(diffs))
}
if purl.Qualifiers.Map()["arch"] != test.metadata.Architecture {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(test.metadata.Architecture, purl.Qualifiers.Map()["arch"], true)
t.Errorf("invalid purl architecture: %s", dmp.DiffPrettyText(diffs))
}
})
}
}
func TestApkMetadata_FileOwner(t *testing.T) {
tests := []struct {
metadata pkg.ApkMetadata
expected []string
}{
{
metadata: pkg.ApkMetadata{
Files: []pkg.ApkFileRecord{
{Path: "/somewhere"},
{Path: "/else"},
},
},
expected: []string{
"/else",
"/somewhere",
},
},
{
metadata: pkg.ApkMetadata{
Files: []pkg.ApkFileRecord{
{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)
}
})
}
}
+13 -17
View File
@@ -13,32 +13,23 @@ import (
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/source"
)
// integrity check
var _ common.ParserFn = parseApkDB
func newApkDBPackage(d *pkg.ApkMetadata) *pkg.Package {
return &pkg.Package{
Name: d.Package,
Version: d.Version,
Licenses: strings.Split(d.License, " "),
Type: pkg.ApkPkg,
MetadataType: pkg.ApkMetadataType,
Metadata: *d,
}
}
var _ generic.Parser = parseApkDB
// parseApkDb parses individual packages from a given Alpine DB file. For more information on specific fields
// see https://wiki.alpinelinux.org/wiki/Apk_spec .
func parseApkDB(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
func parseApkDB(_ source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
// larger capacity for the scanner.
const maxScannerCapacity = 1024 * 1024
// a new larger buffer for the scanner
bufScan := make([]byte, maxScannerCapacity)
packages := make([]*pkg.Package, 0)
pkgs := make([]pkg.Package, 0)
scanner := bufio.NewScanner(reader)
scanner.Buffer(bufScan, maxScannerCapacity)
@@ -55,6 +46,11 @@ func parseApkDB(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relation
return 0, data, bufio.ErrFinalToken
}
var r *linux.Release
if env != nil {
r = env.LinuxRelease
}
scanner.Split(onDoubleLF)
for scanner.Scan() {
metadata, err := parseApkDBEntry(strings.NewReader(scanner.Text()))
@@ -62,7 +58,7 @@ func parseApkDB(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relation
return nil, nil, err
}
if metadata != nil {
packages = append(packages, newApkDBPackage(metadata))
pkgs = append(pkgs, newPackage(*metadata, r, reader.Location))
}
}
@@ -70,7 +66,7 @@ func parseApkDB(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relation
return nil, nil, fmt.Errorf("failed to parse APK DB file: %w", err)
}
return packages, nil, nil
return pkgs, nil, nil
}
// parseApkDBEntry reads and parses a single pkg.ApkMetadata element from the stream, returning nil if their are no more entries.
+25 -30
View File
@@ -7,9 +7,13 @@ import (
"github.com/go-test/deep"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/linux"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/source"
)
func TestExtraFileAttributes(t *testing.T) {
@@ -617,23 +621,14 @@ func TestSinglePackageDetails(t *testing.T) {
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
file, err := os.Open(test.fixture)
if err != nil {
t.Fatal("Unable to read fixture: ", err)
}
defer func() {
err := file.Close()
if err != nil {
t.Fatal("closing file failed:", err)
}
}()
f, err := os.Open(test.fixture)
require.NoError(t, err)
t.Cleanup(func() { f.Close() })
reader := bufio.NewReader(file)
reader := bufio.NewReader(f)
entry, err := parseApkDBEntry(reader)
if err != nil {
t.Fatal("Unable to read file contents: ", err)
}
require.NoError(t, err)
if diff := deep.Equal(*entry, test.expected); diff != nil {
for _, d := range diff {
@@ -647,16 +642,17 @@ func TestSinglePackageDetails(t *testing.T) {
func TestMultiplePackages(t *testing.T) {
tests := []struct {
fixture string
expected []*pkg.Package
expected []pkg.Package
}{
{
fixture: "test-fixtures/multiple",
expected: []*pkg.Package{
expected: []pkg.Package{
{
Name: "libc-utils",
Version: "0.7.2-r0",
Licenses: []string{"BSD"},
Type: pkg.ApkPkg,
PURL: "pkg:alpine/libc-utils@0.7.2-r0?arch=x86_64&upstream=libc-dev&distro=alpine",
MetadataType: pkg.ApkMetadataType,
Metadata: pkg.ApkMetadata{
Package: "libc-utils",
@@ -680,6 +676,7 @@ func TestMultiplePackages(t *testing.T) {
Version: "1.1.24-r2",
Licenses: []string{"MIT", "BSD", "GPL2+"},
Type: pkg.ApkPkg,
PURL: "pkg:alpine/musl-utils@1.1.24-r2?arch=x86_64&upstream=musl&distro=alpine",
MetadataType: pkg.ApkMetadataType,
Metadata: pkg.ApkMetadata{
Package: "musl-utils",
@@ -764,22 +761,20 @@ func TestMultiplePackages(t *testing.T) {
for _, test := range tests {
t.Run(test.fixture, func(t *testing.T) {
file, err := os.Open(test.fixture)
if err != nil {
t.Fatal("Unable to read: ", err)
}
defer func() {
err := file.Close()
if err != nil {
t.Fatal("closing file failed:", err)
}
}()
f, err := os.Open(test.fixture)
require.NoError(t, err)
t.Cleanup(func() { f.Close() })
// TODO: no relationships are under test yet
pkgs, _, err := parseApkDB(file.Name(), file)
if err != nil {
t.Fatal("Unable to read file contents: ", err)
}
pkgs, _, err := parseApkDB(nil, &generic.Environment{
LinuxRelease: &linux.Release{
ID: "alpine",
},
}, source.LocationReadCloser{
Location: source.NewLocation(f.Name()),
ReadCloser: f,
})
require.NoError(t, err)
if len(pkgs) != 2 {
t.Fatalf("unexpected number of entries: %d", len(pkgs))