diff --git a/Dockerfile b/Dockerfile index 41e4c21..1c0ce92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,17 +26,20 @@ RUN wget https://github.com/syvaidya/openstego/releases/download/openstego-0.8.6 dpkg -i /tmp/openstego.deb && \ rm /tmp/openstego.deb -# Clean up apt cache and remove useless packages -RUN apt-get remove -y wget && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - +# Copy application COPY aperisolve/ /aperisolve/ RUN mkdir -p /data +# Install Python dependencies RUN pip install --no-cache-dir -r /aperisolve/requirements.txt ENV PYTHONUNBUFFERED=1 +# Auto clean after build +RUN apt-get remove -y wget && \ + apt-get autoremove -y && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:5000", "--access-logfile", "-", "--error-logfile", "-", "--log-level", "info", "--capture-output", "aperisolve.wsgi:application"] diff --git a/aperisolve/analyzers/binwalk.py b/aperisolve/analyzers/binwalk.py index 69920e7..6c05b9a 100644 --- a/aperisolve/analyzers/binwalk.py +++ b/aperisolve/analyzers/binwalk.py @@ -3,6 +3,7 @@ import shutil import subprocess from pathlib import Path +from typing import Any from .utils import MAX_PENDING_TIME, update_data @@ -17,7 +18,7 @@ def analyze_binwalk(input_img: Path, output_dir: Path) -> None: stderr = "" # Run binwalk data = subprocess.run( - ["binwalk", "-e", "../" + str(image_name), "--run-as=root"], + ["binwalk", "--matryoshka", "-e", "../" + str(image_name), "--run-as=root"], cwd=output_dir, capture_output=True, text=True, @@ -47,16 +48,17 @@ def analyze_binwalk(input_img: Path, output_dir: Path) -> None: # Only report error if stderr exists AND extraction failed. # If zip_exist is True, binwalk worked (despite warnings), so we proceed. if len(stderr) > 0 and not zip_exist: - err = { + err: dict[str, dict[str, Any]] = { "binwalk": { "status": "error", + "output": data.stdout.split("\n") if data else [], "error": stderr, } } update_data(output_dir, err) return None - output_data = { + output_data: dict[str, dict[str, Any]] = { "binwalk": { "status": "ok", "output": data.stdout.split("\n") if data else [], diff --git a/aperisolve/app.py b/aperisolve/app.py index 75359d2..f9e2ff9 100644 --- a/aperisolve/app.py +++ b/aperisolve/app.py @@ -6,6 +6,7 @@ import os import time from datetime import datetime, timezone from pathlib import Path +from shutil import rmtree from typing import Any, Optional from flask import Flask, Response, abort, jsonify, render_template, request, send_file @@ -112,34 +113,33 @@ def upload_image() -> tuple[Response, int]: submission_hash = hashlib.md5(submission_data).hexdigest() submission_path = RESULT_FOLDER / img_hash / submission_hash - # Check if hash is present on the DB - submission: Submission = Submission.query.filter_by( - hash=submission_hash - ).first() + # If submission already exists, return submission hash + if submission_path.exists(): + submission: Submission = Submission.query.filter_by( + hash=submission_hash + ).first() - # Only return if BOTH the file exists AND the DB entry exists - if submission_path.exists() and submission is not None: - return jsonify({"submission_hash": submission.hash}), 200 - + if submission is not None: # File exist but not in DB (should not happen), + rmtree(submission_path) # Removing file and continue processing as new + db.session.delete(submission) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member + else: + return jsonify({"submission_hash": submission.hash}), 200 - # Self-Healing Logic + # If image is new, create a new Image entry new_img_path = RESULT_FOLDER / img_hash / image_name + sub_img = Image.query.filter_by(hash=img_hash).first() # type: ignore - # Try to find image in DB by hash - sub_img = Image.query.filter_by( - hash=img_hash - ).first() + if new_img_path.parent.exists() and sub_img is None: + # Inconsistent state: image file exists but no DB entry + rmtree(new_img_path.parent) # Remove the existing files and continue - # If DB entry is missing (even if file exists on disk), create it! - if sub_img is None: - if not new_img_path.parent.exists() or not new_img_path.exists(): - new_img_path.parent.mkdir(parents=True, exist_ok=True) - with open(new_img_path, "wb") as f: - f.write(image_data) + if not new_img_path.parent.exists(): + new_img_path.parent.mkdir(parents=True, exist_ok=True) + with open(new_img_path, "wb") as f: # Write file to disk + f.write(image_data) - - # Recreate the missing Database Entry - sub_img = Image( + new_img = Image( file=str(new_img_path), hash=img_hash, size=len(image_data), @@ -147,39 +147,33 @@ def upload_image() -> tuple[Response, int]: first_submission_date=datetime.now(timezone.utc), last_submission_date=datetime.now(timezone.utc), ) - db.session.add(sub_img) - db.session.commit() + db.session.add(new_img) # pylint: disable=no-member + db.session.commit() # pylint: disable=no-member - # Increment upload count for the image + sub_img = Image.query.filter_by(hash=img_hash).first() # type: ignore sub_img.upload_count += 1 - db.session.commit() + db.session.commit() # pylint: disable=no-member # Create new Submission entry submission_path.mkdir(parents=True, exist_ok=True) + submission = Submission( + filename=image.filename, + password=password, + deep_analysis=deep_analysis, + hash=submission_hash, + status="pending", + date=time.time(), + image_hash=sub_img.hash, # type: ignore + ) + db.session.add(submission) # pylint: disable=no-member - # Only create a new submission if it does not already exist - # New submission for new image - if not submission: - submission = Submission( - filename=image.filename, - password=password, - deep_analysis=deep_analysis, - hash=submission_hash, - status="pending", - date=time.time(), - image_hash=sub_img.hash, - ) - db.session.add(submission) - db.session.commit() - + db.session.commit() # pylint: disable=no-member # Start the analysis jobs queue.enqueue("aperisolve.workers.analyze_image", submission.hash, job_timeout=300) - # Return submission identifier return jsonify({"submission_hash": submission.hash}), 200 - @app.route("/status/", methods=["GET"]) def get_status(hash_val: str) -> Response: """Get the processing status of a submission.""" diff --git a/aperisolve/static/js/aperisolve.js b/aperisolve/static/js/aperisolve.js index 54ecf94..0090c17 100644 --- a/aperisolve/static/js/aperisolve.js +++ b/aperisolve/static/js/aperisolve.js @@ -252,6 +252,38 @@ function parseResult(result) { analyzer.innerHTML += `

