Files
Nexus/scripts/generate_bug_scan_registry.py
T
2026-07-08 22:31:31 +08:00

571 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate docs/audit/2026-06-04-bug-scan-registry.json — hand-written source SSOT."""
from __future__ import annotations
import json
import re
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "docs/audit/2026-06-04-bug-scan-registry.json"
# Batch 16 coverage (path relative to ROOT)
BATCH_1_6 = {
"server/api/install.py": {"batch_id": 1, "status": "done", "needs_full_rescan": False},
"web/app/install.html": {"batch_id": 1, "status": "done", "needs_full_rescan": False},
"server/application/services/script_job_callback.py": {
"batch_id": 1,
"status": "done",
"needs_full_rescan": False,
},
"server/api/agent.py": {"batch_id": 1, "status": "partial", "needs_full_rescan": True},
"server/api/servers.py": {"batch_id": 1, "status": "partial", "needs_full_rescan": True},
"server/background/schedule_runner.py": {"batch_id": 2, "status": "done", "needs_full_rescan": False},
"server/application/services/auth_service.py": {
"batch_id": 2,
"status": "partial",
"needs_full_rescan": True,
},
"server/application/services/sync_engine_v2.py": {
"batch_id": 2,
"status": "partial",
"needs_full_rescan": True,
},
"server/infrastructure/redis/script_execution_store.py": {
"batch_id": 2,
"status": "partial",
"needs_full_rescan": True,
},
"frontend/src/composables/terminal/useTerminalSessions.ts": {
"batch_id": 2,
"status": "partial",
"needs_full_rescan": True,
},
"server/background/retry_runner.py": {"batch_id": 3, "status": "done", "needs_full_rescan": False},
"server/application/services/files_browse_service.py": {
"batch_id": 3,
"status": "partial",
"needs_full_rescan": True,
},
"frontend/src/pages/DashboardPage.vue": {
"batch_id": 3,
"status": "partial",
"needs_full_rescan": True,
},
"server/api/sync_v2.py": {"batch_id": 4, "status": "partial", "needs_full_rescan": True},
"server/api/files.py": {"batch_id": 4, "status": "done", "needs_full_rescan": False},
"server/background/heartbeat_flush.py": {
"batch_id": 4,
"status": "partial",
"needs_full_rescan": True,
},
"frontend/src/composables/push/usePushProgress.ts": {
"batch_id": 4,
"status": "done",
"needs_full_rescan": False,
},
"server/api/schemas.py": {"batch_id": 4, "status": "partial", "needs_full_rescan": True},
"server/background/self_monitor.py": {"batch_id": 5, "status": "done", "needs_full_rescan": False},
"server/api/auth_jwt.py": {"batch_id": 6, "status": "done", "needs_full_rescan": False},
"server/background/script_execution_flush.py": {
"batch_id": 6,
"status": "done",
"needs_full_rescan": False,
},
"frontend/src/composables/files/useFilesBrowse.ts": {
"batch_id": 6,
"status": "done",
"needs_full_rescan": False,
},
"frontend/src/stores/files.ts": {"batch_id": 6, "status": "done", "needs_full_rescan": False},
}
EXCLUDED_JUNK = {"scripts/sedul3hFb"}
# Local secrets — not in repo SSOT; example only in docker/.env.example
EXCLUDED_LOCAL = {"docker/.env", ".env"}
LARGE_FILE_THRESHOLD = 800
# Planned batch assignments (relative path -> batch_id)
PLANNED_BATCHES: dict[str, int] = {}
def _add_batch(batch_id: int, paths: list[str]) -> None:
for p in paths:
PLANNED_BATCHES[p] = batch_id
# Phase A B7-B16
_add_batch(7, ["server/api/servers.py"])
_add_batch(8, ["server/api/sync_v2.py"])
_add_batch(
9,
[
"server/api/webssh.py",
"server/api/auth.py",
"server/api/websocket.py",
"server/api/terminal.py",
"server/api/settings.py",
],
)
_add_batch(
10,
[
"server/api/scripts.py",
"server/api/search.py",
"server/api/assets.py",
"server/api/files.py",
"server/api/health.py",
],
)
_add_batch(
11,
[
"server/api/dependencies.py",
"server/api/remote_path_validation.py",
"server/api/schema_path_validators.py",
"server/api/schemas.py",
"server/api/__init__.py",
],
)
_add_batch(12, ["server/api/install.py"])
_add_batch(13, ["server/api/agent.py"])
_add_batch(14, ["server/api/auth_jwt.py"])
# Phase B B17-B24
_add_batch(17, ["server/application/services/script_service.py"])
_add_batch(18, ["server/application/services/sync_engine_v2.py"])
_add_batch(19, ["server/application/services/auth_service.py"])
_add_batch(
20,
[
"server/application/services/script_job_callback.py",
"server/application/services/files_browse_service.py",
"server/application/services/server_service.py",
"server/application/services/sync_service.py",
"server/application/services/script_jobs.py",
],
)
_add_batch(
21,
[
"server/application/__init__.py",
"server/application/services/__init__.py",
],
)
_add_batch(
22,
[
"server/background/schedule_runner.py",
"server/background/retry_runner.py",
"server/background/heartbeat_flush.py",
"server/background/self_monitor.py",
"server/background/script_execution_flush.py",
],
)
_add_batch(
23,
[
"server/background/ip_allowlist_refresh.py",
"server/background/upload_staging_cleanup.py",
"server/background/__init__.py",
],
)
_add_batch(24, ["server/main.py", "server/config.py", "server/__init__.py"])
# Phase C B25-B34 — filled dynamically below after collecting infra paths
INFRA_DB = sorted(
[
"server/infrastructure/database/admin_repo.py",
"server/infrastructure/database/audit_log_repo.py",
"server/infrastructure/database/crypto.py",
"server/infrastructure/database/db_credential_repo.py",
"server/infrastructure/database/engine_compat.py",
"server/infrastructure/database/migrations.py",
"server/infrastructure/database/password_preset_repo.py",
"server/infrastructure/database/platform_node_repo.py",
"server/infrastructure/database/push_schedule_repo.py",
"server/infrastructure/database/script_repo.py",
"server/infrastructure/database/server_repo.py",
"server/infrastructure/database/session.py",
"server/infrastructure/database/setting_repo.py",
"server/infrastructure/database/ssh_key_preset_repo.py",
"server/infrastructure/database/ssh_session_repo.py",
"server/infrastructure/database/sync_log_repo.py",
"server/infrastructure/database/__init__.py",
]
)
INFRA_SSH = sorted(
[
"server/infrastructure/ssh/asyncssh_pool.py",
"server/infrastructure/ssh/files_capability.py",
"server/infrastructure/ssh/remote_archive.py",
"server/infrastructure/ssh/remote_probe.py",
"server/infrastructure/ssh/remote_shell.py",
"server/infrastructure/ssh/__init__.py",
]
)
INFRA_OTHER = sorted(
[
"server/infrastructure/redis/client.py",
"server/infrastructure/redis/script_callback_rate.py",
"server/infrastructure/redis/script_execution_store.py",
"server/infrastructure/redis/__init__.py",
"server/infrastructure/telegram/__init__.py",
"server/infrastructure/agent_url.py",
"server/infrastructure/subscription_parser.py",
"server/infrastructure/__init__.py",
]
)
DOMAIN_UTILS = sorted(
[
"server/domain/__init__.py",
"server/domain/models/__init__.py",
"server/domain/repositories/__init__.py",
"server/utils/files_chmod_policy.py",
"server/utils/files_elevation.py",
"server/utils/posix_paths.py",
"server/utils/sync_error_message.py",
"server/utils/text_io.py",
"server/utils/unix_ls.py",
]
)
def _chunk(paths: list[str], size: int) -> list[list[str]]:
return [paths[i : i + size] for i in range(0, len(paths), size)]
bid = 25
for chunk in _chunk(INFRA_DB, 5):
_add_batch(bid, chunk)
bid += 1
for chunk in _chunk(INFRA_SSH, 5):
_add_batch(bid, chunk)
bid += 1
_add_batch(bid, INFRA_OTHER)
bid += 1
for chunk in _chunk(DOMAIN_UTILS, 5):
_add_batch(bid, chunk)
bid += 1
# Phase D frontend — pages B35+
FRONTEND_PAGES = sorted((ROOT / "frontend/src/pages").glob("*.vue"))
FRONTEND_PAGES = [str(p.relative_to(ROOT)).replace("\\", "/") for p in FRONTEND_PAGES]
bid = 35
for chunk in _chunk(FRONTEND_PAGES, 5):
_add_batch(bid, chunk)
bid += 1
# composables — useTerminalSessions alone first (large)
_add_batch(bid, ["frontend/src/composables/terminal/useTerminalSessions.ts"])
bid += 1
COMPOSABLES = sorted(
(ROOT / "frontend/src/composables").rglob("*"),
key=lambda p: str(p),
)
COMPOSABLES = [
str(p.relative_to(ROOT)).replace("\\", "/")
for p in COMPOSABLES
if p.is_file() and p.suffix in {".ts", ".vue"}
and "useTerminalSessions.ts" not in p.name
]
for chunk in _chunk(COMPOSABLES, 5):
_add_batch(bid, chunk)
bid += 1
COMPONENTS = sorted(
(ROOT / "frontend/src/components").rglob("*.vue"),
key=lambda p: str(p),
)
COMPONENTS = [str(p.relative_to(ROOT)).replace("\\", "/") for p in COMPONENTS]
for chunk in _chunk(COMPONENTS, 5):
_add_batch(bid, chunk)
bid += 1
_add_batch(
bid,
[
"frontend/src/api/index.ts",
"frontend/src/stores/auth.ts",
"frontend/src/stores/files.ts",
"frontend/src/router/index.ts",
"frontend/src/App.vue",
],
)
bid += 1
FRONTEND_REST = sorted(
p
for p in (ROOT / "frontend/src").rglob("*")
if p.is_file()
and p.suffix in {".ts", ".scss"}
and "assets" not in p.parts
and p not in {ROOT / f for f in [
"frontend/src/api/index.ts",
"frontend/src/stores/auth.ts",
"frontend/src/stores/files.ts",
"frontend/src/router/index.ts",
"frontend/src/App.vue",
"frontend/src/main.ts",
]}
)
FRONTEND_REST_PATHS = []
for p in FRONTEND_REST:
rel = str(p.relative_to(ROOT)).replace("\\", "/")
if rel.startswith("frontend/src/pages/"):
continue
if rel.startswith("frontend/src/composables/"):
continue
if rel.startswith("frontend/src/components/"):
continue
if rel.endswith((".png", ".svg", ".jpg", ".ico")):
continue
FRONTEND_REST_PATHS.append(rel)
for chunk in _chunk(sorted(FRONTEND_REST_PATHS), 5):
_add_batch(bid, chunk)
bid += 1
if (ROOT / "frontend/src/main.ts").exists():
PLANNED_BATCHES.setdefault("frontend/src/main.ts", bid)
# Phase E
_add_batch(
bid,
[
"web/app/index.html",
"web/app/install.html",
"web/app/api.js",
"web/app/layout.js",
"web/app/preview.html",
],
)
bid += 1
_add_batch(
bid,
[
"web/app/preview-v3.html",
"web/app/preview-v4.html",
"web/app/preview-v5.html",
"web/agent/agent.py",
"web/agent/agent.sh",
],
)
bid += 1
_add_batch(
bid,
[
"web/agent/install.sh",
"web/agent/uninstall.sh",
"web/agent/config.example.json",
],
)
# Phase F tests
TESTS = sorted((ROOT / "tests").rglob("*.py"))
TESTS = [str(p.relative_to(ROOT)).replace("\\", "/") for p in TESTS]
for chunk in _chunk(TESTS, 5):
_add_batch(bid, chunk)
bid += 1
DEPLOY = sorted(
p
for p in (ROOT / "deploy").rglob("*")
if p.is_file() and p.name != "gate_log.jsonl"
)
DEPLOY = [str(p.relative_to(ROOT)).replace("\\", "/") for p in DEPLOY]
for chunk in _chunk(DEPLOY, 5):
_add_batch(bid, chunk)
bid += 1
SCRIPTS = sorted(
p
for p in (ROOT / "scripts").rglob("*")
if p.is_file() and p.name not in EXCLUDED_JUNK
)
SCRIPTS = [str(p.relative_to(ROOT)).replace("\\", "/") for p in SCRIPTS]
for chunk in _chunk(SCRIPTS, 5):
_add_batch(bid, chunk)
bid += 1
DOCKER_DEPLOY_ROOT = sorted(
[
"docker/generate_env.py",
"docker/sync_root_env.py",
"docker/entrypoint.sh",
"docker/.env.example",
"docker/README.md",
"Dockerfile",
".dockerignore",
]
)
_add_batch(bid, [p for p in DOCKER_DEPLOY_ROOT if (ROOT / p).exists()])
bid += 1
_add_batch(
bid,
[
"frontend/vite.config.mts",
"frontend/package.json",
"requirements.txt",
"requirements-dev.txt",
"ruff.toml",
],
)
def collect_must_audit() -> list[Path]:
paths: list[Path] = []
# server py
paths.extend(ROOT.glob("server/**/*.py"))
# frontend src
for p in (ROOT / "frontend/src").rglob("*"):
if not p.is_file():
continue
if p.suffix not in {".ts", ".vue", ".scss"}:
continue
if "assets" in p.parts and p.suffix in {".png", ".svg", ".jpg", ".ico"}:
continue
paths.append(p)
# web app root html/js
for name in ["*.html", "*.js"]:
paths.extend((ROOT / "web/app").glob(name))
# agent
paths.extend((ROOT / "web/agent").rglob("*"))
paths = [p for p in paths if p.is_file()]
# tests
paths.extend((ROOT / "tests").rglob("*.py"))
# deploy scripts docker (selected)
for p in (ROOT / "deploy").rglob("*"):
if p.is_file() and p.name != "gate_log.jsonl":
paths.append(p)
for p in (ROOT / "scripts").rglob("*"):
if p.is_file() and p.name not in EXCLUDED_JUNK:
paths.append(p)
for p in (ROOT / "docker").rglob("*"):
if p.is_file():
paths.append(p)
for extra in [
"Dockerfile",
".dockerignore",
"frontend/vite.config.mts",
"frontend/package.json",
"requirements.txt",
"requirements-dev.txt",
"ruff.toml",
]:
ep = ROOT / extra
if ep.exists():
paths.append(ep)
# dedupe
seen: set[str] = set()
out: list[Path] = []
for p in sorted(set(paths), key=lambda x: str(x)):
rel = str(p.relative_to(ROOT)).replace("\\", "/")
if rel in seen:
continue
seen.add(rel)
out.append(p)
return out
def line_count(path: Path) -> int:
try:
return sum(1 for _ in path.open("rb"))
except OSError:
return 0
def main() -> None:
must = collect_must_audit()
entries: list[dict] = []
covered_assets = 0
for path in sorted((ROOT / "web/app/assets").rglob("*"), key=lambda x: str(x)):
if not path.is_file():
continue
rel = str(path.relative_to(ROOT)).replace("\\", "/")
entries.append(
{
"path": rel,
"category": "generated",
"lines": line_count(path),
"batch_id": None,
"status": "covered-by-frontend-src",
"needs_full_rescan": False,
"planned_batch": "B-Assets",
}
)
covered_assets += 1
for path in must:
rel = str(path.relative_to(ROOT)).replace("\\", "/")
if rel in EXCLUDED_JUNK:
entries.append(
{
"path": rel,
"category": "excluded-junk",
"lines": line_count(path),
"batch_id": None,
"status": "excluded",
"needs_full_rescan": False,
}
)
continue
if rel in EXCLUDED_LOCAL:
entries.append(
{
"path": rel,
"category": "excluded-local",
"lines": line_count(path),
"batch_id": None,
"status": "excluded",
"needs_full_rescan": False,
"note": "local secrets; audit docker/.env.example instead",
}
)
continue
prev = BATCH_1_6.get(rel, {})
planned = PLANNED_BATCHES.get(rel)
n = line_count(path)
large = n > LARGE_FILE_THRESHOLD
entry = {
"path": rel,
"category": "must_audit",
"lines": n,
"large_file": large,
"batch_id": prev.get("batch_id") or planned,
"planned_batch": planned,
"status": prev.get("status", "pending"),
"needs_full_rescan": prev.get("needs_full_rescan", False),
"read_range": None,
"findings_count": 0,
}
entries.append(entry)
must_audit = [e for e in entries if e["category"] == "must_audit"]
pending = sum(1 for e in must_audit if e["status"] == "pending")
partial_rescan = sum(1 for e in must_audit if e.get("needs_full_rescan"))
done = sum(1 for e in must_audit if e["status"] == "done")
doc = {
"generated_at": datetime.now(timezone.utc).isoformat(),
"summary": {
"total_must_audit": len(must_audit),
"pending": pending,
"done": done,
"partial_rescan": partial_rescan,
"covered_by_frontend_src_assets": covered_assets,
"excluded_junk": len(EXCLUDED_JUNK),
"max_batch_id": max(PLANNED_BATCHES.values()) if PLANNED_BATCHES else 0,
},
"entries": entries,
}
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(doc, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print(json.dumps(doc["summary"], indent=2))
if __name__ == "__main__":
main()