from __future__ import annotations import html import os from pathlib import Path from typing import Any, Dict, List from lab.config import ADVISORIES_DIR, CASE_RUNS_DIR, DASHBOARD_DIR, ROOT, RUNS_DIR from lab.repro import load_profiles from lab.utils import ensure_dir, isoformat, load_json_dir, now_utc, unique, write_json, write_text def mermaid_from_steps(run: Dict[str, Any]) -> str: lines = [ "flowchart LR", 'A["Select Advisory"] --> B["Resolve Repro Profile"]', 'B --> C["Provision Compose Environment"]', 'C --> D["Baseline Snapshot"]', 'D --> E["Controlled Attack Steps"]', 'E --> F["Browser Replay"]', 'F --> G["Collect Logs and Evidence"]', 'G --> H["Update Registry and Reports"]', ] if run.get("blocked_reason"): lines.append(f'H --> I["Blocked: {run["blocked_reason"][:60]}"]') return "\n".join(lines) def _relative_ref(run_dir: Path, ref: str) -> str: try: return str(Path(ref).resolve().relative_to(run_dir.resolve())) except ValueError: return ref def _dashboard_ref(run: Dict[str, Any], ref: str) -> str: try: bundle_dir = Path(run["report_refs"]["bundle_dir"]).resolve() relative = Path(ref).resolve().relative_to(bundle_dir) return f"./runs/{run['run_id']}/{relative.as_posix()}" except Exception: return ref def _artifact_kind(href: str) -> str: suffix = Path(href).suffix.lower() if suffix in {".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg"}: return "image" if suffix in {".json", ".log", ".txt", ".yaml", ".yml", ".md", ".mmd", ".html"}: return "text" return "link" def _artifact_item(run: Dict[str, Any], href: str, label: str | None = None) -> Dict[str, Any]: return { "href": href, "label": label or Path(href).name, "kind": _artifact_kind(href), } def _artifact_group(run: Dict[str, Any], key: str, label: str, refs: List[str], use_dashboard_refs: bool = False) -> Dict[str, Any]: items: List[Dict[str, Any]] = [] for ref in refs: href = ref if use_dashboard_refs else _dashboard_ref(run, ref) items.append(_artifact_item(run, href)) return { "key": key, "label": label, "count": len(items), "items": items, } def _attack_result_refs(run: Dict[str, Any]) -> List[str]: refs: List[str] = [] for step in run.get("attack_steps", []): result_path = step.get("result_path") if result_path: refs.append(str(result_path)) return unique(refs) def _progress_counts(run: Dict[str, Any]) -> Dict[str, int]: counts = {"completed": 0, "skipped": 0, "failed": 0, "blocked": 0, "planned": 0, "other": 0} for item in run.get("timeline", []): status = item.get("status", "other") if status.startswith("blocked"): counts["blocked"] += 1 elif status in counts: counts[status] += 1 else: counts["other"] += 1 return counts def _advisory_meta(advisory: Dict[str, Any]) -> Dict[str, Any]: if not advisory: return {} return { "canonical_id": advisory.get("canonical_id"), "title": advisory.get("title"), "summary": advisory.get("summary"), "display_name": advisory.get("display_name"), "system_id": advisory.get("system_id"), "category": advisory.get("category"), "severity": advisory.get("severity"), "cvss_score": advisory.get("cvss_score"), "exploit_status": advisory.get("exploit_status"), "published_at": advisory.get("published_at"), "updated_at": advisory.get("updated_at"), "official_source_url": advisory.get("official_source_url"), "secondary_source_urls": advisory.get("secondary_source_urls", []), "aliases": advisory.get("aliases", []), "secure_code_topics": advisory.get("secure_code_topics", []), "verification_status": advisory.get("verification_status"), "verification_mode": advisory.get("verification_mode"), "artifact_mode": advisory.get("artifact_mode"), "blocked_reason": advisory.get("blocked_reason"), "browser_evidence": advisory.get("browser_evidence", {}), } def _profile_meta(profile: Dict[str, Any]) -> Dict[str, Any]: if not profile: return {} return { "profile_id": profile.get("profile_id"), "vuln_family": profile.get("vuln_family"), "provisioning_mode": profile.get("provisioning_mode"), "destructive_risk": profile.get("destructive_risk"), "cleanup_policy": profile.get("cleanup_policy"), "artifact_source": profile.get("artifact_source", {}), "success_criteria": profile.get("success_criteria", []), "seed_actions": profile.get("seed_actions", []), "attack_actions": profile.get("attack_actions", []), "browser_assertions": profile.get("browser_assertions", {}), "allowed_target_types": profile.get("allowed_target_types", []), "required_services": profile.get("required_services", []), } def _reasoning_lines(advisory: Dict[str, Any], profile: Dict[str, Any]) -> List[str]: notes: List[str] = [] if advisory.get("summary"): notes.append(advisory["summary"]) for key in ("seed_actions", "attack_actions"): for item in profile.get(key, []): message = item.get("message") if message: notes.append(message) for item in profile.get("success_criteria", []): if item: notes.append(item) if advisory.get("blocked_reason"): notes.append(f"Current blocker: {advisory['blocked_reason']}") return unique(notes) def _dashboard_doc_page(title: str, source_path: Path, description: str) -> str: source_label = source_path.relative_to(ROOT) if source_path.is_relative_to(ROOT) else source_path body = source_path.read_text(encoding="utf-8") if source_path.exists() else f"missing source: {source_path}" return f""" {html.escape(title)}

{html.escape(title)}

{html.escape(description)} | source: {html.escape(str(source_label))}
{html.escape(body)}
""" def _write_dashboard_docs() -> None: docs_dir = DASHBOARD_DIR / "docs" ensure_dir(docs_dir) docs = [ ( "project-features.html", "项目功能与特性总览", ROOT / "docs" / "project-features.md", "Dashboard-local mirror of the repo feature guide.", ), ( "frontend-dashboard-design.html", "本地前端工作台设计文档", ROOT / "docs" / "frontend-dashboard-design.md", "Dashboard-local mirror of the UI and interaction specification.", ), ( "secure-code-index.html", "安全编码修复库索引", ROOT / "05-defense" / "secure-code" / "INDEX.md", "Dashboard-local mirror of the secure-code library index.", ), ] for filename, title, source_path, description in docs: write_text(docs_dir / filename, _dashboard_doc_page(title, source_path, description)) def render_run(run: Dict[str, Any]) -> Dict[str, str]: run_dir = CASE_RUNS_DIR / run["run_id"] ensure_dir(run_dir / "assets") timeline_path = run_dir / "timeline.mmd" write_text(timeline_path, mermaid_from_steps(run)) screenshot_refs = [ref for ref in run.get("browser_refs", []) if ref.endswith((".png", ".jpg", ".jpeg"))] relative_screenshots = [_relative_ref(run_dir, ref) for ref in screenshot_refs] md_lines = [ f"# Run {run['run_id']}", "", "> `LAB ONLY` | `AUTHORIZED TARGETS ONLY` | 自动生成 run bundle", "", f"- Advisory: `{run['advisory_id']}`", f"- 系统: `{run['system_id']}`", f"- Repro Profile: `{run['repro_profile_id']}`", f"- 实证状态: `{run['verification_status']}`", f"- 实证方式: `{run['verification_mode']}`", f"- Artifact 模式: `{run['artifact_mode']}`", f"- 启动时间: `{run['started_at']}`", f"- 完成时间: `{run['finished_at']}`", f"- 阻塞原因: `{run.get('blocked_reason') or '-'}`", f"- Compose 服务: `{', '.join(run.get('compose_services', [])) or '-'}`", "", "## 运行时间线", "", f"- Mermaid: [{timeline_path.name}]({timeline_path})", "", "| 时间 | 步骤 | 状态 | 说明 |", "|------|------|------|------|", ] if run.get("timeline"): for item in run["timeline"]: md_lines.append( f"| `{item.get('at', '')}` | `{item.get('step', '')}` | `{item.get('status', '')}` | {item.get('detail', '') or '-'} |" ) else: md_lines.append("| `-` | `-` | `-` | 无时间线 |") md_lines.extend( [ "", "## Compose 拓扑", "", f"- Compose 文件: `{', '.join(run.get('compose_refs', [])) or '-'}`", f"- 服务列表: `{', '.join(run.get('compose_services', [])) or '-'}`", "", "## 攻击步骤", "", "| 工具/步骤 | 状态 | 结果 |", "|-----------|------|------|", ] ) if run.get("attack_steps"): for step in run["attack_steps"]: outcome = step.get("result_path") or step.get("detail") or "-" md_lines.append(f"| `{step.get('tool') or step.get('kind')}` | `{step.get('status', '-')}` | `{outcome}` |") else: md_lines.append("| `-` | `skipped` | `no attack steps` |") md_lines.extend( [ "", "## 证据摘要", "", f"- Baseline: `{len(run.get('baseline_refs', []))}`", f"- 攻击步骤: `{len(run.get('attack_steps', []))}`", f"- 浏览器证据: `{len(run.get('browser_refs', []))}`", f"- 容器日志: `{len(run.get('container_log_refs', []))}`", f"- 请求日志: `{len(run.get('request_log_refs', []))}`", "", ] ) if relative_screenshots: md_lines.extend(["## 浏览器截图", ""]) for ref in relative_screenshots: md_lines.append(f"![{Path(ref).stem}]({ref})") md_lines.append("") if run.get("browser_refs"): md_lines.extend(["## 浏览器证据", ""]) for ref in run["browser_refs"]: md_lines.append(f"- `{_relative_ref(run_dir, ref)}`") md_lines.append("") if run.get("container_log_refs"): md_lines.extend(["## 容器日志", ""]) for ref in run["container_log_refs"]: md_lines.append(f"- `{_relative_ref(run_dir, ref)}`") md_lines.append("") if run.get("request_log_refs"): md_lines.extend(["## 请求与基线日志", ""]) for ref in run["request_log_refs"]: md_lines.append(f"- `{_relative_ref(run_dir, ref)}`") md_lines.append("") md_lines.extend( [ "## 最小化验证说明", "", "- 仅限自有资产、本地靶场或已授权实验目标。", "- 默认执行 minimal-proof;不会把破坏性或不可回滚动作作为默认路径。", "- 若浏览器证据缺失,前端类案例不会被标为 `verified-*`。", "", ] ) report_md = run_dir / "report.md" write_text(report_md, "\n".join(md_lines)) html_body = [ "", "websafe run report", "", "", f"

Run {html.escape(run['run_id'])}

", "
", f"
Advisory
{html.escape(run['advisory_id'])}
", f"
Status
{html.escape(run['verification_status'])}
", f"
Profile
{html.escape(run['repro_profile_id'])}
", f"
Artifact Mode
{html.escape(run['artifact_mode'])}
", "
", "

Mermaid Timeline

", f"
{html.escape(mermaid_from_steps(run))}
", "

Timeline

", "", ] if run.get("timeline"): for item in run["timeline"]: html_body.append( "" f"" f"" f"" f"" "" ) html_body.extend(["
TimeStepStatusDetail
{html.escape(item.get('at', ''))}{html.escape(item.get('step', ''))}{html.escape(item.get('status', ''))}{html.escape(item.get('detail', '') or '-')}
", "

Attack Steps

", ""]) if run.get("attack_steps"): for step in run["attack_steps"]: html_body.append( "" f"" f"" f"" "" ) else: html_body.append("") html_body.extend(["
ToolStatusOutput
{html.escape(step.get('tool') or step.get('kind') or '-')}{html.escape(step.get('status', '-'))}{html.escape(step.get('result_path') or '-')}
-skippedno attack steps
"]) if relative_screenshots: html_body.extend(["

Browser Screenshots

", "") html_body.extend(["

Evidence

", ""]) report_html = run_dir / "report.html" write_text(report_html, "\n".join(html_body)) return {"bundle_dir": str(run_dir), "report_md": str(report_md), "report_html": str(report_html), "timeline": str(timeline_path)} def render_dashboard() -> Dict[str, str]: ensure_dir(DASHBOARD_DIR) _write_dashboard_docs() advisory_records = load_json_dir(ADVISORIES_DIR) runs = load_json_dir(RUNS_DIR) advisory_map = {item["canonical_id"]: item for item in advisory_records if item.get("canonical_id")} profile_map = load_profiles() runs_dir = DASHBOARD_DIR / "runs" assets_dir = DASHBOARD_DIR / "assets" ensure_dir(runs_dir) ensure_dir(assets_dir) for item in runs: bundle_dir = Path(item.get("report_refs", {}).get("bundle_dir", "")) if not bundle_dir.exists(): continue symlink_path = runs_dir / item["run_id"] relative_target = os.path.relpath(bundle_dir, symlink_path.parent) try: if symlink_path.is_symlink() or symlink_path.exists(): if symlink_path.is_symlink() and os.readlink(symlink_path) == relative_target: pass else: symlink_path.unlink() os.symlink(relative_target, symlink_path, target_is_directory=True) else: os.symlink(relative_target, symlink_path, target_is_directory=True) except OSError: continue systems: Dict[str, Dict[str, Any]] = {} for advisory in advisory_records: system = systems.setdefault( advisory["system_id"], { "system_id": advisory["system_id"], "display_name": advisory.get("display_name", advisory["system_id"]), "total": 0, "verified_real": 0, "verified_synthetic": 0, "blocked": 0, "manual": 0, "browser_required": 0, "browser_present": 0, "latest_update": "", }, ) system["total"] += 1 status = advisory.get("verification_status", "triage-manual") if status == "verified-real": system["verified_real"] += 1 elif status == "verified-synthetic": system["verified_synthetic"] += 1 elif status.startswith("blocked-"): system["blocked"] += 1 else: system["manual"] += 1 browser = advisory.get("browser_evidence", {}) if browser.get("required"): system["browser_required"] += 1 if browser.get("present"): system["browser_present"] += 1 latest = advisory.get("updated_at") or advisory.get("published_at") or "" if latest > system["latest_update"]: system["latest_update"] = latest recent_runs = sorted(runs, key=lambda item: item.get("finished_at") or "", reverse=True)[:100] decorated_runs: List[Dict[str, Any]] = [] for item in recent_runs: cloned = dict(item) advisory = advisory_map.get(item["advisory_id"], {}) profile = profile_map.get(item["repro_profile_id"], {}) browser_evidence = item.get("browser_evidence") or advisory.get("browser_evidence") or { "required": bool(profile.get("browser_assertions", {}).get("required")), "present": False, "refs": [], } request_only_refs = [ref for ref in item.get("request_log_refs", []) if ref not in item.get("baseline_refs", [])] cloned["dashboard_refs"] = { "report_html": f"./runs/{item['run_id']}/report.html", "report_md": f"./runs/{item['run_id']}/report.md", "timeline": f"./runs/{item['run_id']}/timeline.mmd", "bundle": f"./runs/{item['run_id']}/run.json", } cloned["browser_evidence"] = browser_evidence cloned["browser_links"] = [_dashboard_ref(item, ref) for ref in item.get("browser_refs", [])] cloned["container_links"] = [_dashboard_ref(item, ref) for ref in item.get("container_log_refs", [])] cloned["request_links"] = [_dashboard_ref(item, ref) for ref in item.get("request_log_refs", [])] cloned["advisory_meta"] = _advisory_meta(advisory) cloned["profile_meta"] = _profile_meta(profile) cloned["reasoning_lines"] = _reasoning_lines(advisory, profile) cloned["progress"] = _progress_counts(item) cloned["artifact_groups"] = [ _artifact_group( item, "reports", "Reports", [ cloned["dashboard_refs"]["report_html"], cloned["dashboard_refs"]["report_md"], cloned["dashboard_refs"]["timeline"], cloned["dashboard_refs"]["bundle"], ], use_dashboard_refs=True, ), _artifact_group(item, "compose", "Compose", item.get("compose_refs", [])), _artifact_group(item, "baseline", "Baseline Snapshots", item.get("baseline_refs", [])), _artifact_group(item, "attack", "Attack Outputs", _attack_result_refs(item)), _artifact_group(item, "browser", "Browser Evidence", item.get("browser_refs", [])), _artifact_group(item, "container", "Container Logs", item.get("container_log_refs", [])), _artifact_group(item, "requests", "Request Logs", request_only_refs), ] cloned["artifact_groups"] = [group for group in cloned["artifact_groups"] if group["count"]] decorated_runs.append(cloned) summary = { "generated_at": isoformat(now_utc()), "advisory_count": len(advisory_records), "run_count": len(runs), "statuses": {}, "recent_failures": [], } for item in runs: status = item.get("verification_status", "triage-manual") summary["statuses"][status] = summary["statuses"].get(status, 0) + 1 summary["systems"] = sorted(systems.values(), key=lambda item: (-item["total"], item["system_id"])) summary["recent_failures"] = [ { "run_id": item["run_id"], "advisory_id": item["advisory_id"], "status": item.get("verification_status"), "title": item.get("advisory_meta", {}).get("title"), "blocked_reason": item.get("blocked_reason"), } for item in decorated_runs if item.get("verification_status") in {"triage-manual", "blocked-artifact", "blocked-destructive"} ][:20] write_json(DASHBOARD_DIR / "summary.json", summary) write_json(DASHBOARD_DIR / "runs.json", decorated_runs) write_json(DASHBOARD_DIR / "systems.json", summary["systems"]) write_json(DASHBOARD_DIR / "advisories.json", {key: _advisory_meta(value) for key, value in advisory_map.items()}) write_json(DASHBOARD_DIR / "profiles.json", {key: _profile_meta(value) for key, value in profile_map.items()}) styles_css = """ :root { --bg: #07111f; --panel: rgba(9, 18, 32, 0.86); --panel-2: rgba(10, 24, 44, 0.92); --panel-soft: rgba(18, 32, 56, 0.74); --border: rgba(137, 171, 214, 0.22); --text: #f7fafc; --muted: #9fb3ca; --accent: #5eead4; --accent-2: #ffb86b; --accent-3: #90cdf4; --danger: #ff7b7b; --warning: #ffd166; --success: #6ee7a5; --shadow: 0 24px 80px rgba(1, 7, 20, 0.45); --radius: 20px; } * { box-sizing: border-box; } html, body { margin: 0; min-height: 100%; } body { font-family: "IBM Plex Sans", "Avenir Next", "Segoe UI", sans-serif; background: radial-gradient(circle at top left, rgba(94, 234, 212, 0.15), transparent 28%), radial-gradient(circle at top right, rgba(255, 184, 107, 0.18), transparent 22%), linear-gradient(145deg, #050c16 0%, #08111f 44%, #0d1c31 100%); color: var(--text); overflow-x: hidden; } body::before { content: ""; position: fixed; inset: 0; pointer-events: none; background-image: linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px); background-size: 32px 32px; mask-image: radial-gradient(circle at center, black 36%, transparent 78%); opacity: 0.28; } a { color: var(--accent); text-decoration: none; } a:hover { text-decoration: underline; } button, input, select { font: inherit; } .dashboard-shell { position: relative; max-width: 1640px; margin: 0 auto; padding: 32px 24px 40px; } .hero { position: sticky; top: 0; z-index: 20; backdrop-filter: blur(18px); background: linear-gradient(180deg, rgba(7, 17, 31, 0.94), rgba(7, 17, 31, 0.75)); border: 1px solid var(--border); border-radius: 28px; padding: 24px 24px 20px; box-shadow: var(--shadow); } .hero-grid { display: grid; grid-template-columns: 1.6fr 1fr; gap: 20px; align-items: start; } .eyebrow { display: inline-flex; align-items: center; gap: 8px; color: var(--muted); font-size: 0.88rem; letter-spacing: 0.12em; text-transform: uppercase; } .eyebrow::before { content: ""; width: 10px; height: 10px; border-radius: 999px; background: radial-gradient(circle, var(--accent), rgba(94, 234, 212, 0.15)); box-shadow: 0 0 24px rgba(94, 234, 212, 0.8); animation: pulse 2.8s ease-in-out infinite; } .hero h1 { margin: 12px 0 10px; font-family: "IBM Plex Serif", "Iowan Old Style", Georgia, serif; font-size: clamp(2rem, 4vw, 3.5rem); line-height: 1.02; } .hero p { margin: 0; color: var(--muted); max-width: 74ch; } .hero-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 18px; } .chip, .ghost-chip { display: inline-flex; align-items: center; justify-content: center; gap: 8px; border-radius: 999px; border: 1px solid var(--border); padding: 10px 14px; background: rgba(255,255,255,0.06); color: var(--text); } .ghost-chip { background: rgba(255,255,255,0.04); } .hero-meta { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } .meta-card, .glass-panel { background: var(--panel); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); } .meta-card { padding: 18px; min-height: 116px; } .meta-card strong { display: block; color: var(--muted); font-size: 0.84rem; letter-spacing: 0.08em; text-transform: uppercase; } .meta-card span { display: block; margin-top: 10px; font-size: 2rem; font-weight: 700; } .workspace { display: grid; grid-template-columns: 420px minmax(0, 1fr); gap: 20px; margin-top: 22px; } .sidebar { display: flex; flex-direction: column; gap: 18px; } .panel-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; } .panel-header h2, .panel-header h3 { margin: 0; font-size: 1rem; letter-spacing: 0.04em; text-transform: uppercase; color: var(--muted); } .glass-panel { padding: 18px; background: linear-gradient(180deg, rgba(255,255,255,0.04), transparent 35%), var(--panel); } .filters { display: grid; gap: 12px; } .filters label { display: grid; gap: 6px; color: var(--muted); font-size: 0.9rem; } .filters input, .filters select { width: 100%; background: rgba(255,255,255,0.05); color: var(--text); border: 1px solid rgba(159, 179, 202, 0.18); border-radius: 14px; padding: 12px 14px; } .run-list { display: grid; gap: 12px; max-height: calc(100vh - 460px); overflow: auto; padding-right: 4px; } .run-card { width: 100%; text-align: left; padding: 16px; border-radius: 18px; border: 1px solid rgba(159, 179, 202, 0.14); background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)); color: var(--text); cursor: pointer; transition: transform 180ms ease, border-color 180ms ease, background 180ms ease; } .run-card:hover, .run-card.is-active { transform: translateY(-1px); border-color: rgba(94, 234, 212, 0.42); background: linear-gradient(180deg, rgba(94, 234, 212, 0.14), rgba(255,255,255,0.05)); } .run-card-top, .flex-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; } .run-card h4 { margin: 10px 0 8px; font-size: 1rem; line-height: 1.35; } .mini-muted { color: var(--muted); font-size: 0.86rem; } .status-pill { display: inline-flex; align-items: center; gap: 7px; border-radius: 999px; padding: 6px 10px; font-size: 0.82rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; border: 1px solid transparent; } .status-pill::before { content: ""; width: 8px; height: 8px; border-radius: 999px; background: currentColor; box-shadow: 0 0 16px currentColor; } .status-blocked-artifact, .status-blocked-destructive { color: var(--danger); background: rgba(255, 123, 123, 0.14); border-color: rgba(255, 123, 123, 0.24); } .status-triage-manual, .status-suspected { color: var(--warning); background: rgba(255, 209, 102, 0.14); border-color: rgba(255, 209, 102, 0.24); } .status-verified-real { color: var(--success); background: rgba(110, 231, 165, 0.14); border-color: rgba(110, 231, 165, 0.24); } .status-verified-synthetic { color: var(--accent-3); background: rgba(144, 205, 244, 0.14); border-color: rgba(144, 205, 244, 0.24); } .status-default { color: var(--accent); background: rgba(94, 234, 212, 0.14); border-color: rgba(94, 234, 212, 0.24); } .detail-view { display: grid; gap: 18px; } .detail-hero { padding: 22px; overflow: hidden; position: relative; } .detail-hero::after { content: ""; position: absolute; inset: auto -20% -55% 25%; height: 220px; background: radial-gradient(circle, rgba(94, 234, 212, 0.2), transparent 55%); pointer-events: none; } .detail-headline { margin: 8px 0 12px; font-family: "IBM Plex Serif", "Iowan Old Style", Georgia, serif; font-size: clamp(1.6rem, 3vw, 2.8rem); line-height: 1.08; } .tag-row, .link-row, .artifact-row { display: flex; flex-wrap: wrap; gap: 10px; } .tag { display: inline-flex; align-items: center; padding: 7px 10px; border-radius: 999px; background: rgba(255,255,255,0.06); border: 1px solid rgba(159, 179, 202, 0.18); color: var(--text); font-size: 0.86rem; } .stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-top: 18px; } .stat-card { padding: 14px; border-radius: 16px; background: rgba(255,255,255,0.04); border: 1px solid rgba(159, 179, 202, 0.16); } .stat-card strong { display: block; color: var(--muted); font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.08em; } .stat-card span { display: block; margin-top: 10px; font-size: 1.15rem; font-weight: 700; } .detail-grid { display: grid; grid-template-columns: minmax(0, 1fr) 360px; gap: 18px; } .stack { display: grid; gap: 18px; } .progress-strip { display: grid; gap: 12px; margin-bottom: 16px; } .progress-bar { display: flex; width: 100%; min-height: 12px; overflow: hidden; border-radius: 999px; background: rgba(255,255,255,0.08); border: 1px solid rgba(159, 179, 202, 0.14); } .progress-segment { min-width: 10px; transition: width 180ms ease; } .progress-completed { background: linear-gradient(90deg, rgba(110, 231, 165, 0.9), rgba(94, 234, 212, 0.9)); } .progress-blocked { background: linear-gradient(90deg, rgba(255, 123, 123, 0.95), rgba(255, 160, 122, 0.9)); } .progress-failed { background: linear-gradient(90deg, rgba(255, 123, 123, 0.92), rgba(255, 209, 102, 0.88)); } .progress-skipped { background: linear-gradient(90deg, rgba(255,255,255,0.22), rgba(159, 179, 202, 0.3)); } .progress-planned { background: linear-gradient(90deg, rgba(144, 205, 244, 0.82), rgba(94, 234, 212, 0.72)); } .progress-other { background: linear-gradient(90deg, rgba(255,255,255,0.18), rgba(255,255,255,0.1)); } .progress-legend { display: flex; flex-wrap: wrap; gap: 10px; } .progress-legend .tag { gap: 7px; } .progress-legend .swatch { width: 10px; height: 10px; border-radius: 999px; display: inline-block; } .stage-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-bottom: 18px; } .stage-card { padding: 14px; border-radius: 16px; background: rgba(255,255,255,0.04); border: 1px solid rgba(159, 179, 202, 0.16); } .stage-card strong { display: block; margin-bottom: 10px; } .accordion { overflow: hidden; } .accordion > summary { list-style: none; cursor: pointer; padding: 18px 20px; display: flex; align-items: center; justify-content: space-between; gap: 12px; } .accordion > summary::-webkit-details-marker { display: none; } .accordion > summary span { font-size: 1rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); } .accordion .accordion-content { padding: 0 20px 20px; border-top: 1px solid rgba(159, 179, 202, 0.12); } .timeline-list { display: grid; gap: 12px; } .timeline-item { display: grid; grid-template-columns: 120px 180px minmax(0, 1fr); gap: 12px; padding: 12px 0; border-bottom: 1px solid rgba(159, 179, 202, 0.12); } .timeline-item:last-child { border-bottom: 0; } .timeline-step { font-weight: 700; } .artifact-group { margin-bottom: 14px; } .artifact-group h4 { margin: 0 0 10px; color: var(--muted); font-size: 0.88rem; text-transform: uppercase; letter-spacing: 0.08em; } .artifact-button { display: inline-flex; align-items: center; gap: 8px; margin: 0 10px 10px 0; padding: 10px 12px; border-radius: 14px; border: 1px solid rgba(159, 179, 202, 0.16); background: rgba(255,255,255,0.05); color: var(--text); cursor: pointer; } .artifact-button:hover, .artifact-button.is-active { border-color: rgba(94, 234, 212, 0.4); background: rgba(94, 234, 212, 0.12); } .log-viewer { min-height: 420px; display: grid; gap: 14px; } .viewer-toolbar { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 10px; align-items: center; } .viewer-frame { background: rgba(2, 8, 22, 0.88); border: 1px solid rgba(159, 179, 202, 0.18); border-radius: 16px; min-height: 300px; overflow: hidden; } .viewer-frame pre { margin: 0; padding: 18px; max-height: 560px; overflow: auto; font-family: "IBM Plex Mono", "SFMono-Regular", "Menlo", monospace; font-size: 0.88rem; line-height: 1.6; color: #d6e5f5; white-space: pre-wrap; } .viewer-frame img { display: block; width: 100%; height: auto; } .gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; } .gallery button { all: unset; cursor: pointer; border-radius: 18px; overflow: hidden; border: 1px solid rgba(159, 179, 202, 0.18); background: rgba(255,255,255,0.04); } .gallery img { display: block; width: 100%; aspect-ratio: 4 / 3; object-fit: cover; } .gallery figcaption { padding: 10px 12px 14px; color: var(--muted); font-size: 0.84rem; } .failure-callout { padding: 16px 18px; border-radius: 18px; border: 1px solid rgba(255, 123, 123, 0.2); background: rgba(255, 123, 123, 0.09); } .json-block { background: rgba(2, 8, 22, 0.72); border-radius: 16px; border: 1px solid rgba(159, 179, 202, 0.14); padding: 16px; overflow: auto; font-family: "IBM Plex Mono", "SFMono-Regular", monospace; font-size: 0.84rem; line-height: 1.55; color: #c9d8e8; } .empty-state { padding: 40px 24px; text-align: center; color: var(--muted); } .failure-feed { display: grid; gap: 10px; } .failure-item { padding: 12px 14px; border-radius: 16px; background: rgba(255,255,255,0.04); border: 1px solid rgba(159, 179, 202, 0.16); } .system-grid { display: grid; gap: 10px; } .system-card { padding: 14px 16px; border-radius: 16px; background: rgba(255,255,255,0.04); border: 1px solid rgba(159, 179, 202, 0.14); } .meter { position: relative; height: 10px; border-radius: 999px; background: rgba(255,255,255,0.08); overflow: hidden; margin-top: 10px; } .meter > span { position: absolute; inset: 0 auto 0 0; width: var(--fill, 0%); background: linear-gradient(90deg, var(--accent), var(--accent-2)); border-radius: inherit; } .sync-indicator { display: inline-flex; align-items: center; gap: 8px; } .sync-indicator strong { color: var(--text); } .dot { width: 10px; height: 10px; border-radius: 999px; background: var(--accent); box-shadow: 0 0 18px rgba(94, 234, 212, 0.8); } @keyframes pulse { 0%, 100% { transform: scale(1); opacity: 0.88; } 50% { transform: scale(1.35); opacity: 1; } } @media (max-width: 1280px) { .workspace, .detail-grid, .hero-grid { grid-template-columns: 1fr; } .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 760px) { .dashboard-shell { padding: 18px 14px 32px; } .hero { position: static; } .stat-grid, .hero-meta { grid-template-columns: 1fr; } .timeline-item { grid-template-columns: 1fr; } } """ write_text(assets_dir / "styles.css", styles_css) app_js = """ const state = { summary: null, runs: [], systems: [], advisories: {}, profiles: {}, selectedRunId: null, selectedArtifact: null, filters: { search: "", system: "", status: "", family: "" }, autoRefresh: true, refreshMs: 5000, refreshHandle: null, }; const $ = (id) => document.getElementById(id); const statusClass = (status) => `status-pill ${({ "blocked-artifact": "status-blocked-artifact", "blocked-destructive": "status-blocked-destructive", "triage-manual": "status-triage-manual", "verified-real": "status-verified-real", "verified-synthetic": "status-verified-synthetic", "suspected": "status-suspected", "completed": "status-verified-real", "failed": "status-blocked-artifact", "skipped": "status-triage-manual" })[status] || "status-default"}`; function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function timeAgo(value) { if (!value) return "-"; const diff = Date.now() - new Date(value).getTime(); if (Number.isNaN(diff)) return value; const seconds = Math.floor(diff / 1000); if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } async function fetchJson(url) { const response = await fetch(`${url}?t=${Date.now()}`, { cache: "no-store" }); if (!response.ok) { throw new Error(`${url} -> ${response.status}`); } return response.json(); } async function loadData(preserveSelection = true) { $("syncState").innerHTML = `Refreshing${new Date().toLocaleTimeString()}`; const previousRun = state.selectedRunId; try { const [summary, runs, systems, advisories, profiles] = await Promise.all([ fetchJson("./summary.json"), fetchJson("./runs.json"), fetchJson("./systems.json"), fetchJson("./advisories.json"), fetchJson("./profiles.json"), ]); state.summary = summary; state.runs = runs; state.systems = systems; state.advisories = advisories; state.profiles = profiles; hydrateFilterOptions(); const hashRun = location.hash.startsWith("#run=") ? location.hash.replace("#run=", "") : null; const selectedCandidate = preserveSelection ? (hashRun || previousRun) : hashRun; if (selectedCandidate && runs.some((item) => item.run_id === selectedCandidate)) { state.selectedRunId = selectedCandidate; } else { state.selectedRunId = runs[0]?.run_id || null; } renderDashboard(); $("syncState").innerHTML = `Live${summary.generated_at || new Date().toISOString()}`; } catch (error) { $("syncState").innerHTML = `Load Failed${escapeHtml(error.message)}`; $("runList").innerHTML = `
Dashboard load failed: ${escapeHtml(error.message)}
`; $("detailRoot").innerHTML = `
Unable to load dashboard data. Check generated JSON and local static server state.
`; } } function filteredRuns() { return state.runs.filter((item) => { if (state.filters.system && item.system_id !== state.filters.system) return false; if (state.filters.status && item.verification_status !== state.filters.status) return false; if (state.filters.family && item.repro_profile_id !== state.filters.family) return false; if (!state.filters.search) return true; const advisoryTitle = item.advisory_meta?.title || ""; const haystack = [item.run_id, item.advisory_id, item.system_id, item.repro_profile_id, advisoryTitle] .join(" ") .toLowerCase(); return haystack.includes(state.filters.search); }); } function renderMetrics() { const metrics = [ { label: "Advisories", value: state.summary?.advisory_count ?? 0 }, { label: "Run Bundles", value: state.summary?.run_count ?? 0 }, ...Object.entries(state.summary?.statuses || {}).map(([label, value]) => ({ label, value })), ]; $("metrics").innerHTML = metrics .map((item) => `
${escapeHtml(item.label)}${escapeHtml(item.value)}
`) .join(""); } function renderSystemCoverage() { $("systemCoverage").innerHTML = state.systems .map((system) => { const total = Math.max(system.total || 0, 1); const verified = (system.verified_real || 0) + (system.verified_synthetic || 0); const fill = Math.round((verified / total) * 100); return `
${escapeHtml(system.display_name || system.system_id)} ${escapeHtml(system.browser_present || 0)}/${escapeHtml(system.browser_required || 0)} browser
${escapeHtml(system.system_id)} · latest ${escapeHtml(system.latest_update || "-")}
real ${escapeHtml(system.verified_real || 0)} synthetic ${escapeHtml(system.verified_synthetic || 0)} blocked ${escapeHtml(system.blocked || 0)} manual ${escapeHtml(system.manual || 0)}
`; }) .join(""); } function renderFailures() { const failures = state.summary?.recent_failures || []; $("failureFeed").innerHTML = failures.length ? failures .map((item) => `
${escapeHtml(item.run_id)} ${escapeHtml(item.status)}
${escapeHtml(item.title || item.advisory_id)}
${escapeHtml(item.blocked_reason || "-")}
`) .join("") : `
No recent blockers.
`; } function renderRunList() { const filtered = filteredRuns(); $("runCount").textContent = `${filtered.length} shown`; $("runList").innerHTML = filtered.length ? filtered .map((item) => { const active = item.run_id === state.selectedRunId ? "is-active" : ""; const title = item.advisory_meta?.title || item.advisory_id; const reasoning = item.reasoning_lines?.[0] || item.blocked_reason || ""; const browserLabel = item.browser_evidence?.present ? "ready" : (item.browser_evidence?.required ? "required" : "n/a"); return ` `; }) .join("") : `
No runs match the current filters.
`; document.querySelectorAll("[data-run-id]").forEach((button) => { button.addEventListener("click", () => { state.selectedRunId = button.dataset.runId; location.hash = `run=${state.selectedRunId}`; renderRunList(); renderDetail(); }); }); } function renderDashboard() { renderMetrics(); renderSystemCoverage(); renderFailures(); renderRunList(); renderDetail(); } function setFilterListeners() { [["searchInput", "search"], ["systemFilter", "system"], ["statusFilter", "status"], ["familyFilter", "family"]].forEach(([id, key]) => { $(id).addEventListener("input", (event) => { state.filters[key] = String(event.target.value || "").trim().toLowerCase(); if (key !== "search") { state.filters[key] = String(event.target.value || ""); } renderRunList(); }); }); } function hydrateFilterOptions() { const distinct = (items) => [...new Set(items.filter(Boolean))].sort(); const patchOptions = (id, values) => { const control = $(id); const current = control.value; control.innerHTML = control.dataset.base; control.innerHTML += distinct(values).map((value) => ``).join(""); control.value = current; }; patchOptions("systemFilter", state.runs.map((item) => item.system_id)); patchOptions("statusFilter", state.runs.map((item) => item.verification_status)); patchOptions("familyFilter", state.runs.map((item) => item.repro_profile_id)); } function defaultArtifact(run) { const preference = ["attack", "requests", "container", "browser", "baseline", "compose", "reports"]; for (const key of preference) { const group = (run.artifact_groups || []).find((item) => item.key === key && item.items?.length); if (!group) continue; const preferredText = group.items.find((item) => item.kind === "text"); return preferredText || group.items[0]; } return null; } function totalProgress(progress) { const values = Object.values(progress || {}).map((value) => Number(value || 0)); return values.reduce((sum, value) => sum + value, 0); } function renderProgressStrip(progress) { const total = totalProgress(progress); if (!total) { return `
No timeline progress recorded.
`; } const order = [ ["completed", "Completed", "progress-completed"], ["blocked", "Blocked", "progress-blocked"], ["failed", "Failed", "progress-failed"], ["skipped", "Skipped", "progress-skipped"], ["planned", "Planned", "progress-planned"], ["other", "Other", "progress-other"], ]; const segments = order .filter(([key]) => Number(progress?.[key] || 0) > 0) .map(([key, _label, klass]) => { const count = Number(progress?.[key] || 0); const pct = Math.max((count / total) * 100, 4); return `
`; }) .join(""); const legend = order .filter(([key]) => Number(progress?.[key] || 0) > 0) .map(([key, label, klass]) => ` ${escapeHtml(label)} ${escapeHtml(progress?.[key] || 0)} `) .join(""); return `
${segments}
${legend}
`; } function renderStageCards(run) { const timeline = run.timeline || []; if (!timeline.length) { return `
No stage records available.
`; } return `
${timeline.map((item) => `
${escapeHtml(item.step || "-")}
${escapeHtml(item.status || "unknown")}
${escapeHtml(item.detail || "-")}
${escapeHtml(item.at || "-")}
`).join("")}
`; } async function openArtifact(href, label, kind) { state.selectedArtifact = { href, label, kind }; document.querySelectorAll(".artifact-button").forEach((button) => { button.classList.toggle("is-active", button.dataset.href === href); }); $("artifactLabel").textContent = label; $("artifactOpen").href = href; $("artifactMeta").textContent = href; try { if (kind === "image") { $("artifactViewer").innerHTML = `${escapeHtml(label)}`; return; } if (href.endsWith(".html")) { $("artifactViewer").innerHTML = ``; return; } const response = await fetch(`${href}?t=${Date.now()}`, { cache: "no-store" }); if (!response.ok) throw new Error(`${href} -> ${response.status}`); const text = await response.text(); let formatted = text; if (href.endsWith(".json")) { try { formatted = JSON.stringify(JSON.parse(text), null, 2); } catch (_error) { } } $("artifactViewer").innerHTML = `
${escapeHtml(formatted)}
`; } catch (error) { $("artifactViewer").innerHTML = `
Artifact load failed: ${escapeHtml(error.message)}
`; } } function renderDetail() { const run = state.runs.find((item) => item.run_id === state.selectedRunId); if (!run) { $("detailRoot").innerHTML = `
Select a run to inspect full timeline, logs, sources, and reasoning.
`; return; } const advisory = run.advisory_meta || {}; const profile = run.profile_meta || {}; const screenshotItems = (run.artifact_groups || []) .find((group) => group.key === "browser") ?.items.filter((item) => item.kind === "image") || []; $("detailRoot").innerHTML = `
Local Verification Workspace
${escapeHtml(run.verification_status)}
${escapeHtml(run.system_id)} ${escapeHtml(run.repro_profile_id)} ${escapeHtml(run.artifact_mode)} ${escapeHtml(run.verification_mode)} ${escapeHtml(run.target_env || "local-docker")}

${escapeHtml(advisory.title || run.advisory_id)}

${escapeHtml(advisory.summary || "No summary available.")}

Timeline Steps${escapeHtml(run.timeline?.length || 0)}
Artifacts${escapeHtml((run.artifact_groups || []).reduce((sum, group) => sum + group.count, 0))}
Browser${run.browser_evidence?.present ? "Ready" : (run.browser_evidence?.required ? "Required" : "Optional")}
Finished${escapeHtml(timeAgo(run.finished_at))}
Progress Timeline${escapeHtml(run.timeline?.length || 0)} steps
${renderProgressStrip(run.progress)} ${renderStageCards(run)}
${(run.timeline || []).map((item) => `
${escapeHtml(item.at || "-")}
${escapeHtml(item.step || "-")}
${escapeHtml(item.status || "unknown")}
${escapeHtml(item.detail || "-")}
`).join("") || `
No timeline items available.
`}
Attack Plan & Reasoning${escapeHtml(profile.vuln_family || "unknown")}
${run.blocked_reason ? `
Failure reason
${escapeHtml(run.blocked_reason)}
` : ""}
destructive risk ${escapeHtml(profile.destructive_risk || "-")} cleanup ${escapeHtml(profile.cleanup_policy || "-")} targets ${(profile.allowed_target_types || []).join(", ") || "-"}
${(run.reasoning_lines || []).map((line) => `
${escapeHtml(line)}
`).join("")}
${(profile.success_criteria || []).map((line) => `${escapeHtml(line)}`).join("")}
Evidence Explorer${escapeHtml((run.artifact_groups || []).length)} groups
${(run.artifact_groups || []).map((group) => `

${escapeHtml(group.label)} · ${escapeHtml(group.count)}

${group.items.map((item) => ` `).join("")}
`).join("") || `
No artifacts linked for this run.
`} ${screenshotItems.length ? ` ` : ""}
Live Log Viewer${state.selectedArtifact ? "active" : "idle"}
${escapeHtml(state.selectedArtifact?.label || "Select an artifact")}
${escapeHtml(state.selectedArtifact?.href || "Artifacts and logs can be previewed here.")}
Open artifact
Select a report, log, JSON, screenshot, or timeline file to preview it here.
Sources & Fix Topics${escapeHtml((advisory.secondary_source_urls || []).length + (advisory.official_source_url ? 1 : 0))} links
${(advisory.aliases || []).map((alias) => `${escapeHtml(alias)}`).join("")}
${advisory.official_source_url ? `${escapeHtml(advisory.official_source_url)}` : `
No official source linked.
`} ${(advisory.secondary_source_urls || []).map((ref) => `${escapeHtml(ref)}`).join("")}
${(advisory.secure_code_topics || []).map((topic) => `${escapeHtml(topic)}`).join("")}
Run JSONraw
${escapeHtml(JSON.stringify(run, null, 2))}
Advisory JSONraw
${escapeHtml(JSON.stringify(advisory, null, 2))}
Profile JSONraw
${escapeHtml(JSON.stringify(profile, null, 2))}
`; document.querySelectorAll(".artifact-button").forEach((button) => { button.addEventListener("click", () => openArtifact(button.dataset.href, button.dataset.label, button.dataset.kind)); }); $("refreshArtifact")?.addEventListener("click", () => { if (state.selectedArtifact) { openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind); } }); if (!state.selectedArtifact || !(run.artifact_groups || []).some((group) => group.items.some((item) => item.href === state.selectedArtifact.href))) { const artifact = defaultArtifact(run); if (artifact) { openArtifact(artifact.href, artifact.label, artifact.kind); } } else { openArtifact(state.selectedArtifact.href, state.selectedArtifact.label, state.selectedArtifact.kind); } } function attachGlobalActions() { $("searchInput").addEventListener("input", (event) => { state.filters.search = String(event.target.value || "").trim().toLowerCase(); renderRunList(); }); [["systemFilter", "system"], ["statusFilter", "status"], ["familyFilter", "family"]].forEach(([id, key]) => { $(id).addEventListener("input", (event) => { state.filters[key] = String(event.target.value || ""); renderRunList(); }); }); $("refreshDashboard").addEventListener("click", () => loadData(false)); $("autoRefresh").addEventListener("change", (event) => { state.autoRefresh = Boolean(event.target.checked); startRefreshLoop(); }); } function startRefreshLoop() { if (state.refreshHandle) { clearInterval(state.refreshHandle); state.refreshHandle = null; } if (!state.autoRefresh) return; state.refreshHandle = setInterval(() => loadData(true), state.refreshMs); } async function init() { ["systemFilter", "statusFilter", "familyFilter"].forEach((id) => { $(id).dataset.base = $(id).innerHTML; }); attachGlobalActions(); await loadData(false); startRefreshLoop(); window.addEventListener("hashchange", () => loadData(false)); } document.addEventListener("DOMContentLoaded", init); """ write_text(assets_dir / "app.js", app_js) html_page = """ websafe authorized lab dashboard
Authorized Lab Dashboard

本地攻防实证工作台

面向授权实验场景的本地静态前端。聚合 advisory、run bundle、日志、浏览器证据、失败原因、利用思路与源头信息,并支持可折叠细节与自动刷新。

Open Summary JSON Open Feature Docs

Sync State

BootingLoading generated JSON
Select a run to inspect full details.
""" write_text(DASHBOARD_DIR / "index.html", html_page) return { "dashboard_dir": str(DASHBOARD_DIR), "index_html": str(DASHBOARD_DIR / "index.html"), }