Spaces:
Sleeping
Sleeping
| <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 ; } | |
| /* ββββββ PAGE WRAPPER ββββββ */ | |
| #app { max-width: 1280px; margin: 0 auto; padding: 16px 20px 32px; display: flex; flex-direction: column; gap: 12px; } | |
| /* ββββββ HEADER ββββββ */ | |
| .hdr { display: flex; align-items: center; gap: 12px; padding-bottom: 14px; border-bottom: 1px solid var(--bd); flex-wrap: wrap; } | |
| .hdr-logo { display: flex; align-items: center; gap: 9px; } | |
| .logo-ico { width: 34px; height: 34px; 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: 1px; } | |
| .hdr-sep { width: 1px; height: 28px; background: var(--bd); flex-shrink: 0; } | |
| /* Scenario tabs */ | |
| .sc-tabs { display: flex; gap: 4px; } | |
| .sc-tab { padding: 6px 14px; 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: 5px; padding: 4px 10px; | |
| 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: 8px; } | |
| .sc-chip { display: flex; flex-direction: column; align-items: center; padding: 4px 10px; | |
| background: var(--muted); border-radius: var(--r-sm); border: 1px solid var(--bd); } | |
| .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: 1px; } | |
| /* ββββββ INCIDENT BANNER ββββββ */ | |
| .banner { display: flex; align-items: flex-start; gap: 11px; padding: 11px 14px; | |
| 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: 17px; 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: 3px; } | |
| .ban-desc { font-size: 11.5px; color: #78350f; line-height: 1.55; } | |
| .ban-meta { display: flex; gap: 14px; margin-top: 6px; } | |
| .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: 8px; align-items: center; flex-wrap: wrap; } | |
| .btn { display: inline-flex; align-items: center; gap: 5px; padding: 7px 14px; 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: 4px; margin-left: auto; | |
| padding: 4px 12px; 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: 248px 1fr 216px; gap: 12px; 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: 10px; } | |
| .ch { display: flex; align-items: center; justify-content: space-between; padding: 10px 13px 9px; 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: 11px 13px; } | |
| /* ββββββ LEFT COLUMN ββββββ */ | |
| .left-col { display: flex; flex-direction: column; gap: 10px; } | |
| .graph-bg { background: var(--muted); border-radius: var(--r-sm); overflow: hidden; margin: 4px 6px 6px; } | |
| #pkg-graph { width: 100%; display: block; } | |
| .pkg-list, .sec-list { display: flex; flex-direction: column; gap: 2px; max-height: 148px; overflow-y: auto; } | |
| .pkg-row { display: flex; align-items: center; gap: 7px; padding: 5px 7px; 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: 7px; padding: 4px 7px; 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: 10px; } | |
| .log-bar { display: flex; align-items: center; gap: 5px; padding: 8px 11px; 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: 7px; } | |
| .lspin { margin-left: auto; } | |
| #tlog { background: #0d1117; padding: 10px 12px; min-height: 220px; max-height: 248px; overflow-y: auto; | |
| font: 11.5px/1.7 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: 5px; } | |
| .abtn { display: flex; flex-direction: column; align-items: center; gap: 3px; padding: 8px 4px; | |
| 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); } | |
| .prow { display: flex; gap: 7px; align-items: center; margin-top: 8px; flex-wrap: wrap; } | |
| .prow.hidden { display: none; } | |
| .psel { flex: 1; min-width: 140px; padding: 6px 9px; 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: 10px; } | |
| .ring-wrap { display: flex; flex-direction: column; align-items: center; padding: 14px 8px 10px; } | |
| .ring-pct { font-size: 22px; font-weight: 800; font-family: var(--mono); color: var(--fg); line-height: 1; text-align: center; margin-top: -66px; position: relative; z-index: 1; } | |
| .ring-sub { font-size: 9.5px; color: var(--fg3); text-transform: uppercase; letter-spacing: .06em; text-align: center; } | |
| .ring-spacer { height: 18px; } | |
| .ring-row { display: flex; gap: 12px; margin-top: 4px; } | |
| .ring-kv { text-align: center; } | |
| .ring-v { font-size: 13px; font-weight: 700; font-family: var(--mono); color: var(--fg); } | |
| .ring-k { font-size: 9px; color: var(--fg3); text-transform: uppercase; letter-spacing: .05em; } | |
| .si-list { display: flex; flex-direction: column; gap: 8px; } | |
| .si { display: flex; flex-direction: column; gap: 3px; } | |
| .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: 4px; } | |
| .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: 5px; 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: 8px 13px; 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: 8px 12px 10px; } | |
| .chart-l { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--fg3); margin-bottom: 5px; } | |
| #rchart { max-height: 80px; } | |
| .phase-list { display: flex; flex-direction: column; gap: 1px; } | |
| .ph { display: flex; align-items: center; gap: 8px; padding: 6px 7px; border-radius: var(--r-sm); transition: .12s; } | |
| .ph:hover { background: var(--muted); } | |
| .ph-ring { width: 20px; height: 20px; 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: 14px; } | |
| .res-head { margin-bottom: 11px; } | |
| .res-title { font-size: 14px; font-weight: 600; } | |
| .res-sub { font-size: 11px; color: var(--fg3); margin-top: 2px; } | |
| .res-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 11px; } | |
| .ba-row { display: grid; grid-template-columns: 68px 1fr 1fr; gap: 8px; align-items: center; margin-bottom: 7px; } | |
| .ba-lbl { font-size: 11.5px; font-weight: 500; color: var(--fg2); } | |
| .ba-bg { height: 20px; 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 6px; } | |
| .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: 10px 12px; 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: 5px; margin-top: 9px; } | |
| .pill { display: flex; align-items: center; gap: 7px; 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)} } | |
| </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 style="display:flex;flex-direction:column;align-items:center;justify-content:center;padding:6px;border:1px solid var(--bd);border-radius:var(--r-sm);background:var(--muted);gap:2px"> | |
| <span style="font-size:9px;font-weight:600;color:var(--fg3);text-transform:uppercase;letter-spacing:.04em">FP Penalty</span> | |
| <span style="font-size:11px;font-weight:700;font-family:var(--mono);color:var(--fg2)">β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"> | |
| <!-- Threat ring --> | |
| <div class="card"> | |
| <div class="ch"><span class="ct">Threat Actor Progress</span><span class="cs" id="att-step">β</span></div> | |
| <div class="ring-wrap"> | |
| <svg width="106" height="106" viewBox="0 0 106 106"> | |
| <circle cx="53" cy="53" r="44" fill="none" stroke="#f5f0e8" stroke-width="9"/> | |
| <circle id="ring-arc" cx="53" cy="53" r="44" fill="none" stroke="#262626" | |
| stroke-width="9" stroke-linecap="round" | |
| stroke-dasharray="276.5" stroke-dashoffset="276.5" | |
| transform="rotate(-90 53 53)" style="transition:stroke-dashoffset .5s ease"/> | |
| </svg> | |
| <div class="ring-pct" id="ring-pct">0%</div> | |
| <div class="ring-sub">penetration</div> | |
| <div class="ring-spacer"></div> | |
| <div class="ring-row"> | |
| <div class="ring-kv"><div class="ring-v" id="r-step">0</div><div class="ring-k">Step</div></div> | |
| <div class="ring-kv"><div class="ring-v" id="r-unc" style="color:var(--p)">β</div><div class="ring-k">Uncertainty</div></div> | |
| <div class="ring-kv"><div class="ring-v" id="r-rem">β</div><div class="ring-k">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">100%</span></div></div><div class="si-bar"><div class="si-fill" id="bc" style="width:100%"></div></div></div> | |
| </div> | |
| </div> | |
| <div class="tot-row"><span class="tot-l">Total Score</span><span class="tot-v" id="tot-v">0.100</span></div> | |
| </div> | |
| <!-- Reward chart --> | |
| <div class="card"> | |
| <div class="chart-cb"> | |
| <div class="chart-l">Cumulative Reward</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 & 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> | |
| <span class="fh">0.35</span> Γ quarantine_ratio +<br> | |
| <span class="fh">0.35</span> Γ rotate_ratio +<br> | |
| <span class="fh">0.20</span> Γ notify_ratio +<br> | |
| <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}, | |
| ], | |
| }, | |
| 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}, | |
| ], | |
| }, | |
| 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 exfiltrating. Attacker has a 10-step head start.', | |
| graph:{ | |
| nodes:[{id:'lodash',x:44,y:26,mal:false,t:'pkg'},{id:'express',x:126,y:26,mal:false,t:'pkg'},{id:'1odash',x:208,y:26,mal:true,t:'pkg'},{id:'expresss',x:44,y:78,mal:true,t:'pkg'},{id:'axios-http',x:126,y:78,mal:true,t:'pkg'},{id:'node-fetch-lite',x:208,y:78,mal:true,t:'pkg'},{id:'dotenvv',x:126,y:128,mal:true,t:'pkg'},{id:'β 20 services',x:126,y:162,mal:false,t:'svc'}], | |
| edges:[{f:'1odash',t:'β 20 services'},{f:'expresss',t:'β 20 services'},{f:'axios-http',t:'β 20 services'},{f:'node-fetch-lite',t:'β 20 services'},{f:'dotenvv',t:'β 20 services'}], | |
| }, | |
| demo:[ | |
| ...['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:'scan_logs',p:{package:p},d:560})), | |
| ...['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:700})), | |
| ...['NPM_TOKEN','GITHUB_ACTIONS_TOKEN','AWS_ACCESS_KEY_ID','AWS_SECRET_ACCESS_KEY','DATABASE_URL','JWT_SECRET','REDIS_PASSWORD','INTERNAL_WEBHOOK_KEY'].map(s=>({c:'rotate_secret',p:{secret:s},d:460})), | |
| ...['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'].map(t=>({c:'notify',p:{team:t},d:240})), | |
| {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(); | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββ | |
| // 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,[]); updateMetrics(); | |
| 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(); resetChart(); updatePlan({}); | |
| document.getElementById('pkg-list').innerHTML='<div style="color:var(--fg3);font-size:11px;text-align:center;padding:12px">No episode active</div>'; | |
| document.getElementById('sec-list').innerHTML='<div style="color:var(--fg3);font-size:11px;text-align:center;padding:10px">No episode active</div>'; | |
| 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'&¶ms.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'&¶ms.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'&¶ms.package){ msg=`Dependents of ${params.package}: ${d.rn.slice(0,5).join(', ')}${d.rn.length>5?` +${d.rn.length-5} more`:''}`; } | |
| else if(cmd==='quarantine'&¶ms.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'&¶ms.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'&¶ms.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 pct=Math.round(S.attProg*100); | |
| document.getElementById('ring-pct').textContent=pct+'%'; | |
| document.getElementById('ring-arc').style.strokeDashoffset=276.5*(1-S.attProg); | |
| document.getElementById('att-step').textContent=S.active?`Step ${S.step} / ${S.exfil}`:'β'; | |
| document.getElementById('r-step').textContent=S.step; | |
| document.getElementById('r-unc').textContent=S.active?Math.max(0,.55-S.step*.025).toFixed(2):'β'; | |
| document.getElementById('r-rem').textContent=S.active?Math.max(0,S.maxSteps-S.step):'β'; | |
| document.getElementById('rwd-v').textContent=S.totalReward.toFixed(3); | |
| } | |
| function updateScores(){ | |
| const d=SD[S.sc]; if(!d) 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; | |
| const s=(id,bar,v)=>{ document.getElementById(id).textContent=Math.round(v*100)+'%'; document.getElementById(bar).style.width=(v*100)+'%'; }; | |
| s('sq','bq',qr); s('sr','br',rr); s('sn','bn',nr); s('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> | |