fix: Resolve lint errors in validate_config.py

- Fix E501 line too long errors by breaking long lines appropriately
- Fix F541 f-string without placeholders error
- Add proper type annotations for mypy compatibility
- Suppress false positive bandit security warning for valid host check
- Apply black and isort formatting fixes

All flake8, mypy, black, isort, and bandit checks now pass.
This commit is contained in:
William Valentin
2025-09-15 02:35:53 -07:00
parent 0f891aab2d
commit f38e0c1276

View File

@@ -12,13 +12,13 @@ Usage:
python scripts/validate_config.py --check-all python scripts/validate_config.py --check-all
""" """
import os
import sys
import json
import argparse import argparse
from pathlib import Path import json
from typing import List, Dict, Any, Tuple, Optional import os
import re import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
class ConfigValidator: class ConfigValidator:
@@ -42,17 +42,17 @@ class ConfigValidator:
def _load_env_file(self, env_file: str) -> None: def _load_env_file(self, env_file: str) -> None:
"""Load environment variables from file.""" """Load environment variables from file."""
try: try:
with open(env_file, 'r', encoding='utf-8') as f: with open(env_file, "r", encoding="utf-8") as f:
for line_num, line in enumerate(f, 1): for line_num, line in enumerate(f, 1):
line = line.strip() line = line.strip()
if line and not line.startswith('#') and '=' in line: if line and not line.startswith("#") and "=" in line:
key, value = line.split('=', 1) key, value = line.split("=", 1)
key = key.strip() key = key.strip()
value = value.strip().strip('"\'') value = value.strip().strip("\"'")
# Remove inline comments # Remove inline comments
if '#' in value: if "#" in value:
value = value.split('#')[0].strip() value = value.split("#")[0].strip()
# Don't override existing environment variables # Don't override existing environment variables
if key not in self.original_env: if key not in self.original_env:
@@ -71,15 +71,20 @@ class ConfigValidator:
if not value: if not value:
return True # Empty is valid (uses default) return True # Empty is valid (uses default)
valid_true = ('true', '1', 'yes', 'on') valid_true = ("true", "1", "yes", "on")
valid_false = ('false', '0', 'no', 'off') valid_false = ("false", "0", "no", "off")
if value.lower() not in valid_true + valid_false: if value.lower() not in valid_true + valid_false:
self.errors.append(f"{key}: Invalid boolean value '{value}'. Use: {valid_true + valid_false}") valid_options = valid_true + valid_false
self.errors.append(
f"{key}: Invalid boolean value '{value}'. " f"Use: {valid_options}"
)
return False return False
return True return True
def _validate_integer(self, key: str, value: str, min_val: int = 0, max_val: int = None) -> bool: def _validate_integer(
self, key: str, value: str, min_val: int = 0, max_val: Optional[int] = None
) -> bool:
"""Validate integer environment variable.""" """Validate integer environment variable."""
if not value: if not value:
return True # Empty is valid (uses default) return True # Empty is valid (uses default)
@@ -103,12 +108,14 @@ class ConfigValidator:
return True # Empty is valid for optional URLs return True # Empty is valid for optional URLs
url_pattern = re.compile( url_pattern = re.compile(
r'^https?://' # http:// or https:// r"^https?://" # http:// or https://
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|' # domain r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|"
r'localhost|' # localhost r"localhost|" # localhost
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # IP r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})" # IP
r'(?::\d+)?' # optional port r"(?::\d+)?" # optional port
r'(?:/?|[/?]\S+)$', re.IGNORECASE) r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
if not url_pattern.match(value): if not url_pattern.match(value):
self.errors.append(f"{key}: Invalid URL format '{value}'") self.errors.append(f"{key}: Invalid URL format '{value}'")
@@ -120,7 +127,7 @@ class ConfigValidator:
if not value: if not value:
return True # Empty is valid for optional emails return True # Empty is valid for optional emails
email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$') email_pattern = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
if not email_pattern.match(value): if not email_pattern.match(value):
self.errors.append(f"{key}: Invalid email format '{value}'") self.errors.append(f"{key}: Invalid email format '{value}'")
return False return False
@@ -134,13 +141,18 @@ class ConfigValidator:
try: try:
parsed = json.loads(value) parsed = json.loads(value)
if not isinstance(parsed, list): if not isinstance(parsed, list):
self.errors.append(f"{key}: Must be a JSON array, got {type(parsed).__name__}") self.errors.append(
f"{key}: Must be a JSON array, got {type(parsed).__name__}"
)
return False return False
return True return True
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
# Try comma-separated format # Try comma-separated format
if ',' in value and not value.startswith('['): if "," in value and not value.startswith("["):
self.warnings.append(f"{key}: Using comma-separated format. Consider JSON array format for clarity") self.warnings.append(
f"{key}: Using comma-separated format. "
"Consider JSON array format for clarity"
)
return True return True
self.errors.append(f"{key}: Invalid JSON array format: {e}") self.errors.append(f"{key}: Invalid JSON array format: {e}")
return False return False
@@ -150,9 +162,11 @@ class ConfigValidator:
if not value: if not value:
return True return True
valid_levels = ('debug', 'info', 'warning', 'error', 'critical') valid_levels = ("debug", "info", "warning", "error", "critical")
if value.lower() not in valid_levels: if value.lower() not in valid_levels:
self.errors.append(f"{key}: Invalid log level '{value}'. Valid levels: {valid_levels}") self.errors.append(
f"{key}: Invalid log level '{value}'. Valid levels: {valid_levels}"
)
return False return False
return True return True
@@ -161,9 +175,11 @@ class ConfigValidator:
if not value: if not value:
return True return True
valid_envs = ('development', 'staging', 'production', 'test') valid_envs = ("development", "staging", "production", "test")
if value.lower() not in valid_envs: if value.lower() not in valid_envs:
self.warnings.append(f"{key}: Unusual environment '{value}'. Common values: {valid_envs}") self.warnings.append(
f"{key}: Unusual environment '{value}'. Common values: {valid_envs}"
)
return True return True
def _validate_file_extensions(self, key: str, value: str) -> bool: def _validate_file_extensions(self, key: str, value: str) -> bool:
@@ -172,7 +188,7 @@ class ConfigValidator:
return True return True
try: try:
if value.startswith('['): if value.startswith("["):
# JSON array format # JSON array format
extensions = json.loads(value) extensions = json.loads(value)
if not isinstance(extensions, list): if not isinstance(extensions, list):
@@ -180,11 +196,13 @@ class ConfigValidator:
return False return False
else: else:
# Comma-separated format # Comma-separated format
extensions = [ext.strip() for ext in value.split(',')] extensions = [ext.strip() for ext in value.split(",")]
for ext in extensions: for ext in extensions:
if not ext.startswith('.'): if not ext.startswith("."):
self.errors.append(f"{key}: Extension '{ext}' should start with '.'") self.errors.append(
f"{key}: Extension '{ext}' should start with '.'"
)
return False return False
return True return True
except Exception as e: except Exception as e:
@@ -196,45 +214,48 @@ class ConfigValidator:
print("🔍 Validating basic configuration...") print("🔍 Validating basic configuration...")
# Application Information # Application Information
app_name = self._get_env_value('APP_NAME') app_name = self._get_env_value("APP_NAME")
if not app_name: if not app_name:
self.warnings.append("APP_NAME: Not set, using default") self.warnings.append("APP_NAME: Not set, using default")
elif len(app_name) > 100: elif len(app_name) > 100:
self.warnings.append("APP_NAME: Very long name (>100 chars)") self.warnings.append("APP_NAME: Very long name (>100 chars)")
app_version = self._get_env_value('APP_VERSION') app_version = self._get_env_value("APP_VERSION")
if app_version and not re.match(r'^\d+\.\d+\.\d+', app_version): if app_version and not re.match(r"^\d+\.\d+\.\d+", app_version):
self.warnings.append(f"APP_VERSION: Unusual version format '{app_version}'") self.warnings.append(f"APP_VERSION: Unusual version format '{app_version}'")
# URLs # URLs
self._validate_url('GITHUB_URL', self._get_env_value('GITHUB_URL')) self._validate_url("GITHUB_URL", self._get_env_value("GITHUB_URL"))
self._validate_url('DOCUMENTATION_URL', self._get_env_value('DOCUMENTATION_URL')) self._validate_url(
self._validate_url('BUG_REPORTS_URL', self._get_env_value('BUG_REPORTS_URL')) "DOCUMENTATION_URL", self._get_env_value("DOCUMENTATION_URL")
)
self._validate_url("BUG_REPORTS_URL", self._get_env_value("BUG_REPORTS_URL"))
# Email # Email
self._validate_email('CONTACT_EMAIL', self._get_env_value('CONTACT_EMAIL')) self._validate_email("CONTACT_EMAIL", self._get_env_value("CONTACT_EMAIL"))
def validate_server_config(self) -> None: def validate_server_config(self) -> None:
"""Validate server configuration.""" """Validate server configuration."""
print("🔍 Validating server configuration...") print("🔍 Validating server configuration...")
# Boolean values # Boolean values
self._validate_boolean('DEBUG', self._get_env_value('DEBUG')) self._validate_boolean("DEBUG", self._get_env_value("DEBUG"))
self._validate_boolean('RELOAD', self._get_env_value('RELOAD')) self._validate_boolean("RELOAD", self._get_env_value("RELOAD"))
# Environment # Environment
self._validate_environment('ENVIRONMENT', self._get_env_value('ENVIRONMENT')) self._validate_environment("ENVIRONMENT", self._get_env_value("ENVIRONMENT"))
self._validate_log_level('LOG_LEVEL', self._get_env_value('LOG_LEVEL')) self._validate_log_level("LOG_LEVEL", self._get_env_value("LOG_LEVEL"))
# Integer values # Integer values
self._validate_integer('PORT', self._get_env_value('PORT'), 1, 65535) self._validate_integer("PORT", self._get_env_value("PORT"), 1, 65535)
self._validate_integer('WORKERS', self._get_env_value('WORKERS'), 1, 64) self._validate_integer("WORKERS", self._get_env_value("WORKERS"), 1, 64)
# Host validation # Host validation
host = self._get_env_value('HOST') host = self._get_env_value("HOST")
if host and host not in ('0.0.0.0', '127.0.0.1', 'localhost'): valid_hosts = ("0.0.0.0", "127.0.0.1", "localhost") # nosec B104
if host and host not in valid_hosts:
# Basic IP validation # Basic IP validation
if not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', host): if not re.match(r"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", host):
self.warnings.append(f"HOST: Unusual host value '{host}'") self.warnings.append(f"HOST: Unusual host value '{host}'")
def validate_security_config(self) -> None: def validate_security_config(self) -> None:
@@ -242,45 +263,66 @@ class ConfigValidator:
print("🔍 Validating security configuration...") print("🔍 Validating security configuration...")
# Secret key # Secret key
secret_key = self._get_env_value('SECRET_KEY') secret_key = self._get_env_value("SECRET_KEY")
env = self._get_env_value('ENVIRONMENT', 'production').lower() env = self._get_env_value("ENVIRONMENT", "production").lower()
if secret_key: if secret_key:
if len(secret_key) < 32: if len(secret_key) < 32:
self.warnings.append("SECRET_KEY: Too short, use at least 32 characters") self.warnings.append(
if secret_key in ('your-secret-key-here', 'dev-secret-key-change-in-production'): "SECRET_KEY: Too short, use at least 32 characters"
if env == 'production': )
self.errors.append("SECRET_KEY: Using default/example secret key in production!") example_keys = (
"your-secret-key-here",
"dev-secret-key-change-in-production",
)
if secret_key in example_keys:
if env == "production":
self.errors.append(
"SECRET_KEY: Using default/example secret key " "in production!"
)
else: else:
self.warnings.append("SECRET_KEY: Using example secret key (OK for development)") self.warnings.append(
elif env == 'production': "SECRET_KEY: Using example secret key " "(OK for development)"
)
elif env == "production":
self.warnings.append("SECRET_KEY: Not set in production environment") self.warnings.append("SECRET_KEY: Not set in production environment")
# Boolean security settings # Boolean security settings
self._validate_boolean('SECURITY_HEADERS', self._get_env_value('SECURITY_HEADERS')) self._validate_boolean(
self._validate_boolean('CSP_ENABLED', self._get_env_value('CSP_ENABLED')) "SECURITY_HEADERS", self._get_env_value("SECURITY_HEADERS")
)
self._validate_boolean("CSP_ENABLED", self._get_env_value("CSP_ENABLED"))
# HSTS settings # HSTS settings
self._validate_integer('HSTS_MAX_AGE', self._get_env_value('HSTS_MAX_AGE'), 0) self._validate_integer("HSTS_MAX_AGE", self._get_env_value("HSTS_MAX_AGE"), 0)
# CORS # CORS
cors_origins = self._get_env_value('CORS_ORIGINS') cors_origins = self._get_env_value("CORS_ORIGINS")
if cors_origins != '*': if cors_origins != "*":
self._validate_json_array('CORS_ORIGINS', cors_origins) self._validate_json_array("CORS_ORIGINS", cors_origins)
# Allowed hosts # Allowed hosts
self._validate_json_array('ALLOWED_HOSTS', self._get_env_value('ALLOWED_HOSTS')) self._validate_json_array("ALLOWED_HOSTS", self._get_env_value("ALLOWED_HOSTS"))
def validate_feature_flags(self) -> None: def validate_feature_flags(self) -> None:
"""Validate feature flags.""" """Validate feature flags."""
print("🔍 Validating feature flags...") print("🔍 Validating feature flags...")
feature_flags = [ feature_flags = [
'ENABLE_API_METRICS', 'ENABLE_REQUEST_LOGGING', "ENABLE_API_METRICS",
'ENABLE_TEMPLATE_CACHING', 'ENABLE_VALIDATION_CACHING', "ENABLE_REQUEST_LOGGING",
'HEALTH_CHECK_ENABLED', 'METRICS_ENABLED', 'TRACING_ENABLED', "ENABLE_TEMPLATE_CACHING",
'API_DOCS_ENABLED', 'SWAGGER_UI_ENABLED', 'REDOC_ENABLED', "ENABLE_VALIDATION_CACHING",
'HOT_RELOAD', 'SOURCE_MAPS', 'MINIFY_ASSETS', 'COMPRESS_RESPONSES' "HEALTH_CHECK_ENABLED",
"METRICS_ENABLED",
"TRACING_ENABLED",
"API_DOCS_ENABLED",
"SWAGGER_UI_ENABLED",
"REDOC_ENABLED",
"HOT_RELOAD",
"SOURCE_MAPS",
"MINIFY_ASSETS",
"COMPRESS_RESPONSES",
] ]
for flag in feature_flags: for flag in feature_flags:
@@ -291,64 +333,107 @@ class ConfigValidator:
print("🔍 Validating performance configuration...") print("🔍 Validating performance configuration...")
# Timeout settings # Timeout settings
self._validate_integer('REQUEST_TIMEOUT', self._get_env_value('REQUEST_TIMEOUT'), 1, 3600) self._validate_integer(
self._validate_integer('KEEPALIVE_TIMEOUT', self._get_env_value('KEEPALIVE_TIMEOUT'), 1, 300) "REQUEST_TIMEOUT", self._get_env_value("REQUEST_TIMEOUT"), 1, 3600
self._validate_integer('MAX_CONNECTIONS', self._get_env_value('MAX_CONNECTIONS'), 1, 10000) )
self._validate_integer(
"KEEPALIVE_TIMEOUT", self._get_env_value("KEEPALIVE_TIMEOUT"), 1, 300
)
self._validate_integer(
"MAX_CONNECTIONS", self._get_env_value("MAX_CONNECTIONS"), 1, 10000
)
# Cache settings # Cache settings
self._validate_integer('TEMPLATE_CACHE_TTL', self._get_env_value('TEMPLATE_CACHE_TTL'), 0) self._validate_integer(
self._validate_integer('VALIDATION_CACHE_TTL', self._get_env_value('VALIDATION_CACHE_TTL'), 0) "TEMPLATE_CACHE_TTL", self._get_env_value("TEMPLATE_CACHE_TTL"), 0
)
self._validate_integer(
"VALIDATION_CACHE_TTL", self._get_env_value("VALIDATION_CACHE_TTL"), 0
)
# File upload # File upload
self._validate_integer('MAX_UPLOAD_SIZE', self._get_env_value('MAX_UPLOAD_SIZE'), 1024) self._validate_integer(
self._validate_file_extensions('ALLOWED_EXTENSIONS', self._get_env_value('ALLOWED_EXTENSIONS')) "MAX_UPLOAD_SIZE", self._get_env_value("MAX_UPLOAD_SIZE"), 1024
)
self._validate_file_extensions(
"ALLOWED_EXTENSIONS", self._get_env_value("ALLOWED_EXTENSIONS")
)
def validate_production_readiness(self) -> None: def validate_production_readiness(self) -> None:
"""Validate production readiness.""" """Validate production readiness."""
print("🔍 Validating production readiness...") print("🔍 Validating production readiness...")
env = self._get_env_value('ENVIRONMENT', 'production').lower() env = self._get_env_value("ENVIRONMENT", "production").lower()
debug = self._get_env_value('DEBUG', 'false').lower() in ('true', '1', 'yes', 'on') debug = self._get_env_value("DEBUG", "false").lower() in (
"true",
"1",
"yes",
"on",
)
if env == 'production': if env == "production":
if debug: if debug:
self.errors.append("Production environment should not have DEBUG=true") self.errors.append("Production environment should not have DEBUG=true")
if not self._get_env_value('SECRET_KEY'): if not self._get_env_value("SECRET_KEY"):
self.errors.append("Production environment must have SECRET_KEY set") self.errors.append("Production environment must have SECRET_KEY set")
cors_origins = self._get_env_value('CORS_ORIGINS', '*') cors_origins = self._get_env_value("CORS_ORIGINS", "*")
if cors_origins == '*': if cors_origins == "*":
self.warnings.append("Production environment using wildcard CORS origins") self.warnings.append(
"Production environment using wildcard CORS origins"
)
if self._get_env_value('SECURITY_HEADERS', 'true').lower() not in ('true', '1', 'yes', 'on'): security_headers = self._get_env_value("SECURITY_HEADERS", "true").lower()
self.warnings.append("Production environment should enable SECURITY_HEADERS") if security_headers not in ("true", "1", "yes", "on"):
self.warnings.append(
"Production environment should enable SECURITY_HEADERS"
)
workers = int(self._get_env_value('WORKERS', '4')) workers = int(self._get_env_value("WORKERS", "4"))
if workers < 2: if workers < 2:
self.warnings.append("Production environment should use multiple workers") self.warnings.append(
"Production environment should use multiple workers"
)
def check_environment_consistency(self) -> None: def check_environment_consistency(self) -> None:
"""Check for environment consistency issues.""" """Check for environment consistency issues."""
print("🔍 Checking environment consistency...") print("🔍 Checking environment consistency...")
# Debug mode consistency # Debug mode consistency
debug = self._get_env_value('DEBUG', 'false').lower() in ('true', '1', 'yes', 'on') debug = self._get_env_value("DEBUG", "false").lower() in (
env = self._get_env_value('ENVIRONMENT', 'production').lower() "true",
log_level = self._get_env_value('LOG_LEVEL', 'info').lower() "1",
"yes",
"on",
)
env = self._get_env_value("ENVIRONMENT", "production").lower()
log_level = self._get_env_value("LOG_LEVEL", "info").lower()
if debug and env == 'production': if debug and env == "production":
self.warnings.append("DEBUG mode enabled in production environment") self.warnings.append("DEBUG mode enabled in production environment")
if debug and log_level not in ('debug', 'info'): if debug and log_level not in ("debug", "info"):
self.warnings.append("DEBUG mode with non-debug log level") self.warnings.append("DEBUG mode with non-debug log level")
# Performance consistency # Performance consistency
hot_reload = self._get_env_value('HOT_RELOAD', 'false').lower() in ('true', '1', 'yes', 'on') hot_reload = self._get_env_value("HOT_RELOAD", "false").lower() in (
minify = self._get_env_value('MINIFY_ASSETS', 'true').lower() in ('true', '1', 'yes', 'on') "true",
"1",
"yes",
"on",
)
minify = self._get_env_value("MINIFY_ASSETS", "true").lower() in (
"true",
"1",
"yes",
"on",
)
if hot_reload and minify: if hot_reload and minify:
self.warnings.append("HOT_RELOAD and MINIFY_ASSETS both enabled (unusual for development)") self.warnings.append(
"HOT_RELOAD and MINIFY_ASSETS both enabled " "(unusual for development)"
)
def validate_all(self) -> Tuple[int, int, int]: def validate_all(self) -> Tuple[int, int, int]:
"""Run all validations and return counts.""" """Run all validations and return counts."""
@@ -366,9 +451,9 @@ class ConfigValidator:
def print_results(self) -> None: def print_results(self) -> None:
"""Print validation results.""" """Print validation results."""
print("\n" + "="*60) print("\n" + "=" * 60)
print("🔍 CONFIGURATION VALIDATION RESULTS") print("🔍 CONFIGURATION VALIDATION RESULTS")
print("="*60) print("=" * 60)
if self.info: if self.info:
print(f"\n📋 Information ({len(self.info)}):") print(f"\n📋 Information ({len(self.info)}):")
@@ -385,7 +470,7 @@ class ConfigValidator:
for msg in self.errors: for msg in self.errors:
print(f"{msg}") print(f"{msg}")
print(f"\n📊 Summary:") print("\n📊 Summary:")
print(f" • Errors: {len(self.errors)}") print(f" • Errors: {len(self.errors)}")
print(f" • Warnings: {len(self.warnings)}") print(f" • Warnings: {len(self.warnings)}")
print(f" • Info: {len(self.info)}") print(f" • Info: {len(self.info)}")
@@ -397,37 +482,33 @@ class ConfigValidator:
else: else:
print("\n❌ Configuration validation failed.") print("\n❌ Configuration validation failed.")
print("="*60) print("=" * 60)
def main(): def main() -> None:
"""Main function.""" """Main function."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Validate UnitForge environment configuration", description="Validate UnitForge environment configuration",
formatter_class=argparse.RawDescriptionHelpFormatter formatter_class=argparse.RawDescriptionHelpFormatter,
) )
parser.add_argument( parser.add_argument(
'--env-file', "--env-file", type=str, help="Path to environment file to load (default: .env)"
type=str,
help="Path to environment file to load (default: .env)"
) )
parser.add_argument( parser.add_argument(
'--check-all', "--check-all",
action='store_true', action="store_true",
help="Run all validation checks including production readiness" help="Run all validation checks including production readiness",
) )
parser.add_argument( parser.add_argument(
'--quiet', "--quiet", action="store_true", help="Suppress informational output"
action='store_true',
help="Suppress informational output"
) )
args = parser.parse_args() args = parser.parse_args()
# Default to .env if it exists # Default to .env if it exists
env_file = args.env_file env_file = args.env_file
if not env_file and Path('.env').exists(): if not env_file and Path(".env").exists():
env_file = '.env' env_file = ".env"
validator = ConfigValidator(env_file) validator = ConfigValidator(env_file)