3MPER0RR's picture
Update index.html
84b46f1 verified
Raw History Blame
38.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🤖 Unitree CVE Robustness Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js"></script>
<style>
:root {
--bg: #070a0f;
--surface: #0d1117;
--surface2: #161b22;
--border: #21262d;
--accent: #00ff88;
--red: #ff3366;
--orange: #ff8c42;
--yellow: #ffd166;
--blue: #00b4d8;
--purple: #7b2fff;
--text: #c9d1d9;
--dim: #8b949e;
--mono: 'Courier New', monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--text);font-family:var(--mono);min-height:100vh;overflow-x:hidden}
body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(0,255,136,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(0,255,136,.025) 1px,transparent 1px);background-size:44px 44px;pointer-events:none;z-index:0}
#bg{position:fixed;top:0;left:0;width:100%;height:100%;z-index:0;opacity:.25;pointer-events:none}
/* ── layout ── */
.app{position:relative;z-index:1;max-width:1380px;margin:0 auto;padding:0 20px 48px}
/* ── header ── */
header{padding:22px 0 18px;border-bottom:1px solid var(--border);margin-bottom:22px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:12px}
.logo{display:flex;align-items:center;gap:14px}
.logo-icon{width:46px;height:46px;background:linear-gradient(135deg,var(--red),var(--purple));border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:22px;animation:glow 2.4s ease-in-out infinite}
@keyframes glow{0%,100%{box-shadow:0 0 10px rgba(255,51,102,.35)}50%{box-shadow:0 0 28px rgba(255,51,102,.75),0 0 50px rgba(123,47,255,.3)}}
.logo h1{font-size:17px;color:#fff;letter-spacing:2px;text-transform:uppercase}
.logo p{font-size:10px;color:var(--dim);letter-spacing:1px;margin-top:3px}
.badge{display:flex;align-items:center;gap:8px;padding:6px 14px;background:rgba(0,255,136,.07);border:1px solid rgba(0,255,136,.18);border-radius:20px;font-size:10px;color:var(--accent);letter-spacing:1px}
.dot{width:6px;height:6px;border-radius:50%;background:var(--accent);animation:blink 1.6s ease-in-out infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.15}}
/* ── tabs ── */
.tabs{display:flex;gap:4px;margin-bottom:22px;background:var(--surface);padding:4px;border-radius:10px;border:1px solid var(--border)}
.tab{flex:1;padding:10px;text-align:center;font-size:11px;letter-spacing:1px;text-transform:uppercase;cursor:pointer;border-radius:7px;transition:all .2s;color:var(--dim);border:1px solid transparent;background:none;font-family:var(--mono)}
.tab:hover{color:var(--text);background:var(--surface2)}
.tab.active{background:linear-gradient(135deg,rgba(255,51,102,.13),rgba(123,47,255,.13));color:#fff;border-color:rgba(255,51,102,.28)}
.panel{display:none}
.panel.active{display:block;animation:fadein .3s ease}
@keyframes fadein{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
/* ── kpi grid ── */
.kpi-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:20px}
.kpi{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;position:relative;overflow:hidden;transition:transform .2s,border-color .2s}
.kpi:hover{transform:translateY(-2px)}
.kpi::after{content:'';position:absolute;bottom:0;left:0;right:0;height:2px}
.kpi.c1::after{background:var(--red)}.kpi.c2::after{background:var(--orange)}.kpi.c3::after{background:var(--yellow)}.kpi.c4::after{background:var(--blue)}
.kpi:hover.c1{border-color:rgba(255,51,102,.4)}.kpi:hover.c2{border-color:rgba(255,140,66,.4)}.kpi:hover.c3{border-color:rgba(255,209,102,.4)}.kpi:hover.c4{border-color:rgba(0,180,216,.4)}
.kpi-lbl{font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--dim);margin-bottom:8px}
.kpi-val{font-size:38px;font-weight:bold;line-height:1;margin-bottom:4px}
.kpi.c1 .kpi-val{color:var(--red)}.kpi.c2 .kpi-val{color:var(--orange)}.kpi.c3 .kpi-val{color:var(--yellow)}.kpi.c4 .kpi-val{color:var(--blue)}
.kpi-sub{font-size:10px;color:var(--dim)}
/* ── chart cards ── */
.cg2{display:grid;grid-template-columns:1fr 2fr;gap:14px;margin-bottom:14px}
.cg2b{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px}
.card{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:18px}
.card h3{font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--dim);margin-bottom:14px}
.cw{position:relative;height:210px}
.cw-sm{position:relative;height:260px}
/* ── explorer ── */
.controls{display:flex;gap:10px;margin-bottom:14px;flex-wrap:wrap}
input,select{background:var(--surface);border:1px solid var(--border);color:var(--text);padding:8px 12px;border-radius:8px;font-family:var(--mono);font-size:11px;outline:none;transition:border-color .2s}
input:focus,select:focus{border-color:rgba(0,255,136,.35)}
input{flex:1;min-width:200px}
.table-wrap{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden}
table{width:100%;border-collapse:collapse;font-size:11px}
th{background:var(--surface2);padding:11px 14px;text-align:left;color:var(--dim);font-size:9px;letter-spacing:2px;text-transform:uppercase;cursor:pointer;user-select:none;border-bottom:1px solid var(--border)}
th:hover{color:var(--text)}
td{padding:11px 14px;border-bottom:1px solid rgba(33,38,45,.5);vertical-align:middle}
tr:last-child td{border-bottom:none}
tr.data-row{cursor:pointer}
tr.data-row:hover td{background:rgba(255,255,255,.018)}
.sev{padding:2px 7px;border-radius:4px;font-size:9px;font-weight:bold;letter-spacing:1px;text-transform:uppercase}
.s-CRITICAL{background:rgba(255,51,102,.14);color:var(--red);border:1px solid rgba(255,51,102,.28)}
.s-HIGH{background:rgba(255,140,66,.14);color:var(--orange);border:1px solid rgba(255,140,66,.28)}
.s-MEDIUM{background:rgba(255,209,102,.14);color:var(--yellow);border:1px solid rgba(255,209,102,.28)}
.s-LOW{background:rgba(0,180,216,.14);color:var(--blue);border:1px solid rgba(0,180,216,.28)}
.s-INFO{background:rgba(139,148,158,.1);color:var(--dim);border:1px solid rgba(139,148,158,.2)}
.cbar{display:flex;align-items:center;gap:6px}
.ctrack{width:60px;height:4px;background:var(--border);border-radius:2px;overflow:hidden;flex-shrink:0}
.cfill{height:100%;border-radius:2px}
.detail-row td{padding:0}
.detail{display:none;padding:14px 16px;background:rgba(0,255,136,.018);border-top:1px solid var(--border)}
.detail.open{display:block}
.dg{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:10px}
.di .dl{font-size:9px;letter-spacing:1px;text-transform:uppercase;color:var(--dim);margin-bottom:3px}
.di .dv{font-size:11px;color:var(--text)}
.desc{font-size:11px;color:var(--dim);line-height:1.65;padding:10px;background:var(--surface2);border-radius:6px}
#tcount{margin-top:8px;font-size:10px;color:var(--dim)}
/* ── comparison ── */
.cmp-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px}
.scores{display:flex;flex-direction:column;gap:10px}
.mrow{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:14px;transition:all .2s;cursor:default}
.mrow:hover{border-color:rgba(0,255,136,.25);background:rgba(0,255,136,.03)}
.mrh{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
.mname{font-size:13px;color:#fff}
.mscore{font-size:22px;font-weight:bold}
.sbar{height:5px;background:var(--border);border-radius:3px;overflow:hidden;margin-bottom:6px}
.sfill{height:100%;border-radius:3px;transition:width 1s cubic-bezier(.4,0,.2,1)}
.msub{font-size:9px;color:var(--dim)}
/* ── loader ── */
.drop{border:2px dashed var(--border);border-radius:12px;padding:40px;text-align:center;cursor:pointer;transition:all .2s;background:var(--surface);margin-bottom:18px}
.drop:hover,.drop.over{border-color:rgba(0,255,136,.35);background:rgba(0,255,136,.025)}
.drop-ico{font-size:38px;margin-bottom:12px}
.drop h3{color:var(--text);font-size:13px;margin-bottom:6px}
.drop p{color:var(--dim);font-size:11px}
.fmts{display:flex;gap:8px;justify-content:center;margin-top:12px}
.fmt{padding:3px 10px;border-radius:4px;font-size:9px;letter-spacing:1px;background:var(--surface2);border:1px solid var(--border);color:var(--dim)}
.fbtn{display:inline-block;margin-top:14px;padding:8px 18px;background:rgba(0,255,136,.09);border:1px solid rgba(0,255,136,.25);border-radius:8px;color:var(--accent);font-family:var(--mono);font-size:11px;cursor:pointer;transition:all .2s;letter-spacing:1px}
.fbtn:hover{background:rgba(0,255,136,.18)}
.preview{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;max-height:280px;overflow:auto;font-size:10px;color:var(--dim)}
.preview pre{white-space:pre-wrap;word-break:break-all}
.actions{display:flex;gap:10px;margin-top:14px}
.btn{padding:9px 18px;border-radius:8px;font-family:var(--mono);font-size:11px;cursor:pointer;transition:all .2s;letter-spacing:1px;border:none}
.btn-merge{background:rgba(0,255,136,.09);color:var(--accent);border:1px solid rgba(0,255,136,.25)}
.btn-replace{background:rgba(255,51,102,.1);color:var(--red);border:1px solid rgba(255,51,102,.25)}
.btn-merge:hover{background:rgba(0,255,136,.18)}.btn-replace:hover{background:rgba(255,51,102,.2)}
.schema{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;font-size:10px;color:var(--dim);margin-top:20px}
.schema pre{white-space:pre-wrap;line-height:1.7}
/* ── toast ── */
.toast{position:fixed;bottom:22px;right:22px;padding:11px 18px;background:var(--surface2);border:1px solid rgba(0,255,136,.28);border-radius:10px;color:var(--accent);font-size:11px;z-index:999;transform:translateY(80px);opacity:0;transition:all .3s}
.toast.show{transform:translateY(0);opacity:1}
/* ── responsive ── */
@media(max-width:800px){.kpi-grid{grid-template-columns:repeat(2,1fr)}.cg2,.cg2b,.cmp-grid{grid-template-columns:1fr}}
::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:var(--surface)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
</style>
</head>
<body>
<canvas id="bg"></canvas>
<div class="app">
<!-- HEADER -->
<header>
<div class="logo">
<div class="logo-icon">🤖</div>
<div>
<h1>Unitree CVE Dashboard</h1>
<p>Robustness Analysis · CVE Intelligence · Multi-Model</p>
</div>
</div>
<div class="badge"><div class="dot"></div><span id="src-lbl">DEMO DATA · 20 CVEs</span></div>
</header>
<!-- TABS -->
<div class="tabs">
<button class="tab active" onclick="tab('dashboard')">📊 Dashboard</button>
<button class="tab" onclick="tab('explorer')">🔍 CVE Explorer</button>
<button class="tab" onclick="tab('comparison')">⚔️ Comparison</button>
<button class="tab" onclick="tab('loader')">📂 Load Data</button>
</div>
<!-- ══ DASHBOARD ══ -->
<div class="panel active" id="panel-dashboard">
<div class="kpi-grid">
<div class="kpi c1"><div class="kpi-lbl">Total CVEs</div><div class="kpi-val" id="k-total">—</div><div class="kpi-sub">all models</div></div>
<div class="kpi c2"><div class="kpi-lbl">Critical</div><div class="kpi-val" id="k-crit">—</div><div class="kpi-sub">CVSS ≥ 9.0</div></div>
<div class="kpi c3"><div class="kpi-lbl">Avg CVSS</div><div class="kpi-val" id="k-cvss">—</div><div class="kpi-sub">weighted score</div></div>
<div class="kpi c4"><div class="kpi-lbl">Models</div><div class="kpi-val" id="k-models">—</div><div class="kpi-sub">unique targets</div></div>
</div>
<div class="cg2">
<div class="card"><h3>Severity Distribution</h3><div class="cw"><canvas id="ch-sev"></canvas></div></div>
<div class="card"><h3>CVE Timeline by Severity</h3><div class="cw"><canvas id="ch-time"></canvas></div></div>
</div>
<div class="cg2b">
<div class="card"><h3>Attack Vector Breakdown</h3><div class="cw"><canvas id="ch-vec"></canvas></div></div>
<div class="card"><h3>CVEs per Model</h3><div class="cw"><canvas id="ch-mod"></canvas></div></div>
</div>
</div>
<!-- ══ EXPLORER ══ -->
<div class="panel" id="panel-explorer">
<div class="controls">
<input type="text" id="s-search" placeholder="🔍 Search CVE, component, description…" oninput="renderTable()">
<select id="s-model" onchange="renderTable()"><option value="">All Models</option></select>
<select id="s-sev" onchange="renderTable()">
<option value="">All Severities</option>
<option>CRITICAL</option><option>HIGH</option><option>MEDIUM</option><option>LOW</option>
</select>
<select id="s-vec" onchange="renderTable()">
<option value="">All Vectors</option>
<option>Network</option><option>Adjacent</option><option>Local</option><option>Physical</option>
</select>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th onclick="sortBy('id')">CVE ID ↕</th>
<th onclick="sortBy('model')">Model ↕</th>
<th onclick="sortBy('component')">Component ↕</th>
<th onclick="sortBy('cvss')">CVSS ↕</th>
<th onclick="sortBy('severity')">Severity ↕</th>
<th onclick="sortBy('vector')">Vector ↕</th>
<th onclick="sortBy('published')">Published ↕</th>
</tr>
</thead>
<tbody id="tbody"></tbody>
</table>
</div>
<div id="tcount"></div>
</div>
<!-- ══ COMPARISON ══ -->
<div class="panel" id="panel-comparison">
<div class="cmp-grid">
<div>
<div style="font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--dim);margin-bottom:14px">Model Robustness Scores</div>
<div class="scores" id="scores"></div>
</div>
<div class="card"><h3>Attack Surface Radar</h3><div class="cw-sm"><canvas id="ch-radar"></canvas></div></div>
</div>
<div class="card"><h3>Avg CVSS vs Max CVSS per Model</h3><div class="cw"><canvas id="ch-box"></canvas></div></div>
</div>
<!-- ══ LOADER ══ -->
<div class="panel" id="panel-loader">
<div class="drop" id="drop">
<div class="drop-ico">📂</div>
<h3>Drop your CVE database here</h3>
<p>Drag & drop or click to browse — merge or replace the active dataset</p>
<div class="fmts">
<span class="fmt">JSON</span><span class="fmt">JSONL</span><span class="fmt">CSV</span><span class="fmt">PARQUET</span>
</div>
<label class="fbtn">Browse Files
<input type="file" id="file-input" accept=".json,.jsonl,.csv,.parquet" style="display:none" onchange="onFile(event)">
</label>
</div>
<div id="preview-wrap" style="display:none">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px">
<span style="font-size:11px;color:var(--dim)" id="p-label"></span>
<span style="font-size:10px;color:var(--accent)" id="p-count"></span>
</div>
<div class="preview" id="preview"></div>
<div class="actions">
<button class="btn btn-merge" onclick="applyData('merge')">⊕ Merge with existing</button>
<button class="btn btn-replace" onclick="applyData('replace')">⟳ Replace all data</button>
</div>
</div>
<div class="schema">
<div style="font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--dim);margin-bottom:10px">Expected JSON Schema</div>
<pre>{
"id": "CVE-2024-XXXXX", // required
"model": "Go2", // required
"component": "WebRTC Control", // required
"cvss": 9.8, // required (0.0 – 10.0)
"severity": "CRITICAL", // CRITICAL | HIGH | MEDIUM | LOW
"vector": "Network", // Network | Adjacent | Local | Physical
"description": "...", // optional
"published": "2024-03-15", // optional YYYY-MM-DD
"status": "Patch Available", // optional
"cwe": "CWE-306", // optional
"references": ["https://..."] // optional
}
For Parquet / CSV, use the same column names.
Column aliases supported: cve_id → id, score → cvss, robot / product → model.</pre>
</div>
</div>
</div><!-- /.app -->
<div class="toast" id="toast"></div>
<script>
// ═══════════════════════════════════════════════════════
// DEMO DATA (20 sample CVEs — replace via Load Data tab)
// ═══════════════════════════════════════════════════════
const DEMO = [
{id:"CVE-DEMO-001",model:"Go2",component:"WebRTC Control",cvss:9.8,severity:"CRITICAL",vector:"Network",description:"Unauthenticated RCE via WebRTC control channel — no authentication on Go2 SDK server. Attacker on LAN can inject motion commands and gain root shell.",published:"2024-03-15",status:"Patch Available",cwe:"CWE-306",references:[]},
{id:"CVE-DEMO-002",model:"Go2",component:"ROS2 DDS Bridge",cvss:8.1,severity:"HIGH",vector:"Network",description:"No TLS on internal ROS2 DDS bus lets adjacent attackers inject arbitrary topic messages including unsafe velocity commands.",published:"2024-01-22",status:"In Progress",cwe:"CWE-319",references:[]},
{id:"CVE-DEMO-003",model:"Go2",component:"SSH Service",cvss:7.5,severity:"HIGH",vector:"Network",description:"Default credentials (root:123) shipped on firmware < v1.0.24, trivial remote root access over WiFi.",published:"2023-11-10",status:"Patched",cwe:"CWE-1392",references:[]},
{id:"CVE-DEMO-004",model:"Go2",component:"OTA Update",cvss:6.5,severity:"MEDIUM",vector:"Network",description:"Firmware update endpoint lacks signature verification — MITM attacker can push malicious images.",published:"2024-05-08",status:"In Progress",cwe:"CWE-345",references:[]},
{id:"CVE-DEMO-005",model:"Go2",component:"WiFi AP Mode",cvss:5.3,severity:"MEDIUM",vector:"Adjacent",description:"Default WPA2-PSK passphrase derived from predictable SSID pattern — brute-forceable in minutes.",published:"2024-02-14",status:"Patch Available",cwe:"CWE-340",references:[]},
{id:"CVE-DEMO-006",model:"Go1",component:"Perception Module",cvss:9.1,severity:"CRITICAL",vector:"Network",description:"RCE in obstacle detection service via crafted LiDAR packet. Service runs as root with no input validation on raw sensor frames.",published:"2023-09-05",status:"Patched",cwe:"CWE-787",references:[]},
{id:"CVE-DEMO-007",model:"Go1",component:"UDP Command",cvss:8.6,severity:"HIGH",vector:"Adjacent",description:"Unauthenticated UDP command injection on port 8082 bypasses safety layer — arbitrary low-level motion commands from LAN.",published:"2023-07-19",status:"Patched",cwe:"CWE-306",references:[]},
{id:"CVE-DEMO-008",model:"Go1",component:"Web Interface",cvss:7.2,severity:"HIGH",vector:"Network",description:"Stored XSS in management interface allows session token exfiltration and persistent admin panel compromise.",published:"2023-05-30",status:"Patched",cwe:"CWE-79",references:[]},
{id:"CVE-DEMO-009",model:"Go1",component:"Bluetooth LE",cvss:6.1,severity:"MEDIUM",vector:"Adjacent",description:"BLE pairing lacks mutual auth — nearby attacker can spoof mobile app and issue basic control commands.",published:"2023-08-12",status:"Patch Available",cwe:"CWE-287",references:[]},
{id:"CVE-DEMO-010",model:"Go1",component:"File System",cvss:4.4,severity:"MEDIUM",vector:"Local",description:"WiFi credentials and API keys world-readable under /etc/unitree/ — any local process can exfiltrate them.",published:"2023-10-01",status:"Patched",cwe:"CWE-732",references:[]},
{id:"CVE-DEMO-011",model:"Go1",component:"Camera Stream",cvss:3.7,severity:"LOW",vector:"Network",description:"Video feed exposed on LAN port without authentication — passive surveillance of robot's visual sensors.",published:"2023-04-20",status:"Won't Fix",cwe:"CWE-306",references:[]},
{id:"CVE-DEMO-012",model:"H1",component:"Dex3 Hand API",cvss:8.8,severity:"HIGH",vector:"Network",description:"Unauthenticated gRPC endpoint exposes gripper force parameters — remote attacker can set unsafe torque limits risking physical injury.",published:"2024-06-01",status:"In Progress",cwe:"CWE-306",references:[]},
{id:"CVE-DEMO-013",model:"H1",component:"Balance Controller",cvss:7.8,severity:"HIGH",vector:"Adjacent",description:"Race condition in balance state machine triggerable via crafted UDP — causes uncontrolled fall. High physical harm potential.",published:"2024-04-17",status:"Patch Available",cwe:"CWE-362",references:[]},
{id:"CVE-DEMO-014",model:"H1",component:"Joint Controller",cvss:6.9,severity:"MEDIUM",vector:"Local",description:"Joint velocity limits overridable via unprivileged IPC call — risks mechanical damage or operator injury.",published:"2024-02-28",status:"Patch Available",cwe:"CWE-269",references:[]},
{id:"CVE-DEMO-015",model:"H1",component:"Emergency Stop",cvss:5.5,severity:"MEDIUM",vector:"Adjacent",description:"Software e-stop bypassable by replaying captured 'enable' packet — operators may be unable to halt unsafe motion.",published:"2024-03-30",status:"In Progress",cwe:"CWE-294",references:[]},
{id:"CVE-DEMO-016",model:"B1",component:"Industrial API",cvss:7.4,severity:"HIGH",vector:"Network",description:"Hardcoded API key in B1 industrial SDK allows remote attackers to authenticate as privileged client apps.",published:"2023-12-05",status:"Patched",cwe:"CWE-798",references:[]},
{id:"CVE-DEMO-017",model:"B1",component:"Payload Interface",cvss:5.9,severity:"MEDIUM",vector:"Local",description:"Payload attachment interface lacks integrity checks — locally authenticated user can load peripherals that bypass safety limits.",published:"2024-01-11",status:"Patch Available",cwe:"CWE-345",references:[]},
{id:"CVE-DEMO-018",model:"B2",component:"IPC Protocol",cvss:6.7,severity:"MEDIUM",vector:"Network",description:"Static encryption key distributed in SDK — all inter-module IPC traffic decryptable by SDK holder.",published:"2024-05-20",status:"In Progress",cwe:"CWE-321",references:[]},
{id:"CVE-DEMO-019",model:"Z1",component:"Teach Pendant",cvss:4.6,severity:"MEDIUM",vector:"Physical",description:"USB port lacks code signing — physical attacker can load malicious motion programs via flash drive.",published:"2024-04-02",status:"Won't Fix",cwe:"CWE-345",references:[]},
{id:"CVE-DEMO-020",model:"Z1",component:"Arm Controller",cvss:3.2,severity:"LOW",vector:"Local",description:"Motion trajectory logs stored unencrypted — may expose proprietary manufacturing process data to local users.",published:"2024-01-30",status:"Patch Available",cwe:"CWE-312",references:[]},
];
// ═══════════════ STATE ═══════════════
let cves = [...DEMO];
let pending = null;
let sort = {col:'cvss', dir:-1};
let charts = {};
// ═══════════════ ANIMATED BG ═══════════════
(function(){
const c = document.getElementById('bg'), ctx = c.getContext('2d');
let W,H,pts=[];
function resize(){W=c.width=innerWidth;H=c.height=innerHeight}
function init(){pts=Array.from({length:70},()=>({x:Math.random()*W,y:Math.random()*H,vx:(Math.random()-.5)*.35,vy:(Math.random()-.5)*.35,r:Math.random()*1.8+.4,a:Math.random()*.45+.08}))}
function draw(){
ctx.clearRect(0,0,W,H);
pts.forEach(p=>{
p.x=(p.x+p.vx+W)%W; p.y=(p.y+p.vy+H)%H;
ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,6.28);
ctx.fillStyle=`rgba(0,255,136,${p.a})`;ctx.fill();
});
for(let i=0;i<pts.length;i++)for(let j=i+1;j<pts.length;j++){
const dx=pts[i].x-pts[j].x,dy=pts[i].y-pts[j].y,d=Math.sqrt(dx*dx+dy*dy);
if(d<115){ctx.beginPath();ctx.moveTo(pts[i].x,pts[i].y);ctx.lineTo(pts[j].x,pts[j].y);ctx.strokeStyle=`rgba(0,255,136,${.07*(1-d/115)})`;ctx.lineWidth=.5;ctx.stroke()}
}
requestAnimationFrame(draw);
}
resize();init();draw();
addEventListener('resize',()=>{resize();init()});
})();
// ═══════════════ UTILS ═══════════════
const PALETTE=['#ff3366','#ff8c42','#ffd166','#00b4d8','#00ff88','#7b2fff','#06d6a0','#ef476f'];
function sevColor(s){return{CRITICAL:'#ff3366',HIGH:'#ff8c42',MEDIUM:'#ffd166',LOW:'#00b4d8',INFO:'#8b949e'}[s]||'#8b949e'}
function cvssColor(v){if(v>=9)return'#ff3366';if(v>=7)return'#ff8c42';if(v>=4)return'#ffd166';return'#00b4d8'}
function sevRank(s){return{CRITICAL:0,HIGH:1,MEDIUM:2,LOW:3,INFO:4}[s]??5}
function robustness(model){
const m=cves.filter(c=>c.model===model);
if(!m.length)return 100;
const avg=m.reduce((a,b)=>a+b.cvss,0)/m.length;
const crit=m.filter(c=>c.severity==='CRITICAL').length;
const hi=m.filter(c=>c.severity==='HIGH').length;
return Math.max(0,Math.min(100,Math.round(100-avg*5-crit*6-hi*2-m.length*.5)));
}
function robColor(s){return s>=70?'#00ff88':s>=40?'#ffd166':'#ff3366'}
function toast(msg){const el=document.getElementById('toast');el.textContent=msg;el.classList.add('show');setTimeout(()=>el.classList.remove('show'),3000)}
function countAnim(id,n){const el=document.getElementById(id);let i=0;const s=Math.max(1,Math.ceil(n/28));const t=setInterval(()=>{i=Math.min(i+s,n);el.textContent=i;if(i>=n)clearInterval(t)},30)}
function kill(k){if(charts[k]){try{charts[k].destroy()}catch(e){}delete charts[k]}}
function uniq(arr){return[...new Set(arr)]}
// ═══════════════ TAB ═══════════════
function tab(id){
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p=>p.classList.remove('active'));
document.querySelector(`.tab[onclick="tab('${id}')"]`).classList.add('active');
document.getElementById('panel-'+id).classList.add('active');
if(id==='comparison')renderComparison();
}
// ═══════════════ DASHBOARD ═══════════════
function renderDashboard(){
const n=cves.length,crit=cves.filter(c=>c.severity==='CRITICAL').length;
const avg=(cves.reduce((a,b)=>a+b.cvss,0)/n).toFixed(1);
const models=uniq(cves.map(c=>c.model)).length;
countAnim('k-total',n);countAnim('k-crit',crit);
document.getElementById('k-cvss').textContent=avg;
countAnim('k-models',models);
mkSev();mkTime();mkVec();mkMod();
}
function mkSev(){
kill('sev');
const cnt={};cves.forEach(c=>{cnt[c.severity]=(cnt[c.severity]||0)+1});
const lbls=['CRITICAL','HIGH','MEDIUM','LOW'];
const cols=lbls.map(l=>sevColor(l));
charts.sev=new Chart(document.getElementById('ch-sev'),{type:'doughnut',data:{labels:lbls,datasets:[{data:lbls.map(l=>cnt[l]||0),backgroundColor:cols.map(c=>c+'aa'),borderColor:cols,borderWidth:2}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:'bottom',labels:{color:'#8b949e',font:{size:9,family:'Courier New'},boxWidth:10}}},animation:{duration:700}}});
}
function mkTime(){
kill('time');
const yrs={};cves.forEach(c=>{const y=c.published?.substring(0,4)||'?';if(!yrs[y])yrs[y]={CRITICAL:0,HIGH:0,MEDIUM:0,LOW:0};yrs[y][c.severity]=(yrs[y][c.severity]||0)+1});
const lbls=Object.keys(yrs).sort();
const sevs=['CRITICAL','HIGH','MEDIUM','LOW'];
charts.time=new Chart(document.getElementById('ch-time'),{type:'bar',data:{labels:lbls,datasets:sevs.map(s=>({label:s,data:lbls.map(y=>yrs[y][s]||0),backgroundColor:sevColor(s)+'88',borderColor:sevColor(s),borderWidth:1,borderRadius:3}))},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:'#8b949e',font:{size:9,family:'Courier New'},boxWidth:10}}},scales:{x:{stacked:true,grid:{color:'#21262d'},ticks:{color:'#8b949e',font:{size:9}}},y:{stacked:true,grid:{color:'#21262d'},ticks:{color:'#8b949e',font:{size:9}}}},animation:{duration:700}}});
}
function mkVec(){
kill('vec');
const cnt={};cves.forEach(c=>{cnt[c.vector]=(cnt[c.vector]||0)+1});
const lbls=Object.keys(cnt);
charts.vec=new Chart(document.getElementById('ch-vec'),{type:'bar',data:{labels:lbls,datasets:[{data:Object.values(cnt),backgroundColor:lbls.map((_,i)=>PALETTE[i%PALETTE.length]+'88'),borderColor:lbls.map((_,i)=>PALETTE[i%PALETTE.length]),borderWidth:2,borderRadius:5}]},options:{indexAxis:'y',responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{grid:{color:'#21262d'},ticks:{color:'#8b949e',font:{size:9}}},y:{grid:{display:false},ticks:{color:'#8b949e',font:{size:9,family:'Courier New'}}}},animation:{duration:700}}});
}
function mkMod(){
kill('mod');
const cnt={};cves.forEach(c=>{cnt[c.model]=(cnt[c.model]||0)+1});
const lbls=Object.keys(cnt).sort((a,b)=>cnt[b]-cnt[a]);
charts.mod=new Chart(document.getElementById('ch-mod'),{type:'bar',data:{labels:lbls,datasets:[{data:lbls.map(l=>cnt[l]),backgroundColor:lbls.map((_,i)=>PALETTE[i%PALETTE.length]+'88'),borderColor:lbls.map((_,i)=>PALETTE[i%PALETTE.length]),borderWidth:2,borderRadius:5}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false}},scales:{x:{grid:{display:false},ticks:{color:'#8b949e',font:{size:9,family:'Courier New'}}},y:{grid:{color:'#21262d'},ticks:{color:'#8b949e',font:{size:9}}}},animation:{duration:700}}});
}
// ═══════════════ EXPLORER ═══════════════
function sortBy(col){if(sort.col===col)sort.dir*=-1;else{sort.col=col;sort.dir=-1}renderTable()}
function renderTable(){
const q=document.getElementById('s-search').value.toLowerCase();
const fm=document.getElementById('s-model').value;
const fs=document.getElementById('s-sev').value;
const fv=document.getElementById('s-vec').value;
let rows=cves.filter(c=>{
const ms=!q||(c.id+c.model+c.component+(c.description||'')).toLowerCase().includes(q);
return ms&&(!fm||c.model===fm)&&(!fs||c.severity===fs)&&(!fv||c.vector===fv);
});
rows.sort((a,b)=>{
let va=a[sort.col],vb=b[sort.col];
if(sort.col==='severity'){va=sevRank(va);vb=sevRank(vb)}
if(typeof va==='string')return sort.dir*va.localeCompare(vb);
return sort.dir*(vb-va);
});
document.getElementById('tbody').innerHTML=rows.map((c,i)=>`
<tr class="data-row" onclick="tog('d${i}')">
<td style="color:#fff;font-weight:bold">${c.id}</td>
<td>${c.model}</td><td>${c.component}</td>
<td><div class="cbar"><span style="color:${cvssColor(c.cvss)};font-weight:bold">${c.cvss}</span><div class="ctrack"><div class="cfill" style="width:${c.cvss*10}%;background:${cvssColor(c.cvss)}"></div></div></div></td>
<td><span class="sev s-${c.severity}">${c.severity}</span></td>
<td style="color:var(--dim)">${c.vector}</td>
<td style="color:var(--dim)">${c.published||'—'}</td>
</tr>
<tr class="detail-row"><td colspan="7"><div class="detail" id="d${i}">
<div class="dg">
<div class="di"><div class="dl">CWE</div><div class="dv">${c.cwe||'—'}</div></div>
<div class="di"><div class="dl">Status</div><div class="dv">${c.status||'—'}</div></div>
<div class="di"><div class="dl">References</div><div class="dv">${c.references?.length?c.references.join(' '):'None'}</div></div>
</div>
${c.description?`<div class="desc">📋 ${c.description}</div>`:''}
</div></td></tr>`).join('');
document.getElementById('tcount').textContent=`Showing ${rows.length} of ${cves.length} CVEs`;
}
function tog(id){const el=document.getElementById(id);if(el)el.classList.toggle('open')}
function populateFilters(){
const sel=document.getElementById('s-model');
const models=uniq(cves.map(c=>c.model)).sort();
sel.innerHTML='<option value="">All Models</option>'+models.map(m=>`<option>${m}</option>`).join('');
}
// ═══════════════ COMPARISON ═══════════════
function renderComparison(){
const models=uniq(cves.map(c=>c.model)).sort();
document.getElementById('scores').innerHTML=models.map(m=>{
const sc=robustness(m),col=robColor(sc);
const n=cves.filter(c=>c.model===m).length;
const cr=cves.filter(c=>c.model===m&&c.severity==='CRITICAL').length;
return `<div class="mrow"><div class="mrh"><span class="mname">Unitree ${m}</span><span class="mscore" style="color:${col}">${sc}<small style="font-size:12px">/100</small></span></div><div class="sbar"><div class="sfill" style="width:${sc}%;background:${col}"></div></div><div class="msub">${n} CVE${n!==1?'s':''} · ${cr} critical</div></div>`;
}).join('');
kill('radar');
const dims=['Network','Adjacent','Local','Physical'];
charts.radar=new Chart(document.getElementById('ch-radar'),{type:'radar',data:{labels:dims,datasets:models.map((m,i)=>{
const mc=cves.filter(c=>c.model===m);
return{label:m,data:dims.map(d=>mc.filter(c=>c.vector===d).reduce((a,b)=>a+b.cvss,0)),borderColor:PALETTE[i%PALETTE.length],backgroundColor:PALETTE[i%PALETTE.length]+'20',borderWidth:2,pointBackgroundColor:PALETTE[i%PALETTE.length],pointRadius:3};
})},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:'#8b949e',font:{size:9,family:'Courier New'},boxWidth:10}}},scales:{r:{grid:{color:'#21262d'},ticks:{display:false},pointLabels:{color:'#8b949e',font:{size:10,family:'Courier New'}},angleLines:{color:'#21262d'}}},animation:{duration:700}}});
kill('box');
const md=models.map(m=>{const mc=cves.filter(c=>c.model===m);return{m,avg:mc.reduce((a,b)=>a+b.cvss,0)/mc.length,max:Math.max(...mc.map(c=>c.cvss))};}).sort((a,b)=>b.avg-a.avg);
charts.box=new Chart(document.getElementById('ch-box'),{type:'bar',data:{labels:md.map(d=>d.m),datasets:[{label:'Avg CVSS',data:md.map(d=>+d.avg.toFixed(2)),backgroundColor:md.map(d=>cvssColor(d.avg)+'99'),borderColor:md.map(d=>cvssColor(d.avg)),borderWidth:2,borderRadius:4},{label:'Max CVSS',data:md.map(d=>d.max),type:'line',backgroundColor:'transparent',borderColor:md.map(d=>cvssColor(d.max)),borderWidth:1.5,pointBackgroundColor:md.map(d=>cvssColor(d.max)),pointRadius:4,tension:.3}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{labels:{color:'#8b949e',font:{size:9,family:'Courier New'},boxWidth:10}}},scales:{x:{grid:{display:false},ticks:{color:'#8b949e',font:{size:9,family:'Courier New'}}},y:{min:0,max:10,grid:{color:'#21262d'},ticks:{color:'#8b949e',font:{size:9}}}},animation:{duration:700}}});
}
// ═══════════════ FILE LOADER ═══════════════
const drop=document.getElementById('drop');
drop.addEventListener('dragover',e=>{e.preventDefault();drop.classList.add('over')});
drop.addEventListener('dragleave',()=>drop.classList.remove('over'));
drop.addEventListener('drop',e=>{e.preventDefault();drop.classList.remove('over');onFile({target:{files:e.dataTransfer.files}})});
async function onFile(ev){
const file=ev.target.files[0];if(!file)return;
const ext=file.name.split('.').pop().toLowerCase();
document.getElementById('p-label').textContent=`📄 ${file.name} (${(file.size/1024).toFixed(1)} KB)`;
document.getElementById('preview').innerHTML='<span style="color:var(--dim)">Parsing…</span>';
document.getElementById('preview-wrap').style.display='block';
pending=null;
try{
let raw;
if(ext==='json'){raw=normalise(JSON.parse(await file.text()))}
else if(ext==='jsonl'){raw=normalise((await file.text()).trim().split('\n').filter(Boolean).map(l=>JSON.parse(l)))}
else if(ext==='csv'){raw=normalise(parseCSV(await file.text()))}
else if(ext==='parquet'){
try{
const {parquetRead}=await import('https://esm.sh/hyparquet@1');
const rows=[];
const buf={byteLength:file.size,slice:async(s,e)=>(await file.slice(s,e).arrayBuffer())};
await parquetRead({file:buf,onComplete:d=>rows.push(...d)});
raw=normalise(rows);
}catch(e){
document.getElementById('preview').innerHTML=`<span style="color:var(--red)">⚠ Parquet read error: ${e.message}<br>Try converting to JSON/CSV first.</span>`;
return;
}
}else{
document.getElementById('preview').innerHTML='<span style="color:var(--red)">⚠ Unsupported format</span>';return;
}
pending=raw;
document.getElementById('p-count').textContent=`${raw.length} records`;
document.getElementById('preview').innerHTML=`<pre>${JSON.stringify(raw.slice(0,3),null,2)}${raw.length>3?`\n…and ${raw.length-3} more`:''}</pre>`;
}catch(e){
document.getElementById('preview').innerHTML=`<span style="color:var(--red)">⚠ Parse error: ${e.message}</span>`;
}
}
function normalise(arr){
if(!Array.isArray(arr))arr=arr.data||arr.cves||Object.values(arr);
return arr.map(r=>({
id:r.id||r.cve_id||r.CVE_ID||r.cve||'CVE-UNKNOWN',
model:r.model||r.robot||r.target||r.product||'Unknown',
component:r.component||r.module||r.service||'Unknown',
cvss:parseFloat(r.cvss||r.cvss_score||r.score||0),
severity:(r.severity||r.rating||cvssToSev(parseFloat(r.cvss||0))).toUpperCase(),
vector:r.vector||r.attack_vector||r.av||'Network',
description:r.description||r.desc||r.summary||'',
published:r.published||r.date||r.publish_date||'',
status:r.status||r.fix_status||'',
cwe:r.cwe||r.cwe_id||'',
references:Array.isArray(r.references)?r.references:(r.references?[r.references]:[]),
}));
}
function cvssToSev(v){if(v>=9)return'CRITICAL';if(v>=7)return'HIGH';if(v>=4)return'MEDIUM';if(v>0)return'LOW';return'INFO'}
function parseCSV(txt){
const lines=txt.trim().split('\n');
const hdrs=lines[0].split(',').map(h=>h.trim().replace(/"/g,''));
return lines.slice(1).filter(Boolean).map(l=>{
const v=l.split(',').map(x=>x.trim().replace(/"/g,''));
const o={};hdrs.forEach((h,i)=>o[h]=v[i]||'');return o;
});
}
function applyData(mode){
if(!pending?.length){toast('No data to load');return}
cves=mode==='replace'?[...pending]:[...cves,...pending];
pending=null;
document.getElementById('preview-wrap').style.display='none';
document.getElementById('src-lbl').textContent=`${cves.length} CVEs LOADED`;
populateFilters();renderDashboard();renderTable();
toast(`✓ ${mode==='replace'?'Replaced':'Merged'} → ${cves.length} total CVEs`);
tab('dashboard');
}
// ═══════════════ INIT ═══════════════
Chart.defaults.color='#8b949e';
Chart.defaults.font.family='Courier New';
Chart.defaults.borderColor='#21262d';
populateFilters();
renderDashboard();
renderTable();
</script>
</body>
</html>