package alpine import ( "bufio" "context" "fmt" "io" "path" "regexp" "strconv" "strings" "github.com/anchore/syft/internal" "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/unknown" "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/generic" ) // integrity check var _ generic.Parser = parseApkDB var ( repoRegex = regexp.MustCompile(`(?m)^https://.*\.alpinelinux\.org/alpine/v([^/]+)/([a-zA-Z0-9_]+)$`) ) type parsedData struct { License string `mapstructure:"L" json:"license"` pkg.ApkDBEntry } const maxApkDBFieldSize = 10 * 1024 * 1024 // parseApkDB parses packages from a given APK "installed" flat-file DB. For more // information on specific fields, see https://wiki.alpinelinux.org/wiki/Apk_spec. // //nolint:funlen func parseApkDB(ctx context.Context, resolver file.Resolver, env *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) { scanner := bufio.NewScanner(reader) scanner.Buffer(nil, maxApkDBFieldSize) var errs error var apks []parsedData var currentEntry parsedData entryParsingInProgress := false fileParsingCtx := newApkFileParsingContext() // creating a dedicated append-like function here instead of using `append(...)` // below since there is nontrivial logic to be performed for each finalized apk // entry. appendApk := func(p parsedData) { if files := fileParsingCtx.files; len(files) >= 1 { // attached accumulated files to current package p.Files = files // reset file parsing for next use fileParsingCtx = newApkFileParsingContext() } nilFieldsToEmptySlice(&p) apks = append(apks, p) } for scanner.Scan() { line := scanner.Text() if line == "" { // i.e. apk entry separator if entryParsingInProgress { // current entry is complete appendApk(currentEntry) } entryParsingInProgress = false // zero-out currentEntry for use by any future entry currentEntry = parsedData{} continue } field := parseApkField(line) if field == nil { log.Debugf("unable to parse field data from line %q", line) errs = unknown.Appendf(errs, reader, "unable to parse field data from line %q", line) continue } if len(field.name) == 0 { log.Debugf("failed to parse field name from line %q", line) errs = unknown.Appendf(errs, reader, "failed to parse field name from line %q", line) continue } if len(field.value) == 0 { log.Debugf("line %q: parsed field %q appears to have an empty value, skipping", line, field.name) continue } entryParsingInProgress = true field.apply(¤tEntry, fileParsingCtx) } if entryParsingInProgress { // There was no final empty line, so currentEntry hasn't been added to the // collection yet; but we've now reached the end of scanning, so let's be sure to // add currentEntry to the collection. appendApk(currentEntry) } if err := scanner.Err(); err != nil { return nil, nil, fmt.Errorf("failed to parse APK installed DB file: %w", err) } var r *linux.Release 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.LocalPath()) if len(releases) > 0 { r = &releases[0] } } pkgs := make([]pkg.Package, 0, len(apks)) for _, apk := range apks { pkgs = append(pkgs, newPackage(ctx, apk, r, reader.Location)) } return pkgs, nil, errs } func findReleases(resolver file.Resolver, 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", location.FullNestedPath(), err) return nil } defer internal.CloseAndLogError(reposReader, location.FullNestedPath()) return parseReleasesFromAPKRepository(file.LocationReadCloser{ Location: location, ReadCloser: reposReader, }) } func parseReleasesFromAPKRepository(reader file.LocationReadCloser) []linux.Release { var releases []linux.Release reposB, err := io.ReadAll(reader) //nolint:gocritic // regex matching requires full buffer if err != nil { log.Tracef("unable to read APK repositories file %q: %+v", reader.FullNestedPath(), 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 { return nil } f := apkField{ name: parts[0], value: parts[1], } return &f } type apkField struct { name string value string } //nolint:funlen func (f apkField) apply(p *parsedData, ctx *apkFileParsingContext) { switch f.name { // APKINDEX field parsing case "P": p.Package = f.value case "o": p.OriginPackage = f.value case "m": p.Maintainer = f.value case "V": p.Version = f.value case "L": p.License = f.value case "A": p.Architecture = f.value case "U": p.URL = f.value case "T": p.Description = f.value case "S": i, err := strconv.Atoi(f.value) if err != nil { log.Debugf("unable to parse value %q for field %q: %w", f.value, f.name, err) return } p.Size = i case "I": i, err := strconv.Atoi(f.value) if err != nil { log.Debugf("unable to parse value %q for field %q: %w", f.value, f.name, err) return } p.InstalledSize = i case "D": deps := parseListValue(f.value) p.Dependencies = deps case "p": provides := parseListValue(f.value) p.Provides = provides case "C": p.Checksum = f.value case "c": p.GitCommit = f.value // File/directory field parsing: case "F": directory := path.Join("/", f.value) ctx.files = append(ctx.files, pkg.ApkFileRecord{Path: directory}) ctx.indexOfLatestDirectory = len(ctx.files) - 1 case "M": i := ctx.indexOfLatestDirectory if i < 0 { log.Debugf("APK field %q appears before any directory entry", f.name) return } latest := ctx.files[i] var ok bool latest.OwnerUID, latest.OwnerGID, latest.Permissions, ok = processFileInfo(f.value) if !ok { log.Debugf("unexpected value for APK ACL field %q: %q", f.name, f.value) return } // save updated directory ctx.files[i] = latest case "R": var regularFile string dirIndex := ctx.indexOfLatestDirectory if dirIndex < 0 { regularFile = path.Join("/", f.value) } else { latestDirPath := ctx.files[dirIndex].Path regularFile = path.Join(latestDirPath, f.value) } ctx.files = append(ctx.files, pkg.ApkFileRecord{Path: regularFile}) ctx.indexOfLatestRegularFile = len(ctx.files) - 1 case "a": i := ctx.indexOfLatestRegularFile if i < 0 { log.Debugf("APK field %q appears before any file entry", f.name) return } latest := ctx.files[i] var ok bool latest.OwnerUID, latest.OwnerGID, latest.Permissions, ok = processFileInfo(f.value) if !ok { log.Debugf("unexpected value for APK ACL field %q: %q", f.name, f.value) return } // save updated file ctx.files[i] = latest case "Z": i := ctx.indexOfLatestRegularFile if i < 0 { log.Debugf("APK field %q appears before any file entry", f.name) return } latest := ctx.files[i] latest.Digest = processChecksum(f.value) // save updated file ctx.files[i] = latest } } func processFileInfo(v string) (uid, gid, perms string, ok bool) { ok = false fileInfo := strings.Split(v, ":") if len(fileInfo) < 3 { return } uid = fileInfo[0] gid = fileInfo[1] perms = fileInfo[2] // note: there are more optional fields available that we are not capturing, // e.g.: "0:0:755:Q1JaDEHQHBbizhEzoWK1YxuraNU/4=" ok = true return } // apkFileParsingContext helps keep track of what file data has been captured so far for the APK currently being parsed. type apkFileParsingContext struct { files []pkg.ApkFileRecord indexOfLatestDirectory int indexOfLatestRegularFile int } func newApkFileParsingContext() *apkFileParsingContext { return &apkFileParsingContext{ indexOfLatestDirectory: -1, // no directories yet indexOfLatestRegularFile: -1, // no regular files yet } } // parseListValue parses a space-separated list from an apk entry field value. func parseListValue(value string) []string { items := strings.Split(value, " ") if len(items) >= 1 { return items } return nil } func nilFieldsToEmptySlice(p *parsedData) { if p.Dependencies == nil { p.Dependencies = []string{} } if p.Provides == nil { p.Provides = []string{} } if p.Files == nil { p.Files = []pkg.ApkFileRecord{} } } func processChecksum(value string) *file.Digest { // from: https://wiki.alpinelinux.org/wiki/Apk_spec // The package checksum field is the SHA1 hash of the second gzip stream (control stream) in the package. The // binary hash digest is base64 encoded. This is prefixed with Q1 to differentiate it from the MD5 hashes // used in older index formats. It is not possible to compute this checksum with standard command line tools // but the apk-tools can compute it in their index operation. // based on https://github.com/alpinelinux/apk-tools/blob/dd1908f2fc20b4cfe2c15c55fafaa5fadfb599dc/src/blob.c#L379-L393 // it seems that the old md5 checksum value was only the hex representation (not base64) algorithm := "md5" if strings.HasPrefix(value, "Q1") { algorithm = "'Q1'+base64(sha1)" } return &file.Digest{ Algorithm: algorithm, Value: value, } }