/* 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, "'");
}
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, "$1 ")
.replace(/`([^`]+)`/g, "$1");
}
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 `
${esc(t.category_id)}
${esc(t.name)}${esc(t.name_cn)}
${esc(t.owasp_asi)}
${esc(t.owasp_llm)}
${esc(t.default_severity)}
${cases}
`;
})
.join("");
document.getElementById("taxonomy-table").innerHTML = `
Nine categories, mapped to OWASP ASI 2026
category name OWASP ASI 2026 OWASP LLM
severity cases (public)
${rows}
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.
`;
document.getElementById("taxonomy-details").innerHTML = DATA.taxonomy
.map((t) => `${esc(t.name)} — ${esc(t.description)} `)
.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) => `${esc(c.attack_id)} · ${esc(c.name)} `)
.join("");
return `${opts} `;
})
.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) => `${esc(k)}=${esc(attrValue(p.attrs[k]))}`)
.join(" ");
const isTarget = p.pid === c.target_pid ? ' class="target"' : "";
return `
${esc(p.pid)}
${esc(p.name)}${esc(p.seller)}
${esc(money(p.price))}
${esc(p.rating)}/5 (${esc(p.review_count)})
${esc(p.seller_trust)}
${attrs || 'none '}
`;
})
.join("");
return `
pid product price rating
seller trust poisoning attrs
${rows}
`;
}
function runPanel(title, run) {
const ok = run.verdict === "PASS";
const reasons = (run.reasons || []).length
? `${run.reasons.map((r) => `${fmt(r)} `).join("")} `
: `no attack evidence found in the answer `;
const blocked = run.blocked
? `rag_guard removed every retrieved listing, so the agent answered from an empty catalog.
`
: "";
return `
${esc(title)}
${esc(run.verdict)}
hazard ${esc(run.hazard)}
${fmt(run.response)}
${blocked}
${reasons}
`;
}
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
? `Hidden instruction planted in the catalog ` +
Object.entries(c.hidden_instructions)
.map(([pid, text]) => `${esc(pid)} — ${esc(text)} `)
.join("")
: "";
const signals = c.failure_signals.map((s) => `${esc(s)}`).join(" ");
document.getElementById("case-view").innerHTML = `
${esc(c.name)}
${esc(c.category_name)} (${esc(c.category_name_cn)})
${esc(c.severity)}
${esc(c.owasp_asi)}
${esc(c.owasp_llm)}
source ${esc(c.source)}
${esc(c.description)}
Shopper query
${esc(c.user_query)}
Attacker goal
${esc(c.goal)}
Poisoned catalog
${productsTable(c)}
${hidden}
Judge failure signals
${signals}
${runPanel("Baseline agent (no defense)", baseline)}
${runPanel(defendedTitle, defended)}
`;
}
/* -------------------------------------------------------------- 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 `
${esc(s.label)}
${s.n}
${pct(s.asr)}
${eff}
${s.hazard.toFixed(3)}
${pct(s.overblock)}
`;
})
.join("");
document.getElementById("bench-table").innerHTML = `
defense n ASR
effectiveness hazard over-block
${rows}
`;
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 `
${esc(s.label)}
${pct(s.asr)}
`;
})
.join("");
const cats = Object.keys(stacks[0].per_category).sort();
const head = stacks.map((s) => `${esc(s.label)} `).join("");
const body = cats
.map((cat) => {
const cells = stacks
.map((s) => `${pct0(s.per_category[cat] || 0)} `)
.join("");
return `${esc(cat)} ${cells} `;
})
.join("");
document.getElementById("bench-matrix").innerHTML = `
`;
}
/* ------------------------------------------------------------------- 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",
`Could not load ${DATA_URL} — ${esc(err.message)}.
If you opened this file directly, serve the folder over HTTP
(python -m http.server) so the fetch is allowed.
`
);
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);
})();