Skip to content

Commit

Permalink
feat: add conaninfo.txt parser to detect conan packages in docker ima…
Browse files Browse the repository at this point in the history
…ges (anchore#2234)

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

Signed-off-by: Stefan Profanter <[email protected]>

* fix: add NewConanInfoCataloger as a separate cataloger

Signed-off-by: Stefan Profanter <[email protected]>

---------

Signed-off-by: Stefan Profanter <[email protected]>
  • Loading branch information
Pro authored Oct 23, 2023
1 parent 77a92fe commit 7c70b2c
Show file tree
Hide file tree
Showing 8 changed files with 432 additions and 0 deletions.
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

0 comments on commit 7c70b2c

Please sign in to comment.