Update portage cataloger to new generic cataloger (#1316)

* port portage (ha) cataloger to new generic cataloger pattern

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

* update JSON schema to account for removing portage fields

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-11-03 14:49:18 -04:00 committed by GitHub
parent 891f2c576b
commit 2deb96a801
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1769 additions and 177 deletions

View File

@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON encoder // 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. // 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 = "4.1.0" JSONSchemaVersion = "5.0.0"
) )

File diff suppressed because it is too large Load Diff

View File

@ -4,7 +4,7 @@ import (
"github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/sbom"
) )
const ID sbom.FormatID = "syft-4-json" const ID sbom.FormatID = "syft-5-json"
func Format() sbom.Format { func Format() sbom.Format {
return sbom.NewFormat( return sbom.NewFormat(

View File

@ -89,7 +89,7 @@
} }
}, },
"schema": { "schema": {
"version": "4.1.0", "version": "5.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-4.1.0.json" "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-5.0.0.json"
} }
} }

View File

@ -185,7 +185,7 @@
} }
}, },
"schema": { "schema": {
"version": "4.1.0", "version": "5.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-4.1.0.json" "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-5.0.0.json"
} }
} }

View File

@ -112,7 +112,7 @@
} }
}, },
"schema": { "schema": {
"version": "4.1.0", "version": "5.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-4.1.0.json" "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-5.0.0.json"
} }
} }

View File

