Files
thePlug/docs/2026-05-06-codebase-audit.md
T
2026-05-07 09:21:45 +02:00

12 KiB

thePlug Codebase Audit

Date: 2026-05-06

Checks run:

  • cd backend && uv run pytest -q
  • cd backend && uv run ruff check .
  • cd backend && uv run basedpyright app
  • npm --prefix frontend run build

Summary:

  • Functional checks currently pass: backend tests are green, Ruff is clean, frontend builds.
  • Structural quality is much weaker than the green CI surface suggests.
  • The biggest risks are runtime mutation safety around Docker compose rewrites, false confidence in the backend test suite, packaging/type-check fragility, and a very large amount of stateful coordination concentrated in single files.

Findings

Critical

1. Docker port rewrite is not exception-safe and can leave challenge services mutated on disk

  • Files:
    • backend/app/integrations/docker/editor.py:14-51
    • backend/app/services/mitmproxy/manager.py:195-236
  • Why it matters:
    • change_port() rewrites the compose file in place, then runs docker compose up -d.
    • There is no try/finally around the file mutation path.
    • If subprocess startup, YAML serialization, file writes, or any unexpected exception happens after the compose file has been rewritten, the original file is not guaranteed to be restored.
    • In practice, a failed proxy start/stop/recovery can leave the challenge service permanently remapped until manual cleanup.
  • Recommended fix:
    • Make compose rewrite transactional: read once, write temp file, atomically replace, and always restore in finally on failure.
    • Treat backup creation/restoration as part of a single transaction, not best effort.
    • Add integration tests that force failures between rewrite and docker compose up.

2. The backend test suite gives false confidence because most API behavior is mocked at the router seam

  • Files:
    • backend/tests/conftest.py:40-69
    • backend/tests/api/test_proxy.py
  • Why it matters:
    • The API suite mounts a bespoke FastAPI() app instead of the real application lifecycle.
    • Authentication is bypassed through dependency overrides.
    • Core router behavior is tested by monkeypatching repository/service functions directly on the module under test.
    • This catches serialization and status codes, but misses real dependency wiring, startup/shutdown behavior, Mongo access, WebSocket/auth interplay, and real process/runtime interactions.
  • Recommended fix:
    • Split tests into unit, integration, and system/perf layers.
    • Keep monkeypatch for narrow fault injection only.
    • Add real-app integration tests with actual lifespan, real Mongo-backed repositories, and realistic temp runtime directories.

High

3. Log streaming drops data after truncation or rotation

  • File:
    • backend/app/api/proxy/router.py:188-213
  • Why it matters:
    • On truncation, the stream sets offset = size.
    • If the log file is rotated or recreated with fresh content smaller than the old offset, the new bytes already present in the new file are skipped forever.
    • This is exactly the kind of subtle bug that makes operators distrust live logs.
  • Recommended fix:
    • When size < offset, reset to 0 or resend a fresh snapshot and continue from the new file start.
    • Add a regression test that simulates truncation/recreation with immediate new log bytes.

4. The health monitor interval drifts under load

  • File:
    • backend/app/services/mitmproxy/health_monitor.py:30-49
  • Why it matters:
    • The loop sleeps after the cycle completes.
    • Real interval becomes check duration + HEALTHCHECK_INTERVAL_SECONDS.
    • As proxy count or timeout frequency rises, checks become slower and the dashboard silently becomes stale.
  • Recommended fix:
    • Switch to fixed-schedule cadence using monotonic deadlines.
    • Track cycle duration and lag explicitly for observability.

5. Port reassignment in reverse mode has a race window and no reservation mechanism

  • Files:
    • backend/app/integrations/docker/ports.py:4-13
    • backend/app/services/mitmproxy/manager.py:195-215
  • Why it matters:
    • get_random_port() discovers a free port and returns it, but does not reserve it.
    • Another process can claim the port before Docker rebinds the service.
    • The failure mode is intermittent and hard to reproduce, which makes it a good candidate for production-only flakiness.
  • Recommended fix:
    • Move to a reservation strategy or let Docker select the port and then discover the assigned binding.
    • At minimum, retry on bind collision and record the failure reason explicitly.

