Skip to content

Commit

Permalink
Add support for conan lock v2 (#2461)
Browse files Browse the repository at this point in the history
* conan lock 2.x requires field support

Signed-off-by: houdini91 <mdstrauss91@gmail.com>

* PR review, struct renaming

Signed-off-by: houdini91 <mdstrauss91@gmail.com>

---------

Signed-off-by: houdini91 <mdstrauss91@gmail.com>
  • Loading branch information
houdini91 authored and wagoodman committed Feb 2, 2024
1 parent b735106 commit 33a98e6
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 25 deletions.
2 changes: 1 addition & 1 deletion syft/internal/packagemetadata/generated.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func AllTypes() []any {
pkg.ApkDBEntry{},
pkg.BinarySignature{},
pkg.CocoaPodfileLockEntry{},
pkg.ConanLockEntry{},
pkg.ConanV1LockEntry{},
pkg.ConanfileEntry{},
pkg.ConaninfoEntry{},
pkg.DartPubspecLockEntry{},
Expand Down
2 changes: 1 addition & 1 deletion syft/internal/packagemetadata/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ var jsonTypes = makeJSONTypes(
jsonNames(pkg.ApkDBEntry{}, "apk-db-entry", "ApkMetadata"),
jsonNames(pkg.BinarySignature{}, "binary-signature", "BinaryMetadata"),
jsonNames(pkg.CocoaPodfileLockEntry{}, "cocoa-podfile-lock-entry", "CocoapodsMetadataType"),
jsonNames(pkg.ConanLockEntry{}, "c-conan-lock-entry", "ConanLockMetadataType"),
jsonNames(pkg.ConanV1LockEntry{}, "c-conan-lock-entry", "ConanLockMetadataType"),
jsonNames(pkg.ConanfileEntry{}, "c-conan-file-entry", "ConanMetadataType"),
jsonNames(pkg.ConaninfoEntry{}, "c-conan-info-entry"),
jsonNames(pkg.DartPubspecLockEntry{}, "dart-pubspec-lock-entry", "DartPubMetadata"),
Expand Down
4 changes: 2 additions & 2 deletions syft/internal/packagemetadata/names_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func TestReflectTypeFromJSONName_LegacyValues(t *testing.T) {
{
name: "map pkg.ConanLockEntry struct type",
input: "ConanLockMetadataType",
expected: reflect.TypeOf(pkg.ConanLockEntry{}),
expected: reflect.TypeOf(pkg.ConanV1LockEntry{}),
},
{
name: "map pkg.ConanfileEntry struct type",
Expand Down Expand Up @@ -290,7 +290,7 @@ func Test_JSONName_JSONLegacyName(t *testing.T) {
},
{
name: "ConanLockMetadata",
metadata: pkg.ConanLockEntry{},
metadata: pkg.ConanV1LockEntry{},
expectedJSONName: "c-conan-lock-entry",
expectedLegacyName: "ConanLockMetadataType",
},
Expand Down
6 changes: 5 additions & 1 deletion syft/pkg/cataloger/cpp/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,11 @@ func newConanfilePackage(m pkg.ConanfileEntry, locations ...file.Location) *pkg.
return newConanPackage(m.Ref, m, locations...)
}

func newConanlockPackage(m pkg.ConanLockEntry, locations ...file.Location) *pkg.Package {
func newConanlockPackage(m pkg.ConanV1LockEntry, locations ...file.Location) *pkg.Package {
return newConanPackage(m.Ref, m, locations...)
}

func newConanRefrencePackage(m pkg.ConanV2LockEntry, locations ...file.Location) *pkg.Package {
return newConanPackage(m.Ref, m, locations...)
}

Expand Down
82 changes: 79 additions & 3 deletions syft/pkg/cataloger/cpp/parse_conanlock.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,13 @@ type conanLock struct {
Path string `json:"path"`
} `json:"nodes"`
} `json:"graph_lock"`
Version string `json:"version"`
ProfileHost string `json:"profile_host"`
Version string `json:"version"`
ProfileHost string `json:"profile_host"`
ProfileBuild string `json:"profile_build,omitempty"`
// conan v0.5+ lockfiles use "requires", "build_requires" and "python_requires"
Requires []string `json:"requires,omitempty"`
BuildRequires []string `json:"build_requires,omitempty"`
PythonRequires []string `json:"python_requires,omitempty"`
}

// parseConanlock is a parser function for conan.lock contents, returning all packages discovered.
Expand All @@ -42,13 +47,33 @@ func parseConanlock(_ context.Context, _ file.Resolver, _ *generic.Environment,
// in a second iteration
var indexToPkgMap = map[string]pkg.Package{}

// Support for conan lock 2.x requires field
for _, ref := range cl.Requires {
reference := parseConanV2Reference(ref)
if reference.Name == "" {
continue
}

p := newConanRefrencePackage(
reference,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)

if p != nil {
pk := *p
pkgs = append(pkgs, pk)
indexToPkgMap[reference.Name] = pk
}

}

// we do not want to store the index list requires in the conan metadata, because it is not useful to have it in
// the SBOM. Instead, we will store it in a map and then use it to build the relationships
// maps pkg.ID to a list of indices
var parsedPkgRequires = map[artifact.ID][]string{}

for idx, node := range cl.GraphLock.Nodes {
metadata := pkg.ConanLockEntry{
metadata := pkg.ConanV1LockEntry{
Ref: node.Ref,
Options: parseOptions(node.Options),
Path: node.Path,
Expand Down Expand Up @@ -106,3 +131,54 @@ func parseOptions(options string) []pkg.KeyValue {

return o
}

func parseConanV2Reference(ref string) pkg.ConanV2LockEntry {
// very flexible format name/version[@username[/channel]][#rrev][:pkgid[#prev]][%timestamp]
reference := pkg.ConanV2LockEntry{Ref: ref}

parts := strings.SplitN(ref, "%", 2)
if len(parts) == 2 {
ref = parts[0]
reference.TimeStamp = parts[1]
}

parts = strings.SplitN(ref, ":", 2)
if len(parts) == 2 {
ref = parts[0]
parts = strings.SplitN(parts[1], "#", 2)
reference.PackageID = parts[0]
if len(parts) == 2 {
reference.PackageRevision = parts[1]
}
}

parts = strings.SplitN(ref, "#", 2)
if len(parts) == 2 {
ref = parts[0]
reference.RecipeRevision = parts[1]
}

parts = strings.SplitN(ref, "@", 2)
if len(parts) == 2 {
ref = parts[0]
UsernameChannel := parts[1]

parts = strings.SplitN(UsernameChannel, "/", 2)
reference.Username = parts[0]
if len(parts) == 2 {
reference.Channel = parts[1]
}
}

parts = strings.SplitN(ref, "/", 2)
if len(parts) == 2 {
reference.Name = parts[0]
reference.Version = parts[1]
} else {
// consumer conanfile.txt or conanfile.py might not have a name
reference.Name = ""
reference.Version = ref
}

return reference
}
12 changes: 6 additions & 6 deletions syft/pkg/cataloger/cpp/parse_conanlock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func TestParseConanlock(t *testing.T) {
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
Metadata: pkg.ConanLockEntry{
Metadata: pkg.ConanV1LockEntry{
Ref: "mfast/1.2.2@my_user/my_channel#c6f6387c9b99780f0ee05e25f99d0f39",
Options: pkg.KeyValues{
{Key: "fPIC", Value: "True"},
Expand Down Expand Up @@ -110,7 +110,7 @@ func TestParseConanlock(t *testing.T) {
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
Metadata: pkg.ConanLockEntry{
Metadata: pkg.ConanV1LockEntry{
Ref: "boost/1.75.0#a9c318f067216f900900e044e7af4ab1",
Options: pkg.KeyValues{
{Key: "addr2line_location", Value: "/usr/bin/addr2line"},
Expand Down Expand Up @@ -196,7 +196,7 @@ func TestParseConanlock(t *testing.T) {
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
Metadata: pkg.ConanLockEntry{
Metadata: pkg.ConanV1LockEntry{
Ref: "zlib/1.2.12#c67ce17f2e96b972d42393ce50a76a1a",
Options: pkg.KeyValues{
{
Expand All @@ -220,7 +220,7 @@ func TestParseConanlock(t *testing.T) {
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
Metadata: pkg.ConanLockEntry{
Metadata: pkg.ConanV1LockEntry{
Ref: "bzip2/1.0.8#62a8031289639043797cf53fa876d0ef",
Options: []pkg.KeyValue{
{
Expand Down Expand Up @@ -248,7 +248,7 @@ func TestParseConanlock(t *testing.T) {
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
Metadata: pkg.ConanLockEntry{
Metadata: pkg.ConanV1LockEntry{
Ref: "libbacktrace/cci.20210118#76e40b760e0bcd602d46db56b22820ab",
Options: []pkg.KeyValue{
{
Expand All @@ -272,7 +272,7 @@ func TestParseConanlock(t *testing.T) {
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
Metadata: pkg.ConanLockEntry{
Metadata: pkg.ConanV1LockEntry{
Ref: "tinyxml2/9.0.0#9f13a36ebfc222cd55fe531a0a8d94d1",
Options: []pkg.KeyValue{
{
Expand Down
34 changes: 23 additions & 11 deletions syft/pkg/conan.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
package pkg

// ConanLockEntry represents a single "node" entry from a conan.lock file.
type ConanLockEntry struct {
Ref string `json:"ref"`
PackageID string `json:"package_id,omitempty"`
Prev string `json:"prev,omitempty"`
Requires []string `json:"requires,omitempty"`
BuildRequires []string `json:"build_requires,omitempty"`
PythonRequires []string `json:"py_requires,omitempty"`
Options KeyValues `json:"options,omitempty"`
Path string `json:"path,omitempty"`
Context string `json:"context,omitempty"`
type ConanV2LockEntry struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Username string `json:"username,omitempty"`
Channel string `json:"channel,omitempty"`
RecipeRevision string `json:"recipe_revision,omitempty"`
PackageID string `json:"package_id,omitempty"`
PackageRevision string `json:"package_revision,omitempty"`
TimeStamp string `json:"timestamp,omitempty"`
Ref string `json:"ref"`
}

// ConanV1LockEntry represents a single "node" entry from a conan.lock file.
type ConanV1LockEntry struct {
Ref string `json:"ref"`
PackageID string `json:"package_id,omitempty"`
Prev string `json:"prev,omitempty"`
Requires []string `json:"requires,omitempty"`
BuildRequires []string `json:"build_requires,omitempty"`
PythonRequires []string `json:"py_requires,omitempty"`
Options map[string]string `json:"options,omitempty"`
Path string `json:"path,omitempty"`
Context string `json:"context,omitempty"`
}

// ConanfileEntry represents a single "Requires" entry from a conanfile.txt.
Expand Down

0 comments on commit 33a98e6

Please sign in to comment.