${capitalize(tool.replace("_"," "))}

`; + // Parse text output + if (typeof result[tool]["output"] === "string") { + analyzer.innerHTML += `
${escapeHtml(
+        result[tool]["output"]
+      )}
`; + } else if (Array.isArray(result[tool]["output"])) { + if (result[tool]["output"].length > 0) { + var texarea_content = `
`; + texarea_content += ``; + texarea_content += ``; + texarea_content += `
`; + analyzer.innerHTML += texarea_content; + } + } else if (typeof result[tool]["output"] === "object") { + var table_content = `
`; + table_content += ``; + for (const key in result[tool]["output"]) { + table_content += ``; + table_content += ``; + } + table_content += `
${escapeHtml(key)}${escapeHtml( + result[tool]["output"][key] + )}
`; + table_content += ``; + analyzer.innerHTML += table_content; + } + + // Parse images, downloads, ... if (result[tool]["status"] === "ok") { if ("images" in result[tool]) { // Parse image output @@ -286,41 +318,6 @@ function parseResult(result) { } } - // Parse text output - - - if (typeof result[tool]["output"] === "string") { - analyzer.innerHTML += `
${escapeHtml(
-          result[tool]["output"]
-        )}
`; - } else if (Array.isArray(result[tool]["output"])) { - if (result[tool]["output"].length > 0) { - var texarea_content = `
`; - texarea_content += ``; - texarea_content += ``; - texarea_content += `
`; - analyzer.innerHTML += texarea_content; - } - } else if (typeof result[tool]["output"] === "object") { - var table_content = `
`; - table_content += ``; - for (const key in result[tool]["output"]) { - table_content += ``; - table_content += ``; - } - table_content += `
${escapeHtml(key)}${escapeHtml( - result[tool]["output"][key] - )}
`; - table_content += ``; - analyzer.innerHTML += table_content; - } else { - // analyzer.innerHTML += `
${result[tool]["output"]}
`; - } - if ("download" in result[tool]) { // Parse download link analyzer.innerHTML += `