Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add conaninfo.txt parser to detect conan packages in docker images #2234

Merged
merged 2 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions syft/pkg/cataloger/cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func ImageCatalogers(cfg Config) []pkg.Cataloger {
alpm.NewAlpmdbCataloger(),
apkdb.NewApkdbCataloger(),
binary.NewCataloger(),
cpp.NewConanInfoCataloger(),
deb.NewDpkgdbCataloger(),
dotnet.NewDotnetPortableExecutableCataloger(),
golang.NewGoModuleBinaryCataloger(cfg.Golang),
Expand Down
8 changes: 8 additions & 0 deletions syft/pkg/cataloger/cpp/cataloger.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,11 @@ func NewConanCataloger() *generic.Cataloger {
WithParserByGlobs(parseConanfile, "**/conanfile.txt").
WithParserByGlobs(parseConanlock, "**/conan.lock")
}

const catalogerNameInfo = "conan-info-cataloger"

// NewConanInfoCataloger returns a new C++ conaninfo.txt cataloger object.
func NewConanInfoCataloger() *generic.Cataloger {
return generic.NewCataloger(catalogerNameInfo).
WithParserByGlobs(parseConaninfo, "**/conaninfo.txt")
}
25 changes: 25 additions & 0 deletions syft/pkg/cataloger/cpp/cataloger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,28 @@ func TestCataloger_Globs(t *testing.T) {
})
}
}

func TestCatalogerInfo_Globs(t *testing.T) {
tests := []struct {
name string
fixture string
expected []string
}{
{
name: "obtain conan files",
fixture: "test-fixtures/glob-paths",
expected: []string{
"somewhere/src/conaninfo.txt",
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
pkgtest.NewCatalogTester().
FromDirectory(t, test.fixture).
ExpectsResolverContentQueries(test.expected).
TestCataloger(t, NewConanInfoCataloger())
})
}
}
8 changes: 8 additions & 0 deletions syft/pkg/cataloger/cpp/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type conanRef struct {
User string
Channel string
Revision string
PackageID string
Timestamp string
}

Expand All @@ -32,6 +33,13 @@ func splitConanRef(ref string) *conanRef {
cref.Timestamp = tokens[1]
}

// package_id
tokens = strings.Split(text, ":")
text = tokens[0]
if len(tokens) == 2 {
cref.PackageID = tokens[1]
}

// revision
tokens = strings.Split(text, "#")
ref = tokens[0]
Expand Down
141 changes: 141 additions & 0 deletions syft/pkg/cataloger/cpp/parse_conaninfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package cpp

import (
"bufio"
"errors"
"fmt"
"io"
"regexp"
"strings"

"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/generic"
)

var _ generic.Parser = parseConaninfo

func parseConanMetadataFromFilePath(path string) (pkg.ConanLockMetadata, error) {
// fullFilePath = str(reader.Location.VirtualPath)
// Split the full patch into the folders we expect. I.e.:
// $HOME/.conan/data/<pkg-name>/<pkg-version>/<user>/<channel>/package/<package_id>/conaninfo.txt
re := regexp.MustCompile(`.*[/\\](?P<name>[^/\\]+)[/\\](?P<version>[^/\\]+)[/\\](?P<user>[^/\\]+)[/\\](?P<channel>[^/\\]+)[/\\]package[/\\](?P<id>[^/\\]+)[/\\]conaninfo\.txt`)
matches := re.FindStringSubmatch(path)
if len(matches) != 6 {
return pkg.ConanLockMetadata{}, fmt.Errorf("failed to get parent package info from conaninfo file path")
}
mainPackageRef := fmt.Sprintf("%s/%s@%s/%s", matches[1], matches[2], matches[3], matches[4])
return pkg.ConanLockMetadata{
Ref: mainPackageRef,
PackageID: matches[5],
}, nil
}

func getRelationships(pkgs []pkg.Package, mainPackageRef pkg.Package) []artifact.Relationship {
var relationships []artifact.Relationship
for _, p := range pkgs {
// this is a pkg that package "main_package" depends on... make a relationship
relationships = append(relationships, artifact.Relationship{
From: p,
To: mainPackageRef,
Type: artifact.DependencyOfRelationship,
})
}
return relationships
}

func parseFullRequiresLine(line string, reader file.LocationReadCloser, pkgs *[]pkg.Package) {
if len(line) == 0 {
return
}

cref := splitConanRef(line)

meta := pkg.ConanLockMetadata{
Ref: line,
PackageID: cref.PackageID,
}

p := newConanlockPackage(
meta,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)
if p != nil {
*pkgs = append(*pkgs, *p)
}
}

