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(c): add license support for conan lock files #6329

Merged
merged 14 commits into from
Apr 24, 2024
Merged
Show file tree
Hide file tree
Changes from 13 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
19 changes: 13 additions & 6 deletions docs/docs/coverage/language/c.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ Trivy supports [Conan][conan] C/C++ Package Manager.

The following scanners are supported.

| Package manager | SBOM | Vulnerability | License |
| --------------- | :---: | :-----------: | :-----: |
| Conan | ✓ | ✓ | - |
| Package manager | SBOM | Vulnerability | License |
|-----------------|:----:|:-------------:|:-------:|
| Conan | ✓ | ✓ | ✓[^1] |

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] | ✓ | Excluded | ✓ | ✓ |
|-----------------|----------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|
| Conan | conan.lock[^2] | ✓ | Excluded | ✓ | ✓ |

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

### 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]: `conan.lock` is default name. To scan a custom filename use [file-patterns](../../configuration/skipping.md#file-patterns)
[^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).
145 changes: 134 additions & 11 deletions pkg/fanal/analyzer/language/c/conan/conan.go
Original file line number Diff line number Diff line change
@@ -1,42 +1,165 @@
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) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) {
required := func(filePath string, d fs.DirEntry) bool {
return a.Required(filePath, 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)
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))
}
return res, nil

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"
Copy link
Collaborator

@knqyf263 knqyf263 Apr 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't conanfile.txt used?

Copy link
Contributor Author

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.py files:

root@3fcddc7fe2e5:/app# find /root/.conan -name "conanfile.*"
/root/.conan/data/zlib/1.3.1/_/_/export/conanfile.py

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is conanfile.py required? I'm wondering if we just don't find a project using conanfile.txt as most projects use conanfile.py. But we can handle conanfile.txt if we find such a case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC only conanfile.py contains attributes - https://docs.conan.io/2/reference/conanfile_txt.html#conanfile-txt

Therefore, we can't detect package name/license from conanfile.txt files.
Then we don't need to parse the conanfile.txt files.

}

// 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())

// cf. https://docs.conan.io/1/reference/conanfile/attributes.html#name
if n := detectAttribute("name", line); n != "" {
name = n
// Check that the license is already found
if license != "" {
break
}
}
// cf. https://docs.conan.io/1/reference/conanfile/attributes.html#license
if l := detectAttribute("license", line); l != "" {
license = l
// 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 licenses, nil
}

// detectAttribute detects conan attribute (name, license, etc.) from line
// cf. https://docs.conan.io/1/reference/conanfile/attributes.html
func detectAttribute(attributeName, line string) string {
if !strings.HasPrefix(line, attributeName) {
return ""
}

// e.g. `license = "Apache or MIT"` -> ` "Apache or MIT"` -> `"Apache or MIT"` -> `Apache or MIT`
if name, v, ok := strings.Cut(line, "="); ok && strings.TrimSpace(name) == attributeName {
attr := strings.TrimSpace(v)
return strings.TrimPrefix(strings.TrimSuffix(attr, "\""), "\"")
Copy link
Collaborator

@knqyf263 knqyf263 Apr 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

strings.Trim seems better.

Suggested change
return strings.TrimPrefix(strings.TrimSuffix(attr, "\""), "\"")
return strings.Trim(attr, `"`)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 0d67966

}

return ""
}

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 {
Expand Down
Loading