Comprehensive refactoring to address security vulnerabilities, improve error handling, enhance Pydantic model usage, strengthen logic robustness, and add environment-based configuration support with BaseSettings.
The implementation will transform the codebase from a basic scaffolding tool into a production-ready, security-hardened application that follows modern Python best practices. This includes adding subprocess security with timeouts, structured error handling with custom exception types, enhanced Pydantic models with proper validation, environment-based configuration with precedence rules, atomic operations with rollback capability, and comprehensive logging with context.
Key improvements:
- Security: Subprocess timeouts, path validation, input sanitization, secure file permissions
- Configuration: BaseSettings for environment/file-based config with CLI override precedence
- Error Handling: Custom exception hierarchy, structured logging, rollback on failure
- Models: Enhanced validators, field constraints, computed fields, proper serialization
- Logic: Atomic operations, TOCTOU race condition fixes, resource verification
- Observability: Structured logging, operation metrics, progress tracking
New type definitions for security, configuration, error handling, and subprocess management.
Security Types:
from typing import Literal, TypeAlias, Protocol
from enum import Enum
# Subprocess execution result
class SubprocessResult(BaseModel):
"""Result from subprocess execution."""
command: list[str]
returncode: int
stdout: str
stderr: str
duration: float
timed_out: bool
# Security severity levels
class SecurityLevel(str, Enum):
"""Security check severity levels."""
LOW = "LOW"
MEDIUM = "MEDIUM"
HIGH = "HIGH"
# Operation timeouts
class TimeoutConfig(BaseModel):
"""Timeout configuration for operations."""
git_operations: int = Field(default=30, ge=1, le=300)
package_install: int = Field(default=600, ge=60, le=3600)
test_execution: int = Field(default=300, ge=30, le=1800)
docs_build: int = Field(default=180, ge=30, le=900)Configuration Types:
from pydantic import Field, field_validator, ConfigDict, computed_field
from pydantic_settings import BaseSettings, SettingsConfigDict
# Settings for environment-based configuration
class ScaffolderSettings(BaseSettings):
"""Application settings with environment variable support."""
model_config = SettingsConfigDict(
env_prefix="SCAFFOLD_",
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore"
)
# Timeout configurations
timeout_git: int = Field(default=30, ge=1, le=300)
timeout_install: int = Field(default=600, ge=60, le=3600)
timeout_test: int = Field(default=300, ge=30, le=1800)
timeout_docs: int = Field(default=180, ge=30, le=900)
# Security settings
security_fail_level: SecurityLevel = Field(default=SecurityLevel.MEDIUM)
validate_binaries: bool = Field(default=True)
# Logging settings
log_level: str = Field(default="INFO")
log_file: Path | None = Field(default=None)Error Types:
# Custom exception hierarchy
class ScaffolderError(Exception):
"""Base exception for scaffolder errors."""
def __init__(self, message: str, context: dict[str, Any] | None = None):
super().__init__(message)
self.context = context or {}
class ValidationError(ScaffolderError):
"""Validation-related errors."""
pass
class SecurityError(ScaffolderError):
"""Security-related errors."""
pass
class SubprocessError(ScaffolderError):
"""Subprocess execution errors."""
def __init__(self, message: str, result: SubprocessResult, context: dict[str, Any] | None = None):
super().__init__(message, context)
self.result = result
class FileSystemError(ScaffolderError):
"""File system operation errors."""
pass
class RollbackError(ScaffolderError):
"""Errors during rollback operations."""
passModel Enhancements:
from pydantic import EmailStr, HttpUrl, field_serializer
class ProjectConfig(BaseModel):
"""Enhanced configuration model with proper validation."""
model_config = ConfigDict(
validate_assignment=True,
str_strip_whitespace=True,
frozen=False,
extra="forbid"
)
package_name: str = Field(
...,
description="Valid Python package name",
min_length=1,
max_length=100,
pattern=r"^[a-zA-Z_][a-zA-Z0-9_]*$"
)
target_dir: Path = Field(..., description="Absolute path to parent directory")
author_name: str = Field(default="Your Name", min_length=1, max_length=200)
author_email: EmailStr = Field(default="your.email@example.com")
description: str = Field(default="A new Python package", max_length=500)
license_type: Literal["MIT", "Apache-2.0", "GPL-3.0", "BSD-3-Clause"] = Field(default="MIT")
github_username: str = Field(default="your-username", pattern=r"^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$")
@computed_field
@property
def destination_path(self) -> Path:
"""Get the full destination path."""
return self.target_dir / self.package_name
@computed_field
@property
def github_url(self) -> str:
"""Get the GitHub repository URL."""
return f"https://github.com/{self.github_username}/{self.package_name}"Files to create, modify, and enhance throughout the codebase.
New Files:
python_project_deployment/config.py- BaseSettings configuration managementpython_project_deployment/exceptions.py- Custom exception hierarchypython_project_deployment/security.py- Security utilities and validationpython_project_deployment/subprocess_runner.py- Secure subprocess execution wrapperpython_project_deployment/rollback.py- Transaction and rollback managementpython_project_deployment/logging_config.py- Structured logging configurationtests/test_config.py- Tests for configuration managementtests/test_exceptions.py- Tests for exception handlingtests/test_security.py- Tests for security utilitiestests/test_subprocess_runner.py- Tests for subprocess executiontests/test_rollback.py- Tests for rollback functionality.scaffold.env.example- Example environment configuration file
Modified Files:
python_project_deployment/models.py- Enhanced with validators, EmailStr, computed fields, ConfigDictpython_project_deployment/scaffolder.py- Add rollback, security checks, structured logging, subprocess wrapperpython_project_deployment/cli.py- Add config file support, improved error messages, precedence handlingpython_project_deployment/__init__.py- Export new modules and exceptionstests/test_models.py- Add tests for new validators and computed fieldstests/test_scaffolder.py- Add security tests, rollback tests, error handling testspyproject.toml- Add pydantic-settings dependencyREADME.md- Document new configuration options and security features
Configuration Files:
.scaffold.env.example- Example showing all available environment variables- Template update:
python_project_deployment/templates/.env.j2- Include timeout and security settings
New functions and modifications to existing functions for security, configuration, and error handling.
New Functions in config.py:
def load_settings(env_file: Path | None = None) -> ScaffolderSettings:
"""Load settings with optional env file override."""
def merge_cli_with_settings(cli_args: dict[str, Any], settings: ScaffolderSettings) -> dict[str, Any]:
"""Merge CLI arguments with settings, CLI takes precedence."""New Functions in security.py:
def validate_path_traversal(path: Path, base_path: Path) -> Path:
"""Ensure path doesn't escape base directory."""
def sanitize_template_value(value: str, max_length: int = 1000) -> str:
"""Sanitize values before template rendering."""
def validate_binary(binary_path: Path, expected_name: str) -> bool:
"""Verify binary is what it claims to be."""
def set_secure_permissions(path: Path, is_directory: bool = False) -> None:
"""Set secure file/directory permissions."""New Functions in subprocess_runner.py:
def run_command(
command: list[str],
cwd: Path,
timeout: int,
capture_output: bool = True,
check: bool = True,
env: dict[str, str] | None = None
) -> SubprocessResult:
"""Execute command with security checks and timeout."""
def validate_command(command: list[str]) -> None:
"""Validate command before execution."""New Functions in rollback.py:
class RollbackManager:
"""Manages rollback operations for failed scaffolding."""
def __init__(self, destination: Path):
self.destination = destination
self.operations: list[Callable[[], None]] = []
def register_operation(self, rollback_fn: Callable[[], None]) -> None:
"""Register a rollback operation."""
def execute_rollback(self) -> None:
"""Execute all registered rollback operations."""
def __enter__(self) -> "RollbackManager":
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
if exc_type is not None:
self.execute_rollback()
return FalseModified Functions in scaffolder.py:
def __init__(self, config: ProjectConfig, settings: ScaffolderSettings) -> None:
"""Initialize with config and settings."""
def scaffold(self) -> Path:
"""Execute scaffolding with rollback on failure."""
# Use RollbackManager context
def _create_directory_structure(self, dest: Path, rollback: RollbackManager) -> None:
"""Create directories with atomic operations and rollback registration."""
def _validate_prerequisites(self) -> None:
"""Validate git, uv/pip availability before starting."""
def _execute_subprocess(
self,
command: list[str],
description: str,
timeout: int,
cwd: Path | None = None
) -> SubprocessResult:
"""Wrapper for subprocess execution with logging."""Modified Functions in models.py:
@field_validator("package_name", mode="after")
@classmethod
def validate_package_name_reserved(cls, v: str) -> str:
"""Check against Python reserved keywords."""
@field_validator("target_dir", mode="after")
@classmethod
def validate_target_dir_writable(cls, v: Path) -> Path:
"""Verify write permissions before validation."""
@field_validator("author_email", mode="before")
@classmethod
def normalize_email(cls, v: str | EmailStr) -> EmailStr:
"""Normalize and validate email."""
@field_serializer("target_dir", when_used="json")
def serialize_path(self, value: Path) -> str:
"""Serialize Path to string for JSON."""
def to_template_context(self) -> dict[str, str]:
"""Use model_dump with sanitization."""Modified Functions in cli.py:
def load_config_from_file(config_file: Path | None) -> ScaffolderSettings:
"""Load configuration from file."""
def merge_configurations(
cli_config: ProjectConfig,
env_settings: ScaffolderSettings
) -> tuple[ProjectConfig, ScaffolderSettings]:
"""Merge CLI args with environment settings (CLI precedence)."""
@click.command()
@click.option("--config-file", type=click.Path(exists=True), help="Path to .env config file")
def main(..., config_file: str | None) -> None:
"""Updated to support config file and environment variables."""New classes and modifications to existing classes for enhanced functionality.
New Classes:
config.py:
ScaffolderSettings(BaseSettings)- Application settings with environment variable supportTimeoutConfig(BaseModel)- Centralized timeout configuration
exceptions.py:
ScaffolderError(Exception)- Base exceptionValidationError(ScaffolderError)- Validation failuresSecurityError(ScaffolderError)- Security violationsSubprocessError(ScaffolderError)- Subprocess failuresFileSystemError(ScaffolderError)- File system errorsRollbackError(ScaffolderError)- Rollback failures
subprocess_runner.py:
SubprocessResult(BaseModel)- Structured subprocess resultSubprocessRunner- Secure subprocess execution manager
rollback.py:
RollbackManager- Transaction and rollback coordination
logging_config.py:
StructuredLogger- Contextual structured logging
Modified Classes:
models.py - ProjectConfig:
- Add
model_config = ConfigDict(...)for Pydantic v2 configuration - Add
EmailStrfor author_email validation - Add
Literaltype for license_type constraint - Add
@computed_fieldfor destination_path and github_url - Add
@field_serializerfor Path serialization - Add enhanced validators with better error messages
- Add
__repr__and__str__methods
scaffolder.py - Scaffolder:
- Add
settings: ScaffolderSettingsparameter to__init__ - Add
rollback_manager: RollbackManageras instance variable - Add
subprocess_runner: SubprocessRunnerfor secure execution - Add
logger: StructuredLoggerfor contextual logging - Update all methods to use structured logging
- Add
_validate_prerequisites()method - Add
_cleanup_on_failure()method - Refactor subprocess calls to use
SubprocessRunner
New dependencies and version updates for enhanced functionality.
Add to pyproject.toml:
[project]
dependencies = [
"click>=8.3.0",
"jinja2>=3.1.6",
"pydantic>=2.12.3",
"pydantic-settings>=2.10.0", # NEW: For BaseSettings
"python-dotenv>=1.0.0", # NEW: For .env file support
"email-validator>=2.2.0", # NEW: For EmailStr validation
]
[project.optional-dependencies]
dev = [
"pytest>=8.4.2",
"pytest-cov>=7.0.0",
"pytest-timeout>=2.3.0", # NEW: For testing timeouts
"pytest-mock>=3.14.0", # NEW: For mocking
"black>=25.9.0",
"isort>=7.0.0",
"mypy>=1.18.2",
"ruff>=0.14.3",
"pre-commit>=4.3.0",
"bandit>=1.8.6",
"safety>=3.6.2",
"sphinx>=8.2.3",
"sphinx-rtd-theme>=3.0.2",
"sphinx-autodoc-typehints>=3.5.2",
]Rationale:
pydantic-settings: Enables BaseSettings for environment-based configurationpython-dotenv: Provides .env file loading supportemail-validator: Required for EmailStr validationpytest-timeout: Enables timeout testingpytest-mock: Simplifies mocking in tests
New test files and modifications to existing tests for comprehensive coverage.
New Test Files:
tests/test_config.py:
test_load_settings_from_env()- Load from environment variablestest_load_settings_from_file()- Load from .env filetest_settings_precedence()- Verify environment > file precedencetest_invalid_timeout_values()- Validate timeout constraintstest_merge_cli_with_settings()- CLI override precedence
tests/test_exceptions.py:
test_exception_hierarchy()- Verify exception inheritancetest_exception_context()- Test context preservationtest_subprocess_error_details()- Verify SubprocessError includes result
tests/test_security.py:
test_path_traversal_detection()- Detect ../ attackstest_path_traversal_symlink()- Detect symlink attackstest_sanitize_template_value()- Test sanitizationtest_validate_binary()- Binary validationtest_secure_permissions()- File permission setting
tests/test_subprocess_runner.py:
test_command_timeout()- Verify timeout enforcementtest_command_validation()- Reject invalid commandstest_command_execution_success()- Successful executiontest_command_execution_failure()- Handle failurestest_subprocess_result_capture()- Capture stdout/stderr
tests/test_rollback.py:
test_rollback_on_failure()- Execute rollback operationstest_rollback_order()- Verify LIFO execution ordertest_rollback_partial_failure()- Handle rollback errorstest_context_manager_usage()- Test context manager protocol
Modified Test Files:
tests/test_models.py:
- Add
test_email_validation()- Test EmailStr validator - Add
test_computed_fields()- Test destination_path and github_url - Add
test_license_type_constraint()- Test Literal constraint - Add
test_github_username_pattern()- Test username validation - Add
test_field_serialization()- Test Path serialization - Add
test_model_config()- Test ConfigDict settings - Add
test_reserved_keyword_rejection()- Test Python keyword check - Add
test_target_dir_writable()- Test write permission check
tests/test_scaffolder.py:
- Add
test_scaffolder_rollback_on_git_failure()- Rollback test - Add
test_scaffolder_timeout_handling()- Timeout test - Add
test_scaffolder_prerequisites_validation()- Prerequisite check - Add
test_scaffolder_with_settings()- Settings integration - Add
test_subprocess_security()- Security validation - Add
test_atomic_directory_creation()- Atomic operation test - Fix
test_template_files_created()- Remove requirements.txt check - Add
test_file_permissions()- Verify secure permissions
Logical sequence of changes to minimize conflicts and ensure successful integration.
-
Create exception hierarchy (
python_project_deployment/exceptions.py)- Establish base ScaffolderError and derived exceptions
- Add context preservation and error details
- Write tests in
tests/test_exceptions.py
-
Create configuration management (
python_project_deployment/config.py)- Implement ScaffolderSettings with BaseSettings
- Add TimeoutConfig model
- Add load_settings and merge functions
- Write tests in
tests/test_config.py - Update pyproject.toml with pydantic-settings dependency
-
Create security utilities (
python_project_deployment/security.py)- Implement path traversal validation
- Add template value sanitization
- Add binary validation
- Add secure permission setting
- Write tests in
tests/test_security.py
-
Create subprocess runner (
python_project_deployment/subprocess_runner.py)- Implement SubprocessResult model
- Create SubprocessRunner class with timeout enforcement
- Add command validation
- Write tests in
tests/test_subprocess_runner.py
-
Create rollback manager (
python_project_deployment/rollback.py)- Implement RollbackManager with context manager protocol
- Add operation registration and execution
- Write tests in
tests/test_rollback.py
-
Create logging configuration (
python_project_deployment/logging_config.py)- Implement StructuredLogger with context support
- Add correlation ID tracking
- Configure formatters and handlers
-
Enhance ProjectConfig model (
python_project_deployment/models.py)- Add ConfigDict configuration
- Replace author_email with EmailStr
- Add Literal constraint for license_type
- Add computed_field decorators
- Add enhanced validators
- Add field_serializer for Path
- Add repr and str methods
- Update tests in
tests/test_models.py
-
Update Scaffolder class (
python_project_deployment/scaffolder.py)- Add settings parameter to init
- Integrate RollbackManager in scaffold() method
- Replace subprocess.run with SubprocessRunner
- Add _validate_prerequisites() method
- Add structured logging throughout
- Update all subprocess calls with timeouts
- Add security validation calls
- Update tests in
tests/test_scaffolder.py
-
Update CLI (
python_project_deployment/cli.py)- Add --config-file option
- Implement load_config_from_file()
- Implement merge_configurations()
- Update main() to load settings and merge with CLI args
- Improve error message formatting
- Add config precedence documentation
-
Update package exports (
python_project_deployment/__init__.py)- Export new exception classes
- Export ScaffolderSettings
- Export SubprocessRunner
- Update all list
-
Create example configuration (
.scaffold.env.example)- Document all available environment variables
- Include timeout configurations
- Include security settings
- Add usage examples
-
Update documentation (
README.md, docs)- Document new configuration options
- Document environment variable support
- Document precedence rules (CLI > ENV > defaults)
- Add security best practices section
- Update examples with config file usage
-
Update templates (
python_project_deployment/templates/)- Update .env.j2 to include timeout settings
- Ensure pyproject.toml.j2 uses latest Pydantic patterns
-
Run full test suite and fix issues
- Run pytest with coverage
- Fix any test failures
- Ensure coverage > 90%
- Run mypy type checking
- Run security scans (bandit, safety)
-
Final validation and documentation
- Verify all features work end-to-end
- Update CHANGELOG.md
- Update version in init.py and pyproject.toml
- Review and finalize all docstrings