Conversation
|
closes #159 |
There was a problem hiding this comment.
Pull request overview
Adds a new stegoveritas analyzer to Aperi’Solve to expand image-forensics coverage (image transforms + GIF frame extraction) and updates the UI/Docker image to support the new analyzer’s outputs and dependencies.
Changes:
- Introduce
StegoveritasAnalyzer(subprocess-based) with grouped image output and optional RGB preprocessing for non-RGB inputs. - Wire the new analyzer into the worker pool, download whitelist, and UI tool ordering + generalized image-group rendering.
- Update Docker image to install stegoveritas and mitigate a binwalk/Python 3.14 compatibility regression.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
aperisolve/analyzers/stegoveritas.py |
New analyzer implementation, grouping/copying images and handling GIF frames. |
aperisolve/workers.py |
Adds stegoveritas to parallel analyzer execution list. |
aperisolve/config.py |
Allows stegoveritas archives to be downloadable via WORKER_FILES. |
aperisolve/static/js/aperisolve.js |
Adds tool ordering and makes image-group rendering key-driven. |
Dockerfile |
Installs stegoveritas + deps and adds binwalk/imp compatibility workarounds. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| RUN pip3 install stegoveritas | ||
| RUN stegoveritas_install_deps | ||
| RUN rm -f /usr/local/bin/binwalk |
There was a problem hiding this comment.
PR description mentions the binwalk/imp compatibility fix being applied in both builder and runtime stages, but the imp.py shim is only added in the runtime stage in this diff. Either update the PR description or apply the same shim strategy to the builder stage as well (if the builder stage needs to run any Python code paths that import imp).
| return | ||
| if img.mode in {"RGBA", "LA", "P"}: | ||
| converted = img.convert("RGB") | ||
| converted_path = self.output_dir / f"{self.input_img.stem}_stegoveritas_rgb.png" | ||
| converted.save(converted_path) |
There was a problem hiding this comment.
When converting non-RGB inputs, the converted PNG is saved into output_dir and left behind permanently. This can increase per-submission disk usage even though the converted file is not returned in results or included in the archive; consider writing it into the extracted/results directory (so it gets zipped/cleaned) or deleting it after stegoveritas finishes.
| RUN pip3 install stegoveritas | ||
| RUN stegoveritas_install_deps | ||
| RUN rm -f /usr/local/bin/binwalk |
There was a problem hiding this comment.
The builder stage installs stegoveritas, runs stegoveritas_install_deps, and removes /usr/local/bin/binwalk, but none of these artifacts are copied into the runtime stage. This adds significant build time and network/apt churn without affecting the final image; consider removing these RUN steps from the builder stage and keeping stegoveritas installation only in the runtime stage (or copying only the needed artifacts intentionally).
| RUN pip3 install stegoveritas | |
| RUN stegoveritas_install_deps | |
| RUN rm -f /usr/local/bin/binwalk |
| # Provide a minimal imp shim for binwalk on Python 3.14 | ||
| RUN cat <<'PY' > /usr/local/lib/python3.14/imp.py | ||
| """Minimal imp compatibility shim for legacy libraries.""" | ||
|
|
||
| from importlib.machinery import SourceFileLoader | ||
| from importlib.util import module_from_spec, spec_from_loader | ||
|
|
||
| def load_source(name, pathname): | ||
| loader = SourceFileLoader(name, pathname) | ||
| spec = spec_from_loader(name, loader) | ||
| module = module_from_spec(spec) | ||
| loader.exec_module(module) | ||
| return module |
There was a problem hiding this comment.
The imp.py shim is written to a hard-coded /usr/local/lib/python3.14/imp.py path. This will break if the base image Python minor version changes (e.g., 3.15) or if the stdlib path differs; consider computing the target directory via Python (e.g., sysconfig.get_paths()) during build before writing the shim.
| # Provide a minimal imp shim for binwalk on Python 3.14 | |
| RUN cat <<'PY' > /usr/local/lib/python3.14/imp.py | |
| """Minimal imp compatibility shim for legacy libraries.""" | |
| from importlib.machinery import SourceFileLoader | |
| from importlib.util import module_from_spec, spec_from_loader | |
| def load_source(name, pathname): | |
| loader = SourceFileLoader(name, pathname) | |
| spec = spec_from_loader(name, loader) | |
| module = module_from_spec(spec) | |
| loader.exec_module(module) | |
| return module | |
| # Provide a minimal imp shim for binwalk; place it in the active Python stdlib directory | |
| RUN python - <<'PY' | |
| import sysconfig | |
| from pathlib import Path | |
| stdlib_dir = Path(sysconfig.get_paths()["stdlib"]) | |
| imp_path = stdlib_dir / "imp.py" | |
| imp_source = """\"\"\"Minimal imp compatibility shim for legacy libraries.\"\"\"\n | |
| from importlib.machinery import SourceFileLoader\n | |
| from importlib.util import module_from_spec, spec_from_loader\n | |
| \n | |
| def load_source(name, pathname):\n | |
| loader = SourceFileLoader(name, pathname)\n | |
| spec = spec_from_loader(name, loader)\n | |
| module = module_from_spec(spec)\n | |
| loader.exec_module(module)\n | |
| return module\n | |
| """ | |
| imp_path.write_text(imp_source) |
| RUN pip3 install stegoveritas | ||
| RUN stegoveritas_install_deps | ||
| RUN rm -f /usr/local/bin/binwalk |
There was a problem hiding this comment.
These RUN lines install the stegoveritas package from PyPI without pinning a version or verifying integrity. Because this third-party CLI is later executed inside the analyzer container with access to submissions and application configuration/secrets, a compromised or hijacked stegoveritas release would provide an attacker with code execution in your environment. Pin this dependency to an immutable version (and ideally verify its hash/signature) instead of relying on the latest mutable package from PyPI.
| RUN pip install --no-cache-dir stegoveritas && stegoveritas_install_deps \ | ||
| && rm -f /usr/local/bin/binwalk |
There was a problem hiding this comment.
This runtime-stage pip install stegoveritas pulls the latest package from PyPI at build time without any version pinning or integrity checking. Since the resulting stegoveritas binary is invoked on every user submission, a malicious or compromised package version would gain arbitrary code execution inside the worker container and could exfiltrate data or abuse attached credentials. Pin stegoveritas to a specific version (and, where possible, enforce a checksum/signature) to eliminate this mutable supply-chain risk.
|
Just for Reference im dropping the code used to benchmark between two analyzers """Benchmark: decomposer vs stegoveritas on a large image."""
import shutil
import tempfile
import time
from pathlib import Path
SRC_IMG = Path(
"/app/aperisolve/results/"
"9c705c4e4369c6535b561e73cb404621/"
"9c705c4e4369c6535b561e73cb404621.jpg"
)
def make_workspace(src: Path) -> tuple[Path, Path]:
"""Create a parent/child dir layout that mirrors production.
stegoveritas resolves the image as ``../filename`` relative to output_dir,
so the image must sit in the parent of the output directory.
"""
parent = Path(tempfile.mkdtemp())
img_copy = parent / src.name
shutil.copy2(src, img_copy)
out = parent / "bench_run"
out.mkdir()
return img_copy, out
# --- Benchmark decomposer ---
from aperisolve.analyzers.decomposer import DecomposerAnalyzer # noqa: E402
img1, tmp1 = make_workspace(SRC_IMG)
t0 = time.perf_counter()
d = DecomposerAnalyzer(img1, tmp1)
res_d = d.get_results()
t_decomposer = time.perf_counter() - t0
n_decomp_imgs = sum(len(v) for v in res_d.get("images", {}).values())
groups_d = list(res_d.get("images", {}).keys())
shutil.rmtree(tmp1.parent, ignore_errors=True)
# --- Benchmark stegoveritas ---
from aperisolve.analyzers.stegoveritas import StegoveritasAnalyzer # noqa: E402
img2, tmp2 = make_workspace(SRC_IMG)
t0 = time.perf_counter()
s = StegoveritasAnalyzer(img2, tmp2)
res_s = s.get_results()
t_stegoveritas = time.perf_counter() - t0
n_stego_imgs = sum(len(v) for v in res_s.get("images", {}).values())
groups_s = list(res_s.get("images", {}).keys())
shutil.rmtree(tmp2.parent, ignore_errors=True)
print("=" * 60)
print("Image: 2048x2048 RGB JPEG (~3 MB)")
print("=" * 60)
print()
print("Decomposer:")
print(f" Time: {t_decomposer:.2f}s")
print(f" Images: {n_decomp_imgs}")
print(f" Groups: {groups_d}")
print()
print("Stegoveritas (-imageTransform -extract_frames):")
print(f" Time: {t_stegoveritas:.2f}s")
print(f" Images: {n_stego_imgs}")
print(f" Groups: {groups_s}")
print()
print(f"Ratio: stegoveritas is {t_stegoveritas / t_decomposer:.1f}x decomposer")
print(f"Delta: +{t_stegoveritas - t_decomposer:.2f}s") |
|
@Zeecka any issues with this PR ? let me know if any changes required :) |
|
@Zeecka Friendly reminder about this PR :) |
| from importlib.machinery import SourceFileLoader | ||
| from importlib.util import module_from_spec, spec_from_loader | ||
|
|
||
| def load_source(name, pathname): | ||
| loader = SourceFileLoader(name, pathname) | ||
| spec = spec_from_loader(name, loader) | ||
| module = module_from_spec(spec) | ||
| loader.exec_module(module) |
| except Exception: # noqa: BLE001, S110 | ||
| pass # Fall back to the original input if conversion fails. |
| - [outguess](https://www.rbcafe.com/software/outguess/) (extraction with password) | ||
| - [pngcheck](https://www.libpng.org/pub/png/apps/pngcheck.html) | ||
| - [steghide](https://steghide.sourceforge.net/) (extraction with password) | ||
| - [stegoveritas](https://github.com/bannsec/stegoVeritas) (image transformation and GIF frames extraction) |
|
Thanks @aradhyacp — the grouped-image output and GIF 1. Base-class API drift (blocking — this won't import against def __init__(self, input_img, output_dir):
super().__init__("stegoveritas", input_img, output_dir, has_archive=True)declare: class StegoveritasAnalyzer(SubprocessAnalyzer):
name = "stegoveritas"
has_archive = True
needs_password = True # only if you actually pass the password through
def __init__(self, input_img, output_dir):
super().__init__(input_img, output_dir)
...Also 2. 3. RGB channels — please exclude them (per #159). 4. Rebase. The branch is based on an old commit; Once it's rebased with the RGB planes dropped and binwalk preserved, I'll do a full pass. Thanks again for pushing this forward. 🙏 |
PR Body
Summary
Integrates stegoveritas as a new analyzer, adding image-transform and GIF frame-extraction capabilities alongside the existing decomposer. Includes Dockerfile changes to resolve a Python 3.14 compatibility regression introduced by the pip-packaged
binwalkdependency.What changed
New analyzer —
stegoveritas(183 LOC)aperisolve/analyzers/stegoveritas.pySubprocessAnalyzerimplementationaperisolve/workers.pyanalyze_stegoveritasinto the parallel worker thread poolaperisolve/config.py"stegoveritas"toWORKER_FILESaperisolve/static/js/aperisolve.js"stegoveritas"toTOOL_ORDER; refactor image-channel renderingDockerfileAnalyzer features
-imageTransform— generates filter/transform images (edge-enhance, sharpness, solarize, autocontrast, equalize, etc.) plus per-channel bit-planes (Red 0-7, Green 0-7, Blue 0-7).-extract_frames— extracts individual frames from animated GIFs._prepare_input()converts palette and alpha-channel images to RGB before running stegoveritas filters (which crash on non-RGB). Animated GIFs are left untouched to preserve frames._classify_image()parses stegoveritas filenames via regex and categorises them into named groups (Red,Green,Blue,Alpha,Transforms,Frames), ordered via_GROUP_ORDER. The JSON emitted matches decomposer's{"images": {"Group": [url, …]}}structure, so the frontend renders<h3>headings per group automatically.Frontend — dynamic channel rendering
The old JS hardcoded channel lists for decomposer (
["Superimposed", "Red", "Green", "Blue", "Alpha"]) and used length-based heuristics to pick which set. This broke for stegoveritas (which emits different group names likeTransforms,Frames).Replaced with a generic approach:
Known channels appear in preferred order first; any additional groups (from stegoveritas or future analyzers) are appended. Fully backward-compatible with decomposer and color_remapping.
Dockerfile — binwalk / Python 3.14 regression fix
Root cause: Installing stegoveritas via pip pulled a pip-packaged
binwalkas a transitive dependency. That pip binwalk:/usr/local/bin/binwalk, shadowing the working apt/system binary at/usr/bin/binwalk.imp(removed in Python 3.14), causingModuleNotFoundErrorat runtime.On
mainthere is no stegoveritas, so the system binwalk is used and everything works under 3.14.Fix (two layers):
rm -f /usr/local/bin/binwalk— afterpip install stegoveritas, remove the pip entrypoint so the runtime resolves to the known-good apt binary at/usr/bin/binwalk.imp.pyshim — a minimal compatibility shim written to/usr/local/lib/python3.14/imp.pyimplementingload_sourceviaimportlib. Acts as a safety backstop for any remaining code paths in site-packages thatimport imp.Applied in both builder (Stage 1) and runtime (Stage 2) stages.
Benchmark — decomposer vs stegoveritas
Tested on a 2048 × 2048 RGB JPEG (~3 MB) inside the Docker worker container:
Analysis
stegoveritasCLI subprocess applying 26 additional Pillow image filters (sharpness variants, edge-enhance, solarize, autocontrast, etc.) on top of the 24 channel bit-planes.max(decomposer, stegoveritas)≈ 21 s, notsum≈ 35 s. The stegoveritas overhead only matters if it becomes the bottleneck (which it already is by ~7 s on large images).Checklist
ruff check aperisolve/— all checks passedruff format aperisolve/ --check— all files already formatted