Add portage support for Gentoo Linux (#1076)

Co-authored-by: Christopher Phillips <christopher.phillips@anchore.com>
This commit is contained in:
Zac Medico 2022-07-06 13:18:54 -07:00 committed by GitHub
parent ba685eada8
commit 4c55c62834
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1836 additions and 23 deletions

View File

@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON encoder
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "3.3.0"
JSONSchemaVersion = "3.3.1"
)

View File

@ -37,6 +37,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from PHP composer manifest"
case pkg.ConanPkg:
answer = "acquired package info from conan manifest"
case pkg.PortagePkg:
answer = "acquired package info from portage DB"
default:
answer = "acquired package info from the following paths"
}

View File

@ -158,6 +158,14 @@ func Test_SourceInfo(t *testing.T) {
"from conan manifest",
},
},
{
input: pkg.Package{
Type: pkg.PortagePkg,
},
expected: []string{
"from portage DB",
},
},
}
var pkgTypes []pkg.Type
for _, test := range tests {

View File

@ -157,6 +157,12 @@ func unpackMetadata(p *Package, unpacker packageMetadataUnpacker) error {
return err
}
p.Metadata = payload
case pkg.PortageMetadataType:
var payload pkg.PortageMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
default:
log.Warnf("unknown package metadata type=%q for packageID=%q", p.MetadataType, p.ID)
}

View File

@ -88,7 +88,7 @@
}
},
"schema": {
"version": "3.3.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.0.json"
"version": "3.3.1",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.1.json"
}
}

View File

@ -184,7 +184,7 @@
}
},
"schema": {
"version": "3.3.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.0.json"
"version": "3.3.1",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.1.json"
}
}

View File

@ -111,7 +111,7 @@
}
},
"schema": {
"version": "3.3.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.0.json"
"version": "3.3.1",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.1.json"
}
}

View File

@ -25,7 +25,7 @@ Given a version number format `MODEL.REVISION.ADDITION`:
When adding a new `pkg.*Metadata` that is assigned to the `pkg.Package.Metadata` struct field it is important that a few things
are done:
- a new integration test case is added to `test/integration/pkg_cases_test.go` that exercises the new package type with the new metadata
- a new integration test case is added to `test/integration/catalog_packages_cases_test.go` that exercises the new package type with the new metadata
- the new metadata struct is added to the `artifactMetadataContainer` struct within `schema/json/generate.go`
## Generating a New Schema

View File

@ -40,6 +40,7 @@ type artifactMetadataContainer struct {
Php pkg.PhpComposerJSONMetadata
Dart pkg.DartPubMetadata
Dotnet pkg.DotnetDepsMetadata
Portage pkg.PortageMetadata
}
func main() {

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/java"
"github.com/anchore/syft/syft/pkg/cataloger/javascript"
"github.com/anchore/syft/syft/pkg/cataloger/php"
"github.com/anchore/syft/syft/pkg/cataloger/portage"
"github.com/anchore/syft/syft/pkg/cataloger/python"
"github.com/anchore/syft/syft/pkg/cataloger/rpmdb"
"github.com/anchore/syft/syft/pkg/cataloger/ruby"
@ -54,6 +55,7 @@ func ImageCatalogers(cfg Config) []Cataloger {
apkdb.NewApkdbCataloger(),
golang.NewGoModuleBinaryCataloger(),
dotnet.NewDotnetDepsCataloger(),
portage.NewPortageCataloger(),
}, cfg.Catalogers)
}
@ -77,6 +79,7 @@ func DirectoryCatalogers(cfg Config) []Cataloger {
dart.NewPubspecLockCataloger(),
dotnet.NewDotnetDepsCataloger(),
cpp.NewConanfileCataloger(),
portage.NewPortageCataloger(),
}, cfg.Catalogers)
}
@ -103,6 +106,7 @@ func AllCatalogers(cfg Config) []Cataloger {
php.NewPHPComposerInstalledCataloger(),
php.NewPHPComposerLockCataloger(),
cpp.NewConanfileCataloger(),
portage.NewPortageCataloger(),
}, cfg.Catalogers)
}

