From f4d2949d1959d981888de97f33df49c2739d05d7 Mon Sep 17 00:00:00 2001 From: Jaekwon Bang Date: Wed, 17 Sep 2025 17:41:56 +0900 Subject: [PATCH] Use Dependency-check v12.1.3 to analyze .jar --- requirements.txt | 1 - src/fosslight_binary/__init__.py | 149 ++++++++++++++++++++++++++ src/fosslight_binary/_binary_dao.py | 2 +- src/fosslight_binary/_jar_analysis.py | 57 +++++++--- 4 files changed, 195 insertions(+), 14 deletions(-) diff --git a/requirements.txt b/requirements.txt index fe5a598..9cff919 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,3 @@ pytz XlsxWriter PyYAML fosslight_util>=2.1.13 -dependency-check diff --git a/src/fosslight_binary/__init__.py b/src/fosslight_binary/__init__.py index e69de29..8da15a8 100644 --- a/src/fosslight_binary/__init__.py +++ b/src/fosslight_binary/__init__.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2025 LG Electronics Inc. +# SPDX-License-Identifier: Apache-2.0 +import logging +import os +import stat +import subprocess +import tempfile +import urllib.request +import zipfile +import sys + +logger = logging.getLogger(__name__) +DEPENDENCY_CHECK_VERSION = "12.1.7" + + +def _install_dependency_check(): + """Install OWASP dependency-check""" + try: + # Skip if explicitly disabled + if os.environ.get('FOSSLIGHT_SKIP_AUTO_INSTALL', '').lower() in ('1', 'true', 'yes'): + logger.info("Auto-install disabled by environment variable") + return + + env_home = os.environ.get('DEPENDENCY_CHECK_HOME', '').strip() + install_dir = None + forced_env = False + if env_home: + # Normalize + env_home_abs = os.path.abspath(env_home) + # Detect if env_home already the actual extracted root (ends with dependency-check) + candidate_bin_win = os.path.join(env_home_abs, 'bin', 'dependency-check.bat') + candidate_bin_nix = os.path.join(env_home_abs, 'bin', 'dependency-check.sh') + if os.path.exists(candidate_bin_win) or os.path.exists(candidate_bin_nix): + # env points directly to dependency-check root; install_dir is its parent + install_dir = os.path.dirname(env_home_abs) + forced_env = True + else: + # Assume env_home is the base directory where we should extract dependency-check/ + install_dir = env_home_abs + + if not install_dir: + # Fallback hierarchy: executable dir (if frozen) -> CWD + candidate_base = None + if getattr(sys, 'frozen', False): + exe_dir = os.path.dirname(os.path.abspath(sys.executable)) + candidate_base = os.path.join(exe_dir, 'fosslight_dc_bin') + + if not os.access(exe_dir, os.W_OK): + candidate_base = None + else: + logger.debug(f"Using executable directory base: {candidate_base}") + if not candidate_base: + candidate_base = os.path.abspath(os.path.join(os.getcwd(), 'fosslight_dc_bin')) + install_dir = candidate_base + else: + logger.debug(f"Resolved install_dir: {install_dir}") + bin_dir = os.path.join(install_dir, 'dependency-check', 'bin') + if sys.platform.startswith('win'): + dc_path = os.path.join(bin_dir, 'dependency-check.bat') + else: + dc_path = os.path.join(bin_dir, 'dependency-check.sh') + + # Check if dependency-check already exists + if os.path.exists(dc_path): + try: + result = subprocess.run([dc_path, '--version'], capture_output=True, text=True, timeout=10) + if result.returncode == 0: + logger.debug("dependency-check already installed and working") + # If we detected an existing root via env, retain it, else set home now. + if forced_env: + os.environ['DEPENDENCY_CHECK_HOME'] = env_home_abs + else: + os.environ['DEPENDENCY_CHECK_HOME'] = os.path.join(install_dir, 'dependency-check') + os.environ['DEPENDENCY_CHECK_VERSION'] = DEPENDENCY_CHECK_VERSION + return + except (subprocess.TimeoutExpired, FileNotFoundError) as ex: + logger.debug(f"Exception in dependency-check --version: {ex}") + pass + + # Download URL + download_url = (f"https://github.com/dependency-check/DependencyCheck/releases/" + f"download/v{DEPENDENCY_CHECK_VERSION}/" + f"dependency-check-{DEPENDENCY_CHECK_VERSION}-release.zip") + + os.makedirs(install_dir, exist_ok=True) + logger.info(f"Downloading dependency-check {DEPENDENCY_CHECK_VERSION} from {download_url} ...") + + # Download and extract + with urllib.request.urlopen(download_url) as response: + content = response.read() + + with tempfile.NamedTemporaryFile(suffix='.zip', delete=False) as tmp_file: + tmp_file.write(content) + tmp_zip_path = tmp_file.name + + with zipfile.ZipFile(tmp_zip_path, 'r') as zip_ref: + zip_ref.extractall(install_dir) + os.unlink(tmp_file.name) + + # Make shell scripts executable + if os.path.exists(bin_dir): + if sys.platform.startswith('win'): + # Windows: .bat files only + scripts = ["dependency-check.bat"] + else: + # Linux/macOS: .sh files only + scripts = ["dependency-check.sh", "completion-for-dependency-check.sh"] + + for script in scripts: + script_path = os.path.join(bin_dir, script) + if os.path.exists(script_path): + st = os.stat(script_path) + os.chmod(script_path, st.st_mode | stat.S_IEXEC) + + logger.info("✅ OWASP dependency-check installed successfully!") + logger.info(f"Installed to: {os.path.join(install_dir, 'dependency-check')}") + + # Set environment variables after successful installation + os.environ['DEPENDENCY_CHECK_VERSION'] = DEPENDENCY_CHECK_VERSION + os.environ['DEPENDENCY_CHECK_HOME'] = os.path.join(install_dir, 'dependency-check') + + return True + + except Exception as e: + logger.error(f"Failed to install dependency-check: {e}") + logger.info("dependency-check can be installed manually from: https://github.com/dependency-check/DependencyCheck/releases") + return False + + +def _auto_install_dependencies(): + """Auto-install required dependencies if not present.""" + # Only run this once per session + if hasattr(_auto_install_dependencies, '_already_run'): + return + _auto_install_dependencies._already_run = True + + try: + # Install binary version + _install_dependency_check() + + logger.info(f"✅ dependency-check setup completed with version {DEPENDENCY_CHECK_VERSION}") + except Exception as e: + logger.warning(f"Auto-install failed: {e}") + + +# Auto-install on import +_auto_install_dependencies() diff --git a/src/fosslight_binary/_binary_dao.py b/src/fosslight_binary/_binary_dao.py index d9c361f..a9f8ba2 100755 --- a/src/fosslight_binary/_binary_dao.py +++ b/src/fosslight_binary/_binary_dao.py @@ -9,7 +9,7 @@ import pandas as pd import socket from urllib.parse import urlparse -from ._binary import TLSH_CHECKSUM_NULL +from fosslight_binary._binary import TLSH_CHECKSUM_NULL from fosslight_util.oss_item import OssItem import fosslight_util.constant as constant diff --git a/src/fosslight_binary/_jar_analysis.py b/src/fosslight_binary/_jar_analysis.py index c4f2043..016ef1d 100644 --- a/src/fosslight_binary/_jar_analysis.py +++ b/src/fosslight_binary/_jar_analysis.py @@ -7,23 +7,26 @@ import json import os import sys +import subprocess import fosslight_util.constant as constant -from ._binary import BinaryItem, VulnerabilityItem, is_package_dir +from fosslight_binary._binary import BinaryItem, VulnerabilityItem, is_package_dir from fosslight_util.oss_item import OssItem -from dependency_check import run as dependency_check_run - logger = logging.getLogger(constant.LOGGER_NAME) -def run_analysis(params, func): +def run_analysis(command): try: - sys.argv = params - func() - except SystemExit: - pass + result = subprocess.run(command, text=True, timeout=600) + if result.returncode != 0: + logger.error(f"dependency-check failed with return code {result.returncode}") + raise Exception(f"dependency-check failed with return code {result.returncode}") + except subprocess.TimeoutExpired: + logger.error("dependency-check command timed out") + raise except Exception as ex: logger.error(f"Run Analysis : {ex}") + raise def get_oss_ver(version_info): @@ -185,12 +188,27 @@ def analyze_jar_file(path_to_find_bin, path_to_exclude): success = True json_file = "" - command = ['dependency-check', '--scan', f'{path_to_find_bin}', '--out', f'{path_to_find_bin}', + # Use fixed install path: ./fosslight_dc_bin/dependency-check/bin/dependency-check.sh or .bat + if sys.platform.startswith('win'): + depcheck_path = os.path.abspath(os.path.join(os.getcwd(), 'fosslight_dc_bin', 'dependency-check', 'bin', 'dependency-check.bat')) + elif sys.platform.startswith('linux'): + depcheck_path = os.path.abspath(os.path.join(os.getcwd(), 'fosslight_dc_bin', 'dependency-check', 'bin', 'dependency-check.sh')) + elif sys.platform.startswith('darwin'): + depcheck_path = os.path.abspath(os.path.join(os.getcwd(), 'dependency-check')) + + if not (os.path.isfile(depcheck_path) and os.access(depcheck_path, os.X_OK)): + logger.error(f'dependency-check script not found or not executable at {depcheck_path}') + success = False + return owasp_items, vulnerability_items, success + + command = [depcheck_path, '--scan', f'{path_to_find_bin}', '--out', f'{path_to_find_bin}', '--disableArchive', '--disableAssembly', '--disableRetireJS', '--disableNodeJS', '--disableNodeAudit', '--disableNugetconf', '--disableNuspec', '--disableOpenSSL', - '--disableOssIndex', '--disableBundleAudit', '--cveValidForHours', '24', '-f', 'JSON'] + '--disableOssIndex', '--disableBundleAudit', '--disableOssIndex', '--nvdValidForHours', '168', + '--nvdDatafeed', 'https://nvd.nist.gov/feeds/json/cve/2.0/nvdcve-2.0-{0}.json.gz', '-f', 'JSON'] + try: - run_analysis(command, dependency_check_run) + run_analysis(command) except Exception as ex: logger.info(f"Error to analyze .jar file - OSS information for .jar file isn't included in report.\n {ex}") success = False @@ -239,7 +257,22 @@ def analyze_jar_file(path_to_find_bin, path_to_exclude): if not bin_with_path.endswith('.jar'): bin_with_path = bin_with_path.split('.jar')[0] + '.jar' - file_with_path = os.path.relpath(bin_with_path, path_to_find_bin) + try: + path_to_fild_bin_abs = os.path.abspath(path_to_find_bin) + bin_with_path_abs = os.path.abspath(bin_with_path) + if os.name == 'nt': # Windows + drive_bin = os.path.splitdrive(bin_with_path_abs)[0].lower() + drive_root = os.path.splitdrive(path_to_fild_bin_abs)[0].lower() + # Different drive or UNC root -> fallback to basename + if drive_bin and drive_root and drive_bin != drive_root: + file_with_path = os.path.basename(bin_with_path_abs) + else: + file_with_path = os.path.relpath(bin_with_path_abs, path_to_fild_bin_abs) + else: + file_with_path = os.path.relpath(bin_with_path_abs, path_to_fild_bin_abs) + except Exception as e: + file_with_path = os.path.basename(bin_with_path) + logger.error(f"relpath error: {e}; fallback basename: {file_with_path}") # First, Get OSS Name and Version info from pkg_info for pkg_info in all_pkg_info: