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 ### Supported Ecosystems
- Alpine (apk) - Alpine (apk)
- C (conan)
- C++ (conan)
- Dart (pubs) - Dart (pubs)
- Debian (dpkg) - Debian (dpkg)
- Dotnet (deps.json) - Dotnet (deps.json)

View File

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

View File

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

View File

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

@ -13,6 +13,7 @@ import (
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/alpm" "github.com/anchore/syft/syft/pkg/cataloger/alpm"
"github.com/anchore/syft/syft/pkg/cataloger/apkdb" "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/dart"
"github.com/anchore/syft/syft/pkg/cataloger/deb" "github.com/anchore/syft/syft/pkg/cataloger/deb"
"github.com/anchore/syft/syft/pkg/cataloger/dotnet" "github.com/anchore/syft/syft/pkg/cataloger/dotnet"
@ -75,6 +76,7 @@ func DirectoryCatalogers(cfg Config) []Cataloger {
rust.NewCargoLockCataloger(), rust.NewCargoLockCataloger(),
dart.NewPubspecLockCataloger(), dart.NewPubspecLockCataloger(),
dotnet.NewDotnetDepsCataloger(), dotnet.NewDotnetDepsCataloger(),
cpp.NewConanfileCataloger(),
}, cfg.Catalogers) }, cfg.Catalogers)
} }
@ -100,6 +102,7 @@ func AllCatalogers(cfg Config) []Cataloger {
dotnet.NewDotnetDepsCataloger(), dotnet.NewDotnetDepsCataloger(),
php.NewPHPComposerInstalledCataloger(), php.NewPHPComposerInstalledCataloger(),
php.NewPHPComposerLockCataloger(), php.NewPHPComposerLockCataloger(),
cpp.NewConanfileCataloger(),
}, cfg.Catalogers) }, 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" Rust Language = "rust"
Dart Language = "dart" Dart Language = "dart"
Dotnet Language = "dotnet" Dotnet Language = "dotnet"
CPP Language = "c++"
) )
// AllLanguages is a set of all programming languages detected by syft. // AllLanguages is a set of all programming languages detected by syft.
@ -34,6 +35,7 @@ var AllLanguages = []Language{
Rust, Rust,
Dart, Dart,
Dotnet, Dotnet,
CPP,
} }
// String returns the string representation of the language. // String returns the string representation of the language.
@ -70,6 +72,8 @@ func LanguageByName(name string) Language {
return Dart return Dart
case packageurl.TypeDotnet: case packageurl.TypeDotnet:
return Dotnet return Dotnet
case packageurl.TypeConan, string(CPP):
return CPP
default: default:
return UnknownLanguage 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", purl: "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?type=zip&classifier=dist",
want: Java, want: Java,
}, },
{
purl: "pkg:conan/catch2@2.13.8",
want: CPP,
},
} }
var languages []string var languages []string
@ -183,6 +187,14 @@ func TestLanguageByName(t *testing.T) {
name: "unknown", name: "unknown",
language: UnknownLanguage, language: UnknownLanguage,
}, },
{
name: "conan",
language: CPP,
},
{
name: "c++",
language: CPP,
},
} }
for _, test := range tests { for _, test := range tests {

View File

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

View File

@ -23,6 +23,7 @@ const (
KbPkg Type = "msrc-kb" KbPkg Type = "msrc-kb"
DartPubPkg Type = "dart-pub" DartPubPkg Type = "dart-pub"
DotnetPkg Type = "dotnet" DotnetPkg Type = "dotnet"
ConanPkg Type = "conan"
) )
// AllPkgs represents all supported package types // AllPkgs represents all supported package types
@ -42,6 +43,7 @@ var AllPkgs = []Type{
KbPkg, KbPkg,
DartPubPkg, DartPubPkg,
DotnetPkg, DotnetPkg,
ConanPkg,
} }
// PackageURLType returns the PURL package type for the current package. // PackageURLType returns the PURL package type for the current package.
@ -73,6 +75,8 @@ func (t Type) PackageURLType() string {
return packageurl.TypePub return packageurl.TypePub
case DotnetPkg: case DotnetPkg:
return packageurl.TypeDotnet return packageurl.TypeDotnet
case ConanPkg:
return packageurl.TypeConan
default: default:
// TODO: should this be a "generic" purl type instead? // TODO: should this be a "generic" purl type instead?
return "" return ""
@ -116,6 +120,8 @@ func TypeByName(name string) Type {
return DartPubPkg return DartPubPkg
case packageurl.TypeDotnet: case packageurl.TypeDotnet:
return DotnetPkg return DotnetPkg
case packageurl.TypeConan:
return ConanPkg
default: default:
return UnknownPkg 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", purl: "pkg:alpm/arch/linux@5.10.0?arch=x86_64&distro=arch",
expected: AlpmPkg, expected: AlpmPkg,
}, },
{
purl: "pkg:conan/catch2@2.13.8",
expected: ConanPkg,
},
} }
var pkgTypes []string 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", 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 var pkgTypes []string

View File

@ -167,6 +167,19 @@ var dirOnlyTestCases = []testCase{
"github.com/bmatcuk/doublestar": "v1.3.1", "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", name: "find rust crates",
pkgType: pkg.RustPkg, pkgType: pkg.RustPkg,
@ -264,7 +277,6 @@ var commonTestCases = []testCase{
"netbase": "5.4", "netbase": "5.4",
}, },
}, },
{ {
name: "find jenkins plugins", name: "find jenkins plugins",
pkgType: pkg.JenkinsPluginPkg, pkgType: pkg.JenkinsPluginPkg,

View File

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