View File

@ -0,0 +1,151 @@
/*
Package portage provides a concrete Cataloger implementation for Gentoo Portage.
*/
package portage
import (
"bufio"
"fmt"
"path"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
var (
cpvRe = regexp.MustCompile(`/([^/]*/[\w+][\w+-]*)-((\d+)((\.\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\d*)*)(-r\d+)?)/CONTENTS$`)
)
type Cataloger struct{}
// NewPortageCataloger returns a new Portage package cataloger object.
func NewPortageCataloger() *Cataloger {
return &Cataloger{}
}
// Name returns a string that uniquely describes a cataloger
func (c *Cataloger) Name() string {
return "portage-cataloger"
}
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing portage support files.
func (c *Cataloger) Catalog(resolver source.FileResolver) ([]pkg.Package, []artifact.Relationship, error) {
dbFileMatches, err := resolver.FilesByGlob(pkg.PortageDBGlob)
if err != nil {
return nil, nil, fmt.Errorf("failed to find portage files by glob: %w", err)
}
var allPackages []pkg.Package
for _, dbLocation := range dbFileMatches {
cpvMatch := cpvRe.FindStringSubmatch(dbLocation.RealPath)
if cpvMatch == nil {
return nil, nil, fmt.Errorf("failed to match package and version in %s", dbLocation.RealPath)
}
entry := pkg.PortageMetadata{
// ensure the default value for a collection is never nil since this may be shown as JSON
Files: make([]pkg.PortageFileRecord, 0),
Package: cpvMatch[1],
Version: cpvMatch[2],
}
err = addFiles(resolver, dbLocation, &entry)
if err != nil {
return nil, nil, err
}
addSize(resolver, dbLocation, &entry)
p := pkg.Package{
Name: entry.Package,
Version: entry.Version,
Type: pkg.PortagePkg,
MetadataType: pkg.PortageMetadataType,
Metadata: entry,
}
addLicenses(resolver, dbLocation, &p)
p.FoundBy = c.Name()
p.Locations.Add(dbLocation)
p.SetID()
allPackages = append(allPackages, p)
}
return allPackages, nil, nil
}
func addFiles(resolver source.FileResolver, dbLocation source.Location, entry *pkg.PortageMetadata) error {
contentsReader, err := resolver.FileContentsByLocation(dbLocation)
if err != nil {
return err
}
scanner := bufio.NewScanner(contentsReader)
for scanner.Scan() {
line := strings.Trim(scanner.Text(), "\n")
fields := strings.Split(line, " ")
if fields[0] == "obj" {
record := pkg.PortageFileRecord{
Path: fields[1],
}
record.Digest = &file.Digest{
Algorithm: "md5",
Value: fields[2],
}
entry.Files = append(entry.Files, record)
}
}
return nil
}
func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
parentPath := filepath.Dir(dbLocation.RealPath)
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "LICENSE"))
if location != nil {
licenseReader, err := resolver.FileContentsByLocation(*location)
if err == nil {
findings := internal.NewStringSet()
scanner := bufio.NewScanner(licenseReader)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
token := scanner.Text()
if token != "||" && token != "(" && token != ")" {
findings.Add(token)
}
}
p.Licenses = findings.ToSlice()
sort.Strings(p.Licenses)
}
}
}
func addSize(resolver source.FileResolver, dbLocation source.Location, entry *pkg.PortageMetadata) {
parentPath := filepath.Dir(dbLocation.RealPath)
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "SIZE"))
if location != nil {
sizeReader, err := resolver.FileContentsByLocation(*location)
if err != nil {
log.Warnf("failed to fetch portage SIZE (package=%s): %+v", entry.Package, err)
} else {
scanner := bufio.NewScanner(sizeReader)
for scanner.Scan() {
line := strings.Trim(scanner.Text(), "\n")
size, err := strconv.Atoi(line)
if err == nil {
entry.InstalledSize = size
}
}
}
}
}

View File

