-
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 9 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,148 @@ | ||
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 |
||
// trim extra characters - e.g. `name = "openssl"` -> `openssl` | ||
name = strings.TrimSuffix(strings.TrimPrefix(line, `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. |
||
// trim extra characters - e.g. `license = "Apache-2.0"` -> `Apache-2.0` | ||
license = strings.TrimSuffix(strings.TrimPrefix(line, `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. I found this example in the doc.
We probably need a regexp like 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. In addition, if a conanfile.py is small enough, applying regexp to the file content might be simpler and faster.
I am not 100% sure which is faster as I have not compared it with processing one line at a time. I was just thinking out loud, and it's not a performance critical process. You can decide. 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 as bug, but we can also parse this case.
I also don't trust regexp and try not to use them.
In the files that I checked,
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. I added removing extra spaces - 89003d6 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.
Why is it a bug? It does not break the Python semantics. 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. I haven't seen such cases. Also, all test cases use the format In any case, this is easy to solve. 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. Conanfile is written by hand, so any format is possible, isn't it? I think there is a customary preferred format, but it is legitimate. 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.
You are right 👍 |
||
// 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.