mirror of
https://github.com/anchore/syft
synced 2024-11-10 06:14:16 +00:00
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:
parent
00d6269e3c
commit
c61f59e7b7
12 changed files with 2509 additions and 37 deletions
|
@ -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"
|
||||
)
|
||||
|
|
2284
schema/json/schema-16.0.3.json
Normal file
2284
schema/json/schema-16.0.3.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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{},
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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...)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"version": "0.5",
|
||||
"requires": [
|
||||
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
|
||||
"matrix/1.1#905c3f0babc520684c84127378fefdd0%1675278901.7527816"
|
||||
],
|
||||
"build_requires": [],
|
||||
"python_requires": []
|
||||
}
|
|
@ -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"`
|
||||
|
|
Loading…
Reference in a new issue