Files
Nexus/scripts/generate_bug_scan_registry.py
T

571 lines
17 KiB
Python
Raw Normal View History

#!/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()