From b3605047ad145a8859c1ae329351dc6ef96a797a Mon Sep 17 00:00:00 2001 From: Zeecka Date: Sat, 3 Jan 2026 01:44:56 +0100 Subject: [PATCH] add CONTRIBUTING --- CONTRIBUTING.md | 186 +++++++++++++++++++++++++++++++++++ README.md | 23 +---- docs/adding-analyzer.md | 211 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 401 insertions(+), 19 deletions(-) create mode 100644 CONTRIBUTING.md create mode 100644 docs/adding-analyzer.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..84468c0 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,186 @@ +# Contributing to Aperi'Solve + +Thank you for your interest in contributing! We welcome all contributions, whether it's bug fixes, new features, or documentation improvements. + +## Getting Started + +> [!TIP] +> New to the project? Adding a new analyzer is a great first contribution! Check out our [Adding a New Analyzer Guide](docs/adding-analyzer.md). + +### Quick Steps + +1. **Fork the repository** +2. **Create a feature branch** (`git checkout -b feature/amazing-feature`) +3. **Make your changes** +4. **Test your changes** +5. **Commit with clear messages** (`git commit -m 'Add amazing feature'`) +6. **Push to your fork** (`git push origin feature/amazing-feature`) +7. **Open a Pull Request** describing your changes + +## What Can You Contribute? + +### 🔧 Adding New Analyzers + +Want to add support for a new steganography or forensics tool? We'd love that! + +> [!NOTE] +> Check out our detailed tutorial: [Adding a New Analyzer](docs/adding-analyzer.md) + +**Quick checklist for new analyzers:** +- [ ] Create analyzer file in `analyzers/` +- [ ] Extend `SubprocessAnalyzer` base class +- [ ] Register in `workers.py` +- [ ] Add tool to Docker setup if needed +- [ ] Test with sample images +- [ ] Update documentation + +### 🐛 Bug Fixes + +Found a bug? Please: +- Check if it's already reported in [Issues](../../issues) +- If not, open a new issue with reproduction steps +- Feel free to submit a PR with the fix! + +### 📚 Documentation + +Help us improve: +- Fix typos or unclear explanations +- Add examples and use cases +- Improve installation instructions +- Translate documentation + +### 🎨 UI/UX Improvements + +Contributions to the web interface are welcome: +- Better error messages +- Improved result presentation +- Mobile responsiveness +- Accessibility improvements + +## Code Style & Quality + +> [!IMPORTANT] +> Please follow the code style and run linters before submitting. + +The project adheres to: + +- **Black** (line length 100) +- **Isort** (profile black) +- **Flake8** (ignoring E203, E501, W503) +- **Pylint** (ignoring W0718, R0903, R0801) +- **Mypy** (ignoring unused-awaitable) + +See [.pre-commit-config.yaml](.pre-commit-config.yaml) for exact configuration. + +### Running Linters Locally + +> [!TIP] +> Install pre-commit hooks to automatically check your code before commits: +> ```bash +> pip install pre-commit +> pre-commit install +> ``` + +**Manual checks:** +```bash +# Format code +black . --line-length 100 +isort . --profile black + +# Check for issues +flake8 . +pylint **/*.py +mypy . +``` + +> [!NOTE] +> CI will automatically run these checks on every PR. Make sure they pass! + +## Pull Request Guidelines + +Use clear, descriptive titles: +- ✅ `Add stegdetect analyzer` +- ✅ `Fix binwalk extraction error handling` +- ✅ `Update documentation for password-protected tools` +- ❌ `Update code` +- ❌ `Fix bug` + + +### Commit Messages + +Write clear, concise commit messages: +```bash +# Good +git commit -m "Add zsteg analyzer for PNG steganography detection" +git commit -m "Fix foremost error detection when no files extracted" + +# Not so good +git commit -m "updates" +git commit -m "fix" +``` + +## Adding New Dependencies + +> [!CAUTION] +> Adding new dependencies requires careful consideration. + +If your contribution needs new dependencies: + +1. **Python packages**: Add to `requirements.txt` +2. **System tools**: Add to `Dockerfile` +3. **Explain why** in your PR description +4. **Keep dependencies minimal** - avoid adding large libraries for small features + +## Docker and Tool Installation + +When adding a new analyzer that requires a new tool: + +```dockerfile +# In Dockerfile +RUN apt-get update && apt-get install -y \ + your-new-tool \ + && rm -rf /var/lib/apt/lists/* +``` + +Or for tools requiring compilation: +```dockerfile +RUN git clone https://github.com/tool/repo.git /tmp/tool && \ + cd /tmp/tool && \ + make && make install && \ + rm -rf /tmp/tool +``` + +> [!NOTE] +> Test the Docker build locally before submitting! + +## Code Review Process + +1. **Automated checks** run first (linters, tests) +2. **Maintainer review** - we'll provide feedback +3. **Address feedback** - update your PR as needed +4. **Approval & merge** - once everything looks good! + +> [!TIP] +> Don't worry if you need to make changes - it's a normal part of the process! + +## Need Help? + +- 💬 **Questions?** Open a [Discussion](../../discussions) +- 🐛 **Found a bug?** Open an [Issue](../../issues) +- 📧 **Email:** [Your contact email] +- 💡 **Ideas?** We'd love to hear them in Discussions! + +## Recognition + +> [!NOTE] +> All contributors will be recognized in our [Contributors](../../graphs/contributors) page and in release notes! + +## License + +By contributing, you agree that your contributions will be licensed under the same license as the project (see [LICENSE](LICENSE)). + +--- + +**Thank you for contributing! 🎉** + +Your efforts help make this tool better for the entire security and CTF community! diff --git a/README.md b/README.md index b84a9ea..d30b0e6 100644 --- a/README.md +++ b/README.md @@ -191,32 +191,17 @@ This separation keeps heavy tools (binwalk, foremost, zsteg, etc.) isolated and ## Roadmap - [ ] **[Bug]** Duplicate key value violates unique constraint "submission_pkey" +- [ ] **[Enhancement]** Server continuous deployment - [ ] **[Feature]** Zsteg: full extraction (--all) and download of discovered files (mp3, etc.) - [ ] **[Feature]** Mobile-friendly UI / test on mobile - [ ] **[Feature]** i18n (internationalization) - [ ] **[Feature]** Rootless / unprivileged analyzers -- [ ] **[Feature]** Improve analyzer sandboxing (e.g., per-analyzer containers) + +See [Issues](../../issues) for more! ## Contributing -Contributions are welcome! Please: - -1. Fork the repository -2. Create a feature branch -3. Open a pull request describing your change - -> [!IMPORTANT] -> Please follow the code style and run linters before submitting. The project adheres to: -> -> - Black (line length 100) -> - Isort (profile black) -> - Flake8 (ignoring E203, E501, W503) -> - Pylint (ignoring W0718, R0903, R0801) -> - Mypy (ignoring unused-awaitable) -> -> See [.pre-commit-config.yaml](.pre-commit-config.yaml) file. - -CI will run these checks on each PR. +See [CONTRIBUTING.md](CONTRIBUTING.md). ## Security diff --git a/docs/adding-analyzer.md b/docs/adding-analyzer.md new file mode 100644 index 0000000..0c6d0ca --- /dev/null +++ b/docs/adding-analyzer.md @@ -0,0 +1,211 @@ +# 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` + +## Basic Template + +```python +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: + +```python +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: + +```python +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. + +### Tool With Password Support + +Like `steghide` or `openstego`: + +```python +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: + +```python +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: + +```python +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: + +```python +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: + +```python +# 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), + ]) +``` + +## 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:** +```json +{ + "mytool": { + "status": "ok", + "output": "Tool output here", + "download": "/download/hash/mytool" + } +} +``` + +**Error:** +```json +{ + "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 + +## Example: Complete Analyzer + +```python +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.