170 lines
6.0 KiB
Python
170 lines
6.0 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Security Hardening Script for Church Music Database
|
||
|
|
Implements critical security fixes
|
||
|
|
"""
|
||
|
|
|
||
|
|
import os
|
||
|
|
import secrets
|
||
|
|
import sys
|
||
|
|
import re
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
def generate_secure_key():
|
||
|
|
"""Generate cryptographically secure secret key"""
|
||
|
|
return secrets.token_hex(32)
|
||
|
|
|
||
|
|
def check_env_file_security(env_path):
|
||
|
|
"""Check if .env file has secure permissions"""
|
||
|
|
if not os.path.exists(env_path):
|
||
|
|
return False, "File does not exist"
|
||
|
|
|
||
|
|
stat_info = os.stat(env_path)
|
||
|
|
mode = stat_info.st_mode & 0o777
|
||
|
|
|
||
|
|
if mode != 0o600:
|
||
|
|
return False, f"Insecure permissions: {oct(mode)}. Should be 0600"
|
||
|
|
return True, "OK"
|
||
|
|
|
||
|
|
def secure_env_file(env_path):
|
||
|
|
"""Set secure permissions on .env file"""
|
||
|
|
try:
|
||
|
|
os.chmod(env_path, 0o600)
|
||
|
|
return True
|
||
|
|
except Exception as e:
|
||
|
|
return False, str(e)
|
||
|
|
|
||
|
|
def validate_postgresql_uri(uri):
|
||
|
|
"""Validate PostgreSQL connection string"""
|
||
|
|
if not uri or uri == "":
|
||
|
|
return False, "Empty URI"
|
||
|
|
|
||
|
|
# Check for default/weak passwords
|
||
|
|
weak_patterns = [
|
||
|
|
'your_password',
|
||
|
|
'password',
|
||
|
|
'admin',
|
||
|
|
'123456',
|
||
|
|
'postgres'
|
||
|
|
]
|
||
|
|
|
||
|
|
for pattern in weak_patterns:
|
||
|
|
if pattern.lower() in uri.lower():
|
||
|
|
return False, f"Weak/default password detected: {pattern}"
|
||
|
|
|
||
|
|
# Validate format
|
||
|
|
if not uri.startswith('postgresql://'):
|
||
|
|
return False, "Invalid PostgreSQL URI format"
|
||
|
|
|
||
|
|
return True, "OK"
|
||
|
|
|
||
|
|
def main():
|
||
|
|
print("╔══════════════════════════════════════════════════════════════╗")
|
||
|
|
print("║ SECURITY HARDENING - Critical Fixes ║")
|
||
|
|
print("╚══════════════════════════════════════════════════════════════╝")
|
||
|
|
print()
|
||
|
|
|
||
|
|
project_root = Path(__file__).parent
|
||
|
|
backend_dir = project_root / "backend"
|
||
|
|
env_file = backend_dir / ".env"
|
||
|
|
|
||
|
|
issues_found = []
|
||
|
|
fixes_applied = []
|
||
|
|
|
||
|
|
# Check 1: .env file exists and has secure permissions
|
||
|
|
print("🔒 Checking .env file security...")
|
||
|
|
if env_file.exists():
|
||
|
|
is_secure, msg = check_env_file_security(env_file)
|
||
|
|
if not is_secure:
|
||
|
|
issues_found.append(f".env file: {msg}")
|
||
|
|
if secure_env_file(env_file):
|
||
|
|
fixes_applied.append("Set .env permissions to 0600")
|
||
|
|
print(" ✓ Fixed: Set secure permissions (0600)")
|
||
|
|
else:
|
||
|
|
print(f" ✗ Failed to secure .env file")
|
||
|
|
else:
|
||
|
|
print(" ✓ .env file has secure permissions")
|
||
|
|
else:
|
||
|
|
issues_found.append(".env file does not exist")
|
||
|
|
print(" ⚠ .env file not found. Use .env.template to create one.")
|
||
|
|
|
||
|
|
# Check 2: SECRET_KEY strength
|
||
|
|
print("\n🔑 Checking SECRET_KEY...")
|
||
|
|
if env_file.exists():
|
||
|
|
with open(env_file, 'r') as f:
|
||
|
|
content = f.read()
|
||
|
|
secret_match = re.search(r'SECRET_KEY=(.+)', content)
|
||
|
|
if secret_match:
|
||
|
|
secret_key = secret_match.group(1).strip()
|
||
|
|
if len(secret_key) < 32:
|
||
|
|
issues_found.append(f"SECRET_KEY is too short ({len(secret_key)} chars, need 64+)")
|
||
|
|
print(f" ⚠ SECRET_KEY is weak (length: {len(secret_key)})")
|
||
|
|
print(f" → Generate new key: python3 -c \"import secrets; print(secrets.token_hex(32))\"")
|
||
|
|
else:
|
||
|
|
print(" ✓ SECRET_KEY length is adequate")
|
||
|
|
else:
|
||
|
|
issues_found.append("SECRET_KEY not found in .env")
|
||
|
|
print(" ✗ SECRET_KEY not found")
|
||
|
|
|
||
|
|
# Check 3: Database password strength
|
||
|
|
print("\n🗄️ Checking database password...")
|
||
|
|
if env_file.exists():
|
||
|
|
with open(env_file, 'r') as f:
|
||
|
|
content = f.read()
|
||
|
|
uri_match = re.search(r'POSTGRESQL_URI=(.+)', content)
|
||
|
|
if uri_match:
|
||
|
|
uri = uri_match.group(1).strip()
|
||
|
|
is_valid, msg = validate_postgresql_uri(uri)
|
||
|
|
if not is_valid:
|
||
|
|
issues_found.append(f"Database URI: {msg}")
|
||
|
|
print(f" ✗ {msg}")
|
||
|
|
else:
|
||
|
|
print(" ✓ Database URI appears secure")
|
||
|
|
else:
|
||
|
|
issues_found.append("POSTGRESQL_URI not found")
|
||
|
|
print(" ✗ POSTGRESQL_URI not configured")
|
||
|
|
|
||
|
|
# Check 4: .gitignore exists
|
||
|
|
print("\n📝 Checking .gitignore...")
|
||
|
|
gitignore = project_root / ".gitignore"
|
||
|
|
if gitignore.exists():
|
||
|
|
with open(gitignore, 'r') as f:
|
||
|
|
content = f.read()
|
||
|
|
if '*.env' in content or '.env' in content:
|
||
|
|
print(" ✓ .gitignore protects .env files")
|
||
|
|
else:
|
||
|
|
issues_found.append(".env files not in .gitignore")
|
||
|
|
print(" ✗ .env files not protected by .gitignore")
|
||
|
|
else:
|
||
|
|
issues_found.append(".gitignore does not exist")
|
||
|
|
print(" ✗ .gitignore not found")
|
||
|
|
|
||
|
|
# Summary
|
||
|
|
print("\n" + "="*64)
|
||
|
|
print("SUMMARY")
|
||
|
|
print("="*64)
|
||
|
|
|
||
|
|
if issues_found:
|
||
|
|
print(f"\n⚠️ {len(issues_found)} security issue(s) found:")
|
||
|
|
for i, issue in enumerate(issues_found, 1):
|
||
|
|
print(f" {i}. {issue}")
|
||
|
|
else:
|
||
|
|
print("\n✅ No critical security issues found")
|
||
|
|
|
||
|
|
if fixes_applied:
|
||
|
|
print(f"\n✓ {len(fixes_applied)} fix(es) applied:")
|
||
|
|
for fix in fixes_applied:
|
||
|
|
print(f" • {fix}")
|
||
|
|
|
||
|
|
print("\n📋 NEXT STEPS:")
|
||
|
|
print(" 1. Rotate SECRET_KEY immediately if weak")
|
||
|
|
print(" 2. Update database password if using defaults")
|
||
|
|
print(" 3. Never commit .env files to git")
|
||
|
|
print(" 4. Review all environment variables")
|
||
|
|
print(" 5. Run this script regularly")
|
||
|
|
|
||
|
|
return 0 if not issues_found else 1
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|