secure-web/backend/scanner/base.py

170 lines
4.7 KiB
Python

"""
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,
}