diff --git a/docs/docs/coverage/language/c.md b/docs/docs/coverage/language/c.md index 0ae219d8370b..276340a806bd 100644 --- a/docs/docs/coverage/language/c.md +++ b/docs/docs/coverage/language/c.md @@ -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. @@ -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). \ No newline at end of file +[^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 diff --git a/pkg/dependency/parser/c/conan/parse.go b/pkg/dependency/parser/c/conan/parse.go index f5417afd007f..06c583b47282 100644 --- a/pkg/dependency/parser/c/conan/parse.go +++ b/pkg/dependency/parser/c/conan/parse.go @@ -18,6 +18,7 @@ import ( type LockFile struct { GraphLock GraphLock `json:"graph_lock"` + Requires Requires `json:"requires"` } type GraphLock struct { @@ -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 } @@ -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 @@ -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 @@ -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 { @@ -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 @@ -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 +} diff --git a/pkg/dependency/parser/c/conan/parse_test.go b/pkg/dependency/parser/c/conan/parse_test.go index 7502fde76271..abbf9f921a18 100644 --- a/pkg/dependency/parser/c/conan/parse_test.go +++ b/pkg/dependency/parser/c/conan/parse_test.go @@ -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", @@ -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", @@ -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", }, } diff --git a/pkg/dependency/parser/c/conan/testdata/empty.lock b/pkg/dependency/parser/c/conan/testdata/empty_v1.lock similarity index 100% rename from pkg/dependency/parser/c/conan/testdata/empty.lock rename to pkg/dependency/parser/c/conan/testdata/empty_v1.lock diff --git a/pkg/dependency/parser/c/conan/testdata/happy.lock b/pkg/dependency/parser/c/conan/testdata/happy_v1_case1.lock similarity index 100% rename from pkg/dependency/parser/c/conan/testdata/happy.lock rename to pkg/dependency/parser/c/conan/testdata/happy_v1_case1.lock diff --git a/pkg/dependency/parser/c/conan/testdata/happy2.lock b/pkg/dependency/parser/c/conan/testdata/happy_v1_case2.lock similarity index 100% rename from pkg/dependency/parser/c/conan/testdata/happy2.lock rename to pkg/dependency/parser/c/conan/testdata/happy_v1_case2.lock diff --git a/pkg/dependency/parser/c/conan/testdata/happy_v2.lock b/pkg/dependency/parser/c/conan/testdata/happy_v2.lock new file mode 100644 index 000000000000..f4103baae1d0 --- /dev/null +++ b/pkg/dependency/parser/c/conan/testdata/happy_v2.lock @@ -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": [] +} \ No newline at end of file diff --git a/pkg/dependency/parser/c/conan/testdata/sad.lock b/pkg/dependency/parser/c/conan/testdata/sad_v1.lock similarity index 100% rename from pkg/dependency/parser/c/conan/testdata/sad.lock rename to pkg/dependency/parser/c/conan/testdata/sad_v1.lock