diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml index 963e54e..8cc5eb3 100644 --- a/.github/workflows/docker-build-push.yml +++ b/.github/workflows/docker-build-push.yml @@ -31,4 +31,52 @@ jobs: platforms: linux/amd64,linux/arm64 tags: | fosslight/fosslight_scanner:latest - fosslight/fosslight_scanner:${{ github.event.release.tag_name }} \ No newline at end of file + fosslight/fosslight_scanner:${{ github.event.release.tag_name }} + + create-windows-executable: + runs-on: windows-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Install dependencies + run: | + pip install pyinstaller + + - name: Create executable + run: pyinstaller --onefile fosslight_wrapper.py + + - name: Upload executable to release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./dist/fosslight_wrapper.exe + asset_name: fosslight_wrapper.exe + asset_content_type: application/vnd.microsoft.portable-executable + + create-macos-command-file: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Create .command file + run: | + chmod +x fosslight_wrapper_mac.command + + - name: Upload .command file to release + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./fosslight_wrapper_mac.command + asset_name: fosslight_wrapper_mac.command + asset_content_type: application/x-sh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 35f15eb..e683b19 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,23 +3,74 @@ FROM python:3.8-slim-buster COPY . /app -WORKDIR /app +WORKDIR /app -RUN ln -sf /bin/bash /bin/sh && \ +# Install necessary packages including nodejs, npm, and default-jdk +RUN ln -sf /bin/bash /bin/sh && \ apt-get update && \ - apt-get install --no-install-recommends -y \ + apt-get install --no-install-recommends -y \ build-essential \ python3 python3-distutils python3-pip python3-dev python3-magic \ libxml2-dev \ libxslt1-dev \ libhdf5-dev \ - bzip2 xz-utils zlib1g libpopt0 && \ + bzip2 xz-utils zlib1g libpopt0 \ + curl \ + default-jdk && \ + curl -sL https://deb.nodesource.com/setup_14.x | bash - && \ + apt-get install -y nodejs && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* -RUN pip3 install --upgrade pip && \ - pip3 install . && \ - pip3 install dparse && \ - rm -rf ~/.cache/pip /root/.cache/pipe +# Set JAVA_HOME dynamically +RUN echo "export JAVA_HOME=\$(dirname \$(dirname \$(readlink -f \$(which java))))" >> /etc/profile && \ + echo "export PATH=\$JAVA_HOME/bin:\$PATH" >> /etc/profile -ENTRYPOINT ["/usr/local/bin/fosslight"] +# Install license-checker globally +RUN npm install -g license-checker + +RUN pip3 install --upgrade pip && \ + pip3 install fosslight_util && \ + pip3 install python-magic && \ + pip3 install dparse + +RUN pip3 install fosslight_source --no-deps && \ + pip3 show fosslight_source | grep "Requires:" | sed 's/Requires://' | tr ',' '\n' | grep -v "typecode-libmagic" > /tmp/fosslight_source_deps.txt && \ + pip3 install -r /tmp/fosslight_source_deps.txt && \ + rm /tmp/fosslight_source_deps.txt + +COPY requirements.txt /tmp/requirements.txt +RUN grep -vE "fosslight[-_]source" /tmp/requirements.txt > /tmp/custom_requirements.txt && \ + pip3 install -r /tmp/custom_requirements.txt && \ + rm /tmp/requirements.txt /tmp/custom_requirements.txt + +COPY . /fosslight_scanner +WORKDIR /fosslight_scanner +RUN pip3 install . --no-deps && \ + rm -rf ~/.cache/pip /root/.cache/pip + +# Add /usr/local/bin to the PATH +ENV PATH="/usr/local/bin:${PATH}" + +VOLUME /src +WORKDIR /src + +# Create and set up the entrypoint script +RUN echo '#!/bin/bash' > /entrypoint.sh && \ + echo 'source /etc/profile' >> /entrypoint.sh && \ + echo 'export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java))))' >> /entrypoint.sh && \ + echo 'export PATH=$JAVA_HOME/bin:$PATH' >> /entrypoint.sh && \ + echo 'if command -v "$1" > /dev/null 2>&1; then' >> /entrypoint.sh && \ + echo ' exec "$@"' >> /entrypoint.sh && \ + echo 'else' >> /entrypoint.sh && \ + echo ' exec fosslight "$@"' >> /entrypoint.sh && \ + echo 'fi' >> /entrypoint.sh && \ + chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] + +CMD ["-h"] + +# Clean up the build +RUN apt-get clean && \ + rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..ecf2ec0 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# Copyright (c) 2022 LG Electronics Inc. +# SPDX-License-Identifier: Apache-2.0 + +# Set JAVA_HOME dynamically +export JAVA_HOME=$(dirname $(dirname $(readlink -f $(which java)))) +export PATH=$JAVA_HOME/bin:$PATH + +# Check if the first argument is a command, if so execute it +if command -v "$1" > /dev/null 2>&1; then + exec "$@" +else + # If not a command, run fosslight with arguments + exec fosslight "$@" +fi \ No newline at end of file diff --git a/fosslight_wrapper.py b/fosslight_wrapper.py new file mode 100644 index 0000000..264862f --- /dev/null +++ b/fosslight_wrapper.py @@ -0,0 +1,275 @@ +# Copyright (c) 2022 LG Electronics Inc. +# SPDX-License-Identifier: Apache-2.0 + +import sys +import io +import subprocess +import logging +from datetime import datetime +import os + + +def setup_logging(): + current_time = datetime.now().strftime("%Y%m%d_%H%M") + log_filename = f'fosslight_log_{current_time}.txt' + logging.basicConfig(filename=log_filename, level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + encoding='utf-8') + + +def is_double_clicked(): + return sys.argv[0].endswith('.exe') and len(sys.argv) == 1 + + +def check_and_pull_image(image_name): + try: + # Check if the image exists locally + result = subprocess.run(["docker", "image", "inspect", image_name], + capture_output=True, text=True) + if result.returncode == 0: + logging.info(f"Image {image_name} already exists locally.") + return True + + # If the image doesn't exist, pull it + logging.info(f"Pulling the image {image_name} from Docker Hub") + subprocess.run(["docker", "pull", image_name], check=True) + logging.info(f"Successfully pulled the image {image_name}") + return True + except subprocess.CalledProcessError as e: + logging.error(f"Error with Docker image {image_name}: {e}") + return False + + +def get_user_input(auto_image=None): + if auto_image: + return auto_image, 'local', os.getcwd() + + print("FossLight Wrapper") + image = input("Enter Docker image name (e.g., fosslight/fosslight_scanner:latest): ") + analysis_type = input("Choose analysis type (1 for local path, 2 for Git repository): ") + + if analysis_type == '1': + input_path = input("Enter local path to analyze: ") + return image, 'local', input_path + elif analysis_type == '2': + git_url = input("Enter Git repository URL to analyze: ") + return image, 'git', git_url + else: + print("Invalid choice. Exiting.") + sys.exit(1) + + +def display_current_options(options): + if not options: + print("Only the default option has been applied.") + else: + print("Current additional options:") + for i, option in enumerate(options, 1): + print(f"{i}. {option}") + + +def get_additional_options(): + options = [] + while True: + print("\nManage additional options:") + print("1. Add new option") + print("2. Remove option") + print("3. View current options") + print("4. Finish and proceed") + + choice = input("\nEnter your choice (1-4): ") + + if choice == '1': + options.extend(add_option()) + elif choice == '2': + options = remove_option(options) + elif choice == '3': + display_current_options(options) + elif choice == '4': + break + else: + print("Invalid choice. Please try again.") + + return options + + +def add_option(): + print("\nAvailable additional options:") + print("1. -f : FOSSLight Report file format (excel, yaml)") + print("2. -c : Number of processes to analyze source") + print("3. -r: Keep raw data") + print("4. -t: Hide the progress bar") + print("5. -s : Path to apply setting from file") + print("6. --no_correction: Don't correct OSS information") + print("7. --correct_fpath : Path to the sbom-info.yaml file") + print("8. -u : DB Connection (for 'all' or 'bin' mode)") + print("9. -d : Additional arguments for dependency analysis") + + choice = input("\nEnter the number of the option you want to add: ") + + if choice == '1': + format_type = input("Enter format (excel/yaml): ") + return ['-f', format_type] + elif choice == '2': + processes = input("Enter number of processes: ") + return ['-c', processes] + elif choice == '3': + return ['-r'] + elif choice == '4': + return ['-t'] + elif choice == '5': + settings_path = input("Enter path to settings file: ") + return ['-s', settings_path] + elif choice == '6': + return ['--no_correction'] + elif choice == '7': + sbom_path = input("Enter path to sbom-info.yaml: ") + return ['--correct_fpath', sbom_path] + elif choice == '8': + db_url = input("Enter DB URL: ") + return ['-u', db_url] + elif choice == '9': + dep_arg = input("Enter dependency argument: ") + return ['-d', dep_arg] + else: + print("Invalid option. No option added.") + return [] + + +def remove_option(options): + if not options: + print("No options to remove.") + return options + + display_current_options(options) + choice = input("Enter the number of the option you want to remove (or 0 to cancel): ") + + try: + index = int(choice) - 1 + if 0 <= index < len(options): + removed_option = options.pop(index) + print(f"Removed option: {removed_option}") + elif index == -1: + print("Removal cancelled.") + else: + print("Invalid number. No option removed.") + except ValueError: + print("Invalid input. No option removed.") + + return options + + +def remove_wfp_file(output_path): + wfp_file = os.path.join(output_path, "scanner_output.wfp") + if os.path.exists(wfp_file): + try: + os.remove(wfp_file) + logging.info(f"Successfully removed WFP file: {wfp_file}") + except Exception as e: + logging.error(f"Failed to remove WFP file: {wfp_file}. Error: {e}") + + +def run_fosslight(image, analysis_type, input_source, output_path, additional_options): + # Convert Windows paths to Docker-compatible paths + output_path = output_path.replace('\\', '/').replace('C:', '/c') + + # Construct the Docker command + docker_cmd = [ + "docker", "run", "--rm", + "-v", f"{output_path}:/output" + ] + + if analysis_type == 'local': + input_path = input_source.replace('\\', '/').replace('C:', '/c') + docker_cmd.extend(["-v", f"{input_path}:/src"]) + + docker_cmd.extend([ + image, + "fosslight", + "-o", "/output", + ]) + + if analysis_type == 'local': + docker_cmd.extend(["-p", "/src"]) + else: # Git repository + docker_cmd.extend(["-w", input_source]) + + # Add additional options + docker_cmd.extend(additional_options) + + # Log the Docker command + logging.info(f"Running Docker command: {' '.join(docker_cmd)}") + + # Run the Docker command with real-time output and UTF-8 encoding + try: + process = subprocess.Popen(docker_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + bufsize=1, universal_newlines=True, encoding='utf-8') + for line in process.stdout: + line = line.strip() + if line: # Only log non-empty lines + print(line) # Print to console in real-time + sys.stdout.flush() # Ensure real-time output + logging.info(line) # Log to file + process.wait() + if process.returncode != 0: + logging.error(f"FossLight exited with error code {process.returncode}") + else: + logging.info("FossLight completed successfully") + except subprocess.CalledProcessError as e: + logging.error(f"Error running FossLight: {e}") + except Exception as e: + logging.error(f"Unexpected error: {e}") + + remove_wfp_file(output_path) + + +def get_execution_mode(): + if len(sys.argv) > 1 and sys.argv[1] == "--manual": + return "manual" + return "auto" + + +def main(): + # Redirect stdout to use utf-8 encoding without buffering + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', line_buffering=True) + + setup_logging() + + execution_mode = get_execution_mode() + + if execution_mode == "auto": + logging.info("Executing in automatic mode (double-click)") + image_name = "fosslight/fosslight_scanner:latest" + if not check_and_pull_image(image_name): + print(f"Failed to ensure the presence of the Docker image: {image_name}") + input("Press Enter to exit...") + sys.exit(1) + + current_dir = os.getcwd() + image, analysis_type, input_source = image_name, 'local', current_dir + output_path = current_dir + additional_options = ["-f", "excel"] + else: + logging.info("Executing in manual mode (command prompt)") + image, analysis_type, input_source = get_user_input() + output_path = input("Enter path for output: ") + additional_options = get_additional_options() + + # Ensure no duplicate options + additional_options = list(dict.fromkeys(additional_options)) + + logging.info("Starting FossLight wrapper") + logging.info(f"Docker image: {image}") + logging.info(f"Analysis type: {analysis_type}") + logging.info(f"Input source: {input_source}") + logging.info(f"Output path: {output_path}") + logging.info(f"Additional options: {' '.join(additional_options)}") + + run_fosslight(image, analysis_type, input_source, output_path, additional_options) + + print("\nFossLight wrapper completed. Press Enter to exit.") + input() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/fosslight_wrapper_mac.command b/fosslight_wrapper_mac.command new file mode 100755 index 0000000..ae9ec75 --- /dev/null +++ b/fosslight_wrapper_mac.command @@ -0,0 +1,104 @@ +#!/bin/bash + +# Copyright (c) 2022 LG Electronics Inc. +# SPDX-License-Identifier: Apache-2.0 + +# FossLight Wrapper Shell Script + +# Get the directory of the script +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Default values +IMAGE="fosslight/fosslight_scanner:latest" +OUTPUT_DIR="$SCRIPT_DIR/fosslight_output" +MANUAL_MODE=false + +# Function to check if running in terminal +is_running_in_terminal() { + [ -t 0 ] +} + +# Function to pause execution (for GUI mode) +pause() { + if ! is_running_in_terminal; then + echo "Press Enter to exit..." + read + fi +} + +# Function to check and pull Docker image +check_and_pull_image() { + if ! docker image inspect "$IMAGE" &> /dev/null; then + echo "Pulling image $IMAGE..." + if ! docker pull "$IMAGE"; then + echo "Failed to pull image $IMAGE" + exit 1 + fi + fi +} + +# Function to get user input in manual mode +get_user_input() { + echo "FossLight Wrapper (Manual Mode)" + read -p "Choose analysis type (1 for local path, 2 for Git repository): " analysis_type + if [ "$analysis_type" == "1" ]; then + read -p "Enter local path to analyze: " input_source + analysis_type="local" + elif [ "$analysis_type" == "2" ]; then + read -p "Enter Git repository URL to analyze: " input_source + analysis_type="git" + else + echo "Invalid choice. Exiting." + exit 1 + fi + read -p "Enter path for output (default: $OUTPUT_DIR): " user_output + OUTPUT_DIR=${user_output:-$OUTPUT_DIR} +} + +# Function to run FossLight +run_fosslight() { + local docker_cmd="docker run --rm" + + if [ "$analysis_type" == "local" ]; then + docker_cmd="$docker_cmd -v $input_source:/src -v $OUTPUT_DIR:/output" + docker_cmd="$docker_cmd $IMAGE fosslight -p /src -o /output -f excel" + else + docker_cmd="$docker_cmd -v $OUTPUT_DIR:/output" + docker_cmd="$docker_cmd $IMAGE fosslight -o /output -w $input_source -f excel" + fi + + echo "Running FossLight..." + if ! eval $docker_cmd; then + echo "Error running FossLight" + exit 1 + fi +} + +# Main execution +if [ "$1" == "--manual" ]; then + MANUAL_MODE=true +fi + +if [ "$MANUAL_MODE" = true ]; then + get_user_input +else + echo "FossLight Wrapper (Automatic Mode)" + analysis_type="local" + input_source="$SCRIPT_DIR" + echo "Analyzing directory: $input_source" +fi + +# Ensure output directory exists +mkdir -p "$OUTPUT_DIR" + +# Change to the script directory +cd "$SCRIPT_DIR" + +# Check and pull Docker image +check_and_pull_image + +# Run FossLight +run_fosslight + +echo "FossLight analysis completed. Results are in $OUTPUT_DIR" +pause \ No newline at end of file