Add Conan (C/C++) conan.lock file support (#1230)

Co-authored-by: Christopher Phillips <christopher.phillips@anchore.com>
This commit is contained in:
Hiroaki KAWAI 2022-09-30 03:45:59 +09:00 committed by GitHub
parent e6502536a7
commit b9b13d5525
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1925 additions and 58 deletions

View File

@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON encoder // 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. // 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 = "3.3.2" JSONSchemaVersion = "4.0.0"
) )

View File

@ -42,6 +42,10 @@ type artifactMetadataContainer struct {
Dart pkg.DartPubMetadata Dart pkg.DartPubMetadata
Dotnet pkg.DotnetDepsMetadata Dotnet pkg.DotnetDepsMetadata
Portage pkg.PortageMetadata Portage pkg.PortageMetadata
Conan pkg.ConanMetadata
ConanLock pkg.ConanLockMetadata
KbPackage pkg.KbPackageMetadata
Hackage pkg.HackageMetadata
} }
func main() { func main() {

File diff suppressed because it is too large Load Diff

View File

@ -163,12 +163,18 @@ func unpackMetadata(p *Package, unpacker packageMetadataUnpacker) error {
return err return err
} }
p.Metadata = payload p.Metadata = payload
case pkg.ConanaMetadataType: case pkg.ConanMetadataType:
var payload pkg.ConanMetadata var payload pkg.ConanMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err return err
} }
p.Metadata = payload p.Metadata = payload
case pkg.ConanLockMetadataType:
var payload pkg.ConanLockMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
return err
}
p.Metadata = payload
case pkg.DotnetDepsMetadataType: case pkg.DotnetDepsMetadataType:
var payload pkg.DotnetDepsMetadata var payload pkg.DotnetDepsMetadata
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil { if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {

View File

@ -88,7 +88,7 @@
} }
}, },
"schema": { "schema": {
"version": "3.3.2", "version": "4.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.2.json" "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-4.0.0.json"
} }
} }

View File

@ -184,7 +184,7 @@
} }
}, },
"schema": { "schema": {
"version": "3.3.2", "version": "4.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.2.json" "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-4.0.0.json"
} }
} }

View File

@ -111,7 +111,7 @@
} }
}, },
"schema": { "schema": {
"version": "3.3.2", "version": "4.0.0",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.2.json" "url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-4.0.0.json"
} }
} }

View File

@ -8,6 +8,7 @@ import (
func NewConanfileCataloger() *common.GenericCataloger { func NewConanfileCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{ globParsers := map[string]common.ParserFn{
"**/conanfile.txt": parseConanfile, "**/conanfile.txt": parseConanfile,
"**/conan.lock": parseConanlock,
} }
return common.NewGenericCataloger(nil, globParsers, "conan-cataloger") return common.NewGenericCataloger(nil, globParsers, "conan-cataloger")

View File

@ -40,21 +40,23 @@ func parseConanfile(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Rela
inRequirements = false inRequirements = false
} }
splits := strings.Split(strings.TrimSpace(line), "/") m := pkg.ConanMetadata{
if len(splits) < 2 || !inRequirements { Ref: strings.Trim(line, "\n"),
}
pkgName, pkgVersion := m.NameAndVersion()
if pkgName == "" || pkgVersion == "" || !inRequirements {
continue continue
} }
pkgName, pkgVersion := splits[0], splits[1]
pkgs = append(pkgs, &pkg.Package{ pkgs = append(pkgs, &pkg.Package{
Name: pkgName, Name: pkgName,
Version: pkgVersion, Version: pkgVersion,
Language: pkg.CPP, Language: pkg.CPP,
Type: pkg.ConanPkg, Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType, MetadataType: pkg.ConanMetadataType,
Metadata: pkg.ConanMetadata{ Metadata: m,
Name: pkgName,
Version: pkgVersion,
},
}) })
} }
} }

View File

