Spaces:
Running
Running
| """ | |
| app.py β Qualora Enterprise Quality Auditor | |
| =========================================== | |
| Core Flask app with modular architecture for MongoDB write-through, | |
| Strict HTTP-Only JWT auth, RAG pipeline (Atlas Vector), and | |
| multi-tier LLM fallback cascade. Hardened for Vercel deployment. | |
| """ | |
| import os | |
| # Ensure ChromaDB telemetry is disabled as early as possible so imported | |
| # libraries (including chromadb) do not attempt PostHog capture calls. | |
| os.environ.setdefault("CHROMADB_DISABLE_TELEMETRY", "1") | |
| os.environ.setdefault("CHROMA_DISABLE_TELEMETRY", "1") | |
| os.environ.setdefault("ANONYMIZED_TELEMETRY", "True") | |
| import sys | |
| import jwt | |
| from datetime import datetime, timezone | |
| from flask import Flask, request, jsonify, send_from_directory, render_template, redirect, url_for, make_response | |
| from flask_cors import CORS | |
| from services.security import generate_csrf_token, decode_token | |
| # ββ Import infrastructure βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| from core import ( | |
| BASE_DIR, ALLOWED_ORIGIN, MAX_CONTENT_LENGTH, DEBUG, GITHUB_URL, | |
| JWT_SECRET, get_logger, log_request_middleware, get_db, get_db_status, LOG_LEVEL | |
| ) | |
| # Import blueprints with graceful fallback if any fail | |
| try: | |
| from routes.auth_bp import auth_bp | |
| except Exception as e: | |
| auth_bp = None | |
| import traceback | |
| print(f"ERROR: Failed to import auth_bp: {e}\n{traceback.format_exc()}") | |
| try: | |
| from routes.audits_bp import audits_bp | |
| except Exception as e: | |
| audits_bp = None | |
| import traceback | |
| print(f"ERROR: Failed to import audits_bp: {e}\n{traceback.format_exc()}") | |
| try: | |
| from routes.kb_bp import kb_bp | |
| except Exception as e: | |
| kb_bp = None | |
| import traceback | |
| print(f"ERROR: Failed to import kb_bp: {e}\n{traceback.format_exc()}") | |
| try: | |
| from routes.admin_bp import admin_bp | |
| except Exception as e: | |
| admin_bp = None | |
| import traceback | |
| print(f"ERROR: Failed to import admin_bp: {e}\n{traceback.format_exc()}") | |
| try: | |
| from routes.dashboard_bp import dashboard_bp | |
| except Exception as e: | |
| dashboard_bp = None | |
| import traceback | |
| print(f"ERROR: Failed to import dashboard_bp: {e}\n{traceback.format_exc()}") | |
| try: | |
| from routes.alerts_bp import alerts_bp | |
| except Exception as e: | |
| alerts_bp = None | |
| import traceback | |
| print(f"ERROR: Failed to import alerts_bp: {e}\n{traceback.format_exc()}") | |
| try: | |
| from routes.agents_bp import agents_bp | |
| except Exception as e: | |
| agents_bp = None | |
| import traceback | |
| print(f"ERROR: Failed to import agents_bp: {e}\n{traceback.format_exc()}") | |
| # ββ Initialize logger βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| log = get_logger(__name__) | |
| # ββ Create Flask app ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| app = Flask(__name__, static_folder=None, template_folder=BASE_DIR) | |
| app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH | |
| # ββ Enable CORS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| CORS(app, | |
| resources={r"/*": { | |
| "origins": ALLOWED_ORIGIN, | |
| "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"], | |
| "allow_headers": ["Content-Type", "X-CSRF-Token"], # Removed Authorization, using strict HTTP-Only cookies | |
| "expose_headers": ["X-CSRF-Token"], | |
| "supports_credentials": True, | |
| "max_age": 3600 | |
| }}) | |
| # ββ Security Headers Middleware ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def set_security_headers(response): | |
| """Add enterprise-grade security headers to all responses.""" | |
| response.headers['X-Frame-Options'] = 'DENY' | |
| response.headers['X-Content-Type-Options'] = 'nosniff' | |
| response.headers['X-XSS-Protection'] = '1; mode=block' | |
| response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin' | |
| # Content Security Policy: Strict definitions | |
| response.headers['Content-Security-Policy'] = ( | |
| "default-src 'self'; " | |
| "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://fonts.googleapis.com; " | |
| "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " | |
| "font-src 'self' https://fonts.gstatic.com; " | |
| "img-src 'self' data: https:; " | |
| "connect-src 'self' https:; " | |
| "object-src 'none'; " | |
| "frame-ancestors 'none'; " | |
| "upgrade-insecure-requests; " | |
| "base-uri 'self';" | |
| ) | |
| # HSTS: enforce HTTPS (Always on in production, Vercel standard) | |
| if not DEBUG: | |
| response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload' | |
| return response | |
| # ββ Global Jinja Context βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def inject_global_template_vars(): | |
| # Attempt to decode JWT from HTTP-only cookie so templates can | |
| # perform server-side role-aware rendering (non-sensitive). | |
| user = None | |
| try: | |
| token = request.cookies.get('access_token') | |
| if token: | |
| payload = decode_token(token) | |
| if payload: | |
| # expose only non-sensitive fields to templates | |
| user = { | |
| 'email': payload.get('email'), | |
| 'role': payload.get('role'), | |
| 'org_id': payload.get('org_id') | |
| } | |
| except Exception: | |
| user = None | |
| return { | |
| 'github_url': GITHUB_URL, | |
| 'current_year': datetime.now(timezone.utc).year, | |
| 'get_csrf_token': generate_csrf_token, | |
| 'current_user': user, | |
| # Frontend may opt to disable console logging when server is set to OFF | |
| 'quiet_logs': True if (isinstance(LOG_LEVEL, str) and LOG_LEVEL.upper() in ("OFF", "NONE", "DISABLED")) else False | |
| } | |
| # ββ Register middleware & blueprints ββββββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| log_request_middleware(app) | |
| except Exception as e: | |
| log.warning(f"Failed to register request middleware: {e}") | |
| blueprints_to_register = [ | |
| (auth_bp, '/api/auth'), | |
| (audits_bp, '/api/audits'), | |
| (kb_bp, '/api/kb'), | |
| (admin_bp, '/api/admin'), | |
| (dashboard_bp, '/api/dashboard'), | |
| (alerts_bp, '/api/alerts'), | |
| (agents_bp, '/api/agents'), | |
| ] | |
| for bp, url_prefix in blueprints_to_register: | |
| if bp is None: | |
| log.warning(f"Skipping blueprint registration for {url_prefix} β blueprint failed to import") | |
| continue | |
| try: | |
| app.register_blueprint(bp, url_prefix=url_prefix) | |
| except Exception as e: | |
| log.error(f"Failed to register blueprint {bp.name} at {url_prefix}: {e}", exc_info=True) | |
| # ββ Health checks βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def health(): | |
| """Shallow health check β fast, no aggregation (Vercel warm-up).""" | |
| db = get_db() | |
| ok = db is not None | |
| return jsonify({ | |
| 'status': 'healthy' if ok else 'degraded', | |
| 'database': 'connected' if ok else 'disconnected', | |
| 'timestamp': datetime.now(timezone.utc).isoformat() | |
| }), (200 if ok else 503) | |
| def health_deep(): | |
| """Deep health check β ensures MongoDB Atlas Vector Search is functional.""" | |
| status = get_db_status() | |
| db_connected = status.get('connected', False) | |
| return jsonify({ | |
| 'status': 'healthy' if db_connected else 'degraded', | |
| 'mongodb': status, | |
| # RAG strictly uses Atlas Vector Search to avoid Vercel SQLite exhaustion | |
| 'vector_store': 'connected' if status.get('atlas_vector_index') else 'degraded', | |
| 'timestamp': datetime.now(timezone.utc).isoformat() | |
| }), (200 if db_connected else 503) | |
| def get_social_config(): | |
| """Return public configurations.""" | |
| return jsonify({'github': GITHUB_URL}), 200 | |
| # ββ Authentication Helper βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def verify_http_only_jwt() -> bool: | |
| """ | |
| Strictly enforces HTTP-Only cookie JWT validation for MVC routing. | |
| Protects against XSS token theft. | |
| """ | |
| token = request.cookies.get('access_token') | |
| if not token: | |
| return False | |
| try: | |
| jwt.decode(token, JWT_SECRET, algorithms=["HS256"]) | |
| return True | |
| except (jwt.ExpiredSignatureError, jwt.InvalidTokenError): | |
| return False | |
| # ββ MVC Protected UI Routes βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def dashboard(): | |
| if not verify_http_only_jwt(): | |
| return redirect(url_for('index')) | |
| return render_template('dashboard.html') | |
| def audit(): | |
| if not verify_http_only_jwt(): | |
| return redirect(url_for('index')) | |
| return render_template('audit.html') | |
| def knowledge_base(): | |
| if not verify_http_only_jwt(): | |
| return redirect(url_for('index')) | |
| # Enforce server-side RBAC: only admins may access KB UI | |
| token = request.cookies.get('access_token') | |
| payload = decode_token(token) if token else None | |
| if not payload or payload.get('role') != 'admin': | |
| return redirect(url_for('dashboard')) | |
| return render_template('knowledge-base.html') | |
| def agents(): | |
| if not verify_http_only_jwt(): | |
| return redirect(url_for('index')) | |
| # Enforce server-side RBAC: only admins may access Agents UI | |
| token = request.cookies.get('access_token') | |
| payload = decode_token(token) if token else None | |
| if not payload or payload.get('role') != 'admin': | |
| return redirect(url_for('dashboard')) | |
| return render_template('agents.html') | |
| # ββ Public / Static Routes ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def index(): | |
| """Serve landing/login page. Redirect to dashboard if already authenticated.""" | |
| if verify_http_only_jwt(): | |
| return redirect(url_for('dashboard')) | |
| return render_template('index.html') | |
| def serve_assets(filename): | |
| """ | |
| Securely serve static assets. | |
| Relies on Werkzeug's `send_from_directory` C-level path sanitization | |
| to prevent directory traversal attacks (e.g., ../../etc/passwd). | |
| """ | |
| assets_dir = os.path.join(BASE_DIR, 'assets') | |
| if not os.path.isdir(assets_dir): | |
| return jsonify({'error': 'Assets directory missing'}), 404 | |
| return send_from_directory(assets_dir, filename, max_age=3600) | |
| # -- Deterministic custom static routes (consistent with routes.create_app) | |
| def serve_style(): | |
| project_root = os.path.dirname(BASE_DIR) | |
| STATIC_DIR = os.path.join(project_root, 'static') | |
| return send_from_directory(STATIC_DIR, 'css/style.css', mimetype='text/css') | |
| def serve_script(): | |
| project_root = os.path.dirname(BASE_DIR) | |
| STATIC_DIR = os.path.join(project_root, 'static') | |
| return send_from_directory(STATIC_DIR, 'js/script.js', mimetype='application/javascript') | |
| def serve_auth_init(): | |
| project_root = os.path.dirname(BASE_DIR) | |
| STATIC_DIR = os.path.join(project_root, 'static') | |
| return send_from_directory(STATIC_DIR, 'js/auth-init.js', mimetype='application/javascript') | |
| def serve_favicon(): | |
| project_root = os.path.dirname(BASE_DIR) | |
| STATIC_DIR = os.path.join(project_root, 'static') | |
| return send_from_directory(STATIC_DIR, 'img/favicon.svg', mimetype='image/svg+xml') | |
| def serve_data(filename): | |
| """Serve public data files (policies, sitemap) from the project-level data/ directory.""" | |
| project_root = os.path.dirname(BASE_DIR) | |
| data_dir = os.path.join(project_root, 'data') | |
| # Security: only allow safe extensions | |
| ALLOWED_DATA_EXTS = {'.md', '.json', '.txt'} | |
| ext = os.path.splitext(filename)[1].lower() | |
| if ext not in ALLOWED_DATA_EXTS: | |
| return jsonify({'error': 'Access denied'}), 403 | |
| # Traversal guard | |
| full = os.path.normpath(os.path.join(data_dir, filename)) | |
| data_abs = os.path.abspath(data_dir) | |
| if not full.startswith(data_abs + os.sep) and full != data_abs: | |
| return jsonify({'error': 'Access denied'}), 403 | |
| if not os.path.isfile(full): | |
| return jsonify({'error': 'File not found'}), 404 | |
| MIME_MAP = {'.md': 'text/markdown', '.json': 'application/json', '.txt': 'text/plain'} | |
| mime = MIME_MAP.get(ext) | |
| if mime: | |
| return send_from_directory(data_dir, filename, mimetype=mime, max_age=3600) | |
| return send_from_directory(data_dir, filename, max_age=3600) | |
| def serve_static(filename): | |
| """Serve public static assets with unified path resolution.""" | |
| project_root = os.path.dirname(BASE_DIR) | |
| STATIC_DIR = os.path.join(project_root, 'static') | |
| static_abs = os.path.abspath(STATIC_DIR) | |
| # ββ Security gates ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| ALLOWED_EXTS = {'.css', '.js', '.ico', '.png', '.jpg', '.jpeg', '.svg', | |
| '.json', '.txt', '.pdf', '.webp', '.gif', '.md'} | |
| BLOCKED = {'__pycache__', '.git', '.gitignore'} | |
| ext = os.path.splitext(filename)[1].lower() | |
| if ext in {'.py', '.env'}: | |
| return jsonify({'error': 'Access denied'}), 403 | |
| if ext not in ALLOWED_EXTS: | |
| return jsonify({'error': 'Access denied'}), 403 | |
| if any(b in filename for b in BLOCKED): | |
| return jsonify({'error': 'Access denied'}), 403 | |
| # ββ MIME type map βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| MIME_MAP = {'.css': 'text/css', '.js': 'application/javascript', | |
| '.svg': 'image/svg+xml', '.json': 'application/json'} | |
| # ββ Unified path resolution βββββββββββββββββββββββββββββββββββββββββββ | |
| # Strip redundant 'static/' prefix (templates may use /static/css/β¦) | |
| clean = filename.replace('\\', '/') | |
| if clean.startswith('static/'): | |
| clean = clean[len('static/'):] | |
| # Build ordered list of candidate relative paths to try | |
| basename = clean.split('/')[-1] | |
| EXT_DIRS = {'.css': 'css', '.js': 'js', | |
| '.png': 'img', '.jpg': 'img', '.jpeg': 'img', | |
| '.svg': 'img', '.ico': 'img', '.webp': 'img', '.gif': 'img'} | |
| candidates = [clean] # 1) exact relative path (css/style.css) | |
| if ext in EXT_DIRS: | |
| mapped = f"{EXT_DIRS[ext]}/{basename}" | |
| if mapped != clean: | |
| candidates.append(mapped) # 2) extension-mapped path (css/style.css from style.css) | |
| if ext in {'.json', '.txt', '.pdf', '.md'}: | |
| candidates.append(f"assets/{basename}") # 3) assets fallback | |
| candidates.append(basename) # 4) bare filename in root | |
| for rel in candidates: | |
| full = os.path.normpath(os.path.join(STATIC_DIR, rel)) | |
| # Directory traversal guard | |
| if not full.startswith(static_abs + os.sep) and full != static_abs: | |
| continue | |
| if os.path.isfile(full): | |
| safe_rel = os.path.relpath(full, STATIC_DIR).replace('\\', '/') | |
| mime = MIME_MAP.get(ext) | |
| if mime: | |
| return send_from_directory(STATIC_DIR, safe_rel, | |
| mimetype=mime, max_age=3600) | |
| return send_from_directory(STATIC_DIR, safe_rel, max_age=3600) | |
| # ββ Log suppressing for noisy browser/extension probes ββββββββββββββββ | |
| NOISY_PROBES = {'.well-known', 'favicon.ico', 'manifest.json', '.map'} | |
| is_noisy = any(probe in filename for probe in NOISY_PROBES) | |
| if not is_noisy: | |
| log.warning("Static file not found", extra={ | |
| "requested_file": filename, "candidates": candidates, | |
| "static_dir": static_abs | |
| }) | |
| return jsonify({'error': 'File not found'}), 404 | |
| # ββ Error handlers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def bad_request(e): | |
| return jsonify({'error': 'Bad request'}), 400 | |
| def unauthorized(e): | |
| return jsonify({'error': 'Unauthorized', 'message': 'Invalid or missing HTTP-only cookie'}), 401 | |
| def forbidden(e): | |
| return jsonify({'error': 'Forbidden', 'message': 'CSRF validation failed or insufficient permissions'}), 403 | |
| def not_found(e): | |
| if request.path.startswith('/api/'): | |
| log.debug("API endpoint not found", extra={"path": request.path}) | |
| return jsonify({'error': 'API endpoint not found'}), 404 | |
| # Redirect to landing page for unknown routes | |
| return redirect(url_for('index')) | |
| def server_error(e): | |
| log.error("Unhandled server error", extra={ | |
| "error": str(e), | |
| "path": request.path, | |
| "method": request.method, | |
| "remote_ip": request.remote_addr | |
| }) | |
| return jsonify({'error': 'Internal server error'}), 500 | |
| # ββ Startup βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == '__main__': | |
| log.info("Starting Qualora Quality Auditor", extra={ | |
| "environment": "vercel" if os.getenv("VERCEL_ENV") else "local" | |
| }) | |
| # Startup health check: verify critical services (warn, don't exit β for HF Spaces flexibility) | |
| db = get_db() | |
| if db is None: | |
| log.warning("MongoDB connection unavailable at startup. Ensure MONGODB_URI is set via environment or secrets.") | |
| else: | |
| log.info("β MongoDB connected. Ready for audit operations.") | |
| log.info("β Flask app initialized. Ready to handle requests.") | |
| # Fix for Windows: Werkzeug's select() crashes with WinError 10038 during concurrent requests | |
| is_windows = os.name == 'nt' | |
| port = int(os.getenv("PORT", "8000")) | |
| log.info(f"Listening on port {port}") | |
| try: | |
| app.run(debug=DEBUG, port=port, threaded=not is_windows) | |
| except KeyboardInterrupt: | |
| # Graceful local shutdown (Ctrl+C) without emitting a noisy traceback. | |
| log.info("Shutdown requested by user (KeyboardInterrupt).") |