|
41 | 41 | from benchmark.comparator import BenchmarkComparator, quick_benchmark |
42 | 42 | from benchmark.metrics import compute_metrics |
43 | 43 |
|
| 44 | +# ── Authentication ──────────────────────────────────────────────── |
| 45 | +from ui.auth import require_auth |
| 46 | +if not require_auth(): |
| 47 | + st.stop() |
| 48 | + |
44 | 49 | # ── Initialise DB ───────────────────────────────────────────────── |
45 | 50 | initialise() |
46 | 51 |
|
|
77 | 82 | st.caption("Apache 2.0 · [GitHub](https://github.com/marjatmm-sec/qute)") |
78 | 83 |
|
79 | 84 | # ── Tabs ────────────────────────────────────────────────────────── |
80 | | -tab_ingest, tab_rules, tab_quantum, tab_benchmark, tab_report = st.tabs([ |
| 85 | +tab_ingest, tab_rules, tab_quantum, tab_benchmark, tab_report, tab_settings = st.tabs([ |
81 | 86 | "📥 Ingest", |
82 | 87 | "📋 Rules", |
83 | 88 | "⚛️ Quantum", |
84 | 89 | "📊 Benchmark", |
85 | 90 | "📄 Report", |
| 91 | + "⚙️ Settings", |
86 | 92 | ]) |
87 | 93 |
|
88 | 94 |
|
@@ -934,3 +940,172 @@ def get_std(report, head, metric): |
934 | 940 | st.info("No benchmark history to export.") |
935 | 941 | except Exception as e: |
936 | 942 | st.error(f"Export error: {e}") |
| 943 | + |
| 944 | +# ══════════════════════════════════════════════════════════════════ |
| 945 | +# TAB 6 — SETTINGS |
| 946 | +# ══════════════════════════════════════════════════════════════════ |
| 947 | +with tab_settings: |
| 948 | + st.header("Settings") |
| 949 | + |
| 950 | + # ── Authentication ──────────────────────────────────────────── |
| 951 | + st.subheader("UI Authentication") |
| 952 | + |
| 953 | + from ui.auth import is_auth_enabled, check_password |
| 954 | + from config import UI_PASSWORD_HASH |
| 955 | + import bcrypt as _bcrypt |
| 956 | + import re as _re |
| 957 | + |
| 958 | + MIN_LENGTH = 12 |
| 959 | + |
| 960 | + def _check_complexity(pw: str) -> list: |
| 961 | + issues = [] |
| 962 | + if len(pw) < MIN_LENGTH: |
| 963 | + issues.append(f"At least {MIN_LENGTH} characters") |
| 964 | + if not _re.search(r"[A-Z]", pw): |
| 965 | + issues.append("At least one uppercase letter") |
| 966 | + if not _re.search(r"[a-z]", pw): |
| 967 | + issues.append("At least one lowercase letter") |
| 968 | + if not _re.search(r"\d", pw): |
| 969 | + issues.append("At least one number") |
| 970 | + if not _re.search(r"[!@#$%^&*()\-_=+\[\]{};:'\",.<>/?\\|`~]", pw): |
| 971 | + issues.append("At least one special character") |
| 972 | + return issues |
| 973 | + |
| 974 | + def _write_hash_to_env(hash_value: str) -> None: |
| 975 | + from pathlib import Path |
| 976 | + env_path = Path("/app/.env") if Path("/app/.env").exists() else Path(".env") |
| 977 | + if not env_path.exists(): |
| 978 | + env_path.write_text("") |
| 979 | + content = env_path.read_text() |
| 980 | + key = "QUTE_UI_PASSWORD_HASH" |
| 981 | + if key in content: |
| 982 | + lines = content.splitlines() |
| 983 | + lines = [ |
| 984 | + f"{key}={hash_value}" if l.startswith(f"{key}=") else l |
| 985 | + for l in lines |
| 986 | + ] |
| 987 | + env_path.write_text("\n".join(lines) + "\n") |
| 988 | + else: |
| 989 | + with open(env_path, "a") as f: |
| 990 | + f.write(f"\n# UI Authentication\n{key}={hash_value}\n") |
| 991 | + |
| 992 | + if is_auth_enabled(): |
| 993 | + st.success("🔒 Password protection is **enabled**.") |
| 994 | + st.markdown("**Change password**") |
| 995 | + with st.form("change_password_form"): |
| 996 | + current_pw = st.text_input("Current password", type="password") |
| 997 | + new_pw = st.text_input("New password", type="password") |
| 998 | + confirm_pw = st.text_input("Confirm new password", type="password") |
| 999 | + submitted = st.form_submit_button("Update password") |
| 1000 | + |
| 1001 | + if submitted: |
| 1002 | + if not check_password(current_pw): |
| 1003 | + st.error("Current password is incorrect.") |
| 1004 | + elif new_pw != confirm_pw: |
| 1005 | + st.error("New passwords do not match.") |
| 1006 | + else: |
| 1007 | + issues = _check_complexity(new_pw) |
| 1008 | + if issues: |
| 1009 | + st.error("Password requirements not met:") |
| 1010 | + for issue in issues: |
| 1011 | + st.markdown(f" - {issue}") |
| 1012 | + else: |
| 1013 | + h = _bcrypt.hashpw(new_pw.encode(), _bcrypt.gensalt(rounds=12)).decode() |
| 1014 | + _write_hash_to_env(h) |
| 1015 | + st.success("✅ Password updated.") |
| 1016 | + st.info( |
| 1017 | + "To activate: restart the UI.\n\n" |
| 1018 | + "**Docker:** `docker compose restart qute-app`\n\n" |
| 1019 | + "**Local:** stop and rerun `streamlit run src/ui/app.py --server.port=8503`" |
| 1020 | + ) |
| 1021 | + |
| 1022 | + st.markdown("---") |
| 1023 | + st.markdown("**Disable authentication**") |
| 1024 | + if st.button("🔓 Remove password protection", type="secondary"): |
| 1025 | + _write_hash_to_env("") |
| 1026 | + st.warning("Authentication disabled.") |
| 1027 | + st.info( |
| 1028 | + "To activate: restart the UI.\n\n" |
| 1029 | + "**Docker:** `docker compose restart qute-app`\n\n" |
| 1030 | + "**Local:** stop and rerun `streamlit run src/ui/app.py --server.port=8503`" |
| 1031 | + ) |
| 1032 | + |
| 1033 | + else: |
| 1034 | + st.warning("🔓 Password protection is **disabled**. Anyone with access to port 8503 can use the UI.") |
| 1035 | + st.markdown("**Set a password**") |
| 1036 | + st.caption(f"Requirements: {MIN_LENGTH}+ characters, uppercase, lowercase, number, special character.") |
| 1037 | + |
| 1038 | + with st.form("set_password_form"): |
| 1039 | + new_pw = st.text_input("New password", type="password") |
| 1040 | + confirm_pw = st.text_input("Confirm password", type="password") |
| 1041 | + submitted = st.form_submit_button("Enable password protection", type="primary") |
| 1042 | + |
| 1043 | + if submitted: |
| 1044 | + if new_pw != confirm_pw: |
| 1045 | + st.error("Passwords do not match.") |
| 1046 | + else: |
| 1047 | + issues = _check_complexity(new_pw) |
| 1048 | + if issues: |
| 1049 | + st.error("Password requirements not met:") |
| 1050 | + for issue in issues: |
| 1051 | + st.markdown(f" - {issue}") |
| 1052 | + else: |
| 1053 | + h = _bcrypt.hashpw(new_pw.encode(), _bcrypt.gensalt(rounds=12)).decode() |
| 1054 | + _write_hash_to_env(h) |
| 1055 | + st.success("✅ Password set.") |
| 1056 | + st.info( |
| 1057 | + "To activate: restart the UI.\n\n" |
| 1058 | + "**Docker:** `docker compose restart qute-app`\n\n" |
| 1059 | + "**Local:** stop and rerun `streamlit run src/ui/app.py --server.port=8503`" |
| 1060 | + ) |
| 1061 | + |
| 1062 | + # ── Syslog listener settings ────────────────────────────────── |
| 1063 | + st.markdown("---") |
| 1064 | + st.subheader("Syslog Listener") |
| 1065 | + st.caption("Configure the live syslog listener. Changes take effect when you start/restart the listener in the Ingest tab.") |
| 1066 | + |
| 1067 | + from store.settings import get_setting, set_setting |
| 1068 | + from store.settings import SYSLOG_PORT, SYSLOG_PROTOCOL, SYSLOG_ENABLED |
| 1069 | + |
| 1070 | + s_port = st.number_input( |
| 1071 | + "Default port", |
| 1072 | + min_value=1024, max_value=65535, |
| 1073 | + value=get_int_setting(SYSLOG_PORT, 5514), |
| 1074 | + ) |
| 1075 | + s_proto = st.selectbox( |
| 1076 | + "Default protocol", |
| 1077 | + ["both", "udp", "tcp"], |
| 1078 | + index=["both","udp","tcp"].index(get_setting(SYSLOG_PROTOCOL, "both")), |
| 1079 | + ) |
| 1080 | + |
| 1081 | + if st.button("Save listener defaults"): |
| 1082 | + set_setting(SYSLOG_PORT, str(s_port)) |
| 1083 | + set_setting(SYSLOG_PROTOCOL, s_proto) |
| 1084 | + st.success("Defaults saved.") |
| 1085 | + |
| 1086 | + # ── Quantum settings ────────────────────────────────────────── |
| 1087 | + st.markdown("---") |
| 1088 | + st.subheader("Quantum") |
| 1089 | + st.caption("These settings affect new benchmark runs. Currently active session settings are shown in the Quantum tab.") |
| 1090 | + |
| 1091 | + from config import QUANTUM_SHOTS, ISING_NOISE_ENABLED, ISING_DEPOLAR_PROB |
| 1092 | + st.metric("Shots per circuit", QUANTUM_SHOTS) |
| 1093 | + st.metric("Noise enabled", str(ISING_NOISE_ENABLED)) |
| 1094 | + st.metric("Depolarising probability", ISING_DEPOLAR_PROB) |
| 1095 | + st.caption("To change quantum settings edit .env and restart the container.") |
| 1096 | + |
| 1097 | + # ── About ───────────────────────────────────────────────────── |
| 1098 | + st.markdown("---") |
| 1099 | + st.subheader("About") |
| 1100 | + st.markdown(""" |
| 1101 | + **Qute** — Quantum Unified Threat Engine |
| 1102 | +
|
| 1103 | + A local-first, open-source research platform for classical-quantum |
| 1104 | + comparative security detection. |
| 1105 | +
|
| 1106 | + - License: Apache 2.0 |
| 1107 | + - GitHub: [marjatmm-sec/qute](https://github.com/marjatmm-sec/qute) |
| 1108 | + - Docs: [docs/](docs/) |
| 1109 | + """) |
| 1110 | + st.caption(f"CUDA-Q: {'available' if CUDAQ_AVAILABLE else 'not available (CPU mode)'} · " |
| 1111 | + f"LLM: {OLLAMA_MODEL} · DB: {DB_PATH}") |
0 commit comments