656 行
26 KiB
Python
656 行
26 KiB
Python
from __future__ import annotations
|
|
|
|
import html
|
|
import json
|
|
import os
|
|
import shutil
|
|
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
|
|
|
|
|
|
TEMPLATES_DIR = ROOT / "scripts" / "lab" / "dashboard_templates"
|
|
LOVART_TEMPLATE_DIR = TEMPLATES_DIR / "lovart"
|
|
LEGACY_TEMPLATE_DIR = TEMPLATES_DIR / "legacy"
|
|
LOVART_VENDOR_MANIFEST = LOVART_TEMPLATE_DIR / "vendor" / "source-manifest.json"
|
|
ROOT_JSON_FILES = ["summary.json", "runs.json", "systems.json", "advisories.json", "profiles.json"]
|
|
|
|
|
|
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, body: str, description: str) -> str:
|
|
return f"""<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{html.escape(title)}</title>
|
|
<style>
|
|
:root {{
|
|
--bg: #08111f;
|
|
--panel: rgba(9, 18, 32, 0.9);
|
|
--border: rgba(137, 171, 214, 0.2);
|
|
--text: #f7fafc;
|
|
--muted: #9fb3ca;
|
|
--accent: #5eead4;
|
|
}}
|
|
* {{ box-sizing: border-box; }}
|
|
body {{
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
|
color: var(--text);
|
|
background:
|
|
radial-gradient(circle at top left, rgba(94, 234, 212, 0.12), transparent 26%),
|
|
linear-gradient(160deg, #050c16 0%, #091526 50%, #10233d 100%);
|
|
}}
|
|
main {{
|
|
max-width: 1080px;
|
|
margin: 0 auto;
|
|
padding: 32px 20px 40px;
|
|
}}
|
|
.panel {{
|
|
background: var(--panel);
|
|
border: 1px solid var(--border);
|
|
border-radius: 20px;
|
|
padding: 24px;
|
|
box-shadow: 0 24px 80px rgba(1, 7, 20, 0.45);
|
|
}}
|
|
.actions {{
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-bottom: 18px;
|
|
}}
|
|
.chip {{
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--border);
|
|
padding: 10px 14px;
|
|
color: var(--text);
|
|
background: rgba(255,255,255,0.05);
|
|
text-decoration: none;
|
|
}}
|
|
.chip:hover {{ border-color: rgba(94, 234, 212, 0.42); }}
|
|
h1 {{
|
|
margin: 0 0 12px;
|
|
font-family: "IBM Plex Serif", Georgia, serif;
|
|
font-size: clamp(1.8rem, 4vw, 3rem);
|
|
line-height: 1.08;
|
|
}}
|
|
.meta {{
|
|
color: var(--muted);
|
|
margin-bottom: 18px;
|
|
}}
|
|
pre {{
|
|
margin: 0;
|
|
padding: 20px;
|
|
overflow: auto;
|
|
border-radius: 16px;
|
|
border: 1px solid rgba(137, 171, 214, 0.12);
|
|
background: rgba(2, 8, 22, 0.84);
|
|
color: #d6e5f5;
|
|
font-family: "IBM Plex Mono", "SFMono-Regular", monospace;
|
|
font-size: 0.92rem;
|
|
line-height: 1.6;
|
|
white-space: pre-wrap;
|
|
}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<div class="panel">
|
|
<div class="actions">
|
|
<a class="chip" href="../index.html">Back to dashboard</a>
|
|
</div>
|
|
<h1>{html.escape(title)}</h1>
|
|
<div class="meta">{html.escape(description)}</div>
|
|
<pre>{html.escape(body)}</pre>
|
|
</div>
|
|
</main>
|
|
</body>
|
|
</html>
|
|
"""
|
|
|
|
|
|
def _remove_path(path: Path) -> None:
|
|
if not path.exists() and not path.is_symlink():
|
|
return
|
|
if path.is_symlink() or path.is_file():
|
|
path.unlink()
|
|
return
|
|
shutil.rmtree(path)
|
|
|
|
|
|
def _sync_symlink(target: Path, link_path: Path) -> None:
|
|
ensure_dir(link_path.parent)
|
|
relative_target = os.path.relpath(target, link_path.parent)
|
|
if link_path.is_symlink() and os.readlink(link_path) == relative_target:
|
|
return
|
|
_remove_path(link_path)
|
|
os.symlink(relative_target, link_path, target_is_directory=target.is_dir())
|
|
|
|
|
|
def _copy_tree(source: Path, destination: Path) -> None:
|
|
ensure_dir(destination)
|
|
for path in source.rglob("*"):
|
|
relative = path.relative_to(source)
|
|
target = destination / relative
|
|
if path.is_dir():
|
|
ensure_dir(target)
|
|
continue
|
|
ensure_dir(target.parent)
|
|
shutil.copy2(path, target)
|
|
|
|
|
|
def _write_dashboard_docs() -> None:
|
|
docs_dir = DASHBOARD_DIR / "docs"
|
|
ensure_dir(docs_dir)
|
|
sources = [
|
|
(
|
|
"project-features.html",
|
|
"项目功能与特性总览",
|
|
(ROOT / "docs" / "project-features.md").read_text(encoding="utf-8"),
|
|
"Dashboard-local mirror of the repo feature guide.",
|
|
),
|
|
(
|
|
"frontend-dashboard-design.html",
|
|
"本地前端工作台设计文档",
|
|
(ROOT / "docs" / "frontend-dashboard-design.md").read_text(encoding="utf-8"),
|
|
"Dashboard-local mirror of the UI and interaction specification.",
|
|
),
|
|
(
|
|
"secure-code-index.html",
|
|
"安全编码修复库索引",
|
|
(ROOT / "05-defense" / "secure-code" / "INDEX.md").read_text(encoding="utf-8"),
|
|
"Dashboard-local mirror of the secure-code library index.",
|
|
),
|
|
]
|
|
|
|
manifest_body = LOVART_VENDOR_MANIFEST.read_text(encoding="utf-8") if LOVART_VENDOR_MANIFEST.exists() else "{}"
|
|
sources.append(
|
|
(
|
|
"design-source.html",
|
|
"Lovart 设计来源与本地化清单",
|
|
manifest_body,
|
|
"Local vendor manifest for the Lovart-derived dashboard shell.",
|
|
)
|
|
)
|
|
|
|
for filename, title, body, description in sources:
|
|
write_text(docs_dir / filename, _dashboard_doc_page(title, body, description))
|
|
|
|
|
|
def _write_design_source_manifest() -> None:
|
|
assets_dir = DASHBOARD_DIR / "assets"
|
|
ensure_dir(assets_dir)
|
|
manifest = json.loads(LOVART_VENDOR_MANIFEST.read_text(encoding="utf-8")) if LOVART_VENDOR_MANIFEST.exists() else {}
|
|
write_json(assets_dir / "design-source.json", manifest)
|
|
|
|
|
|
def _render_root_dashboard_shell() -> None:
|
|
assets_dir = DASHBOARD_DIR / "assets"
|
|
ensure_dir(assets_dir)
|
|
for filename in ("index.html",):
|
|
shutil.copy2(LOVART_TEMPLATE_DIR / filename, DASHBOARD_DIR / filename)
|
|
_copy_tree(LOVART_TEMPLATE_DIR / "assets", assets_dir)
|
|
|
|
|
|
def _render_legacy_dashboard_shell() -> None:
|
|
legacy_dir = DASHBOARD_DIR / "legacy"
|
|
_remove_path(legacy_dir)
|
|
ensure_dir(legacy_dir)
|
|
shutil.copy2(LEGACY_TEMPLATE_DIR / "index.html", legacy_dir / "index.html")
|
|
_copy_tree(LEGACY_TEMPLATE_DIR / "assets", legacy_dir / "assets")
|
|
for json_name in ROOT_JSON_FILES:
|
|
_sync_symlink(DASHBOARD_DIR / json_name, legacy_dir / json_name)
|
|
_sync_symlink(DASHBOARD_DIR / "runs", legacy_dir / "runs")
|
|
_sync_symlink(DASHBOARD_DIR / "docs", legacy_dir / "docs")
|
|
|
|
|
|
def _sync_run_bundles(runs: List[Dict[str, Any]]) -> None:
|
|
runs_dir = DASHBOARD_DIR / "runs"
|
|
ensure_dir(runs_dir)
|
|
for item in runs:
|
|
bundle_dir = Path(item.get("report_refs", {}).get("bundle_dir", ""))
|
|
if not bundle_dir.exists():
|
|
continue
|
|
_sync_symlink(bundle_dir, runs_dir / item["run_id"])
|
|
|
|
|
|
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"")
|
|
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 = [
|
|
"<!doctype html>",
|
|
"<html><head><meta charset='utf-8'><title>websafe run report</title>",
|
|
"<style>body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.55;background:#f8fafc;color:#0f172a;} code,pre{background:#e2e8f0;padding:.2rem .4rem;border-radius:.3rem;} pre{white-space:pre-wrap;} .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;} .card{border:1px solid #cbd5e1;padding:1rem;border-radius:.75rem;background:#fff;} table{width:100%;border-collapse:collapse;background:#fff;border:1px solid #cbd5e1;border-radius:.75rem;overflow:hidden;} th,td{padding:.75rem;border-bottom:1px solid #e2e8f0;text-align:left;vertical-align:top;} img{max-width:100%;border:1px solid #cbd5e1;border-radius:.5rem;} .gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:1rem;}</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>Timeline</h2>",
|
|
"<table><thead><tr><th>Time</th><th>Step</th><th>Status</th><th>Detail</th></tr></thead><tbody>",
|
|
]
|
|
if run.get("timeline"):
|
|
for item in run["timeline"]:
|
|
html_body.append(
|
|
"<tr>"
|
|
f"<td><code>{html.escape(item.get('at', ''))}</code></td>"
|
|
f"<td><code>{html.escape(item.get('step', ''))}</code></td>"
|
|
f"<td><code>{html.escape(item.get('status', ''))}</code></td>"
|
|
f"<td>{html.escape(item.get('detail', '') or '-')}</td>"
|
|
"</tr>"
|
|
)
|
|
html_body.extend(["</tbody></table>", "<h2>Attack Steps</h2>", "<table><thead><tr><th>Tool</th><th>Status</th><th>Output</th></tr></thead><tbody>"])
|
|
if run.get("attack_steps"):
|
|
for step in run["attack_steps"]:
|
|
html_body.append(
|
|
"<tr>"
|
|
f"<td><code>{html.escape(step.get('tool') or step.get('kind') or '-')}</code></td>"
|
|
f"<td><code>{html.escape(step.get('status', '-'))}</code></td>"
|
|
f"<td><code>{html.escape(step.get('result_path') or '-')}</code></td>"
|
|
"</tr>"
|
|
)
|
|
else:
|
|
html_body.append("<tr><td><code>-</code></td><td><code>skipped</code></td><td><code>no attack steps</code></td></tr>")
|
|
html_body.extend(["</tbody></table>"])
|
|
if relative_screenshots:
|
|
html_body.extend(["<h2>Browser Screenshots</h2>", "<div class='gallery'>"])
|
|
for ref in relative_screenshots:
|
|
html_body.append(
|
|
f"<figure><img src='{html.escape(ref)}' alt='{html.escape(Path(ref).stem)}'><figcaption><code>{html.escape(ref)}</code></figcaption></figure>"
|
|
)
|
|
html_body.append("</div>")
|
|
html_body.extend(["<h2>Evidence</h2><ul>"])
|
|
for ref in run.get("compose_refs", []) + run.get("browser_refs", []) + run.get("container_log_refs", []) + run.get("request_log_refs", []):
|
|
html_body.append(f"<li><code>{html.escape(_relative_ref(run_dir, 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)
|
|
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()
|
|
|
|
_sync_run_bundles(runs)
|
|
|
|
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 entry: (-entry["total"], entry["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()})
|
|
|
|
_write_dashboard_docs()
|
|
_write_design_source_manifest()
|
|
_render_root_dashboard_shell()
|
|
_render_legacy_dashboard_shell()
|
|
|
|
return {
|
|
"dashboard_dir": str(DASHBOARD_DIR),
|
|
"index": str(DASHBOARD_DIR / "index.html"),
|
|
"legacy_index": str(DASHBOARD_DIR / "legacy" / "index.html"),
|
|
"summary_json": str(DASHBOARD_DIR / "summary.json"),
|
|
}
|