@ -16,10 +16,9 @@ func TestParseConanfile(t *testing.T) {
Version: "2.13.8", Version: "2.13.8",
Language: pkg.CPP, Language: pkg.CPP,
Type: pkg.ConanPkg, Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType, MetadataType: pkg.ConanMetadataType,
Metadata: pkg.ConanMetadata{ Metadata: pkg.ConanMetadata{
Name: "catch2", Ref: "catch2/2.13.8",
Version: "2.13.8",
}, },
}, },
{ {
@ -27,10 +26,9 @@ func TestParseConanfile(t *testing.T) {
Version: "0.6.3", Version: "0.6.3",
Language: pkg.CPP, Language: pkg.CPP,
Type: pkg.ConanPkg, Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType, MetadataType: pkg.ConanMetadataType,
Metadata: pkg.ConanMetadata{ Metadata: pkg.ConanMetadata{
Name: "docopt.cpp", Ref: "docopt.cpp/0.6.3",
Version: "0.6.3",
}, },
}, },
{ {
@ -38,10 +36,9 @@ func TestParseConanfile(t *testing.T) {
Version: "8.1.1", Version: "8.1.1",
Language: pkg.CPP, Language: pkg.CPP,
Type: pkg.ConanPkg, Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType, MetadataType: pkg.ConanMetadataType,
Metadata: pkg.ConanMetadata{ Metadata: pkg.ConanMetadata{
Name: "fmt", Ref: "fmt/8.1.1",
Version: "8.1.1",
}, },
}, },
{ {
@ -49,10 +46,9 @@ func TestParseConanfile(t *testing.T) {
Version: "1.9.2", Version: "1.9.2",
Language: pkg.CPP, Language: pkg.CPP,
Type: pkg.ConanPkg, Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType, MetadataType: pkg.ConanMetadataType,
Metadata: pkg.ConanMetadata{ Metadata: pkg.ConanMetadata{
Name: "spdlog", Ref: "spdlog/1.9.2",
Version: "1.9.2",
}, },
}, },
{ {
@ -60,10 +56,9 @@ func TestParseConanfile(t *testing.T) {
Version: "2.0.20", Version: "2.0.20",
Language: pkg.CPP, Language: pkg.CPP,
Type: pkg.ConanPkg, Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType, MetadataType: pkg.ConanMetadataType,
Metadata: pkg.ConanMetadata{ Metadata: pkg.ConanMetadata{
Name: "sdl", Ref: "sdl/2.0.20",
Version: "2.0.20",
}, },
}, },
{ {
@ -71,10 +66,9 @@ func TestParseConanfile(t *testing.T) {
Version: "1.3.8", Version: "1.3.8",
Language: pkg.CPP, Language: pkg.CPP,
Type: pkg.ConanPkg, Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType, MetadataType: pkg.ConanMetadataType,
Metadata: pkg.ConanMetadata{ Metadata: pkg.ConanMetadata{
Name: "fltk", Ref: "fltk/1.3.8",
Version: "1.3.8",
}, },
}, },
} }

View File

@ -0,0 +1,82 @@
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"
)
// integrity check
var _ common.ParserFn = parseConanlock
type conanLock struct {
GraphLock struct {
Nodes map[string]struct {
Ref string `json:"ref"`
PackageID string `json:"package_id"`
Context string `json:"context"`
Prev string `json:"prev"`
Requires string `json:"requires"`
BuildRequires string `json:"build_requires"`
PythonRequires string `json:"py_requires"`
Options string `json:"options"`
Path string `json:"path"`
} `json:"nodes"`
} `json:"graph_lock"`
Version string `json:"version"`
ProfileHost string `json:"profile_host"`
}
// 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{}
var cl conanLock
if err := json.NewDecoder(reader).Decode(&cl); err != nil {
return nil, nil, err
}
for _, node := range cl.GraphLock.Nodes {
metadata := pkg.ConanLockMetadata{
Ref: node.Ref,
Options: parseOptions(node.Options),
Path: node.Path,
Context: node.Context,
}
pkgName, pkgVersion := metadata.NameAndVersion()
if pkgName == "" || pkgVersion == "" {
continue
}
pkgs = append(pkgs, &pkg.Package{
Name: pkgName,
Version: pkgVersion,
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
Metadata: metadata,
})
}
return pkgs, nil, nil
}
func parseOptions(options string) map[string]string {
o := make(map[string]string)
if len(options) == 0 {
return nil
}
kvps := strings.Split(options, "\n")
for _, kvp := range kvps {
kv := strings.Split(kvp, "=")
if len(kv) == 2 {
o[kv[0]] = kv[1]
}
}
return o
}

View File

@ -0,0 +1,47 @@
package cpp
import (
"os"
"testing"
"github.com/go-test/deep"
"github.com/anchore/syft/syft/pkg"
)
func TestParseConanlock(t *testing.T) {
expected := []*pkg.Package{
{
Name: "zlib",
Version: "1.2.12",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
Metadata: pkg.ConanLockMetadata{
Ref: "zlib/1.2.12",
Options: map[string]string{
"fPIC": "True",
"shared": "False",
},
Path: "all/conanfile.py",
Context: "host",
},
},
}
fixture, err := os.Open("test-fixtures/conan.lock")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
// TODO: no relationships are under test yet
actual, _, err := parseConanlock(fixture.Name(), fixture)
if err != nil {
t.Error(err)
}
differences := deep.Equal(expected, actual)
if differences != nil {
t.Errorf("returned package list differed from expectation: %+v", differences)
}
}

View File

@ -0,0 +1,15 @@
{
"graph_lock": {
"nodes": {
"0": {
"ref": "zlib/1.2.12",
"options": "fPIC=True\nshared=False",
"path": "all/conanfile.py",
"context": "host"
}
},
"revisions_enabled": false
},
"version": "0.4",
"profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=9\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n"
}

View File

@ -0,0 +1,50 @@
package pkg
import (
"strings"
"github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/linux"
)
type ConanLockMetadata 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"`
}
func (m ConanLockMetadata) PackageURL(_ *linux.Release) string {
var qualifiers packageurl.Qualifiers
name, version := m.NameAndVersion()
return packageurl.NewPackageURL(
packageurl.TypeConan,
"",
name,
version,
qualifiers,
"",
).ToString()
}
// NameAndVersion returns the name and version of the package.
// If ref is not in the format of "name/version@user/channel", then an empty string is returned for both.
func (m ConanLockMetadata) NameAndVersion() (name, version string) {
if len(m.Ref) < 1 {
return name, version
}
splits := strings.Split(strings.Split(m.Ref, "@")[0], "/")
if len(splits) < 2 {
return name, version
}
return splits[0], splits[1]
}

