|
|
"""Environment variable validation and suggestions.""" |
|
|
|
|
|
from __future__ import annotations |
|
|
|
|
|
import os |
|
|
import re |
|
|
from pathlib import Path |
|
|
from typing import Any, Dict, List, Optional |
|
|
|
|
|
|
|
|
class EnvironmentValidator: |
|
|
"""Validates and suggests environment variables.""" |
|
|
|
|
|
def __init__(self): |
|
|
self.common_vars = { |
|
|
"next.js": ["NEXT_PUBLIC_API_URL", "DATABASE_URL", "NEXTAUTH_SECRET", "NEXTAUTH_URL"], |
|
|
"django": ["SECRET_KEY", "DEBUG", "DATABASE_URL", "ALLOWED_HOSTS"], |
|
|
"fastapi": ["DATABASE_URL", "SECRET_KEY", "CORS_ORIGINS", "ENVIRONMENT"], |
|
|
"react": ["REACT_APP_API_URL", "REACT_APP_ENV"], |
|
|
"express": ["PORT", "NODE_ENV", "DATABASE_URL", "JWT_SECRET"], |
|
|
"nestjs": ["PORT", "DATABASE_URL", "JWT_SECRET", "NODE_ENV"], |
|
|
} |
|
|
|
|
|
def validate_env_file(self, folder_path: str, framework: Optional[str] = None) -> Dict[str, Any]: |
|
|
"""Validate .env files in codebase.""" |
|
|
path = Path(folder_path) |
|
|
env_files = list(path.rglob(".env*")) |
|
|
|
|
|
found_vars = [] |
|
|
missing_vars = [] |
|
|
issues = [] |
|
|
|
|
|
|
|
|
for env_file in env_files: |
|
|
if env_file.is_file() and ".git" not in str(env_file): |
|
|
try: |
|
|
content = env_file.read_text() |
|
|
|
|
|
var_pattern = r'^([A-Z_][A-Z0-9_]*)\s*=' |
|
|
vars_in_file = re.findall(var_pattern, content, re.MULTILINE) |
|
|
found_vars.extend(vars_in_file) |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
|
|
|
if framework: |
|
|
required_vars = self.common_vars.get(framework.lower(), []) |
|
|
for var in required_vars: |
|
|
if var not in found_vars: |
|
|
missing_vars.append({ |
|
|
"variable": var, |
|
|
"required": True, |
|
|
"description": self._get_var_description(var, framework) |
|
|
}) |
|
|
|
|
|
|
|
|
for env_file in env_files: |
|
|
try: |
|
|
content = env_file.read_text() |
|
|
|
|
|
if re.search(r'(password|secret|key|token)\s*=\s*["\'][^"\']+["\']', content, re.IGNORECASE): |
|
|
issues.append({ |
|
|
"file": str(env_file.relative_to(path)), |
|
|
"severity": "high", |
|
|
"issue": "Hardcoded secrets detected - use environment variables or secrets manager" |
|
|
}) |
|
|
|
|
|
|
|
|
gitignore = path / ".gitignore" |
|
|
if gitignore.exists(): |
|
|
gitignore_content = gitignore.read_text() |
|
|
if ".env" not in gitignore_content: |
|
|
issues.append({ |
|
|
"file": ".gitignore", |
|
|
"severity": "medium", |
|
|
"issue": ".env files should be in .gitignore" |
|
|
}) |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
return { |
|
|
"env_files_found": len(env_files), |
|
|
"variables_found": len(set(found_vars)), |
|
|
"missing_required": missing_vars, |
|
|
"issues": issues, |
|
|
"status": "valid" if not missing_vars and not issues else "needs_attention", |
|
|
"recommendations": self._get_recommendations(framework, missing_vars, issues) |
|
|
} |
|
|
|
|
|
def suggest_env_vars(self, framework: str, platform: str) -> List[Dict[str, str]]: |
|
|
"""Suggest environment variables based on framework and platform.""" |
|
|
suggestions = [] |
|
|
|
|
|
|
|
|
framework_vars = self.common_vars.get(framework.lower(), []) |
|
|
for var in framework_vars: |
|
|
suggestions.append({ |
|
|
"variable": var, |
|
|
"required": True, |
|
|
"description": self._get_var_description(var, framework), |
|
|
"category": "framework" |
|
|
}) |
|
|
|
|
|
|
|
|
platform_vars = { |
|
|
"vercel": ["VERCEL_URL", "VERCEL_ENV"], |
|
|
"netlify": ["NETLIFY", "CONTEXT"], |
|
|
"aws": ["AWS_REGION", "AWS_ACCESS_KEY_ID"], |
|
|
"gcp": ["GOOGLE_CLOUD_PROJECT", "GCP_REGION"], |
|
|
"azure": ["AZURE_REGION", "AZURE_SUBSCRIPTION_ID"], |
|
|
} |
|
|
|
|
|
platform_vars_list = platform_vars.get(platform.lower(), []) |
|
|
for var in platform_vars_list: |
|
|
suggestions.append({ |
|
|
"variable": var, |
|
|
"required": False, |
|
|
"description": f"Platform-specific variable for {platform}", |
|
|
"category": "platform" |
|
|
}) |
|
|
|
|
|
return suggestions |
|
|
|
|
|
def _get_var_description(self, var: str, framework: str) -> str: |
|
|
"""Get description for a variable.""" |
|
|
descriptions = { |
|
|
"DATABASE_URL": "Database connection string", |
|
|
"SECRET_KEY": "Secret key for encryption/signing", |
|
|
"API_URL": "API endpoint URL", |
|
|
"NODE_ENV": "Node.js environment (development/production)", |
|
|
"PORT": "Application port number", |
|
|
"DEBUG": "Debug mode flag", |
|
|
} |
|
|
return descriptions.get(var, f"Required for {framework}") |
|
|
|
|
|
def _get_recommendations( |
|
|
self, |
|
|
framework: Optional[str], |
|
|
missing_vars: List[Dict], |
|
|
issues: List[Dict] |
|
|
) -> List[str]: |
|
|
"""Get recommendations based on validation results.""" |
|
|
recommendations = [] |
|
|
|
|
|
if missing_vars: |
|
|
recommendations.append(f"Add {len(missing_vars)} missing required environment variables") |
|
|
|
|
|
if issues: |
|
|
recommendations.append("Review security issues in environment files") |
|
|
|
|
|
recommendations.append("Use a secrets manager (AWS Secrets Manager, HashiCorp Vault) for production") |
|
|
recommendations.append("Never commit .env files to version control") |
|
|
|
|
|
if framework: |
|
|
recommendations.append(f"Follow {framework} best practices for environment configuration") |
|
|
|
|
|
return recommendations |
|
|
|
|
|
|