A-HK's picture
Upload folder using huggingface_hub
e43736b verified
Raw
History Blame Contribute Delete
67.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Agentic Security Lab</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
/* ══════════════════════════════════════════════
DESIGN TOKENS — warm orange / shadcn style
══════════════════════════════════════════════ */
:root {
--bg: #faf6f0;
--card: #ffffff;
--bd: #e8e1d9;
--bd2: #d6cfc6;
--muted: #f5f0e8;
--p: #c2600a;
--p-dk: #9a4c08;
--p-lt: #fef3e8;
--p-fg: #ffffff;
--red: #1c1917; --red-lt: #f5f5f4; --red-bd: #d4d4d4;
--grn: #16a34a; --grn-lt: #f0fdf4; --grn-bd: #bbf7d0;
--amb: #d97706; --amb-lt: #fffbeb; --amb-bd: #fde68a;
--blue: #2563eb;
--fg: #1c1917;
--fg2: #57534e;
--fg3: #a8a29e;
--font: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', sans-serif;
--mono: 'JetBrains Mono','Fira Code','Cascadia Code','Courier New',monospace;
--r: 8px;
--r-sm: 5px;
--r-lg: 12px;
--sh: 0 1px 2px rgba(0,0,0,.05), 0 0 0 1px rgba(0,0,0,.03);
}
*,*::before,*::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body { background: var(--bg); color: var(--fg); font-family: var(--font); font-size: 13px; }
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--bd2); border-radius: 99px; }
button { font-family: inherit; cursor: pointer; }
select { font-family: inherit; }
.hidden { display: none !important; }
/* ══════ PAGE WRAPPER ══════ */
#app { max-width: 1320px; margin: 0 auto; padding: 22px 28px 40px; display: flex; flex-direction: column; gap: 16px; }
/* ══════ HEADER ══════ */
.hdr { display: flex; align-items: center; gap: 16px; padding-bottom: 18px; border-bottom: 1px solid var(--bd); flex-wrap: wrap; }
.hdr-logo { display: flex; align-items: center; gap: 11px; }
.logo-ico { width: 36px; height: 36px; background: var(--p); border-radius: var(--r-sm); display: flex; align-items: center; justify-content: center; font-size: 17px; flex-shrink: 0; }
.logo-text { font-size: 15px; font-weight: 700; color: var(--fg); }
.logo-sub { font-size: 10.5px; color: var(--fg3); margin-top: 2px; }
.hdr-sep { width: 1px; height: 30px; background: var(--bd); flex-shrink: 0; }
/* Scenario tabs */
.sc-tabs { display: flex; gap: 5px; }
.sc-tab { padding: 7px 15px; border-radius: var(--r-sm); font-size: 12.5px; font-weight: 500;
border: 1px solid var(--bd); background: var(--card); color: var(--fg2); cursor: pointer; transition: .13s; }
.sc-tab:hover:not(.on) { background: var(--muted); border-color: var(--bd2); color: var(--fg); }
.sc-tab.on { background: var(--p); border-color: var(--p); color: var(--p-fg); font-weight: 600; }
/* Status badge */
.ep-badge { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px;
border-radius: 99px; font-size: 11px; font-weight: 600; border: 1px solid; margin-left: auto; }
.ep-ready { background: var(--grn-lt); color: var(--grn); border-color: var(--grn-bd); }
.ep-active { background: var(--amb-lt); color: var(--amb); border-color: var(--amb-bd); }
.ep-done { background: var(--p-lt); color: var(--p-dk); border-color: #fcd9b4; }
.ep-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.ep-active .ep-dot { animation: blink 1.2s infinite; }
/* Scenario meta chips */
.sc-meta { display: flex; gap: 9px; }
.sc-chip { display: flex; flex-direction: column; align-items: center; padding: 6px 12px;
background: var(--muted); border-radius: var(--r-sm); border: 1px solid var(--bd); min-width: 56px; }
.sc-chip-v { font-size: 15px; font-weight: 700; color: var(--p); font-family: var(--mono); line-height: 1; }
.sc-chip-l { font-size: 9px; color: var(--fg3); text-transform: uppercase; letter-spacing: .05em; margin-top: 3px; }
/* ══════ INCIDENT BANNER ══════ */
.banner { display: flex; align-items: flex-start; gap: 13px; padding: 14px 18px;
background: var(--amb-lt); border: 1px solid var(--amb-bd); border-radius: var(--r); border-left: 4px solid var(--amb); }
.banner.hidden { display: none; }
.ban-ico { font-size: 18px; margin-top: 1px; flex-shrink: 0; }
.ban-title { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--p-dk); margin-bottom: 4px; }
.ban-desc { font-size: 11.5px; color: #78350f; line-height: 1.55; }
.ban-meta { display: flex; gap: 16px; margin-top: 7px; }
.ban-kv .ban-k { font-size: 9px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--p); }
.ban-kv .ban-v { font-size: 12px; font-weight: 700; font-family: var(--mono); color: #7c2d12; }
/* ══════ CONTROLS ══════ */
.ctrl-row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 15px; border-radius: var(--r-sm);
font-size: 12.5px; font-weight: 600; border: 1px solid transparent; transition: .13s; white-space: nowrap; }
.btn:disabled { opacity: .38; cursor: not-allowed; }
.btn-p { background: var(--p); color: var(--p-fg); border-color: var(--p); }
.btn-p:hover:not(:disabled) { background: var(--p-dk); border-color: var(--p-dk); }
.btn-o { background: var(--card); color: var(--p-dk); border-color: var(--p); }
.btn-o:hover:not(:disabled) { background: var(--p-lt); }
.btn-s { background: var(--card); color: var(--fg); border-color: var(--bd); }
.btn-s:hover:not(:disabled) { background: var(--muted); border-color: var(--bd2); }
.btn-d { background: var(--card); color: var(--fg2); border-color: var(--bd); }
.btn-d:hover:not(:disabled) { background: var(--muted); border-color: var(--bd2); }
.rwd-pill { display: flex; align-items: baseline; gap: 5px; margin-left: auto;
padding: 6px 14px; background: var(--muted); border: 1px solid var(--bd); border-radius: 99px; }
.rwd-v { font-size: 15px; font-weight: 700; font-family: var(--mono); color: var(--p); }
.rwd-l { font-size: 10px; color: var(--fg3); }
/* ══════ MAIN GRID ══════ */
.grid { display: grid; grid-template-columns: 268px 1fr 244px; gap: 16px; align-items: start; }
/* ══════ CARD ══════ */
.card { background: var(--card); border: 1px solid var(--bd); border-radius: var(--r); box-shadow: var(--sh); overflow: hidden; }
.card + .card { margin-top: 12px; }
.ch { display: flex; align-items: center; justify-content: space-between; padding: 12px 15px 11px; border-bottom: 1px solid var(--bd); }
.ct { font-size: 12px; font-weight: 600; color: var(--fg); }
.cs { font-size: 10.5px; color: var(--fg3); }
.cb { padding: 13px 15px; }
/* ══════ LEFT COLUMN ══════ */
.left-col { display: flex; flex-direction: column; gap: 12px; }
.graph-bg { background: var(--muted); border-radius: var(--r-sm); overflow: hidden; margin: 6px 8px 8px; }
#pkg-graph { width: 100%; display: block; }
.pkg-list, .sec-list { display: flex; flex-direction: column; gap: 3px; max-height: 168px; overflow-y: auto; }
.pkg-list.preview .pi-mal,
.sec-list.preview .sb-exp { animation: none; opacity: .55; }
.pkg-list.preview .pname.mal { opacity: .7; }
.pkg-list.preview .pchip.c-mal { opacity: .7; }
.pkg-list.preview .pkg-row { cursor: default; }
.pkg-list.preview .pkg-row:hover { background: transparent; }
.pkg-row { display: flex; align-items: center; gap: 8px; padding: 6px 9px; border-radius: var(--r-sm); cursor: pointer; transition: .1s; }
.pkg-row:hover { background: var(--muted); }
.pip { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.pi-unk { background: var(--fg3); }
.pi-mal { background: var(--red); box-shadow: 0 0 0 2px var(--red-lt); animation: blink 1.4s infinite; }
.pi-clean { background: var(--grn); box-shadow: 0 0 0 2px var(--grn-lt); }
.pi-qua { background: var(--fg3); opacity: .45; }
.pname { font-size: 11px; font-family: var(--mono); flex: 1; color: var(--fg); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pname.mal { color: var(--red); } .pname.clean { color: var(--grn); } .pname.qua { color: var(--fg3); text-decoration: line-through; }
.pchip { font-size: 9.5px; font-weight: 600; padding: 1px 6px; border-radius: 99px; flex-shrink: 0; }
.c-mal { background: var(--red-lt); color: var(--red); }
.c-clean{ background: var(--grn-lt); color: var(--grn); }
.c-qua { background: var(--muted); color: var(--fg3); }
.c-unk { background: var(--muted); color: var(--fg3); }
.sec-row { display: flex; align-items: center; gap: 8px; padding: 6px 9px; border-radius: var(--r-sm); }
.sbadge { font-size: 9.5px; font-weight: 700; padding: 1px 6px; border-radius: 99px; flex-shrink: 0; text-transform: uppercase; letter-spacing: .04em; }
.sb-exp { background: var(--red-lt); color: var(--red); }
.sb-rot { background: var(--grn-lt); color: var(--grn); }
.sname { font-size: 10.5px; font-family: var(--mono); flex: 1; color: var(--fg2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.scrit { font-size: 9px; font-weight: 700; color: var(--red); flex-shrink: 0; }
/* ══════ CENTER COLUMN ══════ */
.mid-col { display: flex; flex-direction: column; gap: 12px; }
.log-bar { display: flex; align-items: center; gap: 6px; padding: 10px 13px; background: #0d1117; border-bottom: 1px solid #21262d; }
.ld { width: 10px; height: 10px; border-radius: 50%; }
.ld1 { background: #ff5f56; } .ld2 { background: #ffbd2e; } .ld3 { background: #27c93f; }
.llbl { font: 10.5px var(--mono); color: #484f58; margin-left: 8px; }
.lspin { margin-left: auto; }
#tlog { background: #0d1117; padding: 12px 15px; min-height: 240px; max-height: 268px; overflow-y: auto;
font: 11.5px/1.75 var(--mono); }
.ll { display: block; animation: fadein .16s ease; }
.ll.sys { color: #4d87c4; } .ll.cmd { color: #7dd3fc; } .ll.cmd::before { content: '$ '; color: #4d87c4; }
.ll.res { color: #86efac; padding-left: 12px; } .ll.ok { color: #4ade80; padding-left: 12px; }
.ll.err { color: #94a3b8; padding-left: 12px; } .ll.rwp { color: #fcd34d; padding-left: 12px; }
.ll.rwn { color: #94a3b8; padding-left: 12px; } .ll.alr { color: #fb923c; } .ll.alr::before { content: '! '; }
.ll.sep { color: #21262d; border-bottom: 1px solid #21262d; display: block; }
.act-grid { display: grid; grid-template-columns: repeat(4,1fr); gap: 7px; }
.abtn { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 11px 6px;
background: var(--card); border: 1px solid var(--bd); border-radius: var(--r-sm); cursor: pointer; transition: .13s; }
.abtn:hover:not(:disabled) { border-color: var(--p); background: var(--p-lt); }
.abtn:disabled { opacity: .35; cursor: not-allowed; }
.abtn .an { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--fg2); text-align: center; line-height: 1.2; }
.abtn .ar { font-size: 9px; font-family: var(--mono); color: var(--grn); font-weight: 600; }
.abtn .ar.neg { color: var(--red); }
.fp-cell { display: flex; flex-direction: column; align-items: center; justify-content: center;
padding: 8px 6px; gap: 3px; border: 1px dashed var(--bd2); border-radius: var(--r-sm); background: var(--muted); }
.fp-cell .fp-l { font-size: 9px; font-weight: 600; color: var(--fg3); text-transform: uppercase; letter-spacing: .04em; }
.fp-cell .fp-v { font-size: 11px; font-weight: 700; font-family: var(--mono); color: var(--fg2); }
.prow { display: flex; gap: 8px; align-items: center; margin-top: 10px; flex-wrap: wrap; }
.prow.hidden { display: none; }
.psel { flex: 1; min-width: 140px; padding: 7px 11px; border: 1px solid var(--bd); border-radius: var(--r-sm);
background: var(--card); color: var(--fg); font-size: 12px; font-family: var(--mono); }
.psel:focus { outline: none; border-color: var(--p); box-shadow: 0 0 0 3px rgba(194,96,10,.12); }
/* ══════ RIGHT COLUMN ══════ */
.right-col { display: flex; flex-direction: column; gap: 12px; }
/* Incident timeline (replaces the penetration ring) */
.tl-cb { padding: 16px 16px 14px; }
.tl-wrap { position: relative; padding: 20px 4px 8px; }
.tl-track { height: 9px; display: flex; border-radius: 99px; overflow: hidden; border: 1px solid var(--bd); }
.tl-zone { height: 100%; transition: flex .4s ease; }
.tl-safe { background: linear-gradient(90deg, #ecfdf5 0%, #fef3e8 100%); }
.tl-danger { background: linear-gradient(90deg, #fef3e8 0%, #fee2e2 100%); }
.tl-mk-exfil { position: absolute; top: 11px; width: 2px; height: 25px;
background: var(--amb); border-radius: 1px;
transform: translateX(-50%); transition: left .4s ease; }
.tl-mk-exfil::before { content: '⚠'; position: absolute; top: -14px; left: 50%;
transform: translateX(-50%); font-size: 11px; color: var(--amb); }
.tl-now { position: absolute; top: 14px; width: 11px; height: 11px;
background: var(--bd2); border-radius: 50%;
border: 2px solid var(--card);
box-shadow: 0 0 0 1.5px var(--bd2), 0 1px 3px rgba(0,0,0,.10);
transform: translateX(-50%);
transition: left .5s ease, background .25s, box-shadow .25s;
z-index: 2; }
.tl-now::after { content: ''; position: absolute; inset: -7px; border-radius: 50%;
background: transparent; pointer-events: none; }
.tl-now-active { background: var(--p); box-shadow: 0 0 0 1.5px var(--p), 0 1px 3px rgba(0,0,0,.18); }
.tl-now-active::after { background: rgba(194,96,10,.22); animation: tlpulse 1.5s infinite ease-out; }
.tl-now-breach { background: #dc2626; box-shadow: 0 0 0 1.5px #dc2626, 0 1px 3px rgba(0,0,0,.18); }
.tl-now-breach::after { background: rgba(220,38,38,.25); animation: tlpulse .9s infinite ease-out; }
.tl-now-done { background: var(--grn); box-shadow: 0 0 0 1.5px var(--grn), 0 1px 3px rgba(0,0,0,.18); }
.tl-labels { display: flex; justify-content: space-between; padding: 4px 0 0;
font-size: 9px; font-weight: 600; color: var(--fg3);
text-transform: uppercase; letter-spacing: .05em; font-family: var(--mono); }
.tl-labels .tl-l-exfil { color: var(--amb); }
.tl-status { display: flex; align-items: center; justify-content: center; gap: 7px;
padding: 8px 12px; border-radius: 99px; margin: 14px 0 13px;
font-size: 11px; font-weight: 600;
background: var(--muted); border: 1px solid var(--bd); color: var(--fg3);
transition: .25s; }
.tl-s-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex-shrink: 0; }
.tl-status.tl-s-active { background: var(--amb-lt); border-color: var(--amb-bd); color: var(--amb); }
.tl-status.tl-s-active .tl-s-dot { animation: blink 1.2s infinite; }
.tl-status.tl-s-secured { background: var(--p-lt); border-color: #fcd9b4; color: var(--p-dk); }
.tl-status.tl-s-breach { background: #fee2e2; border-color: #fca5a5; color: #991b1b; }
.tl-status.tl-s-breach .tl-s-dot { animation: blink .9s infinite; }
.tl-status.tl-s-done { background: var(--grn-lt); border-color: var(--grn-bd); color: var(--grn); }
.tl-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 7px; }
.tl-stat { text-align: center; background: var(--muted); border: 1px solid var(--bd);
border-radius: var(--r-sm); padding: 10px 5px; transition: border-color .2s; }
.tl-stat:hover { border-color: var(--bd2); }
.tl-v { font-size: 17px; font-weight: 700; font-family: var(--mono); color: var(--fg); line-height: 1; transition: color .25s; }
.tl-v.warn { color: var(--amb); }
.tl-v.danger { color: #dc2626; }
.tl-l { font-size: 8.5px; color: var(--fg3); text-transform: uppercase; letter-spacing: .04em; margin-top: 6px; font-weight: 600; }
.si-list { display: flex; flex-direction: column; gap: 11px; }
.si { display: flex; flex-direction: column; gap: 4px; }
.si-row { display: flex; align-items: center; justify-content: space-between; }
.si-lbl { font-size: 11.5px; color: var(--fg2); }
.si-right { display: flex; align-items: center; gap: 5px; }
.si-wt { font-size: 9.5px; color: var(--fg3); }
.si-pct { font-size: 11.5px; font-weight: 700; font-family: var(--mono); }
.si-bar { height: 6px; background: var(--muted); border-radius: 99px; overflow: hidden; border: 1px solid var(--bd); }
.si-fill { height: 100%; border-radius: 99px; transition: width .5s ease; }
.sq .si-fill { background: var(--red); } .sq .si-pct { color: var(--red); }
.sr .si-fill { background: var(--amb); } .sr .si-pct { color: var(--amb); }
.sn .si-fill { background: var(--blue); } .sn .si-pct { color: var(--blue); }
.sc2 .si-fill { background: var(--grn); } .sc2 .si-pct { color: var(--grn); }
.tot-row { display: flex; align-items: center; justify-content: space-between;
padding: 10px 16px; background: var(--muted); border-top: 1px solid var(--bd); }
.tot-l { font-size: 11px; font-weight: 600; color: var(--fg2); }
.tot-v { font-size: 18px; font-weight: 800; font-family: var(--mono); color: var(--p-dk); }
.chart-cb { padding: 13px 15px 14px; }
.chart-l { display: flex; align-items: center; justify-content: space-between;
font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em;
color: var(--fg3); margin-bottom: 8px; }
.chart-l .chart-v { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--p); letter-spacing: 0; text-transform: none; }
#rchart { max-height: 104px; }
.phase-list { display: flex; flex-direction: column; gap: 2px; }
.ph { display: flex; align-items: center; gap: 10px; padding: 8px 9px; border-radius: var(--r-sm); transition: .12s; }
.ph:hover { background: var(--muted); }
.ph-ring { width: 22px; height: 22px; border-radius: 50%; border: 2px solid var(--bd2); flex-shrink: 0;
display: flex; align-items: center; justify-content: center; font-size: 9.5px; font-weight: 700;
color: var(--fg3); background: var(--card); transition: .2s; }
.ph-ring.done { border-color: var(--grn); background: var(--grn-lt); color: var(--grn); }
.ph-lbl { font-size: 11.5px; color: var(--fg2); flex: 1; }
.ph-lbl.done { color: var(--fg); font-weight: 500; }
/* ══════ TRAINING RESULTS ══════ */
.res-section { border-top: 1px solid var(--bd); padding-top: 20px; margin-top: 8px; }
.res-head { margin-bottom: 14px; }
.res-title { font-size: 14px; font-weight: 600; }
.res-sub { font-size: 11px; color: var(--fg3); margin-top: 3px; }
.res-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
.ba-row { display: grid; grid-template-columns: 72px 1fr 1fr; gap: 10px; align-items: center; margin-bottom: 9px; }
.ba-lbl { font-size: 11.5px; font-weight: 500; color: var(--fg2); }
.ba-bg { height: 22px; background: var(--muted); border-radius: var(--r-sm); overflow: hidden; border: 1px solid var(--bd); }
.ba-fill { height: 100%; border-radius: var(--r-sm); display: flex; align-items: center; padding: 0 7px; }
.ba-bef { background: #e8e1d9; }
.ba-aft-e { background: rgba(22,163,74,.2); }
.ba-aft-m { background: rgba(217,119,6,.2); }
.ba-aft-h { background: rgba(194,96,10,.25); }
.ba-val { font-size: 10.5px; font-weight: 700; color: var(--fg); }
.formula { background: var(--muted); border: 1px solid var(--bd); border-radius: var(--r-sm);
padding: 12px 14px; font: 11.5px/2.0 var(--mono); color: var(--fg2); }
.fh { color: var(--p-dk); font-weight: 700; }
.pill-list { display: flex; flex-direction: column; gap: 6px; margin-top: 11px; }
.pill { display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: var(--fg2); }
.pdot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
/* ══════ SPINNER ══════ */
.spinner { width: 12px; height: 12px; border: 2px solid var(--bd); border-top-color: var(--p);
border-radius: 50%; animation: spin .6s linear infinite; display: inline-block; }
/* ══════ KEYFRAMES ══════ */
@keyframes blink { 0%,100%{opacity:1}50%{opacity:.2} }
@keyframes fadein { from{opacity:0;transform:translateX(-4px)}to{opacity:1;transform:none} }
@keyframes spin { to{transform:rotate(360deg)} }
@keyframes tlpulse { 0%{transform:scale(.7);opacity:.85} 100%{transform:scale(1.7);opacity:0} }
</style>
</head>
<body>
<div id="app">
<!-- ══════ HEADER ══════ -->
<header class="hdr">
<div class="hdr-logo">
<div class="logo-ico" style="font-size:11px;font-weight:800;letter-spacing:-.5px;color:#fff">SL</div>
<div>
<div class="logo-text">Agentic Security Lab</div>
<div class="logo-sub">OpenEnv · Qwen2.5-3B · GRPO</div>
</div>
</div>
<div class="hdr-sep"></div>
<div class="sc-tabs">
<button class="sc-tab on" id="tab-easy" onclick="selectSc('easy')">Easy</button>
<button class="sc-tab" id="tab-medium" onclick="selectSc('medium')">Medium</button>
<button class="sc-tab" id="tab-hard" onclick="selectSc('hard')">Hard</button>
</div>
<div class="sc-meta" id="sc-meta">
<div class="sc-chip"><div class="sc-chip-v" id="m-pkgs"></div><div class="sc-chip-l">Packages</div></div>
<div class="sc-chip"><div class="sc-chip-v" id="m-secs"></div><div class="sc-chip-l">Secrets</div></div>
<div class="sc-chip"><div class="sc-chip-v" id="m-teams"></div><div class="sc-chip-l">Teams</div></div>
<div class="sc-chip"><div class="sc-chip-v" id="m-exfil"></div><div class="sc-chip-l">Exfil Step</div></div>
</div>
<div class="ep-badge ep-ready" id="ep-badge"><div class="ep-dot"></div><span id="ep-txt">Ready</span></div>
</header>
<!-- ══════ INCIDENT BANNER ══════ -->
<div class="banner hidden" id="banner">
<div style="flex:1">
<div class="ban-title">Active Security Incident</div>
<div class="ban-desc" id="ban-desc">Loading…</div>
<div class="ban-meta">
<div class="ban-kv"><div class="ban-k">Exfil Window</div><div class="ban-v" id="ban-ef"></div></div>
<div class="ban-kv"><div class="ban-k">Max Steps</div><div class="ban-v" id="ban-mx"></div></div>
<div class="ban-kv"><div class="ban-k">Mode</div><div class="ban-v">benchmark</div></div>
</div>
</div>
</div>
<!-- ══════ CONTROLS ══════ -->
<div class="ctrl-row">
<button class="btn btn-p" onclick="startEp()" id="btn-start">▶ Start Episode</button>
<button class="btn btn-o" onclick="runDemo()" id="btn-demo" disabled>Run Demo</button>
<button class="btn btn-s" onclick="stepDemo()" id="btn-step" disabled>→ Step</button>
<button class="btn btn-d" onclick="resetEp()" id="btn-rst" disabled>↺ Reset</button>
<div class="rwd-pill"><span class="rwd-v" id="rwd-v">0.000</span><span class="rwd-l">reward</span></div>
</div>
<!-- ══════ MAIN GRID ══════ -->
<div class="grid">
<!-- LEFT -->
<div class="left-col">
<div class="card">
<div class="ch"><span class="ct">Package Dependencies</span><span class="cs" id="graph-cs">0 nodes · 0 risks</span></div>
<div class="graph-bg">
<svg id="pkg-graph" viewBox="0 0 248 168" style="height:168px">
<text x="124" y="88" text-anchor="middle" fill="#a8a29e" font-size="11" font-family="system-ui">Select a scenario</text>
</svg>
</div>
</div>
<div class="card">
<div class="ch"><span class="ct">Packages in Scope</span><span class="cs" id="pkg-cnt">0 packages</span></div>
<div class="cb">
<div class="pkg-list" id="pkg-list">
<div style="color:var(--fg3);font-size:11px;text-align:center;padding:12px">No episode active</div>
</div>
</div>
</div>
<div class="card">
<div class="ch"><span class="ct">Secrets at Risk</span></div>
<div class="cb">
<div class="sec-list" id="sec-list">
<div style="color:var(--fg3);font-size:11px;text-align:center;padding:10px">No episode active</div>
</div>
</div>
</div>
</div><!-- /left -->
<!-- CENTER -->
<div class="mid-col">
<div class="card" style="overflow:hidden">
<div class="log-bar">
<div class="ld ld1"></div><div class="ld ld2"></div><div class="ld ld3"></div>
<span class="llbl">agent@security-lab ~ incident-response</span>
<span class="lspin hidden" id="lspin"><span class="spinner"></span></span>
</div>
<div id="tlog">
<span class="ll sys">Agentic Security Lab — Supply Chain Incident Response</span>
<span class="ll sys">Model: Qwen2.5-3B-Instruct + GRPO · TRL + Unsloth 4-bit QLoRA</span>
<span class="ll sep"> </span>
<span class="ll" style="color:#484f58">Select a scenario and click ▶ Start Episode to begin.</span>
</div>
</div>
<div class="card">
<div class="ch"><span class="ct">Agent Actions</span><span class="cs" id="act-cs">Demo Mode</span></div>
<div class="cb">
<div class="act-grid">
<button class="abtn" onclick="qAct('scan_logs')" id="ab-scan" disabled title="Search CI/CD logs (+0.02)"><span class="an">Scan Logs</span><span class="ar">+0.02</span></button>
<button class="abtn" onclick="qAct('inspect_package')" id="ab-insp" disabled title="Check publisher and IOCs (+0.01)"><span class="an">Inspect Pkg</span><span class="ar">+0.01</span></button>
<button class="abtn" onclick="qAct('check_dependents')" id="ab-deps" disabled title="Trace downstream services (+0.01)"><span class="an">Check Deps</span><span class="ar">+0.01</span></button>
<button class="abtn" onclick="qAct('quarantine')" id="ab-qua" disabled title="Yank malicious package (+0.15)"><span class="an">Quarantine</span><span class="ar">+0.15</span></button>
<button class="abtn" onclick="qAct('rotate_secret')" id="ab-rot" disabled title="Rotate credential (+0.12)"><span class="an">Rotate Secret</span><span class="ar">+0.12</span></button>
<button class="abtn" onclick="qAct('notify')" id="ab-not" disabled title="Alert affected team (+0.04)"><span class="an">Notify Team</span><span class="ar">+0.04</span></button>
<button class="abtn" onclick="qAct('conclude')" id="ab-con" disabled title="End and score episode"><span class="an">Conclude</span><span class="ar">+bonus</span></button>
<div class="fp-cell" title="False-positive penalty when quarantining a clean package">
<span class="fp-l">FP Penalty</span>
<span class="fp-v">−0.05</span>
</div>
</div>
<div class="prow hidden" id="prow">
<select class="psel" id="psel"><option value="">Select target…</option></select>
<button class="btn btn-p" onclick="execAct()" style="padding:6px 12px;font-size:12px">Execute</button>
<button class="btn btn-s" onclick="cancelAct()" style="padding:6px 9px;font-size:12px"></button>
</div>
</div>
</div>
</div><!-- /center -->
<!-- RIGHT -->
<div class="right-col">
<!-- Incident timeline -->
<div class="card">
<div class="ch"><span class="ct">Incident Timeline</span><span class="cs" id="att-step">Step 0 / 0</span></div>
<div class="cb tl-cb">
<div class="tl-wrap">
<div class="tl-track">
<div class="tl-zone tl-safe" id="tl-safe"></div>
<div class="tl-zone tl-danger" id="tl-danger"></div>
</div>
<div class="tl-mk-exfil" id="tl-mk-exfil" style="left:0%"></div>
<div class="tl-now" id="tl-now" style="left:0%"></div>
</div>
<div class="tl-labels">
<span>Step 0</span>
<span class="tl-l-exfil" id="tl-l-exfil">Exfil 0</span>
<span id="tl-l-max">Max 0</span>
</div>
<div class="tl-status" id="tl-status">
<span class="tl-s-dot"></span>
<span id="tl-s-txt">Awaiting episode</span>
</div>
<div class="tl-stats">
<div class="tl-stat"><div class="tl-v" id="tl-v-step">0</div><div class="tl-l">Step</div></div>
<div class="tl-stat"><div class="tl-v warn" id="tl-v-tte"></div><div class="tl-l">To Exfil</div></div>
<div class="tl-stat"><div class="tl-v" id="tl-v-rem"></div><div class="tl-l">Remaining</div></div>
</div>
</div>
</div>
<!-- Score breakdown -->
<div class="card">
<div class="ch"><span class="ct">Score Breakdown</span></div>
<div class="cb">
<div class="si-list">
<div class="si sq"><div class="si-row"><span class="si-lbl">Quarantine</span><div class="si-right"><span class="si-wt">×0.35</span><span class="si-pct" id="sq">0%</span></div></div><div class="si-bar"><div class="si-fill" id="bq" style="width:0%"></div></div></div>
<div class="si sr"><div class="si-row"><span class="si-lbl">Rotate</span><div class="si-right"><span class="si-wt">×0.35</span><span class="si-pct" id="sr">0%</span></div></div><div class="si-bar"><div class="si-fill" id="br" style="width:0%"></div></div></div>
<div class="si sn"><div class="si-row"><span class="si-lbl">Notify</span><div class="si-right"><span class="si-wt">×0.20</span><span class="si-pct" id="sn">0%</span></div></div><div class="si-bar"><div class="si-fill" id="bn" style="width:0%"></div></div></div>
<div class="si sc2"><div class="si-row"><span class="si-lbl">Contain</span><div class="si-right"><span class="si-wt">×0.10</span><span class="si-pct" id="sc2">0%</span></div></div><div class="si-bar"><div class="si-fill" id="bc" style="width:0%"></div></div></div>
</div>
</div>
<div class="tot-row"><span class="tot-l">Total Score</span><span class="tot-v" id="tot-v">0.0000</span></div>
</div>
<!-- Reward chart -->
<div class="card">
<div class="chart-cb">
<div class="chart-l"><span>Cumulative Reward</span><span class="chart-v" id="chart-v">0.000</span></div>
<canvas id="rchart"></canvas>
</div>
</div>
<!-- Mission phases -->
<div class="card">
<div class="ch"><span class="ct">Response Phases</span></div>
<div class="cb" style="padding:8px 10px">
<div class="phase-list">
<div class="ph" id="ph-investigate"> <div class="ph-ring" id="phr-investigate">1</div> <span class="ph-lbl">Reconnaissance</span></div>
<div class="ph" id="ph-trace_root_cause"><div class="ph-ring" id="phr-trace_root_cause">2</div><span class="ph-lbl">Root Cause Analysis</span></div>
<div class="ph" id="ph-contain"> <div class="ph-ring" id="phr-contain">3</div> <span class="ph-lbl">Containment</span></div>
<div class="ph" id="ph-recover"> <div class="ph-ring" id="phr-recover">4</div> <span class="ph-lbl">Secret Rotation</span></div>
<div class="ph" id="ph-notify"> <div class="ph-ring" id="phr-notify">5</div> <span class="ph-lbl">Team Notification</span></div>
<div class="ph" id="ph-conclude"> <div class="ph-ring" id="phr-conclude">6</div> <span class="ph-lbl">Incident Closure</span></div>
</div>
</div>
</div>
</div><!-- /right -->
</div><!-- /grid -->
<!-- ══════ TRAINING RESULTS ══════ -->
<div class="res-section">
<div class="res-head">
<div class="res-title">Training Results — Qwen2.5-3B Before &amp; After GRPO</div>
<div class="res-sub">Curriculum: easy → medium → hard · LoRA r=32 · group_size=4 · T4 GPU · scale_rewards=False</div>
</div>
<div class="res-grid">
<div class="card">
<div class="ch"><span class="ct">Model Performance Comparison</span></div>
<div class="cb">
<div style="display:grid;grid-template-columns:68px 1fr 1fr;gap:8px;margin-bottom:7px">
<div></div>
<div style="font-size:10px;font-weight:600;color:var(--fg3);text-align:center;text-transform:uppercase;letter-spacing:.05em">Baseline</div>
<div style="font-size:10px;font-weight:600;color:var(--grn);text-align:center;text-transform:uppercase;letter-spacing:.05em">Trained (GRPO)</div>
</div>
<div class="ba-row"><span class="ba-lbl">Easy</span>
<div class="ba-bg"><div class="ba-fill ba-bef" style="width:32%"><span class="ba-val">0.32</span></div></div>
<div class="ba-bg"><div class="ba-fill ba-aft-e" style="width:87%"><span class="ba-val">0.87</span></div></div>
</div>
<div class="ba-row"><span class="ba-lbl">Medium</span>
<div class="ba-bg"><div class="ba-fill ba-bef" style="width:14%"><span class="ba-val">0.14</span></div></div>
<div class="ba-bg"><div class="ba-fill ba-aft-m" style="width:63%"><span class="ba-val">0.63</span></div></div>
</div>
<div class="ba-row"><span class="ba-lbl">Hard</span>
<div class="ba-bg"><div class="ba-fill ba-bef" style="width:5%"><span class="ba-val">0.05</span></div></div>
<div class="ba-bg"><div class="ba-fill ba-aft-h" style="width:38%"><span class="ba-val">0.38</span></div></div>
</div>
</div>
</div>
<div class="card">
<div class="ch"><span class="ct">Reward Formula</span></div>
<div class="cb">
<div class="formula">
<span style="color:var(--p-dk)">score</span> =<br>
&nbsp;&nbsp;<span class="fh">0.35</span> × quarantine_ratio +<br>
&nbsp;&nbsp;<span class="fh">0.35</span> × rotate_ratio +<br>
&nbsp;&nbsp;<span class="fh">0.20</span> × notify_ratio +<br>
&nbsp;&nbsp;<span class="fh">0.10</span> × contain_ratio
</div>
<div class="pill-list">
<div class="pill"><div class="pdot" style="background:var(--fg2)"></div>Attacker breach: <b style="font-family:var(--mono);color:var(--fg);margin-left:3px">−0.20</b> + contain_ratio = 0</div>
<div class="pill"><div class="pdot" style="background:var(--amb)"></div>False positive quarantine: <b style="font-family:var(--mono);color:var(--amb);margin-left:3px">−0.05</b> per clean package</div>
<div class="pill"><div class="pdot" style="background:var(--blue)"></div>StarPO-S: keep top 50% prompts by reward variance</div>
<div class="pill"><div class="pdot" style="background:var(--grn)"></div>Adaptive attacker: tightens deadline as agent improves</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /app -->
<script>
// ══════════════════════════════════════════
// SCENARIO DATA
// ══════════════════════════════════════════
const SD = {
easy: {
pkgs:['axios@1.7.4','axios@1.7.3'], mal:['axios@1.7.4'],
secrets:[{name:'STRIPE_SECRET_KEY',critical:true,owner:'payments-service'},{name:'INTERNAL_API_TOKEN',critical:false,owner:'api-gateway'}],
rq:['axios@1.7.4'], rr:['STRIPE_SECRET_KEY','INTERNAL_API_TOKEN'], rn:['payments-service','auth-service','api-gateway'],
maxSteps:20, exfil:14,
banDesc:'axios@1.7.4 compromised via hijacked maintainer. Exfiltrating process.env to 185.220.101.42. STRIPE_SECRET_KEY and INTERNAL_API_TOKEN at risk.',
graph:{
nodes:[{id:'axios@1.7.4',x:148,y:46,mal:true,t:'pkg'},{id:'axios@1.7.3',x:56,y:46,mal:false,t:'pkg'},{id:'payments',x:58,y:132,mal:false,t:'svc'},{id:'auth-service',x:148,y:140,mal:false,t:'svc'},{id:'api-gateway',x:210,y:132,mal:false,t:'svc'}],
edges:[{f:'axios@1.7.4',t:'payments'},{f:'axios@1.7.4',t:'auth-service'},{f:'axios@1.7.4',t:'api-gateway'}],
},
demo:[
{c:'scan_logs',p:{package:'axios@1.7.4'},d:900},{c:'inspect_package',p:{package:'axios@1.7.4'},d:700},{c:'inspect_package',p:{package:'axios@1.7.3'},d:700},
{c:'check_dependents',p:{package:'axios@1.7.4'},d:700},{c:'quarantine',p:{package:'axios@1.7.4'},d:1000},
{c:'rotate_secret',p:{secret:'STRIPE_SECRET_KEY'},d:700},{c:'rotate_secret',p:{secret:'INTERNAL_API_TOKEN'},d:700},
{c:'notify',p:{team:'payments-service'},d:500},{c:'notify',p:{team:'auth-service'},d:500},{c:'notify',p:{team:'api-gateway'},d:500},
{c:'conclude',p:{},d:1200},
],
deps:{'axios@1.7.4':['payments-service','auth-service','api-gateway']},
},
medium: {
pkgs:['node-fetch@3.3.2','form-data@4.0.1','form-data@4.0.0'], mal:['form-data@4.0.1'],
secrets:[{name:'AWS_ACCESS_KEY_ID',critical:true,owner:'data-pipeline'},{name:'AWS_SECRET_ACCESS_KEY',critical:true,owner:'data-pipeline'},{name:'SENDGRID_API_KEY',critical:true,owner:'email-sender'},{name:'WEBHOOK_SIGNING_SECRET',critical:false,owner:'webhook-processor'},{name:'POSTGRES_PASSWORD',critical:true,owner:'reporting-service'}],
rq:['form-data@4.0.1'],
rr:['AWS_ACCESS_KEY_ID','AWS_SECRET_ACCESS_KEY','SENDGRID_API_KEY','WEBHOOK_SIGNING_SECRET','POSTGRES_PASSWORD'],
rn:['checkout-service','inventory-api','notification-service','reporting-service','search-service','legacy-importer','data-pipeline','webhook-processor','email-sender','file-uploader','crm-sync','analytics-job'],
maxSteps:25, exfil:18,
banDesc:'form-data@4.0.1 silently backdoored. DNS exfil via TXT records to attacker-c2.net. AWS credentials actively leaking from data-pipeline.',
graph:{
nodes:[{id:'node-fetch@3.3.2',x:126,y:34,mal:false,t:'pkg'},{id:'form-data@4.0.1',x:126,y:88,mal:true,t:'pkg'},{id:'form-data@4.0.0',x:50,y:88,mal:false,t:'pkg'},{id:'checkout-svc',x:42,y:148,mal:false,t:'svc'},{id:'data-pipeline',x:100,y:154,mal:false,t:'svc'},{id:'email-sender',x:160,y:148,mal:false,t:'svc'},{id:'+9 more',x:216,y:148,mal:false,t:'svc'}],
edges:[{f:'node-fetch@3.3.2',t:'form-data@4.0.1'},{f:'form-data@4.0.1',t:'checkout-svc'},{f:'form-data@4.0.1',t:'data-pipeline'},{f:'form-data@4.0.1',t:'email-sender'},{f:'form-data@4.0.1',t:'+9 more'},{f:'node-fetch@3.3.2',t:'checkout-svc'}],
},
demo:[
{c:'scan_logs',p:{package:'node-fetch@3.3.2'},d:700},{c:'inspect_package',p:{package:'node-fetch@3.3.2'},d:700},{c:'check_dependents',p:{package:'node-fetch@3.3.2'},d:700},
{c:'inspect_package',p:{package:'form-data@4.0.1'},d:700},{c:'scan_logs',p:{package:'form-data@4.0.1'},d:700},{c:'check_dependents',p:{package:'form-data@4.0.1'},d:700},
{c:'quarantine',p:{package:'form-data@4.0.1'},d:1000},
...['AWS_ACCESS_KEY_ID','AWS_SECRET_ACCESS_KEY','SENDGRID_API_KEY','WEBHOOK_SIGNING_SECRET','POSTGRES_PASSWORD'].map(s=>({c:'rotate_secret',p:{secret:s},d:550})),
...['checkout-service','inventory-api','notification-service','reporting-service','search-service','legacy-importer','data-pipeline','webhook-processor','email-sender','file-uploader','crm-sync','analytics-job'].map(t=>({c:'notify',p:{team:t},d:320})),
{c:'conclude',p:{},d:1200},
],
deps:{
'node-fetch@3.3.2':['checkout-service','inventory-api','notification-service','reporting-service','search-service'],
'form-data@4.0.1':['checkout-service','inventory-api','notification-service','reporting-service','search-service','legacy-importer','data-pipeline','webhook-processor','email-sender','file-uploader','crm-sync','analytics-job'],
'form-data@4.0.0':[],
},
},
hard: {
pkgs:['lodash@4.17.21','express@4.18.2','1odash@4.17.21','expresss@4.18.2','axios-http@1.7.4','node-fetch-lite@3.3.2','dotenvv@16.0.3'],
mal:['1odash@4.17.21','expresss@4.18.2','axios-http@1.7.4','node-fetch-lite@3.3.2','dotenvv@16.0.3'],
secrets:[{name:'NPM_TOKEN',critical:true},{name:'GITHUB_ACTIONS_TOKEN',critical:true},{name:'AWS_ACCESS_KEY_ID',critical:true},{name:'AWS_SECRET_ACCESS_KEY',critical:true},{name:'DATABASE_URL',critical:true},{name:'JWT_SECRET',critical:true},{name:'REDIS_PASSWORD',critical:false},{name:'INTERNAL_WEBHOOK_KEY',critical:false}],
rq:['1odash@4.17.21','expresss@4.18.2','axios-http@1.7.4','node-fetch-lite@3.3.2','dotenvv@16.0.3'],
rr:['NPM_TOKEN','GITHUB_ACTIONS_TOKEN','AWS_ACCESS_KEY_ID','AWS_SECRET_ACCESS_KEY','DATABASE_URL','JWT_SECRET','REDIS_PASSWORD','INTERNAL_WEBHOOK_KEY'],
rn:['user-service','cart-service','admin-panel','reporting-v2','api-v2','graphql-gateway','rest-proxy','auth-v2','session-manager','mobile-bff','web-bff','partner-api','scraper-job','feed-aggregator','rss-service','integration-tests','deploy-scripts','migration-runner','seed-service','config-loader'],
maxSteps:35, exfil:10,
banDesc:'CRITICAL — PhantomRaven: 5 slopsquatted packages. NPM_TOKEN & GITHUB_ACTIONS_TOKEN actively exfiltrating. Agent has 10 steps to rotate critical credentials before the breach locks in — cannot complete every action; must triage.',
graph:{
nodes:[
{id:'lodash@4.17.21', x:44, y:20, mal:false,t:'pkg'},
{id:'express@4.18.2', x:204,y:20, mal:false,t:'pkg'},
{id:'1odash@4.17.21', x:28, y:68, mal:true, t:'pkg'},
{id:'expresss@4.18.2', x:80, y:68, mal:true, t:'pkg'},
{id:'axios-http@1.7.4', x:132,y:68, mal:true, t:'pkg'},
{id:'node-fetch-lite@3.3.2', x:184,y:68, mal:true, t:'pkg'},
{id:'dotenvv@16.0.3', x:228,y:68, mal:true, t:'pkg'},
{id:'user-service +3', x:28, y:140,mal:false,t:'svc'},
{id:'api-v2 +4', x:80, y:140,mal:false,t:'svc'},
{id:'mobile-bff +2', x:132,y:140,mal:false,t:'svc'},
{id:'scraper-job +3', x:184,y:140,mal:false,t:'svc'},
{id:'deploy-scripts +3', x:228,y:140,mal:false,t:'svc'},
],
edges:[
{f:'1odash@4.17.21', t:'user-service +3'},
{f:'expresss@4.18.2', t:'api-v2 +4'},
{f:'axios-http@1.7.4', t:'mobile-bff +2'},
{f:'node-fetch-lite@3.3.2', t:'scraper-job +3'},
{f:'dotenvv@16.0.3', t:'deploy-scripts +3'},
],
},
deps:{
'1odash@4.17.21': ['user-service','cart-service','admin-panel','reporting-v2'],
'expresss@4.18.2': ['api-v2','graphql-gateway','rest-proxy','auth-v2','session-manager'],
'axios-http@1.7.4': ['mobile-bff','web-bff','partner-api'],
'node-fetch-lite@3.3.2':['scraper-job','feed-aggregator','rss-service','integration-tests'],
'dotenvv@16.0.3': ['deploy-scripts','migration-runner','seed-service','config-loader'],
'lodash@4.17.21': [],
'express@4.18.2': [],
},
// 34-step plan that fits inside maxSteps=35 and rotates all 6 critical
// secrets BEFORE the exfil deadline at step 10 — README's triage strategy.
demo:[
// Phase 1 (steps 1-2): quick recon on the most CI/CD-targeting slopsquats
{c:'scan_logs',p:{package:'1odash@4.17.21'},d:560},
{c:'scan_logs',p:{package:'node-fetch-lite@3.3.2'},d:560},
// Phase 2 (steps 3-8): rotate all 6 CRITICAL secrets before exfil deadline
{c:'rotate_secret',p:{secret:'NPM_TOKEN'},d:460},
{c:'rotate_secret',p:{secret:'GITHUB_ACTIONS_TOKEN'},d:460},
{c:'rotate_secret',p:{secret:'AWS_ACCESS_KEY_ID'},d:460},
{c:'rotate_secret',p:{secret:'AWS_SECRET_ACCESS_KEY'},d:460},
{c:'rotate_secret',p:{secret:'DATABASE_URL'},d:460},
{c:'rotate_secret',p:{secret:'JWT_SECRET'},d:460},
// Phase 3 (steps 9-13): quarantine all 5 malicious packages
...['1odash@4.17.21','expresss@4.18.2','axios-http@1.7.4','node-fetch-lite@3.3.2','dotenvv@16.0.3'].map(p=>({c:'quarantine',p:{package:p},d:600})),
// Phase 4 (steps 14-15): clean up the 2 non-critical secrets
{c:'rotate_secret',p:{secret:'REDIS_PASSWORD'},d:460},
{c:'rotate_secret',p:{secret:'INTERNAL_WEBHOOK_KEY'},d:460},
// Phase 5 (steps 16-33): notify 18 of 20 teams — triage; 2 will be missed
...['user-service','cart-service','admin-panel','reporting-v2','api-v2','graphql-gateway','rest-proxy','auth-v2','session-manager','mobile-bff','web-bff','partner-api','scraper-job','feed-aggregator','rss-service','integration-tests','deploy-scripts','migration-runner'].map(t=>({c:'notify',p:{team:t},d:240})),
// Phase 6 (step 34): conclude — seed-service & config-loader intentionally skipped
{c:'conclude',p:{},d:1200},
],
},
};
// ══════════════════════════════════════════
// STATE
// ══════════════════════════════════════════
let S = { sc:'easy', active:false, done:false, step:0, maxSteps:20, exfil:14,
totalReward:0, rewardHist:[], quarantined:[], rotated:[], notified:[],
attProg:0, attBreach:false, demoIdx:0, demoRunning:false, demoTimer:null, planProg:{} };
// ══════════════════════════════════════════
// CHART
// ══════════════════════════════════════════
const rChart = new Chart(document.getElementById('rchart').getContext('2d'), {
type:'line',
data:{ labels:[], datasets:[{ data:[], borderColor:'#c2600a', backgroundColor:'rgba(194,96,10,.1)',
borderWidth:2, pointRadius:2, tension:.35, fill:true }] },
options:{ responsive:true, maintainAspectRatio:true, plugins:{ legend:{ display:false } },
scales:{ x:{ display:false }, y:{ display:true,
ticks:{ color:'#a8a29e', font:{ size:9, family:'monospace' } },
grid:{ color:'rgba(168,162,158,.15)' } } }, animation:{ duration:200 } },
});
// ══════════════════════════════════════════
// SCENARIO SELECT
// ══════════════════════════════════════════
function selectSc(sc) {
S.sc = sc;
['easy','medium','hard'].forEach(s => document.getElementById('tab-'+s)?.classList.toggle('on', s===sc));
const d = SD[sc];
document.getElementById('m-pkgs').textContent = d.pkgs.length;
document.getElementById('m-secs').textContent = d.secrets.length;
document.getElementById('m-teams').textContent = d.rn.length;
document.getElementById('m-exfil').textContent = `Step ${d.exfil}`;
drawGraph(sc);
if(S.active) resetEp();
// Pre-fill scenario preview so users see the incident shape before clicking Start
renderPkgList(d.pkgs, []);
renderSecList(d.secrets, []);
document.getElementById('pkg-list').classList.toggle('preview', !S.active);
document.getElementById('sec-list').classList.toggle('preview', !S.active);
updateMetrics();
updateScores();
}
// ══════════════════════════════════════════
// SVG GRAPH
// ══════════════════════════════════════════
function drawGraph(sc) {
const { nodes, edges } = SD[sc].graph;
const svg = document.getElementById('pkg-graph');
const ns = 'http://www.w3.org/2000/svg';
svg.innerHTML = '';
const defs = document.createElementNS(ns,'defs');
const mk = document.createElementNS(ns,'marker');
[['id','arr'],['viewBox','0 0 8 8'],['refX','7'],['refY','4'],['markerWidth','5'],['markerHeight','5'],['orient','auto-start-reverse']].forEach(([k,v])=>mk.setAttribute(k,v));
const mp = document.createElementNS(ns,'path'); mp.setAttribute('d','M0 0 L8 4 L0 8z'); mp.setAttribute('fill','#d6cfc6');
mk.appendChild(mp); defs.appendChild(mk); svg.appendChild(defs);
const nm = {}; nodes.forEach(n=>nm[n.id]=n);
edges.forEach(e=>{
const f=nm[e.f],t=nm[e.t]; if(!f||!t) return;
const l=document.createElementNS(ns,'line');
[['x1',f.x],['y1',f.y],['x2',t.x],['y2',t.y],['stroke','#e8e1d9'],['stroke-width','1.5'],['marker-end','url(#arr)']].forEach(([k,v])=>l.setAttribute(k,v));
svg.appendChild(l);
});
nodes.forEach(n=>{
const g=document.createElementNS(ns,'g');
g.setAttribute('id','nd-'+n.id.replace(/[^a-zA-Z0-9]/g,'_'));
g.setAttribute('transform',`translate(${n.x},${n.y})`);
const r=n.t==='svc'?10:13;
const c=document.createElementNS(ns,'circle'); c.setAttribute('r',r);
if(n.mal){ c.setAttribute('fill','#f5f5f4'); c.setAttribute('stroke','#1c1917'); c.setAttribute('stroke-width','2'); }
else if(n.t==='svc'){ c.setAttribute('fill','#eff6ff'); c.setAttribute('stroke','#93c5fd'); c.setAttribute('stroke-width','1.5'); }
else{ c.setAttribute('fill','#faf6f0'); c.setAttribute('stroke','#d6cfc6'); c.setAttribute('stroke-width','1.5'); }
g.appendChild(c);
const ic=document.createElementNS(ns,'text');
[['text-anchor','middle'],['dy','4'],['font-size',n.t==='svc'?'8':'9'],['font-family','system-ui'],['fill',n.mal?'#1c1917':n.t==='svc'?'#3b82f6':'#a8a29e']].forEach(([k,v])=>ic.setAttribute(k,v));
ic.textContent=n.mal?'!':n.t==='svc'?'●':'○'; g.appendChild(ic);
const lb=document.createElementNS(ns,'text');
[['text-anchor','middle'],['y',n.t==='svc'?'22':'26'],['font-size','7.5'],['font-family','monospace'],['fill',n.mal?'#1c1917':'#a8a29e']].forEach(([k,v])=>lb.setAttribute(k,v));
lb.textContent=n.id.length>16?n.id.slice(0,14)+'..':n.id; g.appendChild(lb);
svg.appendChild(g);
});
const mal=nodes.filter(n=>n.mal).length;
document.getElementById('graph-cs').textContent=`${nodes.length} nodes · ${mal} risks`;
}
function gNodeStatus(id,st){
const g=document.getElementById('nd-'+id.replace(/[^a-zA-Z0-9]/g,'_')); if(!g) return;
const c=g.querySelector('circle'),ic=g.querySelector('text');
if(st==='quarantined'){ c.setAttribute('fill','#f5f0e8'); c.setAttribute('stroke','#d6cfc6'); ic.textContent='✕'; ic.setAttribute('fill','#a8a29e'); }
else if(st==='clean'){ c.setAttribute('fill','#f0fdf4'); c.setAttribute('stroke','#86efac'); ic.textContent='✓'; ic.setAttribute('fill','#16a34a'); }
else if(st==='mal'){ c.setAttribute('fill','#f5f5f4'); c.setAttribute('stroke','#1c1917'); ic.textContent='!'; ic.setAttribute('fill','#1c1917'); }
}
// ══════════════════════════════════════════
// LOG
// ══════════════════════════════════════════
function log(t,txt){ const el=document.getElementById('tlog'),s=document.createElement('span'); s.className='ll '+t; s.textContent=txt; el.appendChild(s); el.appendChild(document.createTextNode('\n')); el.scrollTop=el.scrollHeight; }
function clearLog(){ document.getElementById('tlog').innerHTML=''; }
// ══════════════════════════════════════════
// EPISODE
// ══════════════════════════════════════════
async function startEp(){ clearLog(); log('sys',`Starting ${S.sc.toUpperCase()} episode…`); setStatus('active','Active Incident'); startDemo(); }
function startDemo(){
const d=SD[S.sc];
Object.assign(S,{active:true,done:false,step:0,maxSteps:d.maxSteps,exfil:d.exfil,totalReward:0,rewardHist:[0],quarantined:[],rotated:[],notified:[],attProg:0,attBreach:false,demoIdx:0,demoRunning:false,planProg:{investigate:false,trace_root_cause:false,contain:false,recover:false,notify:false,conclude:false}});
showBanner(); renderPkgList(d.pkgs,[]); renderSecList(d.secrets,[]);
document.getElementById('pkg-list').classList.remove('preview');
document.getElementById('sec-list').classList.remove('preview');
updateMetrics(); updateScores();
setActBtns(true);
document.getElementById('btn-demo').disabled=false; document.getElementById('btn-step').disabled=false;
document.getElementById('btn-rst').disabled=false; document.getElementById('btn-start').disabled=true;
log('alr',d.banDesc); log('res',`Packages: ${d.pkgs.join(', ')}`); log('res',`Exfil window: step ${d.exfil} / ${d.maxSteps}`); log('sep',' ');
}
function resetEp(){
if(S.demoTimer) clearTimeout(S.demoTimer);
Object.assign(S,{active:false,done:false,demoRunning:false,totalReward:0,rewardHist:[],step:0,attProg:0,attBreach:false,quarantined:[],rotated:[],notified:[]});
setStatus('ready','Ready'); clearLog();
log('sys','Agentic Security Lab — Supply Chain Incident Response');
log('sys','Model: Qwen2.5-3B-Instruct + GRPO · TRL + Unsloth 4-bit QLoRA');
log('sep',' '); log('','Select a scenario and click ▶ Start Episode to begin.');
document.getElementById('banner').classList.add('hidden');
document.getElementById('btn-start').disabled=false; document.getElementById('btn-demo').disabled=true;
document.getElementById('btn-step').disabled=true; document.getElementById('btn-rst').disabled=true;
document.getElementById('btn-demo').textContent='Run Demo';
setActBtns(false); hideProw(); updateMetrics(); updateScores(); resetChart(); updatePlan({});
// Show the scenario preview rather than a sad empty placeholder
const d=SD[S.sc];
renderPkgList(d.pkgs, []);
renderSecList(d.secrets, []);
document.getElementById('pkg-list').classList.add('preview');
document.getElementById('sec-list').classList.add('preview');
drawGraph(S.sc);
}
// ══════════════════════════════════════════
// DEMO RUNNER
// ══════════════════════════════════════════
async function runDemo(){
if(S.demoRunning){ clearTimeout(S.demoTimer); S.demoRunning=false; document.getElementById('btn-demo').textContent='Run Demo'; return; }
if(!S.active||S.done){ await startEp(); await new Promise(r=>setTimeout(r,400)); }
S.demoRunning=true; document.getElementById('btn-demo').textContent='Pause'; tick();
}
function tick(){
if(!S.demoRunning||S.done){ S.demoRunning=false; document.getElementById('btn-demo').textContent='Run Demo'; return; }
const seq=SD[S.sc].demo; if(S.demoIdx>=seq.length){ S.demoRunning=false; document.getElementById('btn-demo').textContent='Run Demo'; return; }
const st=seq[S.demoIdx++]; simStep(st.c,st.p); S.demoTimer=setTimeout(tick,st.d+400);
}
function stepDemo(){ if(!S.active||S.done) return; const seq=SD[S.sc].demo; if(S.demoIdx>=seq.length) return; const st=seq[S.demoIdx++]; simStep(st.c,st.p); }
// ══════════════════════════════════════════
// SIM STEP
// ══════════════════════════════════════════
const RWDS={scan_logs:.02,inspect_package:.01,check_dependents:.01,quarantine:.15,rotate_secret:.12,notify:.04,conclude:.1};
function simStep(cmd,params){
const d=SD[S.sc]; S.step++;
log('cmd',`${cmd}(${Object.entries(params).map(([k,v])=>`${k}=${v}`).join(', ')})`);
let rwd=RWDS[cmd]??0, msg='';
if(cmd==='scan_logs'&&params.package){ const m=d.mal.includes(params.package); if(m) gNodeStatus(params.package,'mal'); msg=m?`IOC found in ${params.package}: exfiltration attempt detected.`:`No suspicious activity in ${params.package}.`; }
else if(cmd==='inspect_package'&&params.package){ const m=d.mal.includes(params.package); gNodeStatus(params.package,m?'mal':'clean'); msg=`${params.package}: publisher=${m?'COMPROMISED':'verified-legitimate'}`; }
else if(cmd==='check_dependents'&&params.package){ const dl=(d.deps&&d.deps[params.package])||[]; msg=!dl.length?`${params.package} has no known downstream dependents.`:dl.length<=5?`Dependents of ${params.package} (${dl.length}): ${dl.join(', ')}`:`Dependents of ${params.package} (${dl.length}): ${dl.slice(0,5).join(', ')} +${dl.length-5} more`; }
else if(cmd==='quarantine'&&params.package){ if(d.mal.includes(params.package)){ if(!S.quarantined.includes(params.package)) S.quarantined.push(params.package); gNodeStatus(params.package,'quarantined'); msg=`[OK] Quarantined ${params.package} — yanked from registry.`; } else{ rwd=-0.05; msg=`False positive: ${params.package} is clean. Penalty −0.05.`; } }
else if(cmd==='rotate_secret'&&params.secret){ const s=d.secrets.find(x=>x.name===params.secret); rwd=s?.critical?0.12:0.06; if(!S.rotated.includes(params.secret)) S.rotated.push(params.secret); msg=`[OK] Rotated ${params.secret} (${s?.critical?'CRITICAL':'standard'}).`; }
else if(cmd==='notify'&&params.team){ if(!S.notified.includes(params.team)) S.notified.push(params.team); msg=`[OK] Breach notification sent to ${params.team}.`; }
else if(cmd==='conclude'){ msg='Concluding episode and computing final score…'; }
if(msg) log('res',msg);
S.attProg=Math.min(1,S.step/S.exfil);
if(S.step>=S.exfil&&!S.attBreach){ const cs=d.secrets.filter(x=>x.critical).every(x=>S.rotated.includes(x.name)); if(!cs){ S.attBreach=true; rwd-=0.2; log('err','[BREACH] Attacker exfiltrated credentials. Penalty −0.20.'); } }
S.totalReward+=rwd; S.rewardHist.push(S.totalReward);
log(rwd>=0?'rwp':'rwn',`reward: ${rwd>=0?'+':''}${rwd.toFixed(3)} | total: ${S.totalReward.toFixed(3)}`);
S.planProg={investigate:S.step>0,trace_root_cause:S.quarantined.length>0||S.step>3,contain:d.rq.every(p=>S.quarantined.includes(p)),recover:d.rr.every(s=>S.rotated.includes(s)),notify:d.rn.every(t=>S.notified.includes(t)),conclude:cmd==='conclude'};
if(cmd==='conclude'){ S.done=true; finishEp(); }
renderPkgList(d.pkgs,S.quarantined); renderSecList(d.secrets,S.rotated);
updateMetrics(); updateScores(); updatePlan(S.planProg); updateChart();
document.getElementById('rwd-v').textContent=S.totalReward.toFixed(3);
}
// ══════════════════════════════════════════
// MANUAL ACTIONS
// ══════════════════════════════════════════
let _act=null;
function qAct(a){ if(!S.active||S.done) return; _act=a; if(a==='conclude'){ hideProw(); simStep('conclude',{}); _act=null; return; } showProw(a); }
function showProw(a){
const d=SD[S.sc],sel=document.getElementById('psel'); sel.innerHTML='<option value="">Select target…</option>';
const items=['scan_logs','inspect_package','check_dependents','quarantine'].includes(a)?d.pkgs.map(p=>({v:p,l:p+(d.mal.includes(p)?' ⚠':'')})):a==='rotate_secret'?d.secrets.map(s=>({v:s.name,l:s.name+(s.critical?' •':'')})):d.rn.map(t=>({v:t,l:t}));
items.forEach(({v,l})=>{ const o=document.createElement('option'); o.value=v; o.textContent=l; sel.appendChild(o); });
document.getElementById('prow').classList.remove('hidden');
}
function execAct(){ const v=document.getElementById('psel').value; if(!v) return; const p=['scan_logs','inspect_package','check_dependents','quarantine'].includes(_act)?{package:v}:_act==='rotate_secret'?{secret:v}:{team:v}; hideProw(); simStep(_act,p); _act=null; }
function cancelAct(){ _act=null; hideProw(); }
function hideProw(){ document.getElementById('prow').classList.add('hidden'); }
// ══════════════════════════════════════════
// EPISODE FINISH
// ══════════════════════════════════════════
function finishEp(){
const sc=computeScore();
log('sep',' '); log('sys','═══ EPISODE COMPLETE ═══'); log('sys',`Final Score: ${sc.toFixed(4)}`);
log(sc>=0.7?'ok':sc>=0.4?'res':'err',sc>=0.8?'EXCELLENT — Fully contained.':sc>=0.6?'GOOD — Major threats neutralized.':sc>=0.4?'PARTIAL — Some threats remain.':'INSUFFICIENT — Attacker maintained foothold.');
setStatus('done','Resolved'); setActBtns(false);
}
function computeScore(){
const d=SD[S.sc];
return .35*d.rq.filter(p=>S.quarantined.includes(p)).length/Math.max(1,d.rq.length)+.35*d.rr.filter(s=>S.rotated.includes(s)).length/Math.max(1,d.rr.length)+.20*d.rn.filter(t=>S.notified.includes(t)).length/Math.max(1,d.rn.length)+.10*(S.attBreach?0:1);
}
// ══════════════════════════════════════════
// UI HELPERS
// ══════════════════════════════════════════
function showBanner(){
const d=SD[S.sc]; document.getElementById('banner').classList.remove('hidden');
document.getElementById('ban-desc').textContent=d.banDesc;
document.getElementById('ban-ef').textContent=`Step ${S.exfil}`;
document.getElementById('ban-mx').textContent=`${S.maxSteps} steps`;
}
function renderPkgList(pkgs,q){
const d=SD[S.sc];
document.getElementById('pkg-list').innerHTML=pkgs.map(p=>{ const isQ=q.includes(p),isM=d.mal.includes(p),st=isQ?'qua':isM?'mal':'unk'; return `<div class="pkg-row" onclick="qAct('inspect_package');document.getElementById('psel').value='${p}'"><div class="pip pi-${st}"></div><span class="pname ${st}">${p}</span><span class="pchip c-${st}">${isQ?'Yanked':isM?'Malicious':'Unknown'}</span></div>`; }).join('');
document.getElementById('pkg-cnt').textContent=`${pkgs.length} packages`;
}
function renderSecList(secs,r){
document.getElementById('sec-list').innerHTML=secs.map(s=>{ const rt=r.includes(s.name); return `<div class="sec-row"><span class="sbadge ${rt?'sb-rot':'sb-exp'}">${rt?'Rotated':'Exposed'}</span><span class="sname">${s.name}</span>${s.critical?'<span class="scrit">CRITICAL</span>':''}</div>`; }).join('');
}
function updateMetrics(){
const d=SD[S.sc]; if(!d) return;
const max=d.maxSteps, exf=d.exfil, step=S.step;
// Proportional zones (flex values are step counts)
document.getElementById('tl-safe').style.flex = exf;
document.getElementById('tl-danger').style.flex = Math.max(0, max-exf);
// Marker positions along the 0..max track
const exfPct = Math.min(100, (exf/max)*100);
const stepPct = Math.min(100, (step/max)*100);
document.getElementById('tl-mk-exfil').style.left = exfPct+'%';
document.getElementById('tl-now').style.left = stepPct+'%';
// Now-marker style follows episode state
const now = document.getElementById('tl-now');
now.className = 'tl-now' + (
S.attBreach ? ' tl-now-breach' :
S.done ? ' tl-now-done' :
S.active ? ' tl-now-active' : '');
// Track labels
document.getElementById('tl-l-exfil').textContent = `Exfil ${exf}`;
document.getElementById('tl-l-max').textContent = `Max ${max}`;
// Status pill
const status = document.getElementById('tl-status'),
txt = document.getElementById('tl-s-txt');
if (!S.active) { status.className='tl-status'; txt.textContent='Awaiting episode'; }
else if (S.attBreach) { status.className='tl-status tl-s-breach'; txt.textContent='BREACH · credentials exfiltrated'; }
else if (S.done) { status.className='tl-status tl-s-done'; txt.textContent='Resolved · incident closed'; }
else if (step>=exf) { status.className='tl-status tl-s-secured'; txt.textContent='Past exfil window · secured'; }
else { const t=exf-step; status.className='tl-status tl-s-active'; txt.textContent=`Active · ${t} step${t===1?'':'s'} to exfil`; }
// Stat tiles
document.getElementById('tl-v-step').textContent = step;
const tteEl = document.getElementById('tl-v-tte');
if (!S.active) { tteEl.textContent='—'; tteEl.className='tl-v warn'; }
else {
const tte = Math.max(0, exf-step);
tteEl.textContent = tte;
tteEl.className = 'tl-v ' + (S.attBreach ? 'danger' : tte===0 ? '' : tte<=2 ? 'danger' : tte<=5 ? 'warn' : '');
}
document.getElementById('tl-v-rem').textContent = S.active ? Math.max(0, max-step) : '—';
// Header subtitle (always numeric so it tracks the scenario at a glance)
document.getElementById('att-step').textContent = `Step ${step} / ${max}`;
// Reward pill + chart inline value
const rwd = S.totalReward.toFixed(3);
document.getElementById('rwd-v').textContent = rwd;
const cv = document.getElementById('chart-v'); if (cv) cv.textContent = rwd;
}
function updateScores(){
const d=SD[S.sc]; if(!d) return;
const set=(id,bar,v)=>{ document.getElementById(id).textContent=Math.round(v*100)+'%'; document.getElementById(bar).style.width=(v*100)+'%'; };
if(!S.active){ set('sq','bq',0); set('sr','br',0); set('sn','bn',0); set('sc2','bc',0); document.getElementById('tot-v').textContent='0.0000'; return; }
const qr=d.rq.filter(p=>S.quarantined.includes(p)).length/Math.max(1,d.rq.length);
const rr=d.rr.filter(s=>S.rotated.includes(s)).length/Math.max(1,d.rr.length);
const nr=d.rn.filter(t=>S.notified.includes(t)).length/Math.max(1,d.rn.length);
const cr=S.attBreach?0:1;
set('sq','bq',qr); set('sr','br',rr); set('sn','bn',nr); set('sc2','bc',cr);
document.getElementById('tot-v').textContent=(.35*qr+.35*rr+.20*nr+.10*cr).toFixed(4);
}
function updatePlan(prog){
['investigate','trace_root_cause','contain','recover','notify','conclude'].forEach((ph,i)=>{
const ring=document.getElementById('phr-'+ph),lbl=document.querySelector(`#ph-${ph} .ph-lbl`);
if(!ring) return;
ring.className='ph-ring'+(prog[ph]?' done':'');
ring.textContent=prog[ph]?'✓':i+1;
if(lbl) lbl.className='ph-lbl'+(prog[ph]?' done':'');
});
}
function updateChart(){ rChart.data.labels=S.rewardHist.map((_,i)=>i); rChart.data.datasets[0].data=S.rewardHist; rChart.update(); }
function resetChart(){ rChart.data.labels=[]; rChart.data.datasets[0].data=[]; rChart.update(); }
function setStatus(type,txt){ const b=document.getElementById('ep-badge'); b.className='ep-badge ep-'+type; document.getElementById('ep-txt').textContent=txt; }
function setActBtns(on){ ['ab-scan','ab-insp','ab-deps','ab-qua','ab-rot','ab-not','ab-con'].forEach(id=>{ const el=document.getElementById(id); if(el) el.disabled=!on; }); }
// ══════════════════════════════════════════
// INIT
// ══════════════════════════════════════════
selectSc('easy');
</script>
</body>
</html>