Skip to content

Commit cd1bef6

Browse files
committed
feat: UI auth, settings tab, syslog live connector, auto ground truth, multi-run benchmark, interpretation guide — 125/125
1 parent 5958fca commit cd1bef6

7 files changed

Lines changed: 381 additions & 5 deletions

File tree

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,8 @@ QUTE_SIGMA_COMMUNITY_REPO=https://github.com/SigmaHQ/sigma
4141
# ── UI ────────────────────────────────────────────────────────────
4242
QUTE_UI_PORT=8503
4343
QUTE_UI_THEME=dark
44+
45+
# ── UI Authentication ─────────────────────────────────────────────
46+
# Leave blank to disable authentication (dev mode)
47+
# To set a password run: python3 scripts/set_password.py
48+
QUTE_UI_PASSWORD_HASH=

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,12 @@ These are honest limitations you should understand before using Qute:
204204

205205
v0.1.0 Initial scaffold — schema, parsers, quantum harness, benchmark layer
206206
v0.2.0 Few-shot rule generation (done), GPU validation (done),
207-
Ising CNN decoder integration, Yara-L backend (pending)
207+
Syslog TCP/UDP live connector (done),
208+
Ising CNN decoder integration (pending),
209+
Additional pySigma backends: OpenSearch, QRadar, Elasticsearch
210+
(blocked on pySigma ecosystem version alignment)
208211
v0.3.0 SecGraph-AI integration module (Neo4j export)
212+
Auth hash stored in DuckDB (no restart required for password changes)
209213
v0.4.0 Real QPU backend testing, expanded qubit count
210214

211215
---

config.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ def _resolve_host(env_key: str) -> str:
7272
)
7373

7474
# ── UI ────────────────────────────────────────────────────────────
75-
UI_PORT = int(os.getenv("QUTE_UI_PORT", "8503"))
76-
UI_THEME = os.getenv("QUTE_UI_THEME", "dark")
75+
UI_PORT = int(os.getenv("QUTE_UI_PORT", "8503"))
76+
UI_THEME = os.getenv("QUTE_UI_THEME", "dark")
77+
UI_PASSWORD_HASH = os.getenv("QUTE_UI_PASSWORD_HASH", "")
7778

7879
if __name__ == "__main__":
7980
print(f"OLLAMA_URL : {OLLAMA_URL}")

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ grpcio>=1.62.0
1313
pySigma>=0.11.0
1414
pySigma-backend-splunk>=0.3.0
1515
pySigma-backend-microsoft365defender>=0.3.0
16-
pySigma-backend-qradar-aql>=0.3.0
1716
PyYAML>=6.0.1
1817

1918
# ── LLM / Classical AI ───────────────────────────────────────────
@@ -37,3 +36,4 @@ rich>=13.7.0
3736
uuid6>=2024.1.12
3837
pytz>=2024.1
3938
numpy>=1.26.0
39+
bcrypt>=4.0.0

scripts/set_password.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""
2+
Qute -- scripts/set_password.py
3+
Interactive script to set the UI password.
4+
Generates a bcrypt hash and writes it to .env
5+
6+
Usage:
7+
python3 scripts/set_password.py
8+
"""
9+
10+
import re
11+
import sys
12+
import getpass
13+
from pathlib import Path
14+
15+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
16+
17+
try:
18+
import bcrypt
19+
except ImportError:
20+
print("bcrypt not installed. Run: pip install bcrypt")
21+
sys.exit(1)
22+
23+
ROOT = Path(__file__).resolve().parent.parent
24+
ENV_FILE = ROOT / ".env"
25+
MIN_LENGTH = 12
26+
27+
28+
def check_complexity(password: str) -> list:
29+
issues = []
30+
if len(password) < MIN_LENGTH:
31+
issues.append(f"At least {MIN_LENGTH} characters")
32+
if not re.search(r"[A-Z]", password):
33+
issues.append("At least one uppercase letter")
34+
if not re.search(r"[a-z]", password):
35+
issues.append("At least one lowercase letter")
36+
if not re.search(r"\d", password):
37+
issues.append("At least one number")
38+
if not re.search(r"[!@#$%^&*()\-_=+\[\]{};:'\",.<>/?\\|`~]", password):
39+
issues.append("At least one special character")
40+
return issues
41+
42+
43+
def hash_password(password: str) -> str:
44+
salt = bcrypt.gensalt(rounds=12)
45+
return bcrypt.hashpw(password.encode(), salt).decode()
46+
47+
48+
def update_env(hash_value: str) -> None:
49+
if not ENV_FILE.exists():
50+
ENV_FILE.write_text("")
51+
52+
content = ENV_FILE.read_text()
53+
key = "QUTE_UI_PASSWORD_HASH"
54+
55+
if key in content:
56+
lines = content.splitlines()
57+
new_lines = []
58+
for line in lines:
59+
if line.startswith(f"{key}="):
60+
new_lines.append(f"{key}={hash_value}")
61+
else:
62+
new_lines.append(line)
63+
ENV_FILE.write_text("\n".join(new_lines) + "\n")
64+
else:
65+
with open(ENV_FILE, "a") as f:
66+
f.write(f"\n# UI Authentication\n")
67+
f.write(f"{key}={hash_value}\n")
68+
69+
70+
def main():
71+
print("Qute UI Password Setup")
72+
print("=" * 40)
73+
print("Requirements: 12+ chars, upper, lower, number, special")
74+
print()
75+
76+
while True:
77+
password = getpass.getpass("Enter new password: ")
78+
issues = check_complexity(password)
79+
if issues:
80+
print("Password does not meet requirements:")
81+
for issue in issues:
82+
print(f" x {issue}")
83+
print()
84+
continue
85+
86+
confirm = getpass.getpass("Confirm password: ")
87+
if password != confirm:
88+
print("Passwords do not match. Try again.\n")
89+
continue
90+
break
91+
92+
print("Generating hash...")
93+
hash_value = hash_password(password)
94+
update_env(hash_value)
95+
print(f"Password hash written to {ENV_FILE}")
96+
print("Restart the UI for changes to take effect.")
97+
98+
99+
if __name__ == "__main__":
100+
main()

src/ui/app.py

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
from benchmark.comparator import BenchmarkComparator, quick_benchmark
4242
from benchmark.metrics import compute_metrics
4343

44+
# ── Authentication ────────────────────────────────────────────────
45+
from ui.auth import require_auth
46+
if not require_auth():
47+
st.stop()
48+
4449
# ── Initialise DB ─────────────────────────────────────────────────
4550
initialise()
4651

@@ -77,12 +82,13 @@
7782
st.caption("Apache 2.0 · [GitHub](https://github.com/marjatmm-sec/qute)")
7883

7984
# ── 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([
8186
"📥 Ingest",
8287
"📋 Rules",
8388
"⚛️ Quantum",
8489
"📊 Benchmark",
8590
"📄 Report",
91+
"⚙️ Settings",
8692
])
8793

8894

@@ -934,3 +940,172 @@ def get_std(report, head, metric):
934940
st.info("No benchmark history to export.")
935941
except Exception as e:
936942
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

Comments
 (0)