diff --git a/backend/README.md b/backend/README.md index a5cbcd4..54c7004 100644 Binary files a/backend/README.md and b/backend/README.md differ diff --git a/backend/src/core/prefect_manager.py b/backend/src/core/prefect_manager.py index f48cc86..74a0c39 100644 --- a/backend/src/core/prefect_manager.py +++ b/backend/src/core/prefect_manager.py @@ -382,7 +382,7 @@ class PrefectManager: "image": deploy_image, # Use the worker-accessible registry name "volumes": [], # Populated at run submission with toolbox mount "env": { - "PYTHONPATH": "/opt/prefect/toolbox:/opt/prefect/toolbox/workflows", + "PYTHONPATH": "/opt/prefect/toolbox:/opt/prefect", "WORKFLOW_NAME": name } } diff --git a/backend/toolbox/modules/ai_security/__init__.py b/backend/toolbox/modules/ai_security/__init__.py deleted file mode 100644 index 23a29dc..0000000 --- a/backend/toolbox/modules/ai_security/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -AI Security Modules - -This package contains modules for AI and machine learning model security testing. - -Available modules: -- Garak: LLM/AI model security testing framework for prompt injection, bias, and jailbreaks -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -from typing import List, Type -from ..base import BaseModule - -# Module registry for automatic discovery -AI_SECURITY_MODULES: List[Type[BaseModule]] = [] - -def register_module(module_class: Type[BaseModule]): - """Register an AI security module""" - AI_SECURITY_MODULES.append(module_class) - return module_class - -def get_available_modules() -> List[Type[BaseModule]]: - """Get all available AI security modules""" - return AI_SECURITY_MODULES.copy() - -# Import modules to trigger registration -from .garak import GarakModule \ No newline at end of file diff --git a/backend/toolbox/modules/ai_security/garak.py b/backend/toolbox/modules/ai_security/garak.py deleted file mode 100644 index 335382f..0000000 --- a/backend/toolbox/modules/ai_security/garak.py +++ /dev/null @@ -1,526 +0,0 @@ -""" -Garak AI Security Module - -This module uses Garak for AI red-teaming and LLM vulnerability assessment, -testing for prompt injection, bias, jailbreaks, and other AI-specific security issues. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -import os -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class GarakModule(BaseModule): - """Garak AI red-teaming and LLM vulnerability assessment module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="garak", - version="0.9.0", - description="AI red-teaming framework for testing LLM vulnerabilities including prompt injection, bias, and jailbreaks", - author="FuzzForge Team", - category="ai_security", - tags=["ai", "llm", "prompt-injection", "bias", "jailbreak", "red-team"], - input_schema={ - "type": "object", - "properties": { - "model_type": { - "type": "string", - "enum": ["openai", "huggingface", "anthropic", "local"], - "description": "Type of LLM to test" - }, - "model_name": { - "type": "string", - "description": "Name/path of the model to test" - }, - "api_key": { - "type": "string", - "description": "API key for cloud models (if required)" - }, - "probes": { - "type": "array", - "items": {"type": "string"}, - "default": ["encoding", "promptinject", "malwaregen", "dan"], - "description": "Probe types to run" - }, - "generations": { - "type": "integer", - "default": 10, - "description": "Number of generations per probe" - }, - "detectors": { - "type": "array", - "items": {"type": "string"}, - "description": "Detectors to use for evaluation" - }, - "config_file": { - "type": "string", - "description": "Path to Garak configuration file" - }, - "report_prefix": { - "type": "string", - "default": "garak", - "description": "Prefix for report files" - }, - "parallel_requests": { - "type": "integer", - "default": 1, - "description": "Number of parallel requests" - }, - "temperature": { - "type": "number", - "default": 0.7, - "description": "Model temperature setting" - }, - "max_tokens": { - "type": "integer", - "default": 150, - "description": "Maximum tokens per generation" - }, - "seed": { - "type": "integer", - "description": "Random seed for reproducibility" - }, - "verbose": { - "type": "boolean", - "default": false, - "description": "Enable verbose output" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "probe_name": {"type": "string"}, - "vulnerability_type": {"type": "string"}, - "success_rate": {"type": "number"}, - "prompt": {"type": "string"}, - "response": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - model_type = config.get("model_type") - if not model_type: - raise ValueError("model_type is required") - - model_name = config.get("model_name") - if not model_name: - raise ValueError("model_name is required") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Garak AI security testing""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running Garak AI security assessment") - - # Check Garak installation - await self._check_garak_installation() - - # Run Garak testing - findings = await self._run_garak_assessment(config, workspace) - - # Create summary - summary = self._create_summary(findings) - - logger.info(f"Garak found {len(findings)} AI security issues") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Garak module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _check_garak_installation(self): - """Check if Garak is installed""" - try: - process = await asyncio.create_subprocess_exec( - "python", "-c", "import garak; print(garak.__version__)", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - # Try installing if not available - logger.info("Garak not found, attempting installation...") - install_process = await asyncio.create_subprocess_exec( - "pip", "install", "garak", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - await install_process.communicate() - - except Exception as e: - logger.warning(f"Garak installation check failed: {e}") - - async def _run_garak_assessment(self, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run Garak AI security assessment""" - findings = [] - - try: - # Build Garak command - cmd = ["python", "-m", "garak"] - - # Add model configuration - cmd.extend(["--model_type", config["model_type"]]) - cmd.extend(["--model_name", config["model_name"]]) - - # Add API key if provided - api_key = config.get("api_key") - if api_key: - # Set environment variable instead of command line for security - os.environ["GARAK_API_KEY"] = api_key - - # Add probes - probes = config.get("probes", ["encoding", "promptinject"]) - for probe in probes: - cmd.extend(["--probes", probe]) - - # Add generations - generations = config.get("generations", 10) - cmd.extend(["--generations", str(generations)]) - - # Add detectors if specified - detectors = config.get("detectors", []) - for detector in detectors: - cmd.extend(["--detectors", detector]) - - # Add parallel requests - parallel = config.get("parallel_requests", 1) - if parallel > 1: - cmd.extend(["--parallel_requests", str(parallel)]) - - # Add model parameters - temperature = config.get("temperature", 0.7) - cmd.extend(["--temperature", str(temperature)]) - - max_tokens = config.get("max_tokens", 150) - cmd.extend(["--max_tokens", str(max_tokens)]) - - # Add seed for reproducibility - seed = config.get("seed") - if seed: - cmd.extend(["--seed", str(seed)]) - - # Add configuration file - config_file = config.get("config_file") - if config_file: - config_path = workspace / config_file - if config_path.exists(): - cmd.extend(["--config", str(config_path)]) - - # Set output directory - output_dir = workspace / "garak_output" - output_dir.mkdir(exist_ok=True) - cmd.extend(["--report_prefix", str(output_dir / config.get("report_prefix", "garak"))]) - - # Add verbose flag - if config.get("verbose", False): - cmd.append("--verbose") - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run Garak - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results - findings = self._parse_garak_results(output_dir, workspace, stdout.decode(), stderr.decode()) - - except Exception as e: - logger.warning(f"Error running Garak assessment: {e}") - - return findings - - def _parse_garak_results(self, output_dir: Path, workspace: Path, stdout: str, stderr: str) -> List[ModuleFinding]: - """Parse Garak output for findings""" - findings = [] - - try: - # Look for JSON report files - report_files = list(output_dir.glob("*.report.jsonl")) - - for report_file in report_files: - findings.extend(self._parse_report_file(report_file, workspace)) - - # If no report files, try to parse stdout - if not findings: - findings = self._parse_stdout_output(stdout, stderr, workspace) - - except Exception as e: - logger.warning(f"Error parsing Garak results: {e}") - - return findings - - def _parse_report_file(self, report_file: Path, workspace: Path) -> List[ModuleFinding]: - """Parse Garak JSONL report file""" - findings = [] - - try: - with open(report_file, 'r') as f: - for line in f: - if line.strip(): - data = json.loads(line) - finding = self._create_garak_finding(data, workspace, report_file) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing report file {report_file}: {e}") - - return findings - - def _parse_stdout_output(self, stdout: str, stderr: str, workspace: Path) -> List[ModuleFinding]: - """Parse Garak stdout output""" - findings = [] - - try: - # Look for vulnerability indicators in output - lines = stdout.split('\n') + stderr.split('\n') - - for line in lines: - if any(indicator in line.lower() for indicator in [ - "vulnerability", "injection", "jailbreak", "bias", "harmful" - ]): - # Create a basic finding from the output line - finding = self._create_basic_finding(line, workspace) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing Garak stdout: {e}") - - return findings - - def _create_garak_finding(self, data: Dict[str, Any], workspace: Path, report_file: Path) -> ModuleFinding: - """Create finding from Garak report data""" - try: - # Extract information from Garak data structure - probe_name = data.get("probe", "unknown") - detector_name = data.get("detector", "unknown") - passed = data.get("passed", True) - prompt = data.get("prompt", "")[:500] # Limit length - response = data.get("response", "")[:500] # Limit length - - # Only create findings for failed tests (vulnerabilities found) - if passed: - return None - - # Determine vulnerability type and severity - vulnerability_type = self._get_vulnerability_type(probe_name, detector_name) - severity = self._get_vulnerability_severity(vulnerability_type, probe_name) - - # Create relative path - try: - rel_path = report_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(report_file) - - finding = self.create_finding( - title=f"AI Security Issue: {vulnerability_type}", - description=f"Garak detected a {vulnerability_type} vulnerability using probe '{probe_name}' and detector '{detector_name}'", - severity=severity, - category=self._get_ai_security_category(vulnerability_type), - file_path=file_path, - recommendation=self._get_ai_security_recommendation(vulnerability_type, probe_name), - metadata={ - "probe_name": probe_name, - "detector_name": detector_name, - "vulnerability_type": vulnerability_type, - "prompt_preview": prompt, - "response_preview": response, - "passed": passed, - "fuzzer": "garak" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating Garak finding: {e}") - return None - - def _create_basic_finding(self, line: str, workspace: Path) -> ModuleFinding: - """Create basic finding from output line""" - try: - # Extract basic information from line - vulnerability_type = "ai_security_issue" - if "injection" in line.lower(): - vulnerability_type = "prompt_injection" - elif "jailbreak" in line.lower(): - vulnerability_type = "jailbreak_attempt" - elif "bias" in line.lower(): - vulnerability_type = "bias_detection" - - finding = self.create_finding( - title=f"AI Security Detection: {vulnerability_type.replace('_', ' ').title()}", - description=f"Garak detected potential AI security issue: {line.strip()}", - severity="medium", - category=self._get_ai_security_category(vulnerability_type), - file_path=None, - recommendation=self._get_ai_security_recommendation(vulnerability_type, "general"), - metadata={ - "vulnerability_type": vulnerability_type, - "detection_line": line.strip(), - "fuzzer": "garak" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating basic finding: {e}") - return None - - def _get_vulnerability_type(self, probe_name: str, detector_name: str) -> str: - """Determine vulnerability type from probe and detector names""" - probe_lower = probe_name.lower() - detector_lower = detector_name.lower() - - if "inject" in probe_lower or "inject" in detector_lower: - return "prompt_injection" - elif "jailbreak" in probe_lower or "dan" in probe_lower: - return "jailbreak_attempt" - elif "bias" in probe_lower or "bias" in detector_lower: - return "bias_detection" - elif "malware" in probe_lower or "harmful" in detector_lower: - return "harmful_content_generation" - elif "encoding" in probe_lower: - return "encoding_vulnerability" - elif "leak" in probe_lower: - return "data_leakage" - else: - return "ai_security_vulnerability" - - def _get_vulnerability_severity(self, vulnerability_type: str, probe_name: str) -> str: - """Determine severity based on vulnerability type""" - if vulnerability_type in ["prompt_injection", "jailbreak_attempt"]: - return "high" - elif vulnerability_type in ["harmful_content_generation", "data_leakage"]: - return "high" - elif vulnerability_type in ["bias_detection", "encoding_vulnerability"]: - return "medium" - else: - return "medium" - - def _get_ai_security_category(self, vulnerability_type: str) -> str: - """Get category for AI security vulnerability""" - if "injection" in vulnerability_type: - return "prompt_injection" - elif "jailbreak" in vulnerability_type: - return "jailbreak_attack" - elif "bias" in vulnerability_type: - return "algorithmic_bias" - elif "harmful" in vulnerability_type or "malware" in vulnerability_type: - return "harmful_content" - elif "leak" in vulnerability_type: - return "data_leakage" - elif "encoding" in vulnerability_type: - return "input_manipulation" - else: - return "ai_security" - - def _get_ai_security_recommendation(self, vulnerability_type: str, probe_name: str) -> str: - """Get recommendation for AI security vulnerability""" - if "injection" in vulnerability_type: - return "Implement robust input validation, prompt sanitization, and use structured prompts to prevent injection attacks. Consider implementing content filtering and output validation." - elif "jailbreak" in vulnerability_type: - return "Strengthen model alignment and safety measures. Implement content filtering, use constitutional AI techniques, and add safety classifiers for output validation." - elif "bias" in vulnerability_type: - return "Review training data for bias, implement fairness constraints, use debiasing techniques, and conduct regular bias audits across different demographic groups." - elif "harmful" in vulnerability_type: - return "Implement strict content policies, use safety classifiers, add human oversight for sensitive outputs, and refuse to generate harmful content." - elif "leak" in vulnerability_type: - return "Review data handling practices, implement data anonymization, use differential privacy techniques, and audit model responses for sensitive information disclosure." - elif "encoding" in vulnerability_type: - return "Normalize and validate all input encodings, implement proper character filtering, and use encoding-aware input processing." - else: - return f"Address the {vulnerability_type} vulnerability by implementing appropriate AI safety measures, input validation, and output monitoring." - - def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - vulnerability_counts = {} - probe_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by vulnerability type - vuln_type = finding.metadata.get("vulnerability_type", "unknown") - vulnerability_counts[vuln_type] = vulnerability_counts.get(vuln_type, 0) + 1 - - # Count by probe - probe = finding.metadata.get("probe_name", "unknown") - probe_counts[probe] = probe_counts.get(probe, 0) + 1 - - return { - "total_findings": len(findings), - "severity_counts": severity_counts, - "category_counts": category_counts, - "vulnerability_counts": vulnerability_counts, - "probe_counts": probe_counts, - "ai_security_issues": len(findings), - "high_risk_vulnerabilities": severity_counts.get("high", 0) + severity_counts.get("critical", 0) - } \ No newline at end of file diff --git a/backend/toolbox/modules/cicd_security/__init__.py b/backend/toolbox/modules/cicd_security/__init__.py deleted file mode 100644 index 8bb992b..0000000 --- a/backend/toolbox/modules/cicd_security/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -CI/CD Security Modules - -This package contains modules for CI/CD pipeline and workflow security testing. - -Available modules: -- Zizmor: GitHub Actions workflow security analyzer -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -from typing import List, Type -from ..base import BaseModule - -# Module registry for automatic discovery -CICD_SECURITY_MODULES: List[Type[BaseModule]] = [] - -def register_module(module_class: Type[BaseModule]): - """Register a CI/CD security module""" - CICD_SECURITY_MODULES.append(module_class) - return module_class - -def get_available_modules() -> List[Type[BaseModule]]: - """Get all available CI/CD security modules""" - return CICD_SECURITY_MODULES.copy() - -# Import modules to trigger registration -from .zizmor import ZizmorModule \ No newline at end of file diff --git a/backend/toolbox/modules/cicd_security/zizmor.py b/backend/toolbox/modules/cicd_security/zizmor.py deleted file mode 100644 index f67496c..0000000 --- a/backend/toolbox/modules/cicd_security/zizmor.py +++ /dev/null @@ -1,595 +0,0 @@ -""" -Zizmor CI/CD Security Module - -This module uses Zizmor to analyze GitHub Actions workflows for security -vulnerabilities and misconfigurations. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class ZizmorModule(BaseModule): - """Zizmor GitHub Actions security analysis module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="zizmor", - version="0.2.0", - description="GitHub Actions workflow security analyzer for detecting vulnerabilities and misconfigurations", - author="FuzzForge Team", - category="cicd_security", - tags=["github-actions", "cicd", "workflow", "security", "pipeline"], - input_schema={ - "type": "object", - "properties": { - "workflow_dir": { - "type": "string", - "default": ".github/workflows", - "description": "Directory containing GitHub Actions workflows" - }, - "workflow_files": { - "type": "array", - "items": {"type": "string"}, - "description": "Specific workflow files to analyze" - }, - "format": { - "type": "string", - "enum": ["json", "sarif", "pretty"], - "default": "json", - "description": "Output format" - }, - "verbose": { - "type": "boolean", - "default": false, - "description": "Enable verbose output" - }, - "offline": { - "type": "boolean", - "default": false, - "description": "Run in offline mode (no internet lookups)" - }, - "no_online_audits": { - "type": "boolean", - "default": true, - "description": "Disable online audits for faster execution" - }, - "pedantic": { - "type": "boolean", - "default": false, - "description": "Enable pedantic mode (more strict checking)" - }, - "rules": { - "type": "array", - "items": {"type": "string"}, - "description": "Specific rules to run" - }, - "ignore_rules": { - "type": "array", - "items": {"type": "string"}, - "description": "Rules to ignore" - }, - "min_severity": { - "type": "string", - "enum": ["unknown", "informational", "low", "medium", "high"], - "default": "low", - "description": "Minimum severity level to report" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "rule_id": {"type": "string"}, - "rule_name": {"type": "string"}, - "severity": {"type": "string"}, - "workflow_file": {"type": "string"}, - "line_number": {"type": "integer"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - workflow_dir = config.get("workflow_dir", ".github/workflows") - workflow_files = config.get("workflow_files", []) - - if not workflow_dir and not workflow_files: - raise ValueError("Either workflow_dir or workflow_files must be specified") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Zizmor GitHub Actions security analysis""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running Zizmor GitHub Actions security analysis") - - # Check Zizmor installation - await self._check_zizmor_installation() - - # Find workflow files - workflow_files = self._find_workflow_files(workspace, config) - if not workflow_files: - logger.info("No GitHub Actions workflow files found") - return self.create_result( - findings=[], - status="success", - summary={"total_findings": 0, "workflows_scanned": 0} - ) - - # Run Zizmor analysis - findings = await self._run_zizmor_analysis(workflow_files, config, workspace) - - # Create summary - summary = self._create_summary(findings, len(workflow_files)) - - logger.info(f"Zizmor found {len(findings)} CI/CD security issues") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Zizmor module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _check_zizmor_installation(self): - """Check if Zizmor is installed""" - try: - process = await asyncio.create_subprocess_exec( - "zizmor", "--version", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - raise RuntimeError("Zizmor not found. Install with: cargo install zizmor") - - except FileNotFoundError: - raise RuntimeError("Zizmor not found. Install with: cargo install zizmor") - except Exception as e: - raise RuntimeError(f"Zizmor installation check failed: {e}") - - def _find_workflow_files(self, workspace: Path, config: Dict[str, Any]) -> List[Path]: - """Find GitHub Actions workflow files""" - workflow_files = [] - - # Check for specific files - specific_files = config.get("workflow_files", []) - for file_path in specific_files: - full_path = workspace / file_path - if full_path.exists(): - workflow_files.append(full_path) - - # Check workflow directory - if not workflow_files: - workflow_dir = workspace / config.get("workflow_dir", ".github/workflows") - if workflow_dir.exists(): - # Find YAML files - for pattern in ["*.yml", "*.yaml"]: - workflow_files.extend(workflow_dir.glob(pattern)) - - return list(set(workflow_files)) # Remove duplicates - - async def _run_zizmor_analysis(self, workflow_files: List[Path], config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run Zizmor analysis on workflow files""" - findings = [] - - try: - for workflow_file in workflow_files: - file_findings = await self._analyze_workflow_file(workflow_file, config, workspace) - findings.extend(file_findings) - - except Exception as e: - logger.warning(f"Error running Zizmor analysis: {e}") - - return findings - - async def _analyze_workflow_file(self, workflow_file: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Analyze a single workflow file with Zizmor""" - findings = [] - - try: - # Build Zizmor command - cmd = ["zizmor"] - - # Add format - format_type = config.get("format", "json") - cmd.extend(["--format", format_type]) - - # Add minimum severity - min_severity = config.get("min_severity", "low") - cmd.extend(["--min-severity", min_severity]) - - # Add flags - if config.get("verbose", False): - cmd.append("--verbose") - - if config.get("offline", False): - cmd.append("--offline") - - if config.get("no_online_audits", True): - cmd.append("--no-online-audits") - - if config.get("pedantic", False): - cmd.append("--pedantic") - - # Add specific rules - rules = config.get("rules", []) - for rule in rules: - cmd.extend(["--rules", rule]) - - # Add ignore rules - ignore_rules = config.get("ignore_rules", []) - for rule in ignore_rules: - cmd.extend(["--ignore", rule]) - - # Add workflow file - cmd.append(str(workflow_file)) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run Zizmor - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results (even if return code is non-zero, as it may contain findings) - if stdout.strip(): - findings = self._parse_zizmor_output( - stdout.decode(), workflow_file, workspace, format_type - ) - elif stderr.strip(): - logger.warning(f"Zizmor analysis failed for {workflow_file}: {stderr.decode()}") - - except Exception as e: - logger.warning(f"Error analyzing workflow file {workflow_file}: {e}") - - return findings - - def _parse_zizmor_output(self, output: str, workflow_file: Path, workspace: Path, format_type: str) -> List[ModuleFinding]: - """Parse Zizmor output into findings""" - findings = [] - - try: - if format_type == "json": - findings = self._parse_json_output(output, workflow_file, workspace) - elif format_type == "sarif": - findings = self._parse_sarif_output(output, workflow_file, workspace) - else: - findings = self._parse_text_output(output, workflow_file, workspace) - - except Exception as e: - logger.warning(f"Error parsing Zizmor output: {e}") - - return findings - - def _parse_json_output(self, output: str, workflow_file: Path, workspace: Path) -> List[ModuleFinding]: - """Parse Zizmor JSON output""" - findings = [] - - try: - if not output.strip(): - return findings - - data = json.loads(output) - - # Handle different JSON structures - if isinstance(data, dict): - # Single result - findings.extend(self._process_zizmor_result(data, workflow_file, workspace)) - elif isinstance(data, list): - # Multiple results - for result in data: - findings.extend(self._process_zizmor_result(result, workflow_file, workspace)) - - except json.JSONDecodeError as e: - logger.warning(f"Failed to parse Zizmor JSON output: {e}") - - return findings - - def _parse_sarif_output(self, output: str, workflow_file: Path, workspace: Path) -> List[ModuleFinding]: - """Parse Zizmor SARIF output""" - findings = [] - - try: - data = json.loads(output) - runs = data.get("runs", []) - - for run in runs: - results = run.get("results", []) - for result in results: - finding = self._create_sarif_finding(result, workflow_file, workspace) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing SARIF output: {e}") - - return findings - - def _parse_text_output(self, output: str, workflow_file: Path, workspace: Path) -> List[ModuleFinding]: - """Parse Zizmor text output""" - findings = [] - - try: - lines = output.strip().split('\n') - for line in lines: - if line.strip() and not line.startswith('#'): - # Create basic finding from text line - finding = self._create_text_finding(line, workflow_file, workspace) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing text output: {e}") - - return findings - - def _process_zizmor_result(self, result: Dict[str, Any], workflow_file: Path, workspace: Path) -> List[ModuleFinding]: - """Process a single Zizmor result""" - findings = [] - - try: - # Extract rule information - rule_id = result.get("rule", {}).get("id", "unknown") - rule_name = result.get("rule", {}).get("desc", rule_id) - severity = result.get("severity", "medium") - message = result.get("message", "") - - # Extract location information - locations = result.get("locations", []) - if not locations: - # Create finding without specific location - finding = self._create_zizmor_finding( - rule_id, rule_name, severity, message, workflow_file, workspace - ) - if finding: - findings.append(finding) - else: - # Create finding for each location - for location in locations: - line_number = location.get("line", 0) - column = location.get("column", 0) - - finding = self._create_zizmor_finding( - rule_id, rule_name, severity, message, workflow_file, workspace, - line_number, column - ) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error processing Zizmor result: {e}") - - return findings - - def _create_zizmor_finding(self, rule_id: str, rule_name: str, severity: str, message: str, - workflow_file: Path, workspace: Path, line_number: int = None, column: int = None) -> ModuleFinding: - """Create finding from Zizmor analysis""" - try: - # Map Zizmor severity to our standard levels - finding_severity = self._map_severity(severity) - - # Create relative path - try: - rel_path = workflow_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(workflow_file) - - # Get category and recommendation - category = self._get_cicd_category(rule_id, rule_name) - recommendation = self._get_cicd_recommendation(rule_id, rule_name, message) - - finding = self.create_finding( - title=f"CI/CD Security Issue: {rule_name}", - description=message or f"Zizmor detected a security issue: {rule_name}", - severity=finding_severity, - category=category, - file_path=file_path, - line_start=line_number if line_number else None, - recommendation=recommendation, - metadata={ - "rule_id": rule_id, - "rule_name": rule_name, - "zizmor_severity": severity, - "workflow_file": str(workflow_file.name), - "line_number": line_number, - "column": column, - "tool": "zizmor" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating Zizmor finding: {e}") - return None - - def _create_sarif_finding(self, result: Dict[str, Any], workflow_file: Path, workspace: Path) -> ModuleFinding: - """Create finding from SARIF result""" - try: - rule_id = result.get("ruleId", "unknown") - message = result.get("message", {}).get("text", "") - severity = result.get("level", "warning") - - # Extract location - locations = result.get("locations", []) - line_number = None - if locations: - physical_location = locations[0].get("physicalLocation", {}) - region = physical_location.get("region", {}) - line_number = region.get("startLine") - - return self._create_zizmor_finding( - rule_id, rule_id, severity, message, workflow_file, workspace, line_number - ) - - except Exception as e: - logger.warning(f"Error creating SARIF finding: {e}") - return None - - def _create_text_finding(self, line: str, workflow_file: Path, workspace: Path) -> ModuleFinding: - """Create finding from text line""" - try: - try: - rel_path = workflow_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(workflow_file) - - finding = self.create_finding( - title="CI/CD Security Issue", - description=line.strip(), - severity="medium", - category="workflow_security", - file_path=file_path, - recommendation="Review and address the workflow security issue identified by Zizmor.", - metadata={ - "detection_line": line.strip(), - "workflow_file": str(workflow_file.name), - "tool": "zizmor" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating text finding: {e}") - return None - - def _map_severity(self, zizmor_severity: str) -> str: - """Map Zizmor severity to our standard levels""" - severity_map = { - "high": "high", - "medium": "medium", - "low": "low", - "informational": "info", - "unknown": "low", - "error": "high", - "warning": "medium", - "note": "low" - } - return severity_map.get(zizmor_severity.lower(), "medium") - - def _get_cicd_category(self, rule_id: str, rule_name: str) -> str: - """Get category for CI/CD security issue""" - rule_lower = f"{rule_id} {rule_name}".lower() - - if any(term in rule_lower for term in ["secret", "token", "credential", "password"]): - return "secret_exposure" - elif any(term in rule_lower for term in ["permission", "access", "privilege"]): - return "permission_escalation" - elif any(term in rule_lower for term in ["injection", "command", "script"]): - return "code_injection" - elif any(term in rule_lower for term in ["artifact", "cache", "upload"]): - return "artifact_security" - elif any(term in rule_lower for term in ["environment", "env", "variable"]): - return "environment_security" - elif any(term in rule_lower for term in ["network", "external", "download"]): - return "network_security" - else: - return "workflow_security" - - def _get_cicd_recommendation(self, rule_id: str, rule_name: str, message: str) -> str: - """Get recommendation for CI/CD security issue""" - rule_lower = f"{rule_id} {rule_name}".lower() - - if "secret" in rule_lower or "token" in rule_lower: - return "Store secrets securely using GitHub Secrets or environment variables. Never hardcode credentials in workflow files." - elif "permission" in rule_lower: - return "Follow the principle of least privilege. Grant only necessary permissions and use specific permission scopes." - elif "injection" in rule_lower: - return "Avoid using user input directly in shell commands. Use proper escaping, validation, or structured approaches." - elif "artifact" in rule_lower: - return "Secure artifact handling by validating checksums, using signed artifacts, and restricting artifact access." - elif "environment" in rule_lower: - return "Protect environment variables and avoid exposing sensitive information in logs or outputs." - elif "network" in rule_lower: - return "Use HTTPS for external connections, validate certificates, and avoid downloading from untrusted sources." - elif message: - return f"Address the identified issue: {message}" - else: - return f"Review and fix the workflow security issue: {rule_name}" - - def _create_summary(self, findings: List[ModuleFinding], workflows_count: int) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - rule_counts = {} - workflow_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by rule - rule_id = finding.metadata.get("rule_id", "unknown") - rule_counts[rule_id] = rule_counts.get(rule_id, 0) + 1 - - # Count by workflow - workflow = finding.metadata.get("workflow_file", "unknown") - workflow_counts[workflow] = workflow_counts.get(workflow, 0) + 1 - - return { - "total_findings": len(findings), - "workflows_scanned": workflows_count, - "severity_counts": severity_counts, - "category_counts": category_counts, - "top_rules": dict(sorted(rule_counts.items(), key=lambda x: x[1], reverse=True)[:10]), - "workflows_with_issues": len(workflow_counts), - "workflow_issue_counts": dict(sorted(workflow_counts.items(), key=lambda x: x[1], reverse=True)[:10]) - } \ No newline at end of file diff --git a/backend/toolbox/modules/fuzzing/__init__.py b/backend/toolbox/modules/fuzzing/__init__.py deleted file mode 100644 index f1dc43c..0000000 --- a/backend/toolbox/modules/fuzzing/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Fuzzing Modules - -This package contains modules for various fuzzing techniques and tools. - -Available modules: -- LibFuzzer: LLVM's coverage-guided fuzzing engine -- AFL++: Advanced American Fuzzy Lop with modern features -- AFL-RS: Rust-based AFL implementation -- Atheris: Python fuzzing engine for finding bugs in Python code -- Cargo Fuzz: Rust fuzzing integration with libFuzzer -- Go-Fuzz: Coverage-guided fuzzing for Go packages -- OSS-Fuzz: Google's continuous fuzzing for open source -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -from typing import List, Type -from ..base import BaseModule - -# Module registry for automatic discovery -FUZZING_MODULES: List[Type[BaseModule]] = [] - -def register_module(module_class: Type[BaseModule]): - """Register a fuzzing module""" - FUZZING_MODULES.append(module_class) - return module_class - -def get_available_modules() -> List[Type[BaseModule]]: - """Get all available fuzzing modules""" - return FUZZING_MODULES.copy() - -# Import modules to trigger registration -from .libfuzzer import LibFuzzerModule -from .aflplusplus import AFLPlusPlusModule -from .aflrs import AFLRSModule -from .atheris import AtherisModule -from .cargo_fuzz import CargoFuzzModule -from .go_fuzz import GoFuzzModule -from .oss_fuzz import OSSFuzzModule \ No newline at end of file diff --git a/backend/toolbox/modules/fuzzing/aflplusplus.py b/backend/toolbox/modules/fuzzing/aflplusplus.py deleted file mode 100644 index 24e83b4..0000000 --- a/backend/toolbox/modules/fuzzing/aflplusplus.py +++ /dev/null @@ -1,734 +0,0 @@ -""" -AFL++ Fuzzing Module - -This module uses AFL++ (Advanced American Fuzzy Lop) for coverage-guided -fuzzing with modern features and optimizations. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -import os -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging -import re - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class AFLPlusPlusModule(BaseModule): - """AFL++ advanced fuzzing module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="aflplusplus", - version="4.09c", - description="Advanced American Fuzzy Lop with modern features for coverage-guided fuzzing", - author="FuzzForge Team", - category="fuzzing", - tags=["coverage-guided", "american-fuzzy-lop", "advanced", "mutation", "instrumentation"], - input_schema={ - "type": "object", - "properties": { - "target_binary": { - "type": "string", - "description": "Path to the target binary (compiled with afl-gcc/afl-clang)" - }, - "input_dir": { - "type": "string", - "description": "Directory containing seed input files" - }, - "output_dir": { - "type": "string", - "default": "afl_output", - "description": "Output directory for AFL++ results" - }, - "dictionary": { - "type": "string", - "description": "Dictionary file for fuzzing keywords" - }, - "timeout": { - "type": "integer", - "default": 1000, - "description": "Timeout for each execution (ms)" - }, - "memory_limit": { - "type": "integer", - "default": 50, - "description": "Memory limit for child process (MB)" - }, - "skip_deterministic": { - "type": "boolean", - "default": false, - "description": "Skip deterministic mutations" - }, - "no_arith": { - "type": "boolean", - "default": false, - "description": "Skip arithmetic mutations" - }, - "shuffle_queue": { - "type": "boolean", - "default": false, - "description": "Shuffle queue entries" - }, - "max_total_time": { - "type": "integer", - "default": 3600, - "description": "Maximum total fuzzing time (seconds)" - }, - "power_schedule": { - "type": "string", - "enum": ["explore", "fast", "coe", "lin", "quad", "exploit", "rare"], - "default": "fast", - "description": "Power schedule algorithm" - }, - "mutation_mode": { - "type": "string", - "enum": ["default", "old", "mopt"], - "default": "default", - "description": "Mutation mode to use" - }, - "parallel_fuzzing": { - "type": "boolean", - "default": false, - "description": "Enable parallel fuzzing with multiple instances" - }, - "fuzzer_instances": { - "type": "integer", - "default": 1, - "description": "Number of parallel fuzzer instances" - }, - "master_instance": { - "type": "string", - "default": "master", - "description": "Name for master fuzzer instance" - }, - "slave_prefix": { - "type": "string", - "default": "slave", - "description": "Prefix for slave fuzzer instances" - }, - "hang_timeout": { - "type": "integer", - "default": 1000, - "description": "Timeout for detecting hangs (ms)" - }, - "crash_mode": { - "type": "boolean", - "default": false, - "description": "Run in crash exploration mode" - }, - "target_args": { - "type": "array", - "items": {"type": "string"}, - "description": "Arguments to pass to target binary" - }, - "env_vars": { - "type": "object", - "description": "Environment variables to set" - }, - "ignore_finds": { - "type": "boolean", - "default": false, - "description": "Ignore existing findings and start fresh" - }, - "force_deterministic": { - "type": "boolean", - "default": false, - "description": "Force deterministic mutations" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "crash_id": {"type": "string"}, - "crash_file": {"type": "string"}, - "crash_type": {"type": "string"}, - "signal": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - target_binary = config.get("target_binary") - if not target_binary: - raise ValueError("target_binary is required for AFL++") - - input_dir = config.get("input_dir") - if not input_dir: - raise ValueError("input_dir is required for AFL++") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute AFL++ fuzzing""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running AFL++ fuzzing campaign") - - # Check prerequisites - await self._check_afl_prerequisites(workspace) - - # Setup directories and files - target_binary, input_dir, output_dir = self._setup_afl_directories(config, workspace) - - # Run AFL++ fuzzing - findings = await self._run_afl_fuzzing(target_binary, input_dir, output_dir, config, workspace) - - # Create summary - summary = self._create_summary(findings, output_dir) - - logger.info(f"AFL++ found {len(findings)} crashes") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"AFL++ module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _check_afl_prerequisites(self, workspace: Path): - """Check AFL++ prerequisites and system setup""" - try: - # Check if afl-fuzz exists - process = await asyncio.create_subprocess_exec( - "which", "afl-fuzz", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - raise RuntimeError("afl-fuzz not found. Please install AFL++") - - # Check core dump pattern (important for AFL) - try: - with open("/proc/sys/kernel/core_pattern", "r") as f: - core_pattern = f.read().strip() - if core_pattern != "core": - logger.warning(f"Core dump pattern is '{core_pattern}', AFL++ may not work optimally") - except Exception: - logger.warning("Could not check core dump pattern") - - except Exception as e: - logger.warning(f"AFL++ prerequisite check failed: {e}") - - def _setup_afl_directories(self, config: Dict[str, Any], workspace: Path): - """Setup AFL++ directories and validate files""" - # Check target binary - target_binary = workspace / config["target_binary"] - if not target_binary.exists(): - raise FileNotFoundError(f"Target binary not found: {target_binary}") - - # Check input directory - input_dir = workspace / config["input_dir"] - if not input_dir.exists(): - raise FileNotFoundError(f"Input directory not found: {input_dir}") - - # Check if input directory has files - input_files = list(input_dir.glob("*")) - if not input_files: - raise ValueError(f"Input directory is empty: {input_dir}") - - # Create output directory - output_dir = workspace / config.get("output_dir", "afl_output") - output_dir.mkdir(exist_ok=True) - - return target_binary, input_dir, output_dir - - async def _run_afl_fuzzing(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run AFL++ fuzzing""" - findings = [] - - try: - if config.get("parallel_fuzzing", False): - findings = await self._run_parallel_fuzzing( - target_binary, input_dir, output_dir, config, workspace - ) - else: - findings = await self._run_single_fuzzing( - target_binary, input_dir, output_dir, config, workspace - ) - - except Exception as e: - logger.warning(f"Error running AFL++ fuzzing: {e}") - - return findings - - async def _run_single_fuzzing(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run single-instance AFL++ fuzzing""" - findings = [] - - try: - # Build AFL++ command - cmd = ["afl-fuzz"] - - # Add input and output directories - cmd.extend(["-i", str(input_dir)]) - cmd.extend(["-o", str(output_dir)]) - - # Add dictionary if specified - dictionary = config.get("dictionary") - if dictionary: - dict_path = workspace / dictionary - if dict_path.exists(): - cmd.extend(["-x", str(dict_path)]) - - # Add timeout - timeout = config.get("timeout", 1000) - cmd.extend(["-t", str(timeout)]) - - # Add memory limit - memory_limit = config.get("memory_limit", 50) - cmd.extend(["-m", str(memory_limit)]) - - # Add power schedule - power_schedule = config.get("power_schedule", "fast") - cmd.extend(["-p", power_schedule]) - - # Add mutation options - if config.get("skip_deterministic", False): - cmd.append("-d") - - if config.get("no_arith", False): - cmd.append("-a") - - if config.get("shuffle_queue", False): - cmd.append("-Z") - - # Add hang timeout - hang_timeout = config.get("hang_timeout", 1000) - cmd.extend(["-T", str(hang_timeout)]) - - # Add crash mode - if config.get("crash_mode", False): - cmd.append("-C") - - # Add ignore finds - if config.get("ignore_finds", False): - cmd.append("-f") - - # Add force deterministic - if config.get("force_deterministic", False): - cmd.append("-D") - - # Add target binary and arguments - cmd.append("--") - cmd.append(str(target_binary)) - - target_args = config.get("target_args", []) - cmd.extend(target_args) - - # Set up environment - env = os.environ.copy() - env_vars = config.get("env_vars", {}) - env.update(env_vars) - - # Set AFL environment variables - env["AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES"] = "1" # Avoid interactive prompts - env["AFL_SKIP_CPUFREQ"] = "1" # Skip CPU frequency checks - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run AFL++ with timeout - max_total_time = config.get("max_total_time", 3600) - - try: - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace, - env=env - ) - - # Wait for specified time then terminate - try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=max_total_time - ) - except asyncio.TimeoutError: - logger.info(f"AFL++ fuzzing timed out after {max_total_time} seconds") - process.terminate() - try: - await asyncio.wait_for(process.wait(), timeout=10) - except asyncio.TimeoutError: - process.kill() - await process.wait() - - # Parse results from output directory - findings = self._parse_afl_results(output_dir, workspace) - - except Exception as e: - logger.warning(f"Error running AFL++ process: {e}") - - except Exception as e: - logger.warning(f"Error in single fuzzing: {e}") - - return findings - - async def _run_parallel_fuzzing(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run parallel AFL++ fuzzing""" - findings = [] - - try: - fuzzer_instances = config.get("fuzzer_instances", 2) - master_name = config.get("master_instance", "master") - slave_prefix = config.get("slave_prefix", "slave") - - processes = [] - - # Start master instance - master_cmd = await self._build_afl_command( - target_binary, input_dir, output_dir, config, workspace, - instance_name=master_name, is_master=True - ) - - master_process = await asyncio.create_subprocess_exec( - *master_cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace, - env=self._get_afl_env(config) - ) - processes.append(master_process) - - # Start slave instances - for i in range(1, fuzzer_instances): - slave_name = f"{slave_prefix}{i:02d}" - slave_cmd = await self._build_afl_command( - target_binary, input_dir, output_dir, config, workspace, - instance_name=slave_name, is_master=False - ) - - slave_process = await asyncio.create_subprocess_exec( - *slave_cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace, - env=self._get_afl_env(config) - ) - processes.append(slave_process) - - # Wait for specified time then terminate all - max_total_time = config.get("max_total_time", 3600) - - try: - await asyncio.sleep(max_total_time) - finally: - # Terminate all processes - for process in processes: - if process.returncode is None: - process.terminate() - try: - await asyncio.wait_for(process.wait(), timeout=10) - except asyncio.TimeoutError: - process.kill() - await process.wait() - - # Parse results from output directory - findings = self._parse_afl_results(output_dir, workspace) - - except Exception as e: - logger.warning(f"Error in parallel fuzzing: {e}") - - return findings - - async def _build_afl_command(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path, instance_name: str, is_master: bool) -> List[str]: - """Build AFL++ command for a fuzzer instance""" - cmd = ["afl-fuzz"] - - # Add input and output directories - cmd.extend(["-i", str(input_dir)]) - cmd.extend(["-o", str(output_dir)]) - - # Add instance name - if is_master: - cmd.extend(["-M", instance_name]) - else: - cmd.extend(["-S", instance_name]) - - # Add other options (same as single fuzzing) - dictionary = config.get("dictionary") - if dictionary: - dict_path = workspace / dictionary - if dict_path.exists(): - cmd.extend(["-x", str(dict_path)]) - - cmd.extend(["-t", str(config.get("timeout", 1000))]) - cmd.extend(["-m", str(config.get("memory_limit", 50))]) - cmd.extend(["-p", config.get("power_schedule", "fast")]) - - if config.get("skip_deterministic", False): - cmd.append("-d") - - if config.get("no_arith", False): - cmd.append("-a") - - # Add target - cmd.append("--") - cmd.append(str(target_binary)) - cmd.extend(config.get("target_args", [])) - - return cmd - - def _get_afl_env(self, config: Dict[str, Any]) -> Dict[str, str]: - """Get environment variables for AFL++""" - env = os.environ.copy() - env.update(config.get("env_vars", {})) - env["AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES"] = "1" - env["AFL_SKIP_CPUFREQ"] = "1" - return env - - def _parse_afl_results(self, output_dir: Path, workspace: Path) -> List[ModuleFinding]: - """Parse AFL++ results from output directory""" - findings = [] - - try: - # Look for crashes directory - crashes_dirs = [] - - # Single instance - crashes_dir = output_dir / "crashes" - if crashes_dir.exists(): - crashes_dirs.append(crashes_dir) - - # Multiple instances - for instance_dir in output_dir.iterdir(): - if instance_dir.is_dir(): - instance_crashes = instance_dir / "crashes" - if instance_crashes.exists(): - crashes_dirs.append(instance_crashes) - - # Process crash files - for crashes_dir in crashes_dirs: - crash_files = [f for f in crashes_dir.iterdir() if f.is_file() and f.name.startswith("id:")] - - for crash_file in crash_files: - finding = self._create_afl_crash_finding(crash_file, workspace) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing AFL++ results: {e}") - - return findings - - def _create_afl_crash_finding(self, crash_file: Path, workspace: Path) -> ModuleFinding: - """Create finding from AFL++ crash file""" - try: - # Parse crash filename for information - filename = crash_file.name - crash_info = self._parse_afl_filename(filename) - - # Try to read crash file (limited size) - crash_content = "" - try: - crash_data = crash_file.read_bytes()[:1000] - crash_content = crash_data.hex()[:200] # Hex representation, limited - except Exception: - pass - - # Determine severity based on signal - severity = self._get_crash_severity(crash_info.get("signal", "")) - - # Create relative path - try: - rel_path = crash_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(crash_file) - - finding = self.create_finding( - title=f"AFL++ Crash: {crash_info.get('signal', 'Unknown')}", - description=f"AFL++ discovered a crash with signal {crash_info.get('signal', 'unknown')} in the target program", - severity=severity, - category=self._get_crash_category(crash_info.get("signal", "")), - file_path=file_path, - recommendation=self._get_afl_crash_recommendation(crash_info.get("signal", "")), - metadata={ - "crash_id": crash_info.get("id", ""), - "signal": crash_info.get("signal", ""), - "src": crash_info.get("src", ""), - "crash_file": crash_file.name, - "crash_content_hex": crash_content, - "fuzzer": "afl++" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating AFL++ crash finding: {e}") - return None - - def _parse_afl_filename(self, filename: str) -> Dict[str, str]: - """Parse AFL++ crash filename for information""" - info = {} - - try: - # AFL++ crash filename format: id:XXXXXX,sig:XX,src:XXXXXX,op:XXX,rep:X - parts = filename.split(',') - - for part in parts: - if ':' in part: - key, value = part.split(':', 1) - info[key] = value - - except Exception: - pass - - return info - - def _get_crash_severity(self, signal: str) -> str: - """Determine severity based on crash signal""" - if not signal: - return "medium" - - signal_lower = signal.lower() - - # Critical signals indicating memory corruption - if signal in ["11", "sigsegv", "segv"]: # Segmentation fault - return "critical" - elif signal in ["6", "sigabrt", "abrt"]: # Abort - return "high" - elif signal in ["4", "sigill", "ill"]: # Illegal instruction - return "high" - elif signal in ["8", "sigfpe", "fpe"]: # Floating point exception - return "medium" - elif signal in ["9", "sigkill", "kill"]: # Kill signal - return "medium" - else: - return "medium" - - def _get_crash_category(self, signal: str) -> str: - """Determine category based on crash signal""" - if not signal: - return "program_crash" - - if signal in ["11", "sigsegv", "segv"]: - return "memory_corruption" - elif signal in ["6", "sigabrt", "abrt"]: - return "assertion_failure" - elif signal in ["4", "sigill", "ill"]: - return "illegal_instruction" - elif signal in ["8", "sigfpe", "fpe"]: - return "arithmetic_error" - else: - return "program_crash" - - def _get_afl_crash_recommendation(self, signal: str) -> str: - """Generate recommendation based on crash signal""" - if signal in ["11", "sigsegv", "segv"]: - return "Segmentation fault detected. Investigate memory access patterns, check for buffer overflows, null pointer dereferences, or use-after-free bugs." - elif signal in ["6", "sigabrt", "abrt"]: - return "Program abort detected. Check for assertion failures, memory allocation errors, or explicit abort() calls in the code." - elif signal in ["4", "sigill", "ill"]: - return "Illegal instruction detected. Check for code corruption, invalid function pointers, or architecture-specific instruction issues." - elif signal in ["8", "sigfpe", "fpe"]: - return "Floating point exception detected. Check for division by zero, arithmetic overflow, or invalid floating point operations." - else: - return f"Program crash with signal {signal} detected. Analyze the crash dump and input to identify the root cause." - - def _create_summary(self, findings: List[ModuleFinding], output_dir: Path) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - signal_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by signal - signal = finding.metadata.get("signal", "unknown") - signal_counts[signal] = signal_counts.get(signal, 0) + 1 - - # Try to read AFL++ statistics - stats = self._read_afl_stats(output_dir) - - return { - "total_findings": len(findings), - "severity_counts": severity_counts, - "category_counts": category_counts, - "signal_counts": signal_counts, - "unique_crashes": len(set(f.metadata.get("crash_id", "") for f in findings)), - "afl_stats": stats - } - - def _read_afl_stats(self, output_dir: Path) -> Dict[str, Any]: - """Read AFL++ fuzzer statistics""" - stats = {} - - try: - # Look for fuzzer_stats file in single or multiple instance setup - stats_files = [] - - # Single instance - single_stats = output_dir / "fuzzer_stats" - if single_stats.exists(): - stats_files.append(single_stats) - - # Multiple instances - for instance_dir in output_dir.iterdir(): - if instance_dir.is_dir(): - instance_stats = instance_dir / "fuzzer_stats" - if instance_stats.exists(): - stats_files.append(instance_stats) - - # Read first stats file found - if stats_files: - with open(stats_files[0], 'r') as f: - for line in f: - if ':' in line: - key, value = line.strip().split(':', 1) - stats[key.strip()] = value.strip() - - except Exception as e: - logger.warning(f"Error reading AFL++ stats: {e}") - - return stats \ No newline at end of file diff --git a/backend/toolbox/modules/fuzzing/aflrs.py b/backend/toolbox/modules/fuzzing/aflrs.py deleted file mode 100644 index 3e77238..0000000 --- a/backend/toolbox/modules/fuzzing/aflrs.py +++ /dev/null @@ -1,678 +0,0 @@ -""" -AFL-RS Fuzzing Module - -This module uses AFL-RS (AFL in Rust) for high-performance coverage-guided fuzzing -with modern Rust implementations and optimizations. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -import os -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging -import re - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class AFLRSModule(BaseModule): - """AFL-RS Rust-based fuzzing module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="aflrs", - version="0.2.0", - description="High-performance AFL implementation in Rust with modern fuzzing features", - author="FuzzForge Team", - category="fuzzing", - tags=["coverage-guided", "rust", "afl", "high-performance", "modern"], - input_schema={ - "type": "object", - "properties": { - "target_binary": { - "type": "string", - "description": "Path to the target binary (compiled with AFL-RS instrumentation)" - }, - "input_dir": { - "type": "string", - "description": "Directory containing seed input files" - }, - "output_dir": { - "type": "string", - "default": "aflrs_output", - "description": "Output directory for AFL-RS results" - }, - "dictionary": { - "type": "string", - "description": "Dictionary file for token-based mutations" - }, - "timeout": { - "type": "integer", - "default": 1000, - "description": "Timeout for each execution (ms)" - }, - "memory_limit": { - "type": "integer", - "default": 50, - "description": "Memory limit for target process (MB)" - }, - "max_total_time": { - "type": "integer", - "default": 3600, - "description": "Maximum total fuzzing time (seconds)" - }, - "cpu_cores": { - "type": "integer", - "default": 1, - "description": "Number of CPU cores to use" - }, - "mutation_depth": { - "type": "integer", - "default": 4, - "description": "Maximum depth for cascaded mutations" - }, - "skip_deterministic": { - "type": "boolean", - "default": false, - "description": "Skip deterministic mutations" - }, - "power_schedule": { - "type": "string", - "enum": ["explore", "fast", "coe", "lin", "quad", "exploit", "rare", "mmopt", "seek"], - "default": "fast", - "description": "Power scheduling algorithm" - }, - "custom_mutators": { - "type": "array", - "items": {"type": "string"}, - "description": "Custom mutator libraries to load" - }, - "cmplog": { - "type": "boolean", - "default": true, - "description": "Enable CmpLog for comparison logging" - }, - "redqueen": { - "type": "boolean", - "default": true, - "description": "Enable RedQueen input-to-state correspondence" - }, - "unicorn_mode": { - "type": "boolean", - "default": false, - "description": "Enable Unicorn mode for emulation" - }, - "persistent_mode": { - "type": "boolean", - "default": false, - "description": "Enable persistent mode for faster execution" - }, - "target_args": { - "type": "array", - "items": {"type": "string"}, - "description": "Arguments to pass to target binary" - }, - "env_vars": { - "type": "object", - "description": "Environment variables to set" - }, - "ignore_timeouts": { - "type": "boolean", - "default": false, - "description": "Ignore timeout signals and continue fuzzing" - }, - "ignore_crashes": { - "type": "boolean", - "default": false, - "description": "Ignore crashes and continue fuzzing" - }, - "sync_dir": { - "type": "string", - "description": "Directory for syncing with other AFL instances" - }, - "sync_id": { - "type": "string", - "description": "Fuzzer ID for syncing" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "crash_id": {"type": "string"}, - "crash_file": {"type": "string"}, - "signal": {"type": "string"}, - "execution_time": {"type": "integer"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - target_binary = config.get("target_binary") - if not target_binary: - raise ValueError("target_binary is required for AFL-RS") - - input_dir = config.get("input_dir") - if not input_dir: - raise ValueError("input_dir is required for AFL-RS") - - cpu_cores = config.get("cpu_cores", 1) - if cpu_cores < 1: - raise ValueError("cpu_cores must be at least 1") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute AFL-RS fuzzing""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running AFL-RS fuzzing campaign") - - # Check AFL-RS installation - await self._check_aflrs_installation() - - # Setup directories and files - target_binary, input_dir, output_dir = self._setup_aflrs_directories(config, workspace) - - # Run AFL-RS fuzzing - findings = await self._run_aflrs_fuzzing(target_binary, input_dir, output_dir, config, workspace) - - # Create summary - summary = self._create_summary(findings, output_dir) - - logger.info(f"AFL-RS found {len(findings)} crashes") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"AFL-RS module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _check_aflrs_installation(self): - """Check if AFL-RS is installed and available""" - try: - # Check if aflrs is available (assuming aflrs binary) - process = await asyncio.create_subprocess_exec( - "which", "aflrs", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - # Try alternative AFL-RS command names - alt_commands = ["afl-fuzz-rs", "afl-rs", "cargo-afl"] - found = False - - for cmd in alt_commands: - process = await asyncio.create_subprocess_exec( - "which", cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode == 0: - found = True - break - - if not found: - raise RuntimeError("AFL-RS not found. Please install AFL-RS or ensure it's in PATH") - - except Exception as e: - logger.warning(f"AFL-RS installation check failed: {e}") - - def _setup_aflrs_directories(self, config: Dict[str, Any], workspace: Path): - """Setup AFL-RS directories and validate files""" - # Check target binary - target_binary = workspace / config["target_binary"] - if not target_binary.exists(): - raise FileNotFoundError(f"Target binary not found: {target_binary}") - - # Check input directory - input_dir = workspace / config["input_dir"] - if not input_dir.exists(): - raise FileNotFoundError(f"Input directory not found: {input_dir}") - - # Validate input files exist - input_files = list(input_dir.glob("*")) - if not input_files: - raise ValueError(f"Input directory is empty: {input_dir}") - - # Create output directory - output_dir = workspace / config.get("output_dir", "aflrs_output") - output_dir.mkdir(exist_ok=True) - - return target_binary, input_dir, output_dir - - async def _run_aflrs_fuzzing(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run AFL-RS fuzzing""" - findings = [] - - try: - # Build AFL-RS command - cmd = await self._build_aflrs_command(target_binary, input_dir, output_dir, config, workspace) - - # Set up environment - env = self._setup_aflrs_environment(config) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run AFL-RS with timeout - max_total_time = config.get("max_total_time", 3600) - - try: - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace, - env=env - ) - - # Wait for specified time then terminate - try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=max_total_time - ) - logger.info(f"AFL-RS completed after {max_total_time} seconds") - except asyncio.TimeoutError: - logger.info(f"AFL-RS fuzzing timed out after {max_total_time} seconds, terminating") - process.terminate() - try: - await asyncio.wait_for(process.wait(), timeout=10) - except asyncio.TimeoutError: - process.kill() - await process.wait() - - # Parse results - findings = self._parse_aflrs_results(output_dir, workspace) - - except Exception as e: - logger.warning(f"Error running AFL-RS process: {e}") - - except Exception as e: - logger.warning(f"Error in AFL-RS fuzzing: {e}") - - return findings - - async def _build_aflrs_command(self, target_binary: Path, input_dir: Path, output_dir: Path, config: Dict[str, Any], workspace: Path) -> List[str]: - """Build AFL-RS command""" - # Try to determine the correct AFL-RS command - aflrs_cmd = "aflrs" # Default - - # Try alternative command names - alt_commands = ["aflrs", "afl-fuzz-rs", "afl-rs"] - for cmd in alt_commands: - try: - process = await asyncio.create_subprocess_exec( - "which", cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - if process.returncode == 0: - aflrs_cmd = cmd - break - except Exception: - continue - - cmd = [aflrs_cmd] - - # Add input and output directories - cmd.extend(["-i", str(input_dir)]) - cmd.extend(["-o", str(output_dir)]) - - # Add dictionary if specified - dictionary = config.get("dictionary") - if dictionary: - dict_path = workspace / dictionary - if dict_path.exists(): - cmd.extend(["-x", str(dict_path)]) - - # Add timeout and memory limit - cmd.extend(["-t", str(config.get("timeout", 1000))]) - cmd.extend(["-m", str(config.get("memory_limit", 50))]) - - # Add CPU cores - cpu_cores = config.get("cpu_cores", 1) - if cpu_cores > 1: - cmd.extend(["-j", str(cpu_cores)]) - - # Add mutation depth - mutation_depth = config.get("mutation_depth", 4) - cmd.extend(["-d", str(mutation_depth)]) - - # Add power schedule - power_schedule = config.get("power_schedule", "fast") - cmd.extend(["-p", power_schedule]) - - # Add skip deterministic - if config.get("skip_deterministic", False): - cmd.append("-D") - - # Add custom mutators - custom_mutators = config.get("custom_mutators", []) - for mutator in custom_mutators: - cmd.extend(["-c", mutator]) - - # Add advanced features - if config.get("cmplog", True): - cmd.append("-l") - - if config.get("redqueen", True): - cmd.append("-I") - - if config.get("unicorn_mode", False): - cmd.append("-U") - - if config.get("persistent_mode", False): - cmd.append("-P") - - # Add ignore options - if config.get("ignore_timeouts", False): - cmd.append("-T") - - if config.get("ignore_crashes", False): - cmd.append("-C") - - # Add sync options - sync_dir = config.get("sync_dir") - if sync_dir: - cmd.extend(["-F", sync_dir]) - - sync_id = config.get("sync_id") - if sync_id: - cmd.extend(["-S", sync_id]) - - # Add target binary and arguments - cmd.append("--") - cmd.append(str(target_binary)) - - target_args = config.get("target_args", []) - cmd.extend(target_args) - - return cmd - - def _setup_aflrs_environment(self, config: Dict[str, Any]) -> Dict[str, str]: - """Setup environment variables for AFL-RS""" - env = os.environ.copy() - - # Add user-specified environment variables - env_vars = config.get("env_vars", {}) - env.update(env_vars) - - # Set AFL-RS specific environment variables - env["AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES"] = "1" - env["AFL_SKIP_CPUFREQ"] = "1" - - # Enable advanced features if requested - if config.get("cmplog", True): - env["AFL_USE_CMPLOG"] = "1" - - if config.get("redqueen", True): - env["AFL_USE_REDQUEEN"] = "1" - - return env - - def _parse_aflrs_results(self, output_dir: Path, workspace: Path) -> List[ModuleFinding]: - """Parse AFL-RS results from output directory""" - findings = [] - - try: - # Look for crashes directory - crashes_dir = output_dir / "crashes" - if not crashes_dir.exists(): - logger.info("No crashes directory found in AFL-RS output") - return findings - - # Process crash files - crash_files = [f for f in crashes_dir.iterdir() if f.is_file() and not f.name.startswith(".")] - - for crash_file in crash_files: - finding = self._create_aflrs_crash_finding(crash_file, workspace) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing AFL-RS results: {e}") - - return findings - - def _create_aflrs_crash_finding(self, crash_file: Path, workspace: Path) -> ModuleFinding: - """Create finding from AFL-RS crash file""" - try: - # Parse crash filename - filename = crash_file.name - crash_info = self._parse_aflrs_filename(filename) - - # Try to read crash file (limited size) - crash_content = "" - crash_size = 0 - try: - crash_data = crash_file.read_bytes() - crash_size = len(crash_data) - # Store first 500 bytes as hex - crash_content = crash_data[:500].hex() - except Exception: - pass - - # Determine severity based on signal or crash type - signal = crash_info.get("signal", "") - severity = self._get_crash_severity(signal) - - # Create relative path - try: - rel_path = crash_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(crash_file) - - finding = self.create_finding( - title=f"AFL-RS Crash: {signal or 'Unknown Signal'}", - description=f"AFL-RS discovered a crash in the target program{' with signal ' + signal if signal else ''}", - severity=severity, - category=self._get_crash_category(signal), - file_path=file_path, - recommendation=self._get_crash_recommendation(signal), - metadata={ - "crash_id": crash_info.get("id", ""), - "signal": signal, - "execution_time": crash_info.get("time", ""), - "crash_file": crash_file.name, - "crash_size": crash_size, - "crash_content_hex": crash_content, - "fuzzer": "aflrs" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating AFL-RS crash finding: {e}") - return None - - def _parse_aflrs_filename(self, filename: str) -> Dict[str, str]: - """Parse AFL-RS crash filename for information""" - info = {} - - try: - # AFL-RS may use similar format to AFL++ - # Example: id_000000_sig_11_src_000000_time_12345_op_havoc_rep_128 - parts = filename.replace("id:", "id_").replace("sig:", "sig_").replace("src:", "src_").replace("time:", "time_").replace("op:", "op_").replace("rep:", "rep_").split("_") - - i = 0 - while i < len(parts) - 1: - if parts[i] in ["id", "sig", "src", "time", "op", "rep"]: - info[parts[i]] = parts[i + 1] - i += 2 - else: - i += 1 - - except Exception: - # Fallback: try to extract signal from filename - signal_match = re.search(r'sig[_:]?(\d+)', filename) - if signal_match: - info["signal"] = signal_match.group(1) - - return info - - def _get_crash_severity(self, signal: str) -> str: - """Determine crash severity based on signal""" - if not signal: - return "medium" - - try: - sig_num = int(signal) - except ValueError: - return "medium" - - # Map common signals to severity - if sig_num == 11: # SIGSEGV - return "critical" - elif sig_num == 6: # SIGABRT - return "high" - elif sig_num == 4: # SIGILL - return "high" - elif sig_num == 8: # SIGFPE - return "medium" - elif sig_num == 9: # SIGKILL - return "medium" - else: - return "medium" - - def _get_crash_category(self, signal: str) -> str: - """Determine crash category based on signal""" - if not signal: - return "program_crash" - - try: - sig_num = int(signal) - except ValueError: - return "program_crash" - - if sig_num == 11: # SIGSEGV - return "memory_corruption" - elif sig_num == 6: # SIGABRT - return "assertion_failure" - elif sig_num == 4: # SIGILL - return "illegal_instruction" - elif sig_num == 8: # SIGFPE - return "arithmetic_error" - else: - return "program_crash" - - def _get_crash_recommendation(self, signal: str) -> str: - """Generate recommendation based on crash signal""" - if not signal: - return "Analyze the crash input to reproduce and debug the issue." - - try: - sig_num = int(signal) - except ValueError: - return "Analyze the crash input to reproduce and debug the issue." - - if sig_num == 11: # SIGSEGV - return "Segmentation fault detected. Check for buffer overflows, null pointer dereferences, use-after-free, or invalid memory access patterns." - elif sig_num == 6: # SIGABRT - return "Program abort detected. Check for assertion failures, memory corruption detected by allocator, or explicit abort calls." - elif sig_num == 4: # SIGILL - return "Illegal instruction detected. Check for code corruption, invalid function pointers, or architecture-specific issues." - elif sig_num == 8: # SIGFPE - return "Floating point exception detected. Check for division by zero, arithmetic overflow, or invalid floating point operations." - else: - return f"Program terminated with signal {signal}. Analyze the crash input and use debugging tools to identify the root cause." - - def _create_summary(self, findings: List[ModuleFinding], output_dir: Path) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - signal_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by signal - signal = finding.metadata.get("signal", "unknown") - signal_counts[signal] = signal_counts.get(signal, 0) + 1 - - # Try to read AFL-RS statistics - stats = self._read_aflrs_stats(output_dir) - - return { - "total_findings": len(findings), - "severity_counts": severity_counts, - "category_counts": category_counts, - "signal_counts": signal_counts, - "unique_crashes": len(set(f.metadata.get("crash_id", "") for f in findings)), - "aflrs_stats": stats - } - - def _read_aflrs_stats(self, output_dir: Path) -> Dict[str, Any]: - """Read AFL-RS fuzzer statistics""" - stats = {} - - try: - # Look for AFL-RS stats file - stats_file = output_dir / "fuzzer_stats" - if stats_file.exists(): - with open(stats_file, 'r') as f: - for line in f: - if ':' in line: - key, value = line.strip().split(':', 1) - stats[key.strip()] = value.strip() - - # Also look for AFL-RS specific files - plot_data = output_dir / "plot_data" - if plot_data.exists(): - stats["plot_data_available"] = True - - except Exception as e: - logger.warning(f"Error reading AFL-RS stats: {e}") - - return stats \ No newline at end of file diff --git a/backend/toolbox/modules/fuzzing/atheris.py b/backend/toolbox/modules/fuzzing/atheris.py deleted file mode 100644 index 6c44cdd..0000000 --- a/backend/toolbox/modules/fuzzing/atheris.py +++ /dev/null @@ -1,774 +0,0 @@ -""" -Atheris Fuzzing Module - -This module uses Atheris for fuzzing Python code to find bugs and security -vulnerabilities in Python applications and libraries. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -import os -import sys -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging -import traceback - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class AtherisModule(BaseModule): - """Atheris Python fuzzing module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="atheris", - version="2.3.0", - description="Coverage-guided Python fuzzing engine for finding bugs in Python code", - author="FuzzForge Team", - category="fuzzing", - tags=["python", "coverage-guided", "native", "sanitizers", "libfuzzer"], - input_schema={ - "type": "object", - "properties": { - "target_script": { - "type": "string", - "description": "Path to the Python script containing the fuzz target function" - }, - "target_function": { - "type": "string", - "default": "TestOneInput", - "description": "Name of the target function to fuzz" - }, - "corpus_dir": { - "type": "string", - "description": "Directory containing initial corpus files" - }, - "dict_file": { - "type": "string", - "description": "Dictionary file for fuzzing keywords" - }, - "max_total_time": { - "type": "integer", - "default": 600, - "description": "Maximum total time to run fuzzing (seconds)" - }, - "max_len": { - "type": "integer", - "default": 4096, - "description": "Maximum length of test input" - }, - "timeout": { - "type": "integer", - "default": 25, - "description": "Timeout for individual test cases (seconds)" - }, - "runs": { - "type": "integer", - "default": -1, - "description": "Number of individual test runs (-1 for unlimited)" - }, - "jobs": { - "type": "integer", - "default": 1, - "description": "Number of fuzzing jobs to run in parallel" - }, - "print_final_stats": { - "type": "boolean", - "default": true, - "description": "Print final statistics" - }, - "print_pcs": { - "type": "boolean", - "default": false, - "description": "Print newly covered PCs" - }, - "print_coverage": { - "type": "boolean", - "default": true, - "description": "Print coverage information" - }, - "artifact_prefix": { - "type": "string", - "default": "crash-", - "description": "Prefix for artifact files" - }, - "seed": { - "type": "integer", - "description": "Random seed for reproducibility" - }, - "python_path": { - "type": "array", - "items": {"type": "string"}, - "description": "Additional Python paths to add to sys.path" - }, - "enable_sanitizers": { - "type": "boolean", - "default": true, - "description": "Enable Python-specific sanitizers and checks" - }, - "detect_leaks": { - "type": "boolean", - "default": true, - "description": "Detect memory leaks in native extensions" - }, - "detect_stack_use_after_return": { - "type": "boolean", - "default": false, - "description": "Detect stack use-after-return" - }, - "setup_code": { - "type": "string", - "description": "Python code to execute before fuzzing starts" - }, - "enable_value_profile": { - "type": "boolean", - "default": false, - "description": "Enable value profiling for better mutation" - }, - "shrink": { - "type": "boolean", - "default": true, - "description": "Try to shrink the corpus" - }, - "only_ascii": { - "type": "boolean", - "default": false, - "description": "Only generate ASCII inputs" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "exception_type": {"type": "string"}, - "exception_message": {"type": "string"}, - "stack_trace": {"type": "string"}, - "crash_input": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - target_script = config.get("target_script") - if not target_script: - raise ValueError("target_script is required for Atheris") - - max_total_time = config.get("max_total_time", 600) - if max_total_time <= 0: - raise ValueError("max_total_time must be positive") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Atheris Python fuzzing""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running Atheris Python fuzzing") - - # Check Atheris installation - await self._check_atheris_installation() - - # Validate target script - target_script = workspace / config["target_script"] - if not target_script.exists(): - raise FileNotFoundError(f"Target script not found: {target_script}") - - # Run Atheris fuzzing - findings = await self._run_atheris_fuzzing(target_script, config, workspace) - - # Create summary - summary = self._create_summary(findings) - - logger.info(f"Atheris found {len(findings)} issues") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Atheris module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _check_atheris_installation(self): - """Check if Atheris is installed""" - try: - process = await asyncio.create_subprocess_exec( - sys.executable, "-c", "import atheris; print(atheris.__version__)", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - raise RuntimeError("Atheris not installed. Install with: pip install atheris") - - version = stdout.decode().strip() - logger.info(f"Using Atheris version: {version}") - - except Exception as e: - raise RuntimeError(f"Atheris installation check failed: {e}") - - async def _run_atheris_fuzzing(self, target_script: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run Atheris fuzzing""" - findings = [] - - try: - # Create output directory for artifacts - output_dir = workspace / "atheris_output" - output_dir.mkdir(exist_ok=True) - - # Create wrapper script for fuzzing - wrapper_script = await self._create_atheris_wrapper(target_script, config, workspace, output_dir) - - # Build Atheris command - cmd = [sys.executable, str(wrapper_script)] - - # Add corpus directory - corpus_dir = config.get("corpus_dir") - if corpus_dir: - corpus_path = workspace / corpus_dir - if corpus_path.exists(): - cmd.append(str(corpus_path)) - - # Set up environment - env = self._setup_atheris_environment(config) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run Atheris with timeout - max_total_time = config.get("max_total_time", 600) - - try: - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace, - env=env - ) - - # Wait for specified time then terminate - try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=max_total_time - ) - except asyncio.TimeoutError: - logger.info(f"Atheris fuzzing timed out after {max_total_time} seconds") - process.terminate() - try: - await asyncio.wait_for(process.wait(), timeout=10) - except asyncio.TimeoutError: - process.kill() - await process.wait() - - # Parse results - findings = self._parse_atheris_output( - stdout.decode(), stderr.decode(), output_dir, workspace - ) - - # Look for crash files - crash_findings = self._parse_crash_files(output_dir, workspace) - findings.extend(crash_findings) - - except Exception as e: - logger.warning(f"Error running Atheris process: {e}") - - except Exception as e: - logger.warning(f"Error in Atheris fuzzing: {e}") - - return findings - - async def _create_atheris_wrapper(self, target_script: Path, config: Dict[str, Any], workspace: Path, output_dir: Path) -> Path: - """Create wrapper script for Atheris fuzzing""" - wrapper_path = workspace / "atheris_wrapper.py" - - wrapper_code = f'''#!/usr/bin/env python3 -import sys -import os -import atheris -import traceback - -# Add Python paths -python_paths = {config.get("python_path", [])} -for path in python_paths: - if path not in sys.path: - sys.path.insert(0, path) - -# Add workspace to Python path -sys.path.insert(0, r"{workspace}") - -# Setup code -setup_code = """{config.get("setup_code", "")}""" -if setup_code: - exec(setup_code) - -# Import target script -target_module_name = "{target_script.stem}" -sys.path.insert(0, r"{target_script.parent}") - -try: - target_module = __import__(target_module_name) - target_function = getattr(target_module, "{config.get("target_function", "TestOneInput")}") -except Exception as e: - print(f"Failed to import target: {{e}}") - sys.exit(1) - -# Wrapper function to catch exceptions -original_target = target_function - -def wrapped_target(data): - try: - return original_target(data) - except Exception as e: - # Write crash information - crash_info = {{ - "exception_type": type(e).__name__, - "exception_message": str(e), - "stack_trace": traceback.format_exc(), - "input_data": data[:1000].hex() if isinstance(data, bytes) else str(data)[:1000] - }} - - crash_file = r"{output_dir}" + "/crash_" + type(e).__name__ + ".txt" - with open(crash_file, "a") as f: - f.write(f"Exception: {{type(e).__name__}}\\n") - f.write(f"Message: {{str(e)}}\\n") - f.write(f"Stack trace:\\n{{traceback.format_exc()}}\\n") - f.write(f"Input data (first 1000 chars/bytes): {{crash_info['input_data']}}\\n") - f.write("-" * 80 + "\\n") - - # Re-raise to let Atheris handle it - raise - -if __name__ == "__main__": - # Configure Atheris - atheris.Setup(sys.argv, wrapped_target) - - # Set Atheris options - options = [] - - options.append(f"-max_total_time={{config.get('max_total_time', 600)}}") - options.append(f"-max_len={{config.get('max_len', 4096)}}") - options.append(f"-timeout={{config.get('timeout', 25)}}") - options.append(f"-runs={{config.get('runs', -1)}}") - - if {config.get('jobs', 1)} > 1: - options.append(f"-jobs={{config.get('jobs', 1)}}") - - if {config.get('print_final_stats', True)}: - options.append("-print_final_stats=1") - else: - options.append("-print_final_stats=0") - - if {config.get('print_pcs', False)}: - options.append("-print_pcs=1") - - if {config.get('print_coverage', True)}: - options.append("-print_coverage=1") - - artifact_prefix = "{config.get('artifact_prefix', 'crash-')}" - options.append(f"-artifact_prefix={{r'{output_dir}'}}/" + artifact_prefix) - - seed = {config.get('seed')} - if seed is not None: - options.append(f"-seed={{seed}}") - - if {config.get('enable_value_profile', False)}: - options.append("-use_value_profile=1") - - if {config.get('shrink', True)}: - options.append("-shrink=1") - - if {config.get('only_ascii', False)}: - options.append("-only_ascii=1") - - dict_file = "{config.get('dict_file', '')}" - if dict_file: - dict_path = r"{workspace}" + "/" + dict_file - if os.path.exists(dict_path): - options.append(f"-dict={{dict_path}}") - - # Add options to sys.argv - sys.argv.extend(options) - - # Start fuzzing - atheris.Fuzz() -''' - - with open(wrapper_path, 'w') as f: - f.write(wrapper_code) - - return wrapper_path - - def _setup_atheris_environment(self, config: Dict[str, Any]) -> Dict[str, str]: - """Setup environment variables for Atheris""" - env = os.environ.copy() - - # Enable sanitizers if requested - if config.get("enable_sanitizers", True): - env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_leaks=1:halt_on_error=1" - - if config.get("detect_leaks", True): - env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_leaks=1" - - if config.get("detect_stack_use_after_return", False): - env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":detect_stack_use_after_return=1" - - return env - - def _parse_atheris_output(self, stdout: str, stderr: str, output_dir: Path, workspace: Path) -> List[ModuleFinding]: - """Parse Atheris output for crashes and issues""" - findings = [] - - try: - # Combine stdout and stderr - full_output = stdout + "\n" + stderr - - # Look for Python exceptions in output - exception_patterns = [ - r"Traceback \(most recent call last\):(.*?)(?=\n\w|\nDONE|\n=|\Z)", - r"Exception: (\w+).*?\nMessage: (.*?)\nStack trace:\n(.*?)(?=\n-{20,}|\Z)" - ] - - for pattern in exception_patterns: - import re - matches = re.findall(pattern, full_output, re.DOTALL | re.MULTILINE) - for match in matches: - finding = self._create_exception_finding(match, full_output, output_dir) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing Atheris output: {e}") - - return findings - - def _parse_crash_files(self, output_dir: Path, workspace: Path) -> List[ModuleFinding]: - """Parse crash files created by wrapper""" - findings = [] - - try: - # Look for crash files - crash_files = list(output_dir.glob("crash_*.txt")) - - for crash_file in crash_files: - findings.extend(self._parse_crash_file(crash_file, workspace)) - - # Also look for Atheris artifact files - artifact_files = list(output_dir.glob("crash-*")) - for artifact_file in artifact_files: - finding = self._create_artifact_finding(artifact_file, workspace) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing crash files: {e}") - - return findings - - def _parse_crash_file(self, crash_file: Path, workspace: Path) -> List[ModuleFinding]: - """Parse individual crash file""" - findings = [] - - try: - content = crash_file.read_text() - - # Split by separator - crash_entries = content.split("-" * 80) - - for entry in crash_entries: - if not entry.strip(): - continue - - finding = self._parse_crash_entry(entry, crash_file, workspace) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing crash file {crash_file}: {e}") - - return findings - - def _parse_crash_entry(self, entry: str, crash_file: Path, workspace: Path) -> ModuleFinding: - """Parse individual crash entry""" - try: - lines = entry.strip().split('\n') - - exception_type = "" - exception_message = "" - stack_trace = "" - input_data = "" - - current_section = None - stack_lines = [] - - for line in lines: - if line.startswith("Exception: "): - exception_type = line.replace("Exception: ", "") - elif line.startswith("Message: "): - exception_message = line.replace("Message: ", "") - elif line.startswith("Stack trace:"): - current_section = "stack" - elif line.startswith("Input data"): - current_section = "input" - input_data = line.split(":", 1)[1].strip() if ":" in line else "" - elif current_section == "stack": - stack_lines.append(line) - - stack_trace = '\n'.join(stack_lines) - - if not exception_type: - return None - - # Determine severity based on exception type - severity = self._get_exception_severity(exception_type) - - # Create relative path - try: - rel_path = crash_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(crash_file) - - finding = self.create_finding( - title=f"Atheris Exception: {exception_type}", - description=f"Atheris discovered a Python exception: {exception_type}{': ' + exception_message if exception_message else ''}", - severity=severity, - category=self._get_exception_category(exception_type), - file_path=file_path, - recommendation=self._get_exception_recommendation(exception_type, exception_message), - metadata={ - "exception_type": exception_type, - "exception_message": exception_message, - "stack_trace": stack_trace[:2000] if stack_trace else "", # Limit size - "crash_input_preview": input_data[:500] if input_data else "", - "fuzzer": "atheris" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error parsing crash entry: {e}") - return None - - def _create_exception_finding(self, match, full_output: str, output_dir: Path) -> ModuleFinding: - """Create finding from exception match""" - try: - if isinstance(match, tuple) and len(match) >= 1: - # Handle different match formats - if len(match) == 3: # Exception format - exception_type, exception_message, stack_trace = match - else: - stack_trace = match[0] - exception_type = "Unknown" - exception_message = "" - else: - stack_trace = str(match) - exception_type = "Unknown" - exception_message = "" - - # Try to extract exception type from stack trace - if not exception_type or exception_type == "Unknown": - lines = stack_trace.split('\n') - for line in reversed(lines): - if ':' in line and any(exc in line for exc in ['Error', 'Exception', 'Warning']): - exception_type = line.split(':')[0].strip() - exception_message = line.split(':', 1)[1].strip() if ':' in line else "" - break - - severity = self._get_exception_severity(exception_type) - - finding = self.create_finding( - title=f"Atheris Exception: {exception_type}", - description=f"Atheris discovered a Python exception during fuzzing: {exception_type}", - severity=severity, - category=self._get_exception_category(exception_type), - file_path=None, - recommendation=self._get_exception_recommendation(exception_type, exception_message), - metadata={ - "exception_type": exception_type, - "exception_message": exception_message, - "stack_trace": stack_trace[:2000] if stack_trace else "", - "fuzzer": "atheris" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating exception finding: {e}") - return None - - def _create_artifact_finding(self, artifact_file: Path, workspace: Path) -> ModuleFinding: - """Create finding from Atheris artifact file""" - try: - # Try to read artifact content (limited) - artifact_content = "" - try: - content_bytes = artifact_file.read_bytes()[:1000] - artifact_content = content_bytes.hex() - except Exception: - pass - - # Create relative path - try: - rel_path = artifact_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(artifact_file) - - finding = self.create_finding( - title="Atheris Crash Artifact", - description=f"Atheris generated a crash artifact file: {artifact_file.name}", - severity="medium", - category="program_crash", - file_path=file_path, - recommendation="Analyze the crash artifact to reproduce and debug the issue. The artifact contains the input that caused the crash.", - metadata={ - "artifact_type": "crash", - "artifact_file": artifact_file.name, - "artifact_content_hex": artifact_content, - "fuzzer": "atheris" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating artifact finding: {e}") - return None - - def _get_exception_severity(self, exception_type: str) -> str: - """Determine severity based on exception type""" - if not exception_type: - return "medium" - - exception_lower = exception_type.lower() - - # Critical security issues - if any(term in exception_lower for term in ["segmentationfault", "accessviolation", "memoryerror"]): - return "critical" - - # High severity exceptions - elif any(term in exception_lower for term in ["attributeerror", "typeerror", "indexerror", "keyerror", "valueerror"]): - return "high" - - # Medium severity exceptions - elif any(term in exception_lower for term in ["assertionerror", "runtimeerror", "ioerror", "oserror"]): - return "medium" - - # Lower severity exceptions - elif any(term in exception_lower for term in ["warning", "deprecation"]): - return "low" - - else: - return "medium" - - def _get_exception_category(self, exception_type: str) -> str: - """Determine category based on exception type""" - if not exception_type: - return "python_exception" - - exception_lower = exception_type.lower() - - if any(term in exception_lower for term in ["memory", "segmentation", "access"]): - return "memory_corruption" - elif any(term in exception_lower for term in ["attribute", "type"]): - return "type_error" - elif any(term in exception_lower for term in ["index", "key", "value"]): - return "data_error" - elif any(term in exception_lower for term in ["io", "os", "file"]): - return "io_error" - elif any(term in exception_lower for term in ["assertion"]): - return "assertion_failure" - else: - return "python_exception" - - def _get_exception_recommendation(self, exception_type: str, exception_message: str) -> str: - """Generate recommendation based on exception type""" - if not exception_type: - return "Analyze the exception and fix the underlying code issue." - - exception_lower = exception_type.lower() - - if "attributeerror" in exception_lower: - return "Fix AttributeError by ensuring objects have the expected attributes before accessing them. Add proper error handling and validation." - elif "typeerror" in exception_lower: - return "Fix TypeError by ensuring correct data types are used. Add type checking and validation for function parameters." - elif "indexerror" in exception_lower: - return "Fix IndexError by adding bounds checking before accessing list/array elements. Validate indices are within valid range." - elif "keyerror" in exception_lower: - return "Fix KeyError by checking if keys exist in dictionaries before accessing them. Use .get() method or proper key validation." - elif "valueerror" in exception_lower: - return "Fix ValueError by validating input values before processing. Add proper input sanitization and validation." - elif "memoryerror" in exception_lower: - return "Fix MemoryError by optimizing memory usage, processing data in chunks, or increasing available memory." - elif "assertionerror" in exception_lower: - return "Fix AssertionError by reviewing assertion conditions and ensuring they properly validate the expected state." - else: - return f"Fix the {exception_type} exception by analyzing the root cause and implementing appropriate error handling and validation." - - def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - exception_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by exception type - exception_type = finding.metadata.get("exception_type", "unknown") - exception_counts[exception_type] = exception_counts.get(exception_type, 0) + 1 - - return { - "total_findings": len(findings), - "severity_counts": severity_counts, - "category_counts": category_counts, - "exception_counts": exception_counts, - "unique_exceptions": len(exception_counts), - "python_specific_issues": sum(category_counts.get(cat, 0) for cat in ["type_error", "data_error", "python_exception"]) - } \ No newline at end of file diff --git a/backend/toolbox/modules/fuzzing/cargo_fuzz.py b/backend/toolbox/modules/fuzzing/cargo_fuzz.py deleted file mode 100644 index b6d3d3e..0000000 --- a/backend/toolbox/modules/fuzzing/cargo_fuzz.py +++ /dev/null @@ -1,572 +0,0 @@ -""" -Cargo Fuzz Module - -This module uses cargo-fuzz for fuzzing Rust code with libFuzzer integration. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -import os -from pathlib import Path -from typing import Dict, Any, List, Tuple -import subprocess -import logging -import httpx -import re -from datetime import datetime, timedelta - -try: - from prefect import get_run_context -except ImportError: - # Fallback for when not running in Prefect context - get_run_context = None - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class CargoFuzzModule(BaseModule): - """Cargo Fuzz Rust fuzzing module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="cargo_fuzz", - version="0.11.2", - description="Rust fuzzing integration with libFuzzer using cargo-fuzz", - author="FuzzForge Team", - category="fuzzing", - tags=["rust", "libfuzzer", "cargo", "coverage-guided", "sanitizers"], - input_schema={ - "type": "object", - "properties": { - "project_dir": { - "type": "string", - "description": "Path to Rust project directory (with Cargo.toml)" - }, - "fuzz_target": { - "type": "string", - "description": "Name of the fuzz target to run" - }, - "max_total_time": { - "type": "integer", - "default": 600, - "description": "Maximum total time to run fuzzing (seconds)" - }, - "jobs": { - "type": "integer", - "default": 1, - "description": "Number of worker processes" - }, - "corpus_dir": { - "type": "string", - "description": "Custom corpus directory" - }, - "artifacts_dir": { - "type": "string", - "description": "Custom artifacts directory" - }, - "sanitizer": { - "type": "string", - "enum": ["address", "memory", "thread", "leak", "none"], - "default": "address", - "description": "Sanitizer to use" - }, - "release": { - "type": "boolean", - "default": False, - "description": "Use release mode" - }, - "debug_assertions": { - "type": "boolean", - "default": True, - "description": "Enable debug assertions" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "crash_type": {"type": "string"}, - "artifact_path": {"type": "string"}, - "stack_trace": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - project_dir = config.get("project_dir") - if not project_dir: - raise ValueError("project_dir is required") - - fuzz_target = config.get("fuzz_target") - if not fuzz_target: - raise ValueError("fuzz_target is required") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path, stats_callback=None) -> ModuleResult: - """Execute cargo-fuzz fuzzing""" - self.start_timer() - - try: - # Initialize last observed stats for summary propagation - self._last_stats = { - 'executions': 0, - 'executions_per_sec': 0.0, - 'crashes': 0, - 'corpus_size': 0, - 'elapsed_time': 0, - } - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running cargo-fuzz Rust fuzzing") - - # Check installation - await self._check_cargo_fuzz_installation() - - # Setup project - project_dir = workspace / config["project_dir"] - await self._setup_cargo_fuzz_project(project_dir, config) - - # Run fuzzing - findings = await self._run_cargo_fuzz(project_dir, config, workspace, stats_callback) - - # Create summary and enrich with last observed runtime stats - summary = self._create_summary(findings) - try: - summary.update({ - 'executions': self._last_stats.get('executions', 0), - 'executions_per_sec': self._last_stats.get('executions_per_sec', 0.0), - 'corpus_size': self._last_stats.get('corpus_size', 0), - 'crashes': self._last_stats.get('crashes', 0), - 'elapsed_time': self._last_stats.get('elapsed_time', 0), - }) - except Exception: - pass - - logger.info(f"cargo-fuzz found {len(findings)} issues") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"cargo-fuzz module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _check_cargo_fuzz_installation(self): - """Check if cargo-fuzz is installed""" - try: - process = await asyncio.create_subprocess_exec( - "cargo", "fuzz", "--version", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - raise RuntimeError("cargo-fuzz not installed. Install with: cargo install cargo-fuzz") - - except Exception as e: - raise RuntimeError(f"cargo-fuzz installation check failed: {e}") - - async def _setup_cargo_fuzz_project(self, project_dir: Path, config: Dict[str, Any]): - """Setup cargo-fuzz project""" - if not project_dir.exists(): - raise FileNotFoundError(f"Project directory not found: {project_dir}") - - cargo_toml = project_dir / "Cargo.toml" - if not cargo_toml.exists(): - raise FileNotFoundError(f"Cargo.toml not found in {project_dir}") - - # Check if fuzz directory exists, if not initialize - fuzz_dir = project_dir / "fuzz" - if not fuzz_dir.exists(): - logger.info("Initializing cargo-fuzz project") - process = await asyncio.create_subprocess_exec( - "cargo", "fuzz", "init", - cwd=project_dir, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - await process.communicate() - - async def _run_cargo_fuzz(self, project_dir: Path, config: Dict[str, Any], workspace: Path, stats_callback=None) -> List[ModuleFinding]: - """Run cargo-fuzz with real-time statistics reporting""" - findings = [] - - # Get run_id from Prefect context for statistics reporting - run_id = None - if get_run_context: - try: - context = get_run_context() - run_id = str(context.flow_run.id) - except Exception: - logger.warning("Could not get run_id from Prefect context") - - try: - # Build command - cmd = ["cargo", "fuzz", "run", config["fuzz_target"]] - - # Add options - if config.get("jobs", 1) > 1: - cmd.extend(["--", f"-jobs={config['jobs']}"]) - - max_time = config.get("max_total_time", 600) - cmd.extend(["--", f"-max_total_time={max_time}"]) - - # Set sanitizer - sanitizer = config.get("sanitizer", "address") - if sanitizer != "none": - cmd.append(f"--sanitizer={sanitizer}") - - if config.get("release", False): - cmd.append("--release") - - # Set environment - env = os.environ.copy() - if config.get("debug_assertions", True): - env["RUSTFLAGS"] = env.get("RUSTFLAGS", "") + " -C debug-assertions=on" - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run with streaming output processing for real-time stats - try: - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, # Merge stderr into stdout - cwd=project_dir, - env=env - ) - - # Process output in real-time - stdout_data, stderr_data = await self._process_streaming_output( - process, max_time, config, stats_callback - ) - - # Parse final results - findings = self._parse_cargo_fuzz_output( - stdout_data, stderr_data, project_dir, workspace, config - ) - - except Exception as e: - logger.warning(f"Error running cargo-fuzz: {e}") - - except Exception as e: - logger.warning(f"Error in cargo-fuzz execution: {e}") - - return findings - - def _parse_cargo_fuzz_output(self, stdout: str, stderr: str, project_dir: Path, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]: - """Parse cargo-fuzz output""" - findings = [] - - try: - full_output = stdout + "\n" + stderr - - # Look for crash artifacts - artifacts_dir = project_dir / "fuzz" / "artifacts" / config["fuzz_target"] - if artifacts_dir.exists(): - for artifact in artifacts_dir.iterdir(): - if artifact.is_file(): - finding = self._create_artifact_finding(artifact, workspace, full_output) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing cargo-fuzz output: {e}") - - return findings - - def _create_artifact_finding(self, artifact_path: Path, workspace: Path, output: str) -> ModuleFinding: - """Create finding from artifact file""" - try: - # Try to determine crash type from filename or content - crash_type = "crash" - if "leak" in artifact_path.name.lower(): - crash_type = "memory_leak" - elif "timeout" in artifact_path.name.lower(): - crash_type = "timeout" - - # Extract stack trace from output - stack_trace = self._extract_stack_trace_from_output(output, artifact_path.name) - - try: - rel_path = artifact_path.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(artifact_path) - - severity = "high" if "crash" in crash_type else "medium" - - finding = self.create_finding( - title=f"cargo-fuzz {crash_type.title()}", - description=f"cargo-fuzz discovered a {crash_type} in the Rust code", - severity=severity, - category=self._get_crash_category(crash_type), - file_path=file_path, - recommendation=self._get_crash_recommendation(crash_type), - metadata={ - "crash_type": crash_type, - "artifact_path": str(artifact_path), - "stack_trace": stack_trace, - "fuzzer": "cargo_fuzz" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating artifact finding: {e}") - return None - - def _extract_stack_trace_from_output(self, output: str, artifact_name: str) -> str: - """Extract stack trace from output""" - try: - lines = output.split('\n') - stack_lines = [] - in_stack = False - - for line in lines: - if artifact_name in line or "stack backtrace:" in line.lower(): - in_stack = True - continue - - if in_stack: - if line.strip() and ("at " in line or "::" in line or line.strip().startswith("0:")): - stack_lines.append(line.strip()) - elif not line.strip() and stack_lines: - break - - return '\n'.join(stack_lines[:20]) # Limit stack trace size - - except Exception: - return "" - - def _get_crash_category(self, crash_type: str) -> str: - """Get category for crash type""" - if "leak" in crash_type: - return "memory_leak" - elif "timeout" in crash_type: - return "performance_issues" - else: - return "memory_safety" - - def _get_crash_recommendation(self, crash_type: str) -> str: - """Get recommendation for crash type""" - if "leak" in crash_type: - return "Fix memory leak by ensuring proper cleanup of allocated resources. Review memory management patterns." - elif "timeout" in crash_type: - return "Fix timeout by optimizing performance, avoiding infinite loops, and implementing reasonable bounds." - else: - return "Fix the crash by analyzing the stack trace and addressing memory safety issues." - - async def _process_streaming_output(self, process, max_time: int, config: Dict[str, Any], stats_callback=None) -> Tuple[str, str]: - """Process cargo-fuzz output in real-time and report statistics""" - stdout_lines = [] - start_time = datetime.utcnow() - last_update = start_time - stats_data = { - 'executions': 0, - 'executions_per_sec': 0.0, - 'crashes': 0, - 'corpus_size': 0, - 'elapsed_time': 0 - } - - # Get run_id from Prefect context for statistics reporting - run_id = None - if get_run_context: - try: - context = get_run_context() - run_id = str(context.flow_run.id) - except Exception: - logger.debug("Could not get run_id from Prefect context") - - try: - # Emit an initial baseline update so dashboards show activity immediately - try: - await self._send_stats_via_callback(stats_callback, run_id, stats_data) - except Exception: - pass - # Monitor process output in chunks to capture libFuzzer carriage-return updates - buffer = "" - while True: - try: - chunk = await asyncio.wait_for(process.stdout.read(4096), timeout=1.0) - if not chunk: - # Process finished - break - - buffer += chunk.decode('utf-8', errors='ignore') - - # Split on both newline and carriage return - if "\n" in buffer or "\r" in buffer: - parts = re.split(r"[\r\n]", buffer) - buffer = parts[-1] - for part in parts[:-1]: - line = part.strip() - if not line: - continue - stdout_lines.append(line) - self._parse_stats_from_line(line, stats_data) - - except asyncio.TimeoutError: - # No output this second; continue to periodic update check - pass - - # Periodic update (even if there was no output) - current_time = datetime.utcnow() - stats_data['elapsed_time'] = int((current_time - start_time).total_seconds()) - if current_time - last_update >= timedelta(seconds=3): - try: - self._last_stats = dict(stats_data) - except Exception: - pass - await self._send_stats_via_callback(stats_callback, run_id, stats_data) - last_update = current_time - - # Check if max time exceeded - if stats_data['elapsed_time'] >= max_time: - logger.info("Max time reached, terminating cargo-fuzz") - process.terminate() - break - - # Wait for process to complete - await process.wait() - - # Send final stats update - try: - self._last_stats = dict(stats_data) - except Exception: - pass - await self._send_stats_via_callback(stats_callback, run_id, stats_data) - - except Exception as e: - logger.warning(f"Error processing streaming output: {e}") - - stdout_data = '\n'.join(stdout_lines) - return stdout_data, "" - - def _parse_stats_from_line(self, line: str, stats_data: Dict[str, Any]): - """Parse statistics from a cargo-fuzz output line""" - try: - # cargo-fuzz typically shows stats like: - # "#12345: DONE cov: 1234 ft: 5678 corp: 9/10Mb exec/s: 1500 rss: 234Mb" - # "#12345: NEW cov: 1234 ft: 5678 corp: 9/10Mb exec/s: 1500 rss: 234Mb L: 45/67 MS: 3 ..." - - # Extract execution count (the #number) - exec_match = re.search(r'#(\d+)(?::)?', line) - if exec_match: - stats_data['executions'] = int(exec_match.group(1)) - else: - # libFuzzer stats format alternative - exec_alt = re.search(r'stat::number_of_executed_units:\s*(\d+)', line) - if exec_alt: - stats_data['executions'] = int(exec_alt.group(1)) - else: - exec_alt2 = re.search(r'executed units:?\s*(\d+)', line, re.IGNORECASE) - if exec_alt2: - stats_data['executions'] = int(exec_alt2.group(1)) - - # Extract executions per second - exec_per_sec_match = re.search(r'exec/s:\s*([0-9\.]+)', line) - if exec_per_sec_match: - stats_data['executions_per_sec'] = float(exec_per_sec_match.group(1)) - else: - eps_alt = re.search(r'stat::execs_per_sec:\s*([0-9\.]+)', line) - if eps_alt: - stats_data['executions_per_sec'] = float(eps_alt.group(1)) - - # Extract corpus size (corp: X/YMb) - corp_match = re.search(r'corp(?:us)?:\s*(\d+)', line) - if corp_match: - stats_data['corpus_size'] = int(corp_match.group(1)) - - # Look for crash indicators - if any(keyword in line.lower() for keyword in ['crash', 'assert', 'panic', 'abort']): - stats_data['crashes'] += 1 - - except Exception as e: - logger.debug(f"Error parsing stats from line '{line}': {e}") - - async def _send_stats_via_callback(self, stats_callback, run_id: str, stats_data: Dict[str, Any]): - """Send statistics update via callback function""" - if not stats_callback or not run_id: - return - - try: - # Prepare statistics payload - stats_payload = { - "run_id": run_id, - "workflow": "language_fuzzing", - "executions": stats_data['executions'], - "executions_per_sec": stats_data['executions_per_sec'], - "crashes": stats_data['crashes'], - "unique_crashes": stats_data['crashes'], # Assume all crashes are unique for now - "corpus_size": stats_data['corpus_size'], - "elapsed_time": stats_data['elapsed_time'], - "timestamp": datetime.utcnow().isoformat() - } - - # Call the callback function provided by the Prefect task - await stats_callback(stats_payload) - logger.info( - "LIVE STATS SENT: exec=%s eps=%.2f crashes=%s corpus=%s elapsed=%s", - stats_data['executions'], - stats_data['executions_per_sec'], - stats_data['crashes'], - stats_data['corpus_size'], - stats_data['elapsed_time'], - ) - - except Exception as e: - logger.debug(f"Error sending stats via callback: {e}") - - def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - - for finding in findings: - severity_counts[finding.severity] += 1 - category_counts[finding.category] = category_counts.get(finding.category, 0) + 1 - - return { - "total_findings": len(findings), - "severity_counts": severity_counts, - "category_counts": category_counts - } diff --git a/backend/toolbox/modules/fuzzing/go_fuzz.py b/backend/toolbox/modules/fuzzing/go_fuzz.py deleted file mode 100644 index 89ad165..0000000 --- a/backend/toolbox/modules/fuzzing/go_fuzz.py +++ /dev/null @@ -1,384 +0,0 @@ -""" -Go-Fuzz Module - -This module uses go-fuzz for coverage-guided fuzzing of Go packages. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -import os -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class GoFuzzModule(BaseModule): - """Go-Fuzz Go language fuzzing module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="go_fuzz", - version="1.2.0", - description="Coverage-guided fuzzing for Go packages using go-fuzz", - author="FuzzForge Team", - category="fuzzing", - tags=["go", "golang", "coverage-guided", "packages"], - input_schema={ - "type": "object", - "properties": { - "package_path": { - "type": "string", - "description": "Path to Go package to fuzz" - }, - "fuzz_function": { - "type": "string", - "default": "Fuzz", - "description": "Name of the fuzz function" - }, - "workdir": { - "type": "string", - "default": "go_fuzz_workdir", - "description": "Working directory for go-fuzz" - }, - "procs": { - "type": "integer", - "default": 1, - "description": "Number of parallel processes" - }, - "timeout": { - "type": "integer", - "default": 600, - "description": "Total fuzzing timeout (seconds)" - }, - "race": { - "type": "boolean", - "default": false, - "description": "Enable race detector" - }, - "minimize": { - "type": "boolean", - "default": true, - "description": "Minimize crashers" - }, - "sonar": { - "type": "boolean", - "default": false, - "description": "Enable sonar mode" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "crash_type": {"type": "string"}, - "crash_file": {"type": "string"}, - "stack_trace": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - package_path = config.get("package_path") - if not package_path: - raise ValueError("package_path is required") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute go-fuzz fuzzing""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running go-fuzz Go fuzzing") - - # Check installation - await self._check_go_fuzz_installation() - - # Setup - package_path = workspace / config["package_path"] - workdir = workspace / config.get("workdir", "go_fuzz_workdir") - - # Build and run - findings = await self._run_go_fuzz(package_path, workdir, config, workspace) - - # Create summary - summary = self._create_summary(findings) - - logger.info(f"go-fuzz found {len(findings)} issues") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"go-fuzz module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _check_go_fuzz_installation(self): - """Check if go-fuzz is installed""" - try: - process = await asyncio.create_subprocess_exec( - "go-fuzz", "--help", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - await process.communicate() - - if process.returncode != 0: - # Try building - process = await asyncio.create_subprocess_exec( - "go", "install", "github.com/dvyukov/go-fuzz/go-fuzz@latest", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - await process.communicate() - - except Exception as e: - raise RuntimeError(f"go-fuzz installation failed: {e}") - - async def _run_go_fuzz(self, package_path: Path, workdir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run go-fuzz""" - findings = [] - - try: - # Create workdir - workdir.mkdir(exist_ok=True) - - # Build - await self._build_go_fuzz(package_path, config) - - # Run fuzzing - cmd = ["go-fuzz", "-bin", f"{package_path.name}-fuzz.zip", "-workdir", str(workdir)] - - if config.get("procs", 1) > 1: - cmd.extend(["-procs", str(config["procs"])]) - - if config.get("race", False): - cmd.append("-race") - - if config.get("sonar", False): - cmd.append("-sonar") - - timeout = config.get("timeout", 600) - - try: - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=package_path.parent - ) - - try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=timeout - ) - except asyncio.TimeoutError: - process.terminate() - await process.wait() - - # Parse results - findings = self._parse_go_fuzz_results(workdir, workspace, config) - - except Exception as e: - logger.warning(f"Error running go-fuzz: {e}") - - except Exception as e: - logger.warning(f"Error in go-fuzz execution: {e}") - - return findings - - async def _build_go_fuzz(self, package_path: Path, config: Dict[str, Any]): - """Build go-fuzz binary""" - cmd = ["go-fuzz-build"] - if config.get("race", False): - cmd.append("-race") - - process = await asyncio.create_subprocess_exec( - *cmd, - cwd=package_path, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - raise RuntimeError(f"go-fuzz-build failed: {stderr.decode()}") - - def _parse_go_fuzz_results(self, workdir: Path, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]: - """Parse go-fuzz results""" - findings = [] - - try: - # Look for crashers - crashers_dir = workdir / "crashers" - if crashers_dir.exists(): - for crash_file in crashers_dir.iterdir(): - if crash_file.is_file() and not crash_file.name.startswith("."): - finding = self._create_crash_finding(crash_file, workspace) - if finding: - findings.append(finding) - - # Look for suppressions (potential issues) - suppressions_dir = workdir / "suppressions" - if suppressions_dir.exists(): - for supp_file in suppressions_dir.iterdir(): - if supp_file.is_file(): - finding = self._create_suppression_finding(supp_file, workspace) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing go-fuzz results: {e}") - - return findings - - def _create_crash_finding(self, crash_file: Path, workspace: Path) -> ModuleFinding: - """Create finding from crash file""" - try: - # Read crash output - crash_content = "" - if crash_file.name.endswith(".output"): - crash_content = crash_file.read_text() - - # Determine crash type - crash_type = "panic" - if "runtime error" in crash_content: - crash_type = "runtime_error" - elif "race" in crash_content: - crash_type = "race_condition" - - try: - rel_path = crash_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(crash_file) - - finding = self.create_finding( - title=f"go-fuzz {crash_type.title()}", - description=f"go-fuzz discovered a {crash_type} in the Go code", - severity=self._get_crash_severity(crash_type), - category=self._get_crash_category(crash_type), - file_path=file_path, - recommendation=self._get_crash_recommendation(crash_type), - metadata={ - "crash_type": crash_type, - "crash_file": str(crash_file), - "stack_trace": crash_content[:1000], - "fuzzer": "go_fuzz" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating crash finding: {e}") - return None - - def _create_suppression_finding(self, supp_file: Path, workspace: Path) -> ModuleFinding: - """Create finding from suppression file""" - try: - try: - rel_path = supp_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(supp_file) - - finding = self.create_finding( - title="go-fuzz Potential Issue", - description="go-fuzz identified a potential issue that was suppressed", - severity="low", - category="potential_issue", - file_path=file_path, - recommendation="Review suppressed issue to determine if it requires attention.", - metadata={ - "suppression_file": str(supp_file), - "fuzzer": "go_fuzz" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating suppression finding: {e}") - return None - - def _get_crash_severity(self, crash_type: str) -> str: - """Get crash severity""" - if crash_type == "race_condition": - return "high" - elif crash_type == "runtime_error": - return "high" - else: - return "medium" - - def _get_crash_category(self, crash_type: str) -> str: - """Get crash category""" - if crash_type == "race_condition": - return "race_condition" - elif crash_type == "runtime_error": - return "runtime_error" - else: - return "program_crash" - - def _get_crash_recommendation(self, crash_type: str) -> str: - """Get crash recommendation""" - if crash_type == "race_condition": - return "Fix race condition by adding proper synchronization (mutexes, channels, etc.)" - elif crash_type == "runtime_error": - return "Fix runtime error by adding bounds checking and proper error handling" - else: - return "Analyze the crash and fix the underlying issue" - - def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - - for finding in findings: - severity_counts[finding.severity] += 1 - category_counts[finding.category] = category_counts.get(finding.category, 0) + 1 - - return { - "total_findings": len(findings), - "severity_counts": severity_counts, - "category_counts": category_counts - } \ No newline at end of file diff --git a/backend/toolbox/modules/fuzzing/libfuzzer.py b/backend/toolbox/modules/fuzzing/libfuzzer.py deleted file mode 100644 index 0addcbb..0000000 --- a/backend/toolbox/modules/fuzzing/libfuzzer.py +++ /dev/null @@ -1,705 +0,0 @@ -""" -LibFuzzer Fuzzing Module - -This module uses LibFuzzer (LLVM's coverage-guided fuzzing engine) to find -bugs and security vulnerabilities in C/C++ code. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -import os -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging -import re - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class LibFuzzerModule(BaseModule): - """LibFuzzer coverage-guided fuzzing module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="libfuzzer", - version="17.0.0", - description="LLVM's coverage-guided fuzzing engine for finding bugs in C/C++ code", - author="FuzzForge Team", - category="fuzzing", - tags=["coverage-guided", "c", "cpp", "llvm", "sanitizers", "memory-safety"], - input_schema={ - "type": "object", - "properties": { - "target_binary": { - "type": "string", - "description": "Path to the fuzz target binary (compiled with -fsanitize=fuzzer)" - }, - "corpus_dir": { - "type": "string", - "description": "Directory containing initial corpus files" - }, - "dict_file": { - "type": "string", - "description": "Dictionary file for fuzzing keywords" - }, - "max_total_time": { - "type": "integer", - "default": 600, - "description": "Maximum total time to run fuzzing (seconds)" - }, - "max_len": { - "type": "integer", - "default": 4096, - "description": "Maximum length of test input" - }, - "timeout": { - "type": "integer", - "default": 25, - "description": "Timeout for individual test cases (seconds)" - }, - "runs": { - "type": "integer", - "default": -1, - "description": "Number of individual test runs (-1 for unlimited)" - }, - "jobs": { - "type": "integer", - "default": 1, - "description": "Number of fuzzing jobs to run in parallel" - }, - "workers": { - "type": "integer", - "default": 1, - "description": "Number of workers for parallel fuzzing" - }, - "reload": { - "type": "integer", - "default": 1, - "description": "Reload the main corpus periodically" - }, - "print_final_stats": { - "type": "boolean", - "default": true, - "description": "Print final statistics" - }, - "print_pcs": { - "type": "boolean", - "default": false, - "description": "Print newly covered PCs" - }, - "print_funcs": { - "type": "boolean", - "default": false, - "description": "Print newly covered functions" - }, - "print_coverage": { - "type": "boolean", - "default": true, - "description": "Print coverage information" - }, - "shrink": { - "type": "boolean", - "default": true, - "description": "Try to shrink the corpus" - }, - "reduce_inputs": { - "type": "boolean", - "default": true, - "description": "Try to reduce the size of inputs" - }, - "use_value_profile": { - "type": "boolean", - "default": false, - "description": "Use value profile for fuzzing" - }, - "sanitizers": { - "type": "array", - "items": {"type": "string", "enum": ["address", "memory", "undefined", "thread", "leak"]}, - "default": ["address"], - "description": "Sanitizers to use during fuzzing" - }, - "artifact_prefix": { - "type": "string", - "default": "crash-", - "description": "Prefix for artifact files" - }, - "exact_artifact_path": { - "type": "string", - "description": "Exact path for artifact files" - }, - "fork": { - "type": "integer", - "default": 0, - "description": "Fork mode (number of simultaneous processes)" - }, - "ignore_crashes": { - "type": "boolean", - "default": false, - "description": "Ignore crashes and continue fuzzing" - }, - "ignore_timeouts": { - "type": "boolean", - "default": false, - "description": "Ignore timeouts and continue fuzzing" - }, - "ignore_ooms": { - "type": "boolean", - "default": false, - "description": "Ignore out-of-memory and continue fuzzing" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "crash_type": {"type": "string"}, - "crash_file": {"type": "string"}, - "stack_trace": {"type": "string"}, - "sanitizer": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - target_binary = config.get("target_binary") - if not target_binary: - raise ValueError("target_binary is required for LibFuzzer") - - max_total_time = config.get("max_total_time", 600) - if max_total_time <= 0: - raise ValueError("max_total_time must be positive") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute LibFuzzer fuzzing""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running LibFuzzer fuzzing campaign") - - # Check if target binary exists - target_binary = workspace / config["target_binary"] - if not target_binary.exists(): - raise FileNotFoundError(f"Target binary not found: {target_binary}") - - # Run LibFuzzer - findings = await self._run_libfuzzer(target_binary, config, workspace) - - # Create summary - summary = self._create_summary(findings) - - logger.info(f"LibFuzzer found {len(findings)} issues") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"LibFuzzer module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _run_libfuzzer(self, target_binary: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run LibFuzzer fuzzing""" - findings = [] - - try: - # Create output directory for artifacts - output_dir = workspace / "libfuzzer_output" - output_dir.mkdir(exist_ok=True) - - # Build LibFuzzer command - cmd = [str(target_binary)] - - # Add corpus directory - corpus_dir = config.get("corpus_dir") - if corpus_dir: - corpus_path = workspace / corpus_dir - if corpus_path.exists(): - cmd.append(str(corpus_path)) - else: - logger.warning(f"Corpus directory not found: {corpus_path}") - - # Add dictionary file - dict_file = config.get("dict_file") - if dict_file: - dict_path = workspace / dict_file - if dict_path.exists(): - cmd.append(f"-dict={dict_path}") - - # Add fuzzing parameters - cmd.append(f"-max_total_time={config.get('max_total_time', 600)}") - cmd.append(f"-max_len={config.get('max_len', 4096)}") - cmd.append(f"-timeout={config.get('timeout', 25)}") - cmd.append(f"-runs={config.get('runs', -1)}") - - if config.get("jobs", 1) > 1: - cmd.append(f"-jobs={config['jobs']}") - - if config.get("workers", 1) > 1: - cmd.append(f"-workers={config['workers']}") - - cmd.append(f"-reload={config.get('reload', 1)}") - - # Add output options - if config.get("print_final_stats", True): - cmd.append("-print_final_stats=1") - - if config.get("print_pcs", False): - cmd.append("-print_pcs=1") - - if config.get("print_funcs", False): - cmd.append("-print_funcs=1") - - if config.get("print_coverage", True): - cmd.append("-print_coverage=1") - - # Add corpus management options - if config.get("shrink", True): - cmd.append("-shrink=1") - - if config.get("reduce_inputs", True): - cmd.append("-reduce_inputs=1") - - if config.get("use_value_profile", False): - cmd.append("-use_value_profile=1") - - # Add artifact options - artifact_prefix = config.get("artifact_prefix", "crash-") - cmd.append(f"-artifact_prefix={output_dir / artifact_prefix}") - - exact_artifact_path = config.get("exact_artifact_path") - if exact_artifact_path: - cmd.append(f"-exact_artifact_path={output_dir / exact_artifact_path}") - - # Add fork mode - fork = config.get("fork", 0) - if fork > 0: - cmd.append(f"-fork={fork}") - - # Add ignore options - if config.get("ignore_crashes", False): - cmd.append("-ignore_crashes=1") - - if config.get("ignore_timeouts", False): - cmd.append("-ignore_timeouts=1") - - if config.get("ignore_ooms", False): - cmd.append("-ignore_ooms=1") - - # Set up environment for sanitizers - env = os.environ.copy() - sanitizers = config.get("sanitizers", ["address"]) - self._setup_sanitizer_environment(env, sanitizers) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run LibFuzzer - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace, - env=env - ) - - stdout, stderr = await process.communicate() - - # Parse results - findings = self._parse_libfuzzer_output( - stdout.decode(), stderr.decode(), output_dir, workspace, sanitizers - ) - - # Look for crash files - crash_findings = self._parse_crash_files(output_dir, workspace, sanitizers) - findings.extend(crash_findings) - - except Exception as e: - logger.warning(f"Error running LibFuzzer: {e}") - - return findings - - def _setup_sanitizer_environment(self, env: Dict[str, str], sanitizers: List[str]): - """Set up environment variables for sanitizers""" - if "address" in sanitizers: - env["ASAN_OPTIONS"] = env.get("ASAN_OPTIONS", "") + ":halt_on_error=0:abort_on_error=1" - - if "memory" in sanitizers: - env["MSAN_OPTIONS"] = env.get("MSAN_OPTIONS", "") + ":halt_on_error=0:abort_on_error=1" - - if "undefined" in sanitizers: - env["UBSAN_OPTIONS"] = env.get("UBSAN_OPTIONS", "") + ":halt_on_error=0:abort_on_error=1" - - if "thread" in sanitizers: - env["TSAN_OPTIONS"] = env.get("TSAN_OPTIONS", "") + ":halt_on_error=0:abort_on_error=1" - - if "leak" in sanitizers: - env["LSAN_OPTIONS"] = env.get("LSAN_OPTIONS", "") + ":halt_on_error=0:abort_on_error=1" - - def _parse_libfuzzer_output(self, stdout: str, stderr: str, output_dir: Path, workspace: Path, sanitizers: List[str]) -> List[ModuleFinding]: - """Parse LibFuzzer output for crashes and issues""" - findings = [] - - try: - # Combine stdout and stderr for analysis - full_output = stdout + "\n" + stderr - - # Look for crash indicators - crash_patterns = [ - r"ERROR: AddressSanitizer: (.+)", - r"ERROR: MemorySanitizer: (.+)", - r"ERROR: UndefinedBehaviorSanitizer: (.+)", - r"ERROR: ThreadSanitizer: (.+)", - r"ERROR: LeakSanitizer: (.+)", - r"SUMMARY: (.+Sanitizer): (.+)", - r"==\d+==ERROR: libFuzzer: (.+)" - ] - - for pattern in crash_patterns: - matches = re.finditer(pattern, full_output, re.MULTILINE) - for match in matches: - finding = self._create_crash_finding( - match, full_output, output_dir, sanitizers - ) - if finding: - findings.append(finding) - - # Look for timeout and OOM issues - if "TIMEOUT" in full_output: - finding = self._create_timeout_finding(full_output, output_dir) - if finding: - findings.append(finding) - - if "out-of-memory" in full_output.lower() or "oom" in full_output.lower(): - finding = self._create_oom_finding(full_output, output_dir) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing LibFuzzer output: {e}") - - return findings - - def _parse_crash_files(self, output_dir: Path, workspace: Path, sanitizers: List[str]) -> List[ModuleFinding]: - """Parse crash artifact files""" - findings = [] - - try: - # Look for crash files - crash_patterns = ["crash-*", "leak-*", "timeout-*", "oom-*"] - for pattern in crash_patterns: - crash_files = list(output_dir.glob(pattern)) - for crash_file in crash_files: - finding = self._create_artifact_finding(crash_file, workspace, sanitizers) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing crash files: {e}") - - return findings - - def _create_crash_finding(self, match, full_output: str, output_dir: Path, sanitizers: List[str]) -> ModuleFinding: - """Create finding from crash match""" - try: - crash_type = match.group(1) if match.groups() else "Unknown crash" - - # Extract stack trace - stack_trace = self._extract_stack_trace(full_output, match.start()) - - # Determine sanitizer - sanitizer = self._identify_sanitizer(match.group(0), sanitizers) - - # Determine severity based on crash type - severity = self._get_crash_severity(crash_type, sanitizer) - - # Create finding - finding = self.create_finding( - title=f"LibFuzzer Crash: {crash_type}", - description=f"LibFuzzer detected a crash with {sanitizer}: {crash_type}", - severity=severity, - category=self._get_crash_category(crash_type), - file_path=None, # LibFuzzer doesn't always provide specific files - recommendation=self._get_crash_recommendation(crash_type, sanitizer), - metadata={ - "crash_type": crash_type, - "sanitizer": sanitizer, - "stack_trace": stack_trace[:2000] if stack_trace else "", # Limit size - "fuzzer": "libfuzzer" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating crash finding: {e}") - return None - - def _create_timeout_finding(self, output: str, output_dir: Path) -> ModuleFinding: - """Create finding for timeout issues""" - try: - finding = self.create_finding( - title="LibFuzzer Timeout", - description="LibFuzzer detected a timeout during fuzzing, indicating potential infinite loop or performance issue", - severity="medium", - category="performance_issues", - file_path=None, - recommendation="Review the code for potential infinite loops, excessive computation, or blocking operations that could cause timeouts.", - metadata={ - "issue_type": "timeout", - "fuzzer": "libfuzzer" - } - ) - return finding - - except Exception as e: - logger.warning(f"Error creating timeout finding: {e}") - return None - - def _create_oom_finding(self, output: str, output_dir: Path) -> ModuleFinding: - """Create finding for out-of-memory issues""" - try: - finding = self.create_finding( - title="LibFuzzer Out-of-Memory", - description="LibFuzzer detected an out-of-memory condition during fuzzing, indicating potential memory leak or excessive allocation", - severity="medium", - category="memory_management", - file_path=None, - recommendation="Review memory allocation patterns, check for memory leaks, and consider implementing proper bounds checking.", - metadata={ - "issue_type": "out_of_memory", - "fuzzer": "libfuzzer" - } - ) - return finding - - except Exception as e: - logger.warning(f"Error creating OOM finding: {e}") - return None - - def _create_artifact_finding(self, crash_file: Path, workspace: Path, sanitizers: List[str]) -> ModuleFinding: - """Create finding from crash artifact file""" - try: - crash_type = crash_file.name.split('-')[0] # e.g., "crash", "leak", "timeout" - - # Try to read crash file content (limited) - crash_content = "" - try: - crash_content = crash_file.read_bytes()[:1000].decode('utf-8', errors='ignore') - except Exception: - pass - - # Determine severity - severity = self._get_artifact_severity(crash_type) - - finding = self.create_finding( - title=f"LibFuzzer Artifact: {crash_type}", - description=f"LibFuzzer generated a {crash_type} artifact file indicating a potential issue", - severity=severity, - category=self._get_crash_category(crash_type), - file_path=str(crash_file.relative_to(workspace)), - recommendation=self._get_artifact_recommendation(crash_type), - metadata={ - "artifact_type": crash_type, - "artifact_file": str(crash_file.name), - "crash_content_preview": crash_content, - "fuzzer": "libfuzzer" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating artifact finding: {e}") - return None - - def _extract_stack_trace(self, output: str, start_pos: int) -> str: - """Extract stack trace from output""" - try: - lines = output[start_pos:].split('\n') - stack_lines = [] - - for line in lines[:50]: # Limit to first 50 lines - if any(indicator in line for indicator in ["#0", "#1", "#2", "at ", "in "]): - stack_lines.append(line.strip()) - elif stack_lines and not line.strip(): - break - - return '\n'.join(stack_lines) - - except Exception: - return "" - - def _identify_sanitizer(self, crash_line: str, sanitizers: List[str]) -> str: - """Identify which sanitizer detected the issue""" - crash_lower = crash_line.lower() - - if "addresssanitizer" in crash_lower: - return "AddressSanitizer" - elif "memorysanitizer" in crash_lower: - return "MemorySanitizer" - elif "undefinedbehaviorsanitizer" in crash_lower: - return "UndefinedBehaviorSanitizer" - elif "threadsanitizer" in crash_lower: - return "ThreadSanitizer" - elif "leaksanitizer" in crash_lower: - return "LeakSanitizer" - elif "libfuzzer" in crash_lower: - return "LibFuzzer" - else: - return "Unknown" - - def _get_crash_severity(self, crash_type: str, sanitizer: str) -> str: - """Determine severity based on crash type and sanitizer""" - crash_lower = crash_type.lower() - - # Critical issues - if any(term in crash_lower for term in ["heap-buffer-overflow", "stack-buffer-overflow", "use-after-free", "double-free"]): - return "critical" - - # High severity issues - elif any(term in crash_lower for term in ["heap-use-after-free", "stack-use-after-return", "global-buffer-overflow"]): - return "high" - - # Medium severity issues - elif any(term in crash_lower for term in ["uninitialized", "leak", "race", "deadlock"]): - return "medium" - - # Default to high for any crash - else: - return "high" - - def _get_crash_category(self, crash_type: str) -> str: - """Determine category based on crash type""" - crash_lower = crash_type.lower() - - if any(term in crash_lower for term in ["buffer-overflow", "heap-buffer", "stack-buffer", "global-buffer"]): - return "buffer_overflow" - elif any(term in crash_lower for term in ["use-after-free", "double-free", "invalid-free"]): - return "memory_corruption" - elif any(term in crash_lower for term in ["uninitialized", "uninit"]): - return "uninitialized_memory" - elif any(term in crash_lower for term in ["leak"]): - return "memory_leak" - elif any(term in crash_lower for term in ["race", "data-race"]): - return "race_condition" - elif any(term in crash_lower for term in ["timeout"]): - return "performance_issues" - elif any(term in crash_lower for term in ["oom", "out-of-memory"]): - return "memory_management" - else: - return "memory_safety" - - def _get_artifact_severity(self, artifact_type: str) -> str: - """Determine severity for artifact types""" - if artifact_type == "crash": - return "high" - elif artifact_type == "leak": - return "medium" - elif artifact_type in ["timeout", "oom"]: - return "medium" - else: - return "low" - - def _get_crash_recommendation(self, crash_type: str, sanitizer: str) -> str: - """Generate recommendation based on crash type""" - crash_lower = crash_type.lower() - - if "buffer-overflow" in crash_lower: - return "Fix buffer overflow by implementing proper bounds checking, using safe string functions, and validating array indices." - elif "use-after-free" in crash_lower: - return "Fix use-after-free by setting pointers to NULL after freeing, using smart pointers, or redesigning object lifetime management." - elif "double-free" in crash_lower: - return "Fix double-free by ensuring each allocation has exactly one corresponding free, or use RAII patterns." - elif "uninitialized" in crash_lower: - return "Initialize all variables before use and ensure proper constructor implementation." - elif "leak" in crash_lower: - return "Fix memory leak by ensuring all allocated memory is properly freed, use smart pointers, or implement proper cleanup routines." - elif "race" in crash_lower: - return "Fix data race by using proper synchronization mechanisms like mutexes, atomic operations, or lock-free data structures." - else: - return f"Address the {crash_type} issue detected by {sanitizer}. Review code for memory safety and proper resource management." - - def _get_artifact_recommendation(self, artifact_type: str) -> str: - """Generate recommendation for artifact types""" - if artifact_type == "crash": - return "Analyze the crash artifact file to reproduce the issue and identify the root cause. Fix the underlying bug that caused the crash." - elif artifact_type == "leak": - return "Investigate the memory leak by analyzing allocation patterns and ensuring proper cleanup of resources." - elif artifact_type == "timeout": - return "Optimize code performance to prevent timeouts, check for infinite loops, and implement reasonable time limits." - elif artifact_type == "oom": - return "Reduce memory usage, implement proper memory management, and add bounds checking for allocations." - else: - return f"Analyze the {artifact_type} artifact to understand and fix the underlying issue." - - def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - sanitizer_counts = {} - crash_type_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by sanitizer - sanitizer = finding.metadata.get("sanitizer", "unknown") - sanitizer_counts[sanitizer] = sanitizer_counts.get(sanitizer, 0) + 1 - - # Count by crash type - crash_type = finding.metadata.get("crash_type", finding.metadata.get("issue_type", "unknown")) - crash_type_counts[crash_type] = crash_type_counts.get(crash_type, 0) + 1 - - return { - "total_findings": len(findings), - "severity_counts": severity_counts, - "category_counts": category_counts, - "sanitizer_counts": sanitizer_counts, - "crash_type_counts": crash_type_counts, - "memory_safety_issues": category_counts.get("memory_safety", 0) + - category_counts.get("buffer_overflow", 0) + - category_counts.get("memory_corruption", 0), - "performance_issues": category_counts.get("performance_issues", 0) - } \ No newline at end of file diff --git a/backend/toolbox/modules/fuzzing/oss_fuzz.py b/backend/toolbox/modules/fuzzing/oss_fuzz.py deleted file mode 100644 index 83ff7d8..0000000 --- a/backend/toolbox/modules/fuzzing/oss_fuzz.py +++ /dev/null @@ -1,547 +0,0 @@ -""" -OSS-Fuzz Module - -This module integrates with Google's OSS-Fuzz for continuous fuzzing -of open source projects. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -import os -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class OSSFuzzModule(BaseModule): - """OSS-Fuzz continuous fuzzing module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="oss_fuzz", - version="1.0.0", - description="Google's continuous fuzzing for open source projects integration", - author="FuzzForge Team", - category="fuzzing", - tags=["oss-fuzz", "continuous", "google", "open-source", "docker"], - input_schema={ - "type": "object", - "properties": { - "project_name": { - "type": "string", - "description": "OSS-Fuzz project name" - }, - "source_dir": { - "type": "string", - "description": "Source directory to fuzz" - }, - "build_script": { - "type": "string", - "default": "build.sh", - "description": "Build script path" - }, - "dockerfile": { - "type": "string", - "default": "Dockerfile", - "description": "Dockerfile path" - }, - "project_yaml": { - "type": "string", - "default": "project.yaml", - "description": "Project configuration file" - }, - "sanitizer": { - "type": "string", - "enum": ["address", "memory", "undefined", "coverage"], - "default": "address", - "description": "Sanitizer to use" - }, - "architecture": { - "type": "string", - "enum": ["x86_64", "i386"], - "default": "x86_64", - "description": "Target architecture" - }, - "fuzzing_engine": { - "type": "string", - "enum": ["libfuzzer", "afl", "honggfuzz"], - "default": "libfuzzer", - "description": "Fuzzing engine to use" - }, - "timeout": { - "type": "integer", - "default": 3600, - "description": "Fuzzing timeout (seconds)" - }, - "check_build": { - "type": "boolean", - "default": true, - "description": "Check if build is successful" - }, - "reproduce_bugs": { - "type": "boolean", - "default": false, - "description": "Try to reproduce existing bugs" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "bug_type": {"type": "string"}, - "reproducer": {"type": "string"}, - "stack_trace": {"type": "string"}, - "sanitizer": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - project_name = config.get("project_name") - if not project_name: - raise ValueError("project_name is required") - - source_dir = config.get("source_dir") - if not source_dir: - raise ValueError("source_dir is required") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute OSS-Fuzz integration""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running OSS-Fuzz integration") - - # Check Docker - await self._check_docker() - - # Clone/update OSS-Fuzz if needed - oss_fuzz_dir = await self._setup_oss_fuzz(workspace) - - # Setup project - await self._setup_project(oss_fuzz_dir, config, workspace) - - # Build and run - findings = await self._run_oss_fuzz(oss_fuzz_dir, config, workspace) - - # Create summary - summary = self._create_summary(findings) - - logger.info(f"OSS-Fuzz found {len(findings)} issues") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"OSS-Fuzz module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _check_docker(self): - """Check if Docker is available""" - try: - process = await asyncio.create_subprocess_exec( - "docker", "--version", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - raise RuntimeError("Docker not available. OSS-Fuzz requires Docker.") - - except Exception as e: - raise RuntimeError(f"Docker check failed: {e}") - - async def _setup_oss_fuzz(self, workspace: Path) -> Path: - """Setup OSS-Fuzz repository""" - oss_fuzz_dir = workspace / "oss-fuzz" - - if not oss_fuzz_dir.exists(): - logger.info("Cloning OSS-Fuzz repository") - process = await asyncio.create_subprocess_exec( - "git", "clone", "https://github.com/google/oss-fuzz.git", - cwd=workspace, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - raise RuntimeError(f"Failed to clone OSS-Fuzz: {stderr.decode()}") - - return oss_fuzz_dir - - async def _setup_project(self, oss_fuzz_dir: Path, config: Dict[str, Any], workspace: Path): - """Setup OSS-Fuzz project""" - project_name = config["project_name"] - project_dir = oss_fuzz_dir / "projects" / project_name - - # Create project directory if it doesn't exist - project_dir.mkdir(parents=True, exist_ok=True) - - # Copy source if provided - source_dir = workspace / config["source_dir"] - if source_dir.exists(): - # Create symlink or copy source - logger.info(f"Setting up source directory: {source_dir}") - - # Setup required files if they don't exist - await self._create_project_files(project_dir, config, workspace) - - async def _create_project_files(self, project_dir: Path, config: Dict[str, Any], workspace: Path): - """Create required OSS-Fuzz project files""" - - # Create Dockerfile if it doesn't exist - dockerfile = project_dir / config.get("dockerfile", "Dockerfile") - if not dockerfile.exists(): - dockerfile_content = f'''FROM gcr.io/oss-fuzz-base/base-builder -COPY . $SRC/{config["project_name"]} -WORKDIR $SRC/{config["project_name"]} -COPY {config.get("build_script", "build.sh")} $SRC/ -''' - dockerfile.write_text(dockerfile_content) - - # Create build.sh if it doesn't exist - build_script = project_dir / config.get("build_script", "build.sh") - if not build_script.exists(): - build_content = f'''#!/bin/bash -eu -# Build script for {config["project_name"]} -# Add your build commands here -echo "Building {config['project_name']}..." -''' - build_script.write_text(build_content) - build_script.chmod(0o755) - - # Create project.yaml if it doesn't exist - project_yaml = project_dir / config.get("project_yaml", "project.yaml") - if not project_yaml.exists(): - yaml_content = f'''homepage: "https://example.com" -language: c++ -primary_contact: "security@example.com" -auto_ccs: - - "fuzzing@example.com" -sanitizers: - - {config.get("sanitizer", "address")} -architectures: - - {config.get("architecture", "x86_64")} -fuzzing_engines: - - {config.get("fuzzing_engine", "libfuzzer")} -''' - project_yaml.write_text(yaml_content) - - async def _run_oss_fuzz(self, oss_fuzz_dir: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run OSS-Fuzz""" - findings = [] - - try: - project_name = config["project_name"] - sanitizer = config.get("sanitizer", "address") - architecture = config.get("architecture", "x86_64") - - # Build project - if config.get("check_build", True): - await self._build_project(oss_fuzz_dir, project_name, sanitizer, architecture) - - # Check build - await self._check_build(oss_fuzz_dir, project_name, sanitizer, architecture) - - # Run fuzzing (limited time for this integration) - timeout = min(config.get("timeout", 300), 300) # Max 5 minutes for demo - findings = await self._run_fuzzing(oss_fuzz_dir, project_name, sanitizer, timeout, workspace) - - # Reproduce bugs if requested - if config.get("reproduce_bugs", False): - repro_findings = await self._reproduce_bugs(oss_fuzz_dir, project_name, workspace) - findings.extend(repro_findings) - - except Exception as e: - logger.warning(f"Error running OSS-Fuzz: {e}") - - return findings - - async def _build_project(self, oss_fuzz_dir: Path, project_name: str, sanitizer: str, architecture: str): - """Build OSS-Fuzz project""" - cmd = [ - "python3", "infra/helper.py", "build_image", project_name - ] - - process = await asyncio.create_subprocess_exec( - *cmd, - cwd=oss_fuzz_dir, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - logger.warning(f"Build image failed: {stderr.decode()}") - - async def _check_build(self, oss_fuzz_dir: Path, project_name: str, sanitizer: str, architecture: str): - """Check OSS-Fuzz build""" - cmd = [ - "python3", "infra/helper.py", "check_build", project_name - ] - - process = await asyncio.create_subprocess_exec( - *cmd, - cwd=oss_fuzz_dir, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - stdout, stderr = await process.communicate() - - if process.returncode != 0: - logger.warning(f"Build check failed: {stderr.decode()}") - - async def _run_fuzzing(self, oss_fuzz_dir: Path, project_name: str, sanitizer: str, timeout: int, workspace: Path) -> List[ModuleFinding]: - """Run OSS-Fuzz fuzzing""" - findings = [] - - try: - # This is a simplified version - real OSS-Fuzz runs for much longer - cmd = [ - "python3", "infra/helper.py", "run_fuzzer", project_name, - "--", f"-max_total_time={timeout}" - ] - - process = await asyncio.create_subprocess_exec( - *cmd, - cwd=oss_fuzz_dir, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - - try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), timeout=timeout + 60 - ) - except asyncio.TimeoutError: - process.terminate() - await process.wait() - - # Parse output for crashes - full_output = stdout.decode() + stderr.decode() - findings = self._parse_oss_fuzz_output(full_output, workspace, sanitizer) - - except Exception as e: - logger.warning(f"Error in OSS-Fuzz execution: {e}") - - return findings - - async def _reproduce_bugs(self, oss_fuzz_dir: Path, project_name: str, workspace: Path) -> List[ModuleFinding]: - """Reproduce existing bugs""" - findings = [] - - try: - # Look for existing testcases or artifacts - testcases_dir = oss_fuzz_dir / "projects" / project_name / "testcases" - if testcases_dir.exists(): - for testcase in testcases_dir.iterdir(): - if testcase.is_file(): - finding = self._create_testcase_finding(testcase, workspace) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error reproducing bugs: {e}") - - return findings - - def _parse_oss_fuzz_output(self, output: str, workspace: Path, sanitizer: str) -> List[ModuleFinding]: - """Parse OSS-Fuzz output""" - findings = [] - - try: - # Look for common crash indicators - lines = output.split('\n') - crash_info = None - - for line in lines: - if "ERROR:" in line and any(term in line for term in ["AddressSanitizer", "MemorySanitizer", "UBSan"]): - crash_info = { - "type": self._extract_crash_type(line), - "sanitizer": sanitizer, - "line": line - } - elif crash_info and line.strip().startswith("#"): - # Stack trace line - if "stack_trace" not in crash_info: - crash_info["stack_trace"] = [] - crash_info["stack_trace"].append(line.strip()) - - if crash_info: - finding = self._create_oss_fuzz_finding(crash_info, workspace) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing OSS-Fuzz output: {e}") - - return findings - - def _create_oss_fuzz_finding(self, crash_info: Dict[str, Any], workspace: Path) -> ModuleFinding: - """Create finding from OSS-Fuzz crash""" - try: - bug_type = crash_info.get("type", "unknown") - sanitizer = crash_info.get("sanitizer", "unknown") - stack_trace = '\n'.join(crash_info.get("stack_trace", [])[:20]) - - severity = self._get_oss_fuzz_severity(bug_type) - - finding = self.create_finding( - title=f"OSS-Fuzz {bug_type.title()}", - description=f"OSS-Fuzz detected a {bug_type} using {sanitizer} sanitizer", - severity=severity, - category=self._get_oss_fuzz_category(bug_type), - file_path=None, - recommendation=self._get_oss_fuzz_recommendation(bug_type, sanitizer), - metadata={ - "bug_type": bug_type, - "sanitizer": sanitizer, - "stack_trace": stack_trace, - "fuzzer": "oss_fuzz" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating OSS-Fuzz finding: {e}") - return None - - def _create_testcase_finding(self, testcase_file: Path, workspace: Path) -> ModuleFinding: - """Create finding from testcase file""" - try: - try: - rel_path = testcase_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(testcase_file) - - finding = self.create_finding( - title="OSS-Fuzz Testcase", - description=f"OSS-Fuzz testcase found: {testcase_file.name}", - severity="info", - category="testcase", - file_path=file_path, - recommendation="Analyze testcase to understand potential issues", - metadata={ - "testcase_file": str(testcase_file), - "fuzzer": "oss_fuzz" - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating testcase finding: {e}") - return None - - def _extract_crash_type(self, line: str) -> str: - """Extract crash type from error line""" - if "heap-buffer-overflow" in line: - return "heap_buffer_overflow" - elif "stack-buffer-overflow" in line: - return "stack_buffer_overflow" - elif "use-after-free" in line: - return "use_after_free" - elif "double-free" in line: - return "double_free" - elif "memory leak" in line: - return "memory_leak" - else: - return "unknown_crash" - - def _get_oss_fuzz_severity(self, bug_type: str) -> str: - """Get severity for OSS-Fuzz bug type""" - if bug_type in ["heap_buffer_overflow", "stack_buffer_overflow", "use_after_free", "double_free"]: - return "critical" - elif bug_type == "memory_leak": - return "medium" - else: - return "high" - - def _get_oss_fuzz_category(self, bug_type: str) -> str: - """Get category for OSS-Fuzz bug type""" - if "overflow" in bug_type: - return "buffer_overflow" - elif "free" in bug_type: - return "memory_corruption" - elif "leak" in bug_type: - return "memory_leak" - else: - return "memory_safety" - - def _get_oss_fuzz_recommendation(self, bug_type: str, sanitizer: str) -> str: - """Get recommendation for OSS-Fuzz finding""" - if "overflow" in bug_type: - return "Fix buffer overflow by implementing proper bounds checking and using safe string functions." - elif "use_after_free" in bug_type: - return "Fix use-after-free by ensuring proper object lifetime management and setting pointers to NULL after freeing." - elif "double_free" in bug_type: - return "Fix double-free by ensuring each allocation has exactly one corresponding free operation." - elif "leak" in bug_type: - return "Fix memory leak by ensuring all allocated memory is properly freed in all code paths." - else: - return f"Address the {bug_type} issue detected by OSS-Fuzz with {sanitizer} sanitizer." - - def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - sanitizer_counts = {} - - for finding in findings: - severity_counts[finding.severity] += 1 - category_counts[finding.category] = category_counts.get(finding.category, 0) + 1 - - sanitizer = finding.metadata.get("sanitizer", "unknown") - sanitizer_counts[sanitizer] = sanitizer_counts.get(sanitizer, 0) + 1 - - return { - "total_findings": len(findings), - "severity_counts": severity_counts, - "category_counts": category_counts, - "sanitizer_counts": sanitizer_counts - } \ No newline at end of file diff --git a/backend/toolbox/modules/infrastructure/__init__.py b/backend/toolbox/modules/infrastructure/__init__.py deleted file mode 100644 index d27c14d..0000000 --- a/backend/toolbox/modules/infrastructure/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Infrastructure Security Modules - -This package contains modules for Infrastructure as Code (IaC) security testing. - -Available modules: -- Checkov: Terraform/CloudFormation/Kubernetes IaC security -- Hadolint: Dockerfile security linting and best practices -- Kubesec: Kubernetes security risk analysis -- Polaris: Kubernetes configuration validation -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -from typing import List, Type -from ..base import BaseModule - -# Module registry for automatic discovery -INFRASTRUCTURE_MODULES: List[Type[BaseModule]] = [] - -def register_module(module_class: Type[BaseModule]): - """Register an infrastructure security module""" - INFRASTRUCTURE_MODULES.append(module_class) - return module_class - -def get_available_modules() -> List[Type[BaseModule]]: - """Get all available infrastructure security modules""" - return INFRASTRUCTURE_MODULES.copy() - -# Import modules to trigger registration -from .checkov import CheckovModule -from .hadolint import HadolintModule -from .kubesec import KubesecModule -from .polaris import PolarisModule \ No newline at end of file diff --git a/backend/toolbox/modules/infrastructure/checkov.py b/backend/toolbox/modules/infrastructure/checkov.py deleted file mode 100644 index 76fe2a7..0000000 --- a/backend/toolbox/modules/infrastructure/checkov.py +++ /dev/null @@ -1,411 +0,0 @@ -""" -Checkov Infrastructure Security Module - -This module uses Checkov to scan Infrastructure as Code (IaC) files for -security misconfigurations and compliance violations. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class CheckovModule(BaseModule): - """Checkov Infrastructure as Code security scanning module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="checkov", - version="3.1.34", - description="Infrastructure as Code security scanning for Terraform, CloudFormation, Kubernetes, and more", - author="FuzzForge Team", - category="infrastructure", - tags=["iac", "terraform", "cloudformation", "kubernetes", "security", "compliance"], - input_schema={ - "type": "object", - "properties": { - "frameworks": { - "type": "array", - "items": {"type": "string"}, - "default": ["terraform", "cloudformation", "kubernetes"], - "description": "IaC frameworks to scan" - }, - "checks": { - "type": "array", - "items": {"type": "string"}, - "description": "Specific checks to run" - }, - "skip_checks": { - "type": "array", - "items": {"type": "string"}, - "description": "Checks to skip" - }, - "severity": { - "type": "array", - "items": {"type": "string", "enum": ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]}, - "default": ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"], - "description": "Minimum severity levels to report" - }, - "compact": { - "type": "boolean", - "default": False, - "description": "Use compact output format" - }, - "quiet": { - "type": "boolean", - "default": False, - "description": "Suppress verbose output" - }, - "soft_fail": { - "type": "boolean", - "default": True, - "description": "Return exit code 0 even when issues are found" - }, - "include_patterns": { - "type": "array", - "items": {"type": "string"}, - "description": "File patterns to include" - }, - "exclude_patterns": { - "type": "array", - "items": {"type": "string"}, - "description": "File patterns to exclude" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "check_id": {"type": "string"}, - "check_name": {"type": "string"}, - "severity": {"type": "string"}, - "file_path": {"type": "string"}, - "line_range": {"type": "array"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - frameworks = config.get("frameworks", []) - supported_frameworks = [ - "terraform", "cloudformation", "kubernetes", "dockerfile", - "ansible", "helm", "serverless", "bicep", "github_actions" - ] - - for framework in frameworks: - if framework not in supported_frameworks: - raise ValueError(f"Unsupported framework: {framework}. Supported: {supported_frameworks}") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Checkov IaC security scanning""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info(f"Running Checkov IaC scan on {workspace}") - - # Check if there are any IaC files - iac_files = self._find_iac_files(workspace, config.get("frameworks", [])) - if not iac_files: - logger.info("No Infrastructure as Code files found") - return self.create_result( - findings=[], - status="success", - summary={"total_findings": 0, "files_scanned": 0} - ) - - # Build checkov command - cmd = ["checkov", "-d", str(workspace)] - - # Add output format - cmd.extend(["--output", "json"]) - - # Add frameworks - frameworks = config.get("frameworks", ["terraform", "cloudformation", "kubernetes"]) - cmd.extend(["--framework"] + frameworks) - - # Add specific checks - if config.get("checks"): - cmd.extend(["--check", ",".join(config["checks"])]) - - # Add skip checks - if config.get("skip_checks"): - cmd.extend(["--skip-check", ",".join(config["skip_checks"])]) - - # Add compact flag - if config.get("compact", False): - cmd.append("--compact") - - # Add quiet flag - if config.get("quiet", False): - cmd.append("--quiet") - - # Add soft fail - if config.get("soft_fail", True): - cmd.append("--soft-fail") - - # Add include patterns - if config.get("include_patterns"): - for pattern in config["include_patterns"]: - cmd.extend(["--include", pattern]) - - # Add exclude patterns - if config.get("exclude_patterns"): - for pattern in config["exclude_patterns"]: - cmd.extend(["--exclude", pattern]) - - # Disable update checks and telemetry - cmd.extend(["--no-guide", "--skip-download"]) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run Checkov - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results - findings = [] - if process.returncode == 0 or config.get("soft_fail", True): - findings = self._parse_checkov_output(stdout.decode(), workspace, config) - else: - error_msg = stderr.decode() - logger.error(f"Checkov failed: {error_msg}") - return self.create_result( - findings=[], - status="failed", - error=f"Checkov execution failed: {error_msg}" - ) - - # Create summary - summary = self._create_summary(findings, len(iac_files)) - - logger.info(f"Checkov found {len(findings)} security issues") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Checkov module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - def _find_iac_files(self, workspace: Path, frameworks: List[str]) -> List[Path]: - """Find Infrastructure as Code files in workspace""" - iac_patterns = { - "terraform": ["*.tf", "*.tfvars"], - "cloudformation": ["*.yaml", "*.yml", "*.json", "*template*"], - "kubernetes": ["*.yaml", "*.yml"], - "dockerfile": ["Dockerfile", "*.dockerfile"], - "ansible": ["*.yaml", "*.yml", "playbook*"], - "helm": ["Chart.yaml", "values.yaml", "*.yaml"], - "bicep": ["*.bicep"], - "github_actions": [".github/workflows/*.yaml", ".github/workflows/*.yml"] - } - - found_files = [] - for framework in frameworks: - patterns = iac_patterns.get(framework, []) - for pattern in patterns: - found_files.extend(workspace.rglob(pattern)) - - return list(set(found_files)) # Remove duplicates - - def _parse_checkov_output(self, output: str, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]: - """Parse Checkov JSON output into findings""" - findings = [] - - if not output.strip(): - return findings - - try: - data = json.loads(output) - - # Get severity filter - allowed_severities = set(s.upper() for s in config.get("severity", ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"])) - - # Process failed checks - failed_checks = data.get("results", {}).get("failed_checks", []) - - for check in failed_checks: - # Extract information - check_id = check.get("check_id", "unknown") - check_name = check.get("check_name", "") - severity = check.get("severity", "MEDIUM").upper() - file_path = check.get("file_path", "") - file_line_range = check.get("file_line_range", []) - resource = check.get("resource", "") - description = check.get("description", "") - guideline = check.get("guideline", "") - - # Apply severity filter - if severity not in allowed_severities: - continue - - # Make file path relative to workspace - if file_path: - try: - rel_path = Path(file_path).relative_to(workspace) - file_path = str(rel_path) - except ValueError: - pass - - # Map severity to our standard levels - finding_severity = self._map_severity(severity) - - # Create finding - finding = self.create_finding( - title=f"IaC Security Issue: {check_name}", - description=description or f"Checkov check {check_id} failed for resource {resource}", - severity=finding_severity, - category=self._get_category(check_id, check_name), - file_path=file_path if file_path else None, - line_start=file_line_range[0] if file_line_range and len(file_line_range) > 0 else None, - line_end=file_line_range[1] if file_line_range and len(file_line_range) > 1 else None, - recommendation=self._get_recommendation(check_id, check_name, guideline), - metadata={ - "check_id": check_id, - "check_name": check_name, - "checkov_severity": severity, - "resource": resource, - "guideline": guideline, - "bc_category": check.get("bc_category", ""), - "benchmarks": check.get("benchmarks", {}), - "fixed_definition": check.get("fixed_definition", "") - } - ) - - findings.append(finding) - - except json.JSONDecodeError as e: - logger.warning(f"Failed to parse Checkov output: {e}") - except Exception as e: - logger.warning(f"Error processing Checkov results: {e}") - - return findings - - def _map_severity(self, checkov_severity: str) -> str: - """Map Checkov severity to our standard severity levels""" - severity_map = { - "CRITICAL": "critical", - "HIGH": "high", - "MEDIUM": "medium", - "LOW": "low", - "INFO": "info" - } - return severity_map.get(checkov_severity.upper(), "medium") - - def _get_category(self, check_id: str, check_name: str) -> str: - """Determine finding category based on check""" - check_lower = f"{check_id} {check_name}".lower() - - if any(term in check_lower for term in ["encryption", "encrypt", "kms", "ssl", "tls"]): - return "encryption" - elif any(term in check_lower for term in ["access", "iam", "rbac", "permission"]): - return "access_control" - elif any(term in check_lower for term in ["network", "security group", "firewall", "vpc"]): - return "network_security" - elif any(term in check_lower for term in ["logging", "monitor", "audit"]): - return "logging_monitoring" - elif any(term in check_lower for term in ["storage", "s3", "bucket", "database"]): - return "data_protection" - elif any(term in check_lower for term in ["secret", "password", "key", "credential"]): - return "secrets_management" - elif any(term in check_lower for term in ["backup", "snapshot", "versioning"]): - return "backup_recovery" - else: - return "infrastructure_security" - - def _get_recommendation(self, check_id: str, check_name: str, guideline: str) -> str: - """Generate recommendation based on check""" - if guideline: - return f"Follow the guideline: {guideline}" - - # Generic recommendations based on common patterns - check_lower = f"{check_id} {check_name}".lower() - - if "encryption" in check_lower: - return "Enable encryption for sensitive data at rest and in transit using appropriate encryption algorithms." - elif "access" in check_lower or "iam" in check_lower: - return "Review and tighten access controls. Follow the principle of least privilege." - elif "network" in check_lower or "security group" in check_lower: - return "Restrict network access to only necessary ports and IP ranges." - elif "logging" in check_lower: - return "Enable comprehensive logging and monitoring for security events." - elif "backup" in check_lower: - return "Implement proper backup and disaster recovery procedures." - else: - return f"Review and fix the security configuration issue identified by check {check_id}." - - def _create_summary(self, findings: List[ModuleFinding], total_files: int) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - check_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by check - check_id = finding.metadata.get("check_id", "unknown") - check_counts[check_id] = check_counts.get(check_id, 0) + 1 - - return { - "total_findings": len(findings), - "files_scanned": total_files, - "severity_counts": severity_counts, - "category_counts": category_counts, - "top_checks": dict(sorted(check_counts.items(), key=lambda x: x[1], reverse=True)[:10]), - "files_with_issues": len(set(f.file_path for f in findings if f.file_path)) - } \ No newline at end of file diff --git a/backend/toolbox/modules/infrastructure/hadolint.py b/backend/toolbox/modules/infrastructure/hadolint.py deleted file mode 100644 index a3d812c..0000000 --- a/backend/toolbox/modules/infrastructure/hadolint.py +++ /dev/null @@ -1,406 +0,0 @@ -""" -Hadolint Infrastructure Security Module - -This module uses Hadolint to scan Dockerfiles for security best practices -and potential vulnerabilities. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class HadolintModule(BaseModule): - """Hadolint Dockerfile security scanning module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="hadolint", - version="2.12.0", - description="Dockerfile security linting and best practices validation", - author="FuzzForge Team", - category="infrastructure", - tags=["dockerfile", "docker", "security", "best-practices", "linting"], - input_schema={ - "type": "object", - "properties": { - "severity": { - "type": "array", - "items": {"type": "string", "enum": ["error", "warning", "info", "style"]}, - "default": ["error", "warning", "info", "style"], - "description": "Minimum severity levels to report" - }, - "ignored_rules": { - "type": "array", - "items": {"type": "string"}, - "description": "Hadolint rules to ignore" - }, - "trusted_registries": { - "type": "array", - "items": {"type": "string"}, - "description": "List of trusted Docker registries" - }, - "allowed_maintainers": { - "type": "array", - "items": {"type": "string"}, - "description": "List of allowed maintainer emails" - }, - "dockerfile_patterns": { - "type": "array", - "items": {"type": "string"}, - "default": ["**/Dockerfile", "**/*.dockerfile", "**/Containerfile"], - "description": "Patterns to find Dockerfile-like files" - }, - "strict": { - "type": "boolean", - "default": False, - "description": "Enable strict mode (fail on any issue)" - }, - "no_fail": { - "type": "boolean", - "default": True, - "description": "Don't fail on lint errors (useful for reporting)" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "rule": {"type": "string"}, - "severity": {"type": "string"}, - "message": {"type": "string"}, - "file_path": {"type": "string"}, - "line": {"type": "integer"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - severity_levels = config.get("severity", ["error", "warning", "info", "style"]) - valid_severities = ["error", "warning", "info", "style"] - - for severity in severity_levels: - if severity not in valid_severities: - raise ValueError(f"Invalid severity level: {severity}. Valid: {valid_severities}") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Hadolint Dockerfile security scanning""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info(f"Running Hadolint Dockerfile scan on {workspace}") - - # Find all Dockerfiles - dockerfiles = self._find_dockerfiles(workspace, config) - if not dockerfiles: - logger.info("No Dockerfiles found for Hadolint analysis") - return self.create_result( - findings=[], - status="success", - summary={"total_findings": 0, "files_scanned": 0} - ) - - logger.info(f"Found {len(dockerfiles)} Dockerfile(s) to analyze") - - # Process each Dockerfile - all_findings = [] - for dockerfile in dockerfiles: - findings = await self._scan_dockerfile(dockerfile, workspace, config) - all_findings.extend(findings) - - # Create summary - summary = self._create_summary(all_findings, len(dockerfiles)) - - logger.info(f"Hadolint found {len(all_findings)} issues across {len(dockerfiles)} Dockerfiles") - - return self.create_result( - findings=all_findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Hadolint module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - def _find_dockerfiles(self, workspace: Path, config: Dict[str, Any]) -> List[Path]: - """Find Dockerfile-like files in workspace""" - patterns = config.get("dockerfile_patterns", [ - "**/Dockerfile", "**/*.dockerfile", "**/Containerfile" - ]) - - # Debug logging - logger.info(f"Hadolint searching in workspace: {workspace}") - logger.info(f"Workspace exists: {workspace.exists()}") - if workspace.exists(): - all_files = list(workspace.rglob("*")) - logger.info(f"All files in workspace: {all_files}") - - dockerfiles = [] - for pattern in patterns: - matches = list(workspace.glob(pattern)) - logger.info(f"Pattern '{pattern}' found: {matches}") - dockerfiles.extend(matches) - - logger.info(f"Final dockerfiles list: {dockerfiles}") - return list(set(dockerfiles)) # Remove duplicates - - async def _scan_dockerfile(self, dockerfile: Path, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]: - """Scan a single Dockerfile with Hadolint""" - findings = [] - - try: - # Build hadolint command - cmd = ["hadolint", "--format", "json"] - - # Add severity levels - severity_levels = config.get("severity", ["error", "warning", "info", "style"]) - if "error" not in severity_levels: - cmd.append("--no-error") - if "warning" not in severity_levels: - cmd.append("--no-warning") - if "info" not in severity_levels: - cmd.append("--no-info") - if "style" not in severity_levels: - cmd.append("--no-style") - - # Add ignored rules - ignored_rules = config.get("ignored_rules", []) - for rule in ignored_rules: - cmd.extend(["--ignore", rule]) - - # Add trusted registries - trusted_registries = config.get("trusted_registries", []) - for registry in trusted_registries: - cmd.extend(["--trusted-registry", registry]) - - # Add strict mode - if config.get("strict", False): - cmd.append("--strict-labels") - - # Add the dockerfile - cmd.append(str(dockerfile)) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run hadolint - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results - if process.returncode == 0 or config.get("no_fail", True): - findings = self._parse_hadolint_output( - stdout.decode(), dockerfile, workspace - ) - else: - error_msg = stderr.decode() - logger.warning(f"Hadolint failed for {dockerfile}: {error_msg}") - # Continue with other files even if one fails - - except Exception as e: - logger.warning(f"Error scanning {dockerfile}: {e}") - - return findings - - def _parse_hadolint_output(self, output: str, dockerfile: Path, workspace: Path) -> List[ModuleFinding]: - """Parse Hadolint JSON output into findings""" - findings = [] - - if not output.strip(): - return findings - - try: - # Hadolint outputs JSON array - issues = json.loads(output) - - for issue in issues: - # Extract information - rule = issue.get("code", "unknown") - message = issue.get("message", "") - level = issue.get("level", "warning").lower() - line = issue.get("line", 0) - column = issue.get("column", 0) - - # Make file path relative to workspace - try: - rel_path = dockerfile.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(dockerfile) - - # Map Hadolint level to our severity - severity = self._map_severity(level) - - # Get category based on rule - category = self._get_category(rule, message) - - # Create finding - finding = self.create_finding( - title=f"Dockerfile issue: {rule}", - description=message or f"Hadolint rule {rule} violation", - severity=severity, - category=category, - file_path=file_path, - line_start=line if line > 0 else None, - recommendation=self._get_recommendation(rule, message), - metadata={ - "rule": rule, - "hadolint_level": level, - "column": column, - "file": str(dockerfile) - } - ) - - findings.append(finding) - - except json.JSONDecodeError as e: - logger.warning(f"Failed to parse Hadolint output: {e}") - except Exception as e: - logger.warning(f"Error processing Hadolint results: {e}") - - return findings - - def _map_severity(self, hadolint_level: str) -> str: - """Map Hadolint severity to our standard severity levels""" - severity_map = { - "error": "high", - "warning": "medium", - "info": "low", - "style": "info" - } - return severity_map.get(hadolint_level.lower(), "medium") - - def _get_category(self, rule: str, message: str) -> str: - """Determine finding category based on rule and message""" - rule_lower = rule.lower() - message_lower = message.lower() - - # Security-related categories - if any(term in rule_lower for term in ["dl3", "dl4"]): - if "user" in message_lower or "root" in message_lower: - return "privilege_escalation" - elif "secret" in message_lower or "password" in message_lower: - return "secrets_management" - elif "version" in message_lower or "pin" in message_lower: - return "dependency_management" - elif "add" in message_lower or "copy" in message_lower: - return "file_operations" - else: - return "security_best_practices" - elif any(term in rule_lower for term in ["dl1", "dl2"]): - return "syntax_errors" - elif "3001" in rule or "3002" in rule: - return "user_management" - elif "3008" in rule or "3009" in rule: - return "privilege_escalation" - elif "3014" in rule or "3015" in rule: - return "port_management" - elif "3020" in rule or "3021" in rule: - return "copy_operations" - else: - return "dockerfile_best_practices" - - def _get_recommendation(self, rule: str, message: str) -> str: - """Generate recommendation based on Hadolint rule""" - recommendations = { - # Security-focused recommendations - "DL3002": "Create a non-root user and switch to it before running the application.", - "DL3008": "Pin package versions to ensure reproducible builds and avoid supply chain attacks.", - "DL3009": "Clean up package manager cache after installation to reduce image size and attack surface.", - "DL3020": "Use COPY instead of ADD for local files to avoid unexpected behavior.", - "DL3025": "Use JSON format for CMD and ENTRYPOINT to avoid shell injection vulnerabilities.", - "DL3059": "Use multi-stage builds to reduce final image size and attack surface.", - "DL4001": "Don't use sudo in Dockerfiles as it's unnecessary and can introduce vulnerabilities.", - "DL4003": "Use a package manager instead of downloading and installing manually.", - "DL4004": "Don't use SSH in Dockerfiles as it's a security risk.", - "DL4005": "Use SHELL instruction to specify shell for RUN commands instead of hardcoding paths.", - } - - if rule in recommendations: - return recommendations[rule] - - # Generic recommendations based on patterns - message_lower = message.lower() - if "user" in message_lower and "root" in message_lower: - return "Avoid running containers as root user. Create and use a non-privileged user." - elif "version" in message_lower or "pin" in message_lower: - return "Pin package versions to specific versions to ensure reproducible builds." - elif "cache" in message_lower or "clean" in message_lower: - return "Clean up package manager caches to reduce image size and potential security issues." - elif "secret" in message_lower or "password" in message_lower: - return "Don't include secrets in Dockerfiles. Use build arguments or runtime secrets instead." - else: - return f"Follow Dockerfile best practices to address rule {rule}." - - def _create_summary(self, findings: List[ModuleFinding], total_files: int) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - rule_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by rule - rule = finding.metadata.get("rule", "unknown") - rule_counts[rule] = rule_counts.get(rule, 0) + 1 - - return { - "total_findings": len(findings), - "files_scanned": total_files, - "severity_counts": severity_counts, - "category_counts": category_counts, - "top_rules": dict(sorted(rule_counts.items(), key=lambda x: x[1], reverse=True)[:10]), - "files_with_issues": len(set(f.file_path for f in findings if f.file_path)) - } \ No newline at end of file diff --git a/backend/toolbox/modules/infrastructure/kubesec.py b/backend/toolbox/modules/infrastructure/kubesec.py deleted file mode 100644 index 76c679c..0000000 --- a/backend/toolbox/modules/infrastructure/kubesec.py +++ /dev/null @@ -1,447 +0,0 @@ -""" -Kubesec Infrastructure Security Module - -This module uses Kubesec to scan Kubernetes manifests for security -misconfigurations and best practices violations. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class KubesecModule(BaseModule): - """Kubesec Kubernetes security scanning module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="kubesec", - version="2.14.0", - description="Kubernetes security scanning for YAML/JSON manifests with security best practices validation", - author="FuzzForge Team", - category="infrastructure", - tags=["kubernetes", "k8s", "security", "best-practices", "manifests"], - input_schema={ - "type": "object", - "properties": { - "scan_mode": { - "type": "string", - "enum": ["scan", "http"], - "default": "scan", - "description": "Kubesec scan mode (local scan or HTTP API)" - }, - "threshold": { - "type": "integer", - "default": 15, - "description": "Minimum security score threshold" - }, - "exit_code": { - "type": "integer", - "default": 0, - "description": "Exit code to return on failure" - }, - "format": { - "type": "string", - "enum": ["json", "template"], - "default": "json", - "description": "Output format" - }, - "kubernetes_patterns": { - "type": "array", - "items": {"type": "string"}, - "default": ["**/*.yaml", "**/*.yml", "**/k8s/*.yaml", "**/kubernetes/*.yaml"], - "description": "Patterns to find Kubernetes manifest files" - }, - "exclude_patterns": { - "type": "array", - "items": {"type": "string"}, - "description": "Patterns to exclude from scanning" - }, - "strict": { - "type": "boolean", - "default": False, - "description": "Enable strict mode (fail on any security issue)" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "score": {"type": "integer"}, - "security_issues": {"type": "array"}, - "file_path": {"type": "string"}, - "manifest_kind": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - scan_mode = config.get("scan_mode", "scan") - if scan_mode not in ["scan", "http"]: - raise ValueError(f"Invalid scan mode: {scan_mode}. Valid: ['scan', 'http']") - - threshold = config.get("threshold", 0) - if not isinstance(threshold, int): - raise ValueError(f"Threshold must be an integer, got: {type(threshold)}") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Kubesec Kubernetes security scanning""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info(f"Running Kubesec Kubernetes scan on {workspace}") - - # Find all Kubernetes manifests - k8s_files = self._find_kubernetes_files(workspace, config) - if not k8s_files: - logger.info("No Kubernetes manifest files found") - return self.create_result( - findings=[], - status="success", - summary={"total_findings": 0, "files_scanned": 0} - ) - - logger.info(f"Found {len(k8s_files)} Kubernetes manifest file(s) to analyze") - - # Process each manifest file - all_findings = [] - for k8s_file in k8s_files: - findings = await self._scan_manifest(k8s_file, workspace, config) - all_findings.extend(findings) - - # Create summary - summary = self._create_summary(all_findings, len(k8s_files)) - - logger.info(f"Kubesec found {len(all_findings)} security issues across {len(k8s_files)} manifests") - - return self.create_result( - findings=all_findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Kubesec module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - def _find_kubernetes_files(self, workspace: Path, config: Dict[str, Any]) -> List[Path]: - """Find Kubernetes manifest files in workspace""" - patterns = config.get("kubernetes_patterns", [ - "**/*.yaml", "**/*.yml", "**/k8s/*.yaml", "**/kubernetes/*.yaml" - ]) - exclude_patterns = config.get("exclude_patterns", []) - - k8s_files = [] - for pattern in patterns: - files = workspace.glob(pattern) - for file in files: - # Check if file contains Kubernetes resources - if self._is_kubernetes_manifest(file): - # Check if file should be excluded - should_exclude = False - for exclude_pattern in exclude_patterns: - if file.match(exclude_pattern): - should_exclude = True - break - if not should_exclude: - k8s_files.append(file) - - return list(set(k8s_files)) # Remove duplicates - - def _is_kubernetes_manifest(self, file: Path) -> bool: - """Check if a file is a Kubernetes manifest""" - try: - content = file.read_text(encoding='utf-8') - # Simple heuristic: check for common Kubernetes fields - k8s_indicators = [ - "apiVersion:", "kind:", "metadata:", "spec:", - "Deployment", "Service", "Pod", "ConfigMap", - "Secret", "Ingress", "PersistentVolume" - ] - return any(indicator in content for indicator in k8s_indicators) - except Exception: - return False - - async def _scan_manifest(self, manifest_file: Path, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]: - """Scan a single Kubernetes manifest with Kubesec""" - findings = [] - - try: - # Build kubesec command - cmd = ["kubesec", "scan"] - - # Add format - format_type = config.get("format", "json") - if format_type == "json": - cmd.append("-f") - cmd.append("json") - - # Add the manifest file - cmd.append(str(manifest_file)) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run kubesec - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results - if process.returncode == 0: - findings = self._parse_kubesec_output( - stdout.decode(), manifest_file, workspace, config - ) - else: - error_msg = stderr.decode() - logger.warning(f"Kubesec failed for {manifest_file}: {error_msg}") - - except Exception as e: - logger.warning(f"Error scanning {manifest_file}: {e}") - - return findings - - def _parse_kubesec_output(self, output: str, manifest_file: Path, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]: - """Parse Kubesec JSON output into findings""" - findings = [] - - if not output.strip(): - return findings - - try: - # Kubesec outputs JSON array - results = json.loads(output) - if not isinstance(results, list): - results = [results] - - threshold = config.get("threshold", 0) - - for result in results: - score = result.get("score", 0) - object_name = result.get("object", "Unknown") - valid = result.get("valid", True) - message = result.get("message", "") - - # Make file path relative to workspace - try: - rel_path = manifest_file.relative_to(workspace) - file_path = str(rel_path) - except ValueError: - file_path = str(manifest_file) - - # Process scoring and advise sections - advise = result.get("advise", []) - scoring = result.get("scoring", {}) - - # Create findings for low scores - if score < threshold or not valid: - severity = "high" if score < 0 else "medium" if score < 5 else "low" - - finding = self.create_finding( - title=f"Kubernetes Security Score Low: {object_name}", - description=message or f"Security score {score} below threshold {threshold}", - severity=severity, - category="kubernetes_security", - file_path=file_path, - recommendation=self._get_score_recommendation(score, advise), - metadata={ - "score": score, - "threshold": threshold, - "object": object_name, - "valid": valid, - "advise_count": len(advise), - "scoring_details": scoring - } - ) - findings.append(finding) - - # Create findings for each advisory - for advisory in advise: - selector = advisory.get("selector", "") - reason = advisory.get("reason", "") - href = advisory.get("href", "") - - # Determine severity based on advisory type - severity = self._get_advisory_severity(reason, selector) - category = self._get_advisory_category(reason, selector) - - finding = self.create_finding( - title=f"Kubernetes Security Advisory: {selector}", - description=reason, - severity=severity, - category=category, - file_path=file_path, - recommendation=self._get_advisory_recommendation(reason, href), - metadata={ - "selector": selector, - "href": href, - "object": object_name, - "advisory_type": "kubesec_advise" - } - ) - findings.append(finding) - - except json.JSONDecodeError as e: - logger.warning(f"Failed to parse Kubesec output: {e}") - except Exception as e: - logger.warning(f"Error processing Kubesec results: {e}") - - return findings - - def _get_advisory_severity(self, reason: str, selector: str) -> str: - """Determine severity based on advisory reason and selector""" - reason_lower = reason.lower() - selector_lower = selector.lower() - - # High severity issues - if any(term in reason_lower for term in [ - "privileged", "root", "hostnetwork", "hostpid", "hostipc", - "allowprivilegeescalation", "runasroot", "security", "capabilities" - ]): - return "high" - - # Medium severity issues - elif any(term in reason_lower for term in [ - "resources", "limits", "requests", "readonly", "securitycontext" - ]): - return "medium" - - # Low severity issues - elif any(term in reason_lower for term in [ - "labels", "annotations", "probe", "liveness", "readiness" - ]): - return "low" - - else: - return "medium" - - def _get_advisory_category(self, reason: str, selector: str) -> str: - """Determine category based on advisory""" - reason_lower = reason.lower() - - if any(term in reason_lower for term in ["privilege", "root", "security", "capabilities"]): - return "privilege_escalation" - elif any(term in reason_lower for term in ["network", "host"]): - return "network_security" - elif any(term in reason_lower for term in ["resources", "limits"]): - return "resource_management" - elif any(term in reason_lower for term in ["probe", "health"]): - return "health_monitoring" - else: - return "kubernetes_best_practices" - - def _get_score_recommendation(self, score: int, advise: List[Dict]) -> str: - """Generate recommendation based on score and advisories""" - if score < 0: - return "Critical security issues detected. Address all security advisories immediately." - elif score < 5: - return "Low security score detected. Review and implement security best practices." - elif len(advise) > 0: - return f"Security score is {score}. Review {len(advise)} advisory recommendations for improvement." - else: - return "Review Kubernetes security configuration and apply security hardening measures." - - def _get_advisory_recommendation(self, reason: str, href: str) -> str: - """Generate recommendation for advisory""" - if href: - return f"{reason} For more details, see: {href}" - - reason_lower = reason.lower() - - # Specific recommendations based on common patterns - if "privileged" in reason_lower: - return "Remove privileged: true from security context. Run containers with minimal privileges." - elif "root" in reason_lower or "runasroot" in reason_lower: - return "Configure runAsNonRoot: true and set runAsUser to a non-root user ID." - elif "allowprivilegeescalation" in reason_lower: - return "Set allowPrivilegeEscalation: false to prevent privilege escalation." - elif "resources" in reason_lower: - return "Define resource requests and limits to prevent resource exhaustion." - elif "readonly" in reason_lower: - return "Set readOnlyRootFilesystem: true to prevent filesystem modifications." - elif "capabilities" in reason_lower: - return "Drop unnecessary capabilities and add only required ones." - elif "probe" in reason_lower: - return "Add liveness and readiness probes for better health monitoring." - else: - return f"Address the security concern: {reason}" - - def _create_summary(self, findings: List[ModuleFinding], total_files: int) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - object_counts = {} - scores = [] - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by object - obj = finding.metadata.get("object", "unknown") - object_counts[obj] = object_counts.get(obj, 0) + 1 - - # Collect scores - score = finding.metadata.get("score") - if score is not None: - scores.append(score) - - return { - "total_findings": len(findings), - "files_scanned": total_files, - "severity_counts": severity_counts, - "category_counts": category_counts, - "object_counts": object_counts, - "average_score": sum(scores) / len(scores) if scores else 0, - "min_score": min(scores) if scores else 0, - "max_score": max(scores) if scores else 0, - "files_with_issues": len(set(f.file_path for f in findings if f.file_path)) - } \ No newline at end of file diff --git a/backend/toolbox/modules/infrastructure/polaris.py b/backend/toolbox/modules/infrastructure/polaris.py deleted file mode 100644 index 68d5f10..0000000 --- a/backend/toolbox/modules/infrastructure/polaris.py +++ /dev/null @@ -1,519 +0,0 @@ -""" -Polaris Infrastructure Security Module - -This module uses Polaris to validate Kubernetes resources against security -and best practice policies. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class PolarisModule(BaseModule): - """Polaris Kubernetes best practices validation module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="polaris", - version="8.5.0", - description="Kubernetes best practices validation and policy enforcement using Polaris", - author="FuzzForge Team", - category="infrastructure", - tags=["kubernetes", "k8s", "policy", "best-practices", "validation"], - input_schema={ - "type": "object", - "properties": { - "audit_path": { - "type": "string", - "description": "Path to audit (defaults to workspace)" - }, - "config_file": { - "type": "string", - "description": "Path to Polaris config file" - }, - "only_show_failed_tests": { - "type": "boolean", - "default": True, - "description": "Show only failed validation tests" - }, - "severity_threshold": { - "type": "string", - "enum": ["error", "warning", "info"], - "default": "info", - "description": "Minimum severity level to report" - }, - "format": { - "type": "string", - "enum": ["json", "yaml", "pretty"], - "default": "json", - "description": "Output format" - }, - "kubernetes_patterns": { - "type": "array", - "items": {"type": "string"}, - "default": ["**/*.yaml", "**/*.yml", "**/k8s/*.yaml", "**/kubernetes/*.yaml"], - "description": "Patterns to find Kubernetes manifest files" - }, - "exclude_patterns": { - "type": "array", - "items": {"type": "string"}, - "description": "File patterns to exclude" - }, - "disable_checks": { - "type": "array", - "items": {"type": "string"}, - "description": "List of check names to disable" - }, - "enable_checks": { - "type": "array", - "items": {"type": "string"}, - "description": "List of check names to enable (if using custom config)" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "check_name": {"type": "string"}, - "severity": {"type": "string"}, - "category": {"type": "string"}, - "file_path": {"type": "string"}, - "resource_name": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - severity_threshold = config.get("severity_threshold", "warning") - valid_severities = ["error", "warning", "info"] - if severity_threshold not in valid_severities: - raise ValueError(f"Invalid severity threshold: {severity_threshold}. Valid: {valid_severities}") - - format_type = config.get("format", "json") - valid_formats = ["json", "yaml", "pretty"] - if format_type not in valid_formats: - raise ValueError(f"Invalid format: {format_type}. Valid: {valid_formats}") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Polaris Kubernetes validation""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info(f"Running Polaris Kubernetes validation on {workspace}") - - # Find all Kubernetes manifests - k8s_files = self._find_kubernetes_files(workspace, config) - if not k8s_files: - logger.info("No Kubernetes manifest files found") - return self.create_result( - findings=[], - status="success", - summary={"total_findings": 0, "files_scanned": 0} - ) - - logger.info(f"Found {len(k8s_files)} Kubernetes manifest file(s) to validate") - - # Run Polaris audit - findings = await self._run_polaris_audit(workspace, config, k8s_files) - - # Create summary - summary = self._create_summary(findings, len(k8s_files)) - - logger.info(f"Polaris found {len(findings)} policy violations across {len(k8s_files)} manifests") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Polaris module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - def _find_kubernetes_files(self, workspace: Path, config: Dict[str, Any]) -> List[Path]: - """Find Kubernetes manifest files in workspace""" - patterns = config.get("kubernetes_patterns", [ - "**/*.yaml", "**/*.yml", "**/k8s/*.yaml", "**/kubernetes/*.yaml" - ]) - exclude_patterns = config.get("exclude_patterns", []) - - k8s_files = [] - for pattern in patterns: - files = workspace.glob(pattern) - for file in files: - # Check if file contains Kubernetes resources - if self._is_kubernetes_manifest(file): - # Check if file should be excluded - should_exclude = False - for exclude_pattern in exclude_patterns: - if file.match(exclude_pattern): - should_exclude = True - break - if not should_exclude: - k8s_files.append(file) - - return list(set(k8s_files)) # Remove duplicates - - def _is_kubernetes_manifest(self, file: Path) -> bool: - """Check if a file is a Kubernetes manifest""" - try: - content = file.read_text(encoding='utf-8') - # Simple heuristic: check for common Kubernetes fields - k8s_indicators = [ - "apiVersion:", "kind:", "metadata:", "spec:", - "Deployment", "Service", "Pod", "ConfigMap", - "Secret", "Ingress", "PersistentVolume" - ] - return any(indicator in content for indicator in k8s_indicators) - except Exception: - return False - - async def _run_polaris_audit(self, workspace: Path, config: Dict[str, Any], k8s_files: List[Path]) -> List[ModuleFinding]: - """Run Polaris audit on workspace""" - findings = [] - - try: - # Build polaris command - cmd = ["polaris", "audit"] - - # Add audit path - audit_path = config.get("audit_path", str(workspace)) - cmd.extend(["--audit-path", audit_path]) - - # Add config file if specified - config_file = config.get("config_file") - if config_file: - cmd.extend(["--config", config_file]) - - # Add format - format_type = config.get("format", "json") - cmd.extend(["--format", format_type]) - - # Add only failed tests flag - if config.get("only_show_failed_tests", True): - cmd.append("--only-show-failed-tests") - - # Add severity threshold - severity_threshold = config.get("severity_threshold", "warning") - cmd.extend(["--severity", severity_threshold]) - - # Add disable checks - disable_checks = config.get("disable_checks", []) - for check in disable_checks: - cmd.extend(["--disable-check", check]) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run polaris - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results - if process.returncode == 0 or format_type == "json": - findings = self._parse_polaris_output(stdout.decode(), workspace, config) - else: - error_msg = stderr.decode() - logger.warning(f"Polaris audit failed: {error_msg}") - - except Exception as e: - logger.warning(f"Error running Polaris audit: {e}") - - return findings - - def _parse_polaris_output(self, output: str, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]: - """Parse Polaris JSON output into findings""" - findings = [] - - if not output.strip(): - return findings - - try: - data = json.loads(output) - - # Get severity threshold for filtering - severity_threshold = config.get("severity_threshold", "warning") - severity_levels = {"error": 3, "warning": 2, "info": 1} - min_severity_level = severity_levels.get(severity_threshold, 2) - - # Process audit results - audit_results = data.get("AuditResults", []) - - for result in audit_results: - namespace = result.get("Namespace", "default") - results_by_kind = result.get("Results", {}) - - for kind, kind_results in results_by_kind.items(): - for resource_name, resource_data in kind_results.items(): - # Get container results - container_results = resource_data.get("ContainerResults", {}) - pod_result = resource_data.get("PodResult", {}) - - # Process container results - for container_name, container_data in container_results.items(): - self._process_container_results( - findings, container_data, kind, resource_name, - container_name, namespace, workspace, min_severity_level - ) - - # Process pod-level results - if pod_result: - self._process_pod_results( - findings, pod_result, kind, resource_name, - namespace, workspace, min_severity_level - ) - - except json.JSONDecodeError as e: - logger.warning(f"Failed to parse Polaris output: {e}") - except Exception as e: - logger.warning(f"Error processing Polaris results: {e}") - - return findings - - def _process_container_results(self, findings: List[ModuleFinding], container_data: Dict, - kind: str, resource_name: str, container_name: str, - namespace: str, workspace: Path, min_severity_level: int): - """Process container-level validation results""" - results = container_data.get("Results", {}) - - for check_name, check_result in results.items(): - severity = check_result.get("Severity", "warning") - success = check_result.get("Success", True) - message = check_result.get("Message", "") - category_name = check_result.get("Category", "") - - # Skip if check passed or severity too low - if success: - continue - - severity_levels = {"error": 3, "warning": 2, "info": 1} - if severity_levels.get(severity, 1) < min_severity_level: - continue - - # Map severity to our standard levels - finding_severity = self._map_severity(severity) - category = self._get_category(check_name, category_name) - - finding = self.create_finding( - title=f"Polaris Policy Violation: {check_name}", - description=message or f"Container {container_name} in {kind} {resource_name} failed check {check_name}", - severity=finding_severity, - category=category, - file_path=None, # Polaris doesn't provide file paths in audit mode - recommendation=self._get_recommendation(check_name, message), - metadata={ - "check_name": check_name, - "polaris_severity": severity, - "polaris_category": category_name, - "resource_kind": kind, - "resource_name": resource_name, - "container_name": container_name, - "namespace": namespace, - "context": "container" - } - ) - findings.append(finding) - - def _process_pod_results(self, findings: List[ModuleFinding], pod_result: Dict, - kind: str, resource_name: str, namespace: str, - workspace: Path, min_severity_level: int): - """Process pod-level validation results""" - results = pod_result.get("Results", {}) - - for check_name, check_result in results.items(): - severity = check_result.get("Severity", "warning") - success = check_result.get("Success", True) - message = check_result.get("Message", "") - category_name = check_result.get("Category", "") - - # Skip if check passed or severity too low - if success: - continue - - severity_levels = {"error": 3, "warning": 2, "info": 1} - if severity_levels.get(severity, 1) < min_severity_level: - continue - - # Map severity to our standard levels - finding_severity = self._map_severity(severity) - category = self._get_category(check_name, category_name) - - finding = self.create_finding( - title=f"Polaris Policy Violation: {check_name}", - description=message or f"{kind} {resource_name} failed check {check_name}", - severity=finding_severity, - category=category, - file_path=None, # Polaris doesn't provide file paths in audit mode - recommendation=self._get_recommendation(check_name, message), - metadata={ - "check_name": check_name, - "polaris_severity": severity, - "polaris_category": category_name, - "resource_kind": kind, - "resource_name": resource_name, - "namespace": namespace, - "context": "pod" - } - ) - findings.append(finding) - - def _map_severity(self, polaris_severity: str) -> str: - """Map Polaris severity to our standard severity levels""" - severity_map = { - "error": "high", - "warning": "medium", - "info": "low" - } - return severity_map.get(polaris_severity.lower(), "medium") - - def _get_category(self, check_name: str, category_name: str) -> str: - """Determine finding category based on check name and category""" - check_lower = check_name.lower() - category_lower = category_name.lower() - - # Use Polaris category if available - if "security" in category_lower: - return "security_configuration" - elif "efficiency" in category_lower: - return "resource_efficiency" - elif "reliability" in category_lower: - return "reliability" - - # Fallback to check name analysis - if any(term in check_lower for term in ["security", "privilege", "root", "capabilities"]): - return "security_configuration" - elif any(term in check_lower for term in ["resources", "limits", "requests"]): - return "resource_management" - elif any(term in check_lower for term in ["probe", "health", "liveness", "readiness"]): - return "health_monitoring" - elif any(term in check_lower for term in ["image", "tag", "pull"]): - return "image_management" - elif any(term in check_lower for term in ["network", "host"]): - return "network_security" - else: - return "kubernetes_best_practices" - - def _get_recommendation(self, check_name: str, message: str) -> str: - """Generate recommendation based on check name and message""" - check_lower = check_name.lower() - - # Security-related recommendations - if "privileged" in check_lower: - return "Remove privileged: true from container security context to reduce security risks." - elif "runasroot" in check_lower: - return "Configure runAsNonRoot: true and specify a non-root user ID." - elif "allowprivilegeescalation" in check_lower: - return "Set allowPrivilegeEscalation: false to prevent privilege escalation attacks." - elif "capabilities" in check_lower: - return "Remove unnecessary capabilities and add only required ones using drop/add lists." - elif "readonly" in check_lower: - return "Set readOnlyRootFilesystem: true to prevent filesystem modifications." - - # Resource management recommendations - elif "memory" in check_lower and "requests" in check_lower: - return "Set memory requests to ensure proper resource allocation and scheduling." - elif "memory" in check_lower and "limits" in check_lower: - return "Set memory limits to prevent containers from using excessive memory." - elif "cpu" in check_lower and "requests" in check_lower: - return "Set CPU requests for proper resource allocation and quality of service." - elif "cpu" in check_lower and "limits" in check_lower: - return "Set CPU limits to prevent CPU starvation of other containers." - - # Health monitoring recommendations - elif "liveness" in check_lower: - return "Add liveness probes to detect and recover from container failures." - elif "readiness" in check_lower: - return "Add readiness probes to ensure containers are ready before receiving traffic." - - # Image management recommendations - elif "tag" in check_lower: - return "Use specific image tags instead of 'latest' for reproducible deployments." - elif "pullpolicy" in check_lower: - return "Set imagePullPolicy appropriately based on your deployment requirements." - - # Generic recommendation - elif message: - return f"Address the policy violation: {message}" - else: - return f"Review and fix the configuration issue identified by check: {check_name}" - - def _create_summary(self, findings: List[ModuleFinding], total_files: int) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - check_counts = {} - resource_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by check - check_name = finding.metadata.get("check_name", "unknown") - check_counts[check_name] = check_counts.get(check_name, 0) + 1 - - # Count by resource - resource_kind = finding.metadata.get("resource_kind", "unknown") - resource_counts[resource_kind] = resource_counts.get(resource_kind, 0) + 1 - - return { - "total_findings": len(findings), - "files_scanned": total_files, - "severity_counts": severity_counts, - "category_counts": category_counts, - "top_checks": dict(sorted(check_counts.items(), key=lambda x: x[1], reverse=True)[:10]), - "resource_type_counts": resource_counts, - "unique_resources": len(set(f"{f.metadata.get('resource_kind')}:{f.metadata.get('resource_name')}" for f in findings)), - "namespaces": len(set(f.metadata.get("namespace", "default") for f in findings)) - } \ No newline at end of file diff --git a/backend/toolbox/modules/penetration_testing/__init__.py b/backend/toolbox/modules/penetration_testing/__init__.py deleted file mode 100644 index 3e09d19..0000000 --- a/backend/toolbox/modules/penetration_testing/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Penetration Testing Modules - -This package contains modules for penetration testing and vulnerability assessment. - -Available modules: -- Nuclei: Fast and customizable vulnerability scanner -- Nmap: Network discovery and security auditing -- Masscan: High-speed Internet-wide port scanner -- SQLMap: Automatic SQL injection detection and exploitation -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -from typing import List, Type -from ..base import BaseModule - -# Module registry for automatic discovery -PENETRATION_TESTING_MODULES: List[Type[BaseModule]] = [] - -def register_module(module_class: Type[BaseModule]): - """Register a penetration testing module""" - PENETRATION_TESTING_MODULES.append(module_class) - return module_class - -def get_available_modules() -> List[Type[BaseModule]]: - """Get all available penetration testing modules""" - return PENETRATION_TESTING_MODULES.copy() - -# Import modules to trigger registration -from .nuclei import NucleiModule -from .nmap import NmapModule -from .masscan import MasscanModule -from .sqlmap import SQLMapModule \ No newline at end of file diff --git a/backend/toolbox/modules/penetration_testing/masscan.py b/backend/toolbox/modules/penetration_testing/masscan.py deleted file mode 100644 index 3452168..0000000 --- a/backend/toolbox/modules/penetration_testing/masscan.py +++ /dev/null @@ -1,607 +0,0 @@ -""" -Masscan Penetration Testing Module - -This module uses Masscan for high-speed Internet-wide port scanning. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class MasscanModule(BaseModule): - """Masscan high-speed port scanner module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="masscan", - version="1.3.2", - description="High-speed Internet-wide port scanner for large-scale network discovery", - author="FuzzForge Team", - category="penetration_testing", - tags=["port-scan", "network", "discovery", "high-speed", "mass-scan"], - input_schema={ - "type": "object", - "properties": { - "targets": { - "type": "array", - "items": {"type": "string"}, - "description": "List of targets (IP addresses, CIDR ranges, domains)" - }, - "target_file": { - "type": "string", - "description": "File containing targets to scan" - }, - "ports": { - "type": "string", - "default": "1-1000", - "description": "Port range or specific ports to scan" - }, - "top_ports": { - "type": "integer", - "description": "Scan top N most common ports" - }, - "rate": { - "type": "integer", - "default": 1000, - "description": "Packet transmission rate (packets/second)" - }, - "max_rate": { - "type": "integer", - "description": "Maximum packet rate limit" - }, - "connection_timeout": { - "type": "integer", - "default": 10, - "description": "Connection timeout in seconds" - }, - "wait_time": { - "type": "integer", - "default": 10, - "description": "Time to wait for responses (seconds)" - }, - "retries": { - "type": "integer", - "default": 0, - "description": "Number of retries for failed connections" - }, - "randomize_hosts": { - "type": "boolean", - "default": True, - "description": "Randomize host order" - }, - "source_ip": { - "type": "string", - "description": "Source IP address to use" - }, - "source_port": { - "type": "string", - "description": "Source port range to use" - }, - "interface": { - "type": "string", - "description": "Network interface to use" - }, - "router_mac": { - "type": "string", - "description": "Router MAC address" - }, - "exclude_targets": { - "type": "array", - "items": {"type": "string"}, - "description": "Targets to exclude from scanning" - }, - "exclude_file": { - "type": "string", - "description": "File containing targets to exclude" - }, - "ping": { - "type": "boolean", - "default": False, - "description": "Include ping scan" - }, - "banners": { - "type": "boolean", - "default": False, - "description": "Grab banners from services" - }, - "http_user_agent": { - "type": "string", - "description": "HTTP User-Agent string for banner grabbing" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "host": {"type": "string"}, - "port": {"type": "integer"}, - "protocol": {"type": "string"}, - "state": {"type": "string"}, - "banner": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - targets = config.get("targets", []) - target_file = config.get("target_file") - - if not targets and not target_file: - raise ValueError("Either 'targets' or 'target_file' must be specified") - - rate = config.get("rate", 1000) - if rate <= 0 or rate > 10000000: # Masscan limit - raise ValueError("Rate must be between 1 and 10,000,000 packets/second") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Masscan port scanning""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running Masscan high-speed port scan") - - # Prepare target specification - target_args = self._prepare_targets(config, workspace) - if not target_args: - logger.info("No targets specified for scanning") - return self.create_result( - findings=[], - status="success", - summary={"total_findings": 0, "targets_scanned": 0} - ) - - # Run Masscan scan - findings = await self._run_masscan_scan(target_args, config, workspace) - - # Create summary - target_count = len(config.get("targets", [])) if config.get("targets") else 1 - summary = self._create_summary(findings, target_count) - - logger.info(f"Masscan found {len(findings)} open ports") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Masscan module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - def _prepare_targets(self, config: Dict[str, Any], workspace: Path) -> List[str]: - """Prepare target arguments for masscan""" - target_args = [] - - # Add targets from list - targets = config.get("targets", []) - for target in targets: - target_args.extend(["-t", target]) - - # Add targets from file - target_file = config.get("target_file") - if target_file: - target_path = workspace / target_file - if target_path.exists(): - target_args.extend(["-iL", str(target_path)]) - else: - raise FileNotFoundError(f"Target file not found: {target_file}") - - return target_args - - async def _run_masscan_scan(self, target_args: List[str], config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run Masscan scan""" - findings = [] - - try: - # Build masscan command - cmd = ["masscan"] - - # Add target arguments - cmd.extend(target_args) - - # Add port specification - if config.get("top_ports"): - # Masscan doesn't have built-in top ports, use common ports - top_ports = self._get_top_ports(config["top_ports"]) - cmd.extend(["-p", top_ports]) - else: - ports = config.get("ports", "1-1000") - cmd.extend(["-p", ports]) - - # Add rate limiting - rate = config.get("rate", 1000) - cmd.extend(["--rate", str(rate)]) - - # Add max rate if specified - max_rate = config.get("max_rate") - if max_rate: - cmd.extend(["--max-rate", str(max_rate)]) - - # Add connection timeout - connection_timeout = config.get("connection_timeout", 10) - cmd.extend(["--connection-timeout", str(connection_timeout)]) - - # Add wait time - wait_time = config.get("wait_time", 10) - cmd.extend(["--wait", str(wait_time)]) - - # Add retries - retries = config.get("retries", 0) - if retries > 0: - cmd.extend(["--retries", str(retries)]) - - # Add randomization - if config.get("randomize_hosts", True): - cmd.append("--randomize-hosts") - - # Add source IP - source_ip = config.get("source_ip") - if source_ip: - cmd.extend(["--source-ip", source_ip]) - - # Add source port - source_port = config.get("source_port") - if source_port: - cmd.extend(["--source-port", source_port]) - - # Add interface - interface = config.get("interface") - if interface: - cmd.extend(["-e", interface]) - - # Add router MAC - router_mac = config.get("router_mac") - if router_mac: - cmd.extend(["--router-mac", router_mac]) - - # Add exclude targets - exclude_targets = config.get("exclude_targets", []) - for exclude in exclude_targets: - cmd.extend(["--exclude", exclude]) - - # Add exclude file - exclude_file = config.get("exclude_file") - if exclude_file: - exclude_path = workspace / exclude_file - if exclude_path.exists(): - cmd.extend(["--excludefile", str(exclude_path)]) - - # Add ping scan - if config.get("ping", False): - cmd.append("--ping") - - # Add banner grabbing - if config.get("banners", False): - cmd.append("--banners") - - # Add HTTP User-Agent - user_agent = config.get("http_user_agent") - if user_agent: - cmd.extend(["--http-user-agent", user_agent]) - - # Set output format to JSON - output_file = workspace / "masscan_results.json" - cmd.extend(["-oJ", str(output_file)]) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run masscan - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results from JSON file - if output_file.exists(): - findings = self._parse_masscan_json(output_file, workspace) - else: - # Try to parse stdout if no file was created - if stdout: - findings = self._parse_masscan_output(stdout.decode(), workspace) - else: - error_msg = stderr.decode() - logger.error(f"Masscan scan failed: {error_msg}") - - except Exception as e: - logger.warning(f"Error running Masscan scan: {e}") - - return findings - - def _get_top_ports(self, count: int) -> str: - """Get top N common ports for masscan""" - # Common ports based on Nmap's top ports list - top_ports = [ - 80, 23, 443, 21, 22, 25, 53, 110, 111, 995, 993, 143, 993, 995, 587, 465, - 109, 88, 53, 135, 139, 445, 993, 995, 143, 25, 110, 465, 587, 993, 995, - 80, 8080, 443, 8443, 8000, 8888, 8880, 2222, 9999, 3389, 5900, 5901, - 1433, 3306, 5432, 1521, 50000, 1494, 554, 37, 79, 82, 5060, 50030 - ] - - # Take first N unique ports - selected_ports = list(dict.fromkeys(top_ports))[:count] - return ",".join(map(str, selected_ports)) - - def _parse_masscan_json(self, json_file: Path, workspace: Path) -> List[ModuleFinding]: - """Parse Masscan JSON output into findings""" - findings = [] - - try: - with open(json_file, 'r') as f: - content = f.read().strip() - - # Masscan outputs JSONL format (one JSON object per line) - for line in content.split('\n'): - if not line.strip(): - continue - - try: - result = json.loads(line) - finding = self._process_masscan_result(result) - if finding: - findings.append(finding) - except json.JSONDecodeError: - continue - - except Exception as e: - logger.warning(f"Error parsing Masscan JSON: {e}") - - return findings - - def _parse_masscan_output(self, output: str, workspace: Path) -> List[ModuleFinding]: - """Parse Masscan text output into findings""" - findings = [] - - try: - for line in output.split('\n'): - if not line.strip() or line.startswith('#'): - continue - - # Parse format: "open tcp 80 1.2.3.4" - parts = line.split() - if len(parts) >= 4 and parts[0] == "open": - protocol = parts[1] - port = int(parts[2]) - ip = parts[3] - - result = { - "ip": ip, - "ports": [{"port": port, "proto": protocol, "status": "open"}] - } - - finding = self._process_masscan_result(result) - if finding: - findings.append(finding) - - except Exception as e: - logger.warning(f"Error parsing Masscan output: {e}") - - return findings - - def _process_masscan_result(self, result: Dict) -> ModuleFinding: - """Process a single Masscan result into a finding""" - try: - ip_address = result.get("ip", "") - ports_data = result.get("ports", []) - - if not ip_address or not ports_data: - return None - - # Process first port (Masscan typically reports one port per result) - port_data = ports_data[0] - port_number = port_data.get("port", 0) - protocol = port_data.get("proto", "tcp") - status = port_data.get("status", "open") - service = port_data.get("service", {}) - banner = service.get("banner", "") if service else "" - - # Only report open ports - if status != "open": - return None - - # Determine severity based on port - severity = self._get_port_severity(port_number) - - # Get category - category = self._get_port_category(port_number) - - # Create description - description = f"Open port {port_number}/{protocol} on {ip_address}" - if banner: - description += f" (Banner: {banner[:100]})" - - # Create finding - finding = self.create_finding( - title=f"Open Port: {port_number}/{protocol}", - description=description, - severity=severity, - category=category, - file_path=None, # Network scan, no file - recommendation=self._get_port_recommendation(port_number, banner), - metadata={ - "host": ip_address, - "port": port_number, - "protocol": protocol, - "status": status, - "banner": banner, - "service_info": service - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error processing Masscan result: {e}") - return None - - def _get_port_severity(self, port: int) -> str: - """Determine severity based on port number""" - # High risk ports (commonly exploited or sensitive services) - high_risk_ports = [21, 23, 135, 139, 445, 1433, 1521, 3389, 5900, 6379, 27017] - - # Medium risk ports (network services that could be risky if misconfigured) - medium_risk_ports = [22, 25, 53, 110, 143, 993, 995, 3306, 5432] - - # Web ports are generally lower risk but still noteworthy - web_ports = [80, 443, 8080, 8443, 8000, 8888] - - if port in high_risk_ports: - return "high" - elif port in medium_risk_ports: - return "medium" - elif port in web_ports: - return "low" - elif port < 1024: # Well-known ports - return "medium" - else: - return "low" - - def _get_port_category(self, port: int) -> str: - """Determine category based on port number""" - if port in [80, 443, 8080, 8443, 8000, 8888]: - return "web_services" - elif port == 22: - return "remote_access" - elif port in [20, 21]: - return "file_transfer" - elif port in [25, 110, 143, 587, 993, 995]: - return "email_services" - elif port in [1433, 3306, 5432, 1521, 27017, 6379]: - return "database_services" - elif port == 3389: - return "remote_desktop" - elif port == 53: - return "dns_services" - elif port in [135, 139, 445]: - return "windows_services" - elif port in [23, 5900]: - return "insecure_protocols" - else: - return "network_services" - - def _get_port_recommendation(self, port: int, banner: str) -> str: - """Generate recommendation based on port and banner""" - # Port-specific recommendations - recommendations = { - 21: "FTP service detected. Consider using SFTP instead for secure file transfer.", - 22: "SSH service detected. Ensure strong authentication and key-based access.", - 23: "Telnet service detected. Replace with SSH for secure remote access.", - 25: "SMTP service detected. Ensure proper authentication and encryption.", - 53: "DNS service detected. Verify it's not an open resolver.", - 80: "HTTP service detected. Consider upgrading to HTTPS.", - 110: "POP3 service detected. Consider using secure alternatives like IMAPS.", - 135: "Windows RPC service exposed. Restrict access if not required.", - 139: "NetBIOS service detected. Ensure proper access controls.", - 143: "IMAP service detected. Consider using encrypted IMAPS.", - 445: "SMB service detected. Ensure latest patches and access controls.", - 443: "HTTPS service detected. Verify SSL/TLS configuration.", - 993: "IMAPS service detected. Verify certificate configuration.", - 995: "POP3S service detected. Verify certificate configuration.", - 1433: "SQL Server detected. Ensure strong authentication and network restrictions.", - 1521: "Oracle DB detected. Ensure proper security configuration.", - 3306: "MySQL service detected. Secure with strong passwords and access controls.", - 3389: "RDP service detected. Use strong passwords and consider VPN access.", - 5432: "PostgreSQL detected. Ensure proper authentication and access controls.", - 5900: "VNC service detected. Use strong passwords and encryption.", - 6379: "Redis service detected. Configure authentication and access controls.", - 8080: "HTTP proxy/web service detected. Verify if exposure is intended.", - 8443: "HTTPS service on non-standard port. Verify certificate configuration." - } - - recommendation = recommendations.get(port, f"Port {port} is open. Verify if this service is required and properly secured.") - - # Add banner-specific advice - if banner: - banner_lower = banner.lower() - if "default" in banner_lower or "admin" in banner_lower: - recommendation += " Default credentials may be in use - change immediately." - elif any(version in banner_lower for version in ["1.0", "2.0", "old", "legacy"]): - recommendation += " Service version appears outdated - consider upgrading." - - return recommendation - - def _create_summary(self, findings: List[ModuleFinding], targets_count: int) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - port_counts = {} - host_counts = {} - protocol_counts = {"tcp": 0, "udp": 0} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by port - port = finding.metadata.get("port") - if port: - port_counts[port] = port_counts.get(port, 0) + 1 - - # Count by host - host = finding.metadata.get("host", "unknown") - host_counts[host] = host_counts.get(host, 0) + 1 - - # Count by protocol - protocol = finding.metadata.get("protocol", "tcp") - if protocol in protocol_counts: - protocol_counts[protocol] += 1 - - return { - "total_findings": len(findings), - "targets_scanned": targets_count, - "severity_counts": severity_counts, - "category_counts": category_counts, - "protocol_counts": protocol_counts, - "unique_hosts": len(host_counts), - "top_ports": dict(sorted(port_counts.items(), key=lambda x: x[1], reverse=True)[:10]), - "host_counts": dict(sorted(host_counts.items(), key=lambda x: x[1], reverse=True)[:10]) - } \ No newline at end of file diff --git a/backend/toolbox/modules/penetration_testing/nmap.py b/backend/toolbox/modules/penetration_testing/nmap.py deleted file mode 100644 index 4cfb363..0000000 --- a/backend/toolbox/modules/penetration_testing/nmap.py +++ /dev/null @@ -1,710 +0,0 @@ -""" -Nmap Penetration Testing Module - -This module uses Nmap for network discovery, port scanning, and security auditing. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -import xml.etree.ElementTree as ET -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class NmapModule(BaseModule): - """Nmap network discovery and security auditing module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="nmap", - version="7.94", - description="Network discovery and security auditing using Nmap", - author="FuzzForge Team", - category="penetration_testing", - tags=["network", "port-scan", "discovery", "security-audit", "service-detection"], - input_schema={ - "type": "object", - "properties": { - "targets": { - "type": "array", - "items": {"type": "string"}, - "description": "List of targets (IP addresses, domains, CIDR ranges)" - }, - "target_file": { - "type": "string", - "description": "File containing targets to scan" - }, - "scan_type": { - "type": "string", - "enum": ["syn", "tcp", "udp", "ack", "window", "maimon"], - "default": "syn", - "description": "Type of scan to perform" - }, - "ports": { - "type": "string", - "default": "1-1000", - "description": "Port range or specific ports to scan" - }, - "top_ports": { - "type": "integer", - "description": "Scan top N most common ports" - }, - "service_detection": { - "type": "boolean", - "default": True, - "description": "Enable service version detection" - }, - "os_detection": { - "type": "boolean", - "default": False, - "description": "Enable OS detection (requires root)" - }, - "script_scan": { - "type": "boolean", - "default": True, - "description": "Enable default NSE scripts" - }, - "scripts": { - "type": "array", - "items": {"type": "string"}, - "description": "Specific NSE scripts to run" - }, - "script_categories": { - "type": "array", - "items": {"type": "string"}, - "description": "NSE script categories to run (safe, vuln, etc.)" - }, - "timing_template": { - "type": "string", - "enum": ["paranoid", "sneaky", "polite", "normal", "aggressive", "insane"], - "default": "normal", - "description": "Timing template (0-5)" - }, - "max_retries": { - "type": "integer", - "default": 1, - "description": "Maximum number of retries" - }, - "host_timeout": { - "type": "integer", - "default": 300, - "description": "Host timeout in seconds" - }, - "min_rate": { - "type": "integer", - "description": "Minimum packet rate (packets/second)" - }, - "max_rate": { - "type": "integer", - "description": "Maximum packet rate (packets/second)" - }, - "stealth": { - "type": "boolean", - "default": False, - "description": "Enable stealth scanning options" - }, - "skip_discovery": { - "type": "boolean", - "default": False, - "description": "Skip host discovery (treat all as online)" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "host": {"type": "string"}, - "port": {"type": "integer"}, - "service": {"type": "string"}, - "state": {"type": "string"}, - "version": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - targets = config.get("targets", []) - target_file = config.get("target_file") - - if not targets and not target_file: - raise ValueError("Either 'targets' or 'target_file' must be specified") - - scan_type = config.get("scan_type", "syn") - valid_scan_types = ["syn", "tcp", "udp", "ack", "window", "maimon"] - if scan_type not in valid_scan_types: - raise ValueError(f"Invalid scan type: {scan_type}. Valid: {valid_scan_types}") - - timing = config.get("timing_template", "normal") - valid_timings = ["paranoid", "sneaky", "polite", "normal", "aggressive", "insane"] - if timing not in valid_timings: - raise ValueError(f"Invalid timing template: {timing}. Valid: {valid_timings}") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Nmap network scanning""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running Nmap network scan") - - # Prepare target file - target_file = await self._prepare_targets(config, workspace) - if not target_file: - logger.info("No targets specified for scanning") - return self.create_result( - findings=[], - status="success", - summary={"total_findings": 0, "hosts_scanned": 0} - ) - - # Run Nmap scan - findings = await self._run_nmap_scan(target_file, config, workspace) - - # Create summary - target_count = len(config.get("targets", [])) if config.get("targets") else 1 - summary = self._create_summary(findings, target_count) - - logger.info(f"Nmap found {len(findings)} results") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Nmap module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _prepare_targets(self, config: Dict[str, Any], workspace: Path) -> Path: - """Prepare target file for scanning""" - targets = config.get("targets", []) - target_file = config.get("target_file") - - if target_file: - # Use existing target file - target_path = workspace / target_file - if target_path.exists(): - return target_path - else: - raise FileNotFoundError(f"Target file not found: {target_file}") - - if targets: - # Create temporary target file - target_path = workspace / "nmap_targets.txt" - with open(target_path, 'w') as f: - for target in targets: - f.write(f"{target}\n") - return target_path - - return None - - async def _run_nmap_scan(self, target_file: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run Nmap scan""" - findings = [] - - try: - # Build nmap command - cmd = ["nmap"] - - # Add scan type - scan_type = config.get("scan_type", "syn") - scan_type_map = { - "syn": "-sS", - "tcp": "-sT", - "udp": "-sU", - "ack": "-sA", - "window": "-sW", - "maimon": "-sM" - } - cmd.append(scan_type_map[scan_type]) - - # Add port specification - if config.get("top_ports"): - cmd.extend(["--top-ports", str(config["top_ports"])]) - else: - ports = config.get("ports", "1-1000") - cmd.extend(["-p", ports]) - - # Add service detection - if config.get("service_detection", True): - cmd.append("-sV") - - # Add OS detection - if config.get("os_detection", False): - cmd.append("-O") - - # Add script scanning - if config.get("script_scan", True): - cmd.append("-sC") - - # Add specific scripts - scripts = config.get("scripts", []) - if scripts: - cmd.extend(["--script", ",".join(scripts)]) - - # Add script categories - script_categories = config.get("script_categories", []) - if script_categories: - cmd.extend(["--script", ",".join(script_categories)]) - - # Add timing template - timing = config.get("timing_template", "normal") - timing_map = { - "paranoid": "-T0", - "sneaky": "-T1", - "polite": "-T2", - "normal": "-T3", - "aggressive": "-T4", - "insane": "-T5" - } - cmd.append(timing_map[timing]) - - # Add retry options - max_retries = config.get("max_retries", 1) - cmd.extend(["--max-retries", str(max_retries)]) - - # Add timeout - host_timeout = config.get("host_timeout", 300) - cmd.extend(["--host-timeout", f"{host_timeout}s"]) - - # Add rate limiting - if config.get("min_rate"): - cmd.extend(["--min-rate", str(config["min_rate"])]) - - if config.get("max_rate"): - cmd.extend(["--max-rate", str(config["max_rate"])]) - - # Add stealth options - if config.get("stealth", False): - cmd.extend(["-f", "--randomize-hosts"]) - - # Skip host discovery if requested - if config.get("skip_discovery", False): - cmd.append("-Pn") - - # Add output format - output_file = workspace / "nmap_results.xml" - cmd.extend(["-oX", str(output_file)]) - - # Add targets from file - cmd.extend(["-iL", str(target_file)]) - - # Add verbose and reason flags - cmd.extend(["-v", "--reason"]) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run nmap - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results from XML file - if output_file.exists(): - findings = self._parse_nmap_xml(output_file, workspace) - else: - error_msg = stderr.decode() - logger.error(f"Nmap scan failed: {error_msg}") - - except Exception as e: - logger.warning(f"Error running Nmap scan: {e}") - - return findings - - def _parse_nmap_xml(self, xml_file: Path, workspace: Path) -> List[ModuleFinding]: - """Parse Nmap XML output into findings""" - findings = [] - - try: - tree = ET.parse(xml_file) - root = tree.getroot() - - # Process each host - for host_elem in root.findall(".//host"): - # Get host information - host_status = host_elem.find("status") - if host_status is None or host_status.get("state") != "up": - continue - - # Get IP address - address_elem = host_elem.find("address[@addrtype='ipv4']") - if address_elem is None: - address_elem = host_elem.find("address[@addrtype='ipv6']") - - if address_elem is None: - continue - - ip_address = address_elem.get("addr") - - # Get hostname if available - hostname = "" - hostnames_elem = host_elem.find("hostnames") - if hostnames_elem is not None: - hostname_elem = hostnames_elem.find("hostname") - if hostname_elem is not None: - hostname = hostname_elem.get("name", "") - - # Get OS information - os_info = self._extract_os_info(host_elem) - - # Process ports - ports_elem = host_elem.find("ports") - if ports_elem is not None: - for port_elem in ports_elem.findall("port"): - finding = self._process_port(port_elem, ip_address, hostname, os_info) - if finding: - findings.append(finding) - - # Process host scripts - host_scripts = host_elem.find("hostscript") - if host_scripts is not None: - for script_elem in host_scripts.findall("script"): - finding = self._process_host_script(script_elem, ip_address, hostname) - if finding: - findings.append(finding) - - except ET.ParseError as e: - logger.warning(f"Failed to parse Nmap XML: {e}") - except Exception as e: - logger.warning(f"Error processing Nmap results: {e}") - - return findings - - def _extract_os_info(self, host_elem) -> Dict[str, Any]: - """Extract OS information from host element""" - os_info = {} - - os_elem = host_elem.find("os") - if os_elem is not None: - osmatch_elem = os_elem.find("osmatch") - if osmatch_elem is not None: - os_info["name"] = osmatch_elem.get("name", "") - os_info["accuracy"] = osmatch_elem.get("accuracy", "0") - - return os_info - - def _process_port(self, port_elem, ip_address: str, hostname: str, os_info: Dict) -> ModuleFinding: - """Process a port element into a finding""" - try: - port_id = port_elem.get("portid") - protocol = port_elem.get("protocol") - - # Get state - state_elem = port_elem.find("state") - if state_elem is None: - return None - - state = state_elem.get("state") - reason = state_elem.get("reason", "") - - # Only report open ports - if state != "open": - return None - - # Get service information - service_elem = port_elem.find("service") - service_name = "" - service_version = "" - service_product = "" - service_extra = "" - - if service_elem is not None: - service_name = service_elem.get("name", "") - service_version = service_elem.get("version", "") - service_product = service_elem.get("product", "") - service_extra = service_elem.get("extrainfo", "") - - # Determine severity based on service - severity = self._get_port_severity(int(port_id), service_name) - - # Get category - category = self._get_port_category(int(port_id), service_name) - - # Create description - desc_parts = [f"Open port {port_id}/{protocol}"] - if service_name: - desc_parts.append(f"running {service_name}") - if service_product: - desc_parts.append(f"({service_product}") - if service_version: - desc_parts.append(f"version {service_version}") - desc_parts.append(")") - - description = " ".join(desc_parts) - - # Process port scripts - script_results = [] - script_elems = port_elem.findall("script") - for script_elem in script_elems: - script_id = script_elem.get("id", "") - script_output = script_elem.get("output", "") - if script_output: - script_results.append({"id": script_id, "output": script_output}) - - # Create finding - finding = self.create_finding( - title=f"Open Port: {port_id}/{protocol}", - description=description, - severity=severity, - category=category, - file_path=None, # Network scan, no file - recommendation=self._get_port_recommendation(int(port_id), service_name, script_results), - metadata={ - "host": ip_address, - "hostname": hostname, - "port": int(port_id), - "protocol": protocol, - "state": state, - "reason": reason, - "service_name": service_name, - "service_version": service_version, - "service_product": service_product, - "service_extra": service_extra, - "os_info": os_info, - "script_results": script_results - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error processing port: {e}") - return None - - def _process_host_script(self, script_elem, ip_address: str, hostname: str) -> ModuleFinding: - """Process a host script result into a finding""" - try: - script_id = script_elem.get("id", "") - script_output = script_elem.get("output", "") - - if not script_output or not script_id: - return None - - # Determine if this is a security issue - severity = self._get_script_severity(script_id, script_output) - - if severity == "info": - # Skip informational scripts - return None - - category = self._get_script_category(script_id) - - finding = self.create_finding( - title=f"Host Script Result: {script_id}", - description=script_output.strip(), - severity=severity, - category=category, - file_path=None, - recommendation=self._get_script_recommendation(script_id, script_output), - metadata={ - "host": ip_address, - "hostname": hostname, - "script_id": script_id, - "script_output": script_output.strip() - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error processing host script: {e}") - return None - - def _get_port_severity(self, port: int, service: str) -> str: - """Determine severity based on port and service""" - # High risk ports - high_risk_ports = [21, 23, 135, 139, 445, 1433, 1521, 3389, 5432, 5900, 6379] - # Medium risk ports - medium_risk_ports = [22, 25, 53, 110, 143, 993, 995] - # Web ports are generally lower risk - web_ports = [80, 443, 8080, 8443, 8000, 8888] - - if port in high_risk_ports: - return "high" - elif port in medium_risk_ports: - return "medium" - elif port in web_ports: - return "low" - elif port < 1024: # Well-known ports - return "medium" - else: - return "low" - - def _get_port_category(self, port: int, service: str) -> str: - """Determine category based on port and service""" - service_lower = service.lower() - - if service_lower in ["http", "https"] or port in [80, 443, 8080, 8443]: - return "web_services" - elif service_lower in ["ssh"] or port == 22: - return "remote_access" - elif service_lower in ["ftp", "ftps"] or port in [20, 21]: - return "file_transfer" - elif service_lower in ["smtp", "pop3", "imap"] or port in [25, 110, 143, 587, 993, 995]: - return "email_services" - elif service_lower in ["mysql", "postgresql", "mssql", "oracle"] or port in [1433, 3306, 5432, 1521]: - return "database_services" - elif service_lower in ["rdp"] or port == 3389: - return "remote_desktop" - elif service_lower in ["dns"] or port == 53: - return "dns_services" - elif port in [135, 139, 445]: - return "windows_services" - else: - return "network_services" - - def _get_script_severity(self, script_id: str, output: str) -> str: - """Determine severity for script results""" - script_lower = script_id.lower() - output_lower = output.lower() - - # High severity indicators - if any(term in script_lower for term in ["vuln", "exploit", "backdoor"]): - return "high" - if any(term in output_lower for term in ["vulnerable", "exploit", "critical"]): - return "high" - - # Medium severity indicators - if any(term in script_lower for term in ["auth", "brute", "enum"]): - return "medium" - if any(term in output_lower for term in ["anonymous", "default", "weak"]): - return "medium" - - # Everything else is informational - return "info" - - def _get_script_category(self, script_id: str) -> str: - """Determine category for script results""" - script_lower = script_id.lower() - - if "vuln" in script_lower: - return "vulnerability_detection" - elif "auth" in script_lower or "brute" in script_lower: - return "authentication_testing" - elif "enum" in script_lower: - return "information_gathering" - elif "ssl" in script_lower or "tls" in script_lower: - return "ssl_tls_testing" - else: - return "service_detection" - - def _get_port_recommendation(self, port: int, service: str, scripts: List[Dict]) -> str: - """Generate recommendation for open port""" - # Check for script-based issues - for script in scripts: - script_id = script.get("id", "") - if "vuln" in script_id.lower(): - return "Vulnerability detected by NSE scripts. Review and patch the service." - - # Port-specific recommendations - if port == 21: - return "FTP service detected. Consider using SFTP instead for secure file transfer." - elif port == 23: - return "Telnet service detected. Use SSH instead for secure remote access." - elif port == 135: - return "Windows RPC service exposed. Restrict access if not required." - elif port in [139, 445]: - return "SMB/NetBIOS services detected. Ensure proper access controls and patch levels." - elif port == 1433: - return "SQL Server detected. Ensure strong authentication and network restrictions." - elif port == 3389: - return "RDP service detected. Use strong passwords and consider VPN access." - elif port in [80, 443]: - return "Web service detected. Ensure regular security updates and proper configuration." - else: - return f"Open port {port} detected. Verify if this service is required and properly secured." - - def _get_script_recommendation(self, script_id: str, output: str) -> str: - """Generate recommendation for script results""" - if "vuln" in script_id.lower(): - return "Vulnerability detected. Apply security patches and updates." - elif "auth" in script_id.lower(): - return "Authentication issue detected. Review and strengthen authentication mechanisms." - elif "ssl" in script_id.lower(): - return "SSL/TLS configuration issue. Update SSL configuration and certificates." - else: - return "Review the script output and address any security concerns identified." - - def _create_summary(self, findings: List[ModuleFinding], hosts_count: int) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - port_counts = {} - service_counts = {} - host_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by port - port = finding.metadata.get("port") - if port: - port_counts[port] = port_counts.get(port, 0) + 1 - - # Count by service - service = finding.metadata.get("service_name", "unknown") - service_counts[service] = service_counts.get(service, 0) + 1 - - # Count by host - host = finding.metadata.get("host", "unknown") - host_counts[host] = host_counts.get(host, 0) + 1 - - return { - "total_findings": len(findings), - "hosts_scanned": hosts_count, - "severity_counts": severity_counts, - "category_counts": category_counts, - "unique_hosts": len(host_counts), - "top_ports": dict(sorted(port_counts.items(), key=lambda x: x[1], reverse=True)[:10]), - "top_services": dict(sorted(service_counts.items(), key=lambda x: x[1], reverse=True)[:10]), - "host_counts": dict(sorted(host_counts.items(), key=lambda x: x[1], reverse=True)[:5]) - } \ No newline at end of file diff --git a/backend/toolbox/modules/penetration_testing/nuclei.py b/backend/toolbox/modules/penetration_testing/nuclei.py deleted file mode 100644 index 8114960..0000000 --- a/backend/toolbox/modules/penetration_testing/nuclei.py +++ /dev/null @@ -1,501 +0,0 @@ -""" -Nuclei Penetration Testing Module - -This module uses Nuclei to perform fast and customizable vulnerability scanning -using community-powered templates. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class NucleiModule(BaseModule): - """Nuclei fast vulnerability scanner module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="nuclei", - version="3.1.0", - description="Fast and customizable vulnerability scanner using community-powered templates", - author="FuzzForge Team", - category="penetration_testing", - tags=["vulnerability", "scanner", "web", "network", "templates"], - input_schema={ - "type": "object", - "properties": { - "targets": { - "type": "array", - "items": {"type": "string"}, - "description": "List of targets (URLs, domains, IP addresses)" - }, - "target_file": { - "type": "string", - "description": "File containing targets to scan" - }, - "templates": { - "type": "array", - "items": {"type": "string"}, - "description": "Specific templates to use" - }, - "template_directory": { - "type": "string", - "description": "Directory containing custom templates" - }, - "tags": { - "type": "array", - "items": {"type": "string"}, - "description": "Template tags to include" - }, - "exclude_tags": { - "type": "array", - "items": {"type": "string"}, - "description": "Template tags to exclude" - }, - "severity": { - "type": "array", - "items": {"type": "string", "enum": ["critical", "high", "medium", "low", "info"]}, - "default": ["critical", "high", "medium"], - "description": "Severity levels to include" - }, - "concurrency": { - "type": "integer", - "default": 25, - "description": "Number of concurrent threads" - }, - "rate_limit": { - "type": "integer", - "default": 150, - "description": "Rate limit (requests per second)" - }, - "timeout": { - "type": "integer", - "default": 10, - "description": "Timeout for requests (seconds)" - }, - "retries": { - "type": "integer", - "default": 1, - "description": "Number of retries for failed requests" - }, - "update_templates": { - "type": "boolean", - "default": False, - "description": "Update templates before scanning" - }, - "disable_clustering": { - "type": "boolean", - "default": False, - "description": "Disable template clustering" - }, - "no_interactsh": { - "type": "boolean", - "default": True, - "description": "Disable interactsh server for OAST testing" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "template_id": {"type": "string"}, - "name": {"type": "string"}, - "severity": {"type": "string"}, - "host": {"type": "string"}, - "matched_at": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - targets = config.get("targets", []) - target_file = config.get("target_file") - - if not targets and not target_file: - raise ValueError("Either 'targets' or 'target_file' must be specified") - - severity_levels = config.get("severity", []) - valid_severities = ["critical", "high", "medium", "low", "info"] - for severity in severity_levels: - if severity not in valid_severities: - raise ValueError(f"Invalid severity: {severity}. Valid: {valid_severities}") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Nuclei vulnerability scanning""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running Nuclei vulnerability scan") - - # Update templates if requested - if config.get("update_templates", False): - await self._update_templates(workspace) - - # Prepare target file - target_file = await self._prepare_targets(config, workspace) - if not target_file: - logger.info("No targets specified for scanning") - return self.create_result( - findings=[], - status="success", - summary={"total_findings": 0, "targets_scanned": 0} - ) - - # Run Nuclei scan - findings = await self._run_nuclei_scan(target_file, config, workspace) - - # Create summary - summary = self._create_summary(findings, len(config.get("targets", []))) - - logger.info(f"Nuclei found {len(findings)} vulnerabilities") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Nuclei module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _update_templates(self, workspace: Path): - """Update Nuclei templates""" - try: - logger.info("Updating Nuclei templates...") - cmd = ["nuclei", "-update-templates"] - - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - if process.returncode == 0: - logger.info("Templates updated successfully") - else: - logger.warning(f"Template update failed: {stderr.decode()}") - - except Exception as e: - logger.warning(f"Error updating templates: {e}") - - async def _prepare_targets(self, config: Dict[str, Any], workspace: Path) -> Path: - """Prepare target file for scanning""" - targets = config.get("targets", []) - target_file = config.get("target_file") - - if target_file: - # Use existing target file - target_path = workspace / target_file - if target_path.exists(): - return target_path - else: - raise FileNotFoundError(f"Target file not found: {target_file}") - - if targets: - # Create temporary target file - target_path = workspace / "nuclei_targets.txt" - with open(target_path, 'w') as f: - for target in targets: - f.write(f"{target}\n") - return target_path - - return None - - async def _run_nuclei_scan(self, target_file: Path, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run Nuclei scan""" - findings = [] - - try: - # Build nuclei command - cmd = ["nuclei", "-l", str(target_file)] - - # Add output format - cmd.extend(["-json"]) - - # Add templates - templates = config.get("templates", []) - if templates: - cmd.extend(["-t", ",".join(templates)]) - - # Add template directory - template_dir = config.get("template_directory") - if template_dir: - cmd.extend(["-t", template_dir]) - - # Add tags - tags = config.get("tags", []) - if tags: - cmd.extend(["-tags", ",".join(tags)]) - - # Add exclude tags - exclude_tags = config.get("exclude_tags", []) - if exclude_tags: - cmd.extend(["-exclude-tags", ",".join(exclude_tags)]) - - # Add severity - severity_levels = config.get("severity", ["critical", "high", "medium"]) - cmd.extend(["-severity", ",".join(severity_levels)]) - - # Add concurrency - concurrency = config.get("concurrency", 25) - cmd.extend(["-c", str(concurrency)]) - - # Add rate limit - rate_limit = config.get("rate_limit", 150) - cmd.extend(["-rl", str(rate_limit)]) - - # Add timeout - timeout = config.get("timeout", 10) - cmd.extend(["-timeout", str(timeout)]) - - # Add retries - retries = config.get("retries", 1) - cmd.extend(["-retries", str(retries)]) - - # Add other flags - if config.get("disable_clustering", False): - cmd.append("-no-color") - - if config.get("no_interactsh", True): - cmd.append("-no-interactsh") - - # Add silent flag for JSON output - cmd.append("-silent") - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run nuclei - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results - if process.returncode == 0 or stdout: - findings = self._parse_nuclei_output(stdout.decode(), workspace) - else: - error_msg = stderr.decode() - logger.error(f"Nuclei scan failed: {error_msg}") - - except Exception as e: - logger.warning(f"Error running Nuclei scan: {e}") - - return findings - - def _parse_nuclei_output(self, output: str, workspace: Path) -> List[ModuleFinding]: - """Parse Nuclei JSON output into findings""" - findings = [] - - if not output.strip(): - return findings - - try: - # Parse each line as JSON (JSONL format) - for line in output.strip().split('\n'): - if not line.strip(): - continue - - result = json.loads(line) - - # Extract information - template_id = result.get("template-id", "") - template_name = result.get("info", {}).get("name", "") - severity = result.get("info", {}).get("severity", "medium") - host = result.get("host", "") - matched_at = result.get("matched-at", "") - description = result.get("info", {}).get("description", "") - reference = result.get("info", {}).get("reference", []) - classification = result.get("info", {}).get("classification", {}) - extracted_results = result.get("extracted-results", []) - - # Map severity to our standard levels - finding_severity = self._map_severity(severity) - - # Get category based on template - category = self._get_category(template_id, template_name, classification) - - # Create finding - finding = self.create_finding( - title=f"Nuclei Detection: {template_name}", - description=description or f"Vulnerability detected using template {template_id}", - severity=finding_severity, - category=category, - file_path=None, # Nuclei scans network targets - recommendation=self._get_recommendation(template_id, template_name, reference), - metadata={ - "template_id": template_id, - "template_name": template_name, - "nuclei_severity": severity, - "host": host, - "matched_at": matched_at, - "classification": classification, - "reference": reference, - "extracted_results": extracted_results - } - ) - findings.append(finding) - - except json.JSONDecodeError as e: - logger.warning(f"Failed to parse Nuclei output: {e}") - except Exception as e: - logger.warning(f"Error processing Nuclei results: {e}") - - return findings - - def _map_severity(self, nuclei_severity: str) -> str: - """Map Nuclei severity to our standard severity levels""" - severity_map = { - "critical": "critical", - "high": "high", - "medium": "medium", - "low": "low", - "info": "info" - } - return severity_map.get(nuclei_severity.lower(), "medium") - - def _get_category(self, template_id: str, template_name: str, classification: Dict) -> str: - """Determine finding category based on template and classification""" - template_lower = f"{template_id} {template_name}".lower() - - # Use classification if available - cwe_id = classification.get("cwe-id") - if cwe_id: - # Map common CWE IDs to categories - if cwe_id in ["CWE-79", "CWE-80"]: - return "cross_site_scripting" - elif cwe_id in ["CWE-89"]: - return "sql_injection" - elif cwe_id in ["CWE-22", "CWE-23"]: - return "path_traversal" - elif cwe_id in ["CWE-352"]: - return "csrf" - elif cwe_id in ["CWE-601"]: - return "redirect" - - # Analyze template content - if any(term in template_lower for term in ["xss", "cross-site"]): - return "cross_site_scripting" - elif any(term in template_lower for term in ["sql", "injection"]): - return "sql_injection" - elif any(term in template_lower for term in ["lfi", "rfi", "file", "path", "traversal"]): - return "file_inclusion" - elif any(term in template_lower for term in ["rce", "command", "execution"]): - return "remote_code_execution" - elif any(term in template_lower for term in ["auth", "login", "bypass"]): - return "authentication_bypass" - elif any(term in template_lower for term in ["disclosure", "exposure", "leak"]): - return "information_disclosure" - elif any(term in template_lower for term in ["config", "misconfiguration"]): - return "misconfiguration" - elif any(term in template_lower for term in ["cve-"]): - return "known_vulnerability" - else: - return "web_vulnerability" - - def _get_recommendation(self, template_id: str, template_name: str, references: List) -> str: - """Generate recommendation based on template""" - # Use references if available - if references: - ref_text = ", ".join(references[:3]) # Limit to first 3 references - return f"Review the vulnerability and apply appropriate fixes. References: {ref_text}" - - # Generate based on template type - template_lower = f"{template_id} {template_name}".lower() - - if "xss" in template_lower: - return "Implement proper input validation and output encoding to prevent XSS attacks." - elif "sql" in template_lower: - return "Use parameterized queries and input validation to prevent SQL injection." - elif "lfi" in template_lower or "rfi" in template_lower: - return "Validate and sanitize file paths. Avoid dynamic file includes with user input." - elif "rce" in template_lower: - return "Sanitize user input and avoid executing system commands with user-controlled data." - elif "auth" in template_lower: - return "Review authentication mechanisms and implement proper access controls." - elif "exposure" in template_lower or "disclosure" in template_lower: - return "Restrict access to sensitive information and implement proper authorization." - elif "cve-" in template_lower: - return "Update the affected software to the latest version to patch known vulnerabilities." - else: - return f"Review and remediate the security issue identified by template {template_id}." - - def _create_summary(self, findings: List[ModuleFinding], targets_count: int) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - category_counts = {} - template_counts = {} - host_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by template - template_id = finding.metadata.get("template_id", "unknown") - template_counts[template_id] = template_counts.get(template_id, 0) + 1 - - # Count by host - host = finding.metadata.get("host", "unknown") - host_counts[host] = host_counts.get(host, 0) + 1 - - return { - "total_findings": len(findings), - "targets_scanned": targets_count, - "severity_counts": severity_counts, - "category_counts": category_counts, - "top_templates": dict(sorted(template_counts.items(), key=lambda x: x[1], reverse=True)[:10]), - "affected_hosts": len(host_counts), - "host_counts": dict(sorted(host_counts.items(), key=lambda x: x[1], reverse=True)[:10]) - } \ No newline at end of file diff --git a/backend/toolbox/modules/penetration_testing/sqlmap.py b/backend/toolbox/modules/penetration_testing/sqlmap.py deleted file mode 100644 index 84d888b..0000000 --- a/backend/toolbox/modules/penetration_testing/sqlmap.py +++ /dev/null @@ -1,671 +0,0 @@ -""" -SQLMap Penetration Testing Module - -This module uses SQLMap for automatic SQL injection detection and exploitation. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class SQLMapModule(BaseModule): - """SQLMap automatic SQL injection detection and exploitation module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="sqlmap", - version="1.7.11", - description="Automatic SQL injection detection and exploitation tool", - author="FuzzForge Team", - category="penetration_testing", - tags=["sql-injection", "web", "database", "vulnerability", "exploitation"], - input_schema={ - "type": "object", - "properties": { - "target_url": { - "type": "string", - "description": "Target URL to test for SQL injection" - }, - "target_file": { - "type": "string", - "description": "File containing URLs to test" - }, - "request_file": { - "type": "string", - "description": "Load HTTP request from file (Burp log, etc.)" - }, - "data": { - "type": "string", - "description": "Data string to be sent through POST" - }, - "cookie": { - "type": "string", - "description": "HTTP Cookie header value" - }, - "user_agent": { - "type": "string", - "description": "HTTP User-Agent header value" - }, - "referer": { - "type": "string", - "description": "HTTP Referer header value" - }, - "headers": { - "type": "object", - "description": "Additional HTTP headers" - }, - "method": { - "type": "string", - "enum": ["GET", "POST", "PUT", "DELETE", "PATCH"], - "default": "GET", - "description": "HTTP method to use" - }, - "testable_parameters": { - "type": "array", - "items": {"type": "string"}, - "description": "Comma-separated list of testable parameter(s)" - }, - "skip_parameters": { - "type": "array", - "items": {"type": "string"}, - "description": "Parameters to skip during testing" - }, - "dbms": { - "type": "string", - "enum": ["mysql", "postgresql", "oracle", "mssql", "sqlite", "access", "firebird", "sybase", "db2", "hsqldb", "h2"], - "description": "Force back-end DBMS to provided value" - }, - "level": { - "type": "integer", - "enum": [1, 2, 3, 4, 5], - "default": 1, - "description": "Level of tests to perform (1-5)" - }, - "risk": { - "type": "integer", - "enum": [1, 2, 3], - "default": 1, - "description": "Risk of tests to perform (1-3)" - }, - "technique": { - "type": "array", - "items": {"type": "string", "enum": ["B", "E", "U", "S", "T", "Q"]}, - "description": "SQL injection techniques to use (B=Boolean, E=Error, U=Union, S=Stacked, T=Time, Q=Inline)" - }, - "time_sec": { - "type": "integer", - "default": 5, - "description": "Seconds to delay DBMS response for time-based blind SQL injection" - }, - "union_cols": { - "type": "string", - "description": "Range of columns to test for UNION query SQL injection" - }, - "threads": { - "type": "integer", - "default": 1, - "description": "Maximum number of concurrent HTTP requests" - }, - "timeout": { - "type": "integer", - "default": 30, - "description": "Seconds to wait before timeout connection" - }, - "retries": { - "type": "integer", - "default": 3, - "description": "Retries when connection timeouts" - }, - "randomize": { - "type": "boolean", - "default": True, - "description": "Randomly change value of given parameter(s)" - }, - "safe_url": { - "type": "string", - "description": "URL to visit frequently during testing" - }, - "safe_freq": { - "type": "integer", - "description": "Test requests between visits to safe URL" - }, - "crawl": { - "type": "integer", - "description": "Crawl website starting from target URL (depth)" - }, - "forms": { - "type": "boolean", - "default": False, - "description": "Parse and test forms on target URL" - }, - "batch": { - "type": "boolean", - "default": True, - "description": "Never ask for user input, use default behavior" - }, - "cleanup": { - "type": "boolean", - "default": True, - "description": "Clean up files used by SQLMap" - }, - "check_waf": { - "type": "boolean", - "default": False, - "description": "Check for existence of WAF/IPS protection" - }, - "tamper": { - "type": "array", - "items": {"type": "string"}, - "description": "Use tamper scripts to modify requests" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "url": {"type": "string"}, - "parameter": {"type": "string"}, - "technique": {"type": "string"}, - "dbms": {"type": "string"}, - "payload": {"type": "string"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - target_url = config.get("target_url") - target_file = config.get("target_file") - request_file = config.get("request_file") - - if not any([target_url, target_file, request_file]): - raise ValueError("Either 'target_url', 'target_file', or 'request_file' must be specified") - - level = config.get("level", 1) - if level not in [1, 2, 3, 4, 5]: - raise ValueError("Level must be between 1 and 5") - - risk = config.get("risk", 1) - if risk not in [1, 2, 3]: - raise ValueError("Risk must be between 1 and 3") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute SQLMap SQL injection testing""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info("Running SQLMap SQL injection scan") - - # Run SQLMap scan - findings = await self._run_sqlmap_scan(config, workspace) - - # Create summary - summary = self._create_summary(findings) - - logger.info(f"SQLMap found {len(findings)} SQL injection vulnerabilities") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"SQLMap module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - async def _run_sqlmap_scan(self, config: Dict[str, Any], workspace: Path) -> List[ModuleFinding]: - """Run SQLMap scan""" - findings = [] - - try: - # Build sqlmap command - cmd = ["sqlmap"] - - # Add target specification - target_url = config.get("target_url") - if target_url: - cmd.extend(["-u", target_url]) - - target_file = config.get("target_file") - if target_file: - target_path = workspace / target_file - if target_path.exists(): - cmd.extend(["-m", str(target_path)]) - else: - raise FileNotFoundError(f"Target file not found: {target_file}") - - request_file = config.get("request_file") - if request_file: - request_path = workspace / request_file - if request_path.exists(): - cmd.extend(["-r", str(request_path)]) - else: - raise FileNotFoundError(f"Request file not found: {request_file}") - - # Add HTTP options - data = config.get("data") - if data: - cmd.extend(["--data", data]) - - cookie = config.get("cookie") - if cookie: - cmd.extend(["--cookie", cookie]) - - user_agent = config.get("user_agent") - if user_agent: - cmd.extend(["--user-agent", user_agent]) - - referer = config.get("referer") - if referer: - cmd.extend(["--referer", referer]) - - headers = config.get("headers", {}) - for key, value in headers.items(): - cmd.extend(["--header", f"{key}: {value}"]) - - method = config.get("method") - if method and method != "GET": - cmd.extend(["--method", method]) - - # Add parameter options - testable_params = config.get("testable_parameters", []) - if testable_params: - cmd.extend(["-p", ",".join(testable_params)]) - - skip_params = config.get("skip_parameters", []) - if skip_params: - cmd.extend(["--skip", ",".join(skip_params)]) - - # Add injection options - dbms = config.get("dbms") - if dbms: - cmd.extend(["--dbms", dbms]) - - level = config.get("level", 1) - cmd.extend(["--level", str(level)]) - - risk = config.get("risk", 1) - cmd.extend(["--risk", str(risk)]) - - techniques = config.get("technique", []) - if techniques: - cmd.extend(["--technique", "".join(techniques)]) - - time_sec = config.get("time_sec", 5) - cmd.extend(["--time-sec", str(time_sec)]) - - union_cols = config.get("union_cols") - if union_cols: - cmd.extend(["--union-cols", union_cols]) - - # Add performance options - threads = config.get("threads", 1) - cmd.extend(["--threads", str(threads)]) - - timeout = config.get("timeout", 30) - cmd.extend(["--timeout", str(timeout)]) - - retries = config.get("retries", 3) - cmd.extend(["--retries", str(retries)]) - - # Add request options - if config.get("randomize", True): - cmd.append("--randomize") - - safe_url = config.get("safe_url") - if safe_url: - cmd.extend(["--safe-url", safe_url]) - - safe_freq = config.get("safe_freq") - if safe_freq: - cmd.extend(["--safe-freq", str(safe_freq)]) - - # Add crawling options - crawl_depth = config.get("crawl") - if crawl_depth: - cmd.extend(["--crawl", str(crawl_depth)]) - - if config.get("forms", False): - cmd.append("--forms") - - # Add behavioral options - if config.get("batch", True): - cmd.append("--batch") - - if config.get("cleanup", True): - cmd.append("--cleanup") - - if config.get("check_waf", False): - cmd.append("--check-waf") - - # Add tamper scripts - tamper_scripts = config.get("tamper", []) - if tamper_scripts: - cmd.extend(["--tamper", ",".join(tamper_scripts)]) - - # Set output directory - output_dir = workspace / "sqlmap_output" - output_dir.mkdir(exist_ok=True) - cmd.extend(["--output-dir", str(output_dir)]) - - # Add format for easier parsing - cmd.append("--flush-session") # Start fresh - cmd.append("--fresh-queries") # Ignore previous results - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run sqlmap - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results from output directory - findings = self._parse_sqlmap_output(output_dir, stdout.decode(), workspace) - - # Log results - if findings: - logger.info(f"SQLMap detected {len(findings)} SQL injection vulnerabilities") - else: - logger.info("No SQL injection vulnerabilities found") - # Check for errors - stderr_text = stderr.decode() - if stderr_text: - logger.warning(f"SQLMap warnings/errors: {stderr_text}") - - except Exception as e: - logger.warning(f"Error running SQLMap scan: {e}") - - return findings - - def _parse_sqlmap_output(self, output_dir: Path, stdout: str, workspace: Path) -> List[ModuleFinding]: - """Parse SQLMap output into findings""" - findings = [] - - try: - # Look for session files in output directory - session_files = list(output_dir.glob("**/*.sqlite")) - log_files = list(output_dir.glob("**/*.log")) - - # Parse stdout for injection information - findings.extend(self._parse_stdout_output(stdout)) - - # Parse log files for additional details - for log_file in log_files: - findings.extend(self._parse_log_file(log_file)) - - # If we have session files, we can extract more detailed information - # For now, we'll rely on stdout parsing - - except Exception as e: - logger.warning(f"Error parsing SQLMap output: {e}") - - return findings - - def _parse_stdout_output(self, stdout: str) -> List[ModuleFinding]: - """Parse SQLMap stdout for SQL injection findings""" - findings = [] - - try: - lines = stdout.split('\n') - current_url = None - current_parameter = None - current_technique = None - current_dbms = None - injection_found = False - - for line in lines: - line = line.strip() - - # Extract URL being tested - if "testing URL" in line or "testing connection to the target URL" in line: - # Extract URL from line - if "'" in line: - url_start = line.find("'") + 1 - url_end = line.find("'", url_start) - if url_end > url_start: - current_url = line[url_start:url_end] - - # Extract parameter being tested - elif "testing parameter" in line or "testing" in line and "parameter" in line: - if "'" in line: - param_parts = line.split("'") - if len(param_parts) >= 2: - current_parameter = param_parts[1] - - # Detect SQL injection found - elif any(indicator in line.lower() for indicator in [ - "parameter appears to be vulnerable", - "injectable", - "parameter is vulnerable" - ]): - injection_found = True - - # Extract technique information - elif "Type:" in line: - current_technique = line.replace("Type:", "").strip() - - # Extract database information - elif "back-end DBMS:" in line.lower(): - current_dbms = line.split(":")[-1].strip() - - # Extract payload information - elif "Payload:" in line: - payload = line.replace("Payload:", "").strip() - - # Create finding if we have injection - if injection_found and current_url and current_parameter: - finding = self._create_sqlmap_finding( - current_url, current_parameter, current_technique, - current_dbms, payload - ) - if finding: - findings.append(finding) - - # Reset state - injection_found = False - current_technique = None - - except Exception as e: - logger.warning(f"Error parsing SQLMap stdout: {e}") - - return findings - - def _parse_log_file(self, log_file: Path) -> List[ModuleFinding]: - """Parse SQLMap log file for additional findings""" - findings = [] - - try: - with open(log_file, 'r') as f: - content = f.read() - - # Look for injection indicators in log - if "injectable" in content.lower() or "vulnerable" in content.lower(): - # Could parse more detailed information from log - # For now, we'll rely on stdout parsing - pass - - except Exception as e: - logger.warning(f"Error parsing log file {log_file}: {e}") - - return findings - - def _create_sqlmap_finding(self, url: str, parameter: str, technique: str, dbms: str, payload: str) -> ModuleFinding: - """Create a ModuleFinding for SQL injection""" - try: - # Map technique to readable description - technique_map = { - "boolean-based blind": "Boolean-based blind SQL injection", - "time-based blind": "Time-based blind SQL injection", - "error-based": "Error-based SQL injection", - "UNION query": "UNION-based SQL injection", - "stacked queries": "Stacked queries SQL injection", - "inline query": "Inline query SQL injection" - } - - technique_desc = technique_map.get(technique, technique or "SQL injection") - - # Create description - description = f"SQL injection vulnerability detected in parameter '{parameter}' using {technique_desc}" - if dbms: - description += f" against {dbms} database" - - # Determine severity based on technique - severity = self._get_injection_severity(technique, dbms) - - # Create finding - finding = self.create_finding( - title=f"SQL Injection: {parameter}", - description=description, - severity=severity, - category="sql_injection", - file_path=None, # Web application testing - recommendation=self._get_sqlinjection_recommendation(technique, dbms), - metadata={ - "url": url, - "parameter": parameter, - "technique": technique, - "dbms": dbms, - "payload": payload[:500] if payload else "", # Limit payload length - "injection_type": technique_desc - } - ) - - return finding - - except Exception as e: - logger.warning(f"Error creating SQLMap finding: {e}") - return None - - def _get_injection_severity(self, technique: str, dbms: str) -> str: - """Determine severity based on injection technique and database""" - if not technique: - return "high" # Any SQL injection is serious - - technique_lower = technique.lower() - - # Critical severity for techniques that allow easy data extraction - if any(term in technique_lower for term in ["union", "error-based"]): - return "critical" - - # High severity for techniques that allow some data extraction - elif any(term in technique_lower for term in ["boolean-based", "time-based"]): - return "high" - - # Stacked queries are very dangerous as they allow multiple statements - elif "stacked" in technique_lower: - return "critical" - - else: - return "high" - - def _get_sqlinjection_recommendation(self, technique: str, dbms: str) -> str: - """Generate recommendation for SQL injection""" - base_recommendation = "Implement parameterized queries/prepared statements and input validation to prevent SQL injection attacks." - - if technique: - technique_lower = technique.lower() - if "union" in technique_lower: - base_recommendation += " The UNION-based injection allows direct data extraction - immediate remediation required." - elif "error-based" in technique_lower: - base_recommendation += " Error-based injection reveals database structure - disable error messages in production." - elif "time-based" in technique_lower: - base_recommendation += " Time-based injection allows blind data extraction - implement query timeout limits." - elif "stacked" in technique_lower: - base_recommendation += " Stacked queries injection allows multiple SQL statements - extremely dangerous, fix immediately." - - if dbms: - dbms_lower = dbms.lower() - if "mysql" in dbms_lower: - base_recommendation += " For MySQL: disable LOAD_FILE and INTO OUTFILE if not needed." - elif "postgresql" in dbms_lower: - base_recommendation += " For PostgreSQL: review user privileges and disable unnecessary functions." - elif "mssql" in dbms_lower: - base_recommendation += " For SQL Server: disable xp_cmdshell and review extended stored procedures." - - return base_recommendation - - def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} - technique_counts = {} - dbms_counts = {} - parameter_counts = {} - url_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by technique - technique = finding.metadata.get("technique", "unknown") - technique_counts[technique] = technique_counts.get(technique, 0) + 1 - - # Count by DBMS - dbms = finding.metadata.get("dbms", "unknown") - if dbms != "unknown": - dbms_counts[dbms] = dbms_counts.get(dbms, 0) + 1 - - # Count by parameter - parameter = finding.metadata.get("parameter", "unknown") - parameter_counts[parameter] = parameter_counts.get(parameter, 0) + 1 - - # Count by URL - url = finding.metadata.get("url", "unknown") - url_counts[url] = url_counts.get(url, 0) + 1 - - return { - "total_findings": len(findings), - "severity_counts": severity_counts, - "technique_counts": technique_counts, - "dbms_counts": dbms_counts, - "vulnerable_parameters": list(parameter_counts.keys()), - "vulnerable_urls": len(url_counts), - "most_common_techniques": dict(sorted(technique_counts.items(), key=lambda x: x[1], reverse=True)[:5]), - "affected_databases": list(dbms_counts.keys()) - } \ No newline at end of file diff --git a/backend/toolbox/modules/static_analysis/__init__.py b/backend/toolbox/modules/static_analysis/__init__.py deleted file mode 100644 index 274a1a2..0000000 --- a/backend/toolbox/modules/static_analysis/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Static Analysis Security Testing (SAST) Modules - -This package contains modules for static code analysis and security testing. - -Available modules: -- CodeQL: GitHub's semantic code analysis engine -- SonarQube: Code quality and security analysis platform -- Snyk: Vulnerability scanning for dependencies and code -- OpenGrep: Open-source pattern-based static analysis tool -- Bandit: Python-specific security issue identifier -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -from typing import List, Type -from ..base import BaseModule - -# Module registry for automatic discovery -STATIC_ANALYSIS_MODULES: List[Type[BaseModule]] = [] - -def register_module(module_class: Type[BaseModule]): - """Register a static analysis module""" - STATIC_ANALYSIS_MODULES.append(module_class) - return module_class - -def get_available_modules() -> List[Type[BaseModule]]: - """Get all available static analysis modules""" - return STATIC_ANALYSIS_MODULES.copy() \ No newline at end of file diff --git a/backend/toolbox/modules/static_analysis/bandit.py b/backend/toolbox/modules/static_analysis/bandit.py deleted file mode 100644 index 2ceff4f..0000000 --- a/backend/toolbox/modules/static_analysis/bandit.py +++ /dev/null @@ -1,418 +0,0 @@ -""" -Bandit Static Analysis Module - -This module uses Bandit to detect security vulnerabilities in Python code. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class BanditModule(BaseModule): - """Bandit Python security analysis module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="bandit", - version="1.7.5", - description="Python-specific security issue identifier using Bandit", - author="FuzzForge Team", - category="static_analysis", - tags=["python", "sast", "security", "vulnerabilities"], - input_schema={ - "type": "object", - "properties": { - "confidence": { - "type": "string", - "enum": ["LOW", "MEDIUM", "HIGH"], - "default": "LOW", - "description": "Minimum confidence level for reported issues" - }, - "severity": { - "type": "string", - "enum": ["LOW", "MEDIUM", "HIGH"], - "default": "LOW", - "description": "Minimum severity level for reported issues" - }, - "tests": { - "type": "array", - "items": {"type": "string"}, - "description": "Specific test IDs to run" - }, - "skips": { - "type": "array", - "items": {"type": "string"}, - "description": "Test IDs to skip" - }, - "exclude_dirs": { - "type": "array", - "items": {"type": "string"}, - "default": ["tests", "test", ".git", "__pycache__"], - "description": "Directories to exclude from analysis" - }, - "include_patterns": { - "type": "array", - "items": {"type": "string"}, - "default": ["*.py"], - "description": "File patterns to include" - }, - "aggregate": { - "type": "string", - "enum": ["file", "vuln"], - "default": "file", - "description": "How to aggregate results" - }, - "context_lines": { - "type": "integer", - "default": 3, - "description": "Number of context lines to show" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "test_id": {"type": "string"}, - "test_name": {"type": "string"}, - "confidence": {"type": "string"}, - "severity": {"type": "string"}, - "file_path": {"type": "string"}, - "line_number": {"type": "integer"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - confidence = config.get("confidence", "LOW") - # Handle both string and list formats - if isinstance(confidence, list): - confidence = confidence[0] if confidence else "MEDIUM" - if confidence not in ["LOW", "MEDIUM", "HIGH"]: - raise ValueError("confidence must be LOW, MEDIUM, or HIGH") - - severity = config.get("severity", "LOW") - # Handle both string and list formats - if isinstance(severity, list): - severity = severity[0] if severity else "MEDIUM" - if severity not in ["LOW", "MEDIUM", "HIGH"]: - raise ValueError("severity must be LOW, MEDIUM, or HIGH") - - context_lines = config.get("context_lines", 3) - if not isinstance(context_lines, int) or context_lines < 0 or context_lines > 10: - raise ValueError("context_lines must be between 0 and 10") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute Bandit security analysis""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info(f"Running Bandit analysis on {workspace}") - - # Check if there are any Python files - python_files = list(workspace.rglob("*.py")) - if not python_files: - logger.info("No Python files found for Bandit analysis") - return self.create_result( - findings=[], - status="success", - summary={"total_findings": 0, "files_scanned": 0} - ) - - # Build bandit command - cmd = ["bandit", "-f", "json"] - - # Add confidence level - confidence = config.get("confidence", "LOW") - # Handle both string and list formats - if isinstance(confidence, list): - confidence = confidence[0] if confidence else "MEDIUM" - cmd.extend(["--confidence-level", self._get_confidence_levels(confidence)]) - - # Add severity level - severity = config.get("severity", "LOW") - # Handle both string and list formats - if isinstance(severity, list): - severity = severity[0] if severity else "MEDIUM" - cmd.extend(["--severity-level", self._get_severity_levels(severity)]) - - # Add tests to run - if config.get("tests"): - cmd.extend(["-t", ",".join(config["tests"])]) - - # Add tests to skip - if config.get("skips"): - cmd.extend(["-s", ",".join(config["skips"])]) - - # Add exclude directories - exclude_dirs = config.get("exclude_dirs", ["tests", "test", ".git", "__pycache__"]) - if exclude_dirs: - cmd.extend(["-x", ",".join(exclude_dirs)]) - - # Add aggregate mode - aggregate = config.get("aggregate", "file") - cmd.extend(["-a", aggregate]) - - # Add context lines - context_lines = config.get("context_lines", 3) - cmd.extend(["-n", str(context_lines)]) - - # Add recursive flag and target - cmd.extend(["-r", str(workspace)]) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run Bandit - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results - findings = [] - if process.returncode in [0, 1]: # 0 = no issues, 1 = issues found - findings = self._parse_bandit_output(stdout.decode(), workspace) - else: - error_msg = stderr.decode() - logger.error(f"Bandit failed: {error_msg}") - return self.create_result( - findings=[], - status="failed", - error=f"Bandit execution failed: {error_msg}" - ) - - # Create summary - summary = self._create_summary(findings, len(python_files)) - - logger.info(f"Bandit found {len(findings)} security issues") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"Bandit module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - def _get_confidence_levels(self, min_confidence: str) -> str: - """Get minimum confidence level for Bandit""" - return min_confidence.lower() - - def _get_severity_levels(self, min_severity: str) -> str: - """Get minimum severity level for Bandit""" - return min_severity.lower() - - def _parse_bandit_output(self, output: str, workspace: Path) -> List[ModuleFinding]: - """Parse Bandit JSON output into findings""" - findings = [] - - if not output.strip(): - return findings - - try: - data = json.loads(output) - results = data.get("results", []) - - for result in results: - # Extract information - test_id = result.get("test_id", "unknown") - test_name = result.get("test_name", "") - issue_confidence = result.get("issue_confidence", "MEDIUM") - issue_severity = result.get("issue_severity", "MEDIUM") - issue_text = result.get("issue_text", "") - - # File location - filename = result.get("filename", "") - line_number = result.get("line_number", 0) - line_range = result.get("line_range", []) - - # Code context - code = result.get("code", "") - - # Make file path relative to workspace - if filename: - try: - rel_path = Path(filename).relative_to(workspace) - filename = str(rel_path) - except ValueError: - pass - - # Map Bandit severity to our levels - finding_severity = self._map_severity(issue_severity) - - # Determine category based on test_id - category = self._get_category(test_id, test_name) - - # Create finding - finding = self.create_finding( - title=f"Python security issue: {test_name}", - description=issue_text or f"Bandit test {test_id} detected a security issue", - severity=finding_severity, - category=category, - file_path=filename if filename else None, - line_start=line_number if line_number > 0 else None, - line_end=line_range[-1] if line_range and len(line_range) > 1 else None, - code_snippet=code.strip() if code else None, - recommendation=self._get_recommendation(test_id, test_name), - metadata={ - "test_id": test_id, - "test_name": test_name, - "bandit_confidence": issue_confidence, - "bandit_severity": issue_severity, - "line_range": line_range, - "more_info": result.get("more_info", "") - } - ) - - findings.append(finding) - - except json.JSONDecodeError as e: - logger.warning(f"Failed to parse Bandit output: {e}") - except Exception as e: - logger.warning(f"Error processing Bandit results: {e}") - - return findings - - def _map_severity(self, bandit_severity: str) -> str: - """Map Bandit severity to our standard severity levels""" - severity_map = { - "HIGH": "high", - "MEDIUM": "medium", - "LOW": "low" - } - return severity_map.get(bandit_severity.upper(), "medium") - - def _get_category(self, test_id: str, test_name: str) -> str: - """Determine finding category based on Bandit test""" - # Map common Bandit test categories - if "sql" in test_id.lower() or "injection" in test_name.lower(): - return "injection" - elif "crypto" in test_id.lower() or "hash" in test_name.lower(): - return "cryptography" - elif "shell" in test_id.lower() or "subprocess" in test_name.lower(): - return "command_injection" - elif "hardcode" in test_id.lower() or "password" in test_name.lower(): - return "hardcoded_secrets" - elif "pickle" in test_id.lower() or "deserial" in test_name.lower(): - return "deserialization" - elif "request" in test_id.lower() or "http" in test_name.lower(): - return "web_security" - elif "random" in test_id.lower(): - return "weak_randomness" - elif "path" in test_id.lower() or "traversal" in test_name.lower(): - return "path_traversal" - else: - return "python_security" - - def _get_recommendation(self, test_id: str, test_name: str) -> str: - """Generate recommendation based on Bandit test""" - recommendations = { - # SQL Injection - "B608": "Use parameterized queries instead of string formatting for SQL queries.", - "B703": "Use parameterized queries with Django ORM or raw SQL.", - - # Cryptography - "B101": "Remove hardcoded passwords and use secure configuration management.", - "B105": "Remove hardcoded passwords and use environment variables or secret management.", - "B106": "Remove hardcoded passwords from function arguments.", - "B107": "Remove hardcoded passwords from default function arguments.", - "B303": "Use cryptographically secure hash functions like SHA-256 or better.", - "B324": "Use strong cryptographic algorithms instead of deprecated ones.", - "B413": "Use secure encryption algorithms and proper key management.", - - # Command Injection - "B602": "Validate and sanitize input before using in subprocess calls.", - "B603": "Avoid using subprocess with shell=True. Use array form instead.", - "B605": "Avoid starting processes with shell=True.", - - # Deserialization - "B301": "Avoid using pickle for untrusted data. Use JSON or safer alternatives.", - "B302": "Avoid using marshal for untrusted data.", - "B506": "Use safe YAML loading methods like yaml.safe_load().", - - # Web Security - "B501": "Validate SSL certificates in requests to prevent MITM attacks.", - "B401": "Import and use telnetlib carefully, prefer SSH for remote connections.", - - # Random - "B311": "Use cryptographically secure random generators like secrets module.", - - # Path Traversal - "B108": "Validate file paths to prevent directory traversal attacks." - } - - return recommendations.get(test_id, - f"Review the {test_name} security issue and apply appropriate security measures.") - - def _create_summary(self, findings: List[ModuleFinding], total_files: int) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"high": 0, "medium": 0, "low": 0} - category_counts = {} - test_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by test - test_id = finding.metadata.get("test_id", "unknown") - test_counts[test_id] = test_counts.get(test_id, 0) + 1 - - return { - "total_findings": len(findings), - "files_scanned": total_files, - "severity_counts": severity_counts, - "category_counts": category_counts, - "top_tests": dict(sorted(test_counts.items(), key=lambda x: x[1], reverse=True)[:10]), - "files_with_issues": len(set(f.file_path for f in findings if f.file_path)) - } \ No newline at end of file diff --git a/backend/toolbox/modules/static_analysis/opengrep.py b/backend/toolbox/modules/static_analysis/opengrep.py deleted file mode 100644 index 03353b3..0000000 --- a/backend/toolbox/modules/static_analysis/opengrep.py +++ /dev/null @@ -1,396 +0,0 @@ -""" -OpenGrep Static Analysis Module - -This module uses OpenGrep (open-source version of Semgrep) for pattern-based -static analysis across multiple programming languages. -""" -# Copyright (c) 2025 FuzzingLabs -# -# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file -# at the root of this repository for details. -# -# After the Change Date (four years from publication), this version of the -# Licensed Work will be made available under the Apache License, Version 2.0. -# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0 -# -# Additional attribution and requirements are provided in the NOTICE file. - - -import asyncio -import json -import tempfile -from pathlib import Path -from typing import Dict, Any, List -import subprocess -import logging - -from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult -from . import register_module - -logger = logging.getLogger(__name__) - - -@register_module -class OpenGrepModule(BaseModule): - """OpenGrep static analysis module""" - - def get_metadata(self) -> ModuleMetadata: - """Get module metadata""" - return ModuleMetadata( - name="opengrep", - version="1.45.0", - description="Open-source pattern-based static analysis tool for security vulnerabilities", - author="FuzzForge Team", - category="static_analysis", - tags=["sast", "pattern-matching", "multi-language", "security"], - input_schema={ - "type": "object", - "properties": { - "config": { - "type": "string", - "enum": ["auto", "p/security-audit", "p/owasp-top-ten", "p/cwe-top-25"], - "default": "auto", - "description": "Rule configuration to use" - }, - "languages": { - "type": "array", - "items": {"type": "string"}, - "description": "Specific languages to analyze" - }, - "include_patterns": { - "type": "array", - "items": {"type": "string"}, - "description": "File patterns to include" - }, - "exclude_patterns": { - "type": "array", - "items": {"type": "string"}, - "description": "File patterns to exclude" - }, - "max_target_bytes": { - "type": "integer", - "default": 1000000, - "description": "Maximum file size to analyze (bytes)" - }, - "timeout": { - "type": "integer", - "default": 300, - "description": "Analysis timeout in seconds" - }, - "severity": { - "type": "array", - "items": {"type": "string", "enum": ["ERROR", "WARNING", "INFO"]}, - "default": ["ERROR", "WARNING", "INFO"], - "description": "Minimum severity levels to report" - }, - "confidence": { - "type": "array", - "items": {"type": "string", "enum": ["HIGH", "MEDIUM", "LOW"]}, - "default": ["HIGH", "MEDIUM", "LOW"], - "description": "Minimum confidence levels to report" - } - } - }, - output_schema={ - "type": "object", - "properties": { - "findings": { - "type": "array", - "items": { - "type": "object", - "properties": { - "rule_id": {"type": "string"}, - "severity": {"type": "string"}, - "confidence": {"type": "string"}, - "file_path": {"type": "string"}, - "line_number": {"type": "integer"} - } - } - } - } - } - ) - - def validate_config(self, config: Dict[str, Any]) -> bool: - """Validate configuration""" - timeout = config.get("timeout", 300) - if not isinstance(timeout, int) or timeout < 30 or timeout > 3600: - raise ValueError("Timeout must be between 30 and 3600 seconds") - - max_bytes = config.get("max_target_bytes", 1000000) - if not isinstance(max_bytes, int) or max_bytes < 1000 or max_bytes > 10000000: - raise ValueError("max_target_bytes must be between 1000 and 10000000") - - return True - - async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult: - """Execute OpenGrep static analysis""" - self.start_timer() - - try: - # Validate inputs - self.validate_config(config) - self.validate_workspace(workspace) - - logger.info(f"Running OpenGrep analysis on {workspace}") - - # Build opengrep command - cmd = ["semgrep", "--json"] - - # Add configuration - config_type = config.get("config", "auto") - if config_type == "auto": - cmd.extend(["--config", "auto"]) - else: - cmd.extend(["--config", config_type]) - - # Add timeout - cmd.extend(["--timeout", str(config.get("timeout", 300))]) - - # Add max target bytes - cmd.extend(["--max-target-bytes", str(config.get("max_target_bytes", 1000000))]) - - # Add languages if specified - if config.get("languages"): - for lang in config["languages"]: - cmd.extend(["--lang", lang]) - - # Add include patterns - if config.get("include_patterns"): - for pattern in config["include_patterns"]: - cmd.extend(["--include", pattern]) - - # Add exclude patterns - if config.get("exclude_patterns"): - for pattern in config["exclude_patterns"]: - cmd.extend(["--exclude", pattern]) - - # Add severity filter (semgrep only accepts one severity level) - severity_levels = config.get("severity", ["ERROR", "WARNING", "INFO"]) - if severity_levels: - # Use the highest severity level from the list - severity_priority = {"ERROR": 3, "WARNING": 2, "INFO": 1} - highest_severity = max(severity_levels, key=lambda x: severity_priority.get(x, 0)) - cmd.extend(["--severity", highest_severity]) - - # Add confidence filter (if supported in this version) - confidence_levels = config.get("confidence", ["HIGH", "MEDIUM"]) - if confidence_levels and len(confidence_levels) < 3: # Only if not all levels - # Note: confidence filtering might need to be done post-processing - pass - - # Disable metrics collection - cmd.append("--disable-version-check") - cmd.append("--no-git-ignore") - - # Add target directory - cmd.append(str(workspace)) - - logger.debug(f"Running command: {' '.join(cmd)}") - - # Run OpenGrep - process = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=workspace - ) - - stdout, stderr = await process.communicate() - - # Parse results - findings = [] - if process.returncode in [0, 1]: # 0 = no findings, 1 = findings found - findings = self._parse_opengrep_output(stdout.decode(), workspace, config) - else: - error_msg = stderr.decode() - logger.error(f"OpenGrep failed: {error_msg}") - return self.create_result( - findings=[], - status="failed", - error=f"OpenGrep execution failed: {error_msg}" - ) - - # Create summary - summary = self._create_summary(findings) - - logger.info(f"OpenGrep found {len(findings)} potential issues") - - return self.create_result( - findings=findings, - status="success", - summary=summary - ) - - except Exception as e: - logger.error(f"OpenGrep module failed: {e}") - return self.create_result( - findings=[], - status="failed", - error=str(e) - ) - - def _parse_opengrep_output(self, output: str, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]: - """Parse OpenGrep JSON output into findings""" - findings = [] - - if not output.strip(): - return findings - - try: - data = json.loads(output) - results = data.get("results", []) - - # Get filtering criteria - allowed_severities = set(config.get("severity", ["ERROR", "WARNING", "INFO"])) - allowed_confidences = set(config.get("confidence", ["HIGH", "MEDIUM", "LOW"])) - - for result in results: - # Extract basic info - rule_id = result.get("check_id", "unknown") - message = result.get("message", "") - severity = result.get("extra", {}).get("severity", "INFO").upper() - - # File location info - path_info = result.get("path", "") - start_line = result.get("start", {}).get("line", 0) - end_line = result.get("end", {}).get("line", 0) - start_col = result.get("start", {}).get("col", 0) - end_col = result.get("end", {}).get("col", 0) - - # Code snippet - lines = result.get("extra", {}).get("lines", "") - - # Metadata - metadata = result.get("extra", {}) - cwe = metadata.get("metadata", {}).get("cwe", []) - owasp = metadata.get("metadata", {}).get("owasp", []) - confidence = metadata.get("metadata", {}).get("confidence", "MEDIUM").upper() - - # Apply severity filter - if severity not in allowed_severities: - continue - - # Apply confidence filter - if confidence not in allowed_confidences: - continue - - # Make file path relative to workspace - if path_info: - try: - rel_path = Path(path_info).relative_to(workspace) - path_info = str(rel_path) - except ValueError: - pass - - # Map severity to our standard levels - finding_severity = self._map_severity(severity) - - # Create finding - finding = self.create_finding( - title=f"Security issue: {rule_id}", - description=message or f"OpenGrep rule {rule_id} triggered", - severity=finding_severity, - category=self._get_category(rule_id, metadata), - file_path=path_info if path_info else None, - line_start=start_line if start_line > 0 else None, - line_end=end_line if end_line > 0 and end_line != start_line else None, - code_snippet=lines.strip() if lines else None, - recommendation=self._get_recommendation(rule_id, metadata), - metadata={ - "rule_id": rule_id, - "opengrep_severity": severity, - "confidence": confidence, - "cwe": cwe, - "owasp": owasp, - "fix": metadata.get("fix", ""), - "impact": metadata.get("impact", ""), - "likelihood": metadata.get("likelihood", ""), - "references": metadata.get("references", []) - } - ) - - findings.append(finding) - - except json.JSONDecodeError as e: - logger.warning(f"Failed to parse OpenGrep output: {e}") - except Exception as e: - logger.warning(f"Error processing OpenGrep results: {e}") - - return findings - - def _map_severity(self, opengrep_severity: str) -> str: - """Map OpenGrep severity to our standard severity levels""" - severity_map = { - "ERROR": "high", - "WARNING": "medium", - "INFO": "low" - } - return severity_map.get(opengrep_severity.upper(), "medium") - - def _get_category(self, rule_id: str, metadata: Dict[str, Any]) -> str: - """Determine finding category based on rule and metadata""" - cwe_list = metadata.get("metadata", {}).get("cwe", []) - owasp_list = metadata.get("metadata", {}).get("owasp", []) - - # Check for common security categories - if any("injection" in rule_id.lower() for x in [rule_id]): - return "injection" - elif any("xss" in rule_id.lower() for x in [rule_id]): - return "xss" - elif any("csrf" in rule_id.lower() for x in [rule_id]): - return "csrf" - elif any("auth" in rule_id.lower() for x in [rule_id]): - return "authentication" - elif any("crypto" in rule_id.lower() for x in [rule_id]): - return "cryptography" - elif cwe_list: - return f"cwe-{cwe_list[0]}" - elif owasp_list: - return f"owasp-{owasp_list[0].replace(' ', '-').lower()}" - else: - return "security" - - def _get_recommendation(self, rule_id: str, metadata: Dict[str, Any]) -> str: - """Generate recommendation based on rule and metadata""" - fix_suggestion = metadata.get("fix", "") - if fix_suggestion: - return fix_suggestion - - # Generic recommendations based on rule type - if "injection" in rule_id.lower(): - return "Use parameterized queries or prepared statements to prevent injection attacks." - elif "xss" in rule_id.lower(): - return "Properly encode/escape user input before displaying it in web pages." - elif "crypto" in rule_id.lower(): - return "Use cryptographically secure algorithms and proper key management." - elif "hardcode" in rule_id.lower(): - return "Remove hardcoded secrets and use secure configuration management." - else: - return "Review this security issue and apply appropriate fixes based on your security requirements." - - def _create_summary(self, findings: List[ModuleFinding]) -> Dict[str, Any]: - """Create analysis summary""" - severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0} - category_counts = {} - rule_counts = {} - - for finding in findings: - # Count by severity - severity_counts[finding.severity] += 1 - - # Count by category - category = finding.category - category_counts[category] = category_counts.get(category, 0) + 1 - - # Count by rule - rule_id = finding.metadata.get("rule_id", "unknown") - rule_counts[rule_id] = rule_counts.get(rule_id, 0) + 1 - - return { - "total_findings": len(findings), - "severity_counts": severity_counts, - "category_counts": category_counts, - "top_rules": dict(sorted(rule_counts.items(), key=lambda x: x[1], reverse=True)[:10]), - "files_analyzed": len(set(f.file_path for f in findings if f.file_path)) - } \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 7b2f1e7..5cbe78c 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -152,7 +152,9 @@ services: chmod 755 /prefect-storage && echo '{\"insecure-registries\": [\"registry:5000\", \"localhost:5001\", \"host.docker.internal:5001\"]}' > /tmp/docker/config.json && pip install 'prefect[docker]' && - prefect worker start --pool docker-pool + echo 'Waiting for backend to create work pool...' && + sleep 15 && + prefect worker start --pool docker-pool --type docker " volumes: - prefect_storage:/prefect-storage # Access to shared storage for results diff --git a/test_projects/vulnerable_app/findings-53e5d63f.json b/test_projects/vulnerable_app/findings-53e5d63f.json deleted file mode 100644 index 909e9e4..0000000 --- a/test_projects/vulnerable_app/findings-53e5d63f.json +++ /dev/null @@ -1,675 +0,0 @@ -{ - "tool": { - "name": "FuzzForge Security Assessment", - "version": "1.0.0" - }, - "summary": { - "total_issues": 66, - "by_severity": { - "warning": 49, - "error": 17 - } - }, - "findings": [ - { - "rule_id": "sensitive_file_medium", - "severity": "warning", - "message": "Found potentially sensitive file at private_key.pem", - "location": { - "file": "private_key.pem", - "line": null, - "column": null - } - }, - { - "rule_id": "sensitive_file_medium", - "severity": "warning", - "message": "Found potentially sensitive file at wallet.json", - "location": { - "file": "wallet.json", - "line": null, - "column": null - } - }, - { - "rule_id": "sensitive_file_medium", - "severity": "warning", - "message": "Found potentially sensitive file at .npmrc", - "location": { - "file": ".npmrc", - "line": null, - "column": null - } - }, - { - "rule_id": "sensitive_file_medium", - "severity": "warning", - "message": "Found potentially sensitive file at .env", - "location": { - "file": ".env", - "line": null, - "column": null - } - }, - { - "rule_id": "sensitive_file_medium", - "severity": "warning", - "message": "Found potentially sensitive file at .git-credentials", - "location": { - "file": ".git-credentials", - "line": null, - "column": null - } - }, - { - "rule_id": "sensitive_file_medium", - "severity": "warning", - "message": "Found potentially sensitive file at data/api_keys.txt", - "location": { - "file": "data/api_keys.txt", - "line": null, - "column": null - } - }, - { - "rule_id": "sensitive_file_medium", - "severity": "warning", - "message": "Found potentially sensitive file at data/credentials.json", - "location": { - "file": "data/credentials.json", - "line": null, - "column": null - } - }, - { - "rule_id": "sensitive_file_medium", - "severity": "warning", - "message": "Found potentially sensitive file at .fuzzforge/.env", - "location": { - "file": ".fuzzforge/.env", - "line": null, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via F-string in SQL query", - "location": { - "file": "app.py", - "line": 21, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_high", - "severity": "error", - "message": "Found potential hardcoded API Key in src/api_handler.py", - "location": { - "file": "src/api_handler.py", - "line": 13, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_medium", - "severity": "warning", - "message": "Found potential hardcoded Authentication Token in src/api_handler.py", - "location": { - "file": "src/api_handler.py", - "line": 9, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function eval(): Arbitrary code execution", - "location": { - "file": "src/api_handler.py", - "line": 22, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function eval(): Arbitrary code execution", - "location": { - "file": "src/api_handler.py", - "line": 42, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function exec(): Arbitrary code execution", - "location": { - "file": "src/api_handler.py", - "line": 37, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function os.system(): Command injection risk", - "location": { - "file": "src/api_handler.py", - "line": 32, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function os.system(): Command injection risk", - "location": { - "file": "src/api_handler.py", - "line": 59, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function subprocess with shell=True: Command injection risk", - "location": { - "file": "src/api_handler.py", - "line": 27, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via String concatenation in SQL", - "location": { - "file": "src/database.py", - "line": 31, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via String formatting in SQL", - "location": { - "file": "src/database.py", - "line": 38, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via String formatting in SQL", - "location": { - "file": "src/database.py", - "line": 45, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via F-string in SQL query", - "location": { - "file": "src/database.py", - "line": 38, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via Dynamic query building", - "location": { - "file": "src/database.py", - "line": 31, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via Dynamic query building", - "location": { - "file": "src/database.py", - "line": 63, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function os.system(): Command injection risk", - "location": { - "file": "src/database.py", - "line": 57, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function pickle.load(): Deserialization vulnerability", - "location": { - "file": "src/database.py", - "line": 52, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_high", - "severity": "error", - "message": "Found potential hardcoded Private Key in scripts/backup.js", - "location": { - "file": "scripts/backup.js", - "line": 81, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_medium", - "severity": "warning", - "message": "Found potential hardcoded Potential Secret Hash in scripts/backup.js", - "location": { - "file": "scripts/backup.js", - "line": 81, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function eval(): Arbitrary code execution", - "location": { - "file": "scripts/backup.js", - "line": 23, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function new Function(): Arbitrary code execution", - "location": { - "file": "scripts/backup.js", - "line": 28, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function innerHTML: XSS vulnerability", - "location": { - "file": "scripts/backup.js", - "line": 33, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function innerHTML: XSS vulnerability", - "location": { - "file": "scripts/backup.js", - "line": 37, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function document.write(): XSS vulnerability", - "location": { - "file": "scripts/backup.js", - "line": 42, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_high", - "severity": "error", - "message": "Found potential hardcoded Private Key in src/Main.java", - "location": { - "file": "src/Main.java", - "line": 77, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via String concatenation in SQL", - "location": { - "file": "src/Main.java", - "line": 23, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via String concatenation in SQL", - "location": { - "file": "src/Main.java", - "line": 29, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via Dynamic query building", - "location": { - "file": "src/Main.java", - "line": 23, - "column": null - } - }, - { - "rule_id": "sql_injection_high", - "severity": "error", - "message": "Detected potential SQL injection vulnerability via Dynamic query building", - "location": { - "file": "src/Main.java", - "line": 29, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function eval(): Arbitrary code execution", - "location": { - "file": "scripts/deploy.php", - "line": 28, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function exec(): Command execution", - "location": { - "file": "scripts/deploy.php", - "line": 22, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function exec(): Command execution", - "location": { - "file": "scripts/deploy.php", - "line": 23, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function system(): Command execution", - "location": { - "file": "scripts/deploy.php", - "line": 21, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function shell_exec(): Command execution", - "location": { - "file": "scripts/deploy.php", - "line": 23, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 12, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 21, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 23, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 24, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 31, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 45, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 50, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_GET usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 57, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 13, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 22, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 27, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 32, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 40, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 46, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 53, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 54, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 61, - "column": null - } - }, - { - "rule_id": "dangerous_function_medium", - "severity": "warning", - "message": "Use of potentially dangerous function Direct $_POST usage: Input validation missing", - "location": { - "file": "scripts/deploy.php", - "line": 62, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_high", - "severity": "error", - "message": "Found potential hardcoded API Key in src/utils.rb", - "location": { - "file": "src/utils.rb", - "line": 64, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_medium", - "severity": "warning", - "message": "Found potential hardcoded Hardcoded Password in src/utils.rb", - "location": { - "file": "src/utils.rb", - "line": 63, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_high", - "severity": "error", - "message": "Found potential hardcoded Private Key in src/app.go", - "location": { - "file": "src/app.go", - "line": 59, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_high", - "severity": "error", - "message": "Found potential hardcoded Private Key in src/app.go", - "location": { - "file": "src/app.go", - "line": 62, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_medium", - "severity": "warning", - "message": "Found potential hardcoded Potential Secret Hash in src/app.go", - "location": { - "file": "src/app.go", - "line": 59, - "column": null - } - }, - { - "rule_id": "hardcoded_secret_medium", - "severity": "warning", - "message": "Found potential hardcoded Potential Secret Hash in src/app.go", - "location": { - "file": "src/app.go", - "line": 62, - "column": null - } - } - ] -} \ No newline at end of file diff --git a/test_projects/vulnerable_app/findings-e34d6450.sarif b/test_projects/vulnerable_app/findings-e34d6450.sarif deleted file mode 100644 index e1582d5..0000000 --- a/test_projects/vulnerable_app/findings-e34d6450.sarif +++ /dev/null @@ -1,2548 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", - "version": "2.1.0", - "runs": [ - { - "tool": { - "driver": { - "name": "FuzzForge Security Assessment", - "version": "1.0.0", - "informationUri": "https://fuzzforge.io", - "rules": [ - { - "id": "sensitive_file_medium", - "name": "Sensitive File", - "shortDescription": { - "text": "sensitive_file vulnerability" - }, - "fullDescription": { - "text": "Detection rule for sensitive_file vulnerabilities with medium severity" - }, - "defaultConfiguration": { - "level": "warning" - }, - "properties": { - "category": "sensitive_file", - "severity": "medium", - "tags": [ - "security", - "sensitive_file", - "medium" - ] - } - }, - { - "id": "sql_injection_high", - "name": "Sql Injection", - "shortDescription": { - "text": "sql_injection vulnerability" - }, - "fullDescription": { - "text": "Detection rule for sql_injection vulnerabilities with high severity" - }, - "defaultConfiguration": { - "level": "error" - }, - "properties": { - "category": "sql_injection", - "severity": "high", - "tags": [ - "security", - "sql_injection", - "high" - ] - } - }, - { - "id": "hardcoded_secret_high", - "name": "Hardcoded Secret", - "shortDescription": { - "text": "hardcoded_secret vulnerability" - }, - "fullDescription": { - "text": "Detection rule for hardcoded_secret vulnerabilities with high severity" - }, - "defaultConfiguration": { - "level": "error" - }, - "properties": { - "category": "hardcoded_secret", - "severity": "high", - "tags": [ - "security", - "hardcoded_secret", - "high" - ] - } - }, - { - "id": "hardcoded_secret_medium", - "name": "Hardcoded Secret", - "shortDescription": { - "text": "hardcoded_secret vulnerability" - }, - "fullDescription": { - "text": "Detection rule for hardcoded_secret vulnerabilities with medium severity" - }, - "defaultConfiguration": { - "level": "warning" - }, - "properties": { - "category": "hardcoded_secret", - "severity": "medium", - "tags": [ - "security", - "hardcoded_secret", - "medium" - ] - } - }, - { - "id": "dangerous_function_medium", - "name": "Dangerous Function", - "shortDescription": { - "text": "dangerous_function vulnerability" - }, - "fullDescription": { - "text": "Detection rule for dangerous_function vulnerabilities with medium severity" - }, - "defaultConfiguration": { - "level": "warning" - }, - "properties": { - "category": "dangerous_function", - "severity": "medium", - "tags": [ - "security", - "dangerous_function", - "medium" - ] - } - } - ] - } - }, - "results": [ - { - "ruleId": "sensitive_file_medium", - "level": "warning", - "message": { - "text": "Found potentially sensitive file at private_key.pem" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "private_key.pem", - "uriBaseId": "WORKSPACE" - } - } - } - ], - "properties": { - "findingId": "cc37aa8f-0bc5-4e54-a5aa-d473b9385321", - "title": "Potentially sensitive file: private_key.pem", - "metadata": { - "file_size": 381, - "file_type": "application/octet-stream" - } - } - }, - { - "ruleId": "sensitive_file_medium", - "level": "warning", - "message": { - "text": "Found potentially sensitive file at wallet.json" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "wallet.json", - "uriBaseId": "WORKSPACE" - } - } - } - ], - "properties": { - "findingId": "c914cc7a-cb03-4643-a359-4fcc41bf7646", - "title": "Potentially sensitive file: wallet.json", - "metadata": { - "file_size": 1206, - "file_type": "application/json" - } - } - }, - { - "ruleId": "sensitive_file_medium", - "level": "warning", - "message": { - "text": "Found potentially sensitive file at .npmrc" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": ".npmrc", - "uriBaseId": "WORKSPACE" - } - } - } - ], - "properties": { - "findingId": "ace6ea93-a835-4ca7-b91b-f788ad8209da", - "title": "Potentially sensitive file: .npmrc", - "metadata": { - "file_size": 238, - "file_type": "application/octet-stream" - } - } - }, - { - "ruleId": "sensitive_file_medium", - "level": "warning", - "message": { - "text": "Found potentially sensitive file at .env" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": ".env", - "uriBaseId": "WORKSPACE" - } - } - } - ], - "properties": { - "findingId": "611cf2f2-e55d-4973-9f0b-90b1b4e906d6", - "title": "Potentially sensitive file: .env", - "metadata": { - "file_size": 1546, - "file_type": "application/octet-stream" - } - } - }, - { - "ruleId": "sensitive_file_medium", - "level": "warning", - "message": { - "text": "Found potentially sensitive file at .git-credentials" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": ".git-credentials", - "uriBaseId": "WORKSPACE" - } - } - } - ], - "properties": { - "findingId": "1f952530-9468-442a-93c9-2facefd4720b", - "title": "Potentially sensitive file: .git-credentials", - "metadata": { - "file_size": 168, - "file_type": "application/octet-stream" - } - } - }, - { - "ruleId": "sensitive_file_medium", - "level": "warning", - "message": { - "text": "Found potentially sensitive file at data/api_keys.txt" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "data/api_keys.txt", - "uriBaseId": "WORKSPACE" - } - } - } - ], - "properties": { - "findingId": "e8a4277b-1025-43df-a319-c56d4c9e766e", - "title": "Potentially sensitive file: api_keys.txt", - "metadata": { - "file_size": 1138, - "file_type": "text/plain" - } - } - }, - { - "ruleId": "sensitive_file_medium", - "level": "warning", - "message": { - "text": "Found potentially sensitive file at data/credentials.json" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "data/credentials.json", - "uriBaseId": "WORKSPACE" - } - } - } - ], - "properties": { - "findingId": "060a0d9d-b73a-443c-ae35-09d0a0842695", - "title": "Potentially sensitive file: credentials.json", - "metadata": { - "file_size": 1057, - "file_type": "application/json" - } - } - }, - { - "ruleId": "sensitive_file_medium", - "level": "warning", - "message": { - "text": "Found potentially sensitive file at .fuzzforge/.env.template" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": ".fuzzforge/.env.template", - "uriBaseId": "WORKSPACE" - } - } - } - ], - "properties": { - "findingId": "a5398b83-d5a6-4787-b2cb-b2f1858d0a96", - "title": "Potentially sensitive file: .env.template", - "metadata": { - "file_size": 569, - "file_type": "application/octet-stream" - } - } - }, - { - "ruleId": "sensitive_file_medium", - "level": "warning", - "message": { - "text": "Found potentially sensitive file at .fuzzforge/.env" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": ".fuzzforge/.env", - "uriBaseId": "WORKSPACE" - } - } - } - ], - "properties": { - "findingId": "dd923388-7faf-4dba-8d0e-cd1f08dc0ca5", - "title": "Potentially sensitive file: .env", - "metadata": { - "file_size": 569, - "file_type": "application/octet-stream" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via F-string in SQL query" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "app.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 21, - "snippet": { - "text": "query = f\"SELECT * FROM users WHERE id = {user_id}\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "99a1553f-1536-484a-8dcb-62051fac73d2", - "title": "Potential SQL Injection: F-string in SQL query", - "metadata": { - "vulnerability_type": "F-string in SQL query" - } - } - }, - { - "ruleId": "hardcoded_secret_high", - "level": "error", - "message": { - "text": "Found potential hardcoded API Key in src/api_handler.py" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/api_handler.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 13, - "snippet": { - "text": "STRIPE_API_KEY = \"sk_live_4eC39HqLyjWDarjtT1zdp7dc\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded API Key and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "570809d3-fe4f-4d22-bf0c-e249449ed6d9", - "title": "Hardcoded API Key detected", - "metadata": { - "secret_type": "API Key" - } - } - }, - { - "ruleId": "hardcoded_secret_medium", - "level": "warning", - "message": { - "text": "Found potential hardcoded Authentication Token in src/api_handler.py" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/api_handler.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 9, - "snippet": { - "text": "SECRET_TOKEN = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded Authentication Token and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "568cccd7-5fa3-416b-8378-b07395a6bba5", - "title": "Hardcoded Authentication Token detected", - "metadata": { - "secret_type": "Authentication Token" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function eval(): Arbitrary code execution" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/api_handler.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 22, - "snippet": { - "text": "result = eval(user_data) # Code injection vulnerability" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to eval()" - } - } - ], - "properties": { - "findingId": "97fca15d-5d39-408a-aa8a-4539f9acd70d", - "title": "Dangerous function: eval()", - "metadata": { - "function": "eval()", - "risk": "Arbitrary code execution" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function eval(): Arbitrary code execution" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/api_handler.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 42, - "snippet": { - "text": "func = eval(f\"lambda x: {code}\") # Dangerous eval" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to eval()" - } - } - ], - "properties": { - "findingId": "b3ba0382-afa9-4a59-a4a5-35ca481b4b36", - "title": "Dangerous function: eval()", - "metadata": { - "function": "eval()", - "risk": "Arbitrary code execution" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function exec(): Arbitrary code execution" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/api_handler.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 37, - "snippet": { - "text": "exec(compiled, data) # Code execution vulnerability" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to exec()" - } - } - ], - "properties": { - "findingId": "7aa84df3-6cd6-4a2f-a378-2d3b64020303", - "title": "Dangerous function: exec()", - "metadata": { - "function": "exec()", - "risk": "Arbitrary code execution" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function os.system(): Command injection risk" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/api_handler.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 32, - "snippet": { - "text": "os.system(\"cat \" + filename) # Command injection" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to os.system()" - } - } - ], - "properties": { - "findingId": "634a0fbb-d1f1-49a7-9489-1a2ee7dab592", - "title": "Dangerous function: os.system()", - "metadata": { - "function": "os.system()", - "risk": "Command injection risk" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function os.system(): Command injection risk" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/api_handler.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 59, - "snippet": { - "text": "os.system(f\"echo '{log_message}' >> /var/log/app.log\") # Command injection via logs" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to os.system()" - } - } - ], - "properties": { - "findingId": "020e2824-7809-44b8-bc96-8faf794d1cd4", - "title": "Dangerous function: os.system()", - "metadata": { - "function": "os.system()", - "risk": "Command injection risk" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function subprocess with shell=True: Command injection risk" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/api_handler.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 27, - "snippet": { - "text": "result = subprocess.call(command, shell=True) # Command injection risk" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to subprocess with shell=True" - } - } - ], - "properties": { - "findingId": "44bc73a1-87c9-4bcf-924f-c28cf16ae10b", - "title": "Dangerous function: subprocess with shell=True", - "metadata": { - "function": "subprocess with shell=True", - "risk": "Command injection risk" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via String concatenation in SQL" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/database.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 31, - "snippet": { - "text": "query = \"SELECT * FROM users WHERE username = '\" + user_input + \"'\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "8ad4537c-70d9-4091-a1a1-3c707aa23c43", - "title": "Potential SQL Injection: String concatenation in SQL", - "metadata": { - "vulnerability_type": "String concatenation in SQL" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via String formatting in SQL" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/database.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 38, - "snippet": { - "text": "query = f\"SELECT * FROM products WHERE name LIKE '%{search_term}%' AND category = '{category}'\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "b9de1e8c-3e7a-4e80-a941-8f3e7d19193d", - "title": "Potential SQL Injection: String formatting in SQL", - "metadata": { - "vulnerability_type": "String formatting in SQL" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via String formatting in SQL" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/database.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 45, - "snippet": { - "text": "query = \"UPDATE users SET profile = '%s' WHERE id = %s\" % (data, user_id)" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "a0a1b161-4c9d-40e6-9425-fa2a837e392c", - "title": "Potential SQL Injection: String formatting in SQL", - "metadata": { - "vulnerability_type": "String formatting in SQL" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via F-string in SQL query" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/database.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 38, - "snippet": { - "text": "query = f\"SELECT * FROM products WHERE name LIKE '%{search_term}%' AND category = '{category}'\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "b163cf4a-4096-4d6e-bf1e-7fdd9c2c5e0d", - "title": "Potential SQL Injection: F-string in SQL query", - "metadata": { - "vulnerability_type": "F-string in SQL query" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via Dynamic query building" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/database.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 31, - "snippet": { - "text": "query = \"SELECT * FROM users WHERE username = '\" + user_input + \"'\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "1dfc700e-8d0a-4aea-8274-581eb3d22558", - "title": "Potential SQL Injection: Dynamic query building", - "metadata": { - "vulnerability_type": "Dynamic query building" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via Dynamic query building" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/database.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 63, - "snippet": { - "text": "final_query = base_query + where_clause" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "db3e1ebf-9de7-4ccb-ab64-05d44ec0423b", - "title": "Potential SQL Injection: Dynamic query building", - "metadata": { - "vulnerability_type": "Dynamic query building" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function os.system(): Command injection risk" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/database.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 57, - "snippet": { - "text": "os.system(f\"mysqldump -u {DB_USER} -p{DB_PASSWORD} production > {backup_name}\")" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to os.system()" - } - } - ], - "properties": { - "findingId": "f005e534-82bc-4028-b2a1-60f81b7271ce", - "title": "Dangerous function: os.system()", - "metadata": { - "function": "os.system()", - "risk": "Command injection risk" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function pickle.load(): Deserialization vulnerability" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/database.py", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 52, - "snippet": { - "text": "user_prefs = pickle.loads(data) # Dangerous pickle deserialization" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to pickle.load()" - } - } - ], - "properties": { - "findingId": "46cbf0c4-795d-4c59-95b2-625524dd7b72", - "title": "Dangerous function: pickle.load()", - "metadata": { - "function": "pickle.load()", - "risk": "Deserialization vulnerability" - } - } - }, - { - "ruleId": "hardcoded_secret_high", - "level": "error", - "message": { - "text": "Found potential hardcoded Private Key in scripts/backup.js" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/backup.js", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 81, - "snippet": { - "text": "const BITCOIN_PRIVATE_KEY = \"5KJvsngHeMpm884wtkJNzQGaCErckhHJBGFsvd3VyK5qMZXj3hS\";" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded Private Key and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "cdda30d1-8584-4cf8-a0f2-42dfd9ceef72", - "title": "Hardcoded Private Key detected", - "metadata": { - "secret_type": "Private Key" - } - } - }, - { - "ruleId": "hardcoded_secret_medium", - "level": "warning", - "message": { - "text": "Found potential hardcoded Potential Secret Hash in scripts/backup.js" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/backup.js", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 81, - "snippet": { - "text": "const BITCOIN_PRIVATE_KEY = \"5KJvsngHeMpm884wtkJNzQGaCErckhHJBGFsvd3VyK5qMZXj3hS\";" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded Potential Secret Hash and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "4810636e-ccdc-4e5b-bf63-82201270d5db", - "title": "Hardcoded Potential Secret Hash detected", - "metadata": { - "secret_type": "Potential Secret Hash" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function eval(): Arbitrary code execution" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/backup.js", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 23, - "snippet": { - "text": "eval(userInput); // Code injection vulnerability" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to eval()" - } - } - ], - "properties": { - "findingId": "57e40967-e9e7-49f6-bcdb-ceb284cf46ca", - "title": "Dangerous function: eval()", - "metadata": { - "function": "eval()", - "risk": "Arbitrary code execution" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function new Function(): Arbitrary code execution" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/backup.js", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 28, - "snippet": { - "text": "return new Function(code); // Code injection vulnerability" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to new Function()" - } - } - ], - "properties": { - "findingId": "5102c299-045b-4e63-9aa8-68df218b1128", - "title": "Dangerous function: new Function()", - "metadata": { - "function": "new Function()", - "risk": "Arbitrary code execution" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function innerHTML: XSS vulnerability" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/backup.js", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 33, - "snippet": { - "text": "document.body.innerHTML = message; // XSS vulnerability" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to innerHTML" - } - } - ], - "properties": { - "findingId": "a780bebf-6f9c-4972-9402-0549c4c71ea5", - "title": "Dangerous function: innerHTML", - "metadata": { - "function": "innerHTML", - "risk": "XSS vulnerability" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function innerHTML: XSS vulnerability" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/backup.js", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 37, - "snippet": { - "text": "document.getElementById('content').innerHTML = html; // XSS vulnerability" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to innerHTML" - } - } - ], - "properties": { - "findingId": "b1bd47ed-ca65-41f4-b61a-f6f723adbb6b", - "title": "Dangerous function: innerHTML", - "metadata": { - "function": "innerHTML", - "risk": "XSS vulnerability" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function document.write(): XSS vulnerability" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/backup.js", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 42, - "snippet": { - "text": "document.write(data); // XSS vulnerability" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to document.write()" - } - } - ], - "properties": { - "findingId": "ed5c43b7-cf63-4525-82dd-c879ba3ec410", - "title": "Dangerous function: document.write()", - "metadata": { - "function": "document.write()", - "risk": "XSS vulnerability" - } - } - }, - { - "ruleId": "hardcoded_secret_high", - "level": "error", - "message": { - "text": "Found potential hardcoded Private Key in src/Main.java" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/Main.java", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 77, - "snippet": { - "text": "private static final String PRIVATE_KEY = \"-----BEGIN RSA PRIVATE KEY-----\\nMIIEpAIBAAKCAQ...\";" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded Private Key and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "7680578c-31dc-4796-9a7a-0889d2a376c2", - "title": "Hardcoded Private Key detected", - "metadata": { - "secret_type": "Private Key" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via String concatenation in SQL" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/Main.java", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 23, - "snippet": { - "text": "String query = \"SELECT * FROM users WHERE id = \" + userId; // SQL injection" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "800bbc55-79b0-4813-aa77-d835ebd532fd", - "title": "Potential SQL Injection: String concatenation in SQL", - "metadata": { - "vulnerability_type": "String concatenation in SQL" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via String concatenation in SQL" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/Main.java", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 29, - "snippet": { - "text": "String query = \"SELECT * FROM products WHERE name LIKE '%\" + searchTerm + \"%'\";" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "96d65d69-eeec-4f19-ba41-3eda3d0b6376", - "title": "Potential SQL Injection: String concatenation in SQL", - "metadata": { - "vulnerability_type": "String concatenation in SQL" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via Dynamic query building" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/Main.java", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 23, - "snippet": { - "text": "String query = \"SELECT * FROM users WHERE id = \" + userId; // SQL injection" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "6144ffe8-501e-472e-ad04-c1c04b17951b", - "title": "Potential SQL Injection: Dynamic query building", - "metadata": { - "vulnerability_type": "Dynamic query building" - } - } - }, - { - "ruleId": "sql_injection_high", - "level": "error", - "message": { - "text": "Detected potential SQL injection vulnerability via Dynamic query building" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/Main.java", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 29, - "snippet": { - "text": "String query = \"SELECT * FROM products WHERE name LIKE '%\" + searchTerm + \"%'\";" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Use parameterized queries or prepared statements instead" - } - } - ], - "properties": { - "findingId": "08ae0f60-def0-43fc-81bd-ff37aea0c497", - "title": "Potential SQL Injection: Dynamic query building", - "metadata": { - "vulnerability_type": "Dynamic query building" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function eval(): Arbitrary code execution" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 28, - "snippet": { - "text": "eval($code); // Code execution vulnerability" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to eval()" - } - } - ], - "properties": { - "findingId": "f766d6e6-caa1-4606-a2aa-1bce25883135", - "title": "Dangerous function: eval()", - "metadata": { - "function": "eval()", - "risk": "Arbitrary code execution" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function exec(): Command execution" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 22, - "snippet": { - "text": "exec(\"cat \" . $_POST['file']);" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to exec()" - } - } - ], - "properties": { - "findingId": "1a1b644e-b646-420e-b21b-a2f523dddc2c", - "title": "Dangerous function: exec()", - "metadata": { - "function": "exec()", - "risk": "Command execution" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function exec(): Command execution" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 23, - "snippet": { - "text": "shell_exec(\"ping \" . $_GET['host']);" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to exec()" - } - } - ], - "properties": { - "findingId": "ab3cbe14-db2e-4ca8-a151-88b05276ef15", - "title": "Dangerous function: exec()", - "metadata": { - "function": "exec()", - "risk": "Command execution" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function system(): Command execution" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 21, - "snippet": { - "text": "system(\"ls -la \" . $_GET['directory']);" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to system()" - } - } - ], - "properties": { - "findingId": "b6d73950-c538-46bb-9b75-cc80a9ea9188", - "title": "Dangerous function: system()", - "metadata": { - "function": "system()", - "risk": "Command execution" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function shell_exec(): Command execution" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 23, - "snippet": { - "text": "shell_exec(\"ping \" . $_GET['host']);" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to shell_exec()" - } - } - ], - "properties": { - "findingId": "fb42a045-ca72-4d40-838f-b78e6a1cd606", - "title": "Dangerous function: shell_exec()", - "metadata": { - "function": "shell_exec()", - "risk": "Command execution" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_GET usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 12, - "snippet": { - "text": "$user_id = $_GET['id'];" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_GET usage" - } - } - ], - "properties": { - "findingId": "176fad7b-eae2-4f67-b571-9d05dd55afe2", - "title": "Dangerous function: Direct $_GET usage", - "metadata": { - "function": "Direct $_GET usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_GET usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 21, - "snippet": { - "text": "system(\"ls -la \" . $_GET['directory']);" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_GET usage" - } - } - ], - "properties": { - "findingId": "f22f694f-a90b-4db3-9963-15c1cde45b9e", - "title": "Dangerous function: Direct $_GET usage", - "metadata": { - "function": "Direct $_GET usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_GET usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 23, - "snippet": { - "text": "shell_exec(\"ping \" . $_GET['host']);" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_GET usage" - } - } - ], - "properties": { - "findingId": "0faaf644-0344-4a72-99f0-db753b9ce9fa", - "title": "Dangerous function: Direct $_GET usage", - "metadata": { - "function": "Direct $_GET usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_GET usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 24, - "snippet": { - "text": "passthru(\"ps aux | grep \" . $_GET['process']);" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_GET usage" - } - } - ], - "properties": { - "findingId": "f3d955a5-c4e4-41bf-92dd-8dfc192d05d0", - "title": "Dangerous function: Direct $_GET usage", - "metadata": { - "function": "Direct $_GET usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_GET usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 31, - "snippet": { - "text": "include($_GET['page'] . '.php');" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_GET usage" - } - } - ], - "properties": { - "findingId": "3bd995ce-e253-4f6d-bbd5-3313c88ad46f", - "title": "Dangerous function: Direct $_GET usage", - "metadata": { - "function": "Direct $_GET usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_GET usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 45, - "snippet": { - "text": "echo \"Welcome, \" . $_GET['name'];" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_GET usage" - } - } - ], - "properties": { - "findingId": "fd3c11cd-28cb-4b54-a735-9a5bdc332a5b", - "title": "Dangerous function: Direct $_GET usage", - "metadata": { - "function": "Direct $_GET usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_GET usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 50, - "snippet": { - "text": "$_SESSION['user'] = $_GET['user'];" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_GET usage" - } - } - ], - "properties": { - "findingId": "a03cac8a-b32d-4ef7-8cf1-079cb49990f4", - "title": "Dangerous function: Direct $_GET usage", - "metadata": { - "function": "Direct $_GET usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_GET usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 57, - "snippet": { - "text": "$file = $_GET['file'];" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_GET usage" - } - } - ], - "properties": { - "findingId": "33e1a9cc-f9f9-48b4-bdfb-c69f53070628", - "title": "Dangerous function: Direct $_GET usage", - "metadata": { - "function": "Direct $_GET usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_POST usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 13, - "snippet": { - "text": "$username = $_POST['username'];" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_POST usage" - } - } - ], - "properties": { - "findingId": "53eeb91a-fbff-4751-a564-f6f0051fb7fc", - "title": "Dangerous function: Direct $_POST usage", - "metadata": { - "function": "Direct $_POST usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_POST usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 22, - "snippet": { - "text": "exec(\"cat \" . $_POST['file']);" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_POST usage" - } - } - ], - "properties": { - "findingId": "23b4011b-f3c9-4830-b4a5-d0a841ec6919", - "title": "Dangerous function: Direct $_POST usage", - "metadata": { - "function": "Direct $_POST usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_POST usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 27, - "snippet": { - "text": "$code = $_POST['code'];" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_POST usage" - } - } - ], - "properties": { - "findingId": "21de169a-6bc5-4bba-8f80-cfdd7eb48e46", - "title": "Dangerous function: Direct $_POST usage", - "metadata": { - "function": "Direct $_POST usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_POST usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 32, - "snippet": { - "text": "require_once($_POST['template']);" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_POST usage" - } - } - ], - "properties": { - "findingId": "07f75eb2-6177-47d3-872e-5816e6792cd4", - "title": "Dangerous function: Direct $_POST usage", - "metadata": { - "function": "Direct $_POST usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_POST usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 40, - "snippet": { - "text": "$search = $_POST['search'];" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_POST usage" - } - } - ], - "properties": { - "findingId": "e2eea3e9-291e-4f8b-8812-bb0b1420faa5", - "title": "Dangerous function: Direct $_POST usage", - "metadata": { - "function": "Direct $_POST usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_POST usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 46, - "snippet": { - "text": "print(\"Your search: \" . $_POST['query']);" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_POST usage" - } - } - ], - "properties": { - "findingId": "ce099095-26d9-43f9-b109-96694ae1a58a", - "title": "Dangerous function: Direct $_POST usage", - "metadata": { - "function": "Direct $_POST usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_POST usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 53, - "snippet": { - "text": "$password = md5($_POST['password']); // Weak hashing" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_POST usage" - } - } - ], - "properties": { - "findingId": "c8459eff-68a8-4bf2-ae7d-783cd42baf9b", - "title": "Dangerous function: Direct $_POST usage", - "metadata": { - "function": "Direct $_POST usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_POST usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 54, - "snippet": { - "text": "$encrypted = base64_encode($_POST['sensitive_data']); // Not encryption" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_POST usage" - } - } - ], - "properties": { - "findingId": "6109490b-7d05-4a15-8fb0-a654732ecbe4", - "title": "Dangerous function: Direct $_POST usage", - "metadata": { - "function": "Direct $_POST usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_POST usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 61, - "snippet": { - "text": "$username = $_POST['username'];" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_POST usage" - } - } - ], - "properties": { - "findingId": "a52a06bc-da7a-414d-b9c4-f00735a32d4d", - "title": "Dangerous function: Direct $_POST usage", - "metadata": { - "function": "Direct $_POST usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "dangerous_function_medium", - "level": "warning", - "message": { - "text": "Use of potentially dangerous function Direct $_POST usage: Input validation missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "scripts/deploy.php", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 62, - "snippet": { - "text": "$password = $_POST['password'];" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Consider safer alternatives to Direct $_POST usage" - } - } - ], - "properties": { - "findingId": "d7205d13-889e-40b8-978f-a718b46a849d", - "title": "Dangerous function: Direct $_POST usage", - "metadata": { - "function": "Direct $_POST usage", - "risk": "Input validation missing" - } - } - }, - { - "ruleId": "hardcoded_secret_high", - "level": "error", - "message": { - "text": "Found potential hardcoded API Key in src/utils.rb" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/utils.rb", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 64, - "snippet": { - "text": "ELASTICSEARCH_API_KEY = \"elastic_api_key_789xyz\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded API Key and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "47b6f3dd-4f4f-4255-a225-6147ed9f22ca", - "title": "Hardcoded API Key detected", - "metadata": { - "secret_type": "API Key" - } - } - }, - { - "ruleId": "hardcoded_secret_medium", - "level": "warning", - "message": { - "text": "Found potential hardcoded Hardcoded Password in src/utils.rb" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/utils.rb", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 63, - "snippet": { - "text": "REDIS_PASSWORD = \"redis_cache_password_456\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded Hardcoded Password and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "bb13af1d-c9e3-43b7-b51a-1f47a1b91c64", - "title": "Hardcoded Hardcoded Password detected", - "metadata": { - "secret_type": "Hardcoded Password" - } - } - }, - { - "ruleId": "hardcoded_secret_high", - "level": "error", - "message": { - "text": "Found potential hardcoded Private Key in src/app.go" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/app.go", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 59, - "snippet": { - "text": "const BitcoinPrivateKey = \"5KJvsngHeMpm884wtkJNzQGaCErckhHJBGFsvd3VyK5qMZXj3hS\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded Private Key and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "bb039e28-0b62-4ace-9dee-9015643918f5", - "title": "Hardcoded Private Key detected", - "metadata": { - "secret_type": "Private Key" - } - } - }, - { - "ruleId": "hardcoded_secret_high", - "level": "error", - "message": { - "text": "Found potential hardcoded Private Key in src/app.go" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/app.go", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 62, - "snippet": { - "text": "const EthereumPrivateKey = \"0x4c0883a69102937d6231471b5dbb6204fe512961708279f3e2e1a2e4567890abc\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded Private Key and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "6642fe40-7de1-483b-a026-595c07c781be", - "title": "Hardcoded Private Key detected", - "metadata": { - "secret_type": "Private Key" - } - } - }, - { - "ruleId": "hardcoded_secret_medium", - "level": "warning", - "message": { - "text": "Found potential hardcoded Potential Secret Hash in src/app.go" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/app.go", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 59, - "snippet": { - "text": "const BitcoinPrivateKey = \"5KJvsngHeMpm884wtkJNzQGaCErckhHJBGFsvd3VyK5qMZXj3hS\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded Potential Secret Hash and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "2ddc264a-8dd8-4187-9002-1332294da03e", - "title": "Hardcoded Potential Secret Hash detected", - "metadata": { - "secret_type": "Potential Secret Hash" - } - } - }, - { - "ruleId": "hardcoded_secret_medium", - "level": "warning", - "message": { - "text": "Found potential hardcoded Potential Secret Hash in src/app.go" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "src/app.go", - "uriBaseId": "WORKSPACE" - }, - "region": { - "startLine": 62, - "snippet": { - "text": "const EthereumPrivateKey = \"0x4c0883a69102937d6231471b5dbb6204fe512961708279f3e2e1a2e4567890abc\"" - } - } - } - } - ], - "fixes": [ - { - "description": { - "text": "Remove hardcoded Potential Secret Hash and use environment variables or secure vault" - } - } - ], - "properties": { - "findingId": "7969e0e3-78c5-47c3-89be-8cd761e2853f", - "title": "Hardcoded Potential Secret Hash detected", - "metadata": { - "secret_type": "Potential Secret Hash" - } - } - } - ], - "invocations": [ - { - "executionSuccessful": true, - "endTimeUtc": "2025-09-29T09:02:26.097014Z" - } - ], - "originalUriBaseIds": { - "WORKSPACE": { - "uri": "file:///workspace/", - "description": "The workspace root directory" - } - } - } - ] -} \ No newline at end of file