port conan cataloger to new generic cataloger pattern (#1284)

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-10-24 16:11:20 -04:00 committed by GitHub
parent f36c0ca971
commit eb8ebd9ffc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 132 additions and 156 deletions

View file

@ -82,7 +82,7 @@ func DirectoryCatalogers(cfg Config) []Cataloger {
dart.NewPubspecLockCataloger(),
dotnet.NewDotnetDepsCataloger(),
swift.NewCocoapodsCataloger(),
cpp.NewConanfileCataloger(),
cpp.NewConanCataloger(),
portage.NewPortageCataloger(),
haskell.NewHackageCataloger(),
}, cfg.Catalogers)
@ -113,7 +113,7 @@ func AllCatalogers(cfg Config) []Cataloger {
php.NewPHPComposerInstalledCataloger(),
php.NewPHPComposerLockCataloger(),
swift.NewCocoapodsCataloger(),
cpp.NewConanfileCataloger(),
cpp.NewConanCataloger(),
portage.NewPortageCataloger(),
haskell.NewHackageCataloger(),
}, cfg.Catalogers)

View file

@ -1,15 +1,14 @@
package cpp
import (
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
// NewConanfileCataloger returns a new C++ Conanfile cataloger object.
func NewConanfileCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/conanfile.txt": parseConanfile,
"**/conan.lock": parseConanlock,
}
const catalogerName = "conan-cataloger"
return common.NewGenericCataloger(nil, globParsers, "conan-cataloger")
// NewConanCataloger returns a new C++ conanfile.txt and conan.lock cataloger object.
func NewConanCataloger() *generic.Cataloger {
return generic.NewCataloger(catalogerName).
WithParserByGlobs(parseConanfile, "**/conanfile.txt").
WithParserByGlobs(parseConanlock, "**/conan.lock")
}

View file

