Finalize Conan v2 support (#2587)

* Add support for conan lock v2 (#2461)

* 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>

* decompose conanlock parser + add tests

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: houdini91 <mdstrauss91@gmail.com>
Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
Co-authored-by: mikey strauss <mdstrauss91@gmail.com>
This commit is contained in:
Alex Goodman 2024-02-07 08:24:02 -05:00 committed by GitHub
parent 00d6269e3c
commit c61f59e7b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 2509 additions and 37 deletions

View file

@ -3,5 +3,5 @@ package internal
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 = "16.0.2"
JSONSchemaVersion = "16.0.3"
)

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "anchore.io/schema/syft/json/16.0.2/document",
"$id": "anchore.io/schema/syft/json/16.0.3/document",
"$ref": "#/$defs/Document",
"$defs": {
"AlpmDbEntry": {
@ -275,6 +275,35 @@
"ref"
]
},
"CConanLockV2Entry": {
"properties": {
"ref": {
"type": "string"
},
"packageID": {
"type": "string"
},
"username": {
"type": "string"
},
"channel": {
"type": "string"
},
"recipeRevision": {
"type": "string"
},
"packageRevision": {
"type": "string"
},
"timestamp": {
"type": "string"
}
},
"type": "object",
"required": [
"ref"
]
},
"CPE": {
"properties": {
"cpe": {
@ -1363,6 +1392,9 @@
{
"$ref": "#/$defs/CConanLockEntry"
},
{
"$ref": "#/$defs/CConanLockV2Entry"
},
{
"$ref": "#/$defs/CocoaPodfileLockEntry"
},

View file

@ -11,7 +11,8 @@ func AllTypes() []any {
pkg.ApkDBEntry{},
pkg.BinarySignature{},
pkg.CocoaPodfileLockEntry{},
pkg.ConanLockEntry{},
pkg.ConanV1LockEntry{},
pkg.ConanV2LockEntry{},
pkg.ConanfileEntry{},
pkg.ConaninfoEntry{},
pkg.DartPubspecLockEntry{},

View file

@ -66,7 +66,8 @@ 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.ConanV2LockEntry{}, "c-conan-lock-v2-entry"),
jsonNames(pkg.ConanfileEntry{}, "c-conan-file-entry", "ConanMetadataType"),
jsonNames(pkg.ConaninfoEntry{}, "c-conan-info-entry"),
jsonNames(pkg.DartPubspecLockEntry{}, "dart-pubspec-lock-entry", "DartPubMetadata"),

View file

@ -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",
@ -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",
},

View file

@ -12,7 +12,7 @@ import (
func NewConanCataloger() pkg.Cataloger {
return generic.NewCataloger("conan-cataloger").
WithParserByGlobs(parseConanfile, "**/conanfile.txt").
WithParserByGlobs(parseConanlock, "**/conan.lock")
WithParserByGlobs(parseConanLock, "**/conan.lock")
}
// NewConanInfoCataloger returns a new C/C++ conaninfo.txt cataloger object.

View file

@ -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 newConanReferencePackage(m pkg.ConanV2LockEntry, locations ...file.Location) *pkg.Package {
return newConanPackage(m.Ref, m, locations...)
}

View file

@ -11,7 +11,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)
var _ generic.Parser = parseConanlock
var _ generic.Parser = parseConanLock
type conanLock struct {
GraphLock struct {
@ -28,11 +28,15 @@ type conanLock struct {
} `json:"graph_lock"`
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.
func parseConanlock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var pkgs []pkg.Package
// parseConanLock is a parser function for conan.lock (v1 and V2) contents, returning all packages discovered.
func parseConanLock(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
var cl conanLock
if err := json.NewDecoder(reader).Decode(&cl); err != nil {
return nil, nil, err
@ -42,13 +46,40 @@ func parseConanlock(_ context.Context, _ file.Resolver, _ *generic.Environment,
// in a second iteration
var indexToPkgMap = map[string]pkg.Package{}
v1Pkgs := handleConanLockV2(cl, reader, indexToPkgMap)
// 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{}
v2Pkgs := handleConanLockV1(cl, reader, parsedPkgRequires, indexToPkgMap)
var relationships []artifact.Relationship
var pkgs []pkg.Package
pkgs = append(pkgs, v1Pkgs...)
pkgs = append(pkgs, v2Pkgs...)
for _, p := range pkgs {
requires := parsedPkgRequires[p.ID()]
for _, r := range requires {
// this is a pkg that package "p" depends on... make a relationship
relationships = append(relationships, artifact.Relationship{
From: indexToPkgMap[r],
To: p,
Type: artifact.DependencyOfRelationship,
})
}
}
return pkgs, relationships, nil
}
// handleConanLockV1 handles the parsing of conan lock v1 files (aka v0.4)
func handleConanLockV1(cl conanLock, reader file.LocationReadCloser, parsedPkgRequires map[artifact.ID][]string, indexToPkgMap map[string]pkg.Package) []pkg.Package {
var pkgs []pkg.Package
for idx, node := range cl.GraphLock.Nodes {
metadata := pkg.ConanLockEntry{
metadata := pkg.ConanV1LockEntry{
Ref: node.Ref,
Options: parseOptions(node.Options),
Path: node.Path,
@ -69,22 +100,30 @@ func parseConanlock(_ context.Context, _ file.Resolver, _ *generic.Environment,
indexToPkgMap[idx] = pk
}
}
var relationships []artifact.Relationship
for _, p := range pkgs {
requires := parsedPkgRequires[p.ID()]
for _, r := range requires {
// this is a pkg that package "p" depends on... make a relationship
relationships = append(relationships, artifact.Relationship{
From: indexToPkgMap[r],
To: p,
Type: artifact.DependencyOfRelationship,
})
}
return pkgs
}
return pkgs, relationships, nil
// handleConanLockV2 handles the parsing of conan lock v2 files (aka v0.5)
func handleConanLockV2(cl conanLock, reader file.LocationReadCloser, indexToPkgMap map[string]pkg.Package) []pkg.Package {
var pkgs []pkg.Package
for _, ref := range cl.Requires {
reference, name := parseConanV2Reference(ref)
if name == "" {
continue
}
p := newConanReferencePackage(
reference,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
if p != nil {
pk := *p
pkgs = append(pkgs, pk)
indexToPkgMap[name] = pk
}
}
return pkgs
}
func parseOptions(options string) []pkg.KeyValue {
@ -106,3 +145,53 @@ func parseOptions(options string) []pkg.KeyValue {
return o
}
func parseConanV2Reference(ref string) (pkg.ConanV2LockEntry, string) {
// 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)
var name string
if len(parts) == 2 {
name = parts[0]
} else {
// consumer conanfile.txt or conanfile.py might not have a name
name = ""
}
return reference, name
}

View file

@ -9,7 +9,7 @@ import (
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)
func TestParseConanlock(t *testing.T) {
func TestParseConanLock(t *testing.T) {
fixture := "test-fixtures/conan.lock"
expected := []pkg.Package{
{
@ -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"},
@ -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"},
@ -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{
{
@ -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{
{
@ -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{
{
@ -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{
{
@ -330,5 +330,46 @@ func TestParseConanlock(t *testing.T) {
},
}
pkgtest.TestFileParser(t, fixture, parseConanlock, expected, expectedRelationships)
pkgtest.TestFileParser(t, fixture, parseConanLock, expected, expectedRelationships)
}
func TestParseConanLockV2(t *testing.T) {
fixture := "test-fixtures/conanlock-v2/conan.lock"
expected := []pkg.Package{
{
Name: "matrix",
Version: "1.1",
PURL: "pkg:conan/matrix@1.1",
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
Metadata: pkg.ConanV2LockEntry{
Ref: "matrix/1.1#905c3f0babc520684c84127378fefdd0%1675278901.7527816",
RecipeRevision: "905c3f0babc520684c84127378fefdd0",
TimeStamp: "1675278901.7527816",
},
},
{
Name: "sound32",
Version: "1.0",
PURL: "pkg:conan/sound32@1.0",
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
Metadata: pkg.ConanV2LockEntry{
Ref: "sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
RecipeRevision: "83d4b7bf607b3b60a6546f8b58b5cdd7",
TimeStamp: "1675278904.0791488",
},
},
}
// relationships require IDs to be set to be sorted similarly
for i := range expected {
expected[i].SetID()
}
var expectedRelationships []artifact.Relationship
pkgtest.TestFileParser(t, fixture, parseConanLock, expected, expectedRelationships)
}

View file

@ -0,0 +1,9 @@
{
"version": "0.5",
"requires": [
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
"matrix/1.1#905c3f0babc520684c84127378fefdd0%1675278901.7527816"
],
"build_requires": [],
"python_requires": []
}

View file

@ -1,7 +1,7 @@
package pkg
// ConanLockEntry represents a single "node" entry from a conan.lock file.
type ConanLockEntry struct {
// ConanV1LockEntry represents a single "node" entry from a conan.lock V1 file.
type ConanV1LockEntry struct {
Ref string `json:"ref"`
PackageID string `json:"package_id,omitempty"`
Prev string `json:"prev,omitempty"`
@ -13,6 +13,17 @@ type ConanLockEntry struct {
Context string `json:"context,omitempty"`
}
// ConanV2LockEntry represents a single "node" entry from a conan.lock V2 file.
type ConanV2LockEntry struct {
Ref string `json:"ref"`
PackageID string `json:"packageID,omitempty"`
Username string `json:"username,omitempty"`
Channel string `json:"channel,omitempty"`
RecipeRevision string `json:"recipeRevision,omitempty"`
PackageRevision string `json:"packageRevision,omitempty"`
TimeStamp string `json:"timestamp,omitempty"`
}
// ConanfileEntry represents a single "Requires" entry from a conanfile.txt.
type ConanfileEntry struct {
Ref string `mapstructure:"ref" json:"ref"`