RecRedTeam AuditLab: static audit page, 234 cases x 8 defense combinations precomputed
ca949ba verified Download assets/app.js from NoahIsARider/RecRedTeam-AuditLab: direct link, hf CLI and curl.
- Browser
- Download file 12.2 kB
-
https://huggingface.co/spaces/NoahIsARider/RecRedTeam-AuditLab/resolve/main/assets/app.js
- Command line
-
hf download hf://spaces/NoahIsARider/RecRedTeam-AuditLab/assets/app.js
-
curl -L -o app.js https://huggingface.co/spaces/NoahIsARider/RecRedTeam-AuditLab/resolve/main/assets/app.js
12.2 kB
| /* RecRedTeam AuditLab — static viewer. | |
| * | |
| * This file does no auditing. Every verdict, hazard grade and aggregate shown | |
| * here was computed by the Python engine in `auditlab/` and frozen into | |
| * `data/results.json` by `build_static.py`. The page only renders that JSON, | |
| * which is what makes it publishable as a dependency-free static Space. | |
| */ | |
| const PLUGIN_ORDER = ["sanitizer", "rag_guard", "policy_guard"]; | |
| const DATA_URL = "data/results.json"; | |
| let DATA = null; | |
| let currentCase = null; | |
| /* ------------------------------------------------------------------ utils */ | |
| function esc(value) { | |
| return String(value) | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| function pct(x) { | |
| return (x * 100).toFixed(x === 0 || x === 1 ? 0 : 1) + "%"; | |
| } | |
| /* Escape, then re-introduce the two bits of markdown the engine emits: | |
| **bold** in agent answers and `code` in judge reasons. */ | |
| function fmt(value) { | |
| return esc(value) | |
| .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>") | |
| .replace(/`([^`]+)`/g, "<code>$1</code>"); | |
| } | |
| function pct0(x) { | |
| return Math.round(x * 100) + "%"; | |
| } | |
| function money(x) { | |
| return "$" + Number(x).toFixed(2); | |
| } | |
| function attrValue(v) { | |
| if (Array.isArray(v)) return v.join(" / "); | |
| if (v === true) return "true"; | |
| if (v === false) return "false"; | |
| return String(v); | |
| } | |
| function severityClass(sev) { | |
| return "chip sev-" + esc(sev); | |
| } | |
| /* --------------------------------------------------------------- data load */ | |
| async function load() { | |
| const res = await fetch(DATA_URL, { cache: "no-cache" }); | |
| if (!res.ok) throw new Error("HTTP " + res.status + " for " + DATA_URL); | |
| return res.json(); | |
| } | |
| /* -------------------------------------------------------------- taxonomy */ | |
| function renderTaxonomy() { | |
| const rows = DATA.taxonomy | |
| .map((t) => { | |
| const cases = DATA.counts[t.category_id] || 0; | |
| return `<tr> | |
| <td><code>${esc(t.category_id)}</code></td> | |
| <td>${esc(t.name)}<br><span class="note">${esc(t.name_cn)}</span></td> | |
| <td>${esc(t.owasp_asi)}</td> | |
| <td>${esc(t.owasp_llm)}</td> | |
| <td><span class="${severityClass(t.default_severity)}">${esc(t.default_severity)}</span></td> | |
| <td class="num">${cases}</td> | |
| </tr>`; | |
| }) | |
| .join(""); | |
| document.getElementById("taxonomy-table").innerHTML = ` | |
| <h2>Nine categories, mapped to OWASP ASI 2026</h2> | |
| <div class="table-wrap"> | |
| <table> | |
| <thead><tr> | |
| <th>category</th><th>name</th><th>OWASP ASI 2026</th><th>OWASP LLM</th> | |
| <th>severity</th><th class="num">cases (public)</th> | |
| </tr></thead> | |
| <tbody>${rows}</tbody> | |
| </table> | |
| </div> | |
| <p class="note">This Space ships the public split, ${DATA.meta.n_cases} of the | |
| ${DATA.meta.n_total} cases in the dataset. The held-out split is reserved for reporting.</p>`; | |
| document.getElementById("taxonomy-details").innerHTML = DATA.taxonomy | |
| .map((t) => `<li><b>${esc(t.name)}</b> — ${esc(t.description)}</li>`) | |
| .join(""); | |
| } | |
| /* ------------------------------------------------------------ single case */ | |
| function fillCaseSelect() { | |
| const byCat = new Map(); | |
| for (const c of DATA.cases) { | |
| if (!byCat.has(c.category_id)) byCat.set(c.category_id, []); | |
| byCat.get(c.category_id).push(c); | |
| } | |
| const groups = [...byCat.entries()] | |
| .sort((a, b) => a[0].localeCompare(b[0])) | |
| .map(([catId, cases]) => { | |
| const label = cases[0].category_name; | |
| const opts = cases | |
| .map((c) => `<option value="${esc(c.attack_id)}">${esc(c.attack_id)} · ${esc(c.name)}</option>`) | |
| .join(""); | |
| return `<optgroup label="${esc(label)}">${opts}</optgroup>`; | |
| }) | |
| .join(""); | |
| document.getElementById("case-select").innerHTML = groups; | |
| } | |
| function selectedPlugins() { | |
| return [...document.querySelectorAll("[data-plugin]")] | |
| .filter((el) => el.checked) | |
| .map((el) => el.dataset.plugin) | |
| .sort((a, b) => PLUGIN_ORDER.indexOf(a) - PLUGIN_ORDER.indexOf(b)); | |
| } | |
| function comboKey(plugins) { | |
| return plugins.length ? plugins.join("+") : "baseline"; | |
| } | |
| function productsTable(c) { | |
| const rows = c.products | |
| .map((p) => { | |
| const attrs = Object.keys(p.attrs || {}) | |
| .sort() | |
| .map((k) => `<code>${esc(k)}</code>=${esc(attrValue(p.attrs[k]))}`) | |
| .join("<br>"); | |
| const isTarget = p.pid === c.target_pid ? ' class="target"' : ""; | |
| return `<tr${isTarget}> | |
| <td><code>${esc(p.pid)}</code></td> | |
| <td>${esc(p.name)}<br><span class="note">${esc(p.seller)}</span></td> | |
| <td class="num">${esc(money(p.price))}</td> | |
| <td class="num">${esc(p.rating)}/5<span class="note"> (${esc(p.review_count)})</span></td> | |
| <td class="num">${esc(p.seller_trust)}</td> | |
| <td>${attrs || '<span class="note">none</span>'}</td> | |
| </tr>`; | |
| }) | |
| .join(""); | |
| return `<div class="table-wrap"> | |
| <table> | |
| <thead><tr> | |
| <th>pid</th><th>product</th><th class="num">price</th><th class="num">rating</th> | |
| <th class="num">seller trust</th><th>poisoning attrs</th> | |
| </tr></thead> | |
| <tbody>${rows}</tbody> | |
| </table> | |
| </div>`; | |
| } | |
| function runPanel(title, run) { | |
| const ok = run.verdict === "PASS"; | |
| const reasons = (run.reasons || []).length | |
| ? `<ul class="reasons">${run.reasons.map((r) => `<li>${fmt(r)}</li>`).join("")}</ul>` | |
| : `<ul class="reasons"><li>no attack evidence found in the answer</li></ul>`; | |
| const blocked = run.blocked | |
| ? `<p class="note">rag_guard removed every retrieved listing, so the agent answered from an empty catalog.</p>` | |
| : ""; | |
| return `<div class="run"> | |
| <h3>${esc(title)}</h3> | |
| <div class="verdict ${ok ? "pass" : "fail"}"> | |
| <span>${esc(run.verdict)}</span> | |
| <span class="haz">hazard ${esc(run.hazard)}</span> | |
| </div> | |
| <pre class="answer">${fmt(run.response)}</pre> | |
| ${blocked} | |
| ${reasons} | |
| </div>`; | |
| } | |
| function renderCase() { | |
| const c = DATA.cases.find((x) => x.attack_id === currentCase) || DATA.cases[0]; | |
| currentCase = c.attack_id; | |
| const plugins = selectedPlugins(); | |
| const key = comboKey(plugins); | |
| const baseline = c.runs.baseline; | |
| const defended = c.runs[key]; | |
| const defendedTitle = | |
| plugins.length ? `Defended agent (${plugins.join(" + ")})` : "Defended agent (no plugin selected)"; | |
| const hidden = Object.keys(c.hidden_instructions || {}).length | |
| ? `<h3>Hidden instruction planted in the catalog</h3>` + | |
| Object.entries(c.hidden_instructions) | |
| .map(([pid, text]) => `<blockquote class="query"><code>${esc(pid)}</code> — ${esc(text)}</blockquote>`) | |
| .join("") | |
| : ""; | |
| const signals = c.failure_signals.map((s) => `<code>${esc(s)}</code>`).join(" "); | |
| document.getElementById("case-view").innerHTML = ` | |
| <div class="case-head"> | |
| <h2>${esc(c.name)}</h2> | |
| </div> | |
| <p class="meta-line"> | |
| <span>${esc(c.category_name)} (${esc(c.category_name_cn)})</span> | |
| <span class="chip ${esc(severityClass(c.severity))}">${esc(c.severity)}</span> | |
| <code>${esc(c.owasp_asi)}</code> | |
| <code>${esc(c.owasp_llm)}</code> | |
| <span class="note">source ${esc(c.source)}</span> | |
| </p> | |
| <p>${esc(c.description)}</p> | |
| <h3>Shopper query</h3> | |
| <div class="query">${esc(c.user_query)}</div> | |
| <h3>Attacker goal</h3> | |
| <div class="goal">${esc(c.goal)}</div> | |
| <h3>Poisoned catalog</h3> | |
| ${productsTable(c)} | |
| ${hidden} | |
| <h3>Judge failure signals</h3> | |
| <p>${signals}</p> | |
| <div class="split"> | |
| ${runPanel("Baseline agent (no defense)", baseline)} | |
| ${runPanel(defendedTitle, defended)} | |
| </div>`; | |
| } | |
| /* -------------------------------------------------------------- benchmark */ | |
| function renderBenchmark() { | |
| const stacks = DATA.stacks; | |
| document.getElementById("bench-note").textContent = | |
| `Computed on the full public split — ${DATA.meta.n_cases} cases, ` + | |
| `${DATA.meta.per_category} per category, seed ${DATA.meta.seed}. ` + | |
| `The over-block column is the honest price of a blocklist defense.`; | |
| const rows = stacks | |
| .map((s) => { | |
| const eff = s.effectiveness === null ? "—" : pct(s.effectiveness); | |
| return `<tr> | |
| <td>${esc(s.label)}</td> | |
| <td class="num">${s.n}</td> | |
| <td class="num">${pct(s.asr)}</td> | |
| <td class="num">${eff}</td> | |
| <td class="num">${s.hazard.toFixed(3)}</td> | |
| <td class="num">${pct(s.overblock)}</td> | |
| </tr>`; | |
| }) | |
| .join(""); | |
| document.getElementById("bench-table").innerHTML = ` | |
| <table> | |
| <thead><tr> | |
| <th>defense</th><th class="num">n</th><th class="num">ASR</th> | |
| <th class="num">effectiveness</th><th class="num">hazard</th><th class="num">over-block</th> | |
| </tr></thead> | |
| <tbody>${rows}</tbody> | |
| </table>`; | |
| document.getElementById("bench-bars").innerHTML = stacks | |
| .map((s) => { | |
| const width = Math.max(s.asr * 100, 0.6); | |
| const cls = s.asr >= 0.67 ? "asr-high" : s.asr >= 0.34 ? "asr-mid" : "asr-low"; | |
| return `<div class="bar-row"> | |
| <div>${esc(s.label)}</div> | |
| <div class="bar-track"><div class="bar-fill ${cls}" style="width:${width.toFixed(1)}%"></div></div> | |
| <div class="bar-val">${pct(s.asr)}</div> | |
| </div>`; | |
| }) | |
| .join(""); | |
| const cats = Object.keys(stacks[0].per_category).sort(); | |
| const head = stacks.map((s) => `<th class="num">${esc(s.label)}</th>`).join(""); | |
| const body = cats | |
| .map((cat) => { | |
| const cells = stacks | |
| .map((s) => `<td class="num">${pct0(s.per_category[cat] || 0)}</td>`) | |
| .join(""); | |
| return `<tr><td><code>${esc(cat)}</code></td>${cells}</tr>`; | |
| }) | |
| .join(""); | |
| document.getElementById("bench-matrix").innerHTML = ` | |
| <table> | |
| <thead><tr><th>category</th>${head}</tr></thead> | |
| <tbody>${body}</tbody> | |
| </table>`; | |
| } | |
| /* ------------------------------------------------------------------- tabs */ | |
| function showTab(id) { | |
| document.querySelectorAll(".tab").forEach((tab) => { | |
| const on = tab.id === "tab-" + id; | |
| tab.setAttribute("aria-selected", on ? "true" : "false"); | |
| }); | |
| document.querySelectorAll(".panel").forEach((panel) => { | |
| panel.hidden = panel.id !== "panel-" + id; | |
| }); | |
| if (location.hash.split("#")[1] !== id) { | |
| history.replaceState(null, "", "#" + id); | |
| } | |
| } | |
| function wire() { | |
| document.querySelectorAll(".tab").forEach((tab) => { | |
| tab.addEventListener("click", () => showTab(tab.id.replace("tab-", ""))); | |
| }); | |
| document.getElementById("case-select").addEventListener("change", (e) => { | |
| currentCase = e.target.value; | |
| renderCase(); | |
| }); | |
| document.querySelectorAll("[data-plugin]").forEach((el) => { | |
| el.addEventListener("change", renderCase); | |
| }); | |
| document.addEventListener("keydown", (e) => { | |
| if (e.altKey || e.metaKey || e.ctrlKey) return; | |
| if (e.key === "ArrowRight" || e.key === "ArrowLeft") { | |
| const ids = ["taxonomy", "case", "bench", "about"]; | |
| const active = document.querySelector('.tab[aria-selected="true"]'); | |
| if (!active) return; | |
| const i = ids.indexOf(active.id.replace("tab-", "")); | |
| const next = ids[(i + (e.key === "ArrowRight" ? 1 : ids.length - 1)) % ids.length]; | |
| showTab(next); | |
| } | |
| }); | |
| } | |
| /* ------------------------------------------------------------------- init */ | |
| (async function init() { | |
| try { | |
| DATA = await load(); | |
| } catch (err) { | |
| document.querySelector(".wrap").insertAdjacentHTML( | |
| "afterbegin", | |
| `<div class="load-error">Could not load <code>${DATA_URL}</code> — ${esc(err.message)}. | |
| If you opened this file directly, serve the folder over HTTP | |
| (<code>python -m http.server</code>) so the fetch is allowed.</div>` | |
| ); | |
| return; | |
| } | |
| document.getElementById("build-meta").textContent = | |
| `${DATA.meta.n_cases} of ${DATA.meta.n_total} cases · seed ${DATA.meta.seed} · static snapshot`; | |
| renderTaxonomy(); | |
| fillCaseSelect(); | |
| renderBenchmark(); | |
| wire(); | |
| currentCase = DATA.cases[0].attack_id; | |
| document.getElementById("case-select").value = currentCase; | |
| renderCase(); | |
| const hash = location.hash.replace("#", ""); | |
| if (["taxonomy", "case", "bench", "about"].includes(hash)) showTab(hash); | |
| })(); | |