@ -0,0 +1,106 @@
package portage
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"
"github.com/go-test/deep"
)
func TestPortageCataloger(t *testing.T) {
tests := []struct {
name string
expected []pkg.Package
}{
{
name: "go-case",
expected: []pkg.Package{
{
Name: "app-containers/skopeo",
Version: "1.5.1",
FoundBy: "portage-cataloger",
Licenses: []string{"Apache-2.0", "BSD", "BSD-2", "CC-BY-SA-4.0", "ISC", "MIT"},
Type: pkg.PortagePkg,
MetadataType: pkg.PortageMetadataType,
Metadata: pkg.PortageMetadata{
Package: "app-containers/skopeo",
Version: "1.5.1",
InstalledSize: 27937835,
Files: []pkg.PortageFileRecord{
{
Path: "/usr/bin/skopeo",
Digest: &file.Digest{
Algorithm: "md5",
Value: "376c02bd3b22804df8fdfdc895e7dbfb",
},
},
{
Path: "/etc/containers/policy.json",
Digest: &file.Digest{
Algorithm: "md5",
Value: "c01eb6950f03419e09d4fc88cb42ff6f",
},
},
{
Path: "/etc/containers/registries.d/default.yaml",
Digest: &file.Digest{
Algorithm: "md5",
Value: "e6e66cd3c24623e0667f26542e0e08f6",
},
},
{
Path: "/var/lib/atomic/sigstore/.keep_app-containers_skopeo-0",
Digest: &file.Digest{
Algorithm: "md5",
Value: "d41d8cd98f00b204e9800998ecf8427e",
},
},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
img := imagetest.GetFixtureImage(t, "docker-archive", "image-portage")
s, err := source.NewFromImage(img, "")
if err != nil {
t.Fatal(err)
}
c := NewPortageCataloger()
resolver, err := s.FileResolver(source.SquashedScope)
if err != nil {
t.Errorf("could not get resolver error: %+v", err)
}
actual, _, err := c.Catalog(resolver)
if err != nil {
t.Fatalf("failed to catalog: %+v", err)
}
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))
}
// test remaining fields...
for _, d := range deep.Equal(actual, test.expected) {
t.Errorf("diff: %+v", d)
}
})
}
}

View File

@ -0,0 +1,2 @@
FROM scratch
COPY . .

View File

@ -0,0 +1,13 @@
dir /usr
dir /usr/bin
obj /usr/bin/skopeo 376c02bd3b22804df8fdfdc895e7dbfb 1649284374
dir /etc
dir /etc/containers
obj /etc/containers/policy.json c01eb6950f03419e09d4fc88cb42ff6f 1649284375
dir /etc/containers/registries.d
obj /etc/containers/registries.d/default.yaml e6e66cd3c24623e0667f26542e0e08f6 1649284375
dir /var
dir /var/lib
dir /var/lib/atomic
dir /var/lib/atomic/sigstore
obj /var/lib/atomic/sigstore/.keep_app-containers_skopeo-0 d41d8cd98f00b204e9800998ecf8427e 1649284375

View File

@ -0,0 +1 @@
Apache-2.0 BSD BSD-2 CC-BY-SA-4.0 ISC MIT

View File

@ -26,6 +26,7 @@ const (
GolangBinMetadataType MetadataType = "GolangBinMetadata"
PhpComposerJSONMetadataType MetadataType = "PhpComposerJsonMetadata"
ConanaMetadataType MetadataType = "ConanaMetadataType"
PortageMetadataType MetadataType = "PortageMetadata"
)
var AllMetadataTypes = []MetadataType{
@ -44,6 +45,7 @@ var AllMetadataTypes = []MetadataType{
GolangBinMetadataType,
PhpComposerJSONMetadataType,
ConanaMetadataType,
PortageMetadataType,
}
var MetadataTypeByName = map[MetadataType]reflect.Type{
@ -62,4 +64,5 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{
GolangBinMetadataType: reflect.TypeOf(GolangBinMetadata{}),
PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}),
ConanaMetadataType: reflect.TypeOf(ConanMetadata{}),
PortageMetadataType: reflect.TypeOf(PortageMetadata{}),
}