@ -0,0 +1,76 @@
package cpp
import (
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func newConanfilePackage(m pkg.ConanMetadata, locations ...source.Location) *pkg.Package {
fields := strings.Split(strings.TrimSpace(m.Ref), "/")
if len(fields) < 2 {
return nil
}
pkgName, pkgVersion := fields[0], fields[1]
if pkgName == "" || pkgVersion == "" {
return nil
}
p := pkg.Package{
Name: pkgName,
Version: pkgVersion,
Locations: source.NewLocationSet(locations...),
PURL: packageURL(pkgName, pkgVersion),
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanMetadataType,
Metadata: m,
}
p.SetID()
return &p
}
func newConanlockPackage(m pkg.ConanLockMetadata, locations ...source.Location) *pkg.Package {
fields := strings.Split(strings.Split(m.Ref, "@")[0], "/")
if len(fields) < 2 {
return nil
}
pkgName, pkgVersion := fields[0], fields[1]
if pkgName == "" || pkgVersion == "" {
return nil
}
p := pkg.Package{
Name: pkgName,
Version: pkgVersion,
Locations: source.NewLocationSet(locations...),
PURL: packageURL(pkgName, pkgVersion),
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
Metadata: m,
}
p.SetID()
return &p
}
func packageURL(name, version string) string {
return packageurl.NewPackageURL(
packageurl.TypeConan,
"",
name,
version,
nil, // TODO: no qualifiers (...yet)
"",
).ToString()
}

View file

@ -9,21 +9,21 @@ import (
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/source"
)
// integrity check
var _ common.ParserFn = parseConanfile
var _ generic.Parser = parseConanfile
type Conanfile struct {
Requires []string `toml:"requires"`
}
// parseConanfile is a parser function for conanfile.txt contents, returning all packages discovered.
func parseConanfile(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
func parseConanfile(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
r := bufio.NewReader(reader)
inRequirements := false
pkgs := []*pkg.Package{}
var pkgs []pkg.Package
for {
line, err := r.ReadString('\n')
switch {
@ -44,19 +44,15 @@ func parseConanfile(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Rela
Ref: strings.Trim(line, "\n"),
}
pkgName, pkgVersion := m.NameAndVersion()
if pkgName == "" || pkgVersion == "" || !inRequirements {
if !inRequirements {
continue
}
pkgs = append(pkgs, &pkg.Package{
Name: pkgName,
Version: pkgVersion,
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanMetadataType,
Metadata: m,
})
p := newConanfilePackage(m, reader.Location)
if p == nil {
continue
}
pkgs = append(pkgs, *p)
}
}

View file

@ -5,15 +5,18 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func TestParseConanfile(t *testing.T) {
expected := []*pkg.Package{
expected := []pkg.Package{
{
Name: "catch2",
Version: "2.13.8",
PURL: "pkg:conan/catch2@2.13.8",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanMetadataType,
@ -24,6 +27,7 @@ func TestParseConanfile(t *testing.T) {
{
Name: "docopt.cpp",
Version: "0.6.3",
PURL: "pkg:conan/docopt.cpp@0.6.3",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanMetadataType,
@ -34,6 +38,7 @@ func TestParseConanfile(t *testing.T) {
{
Name: "fmt",
Version: "8.1.1",
PURL: "pkg:conan/fmt@8.1.1",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanMetadataType,
@ -44,6 +49,7 @@ func TestParseConanfile(t *testing.T) {
{
Name: "spdlog",
Version: "1.9.2",
PURL: "pkg:conan/spdlog@1.9.2",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanMetadataType,
@ -54,6 +60,7 @@ func TestParseConanfile(t *testing.T) {
{
Name: "sdl",
Version: "2.0.20",
PURL: "pkg:conan/sdl@2.0.20",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanMetadataType,
@ -64,6 +71,7 @@ func TestParseConanfile(t *testing.T) {
{
Name: "fltk",
Version: "1.3.8",
PURL: "pkg:conan/fltk@1.3.8",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanMetadataType,
@ -74,15 +82,14 @@ func TestParseConanfile(t *testing.T) {
}
fixture, err := os.Open("test-fixtures/conanfile.txt")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
require.NoError(t, err)
// TODO: no relationships are under test yet
actual, _, err := parseConanfile(fixture.Name(), fixture)
if err != nil {
t.Error(err)
}
actual, _, err := parseConanfile(nil, nil, source.LocationReadCloser{
Location: source.NewLocation(fixture.Name()),
ReadCloser: fixture,
})
require.NoError(t, err)
differences := deep.Equal(expected, actual)
if differences != nil {

View file

@ -2,16 +2,15 @@ package cpp
import (
"encoding/json"
"io"
"strings"
"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
"github.com/anchore/syft/syft/source"
)
// integrity check
var _ common.ParserFn = parseConanlock
var _ generic.Parser = parseConanlock
type conanLock struct {
GraphLock struct {
@ -31,8 +30,8 @@ type conanLock struct {
}
// parseConanlock is a parser function for conan.lock contents, returning all packages discovered.
func parseConanlock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
pkgs := []*pkg.Package{}
func parseConanlock(_ source.FileResolver, _ *generic.Environment, reader source.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
var cl conanLock
if err := json.NewDecoder(reader).Decode(&cl); err != nil {
return nil, nil, err
@ -45,19 +44,11 @@ func parseConanlock(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Rela
Context: node.Context,
}
pkgName, pkgVersion := metadata.NameAndVersion()
if pkgName == "" || pkgVersion == "" {
continue
}
p := newConanlockPackage(metadata, reader.Location)
pkgs = append(pkgs, &pkg.Package{
Name: pkgName,
Version: pkgVersion,
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
Metadata: metadata,
})
if p != nil {
pkgs = append(pkgs, *p)
}
}
return pkgs, nil, nil

View file

@ -5,15 +5,18 @@ import (
"testing"
"github.com/go-test/deep"
"github.com/stretchr/testify/require"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
func TestParseConanlock(t *testing.T) {
expected := []*pkg.Package{
expected := []pkg.Package{
{
Name: "zlib",
Version: "1.2.12",
PURL: "pkg:conan/zlib@1.2.12",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
@ -30,15 +33,14 @@ func TestParseConanlock(t *testing.T) {
}
fixture, err := os.Open("test-fixtures/conan.lock")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
require.NoError(t, err)
// TODO: no relationships are under test yet
actual, _, err := parseConanlock(fixture.Name(), fixture)
if err != nil {
t.Error(err)
}
actual, _, err := parseConanlock(nil, nil, source.LocationReadCloser{
Location: source.NewLocation(fixture.Name()),
ReadCloser: fixture,
})
require.NoError(t, err)
differences := deep.Equal(expected, actual)
if differences != nil {

View file

@ -1,44 +1,5 @@
package pkg
import (
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/linux"
)
type ConanMetadata struct {
Ref string `mapstructure:"ref" json:"ref"`
}
func (m ConanMetadata) PackageURL(_ *linux.Release) string {
var qualifiers packageurl.Qualifiers
name, version := m.NameAndVersion()
return packageurl.NewPackageURL(
packageurl.TypeConan,
"",
name,
version,
qualifiers,
"",
).ToString()
}
// NameAndVersion tries to return the name and version of a cpp package
// given the ref format: pkg/version
// it returns empty strings if ref is empty or parsing is unsuccessful
func (m ConanMetadata) NameAndVersion() (name, version string) {
if len(m.Ref) < 1 {
return name, version
}
splits := strings.Split(strings.TrimSpace(m.Ref), "/")
if len(splits) < 2 {
return name, version
}
return splits[0], splits[1]
}

View file

@ -1,27 +0,0 @@
package pkg
import "testing"
func TestConanMetadata_PackageURL(t *testing.T) {
tests := []struct {
name string
m ConanMetadata
want string
}{
{
name: "happy path",
m: ConanMetadata{
Ref: "catch2/2.13.8",
},
want: "pkg:conan/catch2@2.13.8",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if got := test.m.PackageURL(nil); got != test.want {
t.Errorf("ConanMetadata.PackageURL() = %v, want %v", got, test.want)
}
})
}
}

View file

@ -196,36 +196,6 @@ func TestPackageURL(t *testing.T) {
},
expected: "pkg:cocoapods/GlossButtonNode@3.1.2",
},
{
name: "conan",
pkg: Package{
Name: "catch2",
Version: "2.13.8",
Type: ConanPkg,
Language: CPP,
MetadataType: ConanMetadataType,
Metadata: ConanMetadata{
Ref: "catch2/2.13.8",
},
},
expected: "pkg:conan/catch2@2.13.8",
},
// note both Ref should parse the same for conan ecosystem
{
name: "conan lock",
pkg: Package{
Name: "catch2",
Version: "2.13.8",
Type: ConanPkg,
Language: CPP,
MetadataType: ConanLockMetadataType,
Metadata: ConanLockMetadata{
Ref: "catch2/2.13.8",
},
},
expected: "pkg:conan/catch2@2.13.8",
},
{
name: "hackage",
pkg: Package{
@ -254,6 +224,7 @@ func TestPackageURL(t *testing.T) {
expectedTypes.Remove(string(PortagePkg))
expectedTypes.Remove(string(AlpmPkg))
expectedTypes.Remove(string(ApkPkg))
expectedTypes.Remove(string(ConanPkg))
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {