From 01230aa766180c3eabb63eb7b3181c3e878ca3a0 Mon Sep 17 00:00:00 2001 From: Avi Deitcher Date: Thu, 2 Mar 2023 20:04:56 +0200 Subject: [PATCH] read relative etc/apk/repositories for alpine version when no OS provided (#1615) Signed-off-by: Avi Deitcher --- syft/pkg/cataloger/apkdb/cataloger_test.go | 1 + syft/pkg/cataloger/apkdb/parse_apk_db.go | 75 ++++++++++++++++++- syft/pkg/cataloger/apkdb/parse_apk_db_test.go | 48 ++++++++++++ 3 files changed, 122 insertions(+), 2 deletions(-) diff --git a/syft/pkg/cataloger/apkdb/cataloger_test.go b/syft/pkg/cataloger/apkdb/cataloger_test.go index 5a29607913a..9be92209bd6 100644 --- a/syft/pkg/cataloger/apkdb/cataloger_test.go +++ b/syft/pkg/cataloger/apkdb/cataloger_test.go @@ -24,6 +24,7 @@ func TestCataloger_Globs(t *testing.T) { pkgtest.NewCatalogTester(). FromDirectory(t, test.fixture). ExpectsResolverContentQueries(test.expected). + IgnoreUnfulfilledPathResponses("etc/apk/repositories"). TestCataloger(t, NewApkdbCataloger()) }) } diff --git a/syft/pkg/cataloger/apkdb/parse_apk_db.go b/syft/pkg/cataloger/apkdb/parse_apk_db.go index f83b7bb521f..1587a9eaaf0 100644 --- a/syft/pkg/cataloger/apkdb/parse_apk_db.go +++ b/syft/pkg/cataloger/apkdb/parse_apk_db.go @@ -3,7 +3,9 @@ package apkdb import ( "bufio" "fmt" + "io" "path" + "regexp" "strconv" "strings" @@ -20,11 +22,15 @@ import ( // integrity check var _ generic.Parser = parseApkDB +var ( + repoRegex = regexp.MustCompile(`(?m)^https://.*\.alpinelinux\.org/alpine/v([^/]+)/([a-zA-Z0-9_]+)$`) +) + // parseApkDB parses packages from a given APK installed DB file. For more // information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec. // -//nolint:funlen -func parseApkDB(_ source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { +//nolint:funlen,gocognit +func parseApkDB(resolver source.FileResolver, env *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { scanner := bufio.NewScanner(reader) var apks []pkg.ApkMetadata @@ -101,6 +107,19 @@ func parseApkDB(_ source.FileResolver, env *generic.Environment, reader source.L if env != nil { r = env.LinuxRelease } + // this is somewhat ugly, but better than completely failing when we can't find the release, + // e.g. embedded deeper in the tree, like containers or chroots. + // but we now have no way of handling different repository sources. On the other hand, + // we never could before this. At least now, we can handle some. + // This should get fixed with https://gitlab.alpinelinux.org/alpine/apk-tools/-/issues/10875 + if r == nil { + // find the repositories file from the relative directory of the DB file + releases := findReleases(resolver, reader.Location.RealPath) + + if len(releases) > 0 { + r = &releases[0] + } + } pkgs := make([]pkg.Package, 0, len(apks)) for _, apk := range apks { @@ -110,6 +129,58 @@ func parseApkDB(_ source.FileResolver, env *generic.Environment, reader source.L return pkgs, discoverPackageDependencies(pkgs), nil } +func findReleases(resolver source.FileResolver, dbPath string) []linux.Release { + if resolver == nil { + return nil + } + + reposLocation := path.Clean(path.Join(path.Dir(dbPath), "../../../etc/apk/repositories")) + locations, err := resolver.FilesByPath(reposLocation) + if err != nil { + log.Tracef("unable to find APK repositories file %q: %+v", reposLocation, err) + return nil + } + + if len(locations) == 0 { + return nil + } + location := locations[0] + + reposReader, err := resolver.FileContentsByLocation(location) + if err != nil { + log.Tracef("unable to fetch contents for APK repositories file %q: %+v", reposLocation, err) + return nil + } + + return parseReleasesFromAPKRepository(source.LocationReadCloser{ + Location: location, + ReadCloser: reposReader, + }) +} + +func parseReleasesFromAPKRepository(reader source.LocationReadCloser) []linux.Release { + var releases []linux.Release + + reposB, err := io.ReadAll(reader) + if err != nil { + log.Tracef("unable to read APK repositories file %q: %+v", reader.Location.RealPath, err) + return nil + } + + parts := repoRegex.FindAllStringSubmatch(string(reposB), -1) + for _, part := range parts { + if len(part) >= 3 { + releases = append(releases, linux.Release{ + Name: "Alpine Linux", + ID: "alpine", + VersionID: part[1], + }) + } + } + + return releases +} + func parseApkField(line string) *apkField { parts := strings.SplitN(line, ":", 2) if len(parts) != 2 { diff --git a/syft/pkg/cataloger/apkdb/parse_apk_db_test.go b/syft/pkg/cataloger/apkdb/parse_apk_db_test.go index 707c2d46ed8..60f40c98a58 100644 --- a/syft/pkg/cataloger/apkdb/parse_apk_db_test.go +++ b/syft/pkg/cataloger/apkdb/parse_apk_db_test.go @@ -1,8 +1,10 @@ package apkdb import ( + "io" "os" "path/filepath" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -1186,3 +1188,49 @@ func Test_stripVersionSpecifier(t *testing.T) { }) } } + +func TestParseReleasesFromAPKRepository(t *testing.T) { + tests := []struct { + repos string + want []linux.Release + desc string + }{ + { + "https://foo.alpinelinux.org/alpine/v3.14/main", + []linux.Release{ + {Name: "Alpine Linux", ID: "alpine", VersionID: "3.14"}, + }, + "single repo", + }, + { + `https://foo.alpinelinux.org/alpine/v3.14/main +https://foo.alpinelinux.org/alpine/v3.14/community`, + []linux.Release{ + {Name: "Alpine Linux", ID: "alpine", VersionID: "3.14"}, + {Name: "Alpine Linux", ID: "alpine", VersionID: "3.14"}, + }, + "multiple repos", + }, + { + ``, + nil, + "empty", + }, + { + `https://foo.bar.org/alpine/v3.14/main +https://foo.them.org/alpine/v3.14/community`, + nil, + "invalid repos", + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + reposReader := io.NopCloser(strings.NewReader(tt.repos)) + got := parseReleasesFromAPKRepository(source.LocationReadCloser{ + Location: source.NewLocation("test"), + ReadCloser: reposReader, + }) + assert.Equal(t, tt.want, got) + }) + } +}