NoahIsARider's picture
RecRedTeam AuditLab: static audit page, 234 cases x 8 defense combinations precomputed
ca949ba verified
Raw History Blame Contribute Delete
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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
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);
})();