""" 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 ──────────────────────────────────────────────── @app.after_request 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 ───────────────────────────────────────────────────── @app.context_processor 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 ───────────────────────────────────────────────────────────── @app.route('/api/health', methods=['GET']) 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) @app.route('/api/health/deep', methods=['GET']) 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) @app.route('/api/config/social', methods=['GET']) 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 ─────────────────────────────────────────────────── @app.route('/dashboard', methods=['GET']) def dashboard(): if not verify_http_only_jwt(): return redirect(url_for('index')) return render_template('dashboard.html') @app.route('/audit', methods=['GET']) def audit(): if not verify_http_only_jwt(): return redirect(url_for('index')) return render_template('audit.html') @app.route('/knowledge-base', methods=['GET']) 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') @app.route('/agents', methods=['GET']) 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 ──────────────────────────────────────────────────── @app.route('/', methods=['GET']) 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') @app.route('/assets/') 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) @app.route('/style.css', methods=['GET']) 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') @app.route('/script.js', methods=['GET']) 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') @app.route('/auth-init.js', methods=['GET']) 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') @app.route('/favicon.svg', methods=['GET']) 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') @app.route('/data/') 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) @app.route('/') 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 ─────────────────────────────────────────────────────────── @app.errorhandler(400) def bad_request(e): return jsonify({'error': 'Bad request'}), 400 @app.errorhandler(401) def unauthorized(e): return jsonify({'error': 'Unauthorized', 'message': 'Invalid or missing HTTP-only cookie'}), 401 @app.errorhandler(403) def forbidden(e): return jsonify({'error': 'Forbidden', 'message': 'CSRF validation failed or insufficient permissions'}), 403 @app.errorhandler(404) 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')) @app.errorhandler(500) 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).")