@ -18,71 +18,63 @@ import (
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
var ( var (
cpvRe = regexp.MustCompile(`/([^/]*/[\w+][\w+-]*)-((\d+)((\.\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\d*)*)(-r\d+)?)/CONTENTS$`) cpvRe = regexp.MustCompile(`/([^/]*/[\w+][\w+-]*)-((\d+)((\.\d+)*)([a-z]?)((_(pre|p|beta|alpha|rc)\d*)*)(-r\d+)?)/CONTENTS$`)
_ generic.Parser = parsePortageContents
) )
type Cataloger struct{} func NewPortageCataloger() *generic.Cataloger {
return generic.NewCataloger("portage-cataloger").
// NewPortageCataloger returns a new Portage package cataloger object. WithParserByGlobs(parsePortageContents, "**/var/db/pkg/*/*/CONTENTS")
func NewPortageCataloger() *Cataloger {
return &Cataloger{}
} }
// Name returns a string that uniquely describes a cataloger func parsePortageContents(resolver source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
func (c *Cataloger) Name() string { cpvMatch := cpvRe.FindStringSubmatch(reader.Location.RealPath)
return "portage-cataloger" if cpvMatch == nil {
} return nil, nil, fmt.Errorf("failed to match package and version in %s", reader.Location.RealPath)
// 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 { name, version := cpvMatch[1], cpvMatch[2]
cpvMatch := cpvRe.FindStringSubmatch(dbLocation.RealPath) if name == "" || version == "" {
if cpvMatch == nil { log.WithFields("path", reader.Location.RealPath).Warnf("failed to parse portage name and version")
return nil, nil, fmt.Errorf("failed to match package and version in %s", dbLocation.RealPath) return nil, nil, nil
} }
entry := pkg.PortageMetadata{
p := pkg.Package{
Name: name,
Version: version,
PURL: packageURL(name, version),
Locations: source.NewLocationSet(),
Type: pkg.PortagePkg,
MetadataType: pkg.PortageMetadataType,
Metadata: pkg.PortageMetadata{
// ensure the default value for a collection is never nil since this may be shown as JSON // ensure the default value for a collection is never nil since this may be shown as JSON
Files: make([]pkg.PortageFileRecord, 0), 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 addLicenses(resolver, reader.Location, &p)
addSize(resolver, reader.Location, &p)
addFiles(resolver, reader.Location, &p)
p.SetID()
return []pkg.Package{p}, nil, nil
} }
func addFiles(resolver source.FileResolver, dbLocation source.Location, entry *pkg.PortageMetadata) error { func addFiles(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
contentsReader, err := resolver.FileContentsByLocation(dbLocation) contentsReader, err := resolver.FileContentsByLocation(dbLocation)
if err != nil { if err != nil {
return err log.WithFields("path", dbLocation.RealPath).Warnf("failed to fetch portage contents (package=%s): %+v", p.Name, err)
return
}
entry, ok := p.Metadata.(pkg.PortageMetadata)
if !ok {
return
} }
scanner := bufio.NewScanner(contentsReader) scanner := bufio.NewScanner(contentsReader)
@ -101,7 +93,9 @@ func addFiles(resolver source.FileResolver, dbLocation source.Location, entry *p
entry.Files = append(entry.Files, record) entry.Files = append(entry.Files, record)
} }
} }
return nil
p.Metadata = entry
p.Locations.Add(dbLocation)
} }
func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) { func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
@ -109,43 +103,60 @@ func addLicenses(resolver source.FileResolver, dbLocation source.Location, p *pk
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "LICENSE")) location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "LICENSE"))
if location != nil { if location == nil {
licenseReader, err := resolver.FileContentsByLocation(*location) return
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) licenseReader, err := resolver.FileContentsByLocation(*location)
if err != nil {
log.WithFields("path", dbLocation.RealPath).Warnf("failed to fetch portage LICENSE: %+v", err)
return
}
findings := internal.NewStringSet()
scanner := bufio.NewScanner(licenseReader)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
token := scanner.Text()
if token != "||" && token != "(" && token != ")" {
findings.Add(token)
} }
} }
licenses := findings.ToSlice()
sort.Strings(licenses)
p.Licenses = licenses
p.Locations.Add(*location)
} }
func addSize(resolver source.FileResolver, dbLocation source.Location, entry *pkg.PortageMetadata) { func addSize(resolver source.FileResolver, dbLocation source.Location, p *pkg.Package) {
parentPath := filepath.Dir(dbLocation.RealPath) parentPath := filepath.Dir(dbLocation.RealPath)
location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "SIZE")) location := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "SIZE"))
if location != nil { if location == nil {
sizeReader, err := resolver.FileContentsByLocation(*location) return
if err != nil { }
log.Warnf("failed to fetch portage SIZE (package=%s): %+v", entry.Package, err)
} else { entry, ok := p.Metadata.(pkg.PortageMetadata)
scanner := bufio.NewScanner(sizeReader) if !ok {
for scanner.Scan() { return
line := strings.Trim(scanner.Text(), "\n") }
size, err := strconv.Atoi(line)
if err == nil { sizeReader, err := resolver.FileContentsByLocation(*location)
entry.InstalledSize = size if err != nil {
} log.WithFields("name", p.Name).Warnf("failed to fetch portage SIZE: %+v", err)
} return
}
scanner := bufio.NewScanner(sizeReader)
for scanner.Scan() {
line := strings.Trim(scanner.Text(), "\n")
size, err := strconv.Atoi(line)
if err == nil {
entry.InstalledSize = size
} }
} }
p.Metadata = entry
p.Locations.Add(*location)
} }

View File

