From d0c2f2480044a974b5ec16fcceff0f3a6dd6a83e Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sun, 28 Dec 2025 10:33:46 +0100 Subject: [PATCH 1/5] WIP --- Dockerfile | 35 +++++++-------- aperisolve/analyzers/binwalk.py | 8 ++-- aperisolve/static/js/aperisolve.js | 72 +++++++++++++++--------------- 3 files changed, 56 insertions(+), 59 deletions(-) diff --git a/Dockerfile b/Dockerfile index fe69aa9..1ac3310 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,22 +3,14 @@ FROM python:3.14-slim AS build WORKDIR / # Install build tools and dependencies -RUN apt-get update && apt-get install -y \ - wget \ - default-jre \ - ruby \ - zip \ - 7zip -# Install steganography and forensics tools -RUN apt-get update && apt-get install -y \ - binwalk \ - foremost \ - exiftool \ - steghide \ - binutils \ - outguess \ - pngcheck \ + +RUN apt-get update && apt-get install -y --no-install-recommends \ + wget default-jre ruby \ + bzip2 xz-utils gzip lzip lzma lzop tar unzip p7zip-full squashfs-tools \ + file qpdf cpio arj cabextract sharutils lz4 ccache mtd-utils \ + python3-lxml upx-ucl zlib1g-dev liblzma-dev \ + binwalk foremost exiftool steghide binutils outguess pngcheck \ && gem install zsteg # Install OpenStego @@ -26,17 +18,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 32d6fec..823590c 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, @@ -45,16 +46,17 @@ def analyze_binwalk(input_img: Path, output_dir: Path) -> None: shutil.rmtree(extracted_dir) if len(stderr) > 0: - 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/static/js/aperisolve.js b/aperisolve/static/js/aperisolve.js index a093ce7..d7c1e89 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,48 +318,16 @@ 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 += `
Download file`; } - } else if (result[tool]["status"] === "error") { + } + + // Parse errors + if (result[tool]["status"] === "error") { showDanger(result[tool]["error"]); } } From b15dca9c537d418dba39567761b38198207ed5d1 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Tue, 30 Dec 2025 16:57:08 +0100 Subject: [PATCH 2/5] Fix with fs remove --- aperisolve/app.py | 80 ++++++++++++++++++++++------------------------- 1 file changed, 37 insertions(+), 43 deletions(-) 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.""" From 64f13f53c395c8235c9c00a7993c33fd35e77b6c Mon Sep 17 00:00:00 2001 From: Zeecka Date: Tue, 30 Dec 2025 17:06:08 +0100 Subject: [PATCH 3/5] Fix Dockerfile --- Dockerfile | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1ac3310..1c0ce92 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,22 @@ FROM python:3.14-slim AS build WORKDIR / # Install build tools and dependencies +RUN apt-get update && apt-get install -y \ + wget \ + default-jdk \ + ruby \ + zip \ + 7zip - -RUN apt-get update && apt-get install -y --no-install-recommends \ - wget default-jre ruby \ - bzip2 xz-utils gzip lzip lzma lzop tar unzip p7zip-full squashfs-tools \ - file qpdf cpio arj cabextract sharutils lz4 ccache mtd-utils \ - python3-lxml upx-ucl zlib1g-dev liblzma-dev \ - binwalk foremost exiftool steghide binutils outguess pngcheck \ +# Install steganography and forensics tools +RUN apt-get update && apt-get install -y \ + binwalk \ + foremost \ + exiftool \ + steghide \ + binutils \ + outguess \ + pngcheck \ && gem install zsteg # Install OpenStego From 474ad6208588f1cf54300f2f30331cd7464763a5 Mon Sep 17 00:00:00 2001 From: Zeecka Date: Tue, 30 Dec 2025 17:08:34 +0100 Subject: [PATCH 4/5] fix error showing binwalk --- aperisolve/analyzers/binwalk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aperisolve/analyzers/binwalk.py b/aperisolve/analyzers/binwalk.py index 823590c..950d14a 100644 --- a/aperisolve/analyzers/binwalk.py +++ b/aperisolve/analyzers/binwalk.py @@ -45,7 +45,7 @@ def analyze_binwalk(input_img: Path, output_dir: Path) -> None: if extracted_dir.exists(): shutil.rmtree(extracted_dir) - if len(stderr) > 0: + if len(stderr) > 0 and not zip_exist: err: dict[str, dict[str, Any]] = { "binwalk": { "status": "error", From 12382fee6ab9e4fcfc707f4622d1233f150ae4fd Mon Sep 17 00:00:00 2001 From: Zeecka Date: Tue, 30 Dec 2025 17:10:14 +0100 Subject: [PATCH 5/5] add comments --- aperisolve/analyzers/binwalk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aperisolve/analyzers/binwalk.py b/aperisolve/analyzers/binwalk.py index 950d14a..6c05b9a 100644 --- a/aperisolve/analyzers/binwalk.py +++ b/aperisolve/analyzers/binwalk.py @@ -45,6 +45,8 @@ def analyze_binwalk(input_img: Path, output_dir: Path) -> None: if extracted_dir.exists(): shutil.rmtree(extracted_dir) + # 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: dict[str, dict[str, Any]] = { "binwalk": {