View File

@ -0,0 +1,21 @@
package pkg
import (
"github.com/anchore/syft/syft/file"
)
const PortageDBGlob = "**/var/db/pkg/*/*/CONTENTS"
// PortageMetadata represents all captured data for a Package package DB entry.
type PortageMetadata struct {
Package string `mapstructure:"Package" json:"package"`
Version string `mapstructure:"Version" json:"version"`
InstalledSize int `mapstructure:"InstalledSize" json:"installedSize" cyclonedx:"installedSize"`
Files []PortageFileRecord `json:"files"`
}
// PortageFileRecord represents a single file attributed to a portage package.
type PortageFileRecord struct {
Path string `json:"path"`
Digest *file.Digest `json:"digest,omitempty"`
}

View File

@ -24,6 +24,7 @@ const (
DartPubPkg Type = "dart-pub"
DotnetPkg Type = "dotnet"
ConanPkg Type = "conan"
PortagePkg Type = "portage"
)
// AllPkgs represents all supported package types
@ -44,6 +45,7 @@ var AllPkgs = []Type{
DartPubPkg,
DotnetPkg,
ConanPkg,
PortagePkg,
}
// PackageURLType returns the PURL package type for the current package.
@ -77,6 +79,8 @@ func (t Type) PackageURLType() string {
return packageurl.TypeDotnet
case ConanPkg:
return packageurl.TypeConan
case PortagePkg:
return "portage"
default:
// TODO: should this be a "generic" purl type instead?
return ""
@ -122,6 +126,8 @@ func TypeByName(name string) Type {
return DotnetPkg
case packageurl.TypeConan:
return ConanPkg
case "portage":
return PortagePkg
default:
return UnknownPkg
}

View File

@ -83,6 +83,7 @@ func TestTypeFromPURL(t *testing.T) {
// testing microsoft packages and jenkins-plugins is not valid for purl at this time
expectedTypes.Remove(string(KbPkg))
expectedTypes.Remove(string(JenkinsPluginPkg))
expectedTypes.Remove(string(PortagePkg))
for _, test := range tests {
t.Run(string(test.expected), func(t *testing.T) {

View File

@ -233,6 +233,7 @@ func TestPackageURL(t *testing.T) {
// testing microsoft packages is not valid for purl at this time
expectedTypes.Remove(string(KbPkg))
expectedTypes.Remove(string(PortagePkg))
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {

View File

@ -96,7 +96,7 @@ func TestPackagesCmdFlags(t *testing.T) {
name: "squashed-scope-flag",
args: []string{"packages", "-o", "json", "-s", "squashed", coverageImage},
assertions: []traitAssertion{
assertPackageCount(33),
assertPackageCount(34),
assertSuccessfulReturnCode,
},
},

View File

@ -277,6 +277,14 @@ var commonTestCases = []testCase{
"netbase": "5.4",
},
},
{
name: "find portage packages",
pkgType: pkg.PortagePkg,
pkgInfo: map[string]string{
"app-containers/skopeo": "1.5.1",
},
},
{
name: "find jenkins plugins",
pkgType: pkg.JenkinsPluginPkg,

View File

@ -0,0 +1,13 @@
dir /usr
dir /usr/bin
obj /usr/bin/skopeo 376c02bd3b22804df8fdfdc895e7dbfb 1649284374
dir /etc
dir /etc/containers
obj /etc/containers/policy.json c01eb6950f03419e09d4fc88cb42ff6f 1649284375
dir /etc/containers/registries.d
obj /etc/containers/registries.d/default.yaml e6e66cd3c24623e0667f26542e0e08f6 1649284375
dir /var
dir /var/lib
dir /var/lib/atomic
dir /var/lib/atomic/sigstore
obj /var/lib/atomic/sigstore/.keep_app-containers_skopeo-0 d41d8cd98f00b204e9800998ecf8427e 1649284375

View File

@ -0,0 +1 @@
Apache-2.0 BSD BSD-2 CC-BY-SA-4.0 ISC MIT