add CONTRIBUTING
This commit is contained in:
+186
@@ -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!
|
||||||
@@ -191,32 +191,17 @@ This separation keeps heavy tools (binwalk, foremost, zsteg, etc.) isolated and
|
|||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
- [ ] **[Bug]** Duplicate key value violates unique constraint "submission_pkey"
|
- [ ] **[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]** Zsteg: full extraction (--all) and download of discovered files (mp3, etc.)
|
||||||
- [ ] **[Feature]** Mobile-friendly UI / test on mobile
|
- [ ] **[Feature]** Mobile-friendly UI / test on mobile
|
||||||
- [ ] **[Feature]** i18n (internationalization)
|
- [ ] **[Feature]** i18n (internationalization)
|
||||||
- [ ] **[Feature]** Rootless / unprivileged analyzers
|
- [ ] **[Feature]** Rootless / unprivileged analyzers
|
||||||
- [ ] **[Feature]** Improve analyzer sandboxing (e.g., per-analyzer containers)
|
|
||||||
|
See [Issues](../../issues) for more!
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions are welcome! Please:
|
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user