-
Notifications
You must be signed in to change notification settings - Fork 2.8k
feat(c): add license support for conan lock files #6329
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
Changes from 10 commits
441eaa7
052f193
d54d1ce
2b06478
eb3d53d
58efb64
4a56a0d
9cc7e88
61af875
89003d6
9b5d8ce
7bedafb
18b1383
0d67966
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,42 +1,152 @@ | ||
| package conan | ||
|
|
||
| import ( | ||
| "bufio" | ||
| "context" | ||
| "io" | ||
| "io/fs" | ||
| "os" | ||
| "path" | ||
| "path/filepath" | ||
| "sort" | ||
| "strings" | ||
|
|
||
| "golang.org/x/xerrors" | ||
|
|
||
| "github.com/aquasecurity/trivy/pkg/dependency/parser/c/conan" | ||
| godeptypes "github.com/aquasecurity/trivy/pkg/dependency/types" | ||
| "github.com/aquasecurity/trivy/pkg/fanal/analyzer" | ||
| "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" | ||
| "github.com/aquasecurity/trivy/pkg/fanal/types" | ||
| "github.com/aquasecurity/trivy/pkg/log" | ||
| "github.com/aquasecurity/trivy/pkg/utils/fsutils" | ||
| ) | ||
|
|
||
| func init() { | ||
| analyzer.RegisterAnalyzer(&conanLockAnalyzer{}) | ||
| analyzer.RegisterPostAnalyzer(analyzer.TypeConanLock, newConanLockAnalyzer) | ||
| } | ||
|
|
||
| const ( | ||
| version = 1 | ||
| version = 2 | ||
| ) | ||
|
|
||
| // conanLockAnalyzer analyzes conan.lock | ||
| type conanLockAnalyzer struct{} | ||
| type conanLockAnalyzer struct { | ||
| logger *log.Logger | ||
| parser godeptypes.Parser | ||
| } | ||
|
|
||
| func newConanLockAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { | ||
| return conanLockAnalyzer{ | ||
| logger: log.WithPrefix("conan"), | ||
| parser: conan.NewParser(), | ||
| }, nil | ||
| } | ||
|
|
||
| func (a conanLockAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { | ||
| p := conan.NewParser() | ||
| res, err := language.Analyze(types.Conan, input.FilePath, input.Content, p) | ||
| func (a conanLockAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { | ||
| required := func(filePath string, d fs.DirEntry) bool { | ||
| return a.Required(filePath, nil) | ||
| } | ||
|
|
||
| licenses, err := licensesFromCache() | ||
| if err != nil { | ||
| return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err) | ||
| a.logger.Debug("Unable to parse cache directory to obtain licenses", log.Err(err)) | ||
| } | ||
|
|
||
| var apps []types.Application | ||
| if err = fsutils.WalkDir(input.FS, ".", required, func(filePath string, _ fs.DirEntry, r io.Reader) error { | ||
| app, err := language.Parse(types.Conan, filePath, r, a.parser) | ||
| if err != nil { | ||
| return xerrors.Errorf("%s parse error: %w", filePath, err) | ||
| } | ||
|
|
||
| if app == nil { | ||
| return nil | ||
| } | ||
|
|
||
| // Fill licenses | ||
| for i, lib := range app.Libraries { | ||
| if license, ok := licenses[lib.Name]; ok { | ||
| app.Libraries[i].Licenses = []string{ | ||
| license, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| sort.Sort(app.Libraries) | ||
| apps = append(apps, *app) | ||
| return nil | ||
| }); err != nil { | ||
| return nil, xerrors.Errorf("unable to parse conan lock file: %w", err) | ||
| } | ||
|
|
||
| return &analyzer.AnalysisResult{ | ||
| Applications: apps, | ||
| }, nil | ||
| } | ||
|
|
||
| func licensesFromCache() (map[string]string, error) { | ||
| required := func(filePath string, d fs.DirEntry) bool { | ||
| return filepath.Base(filePath) == "conanfile.py" | ||
| } | ||
|
|
||
| // cf. https://docs.conan.io/1/mastering/custom_cache.html | ||
| cacheDir := os.Getenv("CONAN_USER_HOME") | ||
| if cacheDir == "" { | ||
| cacheDir, _ = os.UserHomeDir() | ||
| } | ||
| cacheDir = path.Join(cacheDir, ".conan", "data") | ||
|
|
||
| if !fsutils.DirExists(cacheDir) { | ||
| return nil, xerrors.Errorf("the Conan cache directory (%s) was not found.", cacheDir) | ||
| } | ||
|
|
||
| licenses := make(map[string]string) | ||
| if err := fsutils.WalkDir(os.DirFS(cacheDir), ".", required, func(filePath string, _ fs.DirEntry, r io.Reader) error { | ||
| scanner := bufio.NewScanner(r) | ||
| var name, license string | ||
| for scanner.Scan() { | ||
| line := strings.TrimSpace(scanner.Text()) | ||
|
|
||
| if strings.HasPrefix(line, "name") { // cf. https://docs.conan.io/1/reference/conanfile/attributes.html#name | ||
|
||
| // remove spaces before and after `=` (if used). e.g. `name = "openssl"` -> `name="openssl"` | ||
| name = strings.ReplaceAll(line, " ", "") | ||
| // trim extra characters - e.g. `name="openssl"` -> `openssl` | ||
| name = strings.TrimSuffix(strings.TrimPrefix(name, `name="`), `"`) | ||
| // Check that the license is already found | ||
| if license != "" { | ||
| break | ||
| } | ||
| } else if strings.HasPrefix(line, "license") { // cf. https://docs.conan.io/1/reference/conanfile/attributes.html#license | ||
|
||
| // remove spaces before and after `=` (if used). e.g. `license = "Apache-2.0"` -> `license="Apache-2.0"` | ||
| license = strings.ReplaceAll(line, " ", "") | ||
|
||
| // trim extra characters - e.g. `license = "Apache-2.0"` -> `Apache-2.0` | ||
| license = strings.TrimSuffix(strings.TrimPrefix(license, `license="`), `"`) | ||
| // Check that the name is already found | ||
| if name != "" { | ||
| break | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Skip files without name/license | ||
| if name == "" || license == "" { | ||
| return nil | ||
| } | ||
knqyf263 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| licenses[name] = license | ||
| return nil | ||
| }); err != nil { | ||
| return nil, xerrors.Errorf("the Conan cache dir (%s) walk error: %w", cacheDir, err) | ||
| } | ||
| return res, nil | ||
| return licenses, nil | ||
| } | ||
|
|
||
| func (a conanLockAnalyzer) Required(_ string, fileInfo os.FileInfo) bool { | ||
| func (a conanLockAnalyzer) Required(filePath string, _ os.FileInfo) bool { | ||
| // Lock file name can be anything | ||
| // cf. https://docs.conan.io/en/latest/versioning/lockfiles/introduction.html#locking-dependencies | ||
| // cf. https://docs.conan.io/1/versioning/lockfiles/introduction.html#locking-dependencies | ||
| // By default, we only check the default filename - `conan.lock`. | ||
| return fileInfo.Name() == types.ConanLock | ||
| return filepath.Base(filePath) == types.ConanLock | ||
| } | ||
|
|
||
| func (a conanLockAnalyzer) Type() analyzer.Type { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't
conanfile.txtused?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cache dir contains only
conanfile.pyfiles:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is
conanfile.pyrequired? I'm wondering if we just don't find a project usingconanfile.txtas most projects useconanfile.py. But we can handleconanfile.txtif we find such a case.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC only
conanfile.pycontains attributes - https://docs.conan.io/2/reference/conanfile_txt.html#conanfile-txtTherefore, we can't detect package name/license from
conanfile.txtfiles.Then we don't need to parse the conanfile.txt files.