feat: add support for conan packages (C/C++) (#1083)

This commit is contained in:
cpendery 2022-07-05 10:49:24 -04:00 committed by GitHub
parent 6b28a46ebe
commit 57323a1666
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 299 additions and 2 deletions

View file

@ -30,6 +30,8 @@ A CLI tool and Go library for generating a Software Bill of Materials (SBOM) fro
### Supported Ecosystems
- Alpine (apk)
- C (conan)
- C++ (conan)
- Dart (pubs)
- Debian (dpkg)
- Dotnet (deps.json)

View file

@ -35,6 +35,8 @@ func SourceInfo(p pkg.Package) string {
answer = "acquired package info from rust cargo manifest"
case pkg.PhpComposerPkg:
answer = "acquired package info from PHP composer manifest"
case pkg.ConanPkg:
answer = "acquired package info from conan manifest"
default:
answer = "acquired package info from the following paths"
}

View file

@ -150,6 +150,14 @@ func Test_SourceInfo(t *testing.T) {
"from ALPM DB",
},
},
{
input: pkg.Package{
Type: pkg.ConanPkg,
},
expected: []string{
"from conan manifest",
},
},
}
var pkgTypes []pkg.Type
for _, test := range tests {

View file

@ -63,7 +63,7 @@ func (p *Package) UnmarshalJSON(b []byte) error {
return unpackMetadata(p, unpacker)
}
// nolint:funlen
// nolint:funlen,gocognit,gocyclo
func unpackMetadata(p *Package, unpacker packageMetadataUnpacker) error {
p.MetadataType = unpacker.MetadataType
switch p.MetadataType {
@ -145,6 +145,12 @@ func unpackMetadata(p *Package, unpacker packageMetadataUnpacker) error {
return err
}
p.Metadata = payload
case pkg.ConanaMetadataType:
var payload pkg.ConanMetadata
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 {

View file

@ -13,6 +13,7 @@ import (
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/alpm"
"github.com/anchore/syft/syft/pkg/cataloger/apkdb"
"github.com/anchore/syft/syft/pkg/cataloger/cpp"
"github.com/anchore/syft/syft/pkg/cataloger/dart"
"github.com/anchore/syft/syft/pkg/cataloger/deb"
"github.com/anchore/syft/syft/pkg/cataloger/dotnet"
@ -75,6 +76,7 @@ func DirectoryCatalogers(cfg Config) []Cataloger {
rust.NewCargoLockCataloger(),
dart.NewPubspecLockCataloger(),
dotnet.NewDotnetDepsCataloger(),
cpp.NewConanfileCataloger(),
}, cfg.Catalogers)
}
@ -100,6 +102,7 @@ func AllCatalogers(cfg Config) []Cataloger {
dotnet.NewDotnetDepsCataloger(),
php.NewPHPComposerInstalledCataloger(),
php.NewPHPComposerLockCataloger(),
cpp.NewConanfileCataloger(),
}, cfg.Catalogers)
}

View file

@ -0,0 +1,14 @@
package cpp
import (
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
// NewConanfileCataloger returns a new C++ Conanfile cataloger object.
func NewConanfileCataloger() *common.GenericCataloger {
globParsers := map[string]common.ParserFn{
"**/conanfile.txt": parseConanfile,
}
return common.NewGenericCataloger(nil, globParsers, "conan-cataloger")
}

View file

@ -0,0 +1,60 @@
package cpp
import (
"bufio"
"errors"
"fmt"
"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 = parseConanfile
type Conanfile struct {
Requires []string `toml:"requires"`
}
// parseConanfile is a parser function for conanfile.txt contents, returning all packages discovered.
func parseConanfile(_ string, reader io.Reader) ([]*pkg.Package, []artifact.Relationship, error) {
r := bufio.NewReader(reader)
inRequirements := false
pkgs := []*pkg.Package{}
for {
line, err := r.ReadString('\n')
switch {
case errors.Is(io.EOF, err):
return pkgs, nil, nil
case err != nil:
return nil, nil, fmt.Errorf("failed to parse conanfile.txt file: %w", err)
}
switch {
case strings.Contains(line, "[requires]"):
inRequirements = true
case strings.ContainsAny(line, "[]#"):
inRequirements = false
}
splits := strings.Split(strings.TrimSpace(line), "/")
if len(splits) < 2 || !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,
},
})
}
}

