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(vuln): Handle scanning conan v2.x lockfiles #6357

Merged
merged 3 commits into from
Apr 29, 2024
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
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 @@ -17,6 +17,7 @@ import (

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

type GraphLock struct {
Expand All @@ -30,6 +31,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 @@ -40,17 +49,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 @@ -62,7 +63,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 @@ -76,8 +77,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 @@ -102,25 +101,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 @@ -136,3 +180,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 @@ -70,7 +70,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 @@ -105,13 +105,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": []
}