@@ -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# ══════════════════════════════════════════════════════════════════
13931726with 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# ══════════════════════════════════════════════════════════════════
19582296with 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# ══════════════════════════════════════════════════════════════════
22332571with tab_settings :
22342572 st .header ("Settings" )
0 commit comments