571 lines
17 KiB
Python
571 lines
17 KiB
Python
#!/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 1–6 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()
|