View File

@ -0,0 +1,27 @@
package pkg
import "testing"
func TestConanLockMetadata_PackageURL(t *testing.T) {
tests := []struct {
name string
m ConanLockMetadata
want string
}{
{
name: "happy path",
m: ConanLockMetadata{
Ref: "farmerbrown5/3.13.9",
},
want: "pkg:conan/farmerbrown5@3.13.9",
},
}
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

@ -1,24 +1,44 @@
package pkg package pkg
import ( import (
"strings"
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/linux"
) )
type ConanMetadata struct { type ConanMetadata struct {
Name string `mapstructure:"name" json:"name"` Ref string `mapstructure:"ref" json:"ref"`
Version string `mapstructure:"version" json:"version"`
} }
func (m ConanMetadata) PackageURL(_ *linux.Release) string { func (m ConanMetadata) PackageURL(_ *linux.Release) string {
var qualifiers packageurl.Qualifiers var qualifiers packageurl.Qualifiers
name, version := m.NameAndVersion()
return packageurl.NewPackageURL( return packageurl.NewPackageURL(
packageurl.TypeConan, packageurl.TypeConan,
"", "",
m.Name, name,
m.Version, version,
qualifiers, qualifiers,
"", "",
).ToString() ).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

@ -0,0 +1,27 @@
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

@ -26,7 +26,8 @@ const (
GolangBinMetadataType MetadataType = "GolangBinMetadata" GolangBinMetadataType MetadataType = "GolangBinMetadata"
PhpComposerJSONMetadataType MetadataType = "PhpComposerJsonMetadata" PhpComposerJSONMetadataType MetadataType = "PhpComposerJsonMetadata"
CocoapodsMetadataType MetadataType = "CocoapodsMetadataType" CocoapodsMetadataType MetadataType = "CocoapodsMetadataType"
ConanaMetadataType MetadataType = "ConanaMetadataType" ConanMetadataType MetadataType = "ConanMetadataType"
ConanLockMetadataType MetadataType = "ConanLockMetadataType"
PortageMetadataType MetadataType = "PortageMetadata" PortageMetadataType MetadataType = "PortageMetadata"
HackageMetadataType MetadataType = "HackageMetadataType" HackageMetadataType MetadataType = "HackageMetadataType"
) )
@ -47,7 +48,8 @@ var AllMetadataTypes = []MetadataType{
GolangBinMetadataType, GolangBinMetadataType,
PhpComposerJSONMetadataType, PhpComposerJSONMetadataType,
CocoapodsMetadataType, CocoapodsMetadataType,
ConanaMetadataType, ConanMetadataType,
ConanLockMetadataType,
PortageMetadataType, PortageMetadataType,
HackageMetadataType, HackageMetadataType,
} }
@ -68,7 +70,8 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{
GolangBinMetadataType: reflect.TypeOf(GolangBinMetadata{}), GolangBinMetadataType: reflect.TypeOf(GolangBinMetadata{}),
PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}), PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}),
CocoapodsMetadataType: reflect.TypeOf(CocoapodsMetadata{}), CocoapodsMetadataType: reflect.TypeOf(CocoapodsMetadata{}),
ConanaMetadataType: reflect.TypeOf(ConanMetadata{}), ConanMetadataType: reflect.TypeOf(ConanMetadata{}),
ConanLockMetadataType: reflect.TypeOf(ConanLockMetadata{}),
PortageMetadataType: reflect.TypeOf(PortageMetadata{}), PortageMetadataType: reflect.TypeOf(PortageMetadata{}),
HackageMetadataType: reflect.TypeOf(HackageMetadata{}), HackageMetadataType: reflect.TypeOf(HackageMetadata{}),
} }

View File

@ -239,14 +239,29 @@ func TestPackageURL(t *testing.T) {
Version: "2.13.8", Version: "2.13.8",
Type: ConanPkg, Type: ConanPkg,
Language: CPP, Language: CPP,
MetadataType: ConanaMetadataType, MetadataType: ConanMetadataType,
Metadata: ConanMetadata{ Metadata: ConanMetadata{
Name: "catch2", Ref: "catch2/2.13.8",
Version: "2.13.8",
}, },
}, },
expected: "pkg:conan/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{