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

View File

@ -1,15 +1,14 @@
package cpp package cpp
import ( 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. const catalogerName = "conan-cataloger"
func NewConanfileCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/conanfile.txt": parseConanfile,
"**/conan.lock": parseConanlock,
}
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/artifact"
"github.com/anchore/syft/syft/pkg" "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 _ generic.Parser = parseConanfile
var _ common.ParserFn = parseConanfile
type Conanfile struct { type Conanfile struct {
Requires []string `toml:"requires"` Requires []string `toml:"requires"`
} }
// parseConanfile is a parser function for conanfile.txt contents, returning all packages discovered. // 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) r := bufio.NewReader(reader)
inRequirements := false inRequirements := false
pkgs := []*pkg.Package{} var pkgs []pkg.Package
for { for {
line, err := r.ReadString('\n') line, err := r.ReadString('\n')
switch { switch {
@ -44,19 +44,15 @@ func parseConanfile(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Rela
Ref: strings.Trim(line, "\n"), Ref: strings.Trim(line, "\n"),
} }
pkgName, pkgVersion := m.NameAndVersion() if !inRequirements {
if pkgName == "" || pkgVersion == "" || !inRequirements {
continue continue
} }
pkgs = append(pkgs, &pkg.Package{ p := newConanfilePackage(m, reader.Location)
Name: pkgName, if p == nil {
Version: pkgVersion, continue
Language: pkg.CPP, }
Type: pkg.ConanPkg,
MetadataType: pkg.ConanMetadataType, pkgs = append(pkgs, *p)
Metadata: m,
})
} }
} }

View File

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

View File

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

View File

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

View File

@ -1,44 +1,5 @@
package pkg package pkg
import (
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/linux"
)
type ConanMetadata struct { type ConanMetadata struct {
Ref string `mapstructure:"ref" json:"ref"` 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", 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", name: "hackage",
pkg: Package{ pkg: Package{
@ -254,6 +224,7 @@ func TestPackageURL(t *testing.T) {
expectedTypes.Remove(string(PortagePkg)) expectedTypes.Remove(string(PortagePkg))
expectedTypes.Remove(string(AlpmPkg)) expectedTypes.Remove(string(AlpmPkg))
expectedTypes.Remove(string(ApkPkg)) expectedTypes.Remove(string(ApkPkg))
expectedTypes.Remove(string(ConanPkg))
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {