diff --git a/.gitignore b/.gitignore index 8b6087b..df2c0cb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ __pycache__/ *.pyc .env *.log +.memory/ diff --git a/llm_team_ui.py b/llm_team_ui.py index f1bf4db..6ee3e46 100644 --- a/llm_team_ui.py +++ b/llm_team_ui.py @@ -3,6 +3,7 @@ import json import os +import sys import time import threading import secrets @@ -36,8 +37,23 @@ app.config["SESSION_COOKIE_HTTPONLY"] = True app.config["SESSION_COOKIE_SAMESITE"] = "Lax" # ─── SECURITY LOGGING ───────────────────────────────────────── -# Dedicated security log for fail2ban and audit trail -_sec_handler = logging.FileHandler("/var/log/llm-team-security.log") +# Dedicated security log for fail2ban and audit trail. +# +# Cross-lineage scrum 2026-04-30 (Opus BLOCK OB-1): wrapped in +# try/except. Pre-fix this raised PermissionError at import time +# when the service user couldn't write /var/log/llm-team-security.log, +# crashing the app before Flask started. Now falls back to stderr; +# sec_log still works (ban events still land in journald via stderr), +# but the app starts. Operator should still create the file with +# proper perms on production deploy. Path is overridable via +# LLM_TEAM_SECURITY_LOG env var. +_LOG_PATH = os.environ.get("LLM_TEAM_SECURITY_LOG", "/var/log/llm-team-security.log") +try: + _sec_handler = logging.FileHandler(_LOG_PATH) +except (PermissionError, FileNotFoundError, OSError) as _log_err: + print(f"[security] WARNING: can't open {_LOG_PATH} ({_log_err}); " + f"falling back to stderr.", file=sys.stderr, flush=True) + _sec_handler = logging.StreamHandler(sys.stderr) _sec_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s")) sec_log = logging.getLogger("security") sec_log.addHandler(_sec_handler) @@ -150,8 +166,19 @@ def _check_high_alert_expiry(): # IPs that never get rate-limited (your LAN, localhost) ALLOWLIST_IPS = {"127.0.0.1", "::1", "192.168.1.1"} -# Demo mode state — toggled by admin at runtime -_demo_mode = {"active": True, "started_by": "boot", "showcase": True} +# Demo mode state — toggled by admin at runtime. +# +# Cross-lineage scrum 2026-04-30 (Opus BLOCK OB-5): pre-fix this +# defaulted to active=True, meaning fresh installs shipped with +# public unauthenticated access enabled — login_required let demo +# users straight through. Combined with /api/run + /api/imagegen +# proxies, that was an open LLM/compute abuse surface from first +# boot. Now defaults to active=False; operators flip it on +# explicitly via the admin UI or LLM_TEAM_DEMO_MODE=1 env override +# (the env override exists for the demo systemd unit so the public +# devop.live deployment doesn't need a manual toggle on every restart). +_DEMO_DEFAULT = os.environ.get("LLM_TEAM_DEMO_MODE", "0") == "1" +_demo_mode = {"active": _DEMO_DEFAULT, "started_by": "boot" if _DEMO_DEFAULT else "off", "showcase": _DEMO_DEFAULT} # Routes that demo users CAN trigger (read-like POSTs — enrichment, self-analysis, team runs) DEMO_ALLOWED_POSTS = { @@ -560,8 +587,24 @@ def security_checks(): # Check high-alert expiry _check_high_alert_expiry() - # Exploit scanner detection — log, alert, track velocity, block - if EXPLOIT_PATTERNS.search(path) or EXPLOIT_PATTERNS.search(request.query_string.decode("utf-8", errors="ignore")): + # Exploit scanner detection — log, alert, track velocity, block. + # + # Cross-lineage scrum 2026-04-30 (Opus BLOCK OB-4): pre-fix the + # path regex matched on substrings like UNION, SELECT, ;-- and + # auto-banned after 3 hits. Admin URLs containing those keywords + # in query strings (e.g. an LLM team named "select-rebrand" or a + # docs link to /admin/select_a_mode) self-banned the admin's IP. + # Now: skip the path-based check for authenticated admins from + # an allowlisted IP. The user-agent + body checks (sentinel) still + # apply. Allowlisted-IP admins clicking weird URLs no longer + # lock themselves out. + _skip_exploit_check = False + if ip in ALLOWLIST_IPS and session.get("role") == "admin": + _skip_exploit_check = True + if not _skip_exploit_check and ( + EXPLOIT_PATTERNS.search(path) or + EXPLOIT_PATTERNS.search(request.query_string.decode("utf-8", errors="ignore")) + ): sec_log.warning("EXPLOIT_SCAN ip=%s path=%s ua=%s", ip, path, ua) _track_violation(ip, "exploit_scan") send_security_alert( @@ -1888,7 +1931,18 @@ def get_api_key(provider_name): env_map = {"openrouter": "OPENROUTER_API_KEY", "openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "ollama_cloud": "OLLAMA_CLOUD_API_KEY"} return os.environ.get(env_map.get(provider_name, ""), "") -DB_DSN = "dbname=knowledge_base user=kbuser password=IPbLBA0EQI8u4TeM2YZrbm1OAy5nSwqC host=localhost" +# Cross-lineage scrum 2026-04-30 (Opus BLOCK OB-2 + harness LLM +# convergent finding): DB_DSN previously had the password hardcoded +# in source. Same `kbuser`/`knowledge_base` DSN was leaked in +# voice-ai's audiosocket_bridge.py + sales_assistant.py — confirmed +# canonical leak by 3 independent reviewers across 2 sessions. Now +# sourced from env (set via systemd EnvironmentFile=/etc/llm-team-ui.env). +# No silent fallback to the leaked literal — fail loud. The leaked +# password is in git history regardless; rotate it in Postgres. +DB_DSN = os.environ.get("LLM_TEAM_DB_DSN", "") +if not DB_DSN: + print("[llm-team-ui] WARNING: LLM_TEAM_DB_DSN not set — DB ops will fail. " + "Set in systemd EnvironmentFile or shell env.", file=sys.stderr, flush=True) def get_db(): return psycopg2.connect(DB_DSN)