From 7dae00b6c1ff4a062163671a58335e1d01a42cd9 Mon Sep 17 00:00:00 2001 From: Damian Ellwart Date: Wed, 20 Mar 2024 11:53:16 +0100 Subject: [PATCH 1/3] feat(vuln): handle scanning conan v2.x lockfiles --- pkg/dependency/parser/c/conan/parse.go | 92 ++++++++++++++----- pkg/dependency/parser/c/conan/parse_test.go | 25 ++++- .../testdata/{empty.lock => empty_v1.lock} | 0 .../{happy.lock => happy_v1_case1.lock} | 0 .../{happy2.lock => happy_v1_case2.lock} | 0 .../parser/c/conan/testdata/happy_v2.lock | 12 +++ .../conan/testdata/{sad.lock => sad_v1.lock} | 0 7 files changed, 104 insertions(+), 25 deletions(-) rename pkg/dependency/parser/c/conan/testdata/{empty.lock => empty_v1.lock} (100%) rename pkg/dependency/parser/c/conan/testdata/{happy.lock => happy_v1_case1.lock} (100%) rename pkg/dependency/parser/c/conan/testdata/{happy2.lock => happy_v1_case2.lock} (100%) create mode 100644 pkg/dependency/parser/c/conan/testdata/happy_v2.lock rename pkg/dependency/parser/c/conan/testdata/{sad.lock => sad_v1.lock} (100%) diff --git a/pkg/dependency/parser/c/conan/parse.go b/pkg/dependency/parser/c/conan/parse.go index 2020377a4663..2ed9b53dda76 100644 --- a/pkg/dependency/parser/c/conan/parse.go +++ b/pkg/dependency/parser/c/conan/parse.go @@ -15,7 +15,7 @@ import ( xio "github.com/aquasecurity/trivy/pkg/x/io" ) -type LockFile struct { +type LockFileV1 struct { GraphLock GraphLock `json:"graph_lock"` } @@ -30,27 +30,24 @@ type Node struct { EndLine int } +type LockFileV2 struct { + Requires []string `json:"requires"` +} + type Parser struct { logger *log.Logger } + func NewParser() types.Parser { return &Parser{ logger: log.WithPrefix("conan"), } } -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) parseRequirementsV1(lock LockFileV1) ([]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 @@ -62,7 +59,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, if node.Ref == "" { continue } - lib, err := parseRef(node) + lib, err := parseRefV1(node) if err != nil { p.logger.Debug("Parse ref error", log.Err(err)) continue @@ -76,8 +73,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 { @@ -102,21 +97,64 @@ 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) parseRequirementsV2(lock LockFileV2) ([]types.Library, []types.Dependency, error) { + var libs []types.Library + + for _, req := range lock.Requires { + lib, _ := parseRefV2(req) + libs = append(libs, lib) + } + return libs, []types.Dependency{}, nil +} + +func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, error) { + var lockV1 LockFileV1 + var lockV2 LockFileV2 + input, err := io.ReadAll(r) + if err != nil { + return nil, nil, xerrors.Errorf("failed to read conan lock file: %w", err) + } + + // try to parse requirements as conan v1.x + if err := jfather.Unmarshal(input, &lockV1); err != nil { + return nil, nil, xerrors.Errorf("failed to decode conan lock file: %w", err) + } + if lockV1.GraphLock.Nodes != nil { + log.Logger.Debug("Handling conan lockfile as v1.x") + return p.parseRequirementsV1(lockV1) + } else { + // try to parse requirements as conan v2.x + log.Logger.Debug("Handling conan lockfile as v2.x") + if err := jfather.Unmarshal(input, &lockV2); err != nil { + return nil, nil, xerrors.Errorf("failed to decode conan lock file: %w", err) + } + return p.parseRequirementsV2(lockV2) + } +} + +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 parseRefV1(node Node) (types.Library, error) { + name, version, err := parsePackage(node.Ref) + 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, @@ -126,6 +164,18 @@ func parseRef(node Node) (types.Library, error) { }, nil } +func parseRefV2(req string) (types.Library, error) { + name, version, err := parsePackage(req) + if err != nil { + return types.Library{}, err + } + return types.Library{ + ID: dependency.ID(ftypes.Conan, name, version), + Name: name, + Version: version, + }, nil +} + // UnmarshalJSONWithMetadata needed to detect start and end lines of deps func (n *Node) UnmarshalJSONWithMetadata(node jfather.Node) error { if err := node.Decode(&n); err != nil { diff --git a/pkg/dependency/parser/c/conan/parse_test.go b/pkg/dependency/parser/c/conan/parse_test.go index a22ec90afc1b..7a5af31c8ca6 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", @@ -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", @@ -105,13 +105,30 @@ 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", + }, + { + ID: "sound32/1.0", + Name: "sound32", + Version: "1.0", + }, + }, + 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 From a4c2085e342bfa132737a114637245ef2f3d0027 Mon Sep 17 00:00:00 2001 From: Damian Ellwart Date: Thu, 4 Apr 2024 00:22:06 +0200 Subject: [PATCH 2/3] chore: refactor and apply review fixes --- docs/docs/coverage/language/c.md | 11 +-- pkg/dependency/parser/c/conan/parse.go | 74 +++++++++++---------- pkg/dependency/parser/c/conan/parse_test.go | 12 ++++ 3 files changed, 59 insertions(+), 38 deletions(-) diff --git a/docs/docs/coverage/language/c.md b/docs/docs/coverage/language/c.md index 0ae219d8370b..811aa2398f95 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. @@ -12,19 +12,22 @@ 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 | ✓ | ✓ | +| Conan | conan.lock[^1] | ✓ [^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 2ed9b53dda76..6f5c973d9a02 100644 --- a/pkg/dependency/parser/c/conan/parse.go +++ b/pkg/dependency/parser/c/conan/parse.go @@ -15,8 +15,9 @@ import ( xio "github.com/aquasecurity/trivy/pkg/x/io" ) -type LockFileV1 struct { +type LockFile struct { GraphLock GraphLock `json:"graph_lock"` + Requires Requires `json:"requires"` } type GraphLock struct { @@ -30,10 +31,14 @@ type Node struct { EndLine int } -type LockFileV2 struct { - Requires []string `json:"requires"` +type Require struct { + Dependency string + StartLine int + EndLine int } +type Requires []Require + type Parser struct { logger *log.Logger } @@ -45,7 +50,7 @@ func NewParser() types.Parser { } } -func (p *Parser) parseRequirementsV1(lock LockFileV1) ([]types.Library, []types.Dependency, error) { +func (p *Parser) parseV1(lock LockFile) ([]types.Library, []types.Dependency, error) { var libs []types.Library var deps []types.Dependency var directDeps []string @@ -59,7 +64,7 @@ func (p *Parser) parseRequirementsV1(lock LockFileV1) ([]types.Library, []types. if node.Ref == "" { continue } - lib, err := parseRefV1(node) + lib, err := toLibrary(node.Ref, node.StartLine, node.EndLine) if err != nil { p.logger.Debug("Parse ref error", log.Err(err)) continue @@ -97,38 +102,40 @@ func (p *Parser) parseRequirementsV1(lock LockFileV1) ([]types.Library, []types. return libs, deps, nil } -func (p *Parser) parseRequirementsV2(lock LockFileV2) ([]types.Library, []types.Dependency, error) { +func (p *Parser) parseV2(lock LockFile) ([]types.Library, []types.Dependency, error) { var libs []types.Library for _, req := range lock.Requires { - lib, _ := parseRefV2(req) + lib, err := toLibrary(req.Dependency, req.StartLine, req.EndLine) + if err != nil { + log.Logger.Debug(err) + continue + } + libs = append(libs, lib) } return libs, []types.Dependency{}, nil } func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, error) { - var lockV1 LockFileV1 - var lockV2 LockFileV2 + var lock LockFile + input, err := io.ReadAll(r) if err != nil { return nil, nil, xerrors.Errorf("failed to read conan lock file: %w", err) } - - // try to parse requirements as conan v1.x - if err := jfather.Unmarshal(input, &lockV1); err != nil { + if err := jfather.Unmarshal(input, &lock); err != nil { return nil, nil, xerrors.Errorf("failed to decode conan lock file: %w", err) } - if lockV1.GraphLock.Nodes != nil { + + // try to parse requirements as conan v1.x + if lock.GraphLock.Nodes != nil { log.Logger.Debug("Handling conan lockfile as v1.x") - return p.parseRequirementsV1(lockV1) + return p.parseV1(lock) } else { // try to parse requirements as conan v2.x log.Logger.Debug("Handling conan lockfile as v2.x") - if err := jfather.Unmarshal(input, &lockV2); err != nil { - return nil, nil, xerrors.Errorf("failed to decode conan lock file: %w", err) - } - return p.parseRequirementsV2(lockV2) + return p.parseV2(lock) } } @@ -146,8 +153,8 @@ func parsePackage(text string) (string, string, error) { return ss[0], ss[1], nil } -func parseRefV1(node Node) (types.Library, error) { - name, version, err := parsePackage(node.Ref) +func toLibrary(pkg string, startLine, endLine int) (types.Library, error) { + name, version, err := parsePackage(pkg) if err != nil { return types.Library{}, err } @@ -157,25 +164,13 @@ func parseRefV1(node Node) (types.Library, error) { Version: version, Locations: []types.Location{ { - StartLine: node.StartLine, - EndLine: node.EndLine, + StartLine: startLine, + EndLine: endLine, }, }, }, nil } -func parseRefV2(req string) (types.Library, error) { - name, version, err := parsePackage(req) - if err != nil { - return types.Library{}, err - } - return types.Library{ - ID: dependency.ID(ftypes.Conan, name, version), - Name: name, - Version: version, - }, nil -} - // UnmarshalJSONWithMetadata needed to detect start and end lines of deps func (n *Node) UnmarshalJSONWithMetadata(node jfather.Node) error { if err := node.Decode(&n); err != nil { @@ -186,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 7a5af31c8ca6..d32268dd512c 100644 --- a/pkg/dependency/parser/c/conan/parse_test.go +++ b/pkg/dependency/parser/c/conan/parse_test.go @@ -113,11 +113,23 @@ func TestParse(t *testing.T) { 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{}, From 3fb527e5e90ed3ffb08df515c77b7dee65b1112a Mon Sep 17 00:00:00 2001 From: Damian Ellwart Date: Wed, 17 Apr 2024 00:02:40 +0200 Subject: [PATCH 3/3] chore: rebase adjustments and conan docs updates Update docs/docs/coverage/language/c.md Co-authored-by: Teppei Fukuda --- docs/docs/coverage/language/c.md | 7 ++++--- pkg/dependency/parser/c/conan/parse.go | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/docs/coverage/language/c.md b/docs/docs/coverage/language/c.md index 811aa2398f95..276340a806bd 100644 --- a/docs/docs/coverage/language/c.md +++ b/docs/docs/coverage/language/c.md @@ -10,9 +10,10 @@ 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[^1] | ✓ [^3] | 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]. diff --git a/pkg/dependency/parser/c/conan/parse.go b/pkg/dependency/parser/c/conan/parse.go index 6f5c973d9a02..715ecb6c1dc0 100644 --- a/pkg/dependency/parser/c/conan/parse.go +++ b/pkg/dependency/parser/c/conan/parse.go @@ -43,7 +43,6 @@ type Parser struct { logger *log.Logger } - func NewParser() types.Parser { return &Parser{ logger: log.WithPrefix("conan"), @@ -108,7 +107,7 @@ func (p *Parser) parseV2(lock LockFile) ([]types.Library, []types.Dependency, er for _, req := range lock.Requires { lib, err := toLibrary(req.Dependency, req.StartLine, req.EndLine) if err != nil { - log.Logger.Debug(err) + p.logger.Debug("Creating library entry from requirement failed", err) continue } @@ -130,11 +129,11 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]types.Library, []types.Dependency, // try to parse requirements as conan v1.x if lock.GraphLock.Nodes != nil { - log.Logger.Debug("Handling conan lockfile as v1.x") + p.logger.Debug("Handling conan lockfile as v1.x") return p.parseV1(lock) } else { // try to parse requirements as conan v2.x - log.Logger.Debug("Handling conan lockfile as v2.x") + p.logger.Debug("Handling conan lockfile as v2.x") return p.parseV2(lock) } }