mirror of
https://github.com/anchore/syft.git
synced 2025-11-18 17:03:17 +01:00
add conffile listing to dpkg metadata + normalize digests
Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
parent
7a10cbae0c
commit
269832ce8d
@ -8,14 +8,17 @@ import (
|
||||
"io"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
const (
|
||||
md5sumsExt = ".md5sums"
|
||||
docsPath = "/usr/share/doc"
|
||||
md5sumsExt = ".md5sums"
|
||||
conffilesExt = ".conffiles"
|
||||
docsPath = "/usr/share/doc"
|
||||
)
|
||||
|
||||
type Cataloger struct{}
|
||||
@ -56,44 +59,13 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error)
|
||||
p.FoundBy = c.Name()
|
||||
p.Locations = []source.Location{dbLocation}
|
||||
|
||||
metadata := p.Metadata.(pkg.DpkgMetadata)
|
||||
// the current entry only has what may have been listed in the status file, however, there are additional
|
||||
// files that are listed in multiple other locations. We should retrieve them all and merge the file lists
|
||||
// together.
|
||||
mergeFileListing(resolver, dbLocation, p)
|
||||
|
||||
md5Reader, md5Location, err := fetchMd5Contents(resolver, dbLocation, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find dpkg md5 contents: %w", err)
|
||||
}
|
||||
|
||||
if md5Reader != nil {
|
||||
// attach the file list
|
||||
metadata.Files = parseDpkgMD5Info(md5Reader)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if md5Location != nil {
|
||||
p.Locations = append(p.Locations, *md5Location)
|
||||
}
|
||||
} else {
|
||||
// ensure the file list is an empty collection (not nil)
|
||||
metadata.Files = make([]pkg.DpkgFileRecord, 0)
|
||||
}
|
||||
|
||||
// persist alterations
|
||||
p.Metadata = metadata
|
||||
|
||||
// get license information from the copyright file
|
||||
copyrightReader, copyrightLocation, err := fetchCopyrightContents(resolver, dbLocation, p)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to find dpkg copyright contents: %w", err)
|
||||
}
|
||||
|
||||
if copyrightReader != nil {
|
||||
// attach the licenses
|
||||
p.Licenses = parseLicensesFromCopyright(copyrightReader)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if copyrightLocation != nil {
|
||||
p.Locations = append(p.Locations, *copyrightLocation)
|
||||
}
|
||||
}
|
||||
// fetch additional data from the copyright file to derive the license information
|
||||
addLicenses(resolver, dbLocation, p)
|
||||
}
|
||||
|
||||
results = append(results, pkgs...)
|
||||
@ -101,48 +73,152 @@ func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, error)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func fetchMd5Contents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.Reader, *source.Location, error) {
|
||||
func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
|
||||
// get license information from the copyright file
|
||||
copyrightReader, copyrightLocation := fetchCopyrightContents(resolver, dbLocation, p)
|
||||
|
||||
if copyrightReader != nil {
|
||||
// attach the licenses
|
||||
p.Licenses = parseLicensesFromCopyright(copyrightReader)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if copyrightLocation != nil {
|
||||
p.Locations = append(p.Locations, *copyrightLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mergeFileListing(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
|
||||
metadata := p.Metadata.(pkg.DpkgMetadata)
|
||||
|
||||
// get file listing (package files + additional config files)
|
||||
files, infoLocations := getAdditionalFileListing(resolver, dbLocation, p)
|
||||
loopNewFiles:
|
||||
for _, newFile := range files {
|
||||
for _, existingFile := range metadata.Files {
|
||||
if existingFile.Path == newFile.Path {
|
||||
// skip adding this file since it already exists
|
||||
continue loopNewFiles
|
||||
}
|
||||
}
|
||||
metadata.Files = append(metadata.Files, newFile)
|
||||
}
|
||||
|
||||
// sort files by path
|
||||
sort.SliceStable(metadata.Files, func(i, j int) bool {
|
||||
return metadata.Files[i].Path < metadata.Files[j].Path
|
||||
})
|
||||
|
||||
// persist alterations
|
||||
p.Metadata = metadata
|
||||
|
||||
// persist location information from each new source of information
|
||||
p.Locations = append(p.Locations, infoLocations...)
|
||||
}
|
||||
|
||||
func getAdditionalFileListing(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) ([]pkg.DpkgFileRecord, []source.Location) {
|
||||
// ensure the file list is an empty collection (not nil)
|
||||
var files = make([]pkg.DpkgFileRecord, 0)
|
||||
var locations []source.Location
|
||||
|
||||
md5Reader, md5Location := fetchMd5Contents(resolver, dbLocation, p)
|
||||
|
||||
if md5Reader != nil {
|
||||
// attach the file list
|
||||
files = append(files, parseDpkgMD5Info(md5Reader)...)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if md5Location != nil {
|
||||
locations = append(locations, *md5Location)
|
||||
}
|
||||
}
|
||||
|
||||
conffilesReader, conffilesLocation := fetchConffileContents(resolver, dbLocation, p)
|
||||
|
||||
if conffilesReader != nil {
|
||||
// attach the file list
|
||||
files = append(files, parseDpkgConffileInfo(md5Reader)...)
|
||||
|
||||
// keep a record of the file where this was discovered
|
||||
if md5Location != nil {
|
||||
locations = append(locations, *conffilesLocation)
|
||||
}
|
||||
}
|
||||
|
||||
return files, locations
|
||||
}
|
||||
|
||||
func fetchMd5Contents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.ReadCloser, *source.Location) {
|
||||
var md5Reader io.ReadCloser
|
||||
var err error
|
||||
|
||||
parentPath := filepath.Dir(dbLocation.RealPath)
|
||||
|
||||
// look for /var/lib/dpkg/info/NAME:ARCH.md5sums
|
||||
name := md5Key(p)
|
||||
md5SumLocation := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+md5sumsExt))
|
||||
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+md5sumsExt))
|
||||
|
||||
if md5SumLocation == nil {
|
||||
if location == nil {
|
||||
// the most specific key did not work, fallback to just the name
|
||||
// look for /var/lib/dpkg/info/NAME.md5sums
|
||||
md5SumLocation = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", p.Name+md5sumsExt))
|
||||
location = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", p.Name+md5sumsExt))
|
||||
}
|
||||
|
||||
// this is unexpected, but not a show-stopper
|
||||
if md5SumLocation == nil {
|
||||
return nil, nil, nil
|
||||
if location != nil {
|
||||
md5Reader, err = resolver.FileContentsByLocation(*location)
|
||||
if err != nil {
|
||||
log.Warnf("failed to fetch deb md5 contents (package=%s): %+v", p.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
reader, err := resolver.FileContentsByLocation(*md5SumLocation)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to fetch deb md5 contents (%+v): %w", p, err)
|
||||
}
|
||||
return reader, md5SumLocation, nil
|
||||
return md5Reader, location
|
||||
}
|
||||
|
||||
func fetchCopyrightContents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.Reader, *source.Location, error) {
|
||||
func fetchConffileContents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.ReadCloser, *source.Location) {
|
||||
var reader io.ReadCloser
|
||||
var err error
|
||||
|
||||
parentPath := filepath.Dir(dbLocation.RealPath)
|
||||
|
||||
// look for /var/lib/dpkg/info/NAME:ARCH.conffiles
|
||||
name := md5Key(p)
|
||||
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+conffilesExt))
|
||||
|
||||
if location == nil {
|
||||
// the most specific key did not work, fallback to just the name
|
||||
// look for /var/lib/dpkg/info/NAME.conffiles
|
||||
location = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", p.Name+conffilesExt))
|
||||
}
|
||||
|
||||
// this is unexpected, but not a show-stopper
|
||||
if location != nil {
|
||||
reader, err = resolver.FileContentsByLocation(*location)
|
||||
if err != nil {
|
||||
log.Warnf("failed to fetch deb conffiles contents (package=%s): %+v", p.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return reader, location
|
||||
}
|
||||
|
||||
func fetchCopyrightContents(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) (io.ReadCloser, *source.Location) {
|
||||
// look for /usr/share/docs/NAME/copyright files
|
||||
name := p.Name
|
||||
copyrightPath := path.Join(docsPath, name, "copyright")
|
||||
copyrightLocation := resolver.RelativeFileByPath(dbLocation, copyrightPath)
|
||||
location := resolver.RelativeFileByPath(dbLocation, copyrightPath)
|
||||
|
||||
// we may not have a copyright file for each package, ignore missing files
|
||||
if copyrightLocation == nil {
|
||||
return nil, nil, nil
|
||||
if location == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
reader, err := resolver.FileContentsByLocation(*copyrightLocation)
|
||||
reader, err := resolver.FileContentsByLocation(*location)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to fetch deb copyright contents (%+v): %w", p, err)
|
||||
log.Warnf("failed to fetch deb copyright contents (package=%s): %w", p.Name, err)
|
||||
}
|
||||
|
||||
return reader, copyrightLocation, nil
|
||||
return reader, location
|
||||
}
|
||||
|
||||
func md5Key(p *pkg.Package) string {
|
||||
|
||||
@ -3,6 +3,8 @@ package deb
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
@ -19,7 +21,12 @@ func TestDpkgCataloger(t *testing.T) {
|
||||
{
|
||||
name: "go-case",
|
||||
sources: map[string][]string{
|
||||
"libpam-runtime": {"/var/lib/dpkg/status", "/var/lib/dpkg/info/libpam-runtime.md5sums", "/usr/share/doc/libpam-runtime/copyright"},
|
||||
"libpam-runtime": {
|
||||
"/var/lib/dpkg/status",
|
||||
"/var/lib/dpkg/info/libpam-runtime.md5sums",
|
||||
"/var/lib/dpkg/info/libpam-runtime.conffiles",
|
||||
"/usr/share/doc/libpam-runtime/copyright",
|
||||
},
|
||||
},
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
@ -37,10 +44,38 @@ func TestDpkgCataloger(t *testing.T) {
|
||||
Maintainer: "Steve Langasek <vorlon@debian.org>",
|
||||
InstalledSize: 1016,
|
||||
Files: []pkg.DpkgFileRecord{
|
||||
{Path: "/lib/x86_64-linux-gnu/libz.so.1.2.11", MD5: "55f905631797551d4d936a34c7e73474"},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.Debian.gz", MD5: "cede84bda30d2380217f97753c8ccf3a"},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.gz", MD5: "f3c9dafa6da7992c47328b4464f6d122"},
|
||||
{Path: "/usr/share/doc/zlib1g/copyright", MD5: "a4fae96070439a5209a62ae5b8017ab2"},
|
||||
{
|
||||
Path: "/etc/pam.conf",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "87fc76f18e98ee7d3848f6b81b3391e5",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
{
|
||||
Path: "/etc/pam.d/other",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "31aa7f2181889ffb00b87df4126d1701",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
{Path: "/lib/x86_64-linux-gnu/libz.so.1.2.11", Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "55f905631797551d4d936a34c7e73474",
|
||||
}},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.Debian.gz", Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "cede84bda30d2380217f97753c8ccf3a",
|
||||
}},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.gz", Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "f3c9dafa6da7992c47328b4464f6d122",
|
||||
}},
|
||||
{Path: "/usr/share/doc/zlib1g/copyright", Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "a4fae96070439a5209a62ae5b8017ab2",
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -5,12 +5,11 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func parseDpkgMD5Info(reader io.Reader) []pkg.DpkgFileRecord {
|
||||
// we must preallocate to ensure the resulting struct does not have null
|
||||
var findings = make([]pkg.DpkgFileRecord, 0)
|
||||
func parseDpkgMD5Info(reader io.Reader) (findings []pkg.DpkgFileRecord) {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
@ -23,9 +22,53 @@ func parseDpkgMD5Info(reader io.Reader) []pkg.DpkgFileRecord {
|
||||
}
|
||||
findings = append(findings, pkg.DpkgFileRecord{
|
||||
Path: path,
|
||||
MD5: strings.TrimSpace(fields[0]),
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: strings.TrimSpace(fields[0]),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
func parseDpkgConffileInfo(reader io.Reader) (findings []pkg.DpkgFileRecord) {
|
||||
scanner := bufio.NewScanner(reader)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.Trim(scanner.Text(), " \n")
|
||||
fields := strings.SplitN(line, " ", 2)
|
||||
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var path string
|
||||
if len(fields) >= 1 {
|
||||
path = strings.TrimSpace(fields[0])
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
}
|
||||
|
||||
var digest *file.Digest
|
||||
if len(fields) >= 2 {
|
||||
digest = &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: strings.TrimSpace(fields[1]),
|
||||
}
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
record := pkg.DpkgFileRecord{
|
||||
Path: path,
|
||||
IsConfigFile: true,
|
||||
}
|
||||
if digest != nil {
|
||||
record.Digest = digest
|
||||
}
|
||||
findings = append(findings, record)
|
||||
}
|
||||
}
|
||||
return findings
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@ -17,10 +19,22 @@ func TestMD5SumInfoParsing(t *testing.T) {
|
||||
{
|
||||
fixture: "test-fixtures/info/zlib1g.md5sums",
|
||||
expected: []pkg.DpkgFileRecord{
|
||||
{Path: "/lib/x86_64-linux-gnu/libz.so.1.2.11", MD5: "55f905631797551d4d936a34c7e73474"},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.Debian.gz", MD5: "cede84bda30d2380217f97753c8ccf3a"},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.gz", MD5: "f3c9dafa6da7992c47328b4464f6d122"},
|
||||
{Path: "/usr/share/doc/zlib1g/copyright", MD5: "a4fae96070439a5209a62ae5b8017ab2"},
|
||||
{Path: "/lib/x86_64-linux-gnu/libz.so.1.2.11", Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "55f905631797551d4d936a34c7e73474",
|
||||
}},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.Debian.gz", Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "cede84bda30d2380217f97753c8ccf3a",
|
||||
}},
|
||||
{Path: "/usr/share/doc/zlib1g/changelog.gz", Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "f3c9dafa6da7992c47328b4464f6d122",
|
||||
}},
|
||||
{Path: "/usr/share/doc/zlib1g/copyright", Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "a4fae96070439a5209a62ae5b8017ab2",
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -55,3 +69,52 @@ func TestMD5SumInfoParsing(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConffileInfoParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
expected []pkg.DpkgFileRecord
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/info/util-linux.conffiles",
|
||||
expected: []pkg.DpkgFileRecord{
|
||||
{Path: "/etc/default/hwclock", IsConfigFile: true},
|
||||
{Path: "/etc/init.d/hwclock.sh", IsConfigFile: true},
|
||||
{Path: "/etc/pam.d/runuser", IsConfigFile: true},
|
||||
{Path: "/etc/pam.d/runuser-l", IsConfigFile: true},
|
||||
{Path: "/etc/pam.d/su", IsConfigFile: true},
|
||||
{Path: "/etc/pam.d/su-l", IsConfigFile: true},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}()
|
||||
|
||||
actual := parseDpkgConffileInfo(file)
|
||||
|
||||
if len(actual) != len(test.expected) {
|
||||
for _, a := range actual {
|
||||
t.Logf(" %+v", a)
|
||||
}
|
||||
t.Fatalf("unexpected package count: %d!=%d", len(actual), len(test.expected))
|
||||
}
|
||||
|
||||
diffs := deep.Equal(actual, test.expected)
|
||||
for _, d := range diffs {
|
||||
t.Errorf("diff: %+v", d)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,10 @@ import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
var errEndOfPackages = fmt.Errorf("no more packages to read")
|
||||
var (
|
||||
errEndOfPackages = fmt.Errorf("no more packages to read")
|
||||
sourceRegexp = regexp.MustCompile(`(?P<name>\S+)( \((?P<version>.*)\))?`)
|
||||
)
|
||||
|
||||
// parseDpkgStatus is a parser function for Debian DB status contents, returning all Debian packages listed.
|
||||
func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
|
||||
@ -48,20 +51,49 @@ func parseDpkgStatus(reader io.Reader) ([]pkg.Package, error) {
|
||||
}
|
||||
|
||||
// parseDpkgStatusEntry returns an individual Dpkg entry, or returns errEndOfPackages if there are no more packages to parse from the reader.
|
||||
// nolint:funlen
|
||||
func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err error) {
|
||||
dpkgFields := make(map[string]interface{})
|
||||
func parseDpkgStatusEntry(reader *bufio.Reader) (pkg.DpkgMetadata, error) {
|
||||
var retErr error
|
||||
dpkgFields, err := extractAllFields(reader)
|
||||
if err != nil {
|
||||
if !errors.Is(err, errEndOfPackages) {
|
||||
return pkg.DpkgMetadata{}, err
|
||||
}
|
||||
retErr = err
|
||||
}
|
||||
|
||||
var entry pkg.DpkgMetadata
|
||||
err = mapstructure.Decode(dpkgFields, &entry)
|
||||
if err != nil {
|
||||
return pkg.DpkgMetadata{}, err
|
||||
}
|
||||
|
||||
name, version := extractSourceVersion(entry.Source)
|
||||
if version != "" {
|
||||
entry.SourceVersion = version
|
||||
entry.Source = name
|
||||
}
|
||||
|
||||
// there may be an optional conffiles section that we should persist as files
|
||||
if conffilesSection, exists := dpkgFields["Conffiles"]; exists && conffilesSection != nil {
|
||||
if sectionStr, ok := conffilesSection.(string); ok {
|
||||
entry.Files = parseDpkgConffileInfo(strings.NewReader(sectionStr))
|
||||
}
|
||||
}
|
||||
|
||||
return entry, retErr
|
||||
}
|
||||
|
||||
func extractAllFields(reader *bufio.Reader) (map[string]interface{}, error) {
|
||||
dpkgFields := make(map[string]interface{})
|
||||
var key string
|
||||
|
||||
for {
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
retErr = errEndOfPackages
|
||||
break
|
||||
return dpkgFields, errEndOfPackages
|
||||
}
|
||||
return pkg.DpkgMetadata{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
line = strings.TrimRight(line, "\n")
|
||||
@ -79,12 +111,12 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err err
|
||||
case strings.HasPrefix(line, " "):
|
||||
// a field-body continuation
|
||||
if len(key) == 0 {
|
||||
return pkg.DpkgMetadata{}, fmt.Errorf("no match for continuation: line: '%s'", line)
|
||||
return nil, fmt.Errorf("no match for continuation: line: '%s'", line)
|
||||
}
|
||||
|
||||
val, ok := dpkgFields[key]
|
||||
if !ok {
|
||||
return pkg.DpkgMetadata{}, fmt.Errorf("no previous key exists, expecting: %s", key)
|
||||
return nil, fmt.Errorf("no previous key exists, expecting: %s", key)
|
||||
}
|
||||
// concatenate onto previous value
|
||||
val = fmt.Sprintf("%s\n %s", val, strings.TrimSpace(line))
|
||||
@ -94,36 +126,18 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (entry pkg.DpkgMetadata, err err
|
||||
var val interface{}
|
||||
key, val, err = handleNewKeyValue(line)
|
||||
if err != nil {
|
||||
return pkg.DpkgMetadata{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := dpkgFields[key]; ok {
|
||||
return pkg.DpkgMetadata{}, fmt.Errorf("duplicate key discovered: %s", key)
|
||||
return nil, fmt.Errorf("duplicate key discovered: %s", key)
|
||||
}
|
||||
dpkgFields[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
err = mapstructure.Decode(dpkgFields, &entry)
|
||||
if err != nil {
|
||||
return pkg.DpkgMetadata{}, err
|
||||
}
|
||||
|
||||
name, version := extractSourceVersion(entry.Source)
|
||||
if version != "" {
|
||||
entry.SourceVersion = version
|
||||
entry.Source = name
|
||||
}
|
||||
|
||||
return entry, retErr
|
||||
return dpkgFields, nil
|
||||
}
|
||||
|
||||
// match examples:
|
||||
// "a-thing (1.2.3)" name="a-thing" version="1.2.3"
|
||||
// "a-thing" name="a-thing" version=""
|
||||
// "" name="" version=""
|
||||
var sourceRegexp = regexp.MustCompile(`(?P<name>\S+)( \((?P<version>.*)\))?`)
|
||||
|
||||
// If the source entry string is of the form "<name> (<version>)" then parse and return the components, if
|
||||
// of the "<name>" form, then return name and nil
|
||||
func extractSourceVersion(source string) (string, string) {
|
||||
|
||||
@ -5,6 +5,8 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/go-test/deep"
|
||||
)
|
||||
@ -30,6 +32,40 @@ func TestSinglePackage(t *testing.T) {
|
||||
Architecture: "amd64",
|
||||
InstalledSize: 4064,
|
||||
Maintainer: "APT Development Team <deity@lists.debian.org>",
|
||||
Files: []pkg.DpkgFileRecord{
|
||||
{
|
||||
Path: "/etc/apt/apt.conf.d/01autoremove",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "76120d358bc9037bb6358e737b3050b5",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
{
|
||||
Path: "/etc/cron.daily/apt-compat",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "49e9b2cfa17849700d4db735d04244f3",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
{
|
||||
Path: "/etc/kernel/postinst.d/apt-auto-removal",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "4ad976a68f045517cf4696cec7b8aa3a",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
{
|
||||
Path: "/etc/logrotate.d/apt",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "179f2ed4f85cbaca12fa3d69c2a4a1c3",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -81,6 +117,56 @@ func TestMultiplePackages(t *testing.T) {
|
||||
Architecture: "amd64",
|
||||
InstalledSize: 4327,
|
||||
Maintainer: "LaMont Jones <lamont@debian.org>",
|
||||
Files: []pkg.DpkgFileRecord{
|
||||
{
|
||||
Path: "/etc/default/hwclock",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "3916544450533eca69131f894db0ca12",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
{
|
||||
Path: "/etc/init.d/hwclock.sh",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "1ca5c0743fa797ffa364db95bb8d8d8e",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
{
|
||||
Path: "/etc/pam.d/runuser",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "b8b44b045259525e0fae9e38fdb2aeeb",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
{
|
||||
Path: "/etc/pam.d/runuser-l",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "2106ea05877e8913f34b2c77fa02be45",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
{
|
||||
Path: "/etc/pam.d/su",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "ce6dcfda3b190a27a455bb38a45ff34a",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
{
|
||||
Path: "/etc/pam.d/su-l",
|
||||
Digest: &file.Digest{
|
||||
Algorithm: "md5",
|
||||
Value: "756fef5687fecc0d986e5951427b0c4f",
|
||||
},
|
||||
IsConfigFile: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
/etc/pam.conf
|
||||
/etc/pam.d/other
|
||||
@ -0,0 +1,6 @@
|
||||
/etc/default/hwclock
|
||||
/etc/init.d/hwclock.sh
|
||||
/etc/pam.d/runuser
|
||||
/etc/pam.d/runuser-l
|
||||
/etc/pam.d/su
|
||||
/etc/pam.d/su-l
|
||||
@ -3,6 +3,8 @@ package pkg
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/package-url/packageurl-go"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
@ -27,8 +29,9 @@ type DpkgMetadata struct {
|
||||
|
||||
// DpkgFileRecord represents a single file attributed to a debian package.
|
||||
type DpkgFileRecord struct {
|
||||
Path string `json:"path"`
|
||||
MD5 string `json:"md5"`
|
||||
Path string `json:"path"`
|
||||
Digest *file.Digest `json:"digest,omitempty"`
|
||||
IsConfigFile bool `json:"isConfigFile"`
|
||||
}
|
||||
|
||||
// PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user