secure-web/backend/scanner/scanners/base.py

162 lines
4.4 KiB
Python

"""
Base scanner interface and result structures.
All scanner implementations should inherit from BaseScanner
and return ScannerResult objects.
"""
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from enum import Enum
logger = logging.getLogger(__name__)
class ScannerStatus(str, Enum):
"""Status of a scanner execution."""
SUCCESS = "success"
PARTIAL = "partial"
FAILED = "failed"
SKIPPED = "skipped"
@dataclass
class IssueData:
"""
Represents a single issue found by a scanner.
Attributes:
category: Issue category (security, performance, etc.)
severity: Issue severity (critical, high, medium, low, info)
title: Brief title of the issue
description: Detailed description
tool: The scanner that found this issue
affected_url: Specific URL affected (optional)
remediation: Suggested fix (optional)
raw_data: Original scanner data (optional)
"""
category: str
severity: str
title: str
description: str
tool: str
affected_url: Optional[str] = None
remediation: Optional[str] = None
raw_data: Optional[Dict[str, Any]] = None
@dataclass
class MetricData:
"""
Represents a single metric measured by a scanner.
Attributes:
name: Internal name (e.g., 'first_contentful_paint_ms')
display_name: Human-readable name
value: Numeric value
unit: Unit of measurement
source: The scanner that measured this
score: Normalized score (0-1) if available
"""
name: str
display_name: str
value: float
unit: str
source: str
score: Optional[float] = None
@dataclass
class ScannerResult:
"""
Result of a scanner execution.
Attributes:
scanner_name: Name of the scanner
status: Execution status
issues: List of issues found
metrics: List of metrics measured
scores: Dictionary of category scores
raw_data: Original scanner output
error_message: Error details if failed
"""
scanner_name: str
status: ScannerStatus
issues: List[IssueData] = field(default_factory=list)
metrics: List[MetricData] = field(default_factory=list)
scores: Dict[str, int] = field(default_factory=dict)
raw_data: Optional[Dict[str, Any]] = None
error_message: Optional[str] = None
class BaseScanner(ABC):
"""
Abstract base class for all scanners.
Each scanner implementation must implement the `run` method
which performs the actual scan and returns a ScannerResult.
"""
name: str = "base"
def __init__(self, config: Optional[Dict[str, Any]] = None):
"""
Initialize the scanner with optional configuration.
Args:
config: Scanner-specific configuration dictionary
"""
self.config = config or {}
self.logger = logging.getLogger(f"scanner.{self.name}")
@abstractmethod
def run(self, url: str) -> ScannerResult:
"""
Run the scanner against the given URL.
Args:
url: The URL to scan
Returns:
ScannerResult with findings, metrics, and status
"""
pass
def is_available(self) -> bool:
"""
Check if the scanner service/tool is available.
Returns:
True if the scanner can be used, False otherwise
"""
return True
def _create_error_result(self, error: Exception) -> ScannerResult:
"""
Create a failed result from an exception.
Args:
error: The exception that occurred
Returns:
ScannerResult with failed status
"""
self.logger.error(f"Scanner {self.name} failed: {error}")
return ScannerResult(
scanner_name=self.name,
status=ScannerStatus.FAILED,
error_message=str(error),
issues=[
IssueData(
category="scanner",
severity="info",
title=f"{self.name.title()} scan failed",
description=f"The {self.name} scanner encountered an error: {error}",
tool=self.name,
remediation="Check scanner service configuration and availability."
)
]
)