Skip to content

Commit

Permalink
feat(vuln): Handle scanning conan v2.x lockfiles (aquasecurity#6357)
Browse files Browse the repository at this point in the history
Co-authored-by: Teppei Fukuda <knqyf263@gmail.com>
  • Loading branch information
dus7eh and knqyf263 authored Apr 29, 2024
1 parent e3bef02 commit 29b8faf
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 32 deletions.
16 changes: 10 additions & 6 deletions docs/docs/coverage/language/c.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# C/C++

Trivy supports [Conan][conan] C/C++ Package Manager.
Trivy supports Conan C/C++ Package Manager ([v1][conanV1] and [v2][conanV2] with limitations).

The following scanners are supported.

Expand All @@ -10,21 +10,25 @@ The following scanners are supported.

The following table provides an outline of the features Trivy offers.

| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position |
|-----------------|----------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|
| Conan | conan.lock[^2] || Excluded |||
| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position |
|-----------------------|----------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|
| Conan (lockfile v1) | conan.lock[^2] || Excluded |||
| Conan (lockfile v2) | conan.lock[^2] |[^3] | Excluded | - ||

## Conan
In order to detect dependencies, Trivy searches for `conan.lock`[^1].

[conanV1]: https://docs.conan.io/1/index.html
[conanV2]: https://docs.conan.io/2/

### Licenses
The Conan lock file doesn't contain any license information.
To obtain licenses we parse the `conanfile.py` files from the [conan cache directory][conan-cache-dir].
To correctly detection licenses, ensure that the cache directory contains all dependencies used.

[conan]: https://docs.conan.io/1/index.html
[conan-cache-dir]: https://docs.conan.io/1/mastering/custom_cache.html
[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies

[^1]: The local cache should contain the dependencies used. See [licenses](#licenses).
[^2]: `conan.lock` is default name. To scan a custom filename use [file-patterns](../../configuration/skipping.md#file-patterns).
[^2]: `conan.lock` is default name. To scan a custom filename use [file-patterns](../../configuration/skipping.md#file-patterns).
[^3]: For `conan.lock` in version 2, indirect dependencies are included in analysis but not flagged explicitly in dependency tree
99 changes: 77 additions & 22 deletions pkg/dependency/parser/c/conan/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

type LockFile struct {
GraphLock GraphLock `json:"graph_lock"`
Requires Requires `json:"requires"`
}

type GraphLock struct {
Expand All @@ -31,6 +32,14 @@ type Node struct {
EndLine int
}

type Require struct {
Dependency string
StartLine int
EndLine int
}

type Requires []Require

type Parser struct {
logger *log.Logger
}
Expand All @@ -41,17 +50,9 @@ func NewParser() types.Parser {
}
}

func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, error) {
var lock LockFile
input, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("failed to read canon lock file: %w", err)
}
if err := jfather.Unmarshal(input, &lock); err != nil {
return nil, nil, xerrors.Errorf("failed to decode canon lock file: %w", err)
}

// Get a list of direct dependencies
func (p *Parser) parseV1(lock LockFile) ([]types.Library, []types.Dependency, error) {
var libs []types.Library
var deps []types.Dependency
var directDeps []string
if root, ok := lock.GraphLock.Nodes["0"]; ok {
directDeps = root.Requires
Expand All @@ -63,7 +64,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency,
if node.Ref == "" {
continue
}
lib, err := parseRef(node)
lib, err := toLibrary(node.Ref, node.StartLine, node.EndLine)
if err != nil {
p.logger.Debug("Parse ref error", log.Err(err))
continue
Expand All @@ -77,8 +78,6 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency,
}

// Parse dependency graph
var libs []types.Library
var deps []types.Dependency
for i, node := range lock.GraphLock.Nodes {
lib, ok := parsed[i]
if !ok {
Expand All @@ -103,25 +102,70 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency,
return libs, deps, nil
}

func parseRef(node Node) (types.Library, error) {
func (p *Parser) parseV2(lock LockFile) ([]types.Library, []types.Dependency, error) {
var libs []types.Library

for _, req := range lock.Requires {
lib, err := toLibrary(req.Dependency, req.StartLine, req.EndLine)
if err != nil {
p.logger.Debug("Creating library entry from requirement failed", err)
continue
}

libs = append(libs, lib)
}
return libs, []types.Dependency{}, nil
}

func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, error) {
var lock LockFile

input, err := io.ReadAll(r)
if err != nil {
return nil, nil, xerrors.Errorf("failed to read conan lock file: %w", err)
}
if err := jfather.Unmarshal(input, &lock); err != nil {
return nil, nil, xerrors.Errorf("failed to decode conan lock file: %w", err)
}

// try to parse requirements as conan v1.x
if lock.GraphLock.Nodes != nil {
p.logger.Debug("Handling conan lockfile as v1.x")
return p.parseV1(lock)
} else {
// try to parse requirements as conan v2.x
p.logger.Debug("Handling conan lockfile as v2.x")
return p.parseV2(lock)
}
}

func parsePackage(text string) (string, string, error) {
// full ref format: package/version@user/channel#rrev:package_id#prev
// various examples:
// 'pkga/0.1@user/testing'
// 'pkgb/0.1.0'
// 'pkgc/system'
// 'pkgd/0.1.0#7dcb50c43a5a50d984c2e8fa5898bf18'
ss := strings.Split(strings.Split(strings.Split(node.Ref, "@")[0], "#")[0], "/")
ss := strings.Split(strings.Split(strings.Split(text, "@")[0], "#")[0], "/")
if len(ss) != 2 {
return types.Library{}, xerrors.Errorf("Unable to determine conan dependency: %q", node.Ref)
return "", "", xerrors.Errorf("Unable to determine conan dependency: %q", text)
}
return ss[0], ss[1], nil
}

func toLibrary(pkg string, startLine, endLine int) (types.Library, error) {
name, version, err := parsePackage(pkg)
if err != nil {
return types.Library{}, err
}
return types.Library{
ID: dependency.ID(ftypes.Conan, ss[0], ss[1]),
Name: ss[0],
Version: ss[1],
ID: dependency.ID(ftypes.Conan, name, version),
Name: name,
Version: version,
Locations: []types.Location{
{
StartLine: node.StartLine,
EndLine: node.EndLine,
StartLine: startLine,
EndLine: endLine,
},
},
}, nil
Expand All @@ -137,3 +181,14 @@ func (n *Node) UnmarshalJSONWithMetadata(node jfather.Node) error {
n.EndLine = node.Range().End.Line
return nil
}

func (r *Require) UnmarshalJSONWithMetadata(node jfather.Node) error {
var dep string
if err := node.Decode(&dep); err != nil {
return err
}
r.Dependency = dep
r.StartLine = node.Range().Start.Line
r.EndLine = node.Range().End.Line
return nil
}
37 changes: 33 additions & 4 deletions pkg/dependency/parser/c/conan/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestParse(t *testing.T) {
}{
{
name: "happy path",
inputFile: "testdata/happy.lock",
inputFile: "testdata/happy_v1_case1.lock",
wantLibs: []types.Library{
{
ID: "pkga/0.0.1",
Expand Down Expand Up @@ -72,7 +72,7 @@ func TestParse(t *testing.T) {
},
{
name: "happy path. lock file with revisions support",
inputFile: "testdata/happy2.lock",
inputFile: "testdata/happy_v1_case2.lock",
wantLibs: []types.Library{
{
ID: "openssl/3.0.3",
Expand Down Expand Up @@ -108,13 +108,42 @@ func TestParse(t *testing.T) {
},
},
},
{
name: "happy path conan v2",
inputFile: "testdata/happy_v2.lock",
wantLibs: []types.Library{
{
ID: "matrix/1.3",
Name: "matrix",
Version: "1.3",
Locations: []types.Location{
{
StartLine: 5,
EndLine: 5,
},
},
},
{
ID: "sound32/1.0",
Name: "sound32",
Version: "1.0",
Locations: []types.Location{
{
StartLine: 4,
EndLine: 4,
},
},
},
},
wantDeps: []types.Dependency{},
},
{
name: "happy path. lock file without dependencies",
inputFile: "testdata/empty.lock",
inputFile: "testdata/empty_v1.lock",
},
{
name: "sad path. wrong ref format",
inputFile: "testdata/sad.lock",
inputFile: "testdata/sad_v1.lock",
},
}

Expand Down
12 changes: 12 additions & 0 deletions pkg/dependency/parser/c/conan/testdata/happy_v2.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"version": "0.5",
"requires": [
"sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488",
"matrix/1.3#905c3f0babc520684c84127378fefdd0%1675278900.0103245"
],
"build_requires": [
"automake/1.16.5#058bda3e21c36c9aa8425daf3c1faf50%1701120593.68",
"autoconf/2.71#00a1e46d8ba5baaf7f10d64c1a6a0342%1709043523.063"
],
"python_requires": []
}

0 comments on commit 29b8faf

Please sign in to comment.