// parseConaninfo is a parser function for conaninfo.txt contents, returning all packages discovered.
// The conaninfo.txt file is typically present for an installed conan package under:
// $HOME/.conan/data/<pkg-name>/<pkg-version>/<user>/<channel>/package/<package_id>/conaninfo.txt
// Based on the relative path we can get:
// - package name
// - package version
// - package id
// - user
// - channel
// The conaninfo.txt gives:
// - package requires (full_requires)
// - recipe revision (recipe_hash)
func parseConaninfo(_ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
// First set the base package info by checking the relative path
fullFilePath := string(reader.Location.LocationData.Reference().RealPath)
if len(fullFilePath) == 0 {
fullFilePath = reader.Location.LocationData.RealPath
}

mainMetadata, err := parseConanMetadataFromFilePath(fullFilePath)
if err != nil {
return nil, nil, err
}

r := bufio.NewReader(reader)
inRequirements := false
inRecipeHash := false
var pkgs []pkg.Package

for {
line, err := r.ReadString('\n')
switch {
case errors.Is(io.EOF, err):
mainPackage := newConanlockPackage(
mainMetadata,
reader.Location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation),
)

mainPackageRef := *mainPackage
relationships := getRelationships(pkgs, mainPackageRef)

pkgs = append(pkgs, mainPackageRef)

return pkgs, relationships, nil
case err != nil:
return nil, nil, fmt.Errorf("failed to parse conaninfo.txt file: %w", err)
}

switch {
case strings.Contains(line, "[full_requires]"):
inRequirements = true
inRecipeHash = false
continue
case strings.Contains(line, "[recipe_hash]"):
inRequirements = false
inRecipeHash = true
continue
case strings.ContainsAny(line, "[]") || strings.HasPrefix(strings.TrimSpace(line), "#"):
inRequirements = false
inRecipeHash = false
continue
}

if inRequirements {
parseFullRequiresLine(strings.Trim(line, "\n "), reader, &pkgs)
}
if inRecipeHash {
// add recipe hash to the metadata ref
mainMetadata.Ref = mainMetadata.Ref + "#" + strings.Trim(line, "\n ")
inRecipeHash = false
}
}
}
134 changes: 134 additions & 0 deletions syft/pkg/cataloger/cpp/parse_conaninfo_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package cpp

import (
"testing"

"github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
)

func TestParseConaninfo(t *testing.T) {
fixture := "test-fixtures/conaninfo/mfast/1.2.2/my_user/my_channel/package/9d1f076b471417647c2022a78d5e2c1f834289ac/conaninfo.txt"
expected := []pkg.Package{
{
Name: "mfast",
Version: "1.2.2",
PURL: "pkg:conan/my_user/[email protected]?channel=my_channel",
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
Metadata: pkg.ConanLockMetadata{
Ref: "mfast/1.2.2@my_user/my_channel#c6f6387c9b99780f0ee05e25f99d0f39",
PackageID: "9d1f076b471417647c2022a78d5e2c1f834289ac",
},
},
{
Name: "boost",
Version: "1.75.0",
PURL: "pkg:conan/[email protected]",
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
Metadata: pkg.ConanLockMetadata{
Ref: "boost/1.75.0:dc8aedd23a0f0a773a5fcdcfe1ae3e89c4205978",
PackageID: "dc8aedd23a0f0a773a5fcdcfe1ae3e89c4205978",
},
},
{
Name: "zlib",
Version: "1.2.13",
PURL: "pkg:conan/[email protected]",
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
Metadata: pkg.ConanLockMetadata{
Ref: "zlib/1.2.13:dfbe50feef7f3c6223a476cd5aeadb687084a646",
PackageID: "dfbe50feef7f3c6223a476cd5aeadb687084a646",
},
},
{
Name: "bzip2",
Version: "1.0.8",
PURL: "pkg:conan/[email protected]",
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
Metadata: pkg.ConanLockMetadata{
Ref: "bzip2/1.0.8:c32092bf4d4bb47cf962af898e02823f499b017e",
PackageID: "c32092bf4d4bb47cf962af898e02823f499b017e",
},
},
{
Name: "libbacktrace",
Version: "cci.20210118",
PURL: "pkg:conan/[email protected]",
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
Metadata: pkg.ConanLockMetadata{
Ref: "libbacktrace/cci.20210118:dfbe50feef7f3c6223a476cd5aeadb687084a646",
PackageID: "dfbe50feef7f3c6223a476cd5aeadb687084a646",
},
},
{
Name: "tinyxml2",
Version: "9.0.0",
PURL: "pkg:conan/[email protected]",
Locations: file.NewLocationSet(file.NewLocation(fixture)),
Language: pkg.CPP,
Type: pkg.ConanPkg,
MetadataType: pkg.ConanLockMetadataType,
Metadata: pkg.ConanLockMetadata{
Ref: "tinyxml2/9.0.0:6557f18ca99c0b6a233f43db00e30efaa525e27e",
PackageID: "6557f18ca99c0b6a233f43db00e30efaa525e27e",
},
},
}

// relationships require IDs to be set to be sorted similarly
for i := range expected {
expected[i].SetID()
}

var expectedRelationships = []artifact.Relationship{
{
From: expected[1], // boost
To: expected[0], // mfast
Type: artifact.DependencyOfRelationship,
Data: nil,
},
{
From: expected[5], // tinyxml2
To: expected[0], // mfast
Type: artifact.DependencyOfRelationship,
Data: nil,
},
{
From: expected[2], // zlib
To: expected[0], // mfast
Type: artifact.DependencyOfRelationship,
Data: nil,
},
{
From: expected[3], // bzip2
To: expected[0], // mfast
Type: artifact.DependencyOfRelationship,
Data: nil,
},
{
From: expected[4], // libbacktrace
To: expected[0], // mfast
Type: artifact.DependencyOfRelationship,
Data: nil,
},
}

pkgtest.TestFileParser(t, fixture, parseConaninfo, expected, expectedRelationships)
}
Loading
Loading