mirror of
https://github.com/anchore/syft.git
synced 2025-11-17 16:33:21 +01:00
Add Conan (C/C++) conan.lock file support (#1230)
Co-authored-by: Christopher Phillips <christopher.phillips@anchore.com>
This commit is contained in:
parent
e6502536a7
commit
b9b13d5525
@ -6,5 +6,5 @@ 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 = "3.3.2"
|
||||
JSONSchemaVersion = "4.0.0"
|
||||
)
|
||||
|
||||
@ -42,6 +42,10 @@ type artifactMetadataContainer struct {
|
||||
Dart pkg.DartPubMetadata
|
||||
Dotnet pkg.DotnetDepsMetadata
|
||||
Portage pkg.PortageMetadata
|
||||
Conan pkg.ConanMetadata
|
||||
ConanLock pkg.ConanLockMetadata
|
||||
KbPackage pkg.KbPackageMetadata
|
||||
Hackage pkg.HackageMetadata
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
1574
schema/json/schema-4.0.0.json
Normal file
1574
schema/json/schema-4.0.0.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -163,12 +163,18 @@ func unpackMetadata(p *Package, unpacker packageMetadataUnpacker) error {
|
||||
return err
|
||||
}
|
||||
p.Metadata = payload
|
||||
case pkg.ConanaMetadataType:
|
||||
case pkg.ConanMetadataType:
|
||||
var payload pkg.ConanMetadata
|
||||
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
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:
|
||||
var payload pkg.DotnetDepsMetadata
|
||||
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||
|
||||
@ -88,7 +88,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "3.3.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.2.json"
|
||||
"version": "4.0.0",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-4.0.0.json"
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,7 +184,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "3.3.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.2.json"
|
||||
"version": "4.0.0",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-4.0.0.json"
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,7 +111,7 @@
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"version": "3.3.2",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-3.3.2.json"
|
||||
"version": "4.0.0",
|
||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-4.0.0.json"
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
func NewConanfileCataloger() *common.GenericCataloger {
|
||||
globParsers := map[string]common.ParserFn{
|
||||
"**/conanfile.txt": parseConanfile,
|
||||
"**/conan.lock": parseConanlock,
|
||||
}
|
||||
|
||||
return common.NewGenericCataloger(nil, globParsers, "conan-cataloger")
|
||||
|
||||
@ -40,21 +40,23 @@ func parseConanfile(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Rela
|
||||
inRequirements = false
|
||||
}
|
||||
|
||||
splits := strings.Split(strings.TrimSpace(line), "/")
|
||||
if len(splits) < 2 || !inRequirements {
|
||||
m := pkg.ConanMetadata{
|
||||
Ref: strings.Trim(line, "\n"),
|
||||
}
|
||||
|
||||
pkgName, pkgVersion := m.NameAndVersion()
|
||||
|
||||
if pkgName == "" || pkgVersion == "" || !inRequirements {
|
||||
continue
|
||||
}
|
||||
pkgName, pkgVersion := splits[0], splits[1]
|
||||
|
||||
pkgs = append(pkgs, &pkg.Package{
|
||||
Name: pkgName,
|
||||
Version: pkgVersion,
|
||||
Language: pkg.CPP,
|
||||
Type: pkg.ConanPkg,
|
||||
MetadataType: pkg.ConanaMetadataType,
|
||||
Metadata: pkg.ConanMetadata{
|
||||
Name: pkgName,
|
||||
Version: pkgVersion,
|
||||
},
|
||||
MetadataType: pkg.ConanMetadataType,
|
||||
Metadata: m,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,10 +16,9 @@ func TestParseConanfile(t *testing.T) {
|
||||
Version: "2.13.8",
|
||||
Language: pkg.CPP,
|
||||
Type: pkg.ConanPkg,
|
||||
MetadataType: pkg.ConanaMetadataType,
|
||||
MetadataType: pkg.ConanMetadataType,
|
||||
Metadata: pkg.ConanMetadata{
|
||||
Name: "catch2",
|
||||
Version: "2.13.8",
|
||||
Ref: "catch2/2.13.8",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -27,10 +26,9 @@ func TestParseConanfile(t *testing.T) {
|
||||
Version: "0.6.3",
|
||||
Language: pkg.CPP,
|
||||
Type: pkg.ConanPkg,
|
||||
MetadataType: pkg.ConanaMetadataType,
|
||||
MetadataType: pkg.ConanMetadataType,
|
||||
Metadata: pkg.ConanMetadata{
|
||||
Name: "docopt.cpp",
|
||||
Version: "0.6.3",
|
||||
Ref: "docopt.cpp/0.6.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -38,10 +36,9 @@ func TestParseConanfile(t *testing.T) {
|
||||
Version: "8.1.1",
|
||||
Language: pkg.CPP,
|
||||
Type: pkg.ConanPkg,
|
||||
MetadataType: pkg.ConanaMetadataType,
|
||||
MetadataType: pkg.ConanMetadataType,
|
||||
Metadata: pkg.ConanMetadata{
|
||||
Name: "fmt",
|
||||
Version: "8.1.1",
|
||||
Ref: "fmt/8.1.1",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -49,10 +46,9 @@ func TestParseConanfile(t *testing.T) {
|
||||
Version: "1.9.2",
|
||||
Language: pkg.CPP,
|
||||
Type: pkg.ConanPkg,
|
||||
MetadataType: pkg.ConanaMetadataType,
|
||||
MetadataType: pkg.ConanMetadataType,
|
||||
Metadata: pkg.ConanMetadata{
|
||||
Name: "spdlog",
|
||||
Version: "1.9.2",
|
||||
Ref: "spdlog/1.9.2",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -60,10 +56,9 @@ func TestParseConanfile(t *testing.T) {
|
||||
Version: "2.0.20",
|
||||
Language: pkg.CPP,
|
||||
Type: pkg.ConanPkg,
|
||||
MetadataType: pkg.ConanaMetadataType,
|
||||
MetadataType: pkg.ConanMetadataType,
|
||||
Metadata: pkg.ConanMetadata{
|
||||
Name: "sdl",
|
||||
Version: "2.0.20",
|
||||
Ref: "sdl/2.0.20",
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -71,10 +66,9 @@ func TestParseConanfile(t *testing.T) {
|
||||
Version: "1.3.8",
|
||||
Language: pkg.CPP,
|
||||
Type: pkg.ConanPkg,
|
||||
MetadataType: pkg.ConanaMetadataType,
|
||||
MetadataType: pkg.ConanMetadataType,
|
||||
Metadata: pkg.ConanMetadata{
|
||||
Name: "fltk",
|
||||
Version: "1.3.8",
|
||||
Ref: "fltk/1.3.8",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
82
syft/pkg/cataloger/cpp/parse_conanlock.go
Normal file
82
syft/pkg/cataloger/cpp/parse_conanlock.go
Normal 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
|
||||
}
|
||||
47
syft/pkg/cataloger/cpp/parse_conanlock_test.go
Normal file
47
syft/pkg/cataloger/cpp/parse_conanlock_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
15
syft/pkg/cataloger/cpp/test-fixtures/conan.lock
Normal file
15
syft/pkg/cataloger/cpp/test-fixtures/conan.lock
Normal 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"
|
||||
}
|
||||
50
syft/pkg/conan_lock_metadata.go
Normal file
50
syft/pkg/conan_lock_metadata.go
Normal 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]
|
||||
}
|
||||
27
syft/pkg/conan_lock_metadata_test.go
Normal file
27
syft/pkg/conan_lock_metadata_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,44 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/linux"
|
||||
)
|
||||
|
||||
type ConanMetadata struct {
|
||||
Name string `mapstructure:"name" json:"name"`
|
||||
Version string `mapstructure:"version" json:"version"`
|
||||
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,
|
||||
"",
|
||||
m.Name,
|
||||
m.Version,
|
||||
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]
|
||||
}
|
||||
|
||||
27
syft/pkg/conan_metadata_test.go
Normal file
27
syft/pkg/conan_metadata_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -26,7 +26,8 @@ const (
|
||||
GolangBinMetadataType MetadataType = "GolangBinMetadata"
|
||||
PhpComposerJSONMetadataType MetadataType = "PhpComposerJsonMetadata"
|
||||
CocoapodsMetadataType MetadataType = "CocoapodsMetadataType"
|
||||
ConanaMetadataType MetadataType = "ConanaMetadataType"
|
||||
ConanMetadataType MetadataType = "ConanMetadataType"
|
||||
ConanLockMetadataType MetadataType = "ConanLockMetadataType"
|
||||
PortageMetadataType MetadataType = "PortageMetadata"
|
||||
HackageMetadataType MetadataType = "HackageMetadataType"
|
||||
)
|
||||
@ -47,7 +48,8 @@ var AllMetadataTypes = []MetadataType{
|
||||
GolangBinMetadataType,
|
||||
PhpComposerJSONMetadataType,
|
||||
CocoapodsMetadataType,
|
||||
ConanaMetadataType,
|
||||
ConanMetadataType,
|
||||
ConanLockMetadataType,
|
||||
PortageMetadataType,
|
||||
HackageMetadataType,
|
||||
}
|
||||
@ -68,7 +70,8 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{
|
||||
GolangBinMetadataType: reflect.TypeOf(GolangBinMetadata{}),
|
||||
PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}),
|
||||
CocoapodsMetadataType: reflect.TypeOf(CocoapodsMetadata{}),
|
||||
ConanaMetadataType: reflect.TypeOf(ConanMetadata{}),
|
||||
ConanMetadataType: reflect.TypeOf(ConanMetadata{}),
|
||||
ConanLockMetadataType: reflect.TypeOf(ConanLockMetadata{}),
|
||||
PortageMetadataType: reflect.TypeOf(PortageMetadata{}),
|
||||
HackageMetadataType: reflect.TypeOf(HackageMetadata{}),
|
||||
}
|
||||
|
||||
@ -239,14 +239,29 @@ func TestPackageURL(t *testing.T) {
|
||||
Version: "2.13.8",
|
||||
Type: ConanPkg,
|
||||
Language: CPP,
|
||||
MetadataType: ConanaMetadataType,
|
||||
MetadataType: ConanMetadataType,
|
||||
Metadata: ConanMetadata{
|
||||
Name: "catch2",
|
||||
Version: "2.13.8",
|
||||
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",
|
||||
pkg: Package{
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user