@ -3,62 +3,58 @@ package portage
import ( import (
"testing" "testing"
"github.com/go-test/deep" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
"github.com/anchore/syft/syft/source" "github.com/anchore/syft/syft/source"
) )
func TestPortageCataloger(t *testing.T) { func TestPortageCataloger(t *testing.T) {
tests := []struct {
name string expectedPkgs := []pkg.Package{
expected []pkg.Package
}{
{ {
name: "go-case", Name: "app-containers/skopeo",
expected: []pkg.Package{ Version: "1.5.1",
{ FoundBy: "portage-cataloger",
Name: "app-containers/skopeo", PURL: "pkg:ebuild/app-containers/skopeo@1.5.1",
Version: "1.5.1", Locations: source.NewLocationSet(
FoundBy: "portage-cataloger", source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/CONTENTS"),
Licenses: []string{"Apache-2.0", "BSD", "BSD-2", "CC-BY-SA-4.0", "ISC", "MIT"}, source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/LICENSE"),
Type: pkg.PortagePkg, source.NewLocation("var/db/pkg/app-containers/skopeo-1.5.1/SIZE"),
MetadataType: pkg.PortageMetadataType, ),
Metadata: pkg.PortageMetadata{ Licenses: []string{"Apache-2.0", "BSD", "BSD-2", "CC-BY-SA-4.0", "ISC", "MIT"},
Package: "app-containers/skopeo", Type: pkg.PortagePkg,
Version: "1.5.1", MetadataType: pkg.PortageMetadataType,
InstalledSize: 27937835, Metadata: pkg.PortageMetadata{
Files: []pkg.PortageFileRecord{ InstalledSize: 27937835,
{ Files: []pkg.PortageFileRecord{
Path: "/usr/bin/skopeo", {
Digest: &file.Digest{ Path: "/usr/bin/skopeo",
Algorithm: "md5", Digest: &file.Digest{
Value: "376c02bd3b22804df8fdfdc895e7dbfb", Algorithm: "md5",
}, Value: "376c02bd3b22804df8fdfdc895e7dbfb",
}, },
{ },
Path: "/etc/containers/policy.json", {
Digest: &file.Digest{ Path: "/etc/containers/policy.json",
Algorithm: "md5", Digest: &file.Digest{
Value: "c01eb6950f03419e09d4fc88cb42ff6f", Algorithm: "md5",
}, Value: "c01eb6950f03419e09d4fc88cb42ff6f",
}, },
{ },
Path: "/etc/containers/registries.d/default.yaml", {
Digest: &file.Digest{ Path: "/etc/containers/registries.d/default.yaml",
Algorithm: "md5", Digest: &file.Digest{
Value: "e6e66cd3c24623e0667f26542e0e08f6", Algorithm: "md5",
}, Value: "e6e66cd3c24623e0667f26542e0e08f6",
}, },
{ },
Path: "/var/lib/atomic/sigstore/.keep_app-containers_skopeo-0", {
Digest: &file.Digest{ Path: "/var/lib/atomic/sigstore/.keep_app-containers_skopeo-0",
Algorithm: "md5", Digest: &file.Digest{
Value: "d41d8cd98f00b204e9800998ecf8427e", Algorithm: "md5",
}, Value: "d41d8cd98f00b204e9800998ecf8427e",
},
}, },
}, },
}, },
@ -66,41 +62,12 @@ func TestPortageCataloger(t *testing.T) {
}, },
} }
for _, test := range tests { // TODO: relationships are not under test yet
t.Run(test.name, func(t *testing.T) { var expectedRelationships []artifact.Relationship
img := imagetest.GetFixtureImage(t, "docker-archive", "image-portage") pkgtest.NewCatalogTester().
FromDirectory(t, "test-fixtures/image-portage").
s, err := source.NewFromImage(img, "") Expects(expectedPkgs, expectedRelationships).
if err != nil { TestCataloger(t, NewPortageCataloger())
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,18 @@
package portage
import (
"github.com/anchore/packageurl-go"
)
func packageURL(name, version string) string {
var qualifiers packageurl.Qualifiers
return packageurl.NewPackageURL(
"ebuild", // currently this is the proposed type for portage packages at https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst
"",
name,
version,
qualifiers,
"",
).ToString()
}

View File

@ -0,0 +1,28 @@
package portage
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_packageURL(t *testing.T) {
tests := []struct {
name string
version string
want string
}{
{
"app-admin/eselect",
"1.4.15",
"pkg:ebuild/app-admin/eselect@1.4.15",
},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("%s@%s", tt.name, tt.version), func(t *testing.T) {
assert.Equal(t, tt.want, packageURL(tt.name, tt.version))
})
}
}

View File

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

View File

@ -4,12 +4,8 @@ import (
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
) )
const PortageDBGlob = "**/var/db/pkg/*/*/CONTENTS"
// PortageMetadata represents all captured data for a Package package DB entry. // PortageMetadata represents all captured data for a Package package DB entry.
type PortageMetadata struct { type PortageMetadata struct {
Package string `mapstructure:"Package" json:"package"`
Version string `mapstructure:"Version" json:"version"`
InstalledSize int `mapstructure:"InstalledSize" json:"installedSize" cyclonedx:"installedSize"` InstalledSize int `mapstructure:"InstalledSize" json:"installedSize" cyclonedx:"installedSize"`
Files []PortageFileRecord `json:"files"` Files []PortageFileRecord `json:"files"`
} }