kantorcodes1's picture
HOL plugin-security explorer: honest OWASP MCP Top 10 map + dataset tables
0e3c00e verified
Raw History Blame Contribute Delete
7.56 kB
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 = "<strong>Dataset load error.</strong> " + DATASET_ERROR + "<br/><code></code>";
p.querySelector("code").textContent = String(err);
box.appendChild(p);
}
})();