View file

@ -0,0 +1,96 @@
package cpp
import (
"os"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/go-test/deep"
)
func TestParseConanfile(t *testing.T) {
expected := []*pkg.Package{
{
Name: "catch2",
Version: "2.13.8",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType,
Metadata: pkg.ConanMetadata{
Name: "catch2",
Version: "2.13.8",
},
},
{
Name: "docopt.cpp",
Version: "0.6.3",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType,
Metadata: pkg.ConanMetadata{
Name: "docopt.cpp",
Version: "0.6.3",
},
},
{
Name: "fmt",
Version: "8.1.1",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType,
Metadata: pkg.ConanMetadata{
Name: "fmt",
Version: "8.1.1",
},
},
{
Name: "spdlog",
Version: "1.9.2",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType,
Metadata: pkg.ConanMetadata{
Name: "spdlog",
Version: "1.9.2",
},
},
{
Name: "sdl",
Version: "2.0.20",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType,
Metadata: pkg.ConanMetadata{
Name: "sdl",
Version: "2.0.20",
},
},
{
Name: "fltk",
Version: "1.3.8",
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanaMetadataType,
Metadata: pkg.ConanMetadata{
Name: "fltk",
Version: "1.3.8",
},
},
}
fixture, err := os.Open("test-fixtures/conanfile.txt")
if err != nil {
t.Fatalf("failed to open fixture: %+v", err)
}
// TODO: no relationships are under test yet
actual, _, err := parseConanfile(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,12 @@
# Docs at https://docs.conan.io/en/latest/reference/conanfile_txt.html
[requires]
catch2/2.13.8
docopt.cpp/0.6.3
fmt/8.1.1
spdlog/1.9.2
sdl/2.0.20
fltk/1.3.8
[generators]
cmake_find_package_multi

View file

@ -0,0 +1,24 @@
package pkg
import (
"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"`
}
func (m ConanMetadata) PackageURL(_ *linux.Release) string {
var qualifiers packageurl.Qualifiers
return packageurl.NewPackageURL(
packageurl.TypeConan,
"",
m.Name,
m.Version,
qualifiers,
"",
).ToString()
}

View file

@ -21,6 +21,7 @@ const (
Rust Language = "rust"
Dart Language = "dart"
Dotnet Language = "dotnet"
CPP Language = "c++"
)
// AllLanguages is a set of all programming languages detected by syft.
@ -34,6 +35,7 @@ var AllLanguages = []Language{
Rust,
Dart,
Dotnet,
CPP,
}
// String returns the string representation of the language.
@ -70,6 +72,8 @@ func LanguageByName(name string) Language {
return Dart
case packageurl.TypeDotnet:
return Dotnet
case packageurl.TypeConan, string(CPP):
return CPP
default:
return UnknownLanguage
}

View file

@ -50,6 +50,10 @@ func TestLanguageFromPURL(t *testing.T) {
purl: "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?type=zip&classifier=dist",
want: Java,
},
{
purl: "pkg:conan/catch2@2.13.8",
want: CPP,
},
}
var languages []string
@ -183,6 +187,14 @@ func TestLanguageByName(t *testing.T) {
name: "unknown",
language: UnknownLanguage,
},
{
name: "conan",
language: CPP,
},
{
name: "c++",
language: CPP,
},
}
for _, test := range tests {

View file

@ -25,6 +25,7 @@ const (
KbPackageMetadataType MetadataType = "KbPackageMetadata"
GolangBinMetadataType MetadataType = "GolangBinMetadata"
PhpComposerJSONMetadataType MetadataType = "PhpComposerJsonMetadata"
ConanaMetadataType MetadataType = "ConanaMetadataType"
)
var AllMetadataTypes = []MetadataType{
@ -42,6 +43,7 @@ var AllMetadataTypes = []MetadataType{
KbPackageMetadataType,
GolangBinMetadataType,
PhpComposerJSONMetadataType,
ConanaMetadataType,
}
var MetadataTypeByName = map[MetadataType]reflect.Type{
@ -59,4 +61,5 @@ var MetadataTypeByName = map[MetadataType]reflect.Type{
KbPackageMetadataType: reflect.TypeOf(KbPackageMetadata{}),
GolangBinMetadataType: reflect.TypeOf(GolangBinMetadata{}),
PhpComposerJSONMetadataType: reflect.TypeOf(PhpComposerJSONMetadata{}),
ConanaMetadataType: reflect.TypeOf(ConanMetadata{}),
}

View file

@ -23,6 +23,7 @@ const (
KbPkg Type = "msrc-kb"
DartPubPkg Type = "dart-pub"
DotnetPkg Type = "dotnet"
ConanPkg Type = "conan"
)
// AllPkgs represents all supported package types
@ -42,6 +43,7 @@ var AllPkgs = []Type{
KbPkg,
DartPubPkg,
DotnetPkg,
ConanPkg,
}
// PackageURLType returns the PURL package type for the current package.
@ -73,6 +75,8 @@ func (t Type) PackageURLType() string {
return packageurl.TypePub
case DotnetPkg:
return packageurl.TypeDotnet
case ConanPkg:
return packageurl.TypeConan
default:
// TODO: should this be a "generic" purl type instead?
return ""
@ -116,6 +120,8 @@ func TypeByName(name string) Type {
return DartPubPkg
case packageurl.TypeDotnet:
return DotnetPkg
case packageurl.TypeConan:
return ConanPkg
default:
return UnknownPkg
}

View file

@ -68,6 +68,10 @@ func TestTypeFromPURL(t *testing.T) {
purl: "pkg:alpm/arch/linux@5.10.0?arch=x86_64&distro=arch",
expected: AlpmPkg,
},
{
purl: "pkg:conan/catch2@2.13.8",
expected: ConanPkg,
},
}
var pkgTypes []string

View file

@ -208,6 +208,21 @@ func TestPackageURL(t *testing.T) {
expected: "pkg:alpm/arch/linux@5.10.0?distro=arch-rolling",
},
{
name: "conan",
pkg: Package{
Name: "catch2",
Version: "2.13.8",
Type: ConanPkg,
Language: CPP,
MetadataType: ConanaMetadataType,
Metadata: ConanMetadata{
Name: "catch2",
Version: "2.13.8",
},
},
expected: "pkg:conan/catch2@2.13.8",
},
}
var pkgTypes []string

View file

@ -167,6 +167,19 @@ var dirOnlyTestCases = []testCase{
"github.com/bmatcuk/doublestar": "v1.3.1",
},
},
{
name: "find conan packages",
pkgType: pkg.ConanPkg,
pkgLanguage: pkg.CPP,
pkgInfo: map[string]string{
"catch2": "2.13.8",
"docopt.cpp": "0.6.3",
"fmt": "8.1.1",
"spdlog": "1.9.2",
"sdl": "2.0.20",
"fltk": "1.3.8",
},
},
{
name: "find rust crates",
pkgType: pkg.RustPkg,
@ -264,7 +277,6 @@ var commonTestCases = []testCase{
"netbase": "5.4",
},
},
{
name: "find jenkins plugins",
pkgType: pkg.JenkinsPluginPkg,

View file

@ -67,6 +67,7 @@ func TestPkgCoverageImage(t *testing.T) {
definedLanguages.Remove(pkg.Rust.String())
definedLanguages.Remove(pkg.Dart.String())
definedLanguages.Remove(pkg.Dotnet.String())
definedLanguages.Remove(pkg.CPP.String())
observedPkgs := internal.NewStringSet()
definedPkgs := internal.NewStringSet()
@ -80,6 +81,7 @@ func TestPkgCoverageImage(t *testing.T) {
definedPkgs.Remove(string(pkg.RustPkg))
definedPkgs.Remove(string(pkg.DartPubPkg))
definedPkgs.Remove(string(pkg.DotnetPkg))
definedPkgs.Remove(string(pkg.ConanPkg))
var cases []testCase
cases = append(cases, commonTestCases...)

View file

@ -0,0 +1,12 @@
# Docs at https://docs.conan.io/en/latest/reference/conanfile_txt.html
[requires]
catch2/2.13.8
docopt.cpp/0.6.3
fmt/8.1.1
spdlog/1.9.2
sdl/2.0.20
fltk/1.3.8
[generators]
cmake_find_package_multi