-
Notifications
You must be signed in to change notification settings - Fork 2.4k
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
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto |
||
// 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be more strict. Otherwise, it matches other than the license line. That's why I suggested regexp this time. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it looks like we can't avoid using regexp... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If possible, I don't want to use regexp. I'm still considering a way to parse the license line without regexp, but... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. take a look this -https://go.dev/play/p/yWIioLCblQ9 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
// remove spaces before and after `=` (if used). e.g. `license = "Apache-2.0"` -> `license="Apache-2.0"` | ||
license = strings.ReplaceAll(line, " ", "") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It introduces another bug with `license = "Apache or MIT". |
||
// 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 { | ||
|
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.txt
used?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.py
files: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.py
required? I'm wondering if we just don't find a project usingconanfile.txt
as most projects useconanfile.py
. But we can handleconanfile.txt
if 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.py
contains attributes - https://docs.conan.io/2/reference/conanfile_txt.html#conanfile-txtTherefore, we can't detect package name/license from
conanfile.txt
files.Then we don't need to parse the conanfile.txt files.