""" OWASP ZAP Security Scanner Integration. This module integrates with OWASP ZAP (Zed Attack Proxy) to perform security vulnerability scanning. """ import time import logging from typing import Dict, List, Optional import httpx from django.conf import settings from .base import BaseScanner, ScannerResult, ScannerStatus logger = logging.getLogger('scanner') class ZAPScanner(BaseScanner): """ Security scanner using OWASP ZAP. Performs: - Spider crawling - Passive scanning - Active scanning (optional) - Security vulnerability detection """ name = "owasp_zap" # ZAP risk levels mapped to our severity RISK_MAPPING = { '0': 'info', # Informational '1': 'low', # Low '2': 'medium', # Medium '3': 'high', # High } def __init__(self, config: dict = None): super().__init__(config) self.zap_url = self.config.get( 'zap_url', settings.SCANNER_CONFIG.get('ZAP_HOST', 'http://zap:8080') ) self.api_key = self.config.get( 'api_key', settings.SCANNER_CONFIG.get('ZAP_API_KEY', '') ) self.timeout = self.config.get( 'timeout', settings.SCANNER_CONFIG.get('ZAP_TIMEOUT', 120) ) # Whether to run active scan (slower, more intrusive) self.active_scan = self.config.get('active_scan', False) def is_available(self) -> bool: """Check if ZAP is available.""" try: response = httpx.get( f"{self.zap_url}/JSON/core/view/version/", params={'apikey': self.api_key}, timeout=10 ) return response.status_code == 200 except Exception as e: self.logger.warning(f"ZAP not available: {e}") return False def run(self, url: str) -> ScannerResult: """ Run ZAP security scan on the given URL. Args: url: The URL to scan Returns: ScannerResult with security findings """ start_time = time.time() if not self.is_available(): return ScannerResult( status=ScannerStatus.FAILED, scanner_name=self.name, error_message="OWASP ZAP is not available. Check ZAP service configuration.", execution_time_seconds=time.time() - start_time ) try: # Access the URL to seed ZAP self._access_url(url) # Run spider to crawl the site self._run_spider(url) # Wait for passive scan to complete self._wait_for_passive_scan() # Optionally run active scan if self.active_scan: self._run_active_scan(url) # Get alerts alerts = self._get_alerts(url) # Process alerts into issues issues = self._process_alerts(alerts) # Calculate security score based on findings scores = self._calculate_scores(issues) raw_data = { 'alerts': alerts, 'alert_count': len(alerts), 'scan_type': 'active' if self.active_scan else 'passive', } execution_time = time.time() - start_time return ScannerResult( status=ScannerStatus.SUCCESS, scanner_name=self.name, scores=scores, issues=issues, raw_data=raw_data, execution_time_seconds=execution_time ) except httpx.TimeoutException: return ScannerResult( status=ScannerStatus.FAILED, scanner_name=self.name, error_message="ZAP scan timed out", execution_time_seconds=time.time() - start_time ) except Exception as e: logger.exception(f"ZAP scan failed for {url}") return ScannerResult( status=ScannerStatus.FAILED, scanner_name=self.name, error_message=f"ZAP scan error: {e}", execution_time_seconds=time.time() - start_time ) def _zap_request(self, endpoint: str, params: dict = None) -> dict: """Make a request to ZAP API.""" params = params or {} params['apikey'] = self.api_key response = httpx.get( f"{self.zap_url}{endpoint}", params=params, timeout=self.timeout ) response.raise_for_status() return response.json() def _access_url(self, url: str): """Access URL through ZAP to initialize scanning.""" self.logger.info(f"Accessing URL through ZAP: {url}") self._zap_request('/JSON/core/action/accessUrl/', {'url': url}) time.sleep(2) # Give ZAP time to process def _run_spider(self, url: str): """Run ZAP spider to crawl the site.""" self.logger.info(f"Starting ZAP spider for: {url}") # Start spider result = self._zap_request('/JSON/spider/action/scan/', { 'url': url, 'maxChildren': '10', # Limit crawl depth 'recurse': 'true', }) scan_id = result.get('scan') if not scan_id: return # Wait for spider to complete (with timeout) max_wait = 60 # seconds waited = 0 while waited < max_wait: status = self._zap_request('/JSON/spider/view/status/', {'scanId': scan_id}) progress = int(status.get('status', '100')) if progress >= 100: break time.sleep(2) waited += 2 self.logger.info("Spider completed") def _wait_for_passive_scan(self): """Wait for passive scanning to complete.""" self.logger.info("Waiting for passive scan...") max_wait = 30 waited = 0 while waited < max_wait: result = self._zap_request('/JSON/pscan/view/recordsToScan/') records = int(result.get('recordsToScan', '0')) if records == 0: break time.sleep(2) waited += 2 self.logger.info("Passive scan completed") def _run_active_scan(self, url: str): """Run active security scan (more intrusive).""" self.logger.info(f"Starting active scan for: {url}") result = self._zap_request('/JSON/ascan/action/scan/', { 'url': url, 'recurse': 'true', 'inScopeOnly': 'false', }) scan_id = result.get('scan') if not scan_id: return # Wait for active scan (with timeout) max_wait = 120 waited = 0 while waited < max_wait: status = self._zap_request('/JSON/ascan/view/status/', {'scanId': scan_id}) progress = int(status.get('status', '100')) if progress >= 100: break time.sleep(5) waited += 5 self.logger.info("Active scan completed") def _get_alerts(self, url: str) -> List[dict]: """Get all security alerts for the URL.""" result = self._zap_request('/JSON/core/view/alerts/', { 'baseurl': url, 'start': '0', 'count': '100', # Limit alerts }) return result.get('alerts', []) def _process_alerts(self, alerts: List[dict]) -> List[dict]: """Convert ZAP alerts to our issue format.""" issues = [] for alert in alerts: risk = alert.get('risk', '0') severity = self.RISK_MAPPING.get(risk, 'info') # Determine category based on alert type category = self._categorize_alert(alert) # Build remediation from ZAP's solution remediation = alert.get('solution', '') if alert.get('reference'): remediation += f"\n\nReferences: {alert.get('reference')}" issues.append(self._create_issue( category=category, severity=severity, title=alert.get('alert', 'Unknown vulnerability'), description=alert.get('description', ''), affected_url=alert.get('url'), remediation=remediation.strip() if remediation else None, raw_data={ 'pluginId': alert.get('pluginId'), 'cweid': alert.get('cweid'), 'wascid': alert.get('wascid'), 'evidence': alert.get('evidence', '')[:200], # Truncate 'param': alert.get('param'), 'attack': alert.get('attack', '')[:200], 'confidence': alert.get('confidence'), } )) return issues def _categorize_alert(self, alert: dict) -> str: """Categorize ZAP alert into our categories.""" alert_name = alert.get('alert', '').lower() cwe_id = alert.get('cweid', '') # XSS related if 'xss' in alert_name or 'cross-site scripting' in alert_name or cwe_id == '79': return 'security' # SQL Injection if 'sql' in alert_name and 'injection' in alert_name or cwe_id == '89': return 'security' # Header related if any(h in alert_name for h in ['header', 'csp', 'hsts', 'x-frame', 'x-content-type']): return 'headers' # Cookie related if 'cookie' in alert_name: return 'security' # TLS/SSL related if any(t in alert_name for t in ['ssl', 'tls', 'certificate', 'https']): return 'tls' # CORS related if 'cors' in alert_name or 'cross-origin' in alert_name: return 'cors' # Default to security return 'security' def _calculate_scores(self, issues: List[dict]) -> dict: """Calculate security score based on issues found.""" # Start at 100, deduct based on severity score = 100 severity_deductions = { 'critical': 25, 'high': 15, 'medium': 8, 'low': 3, 'info': 1, } for issue in issues: severity = issue.get('severity', 'info') score -= severity_deductions.get(severity, 0) return { 'zap_security': max(0, min(100, score)) }