6. Backend packaging/import structure is fragile and currently fails full static analysis badly

  • Files:
    • Broadly across backend/app/**
  • Evidence:
    • uv run basedpyright app reports 86 errors and 243 warnings.
  • Why it matters:
    • The codebase relies on pythonpath = ["app"] and implicit absolute imports such as from core.config import settings.
    • This is brittle outside the current dev/test invocation shape and blocks meaningful type-checking from being trustworthy.
    • Static type health is effectively unusable right now.
  • Recommended fix:
    • Convert backend imports to package-consistent absolute imports from app... or explicit relative imports.
    • Add a real basedpyright target/config to CI once import hygiene is fixed.

7. The frontend control plane is a God object that now owns too many concerns

  • File:
    • frontend/src/composables/useControlPlane.ts (1534 LOC)
  • Why it matters:
    • One composable currently owns auth, routing, discovery, drafts, editor save flow, templates, logs, health, websocket reconnection, notices, modal state, and error routing.
    • This is a high-risk shape for regressions because unrelated fixes share the same reactive graph and watcher set.
    • State restoration and race-condition fixes will keep getting harder, not easier.
  • Recommended fix:
    • Split into focused composables:
      • session/auth
      • proxy workspace state
      • discovery state
      • logs stream state
      • health stream state
      • wizard/settings draft state
    • Keep one thin composition root that wires them together.

8. Full refresh is all-or-nothing, so one failing endpoint blocks unrelated UI surfaces

  • File:
    • frontend/src/composables/useControlPlane.ts:648-673
  • Why it matters:
    • refreshAll() uses one Promise.all() for proxies, services, options, and templates.
    • If one endpoint fails, none of the others update.
    • A temporary failure in options or templates can block the basic proxy/discovery view.
  • Recommended fix:
    • Separate critical and non-critical refreshes.
    • Fetch proxies/services independently from options/templates and degrade per panel.

Medium

9. Template API and UI contract are inconsistent

  • Files:
    • backend/app/models/template.py:22-39
    • frontend/src/composables/useControlPlane.ts:1098-1126
    • frontend/src/composables/useControlPlane.ts:483-516
  • Why it matters:
    • Backend models allow arbitrary protocol values and default to "custom".
    • The UI only exposes http and tcp lists.
    • A custom template persisted with any other protocol value becomes effectively invisible in the current UI.
  • Recommended fix:
    • Validate template protocol/category on the backend to match supported UI semantics.
    • Or explicitly support additional categories in the UI instead of silently hiding them.

10. Template behavior is inconsistent with a predictable “block” contract

  • File:
    • backend/app/services/mitmproxy/templates.py
  • Why it matters:
    • Built-in “block” templates do not behave consistently:
      • some flow.kill()
      • some return empty 200
      • some return 404
      • some return 413 or 405
    • This may be intentional per challenge type, but as a product surface it is hard to reason about and document.
  • Recommended fix:
    • Decide whether built-in templates are “semantic examples” or “operational policies”.
    • If they are operational policies, define a small set of standard outcomes and label deviations explicitly.

11. Synchronous filesystem work happens on the event loop in hot backend paths

  • Files:
    • backend/app/services/mitmproxy/manager.py:287-303
    • backend/app/api/proxy/router.py:177-180
    • backend/app/integrations/docker/editor.py:39-51
  • Why it matters:
    • read_bytes, write_text, YAML dump/write, and similar disk operations happen directly inside async request/runtime paths.
    • This is acceptable at tiny scale, but it will block the event loop during heavier operator usage or larger logs/files.
  • Recommended fix:
    • Move blocking file I/O behind asyncio.to_thread() or a thin async wrapper for runtime/log/config file operations.

12. Health history limit is caller-controlled without a server-side cap

  • File:
    • backend/app/api/proxy/router.py:387-397
  • Why it matters:
    • limit is accepted directly from query params and forwarded to Mongo.
    • A large limit can create unnecessary memory and serialization pressure.
  • Recommended fix:
    • Clamp the value server-side, for example 1..200 or similar.

13. app.state.services is stale mutable state with no real consumer value

  • Files:
    • backend/app/main.py:47-56
    • backend/app/api/proxy/router.py:221-230
  • Why it matters:
    • Service discovery results are written into app.state.services, but the state is not used as a trusted cache anywhere meaningful.
    • This adds mutable global state without clear ownership and can mislead future maintainers.
  • Recommended fix:
    • Remove it or turn it into an intentional cache with invalidation and a real read path.

14. Docker compose editor leaves backups behind and does not manage them as lifecycle artifacts

  • File:
    • backend/app/integrations/docker/editor.py:7-44
  • Why it matters:
    • Backup files are created every time a port rewrite occurs.
    • They are never cleaned up or versioned intentionally.
    • Over time this pollutes challenge folders and complicates manual debugging.
  • Recommended fix:
    • Either manage backups explicitly as runtime artifacts or stop generating long-lived .bak files per rewrite.

Low

15. Static Python highlighting in template preview is intentionally lightweight but very naive

  • File:
    • frontend/src/components/TemplateCodePreview.vue:1-40
  • Why it matters:
    • Regex-based highlighting on escaped HTML will mis-handle more complex Python constructs.
    • This is mostly cosmetic today, but it limits confidence in template readability as the library grows.
  • Recommended fix:
    • Accept it as a lightweight preview or replace it with a read-only CodeMirror/Lezer-based renderer if template complexity grows.

16. Oversized files are now a maintenance signal on their own

  • Evidence:
    • frontend/src/composables/useControlPlane.ts: 1534 LOC
    • frontend/src/styles.css: 1798 LOC
    • backend/app/api/proxy/router.py: 549 LOC
    • backend/app/services/mitmproxy/manager.py: 443 LOC
    • backend/tests/api/test_proxy.py: 859 LOC
  • Why it matters:
    • Even when behavior is correct, files of this size hide coupling and slow review/debug cycles.
  • Recommended fix:
    • Split by responsibility before the next major feature wave.

Roadmap

P0 — Correctness and operator safety

  • Make Docker compose port rewrites transactional and exception-safe.
  • Fix log stream truncation/rotation handling.
  • Add a server-side cap for health history limits.

P1 — Confidence and hardening

  • Restructure backend tests into real app/integration layers and reduce router-level monkeypatching.
  • Fix import/package hygiene and make basedpyright a meaningful signal.
  • Add explicit tests for reverse-mode port reassignment failure paths and orphan recovery.

P2 — Maintainability and resilience

  • Split useControlPlane.ts into smaller composables.
  • Separate refreshAll() into panel-aware refreshes.
  • Remove or formalize app.state.services.
  • Move blocking file I/O off the event loop.

P3 — Product quality and performance

  • Add external load/perf harness for backend APIs, websockets, and lifecycle operations.
  • Normalize built-in template contract and document intended operator semantics.
  • Clean up runtime artifact and backup-file lifecycle policy.