From 57323a16661f575749b9a91573808b8381f3aef9 Mon Sep 17 00:00:00 2001 From: cpendery <35637443+cpendery@users.noreply.github.com> Date: Tue, 5 Jul 2022 10:49:24 -0400 Subject: [PATCH] feat: add support for conan packages (C/C++) (#1083) --- README.md | 2 + .../formats/common/spdxhelpers/source_info.go | 2 + .../common/spdxhelpers/source_info_test.go | 8 ++ internal/formats/syftjson/model/package.go | 8 +- syft/pkg/cataloger/cataloger.go | 3 + syft/pkg/cataloger/cpp/cataloger.go | 14 +++ syft/pkg/cataloger/cpp/parse_conanfile.go | 60 ++++++++++++ .../pkg/cataloger/cpp/parse_conanfile_test.go | 96 +++++++++++++++++++ .../cataloger/cpp/test-fixtures/conanfile.txt | 12 +++ syft/pkg/conan_metadata.go | 24 +++++ syft/pkg/language.go | 4 + syft/pkg/language_test.go | 12 +++ syft/pkg/metadata.go | 3 + syft/pkg/type.go | 6 ++ syft/pkg/type_test.go | 4 + syft/pkg/url_test.go | 15 +++ .../catalog_packages_cases_test.go | 14 ++- test/integration/catalog_packages_test.go | 2 + .../image-pkg-coverage/conan/conanfile.txt | 12 +++ 19 files changed, 299 insertions(+), 2 deletions(-) create mode 100644 syft/pkg/cataloger/cpp/cataloger.go create mode 100644 syft/pkg/cataloger/cpp/parse_conanfile.go create mode 100644 syft/pkg/cataloger/cpp/parse_conanfile_test.go create mode 100644 syft/pkg/cataloger/cpp/test-fixtures/conanfile.txt create mode 100644 syft/pkg/conan_metadata.go create mode 100644 test/integration/test-fixtures/image-pkg-coverage/conan/conanfile.txt diff --git a/README.md b/README.md index 2974b5afd..b9af89b74 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/internal/formats/common/spdxhelpers/source_info.go b/internal/formats/common/spdxhelpers/source_info.go index 7f4a4a33b..1e4a565d0 100644 --- a/internal/formats/common/spdxhelpers/source_info.go +++ b/internal/formats/common/spdxhelpers/source_info.go @@ -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" } diff --git a/internal/formats/common/spdxhelpers/source_info_test.go b/internal/formats/common/spdxhelpers/source_info_test.go index 4eb652cd0..1e86585d6 100644 --- a/internal/formats/common/spdxhelpers/source_info_test.go +++ b/internal/formats/common/spdxhelpers/source_info_test.go @@ -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 { diff --git a/internal/formats/syftjson/model/package.go b/internal/formats/syftjson/model/package.go index b7bec3ed0..87dd1c120 100644 --- a/internal/formats/syftjson/model/package.go +++ b/internal/formats/syftjson/model/package.go @@ -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 { diff --git a/syft/pkg/cataloger/cataloger.go b/syft/pkg/cataloger/cataloger.go index 518b8d9a3..165f79748 100644 --- a/syft/pkg/cataloger/cataloger.go +++ b/syft/pkg/cataloger/cataloger.go @@ -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) } diff --git a/syft/pkg/cataloger/cpp/cataloger.go b/syft/pkg/cataloger/cpp/cataloger.go new file mode 100644 index 000000000..a8f02877d --- /dev/null +++ b/syft/pkg/cataloger/cpp/cataloger.go @@ -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") +} diff --git a/syft/pkg/cataloger/cpp/parse_conanfile.go b/syft/pkg/cataloger/cpp/parse_conanfile.go new file mode 100644 index 000000000..d3a6481bd --- /dev/null +++ b/syft/pkg/cataloger/cpp/parse_conanfile.go @@ -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, + }, + }) + } +} diff --git a/syft/pkg/cataloger/cpp/parse_conanfile_test.go b/syft/pkg/cataloger/cpp/parse_conanfile_test.go new file mode 100644 index 000000000..bdcb0322b --- /dev/null +++ b/syft/pkg/cataloger/cpp/parse_conanfile_test.go @@ -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) + } +} diff --git a/syft/pkg/cataloger/cpp/test-fixtures/conanfile.txt b/syft/pkg/cataloger/cpp/test-fixtures/conanfile.txt new file mode 100644 index 000000000..c265b1328 --- /dev/null +++ b/syft/pkg/cataloger/cpp/test-fixtures/conanfile.txt @@ -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 diff --git a/syft/pkg/conan_metadata.go b/syft/pkg/conan_metadata.go new file mode 100644 index 000000000..9717e8f3d --- /dev/null +++ b/syft/pkg/conan_metadata.go @@ -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() +} diff --git a/syft/pkg/language.go b/syft/pkg/language.go index 3b1150554..297d09adc 100644 --- a/syft/pkg/language.go +++ b/syft/pkg/language.go @@ -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 } diff --git a/syft/pkg/language_test.go b/syft/pkg/language_test.go index 49ddf1364..af9376a2e 100644 --- a/syft/pkg/language_test.go +++ b/syft/pkg/language_test.go @@ -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 { diff --git a/syft/pkg/metadata.go b/syft/pkg/metadata.go index 11a383210..1b0349ae0 100644 --- a/syft/pkg/metadata.go +++ b/syft/pkg/metadata.go @@ -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{}), } diff --git a/syft/pkg/type.go b/syft/pkg/type.go index 531c3d25a..72b0ad2df 100644 --- a/syft/pkg/type.go +++ b/syft/pkg/type.go @@ -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 } diff --git a/syft/pkg/type_test.go b/syft/pkg/type_test.go index 87a8151a5..dde84285f 100644 --- a/syft/pkg/type_test.go +++ b/syft/pkg/type_test.go @@ -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 diff --git a/syft/pkg/url_test.go b/syft/pkg/url_test.go index 3282b73f6..93b311733 100644 --- a/syft/pkg/url_test.go +++ b/syft/pkg/url_test.go @@ -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 diff --git a/test/integration/catalog_packages_cases_test.go b/test/integration/catalog_packages_cases_test.go index 74e0e64a0..437253ebb 100644 --- a/test/integration/catalog_packages_cases_test.go +++ b/test/integration/catalog_packages_cases_test.go @@ -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, diff --git a/test/integration/catalog_packages_test.go b/test/integration/catalog_packages_test.go index 583743086..8b02136b4 100644 --- a/test/integration/catalog_packages_test.go +++ b/test/integration/catalog_packages_test.go @@ -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...) diff --git a/test/integration/test-fixtures/image-pkg-coverage/conan/conanfile.txt b/test/integration/test-fixtures/image-pkg-coverage/conan/conanfile.txt new file mode 100644 index 000000000..c265b1328 --- /dev/null +++ b/test/integration/test-fixtures/image-pkg-coverage/conan/conanfile.txt @@ -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