""" Base scanner interface and common utilities. All scanner modules inherit from BaseScanner and implement the common interface for running scans and returning results. """ import logging from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any, Optional from enum import Enum logger = logging.getLogger('scanner') class ScannerStatus(Enum): """Status of a scanner execution.""" SUCCESS = 'success' PARTIAL = 'partial' FAILED = 'failed' SKIPPED = 'skipped' @dataclass class ScannerResult: """ Standardized result from any scanner. Attributes: status: The execution status of the scanner scanner_name: Name of the scanner that produced this result scores: Dictionary of score values (0-100) metrics: List of metric dictionaries with name, value, unit issues: List of issue dictionaries with category, severity, title, etc. raw_data: Original response from the scanner tool error_message: Error message if the scan failed execution_time_seconds: How long the scan took """ status: ScannerStatus scanner_name: str scores: dict = field(default_factory=dict) metrics: list = field(default_factory=list) issues: list = field(default_factory=list) raw_data: Optional[dict] = None error_message: Optional[str] = None execution_time_seconds: float = 0.0 def to_dict(self) -> dict: """Convert result to dictionary for serialization.""" return { 'status': self.status.value, 'scanner_name': self.scanner_name, 'scores': self.scores, 'metrics': self.metrics, 'issues': self.issues, 'raw_data': self.raw_data, 'error_message': self.error_message, 'execution_time_seconds': self.execution_time_seconds, } class BaseScanner(ABC): """ Abstract base class for all scanners. Each scanner must implement the `run` method which takes a URL and returns a ScannerResult. """ name: str = "base_scanner" def __init__(self, config: dict = None): """ Initialize the scanner with optional configuration. Args: config: Dictionary of configuration options """ self.config = config or {} self.logger = logging.getLogger(f'scanner.{self.name}') @abstractmethod def run(self, url: str) -> ScannerResult: """ Run the scanner on the given URL. Args: url: The URL to scan Returns: ScannerResult with scan data """ pass def is_available(self) -> bool: """ Check if the scanner is available and properly configured. Returns: True if the scanner can run, False otherwise """ return True def _create_issue( self, category: str, severity: str, title: str, description: str, affected_url: str = None, remediation: str = None, raw_data: dict = None ) -> dict: """ Helper to create a standardized issue dictionary. Args: category: Issue category (security, performance, etc.) severity: Severity level (critical, high, medium, low, info) title: Brief issue title description: Detailed description affected_url: Specific URL affected remediation: Suggested fix raw_data: Original scanner data Returns: Issue dictionary """ return { 'category': category, 'severity': severity, 'title': title, 'description': description, 'affected_url': affected_url, 'remediation': remediation, 'tool': self.name, 'raw_data': raw_data, } def _create_metric( self, name: str, display_name: str, value: float, unit: str, score: float = None ) -> dict: """ Helper to create a standardized metric dictionary. Args: name: Machine-readable metric name display_name: Human-readable name value: Numeric value unit: Unit of measurement score: Optional score (0-1) Returns: Metric dictionary """ return { 'name': name, 'display_name': display_name, 'value': value, 'unit': unit, 'source': self.name, 'score': score, }