"""RecRedTeam AuditLab — interactive red-team audit for shopping agents. Runs entirely offline on a deterministic mock agent + rule-based judge, so every number in this Space is reproducible without an API key. """ from __future__ import annotations import html as html_lib from typing import Dict, List import gradio as gr from auditlab import DEFENSES, load_cases, load_taxonomy from auditlab.pipeline import per_category, run_case, run_suite, summarize CASES = load_cases("public") TAXONOMY = load_taxonomy() BY_ID: Dict[str, object] = {c.attack_id: c for c in CASES} CATEGORIES = sorted({c.category_id for c in CASES}) STACKS: List[tuple] = [ ("baseline (no defense)", []), ("sanitizer", ["sanitizer"]), ("rag_guard", ["rag_guard"]), ("policy_guard", ["policy_guard"]), ("full stack (all three)", ["sanitizer", "rag_guard", "policy_guard"]), ] SEVERITY_COLOR = {"critical": "#dc2626", "high": "#ea580c", "medium": "#ca8a04"} # --------------------------------------------------------------------------- # Rendering helpers # --------------------------------------------------------------------------- def _esc(text: str) -> str: return html_lib.escape(str(text)) def _badge(result: dict) -> str: passed = result["verdict"] == "PASS" color = "#16a34a" if passed else "#dc2626" items = "".join(f"
Select at least one category.
", "", "" runs = {label: run_suite(selected, plugins) for label, plugins in STACKS} stats = {label: summarize(res) for label, res in runs.items()} base_asr = stats["baseline (no defense)"]["asr"] or 1.0 leaderboard = [] for label, _ in STACKS: s = stats[label] eff = "—" if label.startswith("baseline") else f"{max(0.0, 1 - s['asr'] / base_asr):.1%}" leaderboard.append( [ label, s["n"], f"{s['asr']:.1%}", eff, f"{s['mean_hazard']:.3f}", f"{s['overblock']:.1%}", ] ) chart = _bars([(label, stats[label]["asr"]) for label, _ in STACKS]) labels = [label for label, _ in STACKS] per_cat_asr: Dict[str, Dict[str, float]] = {} for label, res in runs.items(): for cat, sub in per_category(res).items(): per_cat_asr.setdefault(cat, {})[label] = sub["asr"] matrix = [ [cat] + [f"{per_cat_asr.get(cat, {}).get(l, 0.0):.0%}" for l in labels] for cat in sorted(per_cat_asr) ] board_html = _table( ["defense", "n", "ASR", "effectiveness", "hazard", "over-block"], leaderboard ) matrix_html = _table(["category"] + labels, matrix) return board_html, chart, matrix_html # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- CHOICES = [ (f"{c.attack_id} · {c.category_name}", c.attack_id) for c in sorted(CASES, key=lambda c: (c.category_id, c.attack_id)) ] with gr.Blocks(title="RecRedTeam AuditLab") as demo: gr.Markdown( """ # 🛡️ RecRedTeam AuditLab **Red-team audit for LLM / agentic recommendation and shopping assistants.** Pick an attack from a 9-category taxonomy mapped to **OWASP ASI 2026**, run it against a deterministic shopping agent, and watch the verdict change as you switch defenses on. No API key, no network call — everything is computed locally and reproducibly. **Metrics** — `ASR` attack success rate · `hazard` mean graded harm (0–1) · `effectiveness` = `1 − ASR(defended) / ASR(baseline)` · `over-block` share of cases where the defense filtered the entire catalog away. """ ) with gr.Tabs(): with gr.Tab("Threat taxonomy"): gr.Markdown(taxonomy_view()) with gr.Tab("Single attack"): with gr.Row(): attack_dd = gr.Dropdown( choices=CHOICES, value=CHOICES[0][1], label="Attack case", scale=3 ) defense_cg = gr.CheckboxGroup( choices=list(DEFENSES), value=[], label="Defense plugins", scale=2 ) case_md = gr.Markdown(inspect(CHOICES[0][1])) run_btn = gr.Button("Run audit", variant="primary") with gr.Row(): with gr.Column(): gr.Markdown("#### Baseline agent (no defense)") base_out = gr.Textbox(lines=14, label="Agent answer") base_verdict = gr.HTML() with gr.Column(): gr.Markdown("#### Defended agent") def_out = gr.Textbox(lines=14, label="Agent answer") def_verdict = gr.HTML() attack_dd.change(inspect, inputs=[attack_dd], outputs=[case_md]) run_btn.click( audit, inputs=[attack_dd, defense_cg], outputs=[base_out, base_verdict, def_out, def_verdict], ) with gr.Tab("Benchmark / leaderboard"): with gr.Row(): n_slider = gr.Slider( minimum=2, maximum=26, value=10, step=1, label="Cases per category", scale=1, ) cat_cg = gr.CheckboxGroup( choices=CATEGORIES, value=CATEGORIES, label="Threat categories", scale=3, ) bench_btn = gr.Button("Run benchmark", variant="primary") board = gr.HTML(label="Leaderboard") chart_html = gr.HTML(label="ASR chart") matrix_tbl = gr.HTML(label="Per-category ASR") bench_btn.click( benchmark, inputs=[n_slider, cat_cg], outputs=[board, chart_html, matrix_tbl], ) with gr.Tab("About"): gr.Markdown( f""" ### How an audit runs 1. **Poison** — hidden instructions from the case are planted into the product catalog. 2. **Defend** — selected plugins act at retrieval (`rag_guard`), content (`sanitizer`) and output (`policy_guard`) level. 3. **Answer** — a deterministic mock shopping agent ranks the catalog and paraphrases what it retrieved. Its only flaw is the realistic one: *it trusts retrieved content*. 4. **Judge** — a rule-based dual-channel judge looks for PII disclosure, untrusted destinations, executed instructions and category failure signals. 5. **Grade** — verdict (`PASS`/`FAIL`) plus a hazard grade from the category severity. ### Reading the leaderboard Each plugin has a deliberately different coverage profile, which is the point of the framework: - `sanitizer` kills injected instructions and unverifiable claims — but is blind to ranking manipulation and metadata exposure. - `rag_guard` removes poisoned listings at retrieval — powerful, but it over-blocks, and it cannot see a fabricated spec that looks exactly like a real one. - `policy_guard` only rewrites the answer — it stops PII leaks and phishing redirects and nothing else. **Product hallucination is the category no rule-based defense solves.** Fabricated but plausible specs ("30-day battery life", "#1 best seller", "50,000 units sold") are lexically indistinguishable from real ones; closing that gap needs grounding and external verification, which is the open research direction. ### Limitations The agent under test is a **deterministic mock**, not a real LLM. Absolute ASR values therefore describe this mock; what transfers to real agents is the *relative* coverage profile of the defenses and the taxonomy itself. The full framework adds an optional LLM judge channel and LangGraph / OpenAI-Agents adapters. ### Links - Dataset — [NoahIsARider/RecRedTeam-ASI-Bench](https://huggingface.co/datasets/NoahIsARider/RecRedTeam-ASI-Bench) - Framework — [github.com/NoahIsARider/RecRedTeam](https://github.com/NoahIsARider/RecRedTeam) - Author — [noahisarider.github.io](https://noahisarider.github.io/) Defensive research only. Attack cases run against a local simulated catalog, never against real systems. """ ) demo.launch()