增强系统级实体覆盖摘要与工作台索引
这个提交包含在:
@@ -599,19 +599,55 @@ def build_entity_views(source_map: Dict[str, Any], advisories: List[AdvisoryReco
|
||||
"system_id": entity["root_system_id"],
|
||||
"display_name": systems.get(entity["root_system_id"], {}).get("display_name", entity["root_system_id"]),
|
||||
"cataloged_entity_total": 0,
|
||||
"child_entity_total": 0,
|
||||
"candidate_entity_total": 0,
|
||||
"workflow_complete_count": 0,
|
||||
"version_mapped_count": 0,
|
||||
"official_source_covered_count": 0,
|
||||
"history_full_complete_count": 0,
|
||||
"latest_green_count": 0,
|
||||
"version_gap_entity_count": 0,
|
||||
"workflow_gap_entity_count": 0,
|
||||
"plugin_total": 0,
|
||||
"entity_type_counts": {},
|
||||
"top_entities": [],
|
||||
"backlog_preview": [],
|
||||
},
|
||||
)
|
||||
summary["cataloged_entity_total"] += 1
|
||||
if entity["entity_type"] != "system":
|
||||
summary["child_entity_total"] += 1
|
||||
summary["entity_type_counts"][entity["entity_type"]] = summary["entity_type_counts"].get(entity["entity_type"], 0) + 1
|
||||
summary["workflow_complete_count"] += 1 if entity["advisory_count"] and entity["workflow_complete_advisory_count"] >= entity["advisory_count"] else 0
|
||||
summary["version_mapped_count"] += 1 if entity["advisory_count"] and entity["version_mapped_advisory_count"] >= entity["advisory_count"] else 0
|
||||
summary["official_source_covered_count"] += 1 if entity["official_source_covered"] else 0
|
||||
summary["history_full_complete_count"] += 1 if entity.get("history_backfill_status") == "complete" else 0
|
||||
summary["latest_green_count"] += 1 if entity.get("latest_sync_status") == "green" else 0
|
||||
summary["version_gap_entity_count"] += 1 if entity.get("advisory_count") and entity.get("version_mapped_advisory_count", 0) < entity.get("advisory_count", 0) else 0
|
||||
summary["workflow_gap_entity_count"] += 1 if entity.get("advisory_count") and entity.get("workflow_complete_advisory_count", 0) < entity.get("advisory_count", 0) else 0
|
||||
if entity["entity_type"] in PLUGINISH_ENTITY_TYPES:
|
||||
summary["plugin_total"] += 1
|
||||
|
||||
for system_id, summary in system_summary.items():
|
||||
ranked_entities = sorted(
|
||||
[
|
||||
entity
|
||||
for entity in entities.values()
|
||||
if entity["root_system_id"] == system_id and entity["entity_type"] != "system"
|
||||
],
|
||||
key=lambda item: (-(item.get("advisory_count") or 0), item["entity_type"], item["display_name"].lower()),
|
||||
)
|
||||
summary["top_entities"] = [
|
||||
{
|
||||
"entity_id": entity["entity_id"],
|
||||
"entity_type": entity["entity_type"],
|
||||
"display_name": entity["display_name"],
|
||||
"advisory_count": entity.get("advisory_count", 0),
|
||||
"history_backfill_status": entity.get("history_backfill_status"),
|
||||
"latest_sync_status": entity.get("latest_sync_status"),
|
||||
}
|
||||
for entity in ranked_entities[:5]
|
||||
]
|
||||
for candidate in candidate_backlog:
|
||||
system_summary.setdefault(
|
||||
candidate["root_system_id"],
|
||||
@@ -619,13 +655,31 @@ def build_entity_views(source_map: Dict[str, Any], advisories: List[AdvisoryReco
|
||||
"system_id": candidate["root_system_id"],
|
||||
"display_name": systems.get(candidate["root_system_id"], {}).get("display_name", candidate["root_system_id"]),
|
||||
"cataloged_entity_total": 0,
|
||||
"child_entity_total": 0,
|
||||
"candidate_entity_total": 0,
|
||||
"workflow_complete_count": 0,
|
||||
"version_mapped_count": 0,
|
||||
"official_source_covered_count": 0,
|
||||
"history_full_complete_count": 0,
|
||||
"latest_green_count": 0,
|
||||
"version_gap_entity_count": 0,
|
||||
"workflow_gap_entity_count": 0,
|
||||
"plugin_total": 0,
|
||||
"entity_type_counts": {},
|
||||
"top_entities": [],
|
||||
"backlog_preview": [],
|
||||
},
|
||||
)["candidate_entity_total"] += 1
|
||||
preview = system_summary[candidate["root_system_id"]]["backlog_preview"]
|
||||
if len(preview) < 5:
|
||||
preview.append(
|
||||
{
|
||||
"candidate_id": candidate["candidate_id"],
|
||||
"display_name": candidate["display_name"],
|
||||
"entity_type": candidate["entity_type"],
|
||||
"risk": candidate["risk"],
|
||||
}
|
||||
)
|
||||
|
||||
cataloged_entities = [entity for entity in entities.values() if entity.get("status") == "cataloged"]
|
||||
history_full_complete_count = len(
|
||||
|
||||
@@ -534,6 +534,7 @@ def render_registry(
|
||||
) -> None:
|
||||
run_map = latest_runs_by_advisory()
|
||||
entity_views = build_entity_views(source_map, advisories)
|
||||
entity_summary_map = {item["system_id"]: item for item in entity_views["completeness"]["systems"]}
|
||||
grouped: Dict[str, List[AdvisoryRecord]] = defaultdict(list)
|
||||
advisory_payloads: Dict[str, Dict[str, Any]] = {}
|
||||
for advisory in advisories:
|
||||
@@ -556,6 +557,7 @@ def render_registry(
|
||||
items = grouped.get(system_id, [])
|
||||
merged_items = [_merged_item(item, run_map) for item in items]
|
||||
counts = _status_counts(merged_items)
|
||||
entity_summary = entity_summary_map.get(system_id, {})
|
||||
system_payloads[f"{system_id}.json"] = {
|
||||
"system_id": system_id,
|
||||
"display_name": system["display_name"],
|
||||
@@ -571,6 +573,7 @@ def render_registry(
|
||||
"verified_synthetic": counts["verified_synthetic"],
|
||||
"blocked_count": counts["blocked"],
|
||||
"manual_count": counts["manual"],
|
||||
"entity_summary": entity_summary,
|
||||
"items": [item.canonical_id for item in sorted(items, key=lambda item: item.published_at or "", reverse=True)],
|
||||
}
|
||||
if selected_system_ids:
|
||||
|
||||
@@ -162,6 +162,7 @@ def validate(source_map: Dict[str, Any]) -> List[str]:
|
||||
GENERATED_DIR / "dashboard" / "advisories.json",
|
||||
GENERATED_DIR / "dashboard" / "profiles.json",
|
||||
GENERATED_DIR / "dashboard" / "architecture.json",
|
||||
GENERATED_DIR / "dashboard" / "entities.json",
|
||||
GENERATED_DIR / "dashboard" / "assets" / "app.js",
|
||||
GENERATED_DIR / "dashboard" / "assets" / "styles.css",
|
||||
GENERATED_DIR / "dashboard" / "assets" / "icons.svg",
|
||||
|
||||
@@ -65,6 +65,7 @@ const DATA_HUB_ITEMS = [
|
||||
{ title: "entity-queues.json", href: "/data/entity-queues.json", description: "discovery/history/latest/workflow 四类队列摘要。", badge: "json" },
|
||||
{ title: "runs.json", href: "/runs.json", description: "最近运行的结构化详情,可用于 UI 和调试。", badge: "json" },
|
||||
{ title: "systems.json", href: "/systems.json", description: "系统级覆盖、分类、更新时间和浏览器证据统计。", badge: "json" },
|
||||
{ title: "entities.json", href: "/entities.json", description: "分层实体索引、实体状态和系统归属。", badge: "json" },
|
||||
{ title: "advisories.json", href: "/advisories.json", description: "漏洞条目元数据、来源和 secure-code 主题。", badge: "json" },
|
||||
{ title: "profiles.json", href: "/profiles.json", description: "复现档案元数据、成功判据和 browser assertions。", badge: "json" },
|
||||
{ title: "architecture.json", href: "/architecture.json", description: "当前架构库的结构化真值。", badge: "json" },
|
||||
@@ -94,6 +95,7 @@ const state = {
|
||||
summary: null,
|
||||
runs: [],
|
||||
systems: [],
|
||||
entities: [],
|
||||
advisories: {},
|
||||
profiles: {},
|
||||
architecture: null,
|
||||
@@ -599,6 +601,9 @@ function renderSystemCards(items, compact = false) {
|
||||
const total = Math.max(Number(system.total || 0), 1);
|
||||
const verified = Number(system.verified_real || 0) + Number(system.verified_synthetic || 0);
|
||||
const coverage = Math.round((verified / total) * 100);
|
||||
const entitySummary = system.entity_summary || {};
|
||||
const topEntities = system.top_entities || [];
|
||||
const backlogPreview = system.backlog_preview || [];
|
||||
return `
|
||||
<article class="system-card ${compact ? "system-card-compact" : ""}">
|
||||
<div class="timeline-head">
|
||||
@@ -610,8 +615,28 @@ function renderSystemCards(items, compact = false) {
|
||||
<span class="tag">真实 ${escapeHtml(system.verified_real || 0)}</span>
|
||||
<span class="tag">合成 ${escapeHtml(system.verified_synthetic || 0)}</span>
|
||||
<span class="tag">阻塞 ${escapeHtml(system.blocked || 0)}</span>
|
||||
<span class="tag">实体 ${escapeHtml(entitySummary.cataloged_entity_total || 0)}</span>
|
||||
<span class="tag">backlog ${escapeHtml(entitySummary.candidate_entity_total || 0)}</span>
|
||||
</div>
|
||||
<div class="meter"><span style="--fill:${coverage}%"></span></div>
|
||||
${compact ? "" : `
|
||||
<div class="plan-grid" style="margin-top:12px;">
|
||||
<article class="plan-card">
|
||||
<span class="plan-label">实体覆盖</span>
|
||||
<div class="plan-copy">cataloged ${escapeHtml(entitySummary.cataloged_entity_total || 0)} · child ${escapeHtml(entitySummary.child_entity_total || 0)} · plugins ${escapeHtml(entitySummary.plugin_total || 0)}</div>
|
||||
</article>
|
||||
<article class="plan-card">
|
||||
<span class="plan-label">队列与缺口</span>
|
||||
<div class="plan-copy">history complete ${escapeHtml(entitySummary.history_full_complete_count || 0)} · latest green ${escapeHtml(entitySummary.latest_green_count || 0)} · version gap ${escapeHtml(entitySummary.version_gap_entity_count || 0)}</div>
|
||||
</article>
|
||||
</div>
|
||||
${(topEntities.length || backlogPreview.length) ? `
|
||||
<div class="tag-row" style="margin-top:10px;">
|
||||
${topEntities.map((item) => `<span class="tag">${escapeHtml(item.entity_type)} · ${escapeHtml(item.display_name)} · ${escapeHtml(item.advisory_count || 0)}</span>`).join("")}
|
||||
${backlogPreview.map((item) => `<span class="tag">${escapeHtml(item.entity_type)} backlog · ${escapeHtml(item.display_name)}</span>`).join("")}
|
||||
</div>
|
||||
` : ""}
|
||||
`}
|
||||
<div class="detail-actions" style="margin-top:12px;">
|
||||
<button class="button button-secondary button-small" type="button" data-filter-key="system" data-filter-value="${escapeHtml(system.system_id)}">锁定系统</button>
|
||||
<a class="button button-secondary button-small" href="${escapeHtml(buildUrl("runs", { system: system.system_id, run: null }))}">查看运行</a>
|
||||
@@ -1609,10 +1634,11 @@ async function loadData(preserveSelection = true) {
|
||||
renderSyncState("loading", "刷新中", `本地时间 ${new Date().toLocaleTimeString("zh-CN", { hour12: false })}`);
|
||||
|
||||
try {
|
||||
const [summary, runs, systems, advisories, profiles, architecture, completeness, entityCompleteness, sourceHealth, alerts, monitorSummary] = await Promise.all([
|
||||
const [summary, runs, systems, entities, advisories, profiles, architecture, completeness, entityCompleteness, sourceHealth, alerts, monitorSummary] = await Promise.all([
|
||||
fetchJson("/summary.json"),
|
||||
fetchJson("/runs.json"),
|
||||
fetchJson("/systems.json"),
|
||||
fetchJson("/entities.json"),
|
||||
fetchJson("/advisories.json"),
|
||||
fetchJson("/profiles.json"),
|
||||
fetchJson("/architecture.json"),
|
||||
@@ -1626,6 +1652,7 @@ async function loadData(preserveSelection = true) {
|
||||
state.summary = summary;
|
||||
state.runs = runs;
|
||||
state.systems = systems;
|
||||
state.entities = entities;
|
||||
state.advisories = advisories;
|
||||
state.profiles = profiles;
|
||||
state.architecture = architecture;
|
||||
|
||||
@@ -18,6 +18,7 @@ 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"]
|
||||
ROOT_JSON_FILES.append("architecture.json")
|
||||
ROOT_JSON_FILES.append("entities.json")
|
||||
SECTION_ROUTE_DIRS = ["overview", "runs", "systems", "architecture", "data"]
|
||||
|
||||
CATEGORY_LABELS = {
|
||||
@@ -570,6 +571,7 @@ def _build_architecture_data(summary: Dict[str, Any], source_map: Dict[str, Any]
|
||||
_link("entity-queues.json", "/data/entity-queues.json", "discovery/history/latest/workflow 四类队列摘要。"),
|
||||
_link("runs.json", "/runs.json", "最近 run 的结构化详情。"),
|
||||
_link("systems.json", "/systems.json", "系统级覆盖与浏览器证据摘要。"),
|
||||
_link("entities.json", "/entities.json", "分层实体索引、实体状态和系统归属。"),
|
||||
_link("advisories.json", "/advisories.json", "漏洞条目元数据与来源。"),
|
||||
_link("profiles.json", "/profiles.json", "复现档案元数据。"),
|
||||
_link("architecture.json", "/architecture.json", "当前架构库结构化 JSON。"),
|
||||
@@ -1363,12 +1365,17 @@ def render_dashboard(
|
||||
entity_completeness = read_json(ROOT / "08-threat-intel" / "generated" / "entity-completeness.json", default={}) or {}
|
||||
entity_backlog = read_json(ROOT / "08-threat-intel" / "generated" / "entity-discovery-backlog.json", default=[]) or []
|
||||
entity_queues = read_json(ROOT / "08-threat-intel" / "generated" / "entity-queues.json", default={}) or {}
|
||||
entity_records = load_json_dir(ROOT / "08-threat-intel" / "registry" / "entities")
|
||||
source_map = source_map_data if source_map_data is not None else (read_yaml(SOURCE_MAP_PATH, default={}) or {})
|
||||
repro_map = repro_map_data if repro_map_data is not None else (read_yaml(REPRO_MAP_PATH, default={}) or {})
|
||||
source_system_map = {item["system_id"]: item for item in source_map.get("systems", []) if item.get("system_id")}
|
||||
merged_advisories = _merge_latest_advisories(advisory_records, runs, source_system_map)
|
||||
advisory_map = {item["canonical_id"]: item for item in merged_advisories if item.get("canonical_id")}
|
||||
profile_map = load_profiles()
|
||||
entity_summary_map = {item.get("system_id"): item for item in (entity_completeness.get("systems") or []) if item.get("system_id")}
|
||||
entities_by_system: Dict[str, List[Dict[str, Any]]] = {}
|
||||
for item in sorted(entity_records, key=lambda value: (value.get("root_system_id") or "", value.get("entity_type") or "", value.get("display_name") or "")):
|
||||
entities_by_system.setdefault(item.get("root_system_id") or "", []).append(item)
|
||||
|
||||
_sync_run_bundles(runs)
|
||||
|
||||
@@ -1419,6 +1426,15 @@ def render_dashboard(
|
||||
elif status not in {"verified-synthetic"}:
|
||||
family_entry["manual"] += 1
|
||||
|
||||
for system_id, system in systems.items():
|
||||
entity_summary = entity_summary_map.get(system_id, {})
|
||||
system["entity_summary"] = entity_summary
|
||||
system["top_entities"] = entity_summary.get("top_entities", [])
|
||||
system["backlog_preview"] = entity_summary.get("backlog_preview", [])
|
||||
system["entity_total"] = entity_summary.get("cataloged_entity_total", 0)
|
||||
system["entity_backlog"] = entity_summary.get("candidate_entity_total", 0)
|
||||
system["entity_type_counts"] = entity_summary.get("entity_type_counts", {})
|
||||
|
||||
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:
|
||||
@@ -1532,6 +1548,7 @@ def render_dashboard(
|
||||
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 / "entities.json", entity_records)
|
||||
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_json(DASHBOARD_DIR / "data" / "completeness.json", completeness)
|
||||
|
||||
在新工单中引用
屏蔽一个用户