
- Add PathValidator class for preventing path traversal attacks - Add SecureSubprocessRunner for safe command execution - Replace unsafe XML parsing with defusedxml for security - Add comprehensive input validation tools for circuit generation - Include security dependencies (defusedxml, bandit) in pyproject.toml - Add security scanning job to CI/CD pipeline - Add comprehensive test coverage for security utilities - Add timeout constants for safe operation limits - Add boundary validation for component positioning This establishes a strong security foundation for the KiCad MCP server by implementing defense-in-depth security measures across all input vectors and external process interactions. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
366 lines
13 KiB
Python
366 lines
13 KiB
Python
"""
|
||
Boundary validation system for KiCad circuit generation.
|
||
|
||
Provides comprehensive validation for component positioning, boundary checking,
|
||
and validation report generation to prevent out-of-bounds placement issues.
|
||
"""
|
||
|
||
from dataclasses import dataclass
|
||
from enum import Enum
|
||
import json
|
||
from typing import Any
|
||
|
||
from kicad_mcp.utils.component_layout import ComponentLayoutManager, SchematicBounds
|
||
from kicad_mcp.utils.coordinate_converter import CoordinateConverter, validate_position
|
||
|
||
|
||
class ValidationSeverity(Enum):
|
||
"""Severity levels for validation issues."""
|
||
|
||
ERROR = "error"
|
||
WARNING = "warning"
|
||
INFO = "info"
|
||
|
||
|
||
@dataclass
|
||
class ValidationIssue:
|
||
"""Represents a validation issue found during boundary checking."""
|
||
|
||
severity: ValidationSeverity
|
||
component_ref: str
|
||
message: str
|
||
position: tuple[float, float]
|
||
suggested_position: tuple[float, float] | None = None
|
||
component_type: str = "default"
|
||
|
||
|
||
@dataclass
|
||
class ValidationReport:
|
||
"""Comprehensive validation report for circuit positioning."""
|
||
|
||
success: bool
|
||
issues: list[ValidationIssue]
|
||
total_components: int
|
||
validated_components: int
|
||
out_of_bounds_count: int
|
||
corrected_positions: dict[str, tuple[float, float]]
|
||
|
||
def has_errors(self) -> bool:
|
||
"""Check if report contains any error-level issues."""
|
||
return any(issue.severity == ValidationSeverity.ERROR for issue in self.issues)
|
||
|
||
def has_warnings(self) -> bool:
|
||
"""Check if report contains any warning-level issues."""
|
||
return any(issue.severity == ValidationSeverity.WARNING for issue in self.issues)
|
||
|
||
def get_issues_by_severity(self, severity: ValidationSeverity) -> list[ValidationIssue]:
|
||
"""Get all issues of a specific severity level."""
|
||
return [issue for issue in self.issues if issue.severity == severity]
|
||
|
||
|
||
class BoundaryValidator:
|
||
"""
|
||
Comprehensive boundary validation system for KiCad circuit generation.
|
||
|
||
Features:
|
||
- Pre-generation coordinate validation
|
||
- Automatic position correction
|
||
- Detailed validation reports
|
||
- Integration with circuit generation pipeline
|
||
"""
|
||
|
||
def __init__(self, bounds: SchematicBounds | None = None):
|
||
"""
|
||
Initialize the boundary validator.
|
||
|
||
Args:
|
||
bounds: Schematic boundaries (defaults to A4)
|
||
"""
|
||
self.bounds = bounds or SchematicBounds()
|
||
self.converter = CoordinateConverter()
|
||
self.layout_manager = ComponentLayoutManager(self.bounds)
|
||
|
||
def validate_component_position(
|
||
self, component_ref: str, x: float, y: float, component_type: str = "default"
|
||
) -> ValidationIssue:
|
||
"""
|
||
Validate a single component position.
|
||
|
||
Args:
|
||
component_ref: Component reference (e.g., "R1")
|
||
x: X coordinate in mm
|
||
y: Y coordinate in mm
|
||
component_type: Type of component
|
||
|
||
Returns:
|
||
ValidationIssue describing the validation result
|
||
"""
|
||
# Check if position is within A4 bounds
|
||
if not validate_position(x, y, use_margins=True):
|
||
# Find a corrected position
|
||
corrected_x, corrected_y = self.layout_manager.find_valid_position(
|
||
component_ref, component_type, x, y
|
||
)
|
||
|
||
return ValidationIssue(
|
||
severity=ValidationSeverity.ERROR,
|
||
component_ref=component_ref,
|
||
message=f"Component {component_ref} at ({x:.2f}, {y:.2f}) is outside A4 bounds",
|
||
position=(x, y),
|
||
suggested_position=(corrected_x, corrected_y),
|
||
component_type=component_type,
|
||
)
|
||
|
||
# Check if position is within usable area (with margins)
|
||
if not validate_position(x, y, use_margins=False):
|
||
# Position is within absolute bounds but outside usable area
|
||
return ValidationIssue(
|
||
severity=ValidationSeverity.WARNING,
|
||
component_ref=component_ref,
|
||
message=f"Component {component_ref} at ({x:.2f}, {y:.2f}) is outside usable area (margins)",
|
||
position=(x, y),
|
||
component_type=component_type,
|
||
)
|
||
|
||
# Position is valid
|
||
return ValidationIssue(
|
||
severity=ValidationSeverity.INFO,
|
||
component_ref=component_ref,
|
||
message=f"Component {component_ref} position is valid",
|
||
position=(x, y),
|
||
component_type=component_type,
|
||
)
|
||
|
||
def validate_circuit_components(self, components: list[dict[str, Any]]) -> ValidationReport:
|
||
"""
|
||
Validate positioning for all components in a circuit.
|
||
|
||
Args:
|
||
components: List of component dictionaries with position information
|
||
|
||
Returns:
|
||
ValidationReport with comprehensive validation results
|
||
"""
|
||
issues = []
|
||
corrected_positions = {}
|
||
out_of_bounds_count = 0
|
||
|
||
# Reset layout manager for this validation
|
||
self.layout_manager.clear_layout()
|
||
|
||
for component in components:
|
||
component_ref = component.get("reference", "Unknown")
|
||
component_type = component.get("component_type", "default")
|
||
|
||
# Extract position - handle different formats
|
||
position = component.get("position")
|
||
if position is None:
|
||
# No position specified - this is an info issue
|
||
issues.append(
|
||
ValidationIssue(
|
||
severity=ValidationSeverity.INFO,
|
||
component_ref=component_ref,
|
||
message=f"Component {component_ref} has no position specified",
|
||
position=(0, 0),
|
||
component_type=component_type,
|
||
)
|
||
)
|
||
continue
|
||
|
||
# Handle position as tuple or list
|
||
if isinstance(position, list | tuple) and len(position) >= 2:
|
||
x, y = float(position[0]), float(position[1])
|
||
else:
|
||
issues.append(
|
||
ValidationIssue(
|
||
severity=ValidationSeverity.ERROR,
|
||
component_ref=component_ref,
|
||
message=f"Component {component_ref} has invalid position format: {position}",
|
||
position=(0, 0),
|
||
component_type=component_type,
|
||
)
|
||
)
|
||
continue
|
||
|
||
# Validate the position
|
||
validation_issue = self.validate_component_position(component_ref, x, y, component_type)
|
||
issues.append(validation_issue)
|
||
|
||
# Track out of bounds components
|
||
if validation_issue.severity == ValidationSeverity.ERROR:
|
||
out_of_bounds_count += 1
|
||
if validation_issue.suggested_position:
|
||
corrected_positions[component_ref] = validation_issue.suggested_position
|
||
|
||
# Generate report
|
||
report = ValidationReport(
|
||
success=out_of_bounds_count == 0,
|
||
issues=issues,
|
||
total_components=len(components),
|
||
validated_components=len([c for c in components if c.get("position") is not None]),
|
||
out_of_bounds_count=out_of_bounds_count,
|
||
corrected_positions=corrected_positions,
|
||
)
|
||
|
||
return report
|
||
|
||
def validate_wire_connection(
|
||
self, start_x: float, start_y: float, end_x: float, end_y: float
|
||
) -> list[ValidationIssue]:
|
||
"""
|
||
Validate wire connection endpoints.
|
||
|
||
Args:
|
||
start_x: Starting X coordinate in mm
|
||
start_y: Starting Y coordinate in mm
|
||
end_x: Ending X coordinate in mm
|
||
end_y: Ending Y coordinate in mm
|
||
|
||
Returns:
|
||
List of validation issues for wire endpoints
|
||
"""
|
||
issues = []
|
||
|
||
# Validate start point
|
||
if not validate_position(start_x, start_y, use_margins=True):
|
||
issues.append(
|
||
ValidationIssue(
|
||
severity=ValidationSeverity.ERROR,
|
||
component_ref="WIRE_START",
|
||
message=f"Wire start point ({start_x:.2f}, {start_y:.2f}) is outside bounds",
|
||
position=(start_x, start_y),
|
||
)
|
||
)
|
||
|
||
# Validate end point
|
||
if not validate_position(end_x, end_y, use_margins=True):
|
||
issues.append(
|
||
ValidationIssue(
|
||
severity=ValidationSeverity.ERROR,
|
||
component_ref="WIRE_END",
|
||
message=f"Wire end point ({end_x:.2f}, {end_y:.2f}) is outside bounds",
|
||
position=(end_x, end_y),
|
||
)
|
||
)
|
||
|
||
return issues
|
||
|
||
def auto_correct_positions(
|
||
self, components: list[dict[str, Any]]
|
||
) -> tuple[list[dict[str, Any]], ValidationReport]:
|
||
"""
|
||
Automatically correct out-of-bounds component positions.
|
||
|
||
Args:
|
||
components: List of component dictionaries
|
||
|
||
Returns:
|
||
Tuple of (corrected_components, validation_report)
|
||
"""
|
||
# First validate to get correction suggestions
|
||
validation_report = self.validate_circuit_components(components)
|
||
|
||
# Apply corrections
|
||
corrected_components = []
|
||
for component in components:
|
||
component_ref = component.get("reference", "Unknown")
|
||
|
||
if component_ref in validation_report.corrected_positions:
|
||
# Apply correction
|
||
corrected_component = component.copy()
|
||
corrected_component["position"] = validation_report.corrected_positions[
|
||
component_ref
|
||
]
|
||
corrected_components.append(corrected_component)
|
||
else:
|
||
corrected_components.append(component)
|
||
|
||
return corrected_components, validation_report
|
||
|
||
def generate_validation_report_text(self, report: ValidationReport) -> str:
|
||
"""
|
||
Generate a human-readable validation report.
|
||
|
||
Args:
|
||
report: ValidationReport to format
|
||
|
||
Returns:
|
||
Formatted text report
|
||
"""
|
||
lines = []
|
||
lines.append("=" * 60)
|
||
lines.append("BOUNDARY VALIDATION REPORT")
|
||
lines.append("=" * 60)
|
||
|
||
# Summary
|
||
lines.append(f"Status: {'PASS' if report.success else 'FAIL'}")
|
||
lines.append(f"Total Components: {report.total_components}")
|
||
lines.append(f"Validated Components: {report.validated_components}")
|
||
lines.append(f"Out of Bounds: {report.out_of_bounds_count}")
|
||
lines.append(f"Corrected Positions: {len(report.corrected_positions)}")
|
||
lines.append("")
|
||
|
||
# Issues by severity
|
||
errors = report.get_issues_by_severity(ValidationSeverity.ERROR)
|
||
warnings = report.get_issues_by_severity(ValidationSeverity.WARNING)
|
||
info = report.get_issues_by_severity(ValidationSeverity.INFO)
|
||
|
||
if errors:
|
||
lines.append("ERRORS:")
|
||
for issue in errors:
|
||
lines.append(f" ❌ {issue.message}")
|
||
if issue.suggested_position:
|
||
lines.append(f" → Suggested: {issue.suggested_position}")
|
||
lines.append("")
|
||
|
||
if warnings:
|
||
lines.append("WARNINGS:")
|
||
for issue in warnings:
|
||
lines.append(f" ⚠️ {issue.message}")
|
||
lines.append("")
|
||
|
||
if info:
|
||
lines.append("INFO:")
|
||
for issue in info:
|
||
lines.append(f" ℹ️ {issue.message}")
|
||
lines.append("")
|
||
|
||
# Corrected positions
|
||
if report.corrected_positions:
|
||
lines.append("CORRECTED POSITIONS:")
|
||
for component_ref, (x, y) in report.corrected_positions.items():
|
||
lines.append(f" {component_ref}: ({x:.2f}, {y:.2f})")
|
||
|
||
return "\n".join(lines)
|
||
|
||
def export_validation_report(self, report: ValidationReport, filepath: str) -> None:
|
||
"""
|
||
Export validation report to JSON file.
|
||
|
||
Args:
|
||
report: ValidationReport to export
|
||
filepath: Path to output file
|
||
"""
|
||
# Convert report to serializable format
|
||
export_data = {
|
||
"success": report.success,
|
||
"total_components": report.total_components,
|
||
"validated_components": report.validated_components,
|
||
"out_of_bounds_count": report.out_of_bounds_count,
|
||
"corrected_positions": report.corrected_positions,
|
||
"issues": [
|
||
{
|
||
"severity": issue.severity.value,
|
||
"component_ref": issue.component_ref,
|
||
"message": issue.message,
|
||
"position": issue.position,
|
||
"suggested_position": issue.suggested_position,
|
||
"component_type": issue.component_type,
|
||
}
|
||
for issue in report.issues
|
||
],
|
||
}
|
||
|
||
with open(filepath, "w") as f:
|
||
json.dump(export_data, f, indent=2)
|