Skip to content

Commit 856cadd

Browse files
committed
feat: Visualise tab with live detection timeline, confidence distribution, FV compression
- New 📡 Visualise tab with three panels (timeline first): 1. Live detection timeline — scatter plot of last 500 detections, x=time y=VQC confidence, coloured by verdict, threshold line at 0.80 2. VQC confidence distribution — histogram of all recent detections showing bimodal separation between anomaly (0.995) and benign (0.002) 3. Feature vector → qubit compression — 24-dim FV bar chart coloured by qubit composite, alongside 6 compressed qubit values - Dark SOC aesthetic throughout: monospace fonts, near-black backgrounds, amber/cyan/red accents on plotly charts - Benchmark tab: Rule-based ground truth as default label source
1 parent 4d03656 commit 856cadd

1 file changed

Lines changed: 344 additions & 6 deletions

File tree

src/ui/app.py

Lines changed: 344 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,12 @@ def _investigation_step() -> str:
204204
st.caption("Apache 2.0 · [GitHub](https://github.com/marjatmm-sec/qute)")
205205

206206
# ── Tabs ──────────────────────────────────────────────────────────
207-
tab_ingest, tab_rules, tab_detections, tab_quantum, tab_benchmark, tab_report, tab_settings = st.tabs([
207+
tab_ingest, tab_rules, tab_detections, tab_quantum, tab_visualise, tab_benchmark, tab_report, tab_settings = st.tabs([
208208
"📥 Ingest",
209209
"📋 Rules",
210210
"🚨 Detections",
211211
"⚛️ Quantum",
212+
"📡 Visualise",
212213
"📊 Benchmark",
213214
"📄 Report",
214215
"⚙️ Settings",
@@ -1387,8 +1388,340 @@ def _rule_sort(r):
13871388
st.error(f"DB error: {e}")
13881389

13891390

1391+
1392+
# ══════════════════════════════════════════════════════════════════
1393+
# TAB 5 — VISUALISE
1394+
# ══════════════════════════════════════════════════════════════════
1395+
with tab_visualise:
1396+
st.header("📡 Pipeline Visualisation")
1397+
st.caption(
1398+
"Live view of the quantum detection pipeline — "
1399+
"confidence distributions, feature vector composition, and detection timeline."
1400+
)
1401+
1402+
try:
1403+
import plotly.graph_objects as go
1404+
import plotly.express as px
1405+
import duckdb as _ddb
1406+
from config import DB_PATH
1407+
_vcon = _ddb.connect(str(DB_PATH), read_only=True)
1408+
1409+
_verdict_map = {
1410+
"confirmed": "Confirmed (rule+VQC)",
1411+
"rule_match": "Rule match only",
1412+
"quantum_anomaly":"Quantum anomaly",
1413+
"benign": "Benign",
1414+
}
1415+
1416+
# ── Panel 3: Live detection timeline ─────────────────────
1417+
st.markdown("---")
1418+
st.subheader("🕐 Live Detection Timeline")
1419+
st.caption(
1420+
"Detections as they arrive — coloured by verdict. "
1421+
"Each point is one event; y-axis is VQC confidence."
1422+
)
1423+
1424+
_tl_rows = _vcon.execute("""
1425+
SELECT d.detected_at, d.quantum_confidence, d.combined_verdict,
1426+
d.combined_severity, e.process_name, e.source_ip,
1427+
d.rule_count
1428+
FROM detections d
1429+
JOIN events e ON e.id = d.event_id
1430+
WHERE d.quantum_confidence IS NOT NULL
1431+
ORDER BY d.detected_at DESC
1432+
LIMIT 500
1433+
""").fetchall()
1434+
1435+
if _tl_rows:
1436+
_tl_df = pd.DataFrame(_tl_rows,
1437+
columns=["time","confidence","verdict","severity",
1438+
"process","src_ip","rule_count"])
1439+
_tl_df["time"] = pd.to_datetime(_tl_df["time"])
1440+
_tl_df = _tl_df.sort_values("time")
1441+
1442+
_tl_colors = {
1443+
"confirmed": "#ff4444",
1444+
"rule_match": "#ff8800",
1445+
"quantum_anomaly": "#00ccff",
1446+
"benign": "#44bb44",
1447+
}
1448+
_tl_df["color"] = _tl_df["verdict"].map(_tl_colors).fillna("#666666")
1449+
_tl_df["size"] = _tl_df["verdict"].map({
1450+
"confirmed": 12, "rule_match": 8,
1451+
"quantum_anomaly": 12, "benign": 4,
1452+
}).fillna(4)
1453+
1454+
_fig3 = go.Figure()
1455+
for _v, _col in _tl_colors.items():
1456+
_sub = _tl_df[_tl_df["verdict"] == _v]
1457+
if len(_sub) == 0:
1458+
continue
1459+
_fig3.add_trace(go.Scatter(
1460+
x=_sub["time"],
1461+
y=_sub["confidence"],
1462+
mode="markers",
1463+
name=_verdict_map.get(_v, _v),
1464+
marker=dict(
1465+
color=_col,
1466+
size=_sub["size"],
1467+
opacity=0.8,
1468+
line=dict(width=1, color="rgba(255,255,255,0.2)"),
1469+
),
1470+
hovertemplate=(
1471+
"<b>%{customdata[0]}</b><br>"
1472+
"conf: %{y:.3f}<br>"
1473+
"src: %{customdata[1]}<br>"
1474+
"rules: %{customdata[2]}<br>"
1475+
"<extra></extra>"
1476+
),
1477+
customdata=_sub[["process","src_ip","rule_count"]].values,
1478+
))
1479+
1480+
# Add threshold line
1481+
_fig3.add_hline(
1482+
y=0.80,
1483+
line_dash="dash",
1484+
line_color="#ffcc00",
1485+
annotation_text="VQC threshold (0.80)",
1486+
annotation_font_color="#ffcc00",
1487+
annotation_font_size=10,
1488+
)
1489+
1490+
_fig3.update_layout(
1491+
paper_bgcolor="rgba(0,0,0,0)",
1492+
plot_bgcolor="rgba(13,17,23,0.8)",
1493+
font=dict(color="#c9d1d9", family="monospace"),
1494+
xaxis=dict(
1495+
title="Detection time (UTC)",
1496+
gridcolor="#21262d",
1497+
tickfont=dict(color="#8b949e"),
1498+
),
1499+
yaxis=dict(
1500+
title="VQC confidence",
1501+
gridcolor="#21262d",
1502+
range=[-0.05, 1.05],
1503+
tickfont=dict(color="#8b949e"),
1504+
),
1505+
legend=dict(
1506+
bgcolor="rgba(22,27,34,0.9)",
1507+
bordercolor="#30363d",
1508+
borderwidth=1,
1509+
),
1510+
height=400,
1511+
margin=dict(l=60, r=20, t=20, b=60),
1512+
)
1513+
st.plotly_chart(_fig3, use_container_width=True)
1514+
1515+
# Refresh button
1516+
if st.button("🔄 Refresh timeline"):
1517+
st.rerun()
1518+
else:
1519+
st.info("No detections yet — start the detector and ingest some events.")
1520+
1521+
# ── Panel 1: Confidence distribution ─────────────────────
1522+
st.markdown("---")
1523+
st.subheader("⚛️ VQC Confidence Distribution")
1524+
st.caption(
1525+
"Bimodal separation between anomalous and benign events — "
1526+
"the quantum circuit encodes threat signal as measurement probability."
1527+
)
1528+
1529+
_conf_rows = _vcon.execute("""
1530+
SELECT d.quantum_confidence, d.combined_verdict, d.combined_severity,
1531+
e.process_name, e.source_ip
1532+
FROM detections d
1533+
JOIN events e ON e.id = d.event_id
1534+
WHERE d.quantum_confidence IS NOT NULL
1535+
ORDER BY d.detected_at DESC
1536+
LIMIT 2000
1537+
""").fetchall()
1538+
1539+
if _conf_rows:
1540+
_conf_df = pd.DataFrame(_conf_rows,
1541+
columns=["confidence","verdict","severity","process","src_ip"])
1542+
1543+
# Map verdicts to display groups
1544+
1545+
_conf_df["group"] = _conf_df["verdict"].map(_verdict_map).fillna("Unknown")
1546+
1547+
_colors = {
1548+
"Confirmed (rule+VQC)": "#ff4444",
1549+
"Rule match only": "#ff8800",
1550+
"Quantum anomaly": "#00ccff",
1551+
"Benign": "#44bb44",
1552+
}
1553+
1554+
_fig1 = go.Figure()
1555+
for _grp, _col in _colors.items():
1556+
_sub = _conf_df[_conf_df["group"] == _grp]
1557+
if len(_sub) == 0:
1558+
continue
1559+
_fig1.add_trace(go.Histogram(
1560+
x=_sub["confidence"],
1561+
name=_grp,
1562+
marker_color=_col,
1563+
opacity=0.75,
1564+
nbinsx=50,
1565+
))
1566+
1567+
_fig1.update_layout(
1568+
barmode="overlay",
1569+
paper_bgcolor="rgba(0,0,0,0)",
1570+
plot_bgcolor="rgba(13,17,23,0.8)",
1571+
font=dict(color="#c9d1d9", family="monospace"),
1572+
xaxis=dict(
1573+
title="VQC Confidence (1 − P(|000000⟩))",
1574+
gridcolor="#21262d", range=[0, 1],
1575+
tickfont=dict(color="#8b949e"),
1576+
),
1577+
yaxis=dict(
1578+
title="Event count",
1579+
gridcolor="#21262d",
1580+
tickfont=dict(color="#8b949e"),
1581+
),
1582+
legend=dict(
1583+
bgcolor="rgba(22,27,34,0.9)",
1584+
bordercolor="#30363d",
1585+
borderwidth=1,
1586+
),
1587+
height=380,
1588+
margin=dict(l=60, r=20, t=20, b=60),
1589+
)
1590+
st.plotly_chart(_fig1, use_container_width=True)
1591+
1592+
# Stats summary
1593+
_c1, _c2, _c3, _c4 = st.columns(4)
1594+
for _verdict, _label, _col in [
1595+
("confirmed", "Confirmed", _c1),
1596+
("rule_match", "Rule match", _c2),
1597+
("quantum_anomaly","Quantum anomaly", _c3),
1598+
("benign", "Benign", _c4),
1599+
]:
1600+
_sub = _conf_df[_conf_df["verdict"] == _verdict]
1601+
if len(_sub) > 0:
1602+
_col.metric(_label, f"{len(_sub)}",
1603+
delta=f"conf {_sub['confidence'].mean():.3f}")
1604+
else:
1605+
st.info("No detections with VQC confidence yet — run the detector first.")
1606+
1607+
# ── Panel 2: Feature vector composition ──────────────────
1608+
st.markdown("---")
1609+
st.subheader("🧬 Feature Vector → Qubit Compression")
1610+
st.caption(
1611+
"How the 24-dimensional threat feature vector compresses to "
1612+
"6 qubit composites for quantum circuit encoding."
1613+
)
1614+
1615+
# Get a sample confirmed detection and show its feature vector breakdown
1616+
_fv_row = _vcon.execute("""
1617+
SELECT e.feature_vector, e.process_name, e.source_ip,
1618+
d.combined_verdict, d.quantum_confidence, d.matched_rule_titles
1619+
FROM detections d
1620+
JOIN events e ON e.id = d.event_id
1621+
WHERE d.quantum_confidence > 0.8
1622+
AND e.feature_vector IS NOT NULL
1623+
ORDER BY d.detected_at DESC
1624+
LIMIT 1
1625+
""").fetchone()
1626+
1627+
if _fv_row:
1628+
import ast as _ast
1629+
from quantum.circuit import compress_features, FEATURE_DIM
1630+
1631+
_fv_str, _proc, _src_ip, _verdict, _conf, _rules = _fv_row
1632+
try:
1633+
_fv = _ast.literal_eval(_fv_str) if isinstance(_fv_str, str) else _fv_str
1634+
if len(_fv) < FEATURE_DIM:
1635+
_fv = _fv + [0.0] * (FEATURE_DIM - len(_fv))
1636+
_compressed = compress_features(_fv)
1637+
except Exception:
1638+
_fv = [0.0] * FEATURE_DIM
1639+
_compressed = [0.0] * 6
1640+
1641+
_fv_labels = [
1642+
"ip_private","ip_oct3","ip_oct4","ip_entropy",
1643+
"severity","sev_hi","proc_risk","is_auth","is_net",
1644+
"has_fail","has_auth","has_exploit","has_scan","repeat",
1645+
"hour","off_hours",
1646+
"win_proc","is_lolbin","encoded_cmd","cmd_entropy",
1647+
"lateral","persist","av_tamper","path_susp",
1648+
]
1649+
_qubit_labels = [
1650+
"q0: Network/IP","q1: Severity","q2: Process risk",
1651+
"q3: Message threat","q4: Windows exec","q5: Temporal",
1652+
]
1653+
_qubit_colors = ["#00ccff","#ff4444","#ff8800","#ffcc00","#cc44ff","#44bb44"]
1654+
1655+
_fig2_col1, _fig2_col2 = st.columns([3, 2])
1656+
1657+
with _fig2_col1:
1658+
st.caption(f"Sample: `{_proc or 'unknown'}` from `{_src_ip or 'unknown'}` — {_verdict} (conf={_conf:.3f})")
1659+
_fig2a = go.Figure(go.Bar(
1660+
x=_fv_labels,
1661+
y=_fv,
1662+
marker_color=[
1663+
_qubit_colors[0] if i < 4 else
1664+
_qubit_colors[1] if i < 6 else
1665+
_qubit_colors[2] if i < 9 else
1666+
_qubit_colors[3] if i < 16 else
1667+
_qubit_colors[4]
1668+
for i in range(len(_fv_labels))
1669+
],
1670+
text=[f"{v:.2f}" for v in _fv],
1671+
textposition="outside",
1672+
textfont=dict(size=9, color="#8b949e"),
1673+
))
1674+
_fig2a.update_layout(
1675+
paper_bgcolor="rgba(0,0,0,0)",
1676+
plot_bgcolor="rgba(13,17,23,0.8)",
1677+
font=dict(color="#c9d1d9", family="monospace", size=10),
1678+
xaxis=dict(tickangle=-45, gridcolor="#21262d",
1679+
tickfont=dict(color="#8b949e", size=9)),
1680+
yaxis=dict(range=[0, 1.2], gridcolor="#21262d",
1681+
tickfont=dict(color="#8b949e")),
1682+
height=300,
1683+
margin=dict(l=40, r=20, t=20, b=80),
1684+
showlegend=False,
1685+
)
1686+
st.plotly_chart(_fig2a, use_container_width=True)
1687+
1688+
with _fig2_col2:
1689+
st.caption("Compressed to 6 qubit composites")
1690+
_fig2b = go.Figure(go.Bar(
1691+
x=_qubit_labels,
1692+
y=_compressed,
1693+
marker_color=_qubit_colors,
1694+
text=[f"{v:.3f}" for v in _compressed],
1695+
textposition="outside",
1696+
textfont=dict(size=10, color="#c9d1d9"),
1697+
))
1698+
_fig2b.update_layout(
1699+
paper_bgcolor="rgba(0,0,0,0)",
1700+
plot_bgcolor="rgba(13,17,23,0.8)",
1701+
font=dict(color="#c9d1d9", family="monospace", size=10),
1702+
xaxis=dict(tickangle=-30, gridcolor="#21262d",
1703+
tickfont=dict(color="#8b949e", size=9)),
1704+
yaxis=dict(range=[0, 1.2], gridcolor="#21262d",
1705+
tickfont=dict(color="#8b949e")),
1706+
height=300,
1707+
margin=dict(l=40, r=20, t=20, b=80),
1708+
showlegend=False,
1709+
)
1710+
st.plotly_chart(_fig2b, use_container_width=True)
1711+
else:
1712+
st.info("No confirmed detections yet — start the replay generator to generate threat events.")
1713+
1714+
_vcon.close()
1715+
1716+
except ImportError:
1717+
st.error("Plotly is required for visualisations. "
1718+
"Install with: `pip install plotly`")
1719+
except Exception as _ve:
1720+
st.error(f"Visualisation error: {_ve}")
1721+
st.exception(_ve)
1722+
13901723
# ══════════════════════════════════════════════════════════════════
1391-
# TAB 5 — BENCHMARK
1724+
# TAB 6 — BENCHMARK
13921725
# ══════════════════════════════════════════════════════════════════
13931726
with tab_benchmark:
13941727
st.header("📊 Classical vs Quantum Benchmark")
@@ -1485,8 +1818,13 @@ def _rule_sort(r):
14851818
st.markdown("**Ground truth labels**")
14861819
gt_mode = st.radio(
14871820
"Label source",
1488-
["Auto (from event severity)", "Manual (comma-separated)", "Upload CSV"],
1489-
help="Auto mode uses event severity >= 50 as anomaly proxy.",
1821+
["Rule-based (from detections)", "Auto (from event severity)",
1822+
"Manual (comma-separated)", "Upload CSV"],
1823+
help=(
1824+
"Rule-based: uses detector verdicts as ground truth — "
1825+
"events matched by active rules = anomaly. Preferred for live benchmarking. "
1826+
"Auto: uses event severity >= 50 as proxy (less reliable)."
1827+
),
14901828
)
14911829
gt_input = ""
14921830
gt_uploaded = None
@@ -1953,7 +2291,7 @@ def _progress_cb(run_idx: int, event_idx: int, total_events: int):
19532291

19542292

19552293
# ══════════════════════════════════════════════════════════════════
1956-
# TAB 6 — REPORT
2294+
# TAB 7 — REPORT
19572295
# ══════════════════════════════════════════════════════════════════
19582296
with tab_report:
19592297
st.header("📄 Investigation Report")
@@ -2228,7 +2566,7 @@ def get_std(rep, head, metric):
22282566

22292567

22302568
# ══════════════════════════════════════════════════════════════════
2231-
# TAB 7 — SETTINGS
2569+
# TAB 8 — SETTINGS
22322570
# ══════════════════════════════════════════════════════════════════
22332571
with tab_settings:
22342572
st.header("Settings")

0 commit comments

Comments
 (0)