Files
aperisolve/docs/adding-analyzer.md
T

6.5 KiB

Adding a New Analyzer

This guide shows you how to add a new image analysis tool to the system.

Quick Start

  1. Create a new file in analyzers/ from template file template_analyzer.py.
  2. Register it in workers.py, config.py and aperisolve.js.

Basic Template

from pathlib import Path
from typing import Optional
from .base_analyzer import SubprocessAnalyzer

class MyToolAnalyzer(SubprocessAnalyzer):
    def __init__(self, input_img: Path, output_dir: Path) -> None:
        super().__init__("mytool", input_img, output_dir)
        self.cmd = ["mytool", self.img]

def analyze_mytool(input_img: Path, output_dir: Path) -> None:
    analyzer = MyToolAnalyzer(input_img, output_dir)
    analyzer.analyze()

You can also see template_analyzer.py.

Common Scenarios

Simple Text Output Tool

Like strings or exiftool - just reads the file and outputs text:

class StringsAnalyzer(SubprocessAnalyzer):
    def __init__(self, input_img: Path, output_dir: Path) -> None:
        super().__init__("strings", input_img, output_dir)
        self.cmd = ["strings", self.img]

Tool That Extracts Files

Like binwalk or foremost - extracts files that get zipped for download:

class ForemostAnalyzer(SubprocessAnalyzer):
    def __init__(self, input_img: Path, output_dir: Path) -> None:
        super().__init__("foremost", input_img, output_dir, has_archive=True)
        self.cmd = ["foremost", "-o", str(self.get_extracted_dir()), "-i", self.img]

Note the has_archive=True parameter.

If a tool return an archive, it's important to add the worker name in WORKER_FILES variable of config.py.

Tool With Password Support

Like steghide or openstego:

class OutguessAnalyzer(SubprocessAnalyzer):
    def __init__(self, input_img: Path, output_dir: Path) -> None:
        super().__init__("outguess", input_img, output_dir, has_archive=True)

    def build_cmd(self, password: Optional[str] = None) -> list[str]:
        out = str(self.get_extracted_dir() / "outguess.data")
        if password:
            return ["outguess", "-k", password, "-r", self.img, out]
        return ["outguess", "-r", self.img, out]

def analyze_outguess(input_img: Path, output_dir: Path, password: Optional[str] = None) -> None:
    analyzer = OutguessAnalyzer(input_img, output_dir)
    if password:
        analyzer.analyze(password)
    else:
        analyzer.analyze()

Customizing Behavior

Custom Error Detection

Override is_error() to define what counts as an error:

def is_error(self, returncode: int, stdout: str, stderr: str, zip_exist: bool) -> bool:
    # Only error if no files were extracted
    return len(stderr) > 0 and not zip_exist

Custom Output Formatting

Override process_output() to format the results:

def process_output(self, stdout: str, stderr: str) -> dict[str, str]:
    # Parse into key-value pairs
    metadata = {}
    for line in stdout.split("\n"):
        if ":" in line:
            key, value = line.split(":", 1)
            metadata[key.strip()] = value.strip()
    return metadata

User-Friendly Error Messages

Override process_error() to provide helpful messages:

def process_error(self, stdout: str, stderr: str) -> str:
    if "not supported" in stdout:
        return "This file format is not supported (PNG only)"
    return stderr

Registering Your Analyzer

Add it to workers.py in the analyze_image function:

# Import at the top
from .analyzers.mytool import analyze_mytool

# Add to analyzers list
analyzers = [
    (analyze_binwalk, img_path, result_path),
    (analyze_exiftool, img_path, result_path),
    # ... other analyzers ...
    (analyze_mytool, img_path, result_path),
]

# If it needs a password, pass submission.password:
# (analyze_mytool, img_path, result_path, submission.password),

# If only for deep analysis, add it conditionally:
if submission.deep_analysis:
    analyzers.extend([
        (analyze_mytool, img_path, result_path),
    ])

And also in aperisolve.js in the TOOL_ORDER constant:

const TOOL_ORDER = [
  "decomposer",
  "file",
  "exiftool",
  "binwalk",
  "mytool"  // My new tool
  "foremost",
  "outguess",
  "pngcheck",
  "pcrt",
  "steghide",
  "openstego",
  "zsteg",
  "strings",
];

Available Methods to Override

Method Purpose Default Behavior
build_cmd() Build command to run Uses self.cmd
is_error() Check if result is error Returns True if stderr exists
process_output() Format success output Returns stdout as list of lines
process_error() Format error message Returns stderr as-is
process_note() Add informational note Returns None
get_extracted_dir() Set extraction directory Returns output_dir/toolname/

Output Format

Your analyzer produces JSON in results.json:

Success:

{
  "mytool": {
    "status": "ok",
    "output": "Tool output here",
    "download": "/download/hash/mytool"
  }
}

Error:

{
  "mytool": {
    "status": "error",
    "error": "Error message"
  }
}

Tips

  • Use self.img in commands (relative path), not self.input_img
  • Set has_archive=True if your tool extracts files
  • Tools run in parallel threads - no need to worry about concurrency
  • The base class handles timeouts (10 minutes default)
  • Extracted files are automatically zipped into .7z archives
  • All exceptions are caught and logged to Sentry
  • Keep analyzers idempotent and write outputs to the provided results_dir
  • Return structured JSON so the frontend can render links/downloads automatically

Example: Complete Analyzer

from pathlib import Path
from .base_analyzer import SubprocessAnalyzer

class PngcheckAnalyzer(SubprocessAnalyzer):
    def __init__(self, input_img: Path, output_dir: Path) -> None:
        super().__init__("pngcheck", input_img, output_dir)
        self.cmd = ["pngcheck", "-v", self.img]

    def is_error(self, returncode: int, stdout: str, stderr: str, zip_exist: bool) -> bool:
        return "this is neither a PNG or JNG image" in stdout

    def process_error(self, stdout: str, stderr: str) -> str:
        if "neither a PNG or JNG" in stdout:
            return "File format not supported (PNG/JNG only)"
        return stdout

def analyze_pngcheck(input_img: Path, output_dir: Path) -> None:
    analyzer = PngcheckAnalyzer(input_img, output_dir)
    analyzer.analyze()

That's it! Your analyzer will now run in parallel with the others and results will appear in the web interface.