From 441eaa7010461e096cf24a9bd749efd61ee5d307 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Fri, 15 Mar 2024 14:20:29 +0600 Subject: [PATCH 01/13] feat(conan): detect licenses using cache dir --- pkg/fanal/analyzer/language/c/conan/conan.go | 121 +++++++++++++++++-- 1 file changed, 110 insertions(+), 11 deletions(-) diff --git a/pkg/fanal/analyzer/language/c/conan/conan.go b/pkg/fanal/analyzer/language/c/conan/conan.go index d06eb4cd260d..21f8359468dc 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan.go +++ b/pkg/fanal/analyzer/language/c/conan/conan.go @@ -1,42 +1,141 @@ package conan import ( + "bufio" "context" + "io" + "io/fs" "os" + "path" + "path/filepath" + "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/log" "github.com/aquasecurity/trivy/pkg/fanal/types" + "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 { + parser godeptypes.Parser +} + +func newConanLockAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return conanLockAnalyzer{ + 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) + log.Logger.Debugf("Unable to parse cache directory to obtain licenses: %s", 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) + } + + // Fill licenses + for i, lib := range app.Libraries { + if license, ok := licenses[lib.Name]; ok { + app.Libraries[i].Licenses = []string{ + license, + } + } + } + + 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) { + log.Logger.Debugf("The Conan cache directory (%s) was not found. Package licenses will be skipped", cacheDir) + return nil, nil + } + + 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 + // 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 + // trim extra characters - e.g. `license = "Apache-2.0"` -> `Apache-2.0` + license = strings.TrimSuffix(strings.TrimPrefix(line, `license = "`), `"`) + // 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("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 { From 052f193af347855cbc7e758fd3a663d644b95f6d Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Fri, 15 Mar 2024 14:20:52 +0600 Subject: [PATCH 02/13] test(conan): update prev tests, add new test with cache dir --- .../analyzer/language/c/conan/conan_test.go | 84 ++- .../openssl/3.0.5/_/_/export/conanfile.py | 675 ++++++++++++++++++ .../data/zlib/1.2.13/_/_/export/conanfile.py | 110 +++ .../testdata/{empty.lock => empty/conan.lock} | 0 .../testdata/{happy.lock => happy/conan.lock} | 0 5 files changed, 848 insertions(+), 21 deletions(-) create mode 100644 pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py create mode 100644 pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/zlib/1.2.13/_/_/export/conanfile.py rename pkg/fanal/analyzer/language/c/conan/testdata/{empty.lock => empty/conan.lock} (100%) rename pkg/fanal/analyzer/language/c/conan/testdata/{happy.lock => happy/conan.lock} (100%) diff --git a/pkg/fanal/analyzer/language/c/conan/conan_test.go b/pkg/fanal/analyzer/language/c/conan/conan_test.go index fcb3237bfb61..44014114c203 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan_test.go +++ b/pkg/fanal/analyzer/language/c/conan/conan_test.go @@ -1,9 +1,9 @@ package conan import ( + "context" "os" "path/filepath" - "sort" "testing" "github.com/stretchr/testify/assert" @@ -15,18 +15,19 @@ import ( func Test_conanLockAnalyzer_Analyze(t *testing.T) { tests := []struct { - name string - inputFile string - want *analyzer.AnalysisResult + name string + dir string + cacheDir string + want *analyzer.AnalysisResult }{ { - name: "happy path", - inputFile: "testdata/happy.lock", + name: "happy path", + dir: "testdata/happy", want: &analyzer.AnalysisResult{ Applications: []types.Application{ { Type: types.Conan, - FilePath: "testdata/happy.lock", + FilePath: "conan.lock", Libraries: types.Packages{ { ID: "openssl/3.0.5", @@ -60,29 +61,70 @@ func Test_conanLockAnalyzer_Analyze(t *testing.T) { }, }, { - name: "empty file", - inputFile: "testdata/empty.lock", + name: "happy path with cache dir", + dir: "testdata/happy", + cacheDir: "testdata/cacheDir", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Conan, + FilePath: "conan.lock", + Libraries: types.Packages{ + { + ID: "openssl/3.0.5", + Name: "openssl", + Version: "3.0.5", + Licenses: []string{ + "Apache-2.0", + }, + DependsOn: []string{ + "zlib/1.2.12", + }, + Locations: []types.Location{ + { + StartLine: 12, + EndLine: 21, + }, + }, + }, + { + ID: "zlib/1.2.12", + Name: "zlib", + Version: "1.2.12", + Licenses: []string{ + "Zlib", + }, + Indirect: true, + Locations: []types.Location{ + { + StartLine: 22, + EndLine: 28, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "empty file", + dir: "testdata/empty", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - f, err := os.Open(tt.inputFile) + if tt.cacheDir != "" { + t.Setenv("CONAN_USER_HOME", tt.cacheDir) + } + a, err := newConanLockAnalyzer(analyzer.AnalyzerOptions{}) require.NoError(t, err) - defer f.Close() - a := conanLockAnalyzer{} - got, err := a.Analyze(nil, analyzer.AnalysisInput{ - FilePath: tt.inputFile, - Content: f, + got, err := a.PostAnalyze(context.Background(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), }) - if got != nil { - for _, app := range got.Applications { - sort.Sort(app.Libraries) - } - } - assert.NoError(t, err) assert.Equal(t, tt.want, got) }) diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py new file mode 100644 index 000000000000..59da5e3bc243 --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py @@ -0,0 +1,675 @@ +from conan import ConanFile +from conan.errors import ConanInvalidConfiguration +from conan.tools.apple import fix_apple_shared_install_name, is_apple_os, XCRun +from conan.tools.build import build_jobs +from conan.tools.files import chdir, copy, get, rename, replace_in_file, rmdir, save +from conan.tools.gnu import AutotoolsToolchain +from conan.tools.layout import basic_layout +from conan.tools.microsoft import is_msvc, msvc_runtime_flag, unix_path + +import fnmatch +import os +import textwrap + +required_conan_version = ">=1.57.0" + + +class OpenSSLConan(ConanFile): + name = "openssl" + settings = "os", "arch", "compiler", "build_type" + url = "https://github.com/conan-io/conan-center-index" + homepage = "https://github.com/openssl/openssl" + license = "Apache-2.0" + topics = ("ssl", "tls", "encryption", "security") + description = "A toolkit for the Transport Layer Security (TLS) and Secure Sockets Layer (SSL) protocols" + options = { + "shared": [True, False], + "fPIC": [True, False], + "enable_weak_ssl_ciphers": [True, False], + "386": [True, False], + "capieng_dialog": [True, False], + "enable_capieng": [True, False], + "no_aria": [True, False], + "no_asm": [True, False], + "no_async": [True, False], + "no_blake2": [True, False], + "no_bf": [True, False], + "no_camellia": [True, False], + "no_chacha": [True, False], + "no_cms": [True, False], + "no_comp": [True, False], + "no_ct": [True, False], + "no_cast": [True, False], + "no_deprecated": [True, False], + "no_des": [True, False], + "no_dgram": [True, False], + "no_dh": [True, False], + "no_dsa": [True, False], + "no_dso": [True, False], + "no_ec": [True, False], + "no_ecdh": [True, False], + "no_ecdsa": [True, False], + "no_engine": [True, False], + "no_filenames": [True, False], + "no_fips": [True, False], + "no_gost": [True, False], + "no_idea": [True, False], + "no_legacy": [True, False], + "no_md2": [True, False], + "no_md4": [True, False], + "no_mdc2": [True, False], + "no_module": [True, False], + "no_ocsp": [True, False], + "no_pinshared": [True, False], + "no_rc2": [True, False], + "no_rc4": [True, False], + "no_rc5": [True, False], + "no_rfc3779": [True, False], + "no_rmd160": [True, False], + "no_sm2": [True, False], + "no_sm3": [True, False], + "no_sm4": [True, False], + "no_srp": [True, False], + "no_srtp": [True, False], + "no_sse2": [True, False], + "no_ssl": [True, False], + "no_stdio": [True, False], + "no_seed": [True, False], + "no_sock": [True, False], + "no_ssl3": [True, False], + "no_threads": [True, False], + "no_tls1": [True, False], + "no_ts": [True, False], + "no_whirlpool": [True, False], + "no_zlib": [True, False], + "openssldir": [None, "ANY"], + } + default_options = {key: False for key in options.keys()} + default_options["fPIC"] = True + default_options["no_md2"] = True + default_options["openssldir"] = None + + @property + def _settings_build(self): + return getattr(self, "settings_build", self.settings) + + def config_options(self): + if self.settings.os != "Windows": + self.options.rm_safe("capieng_dialog") + self.options.rm_safe("enable_capieng") + else: + self.options.rm_safe("fPIC") + + if self.settings.os == "Emscripten": + self.options.no_asm = True + self.options.no_threads = True + self.options.no_stdio = True + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + self.settings.rm_safe("compiler.libcxx") + self.settings.rm_safe("compiler.cppstd") + + def requirements(self): + if not self.options.no_zlib: + self.requires("zlib/1.2.13") + + def build_requirements(self): + if self._settings_build.os == "Windows": + if not self.options.no_asm: + self.tool_requires("nasm/2.15.05") + if self._use_nmake: + self.tool_requires("strawberryperl/5.32.1.1") + else: + self.win_bash = True + if not self.conf.get("tools.microsoft.bash:path", check_type=str): + self.tool_requires("msys2/cci.latest") + + def validate(self): + if self.settings.os == "Emscripten": + if not all((self.options.no_asm, self.options.no_threads, self.options.no_stdio)): + raise ConanInvalidConfiguration("os=Emscripten requires openssl:{no_asm,no_threads,no_stdio}=True") + + if self.settings.os == "iOS" and self.options.shared: + raise ConanInvalidConfiguration("OpenSSL 3 does not support building shared libraries for iOS") + + def layout(self): + basic_layout(self, src_folder="src") + + @property + def _is_clangcl(self): + return self.settings.compiler == "clang" and self.settings.os == "Windows" + + @property + def _is_mingw(self): + return self.settings.os == "Windows" and self.settings.compiler == "gcc" + + @property + def _use_nmake(self): + return self._is_clangcl or is_msvc(self) + + def source(self): + get(self, **self.conan_data["sources"][self.version], + destination=self.source_folder, strip_root=True) + + @property + def _target(self): + target = f"conan-{self.settings.build_type}-{self.settings.os}-{self.settings.arch}-{self.settings.compiler}-{self.settings.compiler.version}" + if self._use_nmake: + target = f"VC-{target}" # VC- prefix is important as it's checked by Configure + if self._is_mingw: + target = f"mingw-{target}" + return target + + @property + def _perlasm_scheme(self): + # right now, we need to tweak this for iOS & Android only, as they inherit from generic targets + if self.settings.os in ("iOS", "watchOS", "tvOS"): + return { + "armv7": "ios32", + "armv7s": "ios32", + "armv8": "ios64", + "armv8_32": "ios64", + "armv8.3": "ios64", + "armv7k": "ios32", + }.get(str(self.settings.arch), None) + elif self.settings.os == "Android": + return { + "armv7": "void", + "armv8": "linux64", + "mips": "o32", + "mips64": "64", + "x86": "android", + "x86_64": "elf", + }.get(str(self.settings.arch), None) + return None + + @property + def _asm_target(self): + if self.settings.os in ("Android", "iOS", "watchOS", "tvOS"): + return { + "x86": "x86_asm" if self.settings.os == "Android" else None, + "x86_64": "x86_64_asm" if self.settings.os == "Android" else None, + "armv5el": "armv4_asm", + "armv5hf": "armv4_asm", + "armv6": "armv4_asm", + "armv7": "armv4_asm", + "armv7hf": "armv4_asm", + "armv7s": "armv4_asm", + "armv7k": "armv4_asm", + "armv8": "aarch64_asm", + "armv8_32": "aarch64_asm", + "armv8.3": "aarch64_asm", + "mips": "mips32_asm", + "mips64": "mips64_asm", + "sparc": "sparcv8_asm", + "sparcv9": "sparcv9_asm", + "ia64": "ia64_asm", + "ppc32be": "ppc32_asm", + "ppc32": "ppc32_asm", + "ppc64le": "ppc64_asm", + "ppc64": "ppc64_asm", + "s390": "s390x_asm", + "s390x": "s390x_asm" + }.get(str(self.settings.os), None) + + @property + def _targets(self): + is_cygwin = self.settings.get_safe("os.subsystem") == "cygwin" + return { + "Linux-x86-clang": "linux-x86-clang", + "Linux-x86_64-clang": "linux-x86_64-clang", + "Linux-x86-*": "linux-x86", + "Linux-x86_64-*": "linux-x86_64", + "Linux-armv4-*": "linux-armv4", + "Linux-armv4i-*": "linux-armv4", + "Linux-armv5el-*": "linux-armv4", + "Linux-armv5hf-*": "linux-armv4", + "Linux-armv6-*": "linux-armv4", + "Linux-armv7-*": "linux-armv4", + "Linux-armv7hf-*": "linux-armv4", + "Linux-armv7s-*": "linux-armv4", + "Linux-armv7k-*": "linux-armv4", + "Linux-armv8-*": "linux-aarch64", + "Linux-armv8.3-*": "linux-aarch64", + "Linux-armv8-32-*": "linux-arm64ilp32", + "Linux-mips-*": "linux-mips32", + "Linux-mips64-*": "linux-mips64", + "Linux-ppc32-*": "linux-ppc32", + "Linux-ppc32le-*": "linux-pcc32", + "Linux-ppc32be-*": "linux-ppc32", + "Linux-ppc64-*": "linux-ppc64", + "Linux-ppc64le-*": "linux-ppc64le", + "Linux-pcc64be-*": "linux-pcc64", + "Linux-s390x-*": "linux64-s390x", + "Linux-e2k-*": "linux-generic64", + "Linux-sparc-*": "linux-sparcv8", + "Linux-sparcv9-*": "linux64-sparcv9", + "Linux-*-*": "linux-generic32", + "Macos-x86-*": "darwin-i386-cc", + "Macos-x86_64-*": "darwin64-x86_64-cc", + "Macos-ppc32-*": "darwin-ppc-cc", + "Macos-ppc32be-*": "darwin-ppc-cc", + "Macos-ppc64-*": "darwin64-ppc-cc", + "Macos-ppc64be-*": "darwin64-ppc-cc", + "Macos-armv8-*": "darwin64-arm64-cc", + "Macos-*-*": "darwin-common", + "iOS-x86_64-*": "darwin64-x86_64-cc", + "iOS-*-*": "iphoneos-cross", + "watchOS-*-*": "iphoneos-cross", + "tvOS-*-*": "iphoneos-cross", + # Android targets are very broken, see https://github.com/openssl/openssl/issues/7398 + "Android-armv7-*": "linux-generic32", + "Android-armv7hf-*": "linux-generic32", + "Android-armv8-*": "linux-generic64", + "Android-x86-*": "linux-x86-clang", + "Android-x86_64-*": "linux-x86_64-clang", + "Android-mips-*": "linux-generic32", + "Android-mips64-*": "linux-generic64", + "Android-*-*": "linux-generic32", + "Windows-x86-gcc": "Cygwin-x86" if is_cygwin else "mingw", + "Windows-x86_64-gcc": "Cygwin-x86_64" if is_cygwin else "mingw64", + "Windows-*-gcc": "Cygwin-common" if is_cygwin else "mingw-common", + "Windows-ia64-Visual Studio": "VC-WIN64I", # Itanium + "Windows-x86-Visual Studio": "VC-WIN32", + "Windows-x86_64-Visual Studio": "VC-WIN64A", + "Windows-armv7-Visual Studio": "VC-WIN32-ARM", + "Windows-armv8-Visual Studio": "VC-WIN64-ARM", + "Windows-*-Visual Studio": "VC-noCE-common", + "Windows-ia64-clang": "VC-WIN64I", # Itanium + "Windows-x86-clang": "VC-WIN32", + "Windows-x86_64-clang": "VC-WIN64A", + "Windows-armv7-clang": "VC-WIN32-ARM", + "Windows-armv8-clang": "VC-WIN64-ARM", + "Windows-*-clang": "VC-noCE-common", + "WindowsStore-x86-*": "VC-WIN32-UWP", + "WindowsStore-x86_64-*": "VC-WIN64A-UWP", + "WindowsStore-armv7-*": "VC-WIN32-ARM-UWP", + "WindowsStore-armv8-*": "VC-WIN64-ARM-UWP", + "WindowsStore-*-*": "VC-WIN32-ONECORE", + "WindowsCE-*-*": "VC-CE", + "SunOS-x86-gcc": "solaris-x86-gcc", + "SunOS-x86_64-gcc": "solaris64-x86_64-gcc", + "SunOS-sparc-gcc": "solaris-sparcv8-gcc", + "SunOS-sparcv9-gcc": "solaris64-sparcv9-gcc", + "SunOS-x86-suncc": "solaris-x86-cc", + "SunOS-x86_64-suncc": "solaris64-x86_64-cc", + "SunOS-sparc-suncc": "solaris-sparcv8-cc", + "SunOS-sparcv9-suncc": "solaris64-sparcv9-cc", + "SunOS-*-*": "solaris-common", + "*BSD-x86-*": "BSD-x86", + "*BSD-x86_64-*": "BSD-x86_64", + "*BSD-ia64-*": "BSD-ia64", + "*BSD-sparc-*": "BSD-sparcv8", + "*BSD-sparcv9-*": "BSD-sparcv9", + "*BSD-armv8-*": "BSD-generic64", + "*BSD-mips64-*": "BSD-generic64", + "*BSD-ppc64-*": "BSD-generic64", + "*BSD-ppc64le-*": "BSD-generic64", + "*BSD-ppc64be-*": "BSD-generic64", + "AIX-ppc32-gcc": "aix-gcc", + "AIX-ppc64-gcc": "aix64-gcc", + "AIX-pcc32-*": "aix-cc", + "AIX-ppc64-*": "aix64-cc", + "AIX-*-*": "aix-common", + "*BSD-*-*": "BSD-generic32", + "Emscripten-*-*": "cc", + "Neutrino-*-*": "BASE_unix", + } + + @property + def _ancestor_target(self): + if "CONAN_OPENSSL_CONFIGURATION" in os.environ: + return os.environ["CONAN_OPENSSL_CONFIGURATION"] + compiler = "Visual Studio" if self.settings.compiler == "msvc" else self.settings.compiler + query = f"{self.settings.os}-{self.settings.arch}-{compiler}" + ancestor = next((self._targets[i] for i in self._targets if fnmatch.fnmatch(query, i)), None) + if not ancestor: + raise ConanInvalidConfiguration( + f"Unsupported configuration ({self.settings.os}/{self.settings.arch}/{self.settings.compiler}).\n" + f"Please open an issue at {self.url}.\n" + f"Alternatively, set the CONAN_OPENSSL_CONFIGURATION environment variable into your conan profile." + ) + return ancestor + + def _get_default_openssl_dir(self): + if self.settings.os == "Linux": + return "/etc/ssl" + return os.path.join(self.package_folder, "res") + + @property + def _configure_args(self): + openssldir = self.options.openssldir or self._get_default_openssl_dir() + openssldir = unix_path(self, openssldir) if self.win_bash else openssldir + args = [ + '"%s"' % (self._target), + "shared" if self.options.shared else "no-shared", + "--prefix=/", + "--libdir=lib", + "--openssldir=\"%s\"" % openssldir, + "no-unit-test", + "no-threads" if self.options.no_threads else "threads", + "PERL=%s" % self._perl, + "no-tests", + "--debug" if self.settings.build_type == "Debug" else "--release", + ] + + if self.settings.os == "Android": + args.append(" -D__ANDROID_API__=%s" % str(self.settings.os.api_level)) # see NOTES.ANDROID + if self.settings.os == "Emscripten": + args.append("-D__STDC_NO_ATOMICS__=1") + if self.settings.os == "Windows": + if self.options.enable_capieng: + args.append("enable-capieng") + if self.options.capieng_dialog: + args.append("-DOPENSSL_CAPIENG_DIALOG=1") + else: + args.append("-fPIC" if self.options.get_safe("fPIC", True) else "no-pic") + + args.append("no-fips" if self.options.get_safe("no_fips", True) else "enable-fips") + args.append("no-md2" if self.options.get_safe("no_md2", True) else "enable-md2") + + if self.settings.os == "Neutrino": + args.append("no-asm -lsocket -latomic") + + if not self.options.no_zlib: + zlib_info = self.dependencies["zlib"].cpp_info.aggregated_components() + include_path = zlib_info.includedirs[0] + if self.settings.os == "Windows": + lib_path = "%s/%s.lib" % (zlib_info.libdirs[0], zlib_info.libs[0]) + else: + # Just path, linux will find the right file + lib_path = zlib_info.libdirs[0] + if self._settings_build.os == "Windows": + # clang-cl doesn't like backslashes in #define CFLAGS (builldinf.h -> cversion.c) + include_path = include_path.replace("\\", "/") + lib_path = lib_path.replace("\\", "/") + + if self.dependencies["zlib"].options.shared: + args.append("zlib-dynamic") + else: + args.append("zlib") + + args.extend([ + '--with-zlib-include="%s"' % include_path, + '--with-zlib-lib="%s"' % lib_path + ]) + + for option_name in self.default_options.keys(): + if self.options.get_safe(option_name, False) and option_name not in ("shared", "fPIC", "openssldir", "capieng_dialog", "enable_capieng", "zlib", "no_fips", "no_md2"): + self.output.info(f"Activated option: {option_name}") + args.append(option_name.replace("_", "-")) + return args + + def generate(self): + tc = AutotoolsToolchain(self) + env = tc.environment() + env.define_path("PERL", self._perl) + if self.settings.compiler == "apple-clang": + xcrun = XCRun(self) + env.define_path("CROSS_SDK", os.path.basename(xcrun.sdk_path)) + env.define_path("CROSS_TOP", os.path.dirname(os.path.dirname(xcrun.sdk_path))) + + self._create_targets(tc.cflags, tc.cxxflags, tc.defines, tc.ldflags) + tc.generate(env) + + def _create_targets(self, cflags, cxxflags, defines, ldflags): + config_template = textwrap.dedent("""\ + {targets} = ( + "{target}" => {{ + inherit_from => {ancestor}, + cflags => add("{cflags}"), + cxxflags => add("{cxxflags}"), + {defines} + lflags => add("{lflags}"), + {shared_target} + {shared_cflag} + {shared_extension} + {perlasm_scheme} + }}, + ); + """) + + perlasm_scheme = "" + if self._perlasm_scheme: + perlasm_scheme = 'perlasm_scheme => "%s",' % self._perlasm_scheme + + defines = " ".join(defines) + defines = 'defines => add("%s"),' % defines if defines else "" + targets = "my %targets" + includes = "" + if self.settings.os == "Windows": + includes = includes.replace("\\", "/") # OpenSSL doesn't like backslashes + + if self._asm_target: + ancestor = '[ "%s", asm("%s") ]' % (self._ancestor_target, self._asm_target) + else: + ancestor = '[ "%s" ]' % self._ancestor_target + shared_cflag = "" + shared_extension = "" + shared_target = "" + if self.settings.os == "Neutrino": + if self.options.shared: + shared_extension = 'shared_extension => ".so.\$(SHLIB_VERSION_NUMBER)",' + shared_target = 'shared_target => "gnu-shared",' + if self.options.get_safe("fPIC", True): + shared_cflag = 'shared_cflag => "-fPIC",' + + if self.settings.os in ["iOS", "tvOS", "watchOS"] and self.conf.get("tools.apple:enable_bitcode", check_type=bool): + cflags.append("-fembed-bitcode") + cxxflags.append("-fembed-bitcode") + + config = config_template.format( + targets=targets, + target=self._target, + ancestor=ancestor, + cflags=" ".join(cflags), + cxxflags=" ".join(cxxflags), + defines=defines, + perlasm_scheme=perlasm_scheme, + shared_target=shared_target, + shared_extension=shared_extension, + shared_cflag=shared_cflag, + lflags=" ".join(ldflags) + ) + self.output.info("using target: %s -> %s" % (self._target, self._ancestor_target)) + self.output.info(config) + + save(self, os.path.join(self.source_folder, "Configurations", "20-conan.conf"), config) + + def _run_make(self, targets=None, parallel=True, install=False): + command = [self._make_program] + if install: + command.append(f"DESTDIR={self.package_folder}") + if targets: + command.extend(targets) + if not self._use_nmake: + command.append(("-j%s" % build_jobs(self)) if parallel else "-j1") + self.run(" ".join(command), env="conanbuild") + + @property + def _perl(self): + if self._use_nmake: + return self.dependencies.build["strawberryperl"].conf_info.get("user.strawberryperl:perl", check_type=str) + return "perl" + + def _make(self): + with chdir(self, self.source_folder): + # workaround for clang-cl not producing .pdb files + if self._is_clangcl: + save(self, "ossl_static.pdb", "") + args = " ".join(self._configure_args) + + if self._use_nmake: + self._replace_runtime_in_file(os.path.join("Configurations", "10-main.conf")) + + self.run("{perl} ./Configure {args}".format(perl=self._perl, args=args), env="conanbuild") + if self._use_nmake: + # When `--prefix=/`, the scripts derive `\` without escaping, which + # causes issues on Windows + replace_in_file(self, "Makefile", "INSTALLTOP_dir=\\", "INSTALLTOP_dir=\\\\") + self._run_make() + + def _make_install(self): + with chdir(self, self.source_folder): + self._run_make(targets=["install_sw"], parallel=False, install=True) + + def build(self): + self._make() + self.run(f"{self._perl} {self.source_folder}/configdata.pm --dump") + + @property + def _make_program(self): + return "nmake" if self._use_nmake else "make" + + def _replace_runtime_in_file(self, filename): + runtime = msvc_runtime_flag(self) + for e in ["MDd", "MTd", "MD", "MT"]: + replace_in_file(self, filename, f"/{e} ", f"/{runtime} ", strict=False) + replace_in_file(self, filename, f"/{e}\"", f"/{runtime}\"", strict=False) + + def package(self): + copy(self, "*LICENSE*", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses")) + self._make_install() + if is_apple_os(self): + fix_apple_shared_install_name(self) + + for root, _, files in os.walk(self.package_folder): + for filename in files: + if fnmatch.fnmatch(filename, "*.pdb"): + os.unlink(os.path.join(self.package_folder, root, filename)) + if self._use_nmake: + if self.settings.build_type == "Debug": + with chdir(self, os.path.join(self.package_folder, "lib")): + rename(self, "libssl.lib", "libssld.lib") + rename(self, "libcrypto.lib", "libcryptod.lib") + + if self.options.shared: + libdir = os.path.join(self.package_folder, "lib") + for file in os.listdir(libdir): + if self._is_mingw and file.endswith(".dll.a"): + continue + if file.endswith(".a"): + os.unlink(os.path.join(libdir, file)) + + if not self.options.no_fips: + provdir = os.path.join(self.source_folder, "providers") + modules_dir = os.path.join(self.package_folder, "lib", "ossl-modules") + if self.settings.os == "Macos": + copy(self, "fips.dylib", src=provdir, dst=modules_dir) + elif self.settings.os == "Windows": + copy(self, "fips.dll", src=provdir, dst=modules_dir) + else: + copy(self, "fips.so", src=provdir, dst=modules_dir) + + rmdir(self, os.path.join(self.package_folder, "lib", "pkgconfig")) + + self._create_cmake_module_variables( + os.path.join(self.package_folder, self._module_file_rel_path) + ) + + def _create_cmake_module_variables(self, module_file): + content = textwrap.dedent("""\ + set(OPENSSL_FOUND TRUE) + if(DEFINED OpenSSL_INCLUDE_DIR) + set(OPENSSL_INCLUDE_DIR ${OpenSSL_INCLUDE_DIR}) + endif() + if(DEFINED OpenSSL_Crypto_LIBS) + set(OPENSSL_CRYPTO_LIBRARY ${OpenSSL_Crypto_LIBS}) + set(OPENSSL_CRYPTO_LIBRARIES ${OpenSSL_Crypto_LIBS} + ${OpenSSL_Crypto_DEPENDENCIES} + ${OpenSSL_Crypto_FRAMEWORKS} + ${OpenSSL_Crypto_SYSTEM_LIBS}) + elseif(DEFINED openssl_OpenSSL_Crypto_LIBS_%(config)s) + set(OPENSSL_CRYPTO_LIBRARY ${openssl_OpenSSL_Crypto_LIBS_%(config)s}) + set(OPENSSL_CRYPTO_LIBRARIES ${openssl_OpenSSL_Crypto_LIBS_%(config)s} + ${openssl_OpenSSL_Crypto_DEPENDENCIES_%(config)s} + ${openssl_OpenSSL_Crypto_FRAMEWORKS_%(config)s} + ${openssl_OpenSSL_Crypto_SYSTEM_LIBS_%(config)s}) + endif() + if(DEFINED OpenSSL_SSL_LIBS) + set(OPENSSL_SSL_LIBRARY ${OpenSSL_SSL_LIBS}) + set(OPENSSL_SSL_LIBRARIES ${OpenSSL_SSL_LIBS} + ${OpenSSL_SSL_DEPENDENCIES} + ${OpenSSL_SSL_FRAMEWORKS} + ${OpenSSL_SSL_SYSTEM_LIBS}) + elseif(DEFINED openssl_OpenSSL_SSL_LIBS_%(config)s) + set(OPENSSL_SSL_LIBRARY ${openssl_OpenSSL_SSL_LIBS_%(config)s}) + set(OPENSSL_SSL_LIBRARIES ${openssl_OpenSSL_SSL_LIBS_%(config)s} + ${openssl_OpenSSL_SSL_DEPENDENCIES_%(config)s} + ${openssl_OpenSSL_SSL_FRAMEWORKS_%(config)s} + ${openssl_OpenSSL_SSL_SYSTEM_LIBS_%(config)s}) + endif() + if(DEFINED OpenSSL_LIBRARIES) + set(OPENSSL_LIBRARIES ${OpenSSL_LIBRARIES}) + endif() + if(DEFINED OpenSSL_VERSION) + set(OPENSSL_VERSION ${OpenSSL_VERSION}) + endif() + """% {"config":str(self.settings.build_type).upper()}) + save(self, module_file, content) + + @property + def _module_subfolder(self): + return os.path.join("lib", "cmake") + + @property + def _module_file_rel_path(self): + return os.path.join(self._module_subfolder, + "conan-official-{}-variables.cmake".format(self.name)) + + def package_info(self): + self.cpp_info.set_property("cmake_file_name", "OpenSSL") + self.cpp_info.set_property("cmake_find_mode", "both") + self.cpp_info.set_property("pkg_config_name", "openssl") + self.cpp_info.set_property("cmake_build_modules", [self._module_file_rel_path]) + self.cpp_info.names["cmake_find_package"] = "OpenSSL" + self.cpp_info.names["cmake_find_package_multi"] = "OpenSSL" + self.cpp_info.components["ssl"].builddirs.append(self._module_subfolder) + self.cpp_info.components["ssl"].build_modules["cmake_find_package"] = [self._module_file_rel_path] + self.cpp_info.components["ssl"].set_property("cmake_build_modules", [self._module_file_rel_path]) + self.cpp_info.components["crypto"].builddirs.append(self._module_subfolder) + self.cpp_info.components["crypto"].build_modules["cmake_find_package"] = [self._module_file_rel_path] + self.cpp_info.components["crypto"].set_property("cmake_build_modules", [self._module_file_rel_path]) + + if self._use_nmake: + libsuffix = "d" if self.settings.build_type == "Debug" else "" + self.cpp_info.components["ssl"].libs = ["libssl" + libsuffix] + self.cpp_info.components["crypto"].libs = ["libcrypto" + libsuffix] + else: + self.cpp_info.components["ssl"].libs = ["ssl"] + self.cpp_info.components["crypto"].libs = ["crypto"] + + self.cpp_info.components["ssl"].requires = ["crypto"] + + if not self.options.no_zlib: + self.cpp_info.components["crypto"].requires.append("zlib::zlib") + + if self.settings.os == "Windows": + self.cpp_info.components["crypto"].system_libs.extend(["crypt32", "ws2_32", "advapi32", "user32", "bcrypt"]) + elif self.settings.os == "Linux": + self.cpp_info.components["crypto"].system_libs.extend(["dl", "rt"]) + self.cpp_info.components["ssl"].system_libs.append("dl") + if not self.options.no_threads: + self.cpp_info.components["crypto"].system_libs.append("pthread") + self.cpp_info.components["ssl"].system_libs.append("pthread") + elif self.settings.os == "Neutrino": + self.cpp_info.components["crypto"].system_libs.append("atomic") + self.cpp_info.components["ssl"].system_libs.append("atomic") + + self.cpp_info.components["crypto"].set_property("cmake_target_name", "OpenSSL::Crypto") + self.cpp_info.components["crypto"].set_property("pkg_config_name", "libcrypto") + self.cpp_info.components["ssl"].set_property("cmake_target_name", "OpenSSL::SSL") + self.cpp_info.components["ssl"].set_property("pkg_config_name", "libssl") + self.cpp_info.components["crypto"].names["cmake_find_package"] = "Crypto" + self.cpp_info.components["crypto"].names["cmake_find_package_multi"] = "Crypto" + self.cpp_info.components["ssl"].names["cmake_find_package"] = "SSL" + self.cpp_info.components["ssl"].names["cmake_find_package_multi"] = "SSL" + + openssl_modules_dir = os.path.join(self.package_folder, "lib", "ossl-modules") + self.runenv_info.define_path("OPENSSL_MODULES", openssl_modules_dir) + + # For legacy 1.x downstream consumers, remove once recipe is 2.0 only: + self.env_info.OPENSSL_MODULES = openssl_modules_dir + diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/zlib/1.2.13/_/_/export/conanfile.py b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/zlib/1.2.13/_/_/export/conanfile.py new file mode 100644 index 000000000000..ead39ff73661 --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/zlib/1.2.13/_/_/export/conanfile.py @@ -0,0 +1,110 @@ +from conan import ConanFile +from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout +from conan.tools.files import apply_conandata_patches, export_conandata_patches, get, load, replace_in_file, save +from conan.tools.scm import Version +import os + +required_conan_version = ">=1.53.0" + + +class ZlibConan(ConanFile): + license = "Zlib" + name = "zlib" + package_type = "library" + url = "https://github.com/conan-io/conan-center-index" + homepage = "https://zlib.net" + description = ("A Massively Spiffy Yet Delicately Unobtrusive Compression Library " + "(Also Free, Not to Mention Unencumbered by Patents)") + topics = ("zlib", "compression") + + settings = "os", "arch", "compiler", "build_type" + options = { + "shared": [True, False], + "fPIC": [True, False], + } + default_options = { + "shared": False, + "fPIC": True, + } + + @property + def _is_mingw(self): + return self.settings.os == "Windows" and self.settings.compiler == "gcc" + + def export_sources(self): + export_conandata_patches(self) + + def config_options(self): + if self.settings.os == "Windows": + del self.options.fPIC + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + self.settings.rm_safe("compiler.libcxx") + self.settings.rm_safe("compiler.cppstd") + + def layout(self): + cmake_layout(self, src_folder="src") + + def source(self): + get(self, **self.conan_data["sources"][self.version], + destination=self.source_folder, strip_root=True) + + def generate(self): + tc = CMakeToolchain(self) + tc.variables["SKIP_INSTALL_ALL"] = False + tc.variables["SKIP_INSTALL_LIBRARIES"] = False + tc.variables["SKIP_INSTALL_HEADERS"] = False + tc.variables["SKIP_INSTALL_FILES"] = True + # Correct for misuse of "${CMAKE_INSTALL_PREFIX}/" in CMakeLists.txt + tc.variables["INSTALL_LIB_DIR"] = "lib" + tc.variables["INSTALL_INC_DIR"] = "include" + tc.variables["ZLIB_BUILD_EXAMPLES"] = False + tc.generate() + + def _patch_sources(self): + apply_conandata_patches(self) + + is_apple_clang12 = self.settings.compiler == "apple-clang" and Version(self.settings.compiler.version) >= "12.0" + if not is_apple_clang12: + for filename in ['zconf.h', 'zconf.h.cmakein', 'zconf.h.in']: + filepath = os.path.join(self.source_folder, filename) + replace_in_file(self, filepath, + '#ifdef HAVE_UNISTD_H ' + '/* may be set to #if 1 by ./configure */', + '#if defined(HAVE_UNISTD_H) && (1-HAVE_UNISTD_H-1 != 0)') + replace_in_file(self, filepath, + '#ifdef HAVE_STDARG_H ' + '/* may be set to #if 1 by ./configure */', + '#if defined(HAVE_STDARG_H) && (1-HAVE_STDARG_H-1 != 0)') + + def build(self): + self._patch_sources() + cmake = CMake(self) + cmake.configure() + cmake.build() + + def _extract_license(self): + tmp = load(self, os.path.join(self.source_folder, "zlib.h")) + license_contents = tmp[2:tmp.find("*/", 1)] + return license_contents + + def package(self): + save(self, os.path.join(self.package_folder, "licenses", "LICENSE"), self._extract_license()) + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.set_property("cmake_find_mode", "both") + self.cpp_info.set_property("cmake_file_name", "ZLIB") + self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") + self.cpp_info.set_property("pkg_config_name", "zlib") + if self.settings.os == "Windows" and not self._is_mingw: + libname = "zdll" if self.options.shared else "zlib" + else: + libname = "z" + self.cpp_info.libs = [libname] + + self.cpp_info.names["cmake_find_package"] = "ZLIB" + self.cpp_info.names["cmake_find_package_multi"] = "ZLIB" diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/empty.lock b/pkg/fanal/analyzer/language/c/conan/testdata/empty/conan.lock similarity index 100% rename from pkg/fanal/analyzer/language/c/conan/testdata/empty.lock rename to pkg/fanal/analyzer/language/c/conan/testdata/empty/conan.lock diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/happy.lock b/pkg/fanal/analyzer/language/c/conan/testdata/happy/conan.lock similarity index 100% rename from pkg/fanal/analyzer/language/c/conan/testdata/happy.lock rename to pkg/fanal/analyzer/language/c/conan/testdata/happy/conan.lock From d54d1ce3dd6de3d40150e90e2595ad8f23586a47 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Fri, 15 Mar 2024 16:04:28 +0600 Subject: [PATCH 03/13] docs: update conan page --- docs/docs/coverage/language/c.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/docs/coverage/language/c.md b/docs/docs/coverage/language/c.md index 6efe1e87e62b..0ae219d8370b 100644 --- a/docs/docs/coverage/language/c.md +++ b/docs/docs/coverage/language/c.md @@ -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) \ No newline at end of file +[^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). \ No newline at end of file From 2b06478f5733a03bf9dd1fe7fae70c98ad46b997 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Fri, 15 Mar 2024 16:24:17 +0600 Subject: [PATCH 04/13] fix: check empty app --- pkg/fanal/analyzer/language/c/conan/conan.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/fanal/analyzer/language/c/conan/conan.go b/pkg/fanal/analyzer/language/c/conan/conan.go index 21f8359468dc..6ad36ed341bc 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan.go +++ b/pkg/fanal/analyzer/language/c/conan/conan.go @@ -57,6 +57,10 @@ func (a conanLockAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAna 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 { From eb3d53d51a230ce7cc71ed14bb401c70b860d39d Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Fri, 15 Mar 2024 16:39:48 +0600 Subject: [PATCH 05/13] fix test --- pkg/fanal/analyzer/language/c/conan/conan_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/fanal/analyzer/language/c/conan/conan_test.go b/pkg/fanal/analyzer/language/c/conan/conan_test.go index 44014114c203..bdc7388281b9 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan_test.go +++ b/pkg/fanal/analyzer/language/c/conan/conan_test.go @@ -110,6 +110,7 @@ func Test_conanLockAnalyzer_Analyze(t *testing.T) { { name: "empty file", dir: "testdata/empty", + want: &analyzer.AnalysisResult{}, }, } @@ -125,8 +126,8 @@ func Test_conanLockAnalyzer_Analyze(t *testing.T) { FS: os.DirFS(tt.dir), }) - assert.NoError(t, err) - assert.Equal(t, tt.want, got) + require.NoError(t, err) + require.Equal(t, tt.want, got) }) } } From 58efb64fe4433044323007441918032de484b87e Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Fri, 15 Mar 2024 17:20:53 +0600 Subject: [PATCH 06/13] test: fix `Test_conanLockAnalyzer_Required` test --- pkg/fanal/analyzer/language/c/conan/conan_test.go | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pkg/fanal/analyzer/language/c/conan/conan_test.go b/pkg/fanal/analyzer/language/c/conan/conan_test.go index bdc7388281b9..efaefe7786ac 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan_test.go +++ b/pkg/fanal/analyzer/language/c/conan/conan_test.go @@ -3,7 +3,6 @@ package conan import ( "context" "os" - "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -156,16 +155,8 @@ func Test_conanLockAnalyzer_Required(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - dir := t.TempDir() - f, err := os.Create(filepath.Join(dir, tt.filePath)) - require.NoError(t, err) - defer f.Close() - - fi, err := f.Stat() - require.NoError(t, err) - a := conanLockAnalyzer{} - got := a.Required("", fi) + got := a.Required(tt.filePath, nil) assert.Equal(t, tt.want, got) }) } From 4a56a0d9bc0537d919fe61416d09c8a4f9ebaa28 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Fri, 15 Mar 2024 17:33:09 +0600 Subject: [PATCH 07/13] fix: sort libs --- pkg/fanal/analyzer/language/c/conan/conan.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/fanal/analyzer/language/c/conan/conan.go b/pkg/fanal/analyzer/language/c/conan/conan.go index 6ad36ed341bc..83ce5e4f6960 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan.go +++ b/pkg/fanal/analyzer/language/c/conan/conan.go @@ -8,6 +8,7 @@ import ( "os" "path" "path/filepath" + "sort" "strings" "golang.org/x/xerrors" @@ -70,6 +71,7 @@ func (a conanLockAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAna } } + sort.Sort(app.Libraries) apps = append(apps, *app) return nil }); err != nil { From 61af875ec3120ca1fcbae2a7e684173b40ca71b5 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Mon, 15 Apr 2024 11:00:43 +0600 Subject: [PATCH 08/13] refactor after merge main branch --- pkg/fanal/analyzer/language/c/conan/conan.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/fanal/analyzer/language/c/conan/conan.go b/pkg/fanal/analyzer/language/c/conan/conan.go index 83ce5e4f6960..deb661483a04 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan.go +++ b/pkg/fanal/analyzer/language/c/conan/conan.go @@ -17,8 +17,8 @@ import ( 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/log" "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" "github.com/aquasecurity/trivy/pkg/utils/fsutils" ) @@ -32,11 +32,13 @@ const ( // conanLockAnalyzer analyzes conan.lock 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 } @@ -48,7 +50,7 @@ func (a conanLockAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAna licenses, err := licensesFromCache() if err != nil { - log.Logger.Debugf("Unable to parse cache directory to obtain licenses: %s", err) + a.logger.Debug("Unable to parse cache directory to obtain licenses", log.Err(err)) } var apps []types.Application @@ -96,8 +98,7 @@ func licensesFromCache() (map[string]string, error) { cacheDir = path.Join(cacheDir, ".conan", "data") if !fsutils.DirExists(cacheDir) { - log.Logger.Debugf("The Conan cache directory (%s) was not found. Package licenses will be skipped", cacheDir) - return nil, nil + return nil, xerrors.Errorf("the Conan cache directory (%s) was not found.", cacheDir) } licenses := make(map[string]string) @@ -132,7 +133,7 @@ func licensesFromCache() (map[string]string, error) { licenses[name] = license return nil }); err != nil { - return nil, xerrors.Errorf("conan cache dir (%s) walk error: %w", cacheDir, err) + return nil, xerrors.Errorf("the Conan cache dir (%s) walk error: %w", cacheDir, err) } return licenses, nil } From 89003d66e95c433cd42bf98926b47a7deeb22dd5 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 23 Apr 2024 15:11:49 +0600 Subject: [PATCH 09/13] refactor: remove extra spaces --- pkg/fanal/analyzer/language/c/conan/conan.go | 10 +++++++--- .../.conan/data/openssl/3.0.5/_/_/export/conanfile.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/pkg/fanal/analyzer/language/c/conan/conan.go b/pkg/fanal/analyzer/language/c/conan/conan.go index deb661483a04..93e51f107b10 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan.go +++ b/pkg/fanal/analyzer/language/c/conan/conan.go @@ -109,15 +109,19 @@ func licensesFromCache() (map[string]string, error) { line := strings.TrimSpace(scanner.Text()) if strings.HasPrefix(line, "name") { // cf. https://docs.conan.io/1/reference/conanfile/attributes.html#name - // trim extra characters - e.g. `name = "openssl"` -> `openssl` - name = strings.TrimSuffix(strings.TrimPrefix(line, `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(line, `license = "`), `"`) + license = strings.TrimSuffix(strings.TrimPrefix(license, `license="`), `"`) // Check that the name is already found if name != "" { break diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py index 59da5e3bc243..ba393a6668fe 100644 --- a/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py +++ b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py @@ -15,11 +15,11 @@ class OpenSSLConan(ConanFile): - name = "openssl" + name="openssl" settings = "os", "arch", "compiler", "build_type" url = "https://github.com/conan-io/conan-center-index" homepage = "https://github.com/openssl/openssl" - license = "Apache-2.0" + license="Apache-2.0" topics = ("ssl", "tls", "encryption", "security") description = "A toolkit for the Transport Layer Security (TLS) and Secure Sockets Layer (SSL) protocols" options = { From 9b5d8cea1e38fc7311fae4c7a8bfd40dad7dbbf8 Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 23 Apr 2024 16:45:21 +0600 Subject: [PATCH 10/13] refactor: use strings.Cut for attributes --- pkg/fanal/analyzer/language/c/conan/conan.go | 33 ++++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/pkg/fanal/analyzer/language/c/conan/conan.go b/pkg/fanal/analyzer/language/c/conan/conan.go index 93e51f107b10..dc9c8f4971cb 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan.go +++ b/pkg/fanal/analyzer/language/c/conan/conan.go @@ -108,20 +108,17 @@ func licensesFromCache() (map[string]string, error) { 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="`), `"`) + // cf. https://docs.conan.io/1/reference/conanfile/attributes.html#name + if n, ok := detectAttribute("name", line); ok { + name = n // 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="`), `"`) + } + // cf. https://docs.conan.io/1/reference/conanfile/attributes.html#license + if l, ok := detectAttribute("license", line); ok { + license = l // Check that the name is already found if name != "" { break @@ -142,6 +139,22 @@ func licensesFromCache() (map[string]string, error) { 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, bool) { + if !strings.HasPrefix(line, attributeName) { + return "", false + } + + // 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, "\""), "\""), true + } + + return "", false +} + func (a conanLockAnalyzer) Required(filePath string, _ os.FileInfo) bool { // Lock file name can be anything // cf. https://docs.conan.io/1/versioning/lockfiles/introduction.html#locking-dependencies From 7bedafbbdd9f7979e2f62e0dfdcbe2c742ad83db Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 23 Apr 2024 16:53:52 +0600 Subject: [PATCH 11/13] refactor: remove bool output for `detectAttribute` --- pkg/fanal/analyzer/language/c/conan/conan.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/fanal/analyzer/language/c/conan/conan.go b/pkg/fanal/analyzer/language/c/conan/conan.go index dc9c8f4971cb..b031ed0b91bd 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan.go +++ b/pkg/fanal/analyzer/language/c/conan/conan.go @@ -109,7 +109,7 @@ func licensesFromCache() (map[string]string, error) { line := strings.TrimSpace(scanner.Text()) // cf. https://docs.conan.io/1/reference/conanfile/attributes.html#name - if n, ok := detectAttribute("name", line); ok { + if n := detectAttribute("name", line); n != "" { name = n // Check that the license is already found if license != "" { @@ -117,7 +117,7 @@ func licensesFromCache() (map[string]string, error) { } } // cf. https://docs.conan.io/1/reference/conanfile/attributes.html#license - if l, ok := detectAttribute("license", line); ok { + if l := detectAttribute("license", line); l != "" { license = l // Check that the name is already found if name != "" { @@ -141,18 +141,18 @@ func licensesFromCache() (map[string]string, error) { // 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, bool) { +func detectAttribute(attributeName, line string) string { if !strings.HasPrefix(line, attributeName) { - return "", false + 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, "\""), "\""), true + return strings.TrimPrefix(strings.TrimSuffix(attr, "\""), "\"") } - return "", false + return "" } func (a conanLockAnalyzer) Required(filePath string, _ os.FileInfo) bool { From 18b13831c0a5d3853e16d98dd17224870755576b Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Tue, 23 Apr 2024 16:54:02 +0600 Subject: [PATCH 12/13] test: add tests for detectAttribute --- .../analyzer/language/c/conan/conan_test.go | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/pkg/fanal/analyzer/language/c/conan/conan_test.go b/pkg/fanal/analyzer/language/c/conan/conan_test.go index efaefe7786ac..60f1e2a78ef5 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan_test.go +++ b/pkg/fanal/analyzer/language/c/conan/conan_test.go @@ -161,3 +161,56 @@ func Test_conanLockAnalyzer_Required(t *testing.T) { }) } } + +func Test_detectAttribute(t *testing.T) { + tests := []struct { + name string + attrName string + line string + want string + }{ + { + name: "without spaces near `=`", + attrName: "license", + line: `license="bar"`, + want: "bar", + }, + { + name: "with space before `=`", + attrName: "license", + line: `license ="bar"`, + want: "bar", + }, + { + name: "with space after `=`", + attrName: "license", + line: `license= "bar"`, + want: "bar", + }, + { + name: "with space before and after `=`", + attrName: "license", + line: `license = "bar"`, + want: "bar", + }, + { + name: "license with spaces", + attrName: "license", + line: `license = "foo and bar"`, + want: "foo and bar", + }, + { + name: "another attribute", + attrName: "license", + line: `license_contents = "foo"`, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := detectAttribute(tt.attrName, tt.line) + require.Equal(t, tt.want, got) + }) + } +} From 0d679665b864cbbe04ac8a3bd4610ea4e85d285b Mon Sep 17 00:00:00 2001 From: DmitriyLewen Date: Wed, 24 Apr 2024 09:22:09 +0600 Subject: [PATCH 13/13] refactor: use strings.Trim --- pkg/fanal/analyzer/language/c/conan/conan.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/fanal/analyzer/language/c/conan/conan.go b/pkg/fanal/analyzer/language/c/conan/conan.go index b031ed0b91bd..891ebac08695 100644 --- a/pkg/fanal/analyzer/language/c/conan/conan.go +++ b/pkg/fanal/analyzer/language/c/conan/conan.go @@ -149,7 +149,7 @@ func detectAttribute(attributeName, line string) string { // 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, "\""), "\"") + return strings.Trim(attr, `"`) } return ""