更新: 359 个文件 - 2026-03-16 23:30:01
这个提交包含在:
166
scripts/lab/render.py
普通文件
166
scripts/lab/render.py
普通文件
@@ -0,0 +1,166 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from lab.config import CASE_RUNS_DIR, DASHBOARD_DIR, RUNS_DIR
|
||||
from lab.utils import ensure_dir, load_json_dir, read_json, 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 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))
|
||||
|
||||
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"- Mermaid: [{timeline_path.name}]({timeline_path})",
|
||||
"",
|
||||
"## 证据摘要",
|
||||
"",
|
||||
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', []))}`",
|
||||
"",
|
||||
"## 最小化验证说明",
|
||||
"",
|
||||
"- 仅限自有资产、本地靶场或已授权实验目标。",
|
||||
"- 默认执行 minimal-proof;不会把破坏性或不可回滚动作作为默认路径。",
|
||||
"",
|
||||
]
|
||||
if run.get("browser_refs"):
|
||||
md_lines.extend(["## 浏览器证据", ""])
|
||||
for ref in run["browser_refs"]:
|
||||
md_lines.append(f"- {ref}")
|
||||
md_lines.append("")
|
||||
if run.get("container_log_refs"):
|
||||
md_lines.extend(["## 容器日志", ""])
|
||||
for ref in run["container_log_refs"]:
|
||||
md_lines.append(f"- {ref}")
|
||||
md_lines.append("")
|
||||
report_md = run_dir / "report.md"
|
||||
write_text(report_md, "\n".join(md_lines))
|
||||
|
||||
html_body = [
|
||||
"<!doctype html>",
|
||||
"<html><head><meta charset='utf-8'><title>websafe run report</title>",
|
||||
"<style>body{font-family:ui-monospace,Menlo,monospace;margin:2rem;line-height:1.5;} code,pre{background:#f5f5f5;padding:.2rem .4rem;} .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;} .card{border:1px solid #ddd;padding:1rem;border-radius:.5rem;}</style>",
|
||||
"</head><body>",
|
||||
f"<h1>Run {html.escape(run['run_id'])}</h1>",
|
||||
"<div class='grid'>",
|
||||
f"<div class='card'><strong>Advisory</strong><br><code>{html.escape(run['advisory_id'])}</code></div>",
|
||||
f"<div class='card'><strong>Status</strong><br><code>{html.escape(run['verification_status'])}</code></div>",
|
||||
f"<div class='card'><strong>Profile</strong><br><code>{html.escape(run['repro_profile_id'])}</code></div>",
|
||||
f"<div class='card'><strong>Artifact Mode</strong><br><code>{html.escape(run['artifact_mode'])}</code></div>",
|
||||
"</div>",
|
||||
"<h2>Mermaid Timeline</h2>",
|
||||
f"<pre>{html.escape(mermaid_from_steps(run))}</pre>",
|
||||
"<h2>Evidence</h2><ul>",
|
||||
]
|
||||
for ref in run.get("browser_refs", []) + run.get("container_log_refs", []) + run.get("request_log_refs", []):
|
||||
html_body.append(f"<li><code>{html.escape(ref)}</code></li>")
|
||||
html_body.extend(["</ul>", "</body></html>"])
|
||||
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)
|
||||
runs = load_json_dir(RUNS_DIR)
|
||||
summary = {
|
||||
"run_count": len(runs),
|
||||
"statuses": {},
|
||||
"recent_runs": sorted(runs, key=lambda item: item.get("finished_at") or "", reverse=True)[:50],
|
||||
}
|
||||
for item in runs:
|
||||
status = item.get("verification_status", "triage-manual")
|
||||
summary["statuses"][status] = summary["statuses"].get(status, 0) + 1
|
||||
write_json(DASHBOARD_DIR / "summary.json", summary)
|
||||
write_json(DASHBOARD_DIR / "runs.json", summary["recent_runs"])
|
||||
|
||||
html_page = """<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>websafe dashboard</title>
|
||||
<style>
|
||||
body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 2rem; background: #f8fafc; color: #0f172a; }
|
||||
h1, h2 { margin-bottom: .5rem; }
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin: 1rem 0 2rem; }
|
||||
.card { background: white; border: 1px solid #cbd5e1; border-radius: 14px; padding: 1rem; box-shadow: 0 4px 18px rgba(15,23,42,.06); }
|
||||
table { width: 100%%; border-collapse: collapse; background: white; border-radius: 12px; overflow: hidden; }
|
||||
th, td { padding: .75rem; border-bottom: 1px solid #e2e8f0; text-align: left; font-size: .92rem; }
|
||||
code { background: #e2e8f0; padding: .1rem .35rem; border-radius: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>websafe Local Lab Dashboard</h1>
|
||||
<p>LAB ONLY | AUTHORIZED TARGETS ONLY | 本地静态看板</p>
|
||||
<div id="summary" class="cards"></div>
|
||||
<h2>Recent Runs</h2>
|
||||
<table>
|
||||
<thead><tr><th>Run</th><th>Advisory</th><th>Status</th><th>Mode</th><th>Finished</th><th>Report</th></tr></thead>
|
||||
<tbody id="rows"></tbody>
|
||||
</table>
|
||||
<script>
|
||||
async function main() {
|
||||
const summary = await fetch('./summary.json').then(r => r.json());
|
||||
const runs = await fetch('./runs.json').then(r => r.json());
|
||||
const summaryRoot = document.getElementById('summary');
|
||||
const cards = [{label: 'Run Count', value: summary.run_count}];
|
||||
for (const [key, value] of Object.entries(summary.statuses)) {
|
||||
cards.push({label: key, value});
|
||||
}
|
||||
summaryRoot.innerHTML = cards.map(item => `<div class="card"><strong>${item.label}</strong><div style="font-size:2rem;margin-top:.5rem;">${item.value}</div></div>`).join('');
|
||||
const rows = document.getElementById('rows');
|
||||
rows.innerHTML = runs.map(item => {
|
||||
const report = item.report_refs && item.report_refs.report_html ? item.report_refs.report_html : '';
|
||||
return `<tr><td><code>${item.run_id}</code></td><td><code>${item.advisory_id}</code></td><td>${item.verification_status}</td><td>${item.verification_mode}</td><td>${item.finished_at || ''}</td><td>${report ? `<a href="../../../../${report.replace('/Users/x/websafe/', '')}">open</a>` : '-'}</td></tr>`;
|
||||
}).join('');
|
||||
}
|
||||
main();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
write_text(DASHBOARD_DIR / "index.html", html_page)
|
||||
return {
|
||||
"dashboard_dir": str(DASHBOARD_DIR),
|
||||
"index_html": str(DASHBOARD_DIR / "index.html"),
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户