Skip to content

Commit 5dd9bd4

Browse files
authored
feat(c): add license support for conan lock files (#6329)
1 parent 7c2017f commit 5dd9bd4

File tree

7 files changed

+1052
-50
lines changed

7 files changed

+1052
-50
lines changed

docs/docs/coverage/language/c.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,27 @@ Trivy supports [Conan][conan] C/C++ Package Manager.
44

55
The following scanners are supported.
66

7-
| Package manager | SBOM | Vulnerability | License |
8-
| --------------- | :---: | :-----------: | :-----: |
9-
| Conan | || - |
7+
| Package manager | SBOM | Vulnerability | License |
8+
|-----------------|:----:|:-------------:|:-------:|
9+
| Conan ||| [^1] |
1010

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

1313
| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position |
14-
| --------------- | -------------- | :---------------------: | :--------------: | :----------------------------------: | :------: |
15-
| Conan | conan.lock[^1] || Excluded |||
14+
|-----------------|----------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|
15+
| Conan | conan.lock[^2] || Excluded |||
1616

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

20+
### Licenses
21+
The Conan lock file doesn't contain any license information.
22+
To obtain licenses we parse the `conanfile.py` files from the [conan cache directory][conan-cache-dir].
23+
To correctly detection licenses, ensure that the cache directory contains all dependencies used.
24+
2025
[conan]: https://docs.conan.io/1/index.html
26+
[conan-cache-dir]: https://docs.conan.io/1/mastering/custom_cache.html
2127
[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies
2228

23-
[^1]: `conan.lock` is default name. To scan a custom filename use [file-patterns](../../configuration/skipping.md#file-patterns)
29+
[^1]: The local cache should contain the dependencies used. See [licenses](#licenses).
30+
[^2]: `conan.lock` is default name. To scan a custom filename use [file-patterns](../../configuration/skipping.md#file-patterns).

pkg/fanal/analyzer/language/c/conan/conan.go

Lines changed: 134 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,165 @@
11
package conan
22

33
import (
4+
"bufio"
45
"context"
6+
"io"
7+
"io/fs"
58
"os"
9+
"path"
10+
"path/filepath"
11+
"sort"
12+
"strings"
613

714
"golang.org/x/xerrors"
815

916
"github.com/aquasecurity/trivy/pkg/dependency/parser/c/conan"
17+
godeptypes "github.com/aquasecurity/trivy/pkg/dependency/types"
1018
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
1119
"github.com/aquasecurity/trivy/pkg/fanal/analyzer/language"
1220
"github.com/aquasecurity/trivy/pkg/fanal/types"
21+
"github.com/aquasecurity/trivy/pkg/log"
22+
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
1323
)
1424

1525
func init() {
16-
analyzer.RegisterAnalyzer(&conanLockAnalyzer{})
26+
analyzer.RegisterPostAnalyzer(analyzer.TypeConanLock, newConanLockAnalyzer)
1727
}
1828

1929
const (
20-
version = 1
30+
version = 2
2131
)
2232

2333
// conanLockAnalyzer analyzes conan.lock
24-
type conanLockAnalyzer struct{}
34+
type conanLockAnalyzer struct {
35+
logger *log.Logger
36+
parser godeptypes.Parser
37+
}
38+
39+
func newConanLockAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
40+
return conanLockAnalyzer{
41+
logger: log.WithPrefix("conan"),
42+
parser: conan.NewParser(),
43+
}, nil
44+
}
45+
46+
func (a conanLockAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) {
47+
required := func(filePath string, d fs.DirEntry) bool {
48+
return a.Required(filePath, nil)
49+
}
2550

26-
func (a conanLockAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
27-
p := conan.NewParser()
28-
res, err := language.Analyze(types.Conan, input.FilePath, input.Content, p)
51+
licenses, err := licensesFromCache()
2952
if err != nil {
30-
return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err)
53+
a.logger.Debug("Unable to parse cache directory to obtain licenses", log.Err(err))
3154
}
32-
return res, nil
55+
56+
var apps []types.Application
57+
if err = fsutils.WalkDir(input.FS, ".", required, func(filePath string, _ fs.DirEntry, r io.Reader) error {
58+
app, err := language.Parse(types.Conan, filePath, r, a.parser)
59+
if err != nil {
60+
return xerrors.Errorf("%s parse error: %w", filePath, err)
61+
}
62+
63+
if app == nil {
64+
return nil
65+
}
66+
67+
// Fill licenses
68+
for i, lib := range app.Libraries {
69+
if license, ok := licenses[lib.Name]; ok {
70+
app.Libraries[i].Licenses = []string{
71+
license,
72+
}
73+
}
74+
}
75+
76+
sort.Sort(app.Libraries)
77+
apps = append(apps, *app)
78+
return nil
79+
}); err != nil {
80+
return nil, xerrors.Errorf("unable to parse conan lock file: %w", err)
81+
}
82+
83+
return &analyzer.AnalysisResult{
84+
Applications: apps,
85+
}, nil
86+
}
87+
88+
func licensesFromCache() (map[string]string, error) {
89+
required := func(filePath string, d fs.DirEntry) bool {
90+
return filepath.Base(filePath) == "conanfile.py"
91+
}
92+
93+
// cf. https://docs.conan.io/1/mastering/custom_cache.html
94+
cacheDir := os.Getenv("CONAN_USER_HOME")
95+
if cacheDir == "" {
96+
cacheDir, _ = os.UserHomeDir()
97+
}
98+
cacheDir = path.Join(cacheDir, ".conan", "data")
99+
100+
if !fsutils.DirExists(cacheDir) {
101+
return nil, xerrors.Errorf("the Conan cache directory (%s) was not found.", cacheDir)
102+
}
103+
104+
licenses := make(map[string]string)
105+
if err := fsutils.WalkDir(os.DirFS(cacheDir), ".", required, func(filePath string, _ fs.DirEntry, r io.Reader) error {
106+
scanner := bufio.NewScanner(r)
107+
var name, license string
108+
for scanner.Scan() {
109+
line := strings.TrimSpace(scanner.Text())
110+
111+
// cf. https://docs.conan.io/1/reference/conanfile/attributes.html#name
112+
if n := detectAttribute("name", line); n != "" {
113+
name = n
114+
// Check that the license is already found
115+
if license != "" {
116+
break
117+
}
118+
}
119+
// cf. https://docs.conan.io/1/reference/conanfile/attributes.html#license
120+
if l := detectAttribute("license", line); l != "" {
121+
license = l
122+
// Check that the name is already found
123+
if name != "" {
124+
break
125+
}
126+
}
127+
}
128+
129+
// Skip files without name/license
130+
if name == "" || license == "" {
131+
return nil
132+
}
133+
134+
licenses[name] = license
135+
return nil
136+
}); err != nil {
137+
return nil, xerrors.Errorf("the Conan cache dir (%s) walk error: %w", cacheDir, err)
138+
}
139+
return licenses, nil
140+
}
141+
142+
// detectAttribute detects conan attribute (name, license, etc.) from line
143+
// cf. https://docs.conan.io/1/reference/conanfile/attributes.html
144+
func detectAttribute(attributeName, line string) string {
145+
if !strings.HasPrefix(line, attributeName) {
146+
return ""
147+
}
148+
149+
// e.g. `license = "Apache or MIT"` -> ` "Apache or MIT"` -> `"Apache or MIT"` -> `Apache or MIT`
150+
if name, v, ok := strings.Cut(line, "="); ok && strings.TrimSpace(name) == attributeName {
151+
attr := strings.TrimSpace(v)
152+
return strings.Trim(attr, `"`)
153+
}
154+
155+
return ""
33156
}
34157

35-
func (a conanLockAnalyzer) Required(_ string, fileInfo os.FileInfo) bool {
158+
func (a conanLockAnalyzer) Required(filePath string, _ os.FileInfo) bool {
36159
// Lock file name can be anything
37-
// cf. https://docs.conan.io/en/latest/versioning/lockfiles/introduction.html#locking-dependencies
160+
// cf. https://docs.conan.io/1/versioning/lockfiles/introduction.html#locking-dependencies
38161
// By default, we only check the default filename - `conan.lock`.
39-
return fileInfo.Name() == types.ConanLock
162+
return filepath.Base(filePath) == types.ConanLock
40163
}
41164

42165
func (a conanLockAnalyzer) Type() analyzer.Type {

0 commit comments

Comments
 (0)