const DATASET_ERROR = "Could not load snapshot JSON. Open the dataset: https://huggingface.co/datasets/HashgraphOnline/hol-plugin-security"; const PLUGIN_COLS = ["slug","display_name","developer_name","category","trust_score","safety_label","scanner_grade","score_security","score_mcp_posture","findings_high","findings_total","github_stars","mcp_server_count","skill_count","version","ecosystems","scanner_provider","owner_verified","publisher_verified","snapshot_at","description","repository","profile_url"]; const ADV_COLS = ["owasp_mcp","owasp_mcp_name","slug","title","severity","threat_class","summary","surfaces","url","date_modified","curated"]; const FIX_COLS = ["scenario_id","scenario_family","harness_id","comparator","outcome","receipt_created","modeled","harness_independent","median_latency_ms","p95_latency_ms","run_id","schema_version"]; function unique(rows, key) { return [...new Set(rows.map(r => String(r[key] ?? "")).filter(Boolean))].sort(); } function paginate(rows, page, size) { const pages = Math.max(1, Math.ceil(rows.length / size)); page = Math.min(Math.max(1, page), pages); const start = (page - 1) * size; return { slice: rows.slice(start, start + size), page, pages, n: rows.length, start, end: Math.min(start + size, rows.length) }; } function searchFilter(rows, q) { q = (q || "").trim().toLowerCase(); if (!q) return rows; return rows.filter(r => Object.values(r).some(v => String(v).toLowerCase().includes(q))); } function eqFilter(rows, key, value) { if (!value || value === "(all)") return rows; return rows.filter(r => String(r[key]) === value); } function sortRows(rows, key, dir) { const copy = rows.slice(); copy.sort((a, b) => { const va = a[key], vb = b[key]; const na = Number(va), nb = Number(vb); let cmp; if (va !== "" && vb !== "" && va != null && vb != null && !Number.isNaN(na) && !Number.isNaN(nb)) cmp = na - nb; else cmp = String(va).localeCompare(String(vb), undefined, { numeric: true, sensitivity: "base" }); return dir === "desc" ? -cmp : cmp; }); return copy; } function el(tag, attrs, html) { const n = document.createElement(tag); if (attrs) Object.entries(attrs).forEach(([k, v]) => { if (k === "class") n.className = v; else n.setAttribute(k, v); }); if (html != null) n.innerHTML = html; return n; } function renderTable(table, rows, cols) { table.innerHTML = ""; const thead = el("thead"); const tr = el("tr"); cols.forEach(c => tr.appendChild(el("th", { "data-col": c }, c))); thead.appendChild(tr); const tbody = el("tbody"); rows.forEach(r => { const row = el("tr"); cols.forEach(c => { const v = r[c]; const td = el("td"); const s = v == null ? "" : String(v); if (typeof v === "string" && s.startsWith("http")) { const a = el("a", { href: s, target: "_blank", rel: "noopener" }); a.textContent = s.replace(/^https?:\/\//, "").slice(0, 64); td.appendChild(a); } else td.textContent = s; row.appendChild(td); }); tbody.appendChild(row); }); table.appendChild(thead); table.appendChild(tbody); } function bindTable(opts) { const state = { q: "", filters: {}, page: 1, size: opts.pageSize, sortKey: opts.sortKey, sortDir: opts.sortDir || "desc" }; const controls = document.getElementById(opts.controlId); const status = document.getElementById(opts.statusId); const table = document.getElementById(opts.tableId); const search = el("input", { type: "search", placeholder: "Search…" }); search.addEventListener("input", () => { state.q = search.value; state.page = 1; draw(); }); controls.appendChild(search); opts.filterKeys.forEach(key => { const sel = el("select"); sel.appendChild(el("option", { value: "(all)" }, key + ": all")); unique(opts.rows, key).forEach(v => sel.appendChild(el("option", { value: v }, v))); sel.addEventListener("change", () => { state.filters[key] = sel.value; state.page = 1; draw(); }); controls.appendChild(sel); }); const sizeSel = el("select"); opts.sizes.forEach(s => sizeSel.appendChild(el("option", { value: String(s) }, String(s) + " / page"))); sizeSel.value = String(opts.pageSize); sizeSel.addEventListener("change", () => { state.size = Number(sizeSel.value); state.page = 1; draw(); }); controls.appendChild(sizeSel); const prev = el("button", { type: "button" }, "Prev"); const next = el("button", { type: "button" }, "Next"); prev.addEventListener("click", () => { state.page -= 1; draw(); }); next.addEventListener("click", () => { state.page += 1; draw(); }); controls.appendChild(prev); controls.appendChild(next); table.addEventListener("click", (e) => { const th = e.target.closest("th"); if (!th) return; const key = th.getAttribute("data-col"); if (state.sortKey === key) state.sortDir = state.sortDir === "asc" ? "desc" : "asc"; else { state.sortKey = key; state.sortDir = "desc"; } draw(); }); function draw() { let rows = searchFilter(opts.rows, state.q); Object.entries(state.filters).forEach(([k, v]) => { rows = eqFilter(rows, k, v); }); if (state.sortKey) rows = sortRows(rows, state.sortKey, state.sortDir); const pg = paginate(rows, state.page, state.size); state.page = pg.page; status.textContent = pg.n + " rows · page " + pg.page + "/" + pg.pages + " · showing " + (pg.n ? pg.start + 1 : 0) + "–" + pg.end + (state.sortKey ? " · sort " + state.sortKey + " " + state.sortDir : ""); renderTable(table, pg.slice, opts.cols); } draw(); } document.querySelectorAll(".tabs button").forEach(btn => { btn.addEventListener("click", () => { document.querySelectorAll(".tabs button").forEach(b => b.classList.remove("active")); document.querySelectorAll(".panel").forEach(p => p.classList.remove("active")); btn.classList.add("active"); document.getElementById(btn.dataset.tab).classList.add("active"); }); }); async function loadJSON(path) { const res = await fetch(path); if (!res.ok) throw new Error(path + " HTTP " + res.status); return res.json(); } (async () => { try { const [plugins, advisories, runtime] = await Promise.all([ loadJSON("data/plugins.json"), loadJSON("data/advisories.json"), loadJSON("data/runtime_fixtures.json"), ]); bindTable({ rows: plugins, cols: PLUGIN_COLS, controlId: "plugin-controls", statusId: "plugin-status", tableId: "plugin-table", filterKeys: ["safety_label", "scanner_grade", "category"], pageSize: 50, sizes: [25, 50, 100, 205], sortKey: "trust_score", sortDir: "desc" }); bindTable({ rows: advisories, cols: ADV_COLS, controlId: "advisory-controls", statusId: "advisory-status", tableId: "advisory-table", filterKeys: ["owasp_mcp", "severity", "threat_class"], pageSize: 22, sizes: [22, 50], sortKey: "owasp_mcp", sortDir: "asc" }); bindTable({ rows: runtime, cols: FIX_COLS, controlId: "runtime-controls", statusId: "runtime-status", tableId: "runtime-table", filterKeys: ["scenario_family", "harness_id", "comparator", "outcome"], pageSize: 50, sizes: [25, 50, 100, 220], sortKey: "scenario_id", sortDir: "asc" }); } catch (err) { const box = document.querySelector("header"); const p = document.createElement("div"); p.className = "note"; p.innerHTML = "Dataset load error. " + DATASET_ERROR + "
"; p.querySelector("code").textContent = String(err); box.appendChild(p); } })();