Skip to content

Commit

Permalink
feat(c): add license support for conan lock files (aquasecurity#6329)
Browse files Browse the repository at this point in the history
  • Loading branch information
DmitriyLewen authored and fl0pp5 committed May 6, 2024
1 parent a20d50c commit 25ee3c8
Show file tree
Hide file tree
Showing 7 changed files with 1,052 additions and 50 deletions.
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"
}

// 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
}

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.Trim(attr, `"`)
}

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

0 comments on commit 25ee3c8

Please sign in to comment.