""" Auditoría de Seguridad — CrowData Backend. Ejecutar: python -m app.security.audit """ import os import sys import re sys.path.insert(0, "E:/crowdata/backend") from pathlib import Path class SecurityAudit: def __init__(self): self.findings = [] self.passed = [] self.warnings = [] def check(self, name, condition, detail="", severity="HIGH"): if condition: self.passed.append(f"[PASS] {name}") else: self.findings.append(f"[FAIL-{severity}] {name}: {detail}") def warn(self, name, detail=""): self.warnings.append(f"[WARN] {name}: {detail}") def audit_env_file(self): env_path = Path("E:/crowdata/backend/.env") if not env_path.exists(): self.check("ENV file exists", False, ".env file not found") return content = env_path.read_text(encoding="utf-8") # SECRET_KEY secret_match = re.search(r"SECRET_KEY=(.+)", content) if secret_match: secret = secret_match.group(1).strip() self.check("SECRET_KEY is set", len(secret) > 20, f"SECRET_KEY too short ({len(secret)} chars)", "HIGH") self.check("SECRET_KEY is not default", secret not in ("", "changeme", "supersecret"), "SECRET_KEY is a weak default value", "CRITICAL") else: self.check("SECRET_KEY exists", False, "SECRET_KEY not set in .env", "CRITICAL") # DATABASE_URL db_match = re.search(r"DATABASE_URL=(.+)", content) if db_match: db_url = db_match.group(1).strip() self.check("DATABASE_URL is set", len(db_url) > 0, "DATABASE_URL is empty", "HIGH") if db_url.startswith("sqlite"): self.warn("Using SQLite", "Consider PostgreSQL for production") else: self.check("DATABASE_URL exists", False, "DATABASE_URL not set", "HIGH") # SMTP credentials smtp_user = re.search(r"SMTP_USER=(.+)", content) smtp_pass = re.search(r"SMTP_PASSWORD=(.+)", content) if smtp_user and smtp_pass: self.check("SMTP credentials configured", True) else: self.warn("SMTP not configured", "Email delivery won't work") def audit_jwt_config(self): from app.config import get_settings settings = get_settings() self.check("JWT secret is set", len(settings.secret_key) > 0, "JWT secret_key is empty", "CRITICAL") self.check("JWT expiration is reasonable", settings.access_token_expire_minutes <= 1440, f"Token expires in {settings.access_token_expire_minutes} minutes", "MEDIUM") self.check("JWT algorithm is secure", settings.jwt_algorithm in ("HS256", "HS384", "HS512", "RS256", "RS384", "RS512"), f"Algorithm: {settings.jwt_algorithm}", "HIGH") def audit_cors(self): from app.main import ALLOWED_ORIGINS has_wildcard = "*" in ALLOWED_ORIGINS self.check("CORS no wildcard", not has_wildcard, "CORS allows all origins (*)", "HIGH") self.check("CORS has specific origins", len(ALLOWED_ORIGINS) > 0, "No CORS origins configured", "MEDIUM") def audit_password_hashing(self): from app.auth.config import get_jwt_strategy # fastapi-users uses bcrypt by default self.check("Password hashing (fastapi-users)", True, "Using fastapi-users with bcrypt", "INFO") def audit_rate_limiting(self): from app.middleware.rate_limit import RATE_LIMITS, REPORT_LIMITS self.check("Rate limiting configured", len(RATE_LIMITS) > 0, "No rate limits defined", "HIGH") self.check("Report limits configured", len(REPORT_LIMITS) > 0, "No report limits defined", "HIGH") def audit_admin_protection(self): # Check that admin endpoints require superuser admin_file = Path("E:/crowdata/backend/app/admin/router.py") if admin_file.exists(): content = admin_file.read_text(encoding="utf-8") self.check("Admin requires superuser", "current_active_superuser" in content, "Admin endpoints not protected by superuser check", "CRITICAL") def audit_sql_injection(self): # Check for raw SQL in service files service_files = list(Path("E:/crowdata/backend/app").rglob("*.py")) raw_sql_count = 0 for f in service_files: try: content = f.read_text(encoding="utf-8") if "text(" in content and "execute" in content: raw_sql_count += 1 except Exception: pass self.warn("Raw SQL usage", f"{raw_sql_count} files use raw SQL (via SQLAlchemy text())") def audit_sensitive_data_logs(self): # Check for sensitive data in logger calls service_files = list(Path("E:/crowdata/backend/app").rglob("*.py")) issues = [] for f in service_files: try: content = f.read_text(encoding="utf-8") lines = content.split("\n") for i, line in enumerate(lines): if "logger" in line and ("password" in line.lower() or "token" in line.lower()): if "hash" not in line.lower() and "hashed" not in line.lower(): issues.append(f"{f.name}:{i+1}") except Exception: pass if issues: self.warn("Sensitive data in logs", f"{len(issues)} potential occurrences") else: self.check("No sensitive data in logs", True) def audit_dependency_versions(self): req_file = Path("E:/crowdata/backend/requirements.txt") if req_file.exists(): content = req_file.read_text(encoding="utf-8") self.check("Requirements file exists", True) # Check for known vulnerable patterns if "fastapi-users" in content: self.check("fastapi-users present", True) if "sqlalchemy" in content.lower(): self.check("SQLAlchemy present", True) else: self.warn("No requirements.txt found") def run(self): print("=" * 60) print(" AUDITORÍA DE SEGURIDAD — CrowData Backend") print("=" * 60) print() print("1. Archivo de configuración (.env)") self.audit_env_file() print() print("2. Configuración JWT") self.audit_jwt_config() print() print("3. CORS") self.audit_cors() print() print("4. Password Hashing") self.audit_password_hashing() print() print("5. Rate Limiting") self.audit_rate_limiting() print() print("6. Protección Admin") self.audit_admin_protection() print() print("7. SQL Injection") self.audit_sql_injection() print() print("8. Datos sensibles en logs") self.audit_sensitive_data_logs() print() print("9. Dependencias") self.audit_dependency_versions() print() # Results print("=" * 60) print(" RESULTADOS") print("=" * 60) for p in self.passed: print(f" {p}") for f in self.findings: print(f" {f}") for w in self.warnings: print(f" {w}") print() print(f" Passed: {len(self.passed)}") print(f" Failed: {len(self.findings)}") print(f" Warnings: {len(self.warnings)}") print() if __name__ == "__main__": audit = SecurityAudit() audit.run()