From d786c6dab1ffe54a469759f784e0d0f4a424ee2d Mon Sep 17 00:00:00 2001 From: tduhamel42 Date: Tue, 3 Feb 2026 10:15:16 +0100 Subject: [PATCH] fix: block Podman on macOS and remove ghcr.io default (#39) * fix: block Podman on macOS and remove ghcr.io default - Add platform check in PodmanCLI.__init__() that raises FuzzForgeError on macOS with instructions to use Docker instead - Change RegistrySettings.url default from "ghcr.io/fuzzinglabs" to "" (empty string) for local-only mode since no images are published yet - Update _ensure_module_image() to show helpful error when image not found locally and no registry configured - Update tests to mock Linux platform for Podman tests - Add root ruff.toml to fix broken configuration in fuzzforge-runner * rewrite guides for module architecture and update repo links --------- Co-authored-by: AFredefon --- CONTRIBUTING.md | 521 +++++++++++++++--- README.md | 10 +- .../sandboxes/engines/docker/cli.py | 4 +- .../sandboxes/engines/podman/cli.py | 37 +- .../tests/unit/engines/test_podman.py | 69 ++- .../src/fuzzforge_runner/executor.py | 71 ++- .../src/fuzzforge_runner/settings.py | 11 +- ruff.toml | 20 + 8 files changed, 604 insertions(+), 139 deletions(-) create mode 100644 ruff.toml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6529eec..638be6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,17 +1,21 @@ -# Contributing to FuzzForge 🀝 +# Contributing to FuzzForge OSS -Thank you for your interest in contributing to FuzzForge! We welcome contributions from the community and are excited to collaborate with you. +Thank you for your interest in contributing to FuzzForge OSS! We welcome contributions from the community and are excited to collaborate with you. -## 🌟 Ways to Contribute +**Our Vision**: FuzzForge aims to be a **universal platform for security research** across all cybersecurity domains. Through our modular architecture, any security toolβ€”from fuzzing engines to cloud scanners, from mobile app analyzers to IoT security toolsβ€”can be integrated as a containerized module and controlled via AI agents. -- πŸ› **Bug Reports** - Help us identify and fix issues -- πŸ’‘ **Feature Requests** - Suggest new capabilities and improvements -- πŸ”§ **Code Contributions** - Submit bug fixes, features, and enhancements -- πŸ“š **Documentation** - Improve guides, tutorials, and API documentation -- πŸ§ͺ **Testing** - Help test new features and report issues -- πŸ›‘οΈ **Security Workflows** - Contribute new security analysis workflows +## Ways to Contribute -## πŸ“‹ Contribution Guidelines +- **Security Modules** - Create modules for any cybersecurity domain (AppSec, NetSec, Cloud, IoT, etc.) +- **Bug Reports** - Help us identify and fix issues +- **Feature Requests** - Suggest new capabilities and improvements +- **Core Features** - Contribute to the MCP server, runner, or CLI +- **Documentation** - Improve guides, tutorials, and module documentation +- **Testing** - Help test new features and report issues +- **AI Integration** - Improve MCP tools and AI agent interactions +- **Tool Integrations** - Wrap existing security tools as FuzzForge modules + +## Contribution Guidelines ### Code Style @@ -44,9 +48,10 @@ We use conventional commits for clear history: **Examples:** ``` -feat(workflows): add new static analysis workflow for Go -fix(api): resolve authentication timeout issue -docs(readme): update installation instructions +feat(modules): add cloud security scanner module +fix(mcp): resolve module listing timeout +docs(sdk): update module development guide +test(runner): add container execution tests ``` ### Pull Request Process @@ -65,9 +70,14 @@ docs(readme): update installation instructions 3. **Test Your Changes** ```bash - # Test workflows - cd test_projects/vulnerable_app/ - ff workflow security_assessment . + # Test modules + FUZZFORGE_MODULES_PATH=./fuzzforge-modules uv run fuzzforge modules list + + # Run a module + uv run fuzzforge modules run your-module --assets ./test-assets + + # Test MCP integration (if applicable) + uv run fuzzforge mcp status ``` 4. **Submit Pull Request** @@ -76,65 +86,353 @@ docs(readme): update installation instructions - Link related issues using `Fixes #123` or `Closes #123` - Ensure all CI checks pass -## πŸ›‘οΈ Security Workflow Development +## Module Development -### Creating New Workflows +FuzzForge uses a modular architecture where security tools run as isolated containers. The `fuzzforge-modules-sdk` provides everything you need to create new modules. -1. **Workflow Structure** - ``` - backend/toolbox/workflows/your_workflow/ - β”œβ”€β”€ __init__.py - β”œβ”€β”€ workflow.py # Main Temporal workflow - β”œβ”€β”€ activities.py # Workflow activities (optional) - β”œβ”€β”€ metadata.yaml # Workflow metadata (includes vertical field) - └── requirements.txt # Additional dependencies (optional) +**Documentation:** +- [Module SDK Documentation](fuzzforge-modules/fuzzforge-modules-sdk/README.md) - Complete SDK reference +- [Module Template](fuzzforge-modules/fuzzforge-module-template/) - Starting point for new modules +- [USAGE Guide](USAGE.md) - Setup and installation instructions + +### Creating a New Module + +1. **Use the Module Template** + ```bash + # Generate a new module from template + cd fuzzforge-modules/ + cp -r fuzzforge-module-template my-new-module + cd my-new-module ``` -2. **Register Your Workflow** - Add your workflow to `backend/toolbox/workflows/registry.py`: +2. **Module Structure** + ``` + my-new-module/ + β”œβ”€β”€ Dockerfile # Container definition + β”œβ”€β”€ Makefile # Build commands + β”œβ”€β”€ README.md # Module documentation + β”œβ”€β”€ pyproject.toml # Python dependencies + β”œβ”€β”€ mypy.ini # Type checking config + β”œβ”€β”€ ruff.toml # Linting config + └── src/ + └── module/ + β”œβ”€β”€ __init__.py + β”œβ”€β”€ __main__.py # Entry point + β”œβ”€β”€ mod.py # Main module logic + β”œβ”€β”€ models.py # Pydantic models + └── settings.py # Configuration + ``` + +3. **Implement Your Module** + + Edit `src/module/mod.py`: ```python - # Import your workflow - from .your_workflow.workflow import main_flow as your_workflow_flow - - # Add to registry - WORKFLOW_REGISTRY["your_workflow"] = { - "flow": your_workflow_flow, - "module_path": "toolbox.workflows.your_workflow.workflow", - "function_name": "main_flow", - "description": "Description of your workflow", - "version": "1.0.0", - "author": "Your Name", - "tags": ["tag1", "tag2"] - } + from fuzzforge_modules_sdk.api.modules import BaseModule + from fuzzforge_modules_sdk.api.models import ModuleResult + from .models import MyModuleConfig, MyModuleOutput + + class MyModule(BaseModule[MyModuleConfig, MyModuleOutput]): + """Your module description.""" + + def execute(self) -> ModuleResult[MyModuleOutput]: + """Main execution logic.""" + # Access input assets + assets = self.input_path + + # Your security tool logic here + results = self.run_analysis(assets) + + # Return structured results + return ModuleResult( + success=True, + output=MyModuleOutput( + findings=results, + summary="Analysis complete" + ) + ) ``` -3. **Testing Workflows** - - Create test cases in `test_projects/vulnerable_app/` - - Ensure SARIF output format compliance - - Test with various input scenarios +4. **Define Configuration Models** + + Edit `src/module/models.py`: + ```python + from pydantic import BaseModel, Field + from fuzzforge_modules_sdk.api.models import BaseModuleConfig, BaseModuleOutput + + class MyModuleConfig(BaseModuleConfig): + """Configuration for your module.""" + timeout: int = Field(default=300, description="Timeout in seconds") + max_iterations: int = Field(default=1000, description="Max iterations") + + class MyModuleOutput(BaseModuleOutput): + """Output from your module.""" + findings: list[dict] = Field(default_factory=list) + coverage: float = Field(default=0.0) + ``` + +5. **Build Your Module** + ```bash + # Build the SDK first (if not already done) + cd ../fuzzforge-modules-sdk + uv build + mkdir -p .wheels + cp ../../dist/fuzzforge_modules_sdk-*.whl .wheels/ + cd ../.. + docker build -t localhost/fuzzforge-modules-sdk:0.1.0 fuzzforge-modules/fuzzforge-modules-sdk/ + + # Build your module + cd fuzzforge-modules/my-new-module + docker build -t fuzzforge-my-new-module:0.1.0 . + ``` + +6. **Test Your Module** + ```bash + # Run with test assets + uv run fuzzforge modules run my-new-module --assets ./test-assets + + # Check module info + uv run fuzzforge modules info my-new-module + ``` + +### Module Development Guidelines + +**Important Conventions:** +- **Input/Output**: Use `/fuzzforge/input` for assets and `/fuzzforge/output` for results +- **Configuration**: Support JSON configuration via stdin or file +- **Logging**: Use structured logging (structlog is pre-configured) +- **Error Handling**: Return proper exit codes and error messages +- **Security**: Run as non-root user when possible +- **Documentation**: Include clear README with usage examples +- **Dependencies**: Minimize container size, use multi-stage builds + +**See also:** +- [Module SDK API Reference](fuzzforge-modules/fuzzforge-modules-sdk/src/fuzzforge_modules_sdk/api/) +- [Dockerfile Best Practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) + +### Module Types + +FuzzForge is designed to support modules across **all cybersecurity domains**. The modular architecture allows any security tool to be containerized and integrated. Here are the main categories: + +**Application Security** +- Fuzzing engines (coverage-guided, grammar-based, mutation-based) +- Static analysis (SAST, code quality, dependency scanning) +- Dynamic analysis (DAST, runtime analysis, instrumentation) +- Test validation and coverage analysis +- Crash analysis and exploit detection + +**Network & Infrastructure Security** +- Network scanning and service enumeration +- Protocol analysis and fuzzing +- Firewall and configuration testing +- Cloud security (AWS/Azure/GCP misconfiguration detection, IAM analysis) +- Container security (image scanning, Kubernetes security) + +**Web & API Security** +- Web vulnerability scanners (XSS, SQL injection, CSRF) +- Authentication and session testing +- API security (REST/GraphQL/gRPC testing, fuzzing) +- SSL/TLS analysis + +**Binary & Reverse Engineering** +- Binary analysis and disassembly +- Malware sandboxing and behavior analysis +- Exploit development tools +- Firmware extraction and analysis + +**Mobile & IoT Security** +- Mobile app analysis (Android/iOS static/dynamic analysis) +- IoT device security and firmware analysis +- SCADA/ICS and industrial protocol testing +- Automotive security (CAN bus, ECU testing) + +**Data & Compliance** +- Database security testing +- Encryption and cryptography analysis +- Secrets and credential detection +- Privacy tools (PII detection, GDPR compliance) +- Compliance checkers (PCI-DSS, HIPAA, SOC2, ISO27001) + +**Threat Intelligence & Risk** +- OSINT and reconnaissance tools +- Threat hunting and IOC correlation +- Risk assessment and attack surface mapping +- Security audit and policy validation + +**Emerging Technologies** +- AI/ML security (model poisoning, adversarial testing) +- Blockchain and smart contract analysis +- Quantum-safe cryptography testing + +**Custom & Integration** +- Domain-specific security tools +- Bridges to existing security tools +- Multi-tool orchestration and result aggregation + +### Example: Simple Security Scanner Module + +```python +# src/module/mod.py +from pathlib import Path +from fuzzforge_modules_sdk.api.modules import BaseModule +from fuzzforge_modules_sdk.api.models import ModuleResult +from .models import ScannerConfig, ScannerOutput + +class SecurityScanner(BaseModule[ScannerConfig, ScannerOutput]): + """Scans for common security issues in code.""" + + def execute(self) -> ModuleResult[ScannerOutput]: + findings = [] + + # Scan all source files + for file_path in self.input_path.rglob("*"): + if file_path.is_file(): + findings.extend(self.scan_file(file_path)) + + return ModuleResult( + success=True, + output=ScannerOutput( + findings=findings, + files_scanned=len(list(self.input_path.rglob("*"))) + ) + ) + + def scan_file(self, path: Path) -> list[dict]: + """Scan a single file for security issues.""" + # Your scanning logic here + return [] +``` + +### Testing Modules + +Create tests in `tests/`: +```python +import pytest +from module.mod import MyModule +from module.models import MyModuleConfig + +def test_module_execution(): + config = MyModuleConfig(timeout=60) + module = MyModule(config=config, input_path=Path("test_assets")) + result = module.execute() + + assert result.success + assert len(result.output.findings) >= 0 +``` + +Run tests: +```bash +uv run pytest +``` ### Security Guidelines -- πŸ” Never commit secrets, API keys, or credentials -- πŸ›‘οΈ Focus on **defensive security** tools and analysis -- ⚠️ Do not create tools for malicious purposes -- πŸ§ͺ Test workflows thoroughly before submission -- πŸ“‹ Follow responsible disclosure for security issues +**Critical Requirements:** +- Never commit secrets, API keys, or credentials +- Focus on **defensive security** tools and analysis +- Do not create tools for malicious purposes +- Test modules thoroughly before submission +- Follow responsible disclosure for security issues +- Use minimal, secure base images for containers +- Avoid running containers as root when possible -## πŸ› Bug Reports +**Security Resources:** +- [OWASP Container Security](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html) +- [CIS Docker Benchmarks](https://www.cisecurity.org/benchmark/docker) + +## Contributing to Core Features + +Beyond modules, you can contribute to FuzzForge's core components. + +**Useful Resources:** +- [Project Structure](README.md) - Overview of the codebase +- [USAGE Guide](USAGE.md) - Installation and setup +- Python best practices: [PEP 8](https://pep8.org/) + +### Core Components + +- **fuzzforge-mcp** - MCP server for AI agent integration +- **fuzzforge-runner** - Module execution engine +- **fuzzforge-cli** - Command-line interface +- **fuzzforge-common** - Shared utilities and sandbox engines +- **fuzzforge-types** - Type definitions and schemas + +### Development Setup + +1. **Clone and Install** + ```bash + git clone https://github.com/FuzzingLabs/fuzzforge-oss.git + cd fuzzforge-oss + uv sync --all-extras + ``` + +2. **Run Tests** + ```bash + # Run all tests + make test + + # Run specific package tests + cd fuzzforge-mcp + uv run pytest + ``` + +3. **Type Checking** + ```bash + # Type check all packages + make typecheck + + # Type check specific package + cd fuzzforge-runner + uv run mypy . + ``` + +4. **Linting and Formatting** + ```bash + # Format code + make format + + # Lint code + make lint + ``` + +## Bug Reports When reporting bugs, please include: -- **Environment**: OS, Python version, Docker version +- **Environment**: OS, Python version, Docker version, uv version +- **FuzzForge Version**: Output of `uv run fuzzforge --version` +- **Module**: Which module or component is affected - **Steps to Reproduce**: Clear steps to recreate the issue - **Expected Behavior**: What should happen - **Actual Behavior**: What actually happens - **Logs**: Relevant error messages and stack traces +- **Container Logs**: For module issues, include Docker/Podman logs - **Screenshots**: If applicable -Use our [Bug Report Template](.github/ISSUE_TEMPLATE/bug_report.md). +**Example:** +```markdown +**Environment:** +- OS: Ubuntu 22.04 +- Python: 3.14.2 +- Docker: 24.0.7 +- uv: 0.5.13 -## πŸ’‘ Feature Requests +**Module:** my-custom-scanner + +**Steps to Reproduce:** +1. Run `uv run fuzzforge modules run my-scanner --assets ./test-target` +2. Module fails with timeout error + +**Expected:** Module completes analysis +**Actual:** Times out after 30 seconds + +**Logs:** +``` +ERROR: Module execution timeout +... +``` +``` + +## Feature Requests For new features, please provide: @@ -142,33 +440,124 @@ For new features, please provide: - **Proposed Solution**: How should it work? - **Alternatives**: Other approaches considered - **Implementation**: Technical considerations (optional) +- **Module vs Core**: Should this be a module or core feature? -Use our [Feature Request Template](.github/ISSUE_TEMPLATE/feature_request.md). +**Example Feature Requests:** +- New module for cloud security posture management (CSPM) +- Module for analyzing smart contract vulnerabilities +- MCP tool for orchestrating multi-module workflows +- CLI command for batch module execution across multiple targets +- Support for distributed fuzzing campaigns +- Integration with CI/CD pipelines +- Module marketplace/registry features -## πŸ“š Documentation +## Documentation Help improve our documentation: +- **Module Documentation**: Document your modules in their README.md - **API Documentation**: Update docstrings and type hints -- **User Guides**: Create tutorials and how-to guides -- **Workflow Documentation**: Document new security workflows -- **Examples**: Add practical usage examples +- **User Guides**: Improve USAGE.md and tutorial content +- **Module SDK Guides**: Help document the SDK for module developers +- **MCP Integration**: Document AI agent integration patterns +- **Examples**: Add practical usage examples and workflows -## πŸ™ Recognition +### Documentation Standards + +- Use clear, concise language +- Include code examples +- Add command-line examples with expected output +- Document all configuration options +- Explain error messages and troubleshooting + +### Module README Template + +```markdown +# Module Name + +Brief description of what this module does. + +## Features + +- Feature 1 +- Feature 2 + +## Configuration + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| timeout | int | 300 | Timeout in seconds | + +## Usage + +\`\`\`bash +uv run fuzzforge modules run module-name --assets ./path/to/assets +\`\`\` + +## Output + +Describes the output structure and format. + +## Examples + +Practical usage examples. +``` + +## Recognition Contributors will be: - Listed in our [Contributors](CONTRIBUTORS.md) file - Mentioned in release notes for significant contributions -- Invited to join our Discord community -- Eligible for FuzzingLabs Academy courses and swag +- Credited in module documentation (for module authors) +- Invited to join our [Discord community](https://discord.gg/8XEX33UUwZ) -## πŸ“œ License +## Module Submission Checklist -By contributing to FuzzForge, you agree that your contributions will be licensed under the same [Business Source License 1.1](LICENSE) as the project. +Before submitting a new module: + +- [ ] Module follows SDK structure and conventions +- [ ] Dockerfile builds successfully +- [ ] Module executes without errors +- [ ] Configuration options are documented +- [ ] README.md is complete with examples +- [ ] Tests are included (pytest) +- [ ] Type hints are used throughout +- [ ] Linting passes (ruff) +- [ ] Security best practices followed +- [ ] No secrets or credentials in code +- [ ] License headers included + +## Review Process + +1. **Initial Review** - Maintainers review for completeness +2. **Technical Review** - Code quality and security assessment +3. **Testing** - Module tested in isolated environment +4. **Documentation Review** - Ensure docs are clear and complete +5. **Approval** - Module merged and included in next release + +## License + +By contributing to FuzzForge OSS, you agree that your contributions will be licensed under the same license as the project (see [LICENSE](LICENSE)). + +For module contributions: +- Modules you create remain under the project license +- You retain credit as the module author +- Your module may be used by others under the project license terms --- -**Thank you for making FuzzForge better! πŸš€** +## Getting Help -Every contribution, no matter how small, helps build a stronger security community. +Need help contributing? + +- Join our [Discord](https://discord.gg/8XEX33UUwZ) +- Read the [Module SDK Documentation](fuzzforge-modules/fuzzforge-modules-sdk/README.md) +- Check the module template for examples +- Contact: contact@fuzzinglabs.com + +--- + +**Thank you for making FuzzForge better!** + +Every contribution, no matter how small, helps build a stronger security research platform. Whether you're creating a module for web security, cloud scanning, mobile analysis, or any other cybersecurity domain, your work makes FuzzForge more powerful and versatile for the entire security community! diff --git a/README.md b/README.md index 9e26401..2f76ee1 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,8 @@ Instead of manually running security tools, describe what you want and let your If you find FuzzForge useful, please **star the repo** to support development! πŸš€ - - GitHub Stars + + GitHub Stars --- @@ -135,8 +135,8 @@ If you find FuzzForge useful, please **star the repo** to support development! ```bash # Clone the repository -git clone https://github.com/FuzzingLabs/fuzzforge-oss.git -cd fuzzforge-oss +git clone https://github.com/FuzzingLabs/fuzzforge_ai.git +cd fuzzforge_ai # Install dependencies uv sync @@ -246,7 +246,7 @@ class MySecurityModule(FuzzForgeModule): ## πŸ“ Project Structure ``` -fuzzforge-oss/ +fuzzforge_ai/ β”œβ”€β”€ fuzzforge-cli/ # Command-line interface β”œβ”€β”€ fuzzforge-common/ # Shared abstractions (containers, storage) β”œβ”€β”€ fuzzforge-mcp/ # MCP server for AI agents diff --git a/fuzzforge-common/src/fuzzforge_common/sandboxes/engines/docker/cli.py b/fuzzforge-common/src/fuzzforge_common/sandboxes/engines/docker/cli.py index d7eeada..9560bf0 100644 --- a/fuzzforge-common/src/fuzzforge_common/sandboxes/engines/docker/cli.py +++ b/fuzzforge-common/src/fuzzforge_common/sandboxes/engines/docker/cli.py @@ -95,8 +95,8 @@ class DockerCLI(AbstractFuzzForgeSandboxEngine): continue reference = f"{repo}:{tag}" - - if filter_prefix and not reference.startswith(filter_prefix): + + if filter_prefix and filter_prefix not in reference: continue images.append( diff --git a/fuzzforge-common/src/fuzzforge_common/sandboxes/engines/podman/cli.py b/fuzzforge-common/src/fuzzforge_common/sandboxes/engines/podman/cli.py index dfd29f3..c6d2dd1 100644 --- a/fuzzforge-common/src/fuzzforge_common/sandboxes/engines/podman/cli.py +++ b/fuzzforge-common/src/fuzzforge_common/sandboxes/engines/podman/cli.py @@ -31,14 +31,15 @@ def get_logger() -> BoundLogger: def _is_running_under_snap() -> bool: """Check if running under Snap environment. - + VS Code installed via Snap sets XDG_DATA_HOME to a version-specific path, causing Podman to look for storage in non-standard locations. When SNAP is set, we use custom storage paths to ensure consistency. - + Note: Snap only exists on Linux, so this also handles macOS implicitly. """ import os # noqa: PLC0415 + return os.getenv("SNAP") is not None @@ -48,11 +49,11 @@ class PodmanCLI(AbstractFuzzForgeSandboxEngine): This implementation uses subprocess calls to the Podman CLI with --root and --runroot flags when running under Snap, providing isolation from system Podman storage. - + The custom storage is only used when: 1. Running under Snap (SNAP env var is set) - to fix XDG_DATA_HOME issues 2. Custom paths are explicitly provided - + Otherwise, uses default Podman storage which works for: - Native Linux installations - macOS (where Podman runs in a VM via podman machine) @@ -69,16 +70,24 @@ class PodmanCLI(AbstractFuzzForgeSandboxEngine): :param runroot: Path to container runtime state. Custom storage is used when running under Snap AND paths are provided. + + :raises FuzzForgeError: If running on macOS (Podman not supported). """ + import sys # noqa: PLC0415 + + if sys.platform == "darwin": + msg = ( + "Podman is not supported on macOS. Please use Docker instead:\n" + " brew install --cask docker\n" + " # Or download from https://docker.com/products/docker-desktop" + ) + raise FuzzForgeError(msg) + AbstractFuzzForgeSandboxEngine.__init__(self) - + # Use custom storage only under Snap (to fix XDG_DATA_HOME issues) - self.__use_custom_storage = ( - _is_running_under_snap() - and graphroot is not None - and runroot is not None - ) - + self.__use_custom_storage = _is_running_under_snap() and graphroot is not None and runroot is not None + if self.__use_custom_storage: self.__graphroot = graphroot self.__runroot = runroot @@ -98,8 +107,10 @@ class PodmanCLI(AbstractFuzzForgeSandboxEngine): if self.__use_custom_storage and self.__graphroot and self.__runroot: return [ "podman", - "--root", str(self.__graphroot), - "--runroot", str(self.__runroot), + "--root", + str(self.__graphroot), + "--runroot", + str(self.__runroot), ] return ["podman"] diff --git a/fuzzforge-common/tests/unit/engines/test_podman.py b/fuzzforge-common/tests/unit/engines/test_podman.py index 5dd5db3..4bfd88a 100644 --- a/fuzzforge-common/tests/unit/engines/test_podman.py +++ b/fuzzforge-common/tests/unit/engines/test_podman.py @@ -2,32 +2,41 @@ import os import shutil +import sys import uuid from pathlib import Path from unittest import mock import pytest +from fuzzforge_common.exceptions import FuzzForgeError from fuzzforge_common.sandboxes.engines.podman.cli import PodmanCLI, _is_running_under_snap +# Helper to mock Linux platform for testing (since Podman is Linux-only) +def _mock_linux_platform() -> mock._patch[str]: + """Context manager to mock sys.platform as 'linux'.""" + return mock.patch.object(sys, "platform", "linux") + + @pytest.fixture def podman_cli_engine() -> PodmanCLI: """Create a PodmanCLI engine with temporary storage. - + Uses short paths in /tmp to avoid podman's 50-char runroot limit. Simulates Snap environment to test custom storage paths. + Mocks Linux platform since Podman is Linux-only. """ short_id = str(uuid.uuid4())[:8] graphroot = Path(f"/tmp/ff-{short_id}/storage") runroot = Path(f"/tmp/ff-{short_id}/run") - - # Simulate Snap environment for testing - with mock.patch.dict(os.environ, {"SNAP": "/snap/code/123"}): + + # Simulate Snap environment for testing on Linux + with _mock_linux_platform(), mock.patch.dict(os.environ, {"SNAP": "/snap/code/123"}): engine = PodmanCLI(graphroot=graphroot, runroot=runroot) - + yield engine - + # Cleanup parent = graphroot.parent if parent.exists(): @@ -48,21 +57,30 @@ def test_snap_detection_when_snap_not_set() -> None: assert _is_running_under_snap() is False +def test_podman_cli_blocks_macos() -> None: + """Test that PodmanCLI raises error on macOS.""" + with mock.patch.object(sys, "platform", "darwin"): + with pytest.raises(FuzzForgeError) as exc_info: + PodmanCLI() + assert "Podman is not supported on macOS" in str(exc_info.value) + assert "Docker" in str(exc_info.value) + + def test_podman_cli_creates_storage_directories_under_snap() -> None: """Test that PodmanCLI creates storage directories when under Snap.""" short_id = str(uuid.uuid4())[:8] graphroot = Path(f"/tmp/ff-{short_id}/storage") runroot = Path(f"/tmp/ff-{short_id}/run") - + assert not graphroot.exists() assert not runroot.exists() - - with mock.patch.dict(os.environ, {"SNAP": "/snap/code/123"}): - engine = PodmanCLI(graphroot=graphroot, runroot=runroot) - + + with _mock_linux_platform(), mock.patch.dict(os.environ, {"SNAP": "/snap/code/123"}): + PodmanCLI(graphroot=graphroot, runroot=runroot) + assert graphroot.exists() assert runroot.exists() - + # Cleanup shutil.rmtree(graphroot.parent, ignore_errors=True) @@ -72,15 +90,15 @@ def test_podman_cli_base_cmd_under_snap() -> None: short_id = str(uuid.uuid4())[:8] graphroot = Path(f"/tmp/ff-{short_id}/storage") runroot = Path(f"/tmp/ff-{short_id}/run") - - with mock.patch.dict(os.environ, {"SNAP": "/snap/code/123"}): + + with _mock_linux_platform(), mock.patch.dict(os.environ, {"SNAP": "/snap/code/123"}): engine = PodmanCLI(graphroot=graphroot, runroot=runroot) base_cmd = engine._base_cmd() - + assert "podman" in base_cmd assert "--root" in base_cmd assert "--runroot" in base_cmd - + # Cleanup shutil.rmtree(graphroot.parent, ignore_errors=True) @@ -90,25 +108,26 @@ def test_podman_cli_base_cmd_without_snap() -> None: short_id = str(uuid.uuid4())[:8] graphroot = Path(f"/tmp/ff-{short_id}/storage") runroot = Path(f"/tmp/ff-{short_id}/run") - + env = os.environ.copy() env.pop("SNAP", None) - with mock.patch.dict(os.environ, env, clear=True): + with _mock_linux_platform(), mock.patch.dict(os.environ, env, clear=True): engine = PodmanCLI(graphroot=graphroot, runroot=runroot) base_cmd = engine._base_cmd() - + assert base_cmd == ["podman"] assert "--root" not in base_cmd - + # Directories should NOT be created when not under Snap assert not graphroot.exists() def test_podman_cli_default_mode() -> None: """Test PodmanCLI without custom storage paths.""" - engine = PodmanCLI() # No paths provided - base_cmd = engine._base_cmd() - + with _mock_linux_platform(): + engine = PodmanCLI() # No paths provided + base_cmd = engine._base_cmd() + assert base_cmd == ["podman"] assert "--root" not in base_cmd @@ -116,7 +135,7 @@ def test_podman_cli_default_mode() -> None: def test_podman_cli_list_images_returns_list(podman_cli_engine: PodmanCLI) -> None: """Test that list_images returns a list (even if empty).""" images = podman_cli_engine.list_images() - + assert isinstance(images, list) @@ -125,6 +144,6 @@ def test_podman_cli_can_pull_and_list_image(podman_cli_engine: PodmanCLI) -> Non """Test pulling an image and listing it.""" # Pull a small image podman_cli_engine._run(["pull", "docker.io/library/alpine:latest"]) - + images = podman_cli_engine.list_images() assert any("alpine" in img.identifier for img in images) diff --git a/fuzzforge-runner/src/fuzzforge_runner/executor.py b/fuzzforge-runner/src/fuzzforge_runner/executor.py index 0308da8..57ed320 100644 --- a/fuzzforge-runner/src/fuzzforge_runner/executor.py +++ b/fuzzforge-runner/src/fuzzforge_runner/executor.py @@ -136,7 +136,7 @@ class ModuleExecutor: # - fuzzforge-module-{name}:{tag} (OSS local builds with module prefix) # - localhost/fuzzforge-module-{name}:{tag} (standard convention) # - localhost/{name}:{tag} (legacy/short form) - + # For OSS local builds (no localhost/ prefix) for tag in tags_to_check: # Check direct module name (fuzzforge-cargo-fuzzer:0.1.0) @@ -146,7 +146,7 @@ class ModuleExecutor: if not module_identifier.startswith("fuzzforge-"): if engine.image_exists(f"fuzzforge-{module_identifier}:{tag}"): return True - + # For registry-style naming (localhost/ prefix) name_prefixes = [f"fuzzforge-module-{module_identifier}", module_identifier] @@ -166,17 +166,17 @@ class ModuleExecutor: """ engine = self._get_engine() - + # Try common tags tags_to_check = ["latest", "0.1.0", "0.0.1"] - + # Check OSS local builds first (no localhost/ prefix) for tag in tags_to_check: # Direct module name (fuzzforge-cargo-fuzzer:0.1.0) direct_name = f"{module_identifier}:{tag}" if engine.image_exists(direct_name): return direct_name - + # With fuzzforge- prefix if not already present if not module_identifier.startswith("fuzzforge-"): prefixed_name = f"fuzzforge-{module_identifier}:{tag}" @@ -189,7 +189,7 @@ class ModuleExecutor: prefixed_name = f"localhost/fuzzforge-module-{module_identifier}:{tag}" if engine.image_exists(prefixed_name): return prefixed_name - + # Legacy short form: localhost/{name}:{tag} short_name = f"localhost/{module_identifier}:{tag}" if engine.image_exists(short_name): @@ -198,7 +198,7 @@ class ModuleExecutor: # Default fallback return f"localhost/{module_identifier}:latest" - def _pull_module_image(self, module_identifier: str, registry_url: str = "ghcr.io/fuzzinglabs", tag: str = "latest") -> None: + def _pull_module_image(self, module_identifier: str, registry_url: str, tag: str = "latest") -> None: """Pull a module image from the container registry. :param module_identifier: Name/identifier of the module to pull. @@ -238,21 +238,30 @@ class ModuleExecutor: ) raise SandboxError(message) from exc - def _ensure_module_image(self, module_identifier: str, registry_url: str = "ghcr.io/fuzzinglabs", tag: str = "latest") -> None: + def _ensure_module_image(self, module_identifier: str, registry_url: str = "", tag: str = "latest") -> None: """Ensure module image exists, pulling it if necessary. :param module_identifier: Name/identifier of the module image. - :param registry_url: Container registry URL to pull from. + :param registry_url: Container registry URL to pull from (empty = local-only mode). :param tag: Image tag to pull. - :raises SandboxError: If image check or pull fails. + :raises SandboxError: If image not found locally and no registry configured. """ logger = get_logger() - + if self._check_image_exists(module_identifier): logger.debug("module image exists locally", module=module_identifier) return - + + # If no registry configured, we're in local-only mode + if not registry_url: + raise SandboxError( + f"Module image '{module_identifier}' not found locally.\n" + "Build it with: make build-modules\n" + "\n" + "Or configure a registry URL via FUZZFORGE_REGISTRY__URL environment variable." + ) + logger.info( "module image not found locally, pulling from registry", module=module_identifier, @@ -260,7 +269,7 @@ class ModuleExecutor: info="This may take a moment on first run", ) self._pull_module_image(module_identifier, registry_url, tag) - + # Verify image now exists if not self._check_image_exists(module_identifier): message = ( @@ -332,6 +341,7 @@ class ModuleExecutor: try: # Create temporary directory - caller must clean it up after container finishes from tempfile import mkdtemp + temp_path = Path(mkdtemp(prefix="fuzzforge-input-")) # Copy assets to temp directory @@ -341,16 +351,19 @@ class ModuleExecutor: if assets_path.suffix == ".gz" or assets_path.name.endswith(".tar.gz"): # Extract archive contents import tarfile + with tarfile.open(assets_path, "r:gz") as tar: tar.extractall(path=temp_path) logger.debug("extracted tar.gz archive", archive=str(assets_path)) else: # Single file - copy it import shutil + shutil.copy2(assets_path, temp_path / assets_path.name) else: # Directory - copy all files (including subdirectories) import shutil + for item in assets_path.iterdir(): if item.is_file(): shutil.copy2(item, temp_path / item.name) @@ -363,19 +376,23 @@ class ModuleExecutor: if item.name == "input.json": continue if item.is_file(): - resources.append({ - "name": item.stem, - "description": f"Input file: {item.name}", - "kind": "unknown", - "path": f"/data/input/{item.name}", - }) + resources.append( + { + "name": item.stem, + "description": f"Input file: {item.name}", + "kind": "unknown", + "path": f"/data/input/{item.name}", + } + ) elif item.is_dir(): - resources.append({ - "name": item.name, - "description": f"Input directory: {item.name}", - "kind": "unknown", - "path": f"/data/input/{item.name}", - }) + resources.append( + { + "name": item.name, + "description": f"Input directory: {item.name}", + "kind": "unknown", + "path": f"/data/input/{item.name}", + } + ) # Create input.json with settings and resources input_data = { @@ -461,6 +478,7 @@ class ModuleExecutor: try: # Create temporary directory for results from tempfile import mkdtemp + temp_dir = Path(mkdtemp(prefix="fuzzforge-results-")) # Copy entire output directory from container @@ -489,6 +507,7 @@ class ModuleExecutor: # Clean up temp directory import shutil + shutil.rmtree(temp_dir, ignore_errors=True) logger.info("results pulled successfully", sandbox=sandbox, archive=str(archive_path)) @@ -571,6 +590,7 @@ class ModuleExecutor: self.terminate_sandbox(sandbox) if input_dir and input_dir.exists(): import shutil + shutil.rmtree(input_dir, ignore_errors=True) # ------------------------------------------------------------------------- @@ -669,4 +689,5 @@ class ModuleExecutor: self.terminate_sandbox(container_id) if input_dir: import shutil + shutil.rmtree(input_dir, ignore_errors=True) diff --git a/fuzzforge-runner/src/fuzzforge_runner/settings.py b/fuzzforge-runner/src/fuzzforge_runner/settings.py index 9074205..aa98ab8 100644 --- a/fuzzforge-runner/src/fuzzforge_runner/settings.py +++ b/fuzzforge-runner/src/fuzzforge_runner/settings.py @@ -60,10 +60,15 @@ class ProjectSettings(BaseModel): class RegistrySettings(BaseModel): - """Container registry configuration for module images.""" + """Container registry configuration for module images. - #: Registry URL for pulling module images. - url: str = Field(default="ghcr.io/fuzzinglabs") + By default, registry URL is empty (local-only mode). When empty, + modules must be built locally with `make build-modules`. + Set via FUZZFORGE_REGISTRY__URL environment variable if needed. + """ + + #: Registry URL for pulling module images (empty = local-only mode). + url: str = Field(default="") #: Default tag to use when pulling images. default_tag: str = Field(default="latest") diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..f8c919b --- /dev/null +++ b/ruff.toml @@ -0,0 +1,20 @@ +line-length = 120 + +[lint] +select = [ "ALL" ] +ignore = [ + "COM812", # conflicts with the formatter + "D100", # ignoring missing docstrings in public modules + "D104", # ignoring missing docstrings in public packages + "D203", # conflicts with 'D211' + "D213", # conflicts with 'D212' + "TD002", # ignoring missing author in 'TODO' statements + "TD003", # ignoring missing issue link in 'TODO' statements +] + +[lint.per-file-ignores] +"tests/*" = [ + "ANN401", # allowing 'typing.Any' to be used to type function parameters in tests + "PLR2004", # allowing comparisons using unamed numerical constants in tests + "S101", # allowing 'assert' statements in tests +]