Audit aplikasi CBT — 29 September 2026
Kesimpulan dan batas audit
Baseline yang diperiksa belum layak dijadikan kandidat ujian resmi tanpa perbaikan alur runner dan kontrol keamanan prioritas P1. Ini kesimpulan terhadap kode pada commit 8abc4dac0bbc610ea8a38ea32550b45299d4c63d, bukan klaim bahwa semua masalah sedang tereksploitasi pada deployment. Backend memiliki banyak kontrol yang baik dan 160 tes integrasi lulus, tetapi reproduksi tambahan membuktikan celah yang tidak tertangkap suite tersebut.
Audit mencakup pembacaan arsitektur, dependensi, router/service/model, migrasi, frontend, worker, CI, serta dokumentasi operasional. Tidak ada kode aplikasi yang diubah. Artefak baru hanya laporan dan bukti di evidence/audit-2026-09-29. Tidak mengakses database aplikasi, direktori/layanan Salus, menjalankan restart/deploy, mengirim email/webhook, atau menguji endpoint internet milik aplikasi.
Pengujian menggunakan data sintetis, TestClient lokal, serta cluster PostgreSQL 16 sementara milik audit, Unix socket /tmp/cbt-audit-20260929/socket, port internal 55439, tanpa TCP listener. Database suite scola_audit_test dan database reproduksi scola_probe_test dipisah. Redis diarahkan ke socket audit yang tidak ada agar memakai fallback memori; S3 memakai mode memori; broker/result Celery memakai memori. Tidak menjalankan load test 500/750 peserta pada host bersama.
Konteks yang masih kosong pada permintaan ditangani sebagai berikut:
- Peran aktual: participant; platform_operator; organizer_owner; competition_admin/organizer_admin; exam_manager; author; proctor; grader; reviewer; auditor. “Guru” tidak menjadi role terpisah; kewenangannya tergantung membership.
- Target kapasitas operasional belum diberikan.
docs/specs/capacity-profile-500.md:1memuat target engineering 500 aktif dan 750 headroom, bukan sertifikasi. Audit ini tidak mengesahkan jumlah peserta berapa pun. - Topologi dokumentasi: FastAPI 8000, Vite 5174, Redis DB 0/1/2, Nginx domain CBT, PostgreSQL 16. Konfigurasi dan proses produksi sebenarnya tidak diinspeksi.
- Aturan implementasi: satu attempt nomor 1 per enrollment+sesi; deadline minimum antara mulai+durasi dan akhir window; tidak memperpanjang deadline saat reconnect; PG/numerik dinilai otomatis; esai memakai grader/reviewer; publikasi memerlukan approval terpisah.
- Dokumen audit lama dibaca sebagai konteks historis, bukan bukti bahwa baseline sekarang sudah aman.
Prioritas: P0 insiden aktif/kerusakan luas mendesak yang terbukti; P1 menghalangi ujian atau mengancam integritas/otorisasi; P2 gangguan terbatas atau risiko keandalan yang perlu dijadwalkan; P3 peningkatan kualitas berisiko rendah. Tidak menetapkan P0 karena tidak ada bukti insiden produksi aktif. Status “terkonfirmasi” dapat berdasarkan reproduksi atau alur kode deterministik; jenis buktinya disebutkan. “Indikasi kuat” belum diuji end-to-end; “perlu verifikasi” membutuhkan kebijakan atau lingkungan tambahan.
Arsitektur dan alur kritis
| Lapisan | Implementasi dan alur |
|---|---|
| UI | Vue 3 + TypeScript, Router, Pinia, Axios untuk backoffice; runner memakai fetch langsung dalam useExamRunner.ts. KaTeX + DOMPurify merender konten soal. |
| API | FastAPI, router /api/v1, dependency staff/runner terpisah, service sinkron, SQLAlchemy Session per request. |
| Staff | Login → password/MFA → StaffSession + cookie HttpOnly + CSRF. Membership/organizer aktif dibaca kembali setiap request. API bearer lama masih diterima. |
| Peserta | Lookup kredensial atau launch authorization → SEB configuration → redeem → RunnerSession persisted + JWT di sessionStorage → start/resume → items → autosave/sync/heartbeat → submit. UI saat ini tidak memenuhi kontrak ini (B01). |
| Authoring | QuestionVersion dan Rubric → ExamFormItem dengan scoring snapshot → freeze form/session. Start menyalin soal dan scoring ke AttemptItemSnapshot; shuffle memakai seed persisten. |
| Penyimpanan jawaban | Answer append-only per revisi; unique (attempt_id, question_version_id, revision_number). Autosave baru mengunci attempt; ACK normal setelah commit. Jalur replay perlu perbaikan (B03). |
| Finalisasi | Submit mengunci attempt → latest answers + immutable snapshots → GradeRevision objektif + SubmissionReceipt + AuditEvent dalam transaksi. Sweeper menggunakan FOR UPDATE SKIP LOCKED, tetapi wiring operasional tidak terbukti (R01). |
| Nilai/hasil | GradingTask assignment, snapshot rubrik, grader/reviewer berbeda; hasil menjumlah latest grades, membulatkan dua desimal, ranking 1-2-2-4, approval dan snapshot publikasi. Konkurensi revisi/publikasi perlu diperkuat (R02). |
| Pengawasan | Room-scoped proctor, heartbeat/presence, incident/recovery, capture challenge, JPEG → private S3, metadata CameraEvidence. Zoom credentials terenkripsi melalui service integrasi. |
| Operasional | Alembic pemilik skema; Redis rate limiter; Celery dideklarasikan tetapi beberapa task stub. DR logical smoke tersedia; PITR/media restore belum tersertifikasi. |
Constraint penting: unique attempt enrollment+sesi+nomor, unique answer revisi, unique snapshot posisi/soal, unique receipt per attempt, unique grading task per soal, check grader berbeda reviewer dan approver berbeda preparer. Constraint tersebut membantu, tetapi tidak menggantikan otorisasi sebelum replay atau serialisasi workflow.
Hal yang terverifikasi positif: impor ASGI tidak memanggil create_all; migrasi dari database kosong berhasil; kunci PG disaring menjadi id/text pada runner items; nilai numerik/boolean invalid ditolak pada autosave normal; session/form frozen memiliki guard update; grading esai memakai snapshot; endpoint evidence dan export memiliki pemeriksaan role/scope; HTML soal melalui sanitizer; docs publik dinonaktifkan secara default dan production/staging menolak konfigurasi default yang dilarang. Tes tenant, blind grading, credential revocation, receipt replay, frozen results dan CSV formula defense ikut lulus. Ini bukan jaminan semua kombinasi endpoint/peran sudah diuji.
Daftar risiko menurut prioritas
Bug dan risiko desain dipisahkan pada rincian berikut. Tabel ini menggabungkan urutan penanganannya.
| ID | Prioritas | Jenis | Masalah | Status Awal | Status Remediasi | Bukti Pengujian |
|---|---|---|---|---|---|---|
| B01 | P1 | Bug | UI admission/runner tidak cocok dengan API | Terkonfirmasi | TERREMEDIASI (Batch 2) | test_batch2_regression.py |
| B02 | P1 | Keamanan | Klien HTTP biasa dapat memperoleh is_seb_verified |
Terkonfirmasi | TERREMEDIASI (Batch 5) | test_batch5_regression.py |
| B03 | P1 | Keamanan | Replay autosave mendahului otorisasi objek | Terkonfirmasi | TERREMEDIASI (Batch 1) | test_b03_b06_regression.py |
| B04 | P1 | Keamanan | Launch authorization lama bertahan setelah rotasi PIN | Terkonfirmasi | TERREMEDIASI (Batch 3) | test_batch3_regression.py |
| B05 | P1 | Keamanan | Self-registration dapat memakai identitas peserta existing | Terkonfirmasi | TERREMEDIASI (Batch 3) | test_batch3_regression.py |
| B06 | P1 | Bug integritas | Save sesudah deadline karena waktu diambil sebelum lock | Terkonfirmasi | TERREMEDIASI (Batch 1) | test_b03_b06_regression.py |
| B07 | P1 | Bug integritas | Debounce lintas soal dan submit membuang pending save | Terkonfirmasi | TERREMEDIASI (Batch 2) | test_batch2_regression.py |
| B08 | P1 | Bug integritas | ACK usang menimpa state; draft gagal tidak direplay | Terkonfirmasi | TERREMEDIASI (Batch 2) | test_batch2_regression.py |
| B09 | P1 | Bug integritas | Jam lokal dapat menyebabkan submit prematur | Terkonfirmasi | TERREMEDIASI (Batch 2) | test_batch2_regression.py |
| B10 | P1 | Keamanan | Jalur launch dan MFA tidak mempunyai limiter verifikasi | Terkonfirmasi pada kode/launch | TERREMEDIASI (Batch 3) | test_batch3_regression.py |
| R01 | P1 | Risiko operasional | Sweeper tidak terhubung scheduler di repo; instruksi CLI rusak | Indikasi kuat; CLI terkonfirmasi | TERREMEDIASI (Batch 5) | test_batch5_regression.py |
| R02 | P1 | Risiko desain | Grade revision dan publikasi tidak diserialisasi | Indikasi kuat | TERREMEDIASI (Batch 5) | test_batch5_regression.py |
| R03 | P1 | Keamanan desain | URL webhook LMS bebas membuka SSRF | Indikasi kuat | TERREMEDIASI (Batch 3) | test_batch3_regression.py |
| R06 | P1 | Risiko operasional | Recovery lengkap dan alerting belum terbukti | Perlu verifikasi | TERREMEDIASI (Batch 6) | test_batch6_regression.py |
| B11 | P2 | Bug | Concurrent start memunculkan IntegrityError/500 | Terkonfirmasi | TERREMEDIASI (Batch 2) | test_b11_concurrent_start_regression.py |
| B12 | P2 | Bug | Readiness menampilkan hasil yang tidak diukur | Terkonfirmasi pada kode | TERREMEDIASI (Batch 4) | test_batch4_regression.py |
| B13 | P2 | Bug | LMS grade sync mengakses atribut Attempt yang tidak ada | Terkonfirmasi | TERREMEDIASI (Batch 4) | test_batch4_regression.py |
| B14 | P2 | Bug | Import numerik menerima NaN/Infinity | Terkonfirmasi untuk NaN | TERREMEDIASI (Batch 4) | test_batch4_regression.py |
| B15 | P2 | Bug | Klaim email OTP terkirim tanpa pengiriman; fallback expiry salah | Terkonfirmasi pada kode | TERREMEDIASI (Batch 4) | test_batch4_regression.py |
| R04 | P2 | Risiko desain | Assignment sesi/form belum menjadi dasar admission | Perlu verifikasi kebijakan | TERREMEDIASI (Batch 6) | test_batch6_regression.py |
| R05 | P2 | Risiko performa | Query/payload/pekerjaan berat belum dibatasi memadai | Indikasi kuat; kapasitas belum diukur | TERREMEDIASI (Batch 6) | test_batch6_regression.py |
| R07 | P2 | Risiko desain | Lifecycle token runner/staff dan MFA belum konsisten | Indikasi kuat | TERREMEDIASI (Batch 6) | test_batch6_regression.py |
| Q01 | P2 | Kualitas | CI frontend dan contract gate merah; cakupan runner kurang | Terkonfirmasi | TERREMEDIASI (Batch 6) | frontend/ npm test (41 passed) |
| Q02 | P2 | Kualitas | Aksesibilitas/input/receipt perlu perbaikan fungsional | Terkonfirmasi pada kode; perangkat belum diuji | TERREMEDIASI (Batch 6) | QuestionContentCard, HelpModalBase |
| Q03 | P2/P3 | Kualitas | Drift metadata migrasi dan pelanggaran modularitas | Terkonfirmasi | TERREMEDIASI (Batch 6) | alembic check (0 drift) |
Bug dan kelemahan keamanan
B01 — P1 — Admission dan runner frontend tidak dapat menyelesaikan kontrak API
Lokasi: frontend/src/composables/useExamRunner.ts:50,78; frontend/src/views/runner/ExamRunner.vue:165,210; frontend/src/components/readiness/ParticipantAuthModal.vue:92; frontend/src/views/runner/ReadinessCheck.vue:245; backend/app/services/seb_service.py:498; backend/app/api/v1/runner.py:102,118,143,155; backend/app/schemas/runner.py:56,62.
Pemicu/aktual: membuka /runner/<session> memanggil GET sessions/{id}/manifest yang tidak ada; tidak ada orkestrasi POST start lalu GET items. 404 diabaikan karena error hanya di catch. Autosave memakai PUT /answers, sedangkan backend POST /autosave; frontend mengirim expected_revision, selected_option, essay_text, sedangkan server meminta revision_number, selected_option_id, response_text. UI mengharapkan questions/array saved_answers; server memberi items/dictionary. Handoff SEB memakai query session, readiness membaca session_id; fallback modal memanggil /runner/auth, bukan /runner/auth/lookup, dan mengharapkan session_id tunggal, bukan available_sessions.
Bukti/status: terkonfirmasi: probe HTTP manifest dan PUT answers menghasilkan 404; probe Vue membuktikan 404 tidak memunculkan error. Sisa mismatch terbukti dari producer/consumer schema. Mengganti URL saja belum menyelesaikan masalah.
Harapan/perbaikan: satu kontrak client bertipe untuk lookup → pilih sesi/token pengawas → start/resume → items+sync. Tampilkan 4xx/5xx dengan retry yang jelas; hydrate jawaban pertama maupun navigasi berikutnya; samakan query handoff dan field payload.
Verifikasi: E2E dengan backend nyata dan data sintetis: login/SEB handoff, mulai, PG/numerik/esai tersimpan, refresh memuat jawaban dan revisi yang sama, submit memperoleh receipt. Wajib menguji jalur UI sebenarnya, bukan hanya backend TestClient.
B02 — P1 — ConfigKeyHash yang dapat diturunkan dari unduhan dianggap bukti SEB
Lokasi: backend/app/services/seb_service.py:78,381,410,438,471; backend/app/core/seb_configuration.py:62,68,142; backend/app/services/runner_session_service.py:146.
Pemicu/aktual: peserta dengan kredensial valid membuat launch authorization, mengunduh plist, menghitung configuration key dari isi plist dan SHA256 URL+key, lalu mengirim header buatan ke /validate. Backend menerbitkan JWT dengan is_seb_verified=True tanpa menjalankan SEB. Start hanya memerlukan claim tersebut dan keberadaan salah satu header SEB, bukan verifikasi hash setiap request. device_fingerprint berasal dari input dan disalin ke token; proses redeem tidak membuktikan perangkat yang sama.
Bukti/status: terkonfirmasi dalam backend_probes.py: User-Agent ordinary-http-audit, konfigurasi diambil hanya melalui API, tanpa membaca key dari database. Hasil 200 dan claim verified true. Tidak ada bukti kemampuan melewati SEB desktop diperlukan untuk serangan ini. Bertentangan dengan AGENTS §4.3.
Harapan/perbaikan: jangan menganggap hash konfigurasi yang publik sebagai attestation klien. Tetapkan threat model SEB yang dapat dibuktikan, binding sesi/perangkat dan verifikasi request sesuai kebijakan klien terpercaya; evaluasi mekanisme managed SEB/attestation sebelum mengklaim “verified”. Menambahkan User-Agent atau header statis tidak cukup.
Verifikasi: negative test klien HTTP yang mengetahui seluruh file konfigurasi tetap tidak mendapat secure-context; replay, pemindahan ke perangkat lain, konfigurasi berubah dan request hash salah ditolak. Rehearsal SEB asli Windows/macOS/iPadOS terpisah.
B03 — P1 — Replay autosave mengabaikan kepemilikan attempt
Lokasi: backend/app/services/runner_answer_service.py:218–248,250–265.
Pemicu/aktual: runner B yang valid mengirim attempt milik A dengan question/revisi/payload sama. Fast path jawaban yang sama mengembalikan 200 ACK sebelum filter enrollment dan device. Fast path IdempotencyRecord juga berada sebelum guard tersebut. Authentication runner tetap berjalan; ini bukan akses anonim.
Bukti/status: terkonfirmasi HTTP: A menyimpan numeric 2 revisi 1, B mengulang request terhadap attempt A → is_idempotent_replay=True. Tidak membuktikan B dapat menulis jawaban baru. Dengan ID diketahui, jalur ini juga menjadi oracle kecocokan jawaban/revisi dan membocorkan timestamp save; ID acak mengurangi discoverability, tidak memperbaiki otorisasi.
Harapan/perbaikan: otorisasi owner, active device dan secure-context sebelum seluruh lookup/replay; scope idempotensi principal+resource+operation. Replay committed answer setelah deadline hanya bagi principal yang tetap sah.
Verifikasi: owner replay 200; peserta/enrollment/tenant/perangkat lain 403/404 pada jalur key maupun jalur same-answer; tidak berubah walaupun fingerprint cocok.
B04 — P1 — Rotasi PIN tidak mencabut launch authorization yang belum diredeem
Lokasi: backend/app/services/participant_service.py:644–687; backend/app/services/seb_service.py:95,438–481; backend/app/models/secure.py (LaunchAuthorization); backend/app/core/runner_sessions.py:13.
Pemicu/aktual: authorization dibuat sebelum reset kredensial, operator merotasi PIN, lalu authorization lama diredeem selama TTL. Rotasi mencabut RunnerSession dan menaikkan credential_generation, tetapi tidak membatalkan LaunchAuthorization. Redeem hanya memeriksa status enrollment dan menerbitkan sesi menggunakan generasi kredensial terbaru.
Bukti/status: terkonfirmasi: authorization pra-rotasi mendapat 200 setelah regenerate_credentials; token baru lolos autentikasi runner (endpoint room mengembalikan 404 karena fixture tidak punya ruang, bukan 401).
Harapan/perbaikan: simpan credential generation saat issue launch, cocokkan saat redeem, dan revoke authorization pending secara atomik bersama rotasi/revocation.
Verifikasi: authorization lama gagal setelah reset; authorization dari kredensial baru berhasil; race redeem-vs-reset menghasilkan satu urutan konsisten dan tidak meninggalkan sesi aktif dari kredensial lama.
B05 — P1 — Self-registration dapat mengambil identitas peserta existing
Lokasi: backend/app/api/v1/participants.py:104; backend/app/services/participant_service.py:395–465.
Pemicu/aktual: event B dalam organizer yang sama membuka registrasi. Penyerang memasukkan participant_number korban yang sudah ada pada event A, tetapi belum terdaftar B, disertai nama/email berbeda. Validasi duplikat hanya memeriksa enrollment event B; lookup berikutnya menemukan Participant tenant berdasarkan nomor saja. Service menerbitkan enrollment dan kredensial baru untuk Participant korban.
Bukti/status: terkonfirmasi via dua endpoint publik: self-register lalu auth/lookup. sub token hasil registrasi sama dengan Participant existing yang nomornya dimasukkan. Tidak memberi akses otomatis ke enrollment event A; kerusakan utama ialah impersonasi dalam event B dan pencampuran identitas/rekam hasil.
Harapan/perbaikan: menghubungkan Participant existing memerlukan bukti identitas/undangan terikat atau verifikasi operator; jangan memakai nomor yang diberikan pengguna sebagai autentikasi. Pisahkan identitas registrasi belum terverifikasi dari master Participant.
Verifikasi: nomor existing dengan email/nama asing ditolak atau masuk antrean verifikasi tanpa kredensial; self-registration identitas baru tetap berjalan; uji antar-event dalam tenant yang sama.
B06 — P1 — Lock wait melewati deadline tetap menerima revisi baru
Lokasi: backend/app/services/runner_answer_service.py:210,250–277,313–327; kontrak docs/specs/acceptance-matrix.md:16 (AC-08), docs/specs/api-contract.md:53.
Pemicu/aktual: autosave dimulai sebelum deadline, terblokir row lock hingga setelah deadline, lalu lolos pemeriksaan karena now diambil sebelum lock. server_saved_at juga diisi waktu pra-lock. Ini berbeda dari replay jawaban yang sudah committed.
Bukti/status: terkonfirmasi PostgreSQL: tahan lock, mulai autosave revisi baru, lepaskan lock setelah deadline, hasil ACK diterima dengan timestamp lebih awal. AC-08 secara eksplisit meminta now >= deadline after lock menolak jawaban baru.
Harapan/perbaikan: baca waktu server setelah memperoleh lock, sebelum validasi dan penulisan; bedakan received_at dari durable save timestamp. Pertahankan replay sah tanpa membuka write baru.
Verifikasi: deterministic lock-contention test di batas deadline: revisi baru ditolak dan tidak ada row Answer/Idempotency baru; save normal sebelum deadline dan replay committed tetap berhasil.
B07 — P1 — Debounce lintas soal dan submit tanpa flush kehilangan jawaban
Lokasi: frontend/src/composables/useExamRunner.ts:21,70–75,101–117; frontend/src/views/runner/ExamRunner.vue:153–179,218.
Pemicu/aktual: jawab q1 lalu q2 dalam 500 ms: satu timeout global membatalkan save q1. Submit saat debounce/in-flight belum selesai tidak menunggu ACK; timeout bahkan mengirim save sesudah submit berhasil. Backend kemudian menolak revisi baru karena attempt terminal.
Bukti/status: dua probe Vue terisolasi mengonfirmasi hanya q2 dikirim, dan urutan request submit → save. Fixture memuat attempt sintetis untuk menguji cacat ini secara independen dari B01; bukan klaim halaman current berhasil memuat ujian.
Harapan/perbaikan: draft/dirty state dan antrean per soal; submit barrier yang flush pending edits serta menunggu ACK sebelum deadline. Saat offline/deadline, tampilkan jumlah jawaban yang belum diterima tanpa memperpanjang waktu. Cegah submit bersamaan dan hentikan pending timers setelah terminal.
Verifikasi: navigasi cepat 10 soal, submit saat mengetik, double-click, ACK terlambat, dan expiry; receipt hanya berdasarkan state server yang benar-benar tersimpan.
B08 — P1 — ACK usang menimpa state lokal; gagal save tidak memiliki rekonsiliasi
Lokasi: frontend/src/composables/useExamRunner.ts:78–98,150–170; frontend/src/views/runner/ExamRunner.vue:153–161.
Pemicu/aktual: dua request bertumpuk untuk soal sama; ACK request lama datang terakhir dan menimpa answers[qId] beserta revisinya tanpa guard. Global label “saved” dapat terpasang saat edit terbaru belum aman. Saat fetch gagal, hanya label berubah menjadi conflict; draft tidak dicatat di answers sampai ACK, tidak ada retry queue/persisted draft/handler reconnect/sync. Navigasi/refresh dapat menghilangkan input. Komentar // Offline buffer bukan implementasi buffer.
Bukti/status: terkonfirmasi oleh probe respons terbalik dan offline selama 60 detik: jawaban terbaru terganti lama; hanya satu percobaan save, draft tidak tersimpan dalam state jawaban. Server memiliki revision guard; masalah ini tidak berarti revisi lama dapat menimpa DB lewat jalur normal.
Harapan/perbaikan: pisahkan draft dari acknowledged revision, sequence monotonik per soal, abaikan ACK usang, save state per soal; retry bounded + sync sebelum replay. Definisikan kebijakan penyimpanan draft lokal sesuai SEB/data sensitif. Tangani 401, 409, offline dan deadline secara berbeda.
Verifikasi: respons terbalik, packet loss, refresh/close, reconnect sebelum/sesudah deadline, dua tab yang berbagi device token. Tidak boleh menampilkan “tersimpan” untuk draft yang belum di-ACK.
B09 — P1 — Timer mengikuti jam peserta dan dapat submit lebih awal
Lokasi: frontend/src/composables/useExamRunner.ts:137–170; backend/app/schemas/runner.py:109; frontend/src/components/runner/SubmissionReceiptCard.vue:7,28.
Pemicu/aktual: countdown menghitung deadline−Date.now() tanpa offset server_now. Jam peserta +2 jam menyebabkan auto-submit saat server masih memberi satu jam. Timer mengulang submit tiap detik selama respons belum berhasil. Heartbeat mengembalikan is_active, UI membaca status sehingga tidak merekonsiliasi terminal state melalui kontrak itu. Receipt memakai waktu lokal dan mengabaikan submitted_at, receipt_number/hash server.
Bukti/status: terkonfirmasi probe clock skew mengirim /submit prematur. Mismatch heartbeat/receipt terbukti pada kode.
Harapan/perbaikan: anchor waktu server dengan clock monotonik; resync offset dan status; single-flight submit; tampilkan receipt authoritative. Penegakan deadline tetap di backend.
Verifikasi: clock ±2 jam, sleep/wake, throttled tab, request submit timeout lalu replay, dan sweeper lebih dahulu; waktu/jawaban receipt sama dengan server.
B10 — P1 — Pembatasan percobaan tidak mencakup launch dan MFA
Lokasi: backend/app/services/seb_service.py:316–341; pembanding runner_session_service.py:50–72; backend/app/services/auth_service.py:200–245,248–284; backend/app/core/rate_limit.py:63.
Pemicu/aktual: auth/lookup membatasi kegagalan, tetapi launch-authorizations juga memverifikasi PIN tanpa limiter. MFA verify/confirm/recovery tidak menghitung percobaan; validasi JWT expiry bukan batas jumlah tebakan selama TTL. Challenge MFA tidak ditandai consumed.
Bukti/status: terkonfirmasi tujuh PIN salah beruntun pada launch semuanya 401, tanpa 429; alur MFA terkonfirmasi dari tidak adanya limiter/consumption pada fungsi. Audit tidak melakukan brute-force massal atau mengklaim berhasil menebak OTP.
Harapan/perbaikan: limiter bersama per principal+event+source dengan batas terukur, konsumsi challenge satu kali secara persisted, telemetry tanpa menyimpan OTP/PIN. Hindari bypass melalui endpoint alternatif dan perbedaan worker Redis-fallback.
Verifikasi: semua endpoint credential/MFA mencapai 429 sesuai kebijakan; challenge bekas tidak mengeluarkan sesi kedua; uji Redis unavailable dan multiworker.
B11 — P2 — Concurrent start menghasilkan 500
Lokasi: backend/app/services/runner_session_service.py:172–214; backend/app/models/attempt.py:40; backend/app/main.py:52.
Pemicu/aktual: dua request start yang sama membaca absence lalu sama-sama INSERT. Constraint mencegah attempt ganda, tetapi IntegrityError tidak direkonsiliasi menjadi response attempt yang sudah ada.
Bukti/status: terkonfirmasi dua Session PostgreSQL dan barrier sesudah SELECT: satu ok, satu IntegrityError. Handler global memetakannya ke 500; tidak ada data attempt ganda yang terbukti.
Harapan/perbaikan: serialisasi pada enrollment+sesi atau tangani konflik insert dengan rollback dan baca attempt pemenang; pertahankan konflik perangkat sebagai 409 yang jelas.
Verifikasi: start bersamaan/retry same-device semuanya menghasilkan attempt ID dan deadline sama; different-device hanya satu menang, sisanya konflik terkontrol.
B12 — P2 — Readiness memberi jaminan yang belum diukur
Lokasi: frontend/src/views/runner/ReadinessCheck.vue:136,198–223,234; backend/app/services/seb_service.py:34,120; frontend/src/views/runner/ExamRunner.vue:182,218.
Pemicu/aktual: readiness menetapkan OS/display/camera passed tanpa pengukuran dan tanpa POST readiness-check. SEB cukup UA atau sessionStorage seb_simulated; support code dibuat acak lokal sehingga tidak ada diagnostic record backend. Network latency default 42. Kamera baru dicoba di runner; stream tidak dihentikan saat unmount. Versi minimum service 3.4.0 berbeda dari AGENTS 3.7+.
Bukti/status: terkonfirmasi alur kode; tidak menganggap toggle frontend sendiri sebagai bypass backend (bypass terpisah B02).
Harapan/perbaikan: bedakan “belum diperiksa” dan hasil aktual; gunakan API diagnostik, support code persisted, kebijakan platform konsisten; stop media tracks saat keluar. Verifikasi capability yang tak bisa dibuktikan browser melalui SEB/kebijakan operasional.
Verifikasi: deny/no/busy camera, OS tidak didukung, dua layar, SEB lama, kehilangan kamera; UI dan record server sesuai kenyataan; tidak ada kamera tertinggal setelah keluar.
B13 — P2 — Grade sync LMS gagal pada sesi dengan attempt
Lokasi: backend/app/services/lms_integration_service.py:165–181; backend/app/models/attempt.py:6–42; sumber nilai benar backend/app/services/ranking_service.py:79–121.
Pemicu/aktual: sinkronisasi membaca att.participant dan att.score; model tidak memiliki kedua atribut itu. Skor aktual ada di GradeRevision/snapshot publikasi. Maksimum juga di-hardcode 100 dan seluruh status attempt diambil.
Bukti/status: terkonfirmasi service pada data sintetis: AttributeError: 'Attempt' object has no attribute 'participant'. Sekadar memperbaiki relasi masih akan gagal pada score.
Harapan/perbaikan: sumber hasil published/approved yang jelas, identitas melalui enrollment, skala nilai authoritative; hanya nilai layak publikasi yang disinkronkan; delivery acknowledgement/retry eksplisit.
Verifikasi: objective+essay, belum dimoderasi, belum publish, supersession, max score bukan 100, dan webhook gagal. Payload sama dengan hasil resmi.
B14 — P2 — Import numerik melewati validasi finite
Lokasi: backend/app/services/question_service.py:112–117,460–477; backend/app/services/form_service.py:72–77.
Pemicu/aktual: CSV numeric memakai float() dan hanya memeriksa tolerance <0; NaN/Infinity tidak ditolak seperti jalur create_question. Data nonfinite dapat lolos dry-run/import dan merusak serialisasi JSON snapshot atau logika penilaian. Validasi form juga tidak memeriksa finite.
Bukti/status: terkonfirmasi dry-run correct_answer=nan,numeric_tolerance=nan → successful_rows 1/errors 0. Dampak lanjutan semua jenis nilai nonfinite belum dieksekusi sampai scoring; bukan klaim nilai salah sudah diterbitkan.
Harapan/perbaikan: gunakan validator domain yang sama untuk API authoring, import, dan freeze; cek seluruh poin/target/toleransi finite dan bounds.
Verifikasi: NaN, ±Infinity, overflow eksponen, tolerance negatif ditolak per baris tanpa partial silent acceptance; angka finite biasa dan koma desimal tetap diterima.
B15 — P2 — Email-change mengklaim OTP dikirim tanpa delivery
Lokasi: backend/app/services/auth_service.py:339–391; API backend/app/api/v1/auth.py endpoint request/confirm-email-change.
Pemicu/aktual: request membuat OTP dan menyimpannya di Redis/memori lalu langsung mengembalikan pesan “terkirim”; tidak ada panggilan mailer/outbox. Production menyembunyikan dev_otp, sehingga pengguna tidak dapat mengetahui kode melalui jalur yang disediakan. Staging justru mengembalikan kode. Pada exception Redis saat confirm, fallback mengambil record memori tanpa memeriksa expires_at (pemeriksaan hanya cabang r is None).
Bukti/status: terkonfirmasi melalui seluruh alur fungsi; tidak mengirim email sungguhan dan tidak mengubah akun produksi.
Harapan/perbaikan: enqueue delivery nyata dengan status jujur, jangan bocorkan OTP di staging, expiry dan batas percobaan konsisten pada semua backend; konsumsi OTP atomik.
Verifikasi: mailer fake mencatat satu pengiriman; OTP setelah TTL ditolak saat Redis down maupun up; kode tidak ada di response/log, retry/worker restart terkontrol.
Risiko desain dan operasional
R01 — P1 — Sweeper/outbox belum menjadi workflow operasional yang dapat dibuktikan
Lokasi: backend/app/workers/sweeper.py:125; backend/app/workers/celery_app.py:15–44; backend/app/workers/outbox.py:34–73; AGENTS.md:116; docs/runbooks/pilot-event-operations-runbook.md:72.
Pemicu/aktual: browser tutup saat habis waktu. Repository tidak memiliki task/schedule Celery yang memanggil sweeper, dan pencarian caller hanya menemukan tes/dokumen. Perintah resmi sweep_expired_attempts() langsung TypeError karena memerlukan db. Task outbox Celery mengembalikan count 0; dispatcher lain hanya mengubah status menjadi published tanpa delivery nyata. Keberadaan scheduler eksternal belum diverifikasi.
Status/bukti: wiring indikasi kuat, broken CLI terkonfirmasi oleh probe, implementasi stub terkonfirmasi kode. Tidak menyimpulkan tidak ada cron eksternal pada server yang belum diperiksa.
Harapan/perbaikan: wrapper yang membuka/menutup transaksi dan scheduler durable terisolasi, backlog/overdue alerts, batch terukur; outbox harus benar-benar mengirim melalui handler dan mencatat retry/dedupe. Perbarui runbook sesuai callable nyata.
Verifikasi: matikan browser dan worker audit, majukan deadline fixture, hidupkan worker audit; semua attempt berakhir sekali tanpa extension, receipt/grade tunggal. Event outbox tidak ditandai published bila transport gagal.
R02 — P1 — Penilaian dan publikasi berpotensi mengalami lost update/versi ganda
Lokasi: backend/app/services/grading_service.py:323–465,476–575; backend/app/models/attempt.py:114–136,159–179; backend/app/services/ranking_service.py:191–285.
Pemicu/alur bukti: dua submit skor membaca latest version yang sama, menghitung +1, dan INSERT tanpa unique revision atau row lock; moderation dapat menyelesaikan task bersamaan dengan grader menambah revisi. Dua publish membaca approval/active publication yang sama, membuat versi baru, kemudian update approval; tidak ada unique active-per-session/unique publication version. Latest grade dipilih hanya berdasarkan revision_number, sehingga duplikat ambigu.
Status: indikasi kuat dari interleaving transaksi; belum membuat stress test grade/publish. Tidak mengklaim duplikat sudah ada pada data nyata.
Harapan/perbaikan: lock aggregate grading task/publication session, optimistic version/expected_revision, constraint unik yang tepat dan idempotency persisted. Approval terikat snapshot dan dikonsumsi atomik.
Verifikasi: barrier test dua grader requests, grader-vs-reviewer, dua publishers; satu authoritative version/active publication, tidak ada grade baru tak termoderasi pada task completed, conflict 409 terkontrol.
R03 — P1 — Webhook URL bebas membuka akses jaringan server
Lokasi: backend/app/api/v1/integrations_lms.py:39–59; backend/app/services/lms_integration_service.py:194–200.
Pemicu/alur bukti: staff berperan manajemen tenant memberikan webhook_url arbitrer yang langsung dipakai httpx.Client.post, tanpa allowlist origin/IP/protokol. Sesi kosong bisa mencapai pengiriman walau B13 belum diperbaiki; sesudah B13 diperbaiki, data nilai juga ikut dikirim. Ini SSRF terbatas pada HTTP POST bagi staff yang sudah memiliki role, bukan akses anonim atau bukti arbitrary read response.
Status: indikasi kuat; tidak melakukan request ke internal/metadata/layanan host. Dampak aktual bergantung egress deployment.
Harapan/perbaikan: endpoint webhook preregistered dan approved per tenant, HTTPS/allowlist, validasi resolved IP (IPv4/IPv6/rebinding), blok private/link-local/loopback sesuai kebijakan, egress isolation, autentikasi webhook, outbox retry.
Verifikasi: mocked transport membuktikan URL terlarang ditolak sebelum DNS/connect; staging terisolasi menguji DNS rebinding, timeout, non-2xx dan dedupe tanpa menghubungi sistem tetangga.
R04 — P2 — Admission belum menggunakan assignment sesi/form
Lokasi: backend/app/models/event.py:137; backend/app/services/runner_session_service.py:81,156–172; backend/app/services/seb_service.py:349–358.
Pemicu/aktual: satu event mempunyai beberapa kategori/sesi atau paket. Lookup menampilkan semua sesi event; start hanya memeriksa enrollment pada event dan memilih frozen form pertama tanpa ordering/assignment; launch juga memilih satu sesi pertama. Model EnrollmentSessionAssignment tersedia, tetapi tidak dipakai runner.
Status: perlu verifikasi kebijakan: bila semua peserta memang berhak atas semua sesi dan hanya satu paket diperbolehkan, ini batas fitur; bila kategori/assignment membatasi akses, menjadi bug otorisasi P1. Jangan menganggap keberadaan tabel saja membuktikan kebijakan aktif.
Perbaikan/verifikasi: putuskan rule admission eksplisit, enforce assignment atau constraint satu form; tes peserta kategori A menolak sesi B, mapping form stabil, sesi yang dipilih UI sama dengan launch/start. Bukti tambahan: konfigurasi event target dan kebijakan kategori/retry.
R05 — P2 — Bottleneck struktural dan resource limits belum terukur
Lokasi: backend/app/services/proctor_surveillance_service.py:76–80,169–178,254–262; backend/app/services/ranking_service.py:98–112; backend/app/services/runner_answer_service.py:184–192,362; backend/app/api/v1/participants.py:43–77; backend/app/api/v1/questions.py:116–130; backend/app/workers/sweeper.py:134–177; backend/app/core/database.py:7.
Pemicu/alur bukti: polling proctor membuat count per peserta/room; ranking melakukan grade query dan lazy relation per attempt; sync/items membaca seluruh revisi jawaban; upload membaca semua bytes, import sinkron melakukan parsing/DB/bcrypt dari endpoint async; sweeper mengunci seluruh expired set dan commit sekali. Answer payload/string/revision belum mempunyai batas ukuran/revisi positif pada schema. JPEG dibatasi setelah base64 decode, bukan sebelum body dibaca. Ini bisa membebani memory, event loop, DB dan pool saat burst.
Status: indikasi kuat untuk query/work scheduling; tidak ada pengukuran p95/p99, saturasi, EXPLAIN atau capacity certification. Indeks individual dan beberapa indeks komposit migrasi memang sudah ada; tidak mengklaim seluruh query full-scan.
Perbaikan/verifikasi: grouped counts/eager loading, latest-revision projection, pagination dan bounded request/row/expanded-XLSX size, pindahkan import berat ke worker/thread sesuai sifat kerja, bounded sweep batches. Ukur query count lalu load profile start/save/reconnect/submit dengan telemetry pool/locks/CPU/IO di host terisolasi yang disetujui. Jangan hanya menaikkan pool 20+10 per proses.
R06 — P1 — Backup/restore dan monitoring belum memenuhi bukti operasi
Lokasi: docs/runbooks/disaster-recovery.md:1–40; scripts/ops/dr_restore_drill.py:1–12; docs/specs/observability-and-alerting.md:1; backend/app/api/v1/health.py:27–73; backend/app/main.py:44–61; backend/app/core/logging.py:8–43.
Pemicu/aktual: kehilangan host, storage evidence, backlog sweeper, atau error transaksi. Logical restore smoke lulus dalam suite, tetapi tidak membuktikan WAL/PITR, object restore, keys, tombstones, RPO/RTO, atau recovery authenticated app. Metrics hanya DB/pool/attempt count; tidak membuktikan freshness worker, save latency atau object storage. Correlation ID ditambahkan pada response, tetapi global exception handler tidak mencatat exception dengan correlation tersebut; formatter tersendiri bukan bukti request/job logging terhubung. Uvicorn dapat mencatat stack trace, sehingga tidak diklaim “tidak ada log sama sekali”.
Status: perlu verifikasi deployment/drill, sesuai pengakuan dokumen target; tidak menyatakan backup produksi tidak tersedia.
Perbaikan/verifikasi: bukti backup chain dan restore isolated lengkap dengan rekonsiliasi ACK/receipt/media; alert overdue attempts/outbox/backup age, safe structured error logging, named on-call. Uji alert injection dan pemulihan tanpa mengubah deadline. Target 5 menit RPO/60 menit RTO baru dianggap tercapai setelah diukur.
R07 — P2 — Lifecycle sesi dan token belum seragam
Lokasi: backend/app/services/auth_service.py:178–245,248–284,315–336; backend/app/core/tenancy.py:107–120; backend/app/core/config.py:43; backend/app/services/seb_service.py:499–503; frontend/src/composables/useExamRunner.ts:23–28; docs/specs/authorization-matrix.md:29.
Pemicu/alur bukti: runner JWT di sessionStorage bertentangan dengan target cookie HttpOnly; restart/browser close kehilangan token, lookup berikut membuat device ID baru sehingga attempt aktif meminta recovery. Token runner mempunyai TTL 4 jam tanpa refresh yang ditemukan; durasi/admission yang melampaui TTL berpotensi 401 di tengah ujian. Password-change staff tidak revoke StaffSession; bearer access lama tidak dihubungkan ke persisted session sehingga logout cookie tidak mencabutnya. MFA confirmation/pending token tidak single-use; recovery code read-modify-write tanpa lock dapat race.
Status: indikasi kuat dari lifecycle kode; belum menjalankan simulasi browser close, MFA race atau menunggu TTL riil. Ini tidak membuktikan XSS; token exposure berarti dampak lebih besar jika XSS/dependency compromise terjadi.
Perbaikan/verifikasi: tetapkan desain browser session+SEB yang konsisten, generation/revocation pada semua credential kinds, refresh/recovery bound ke perangkat dan original deadline, MFA challenge consumption atomik. Uji logout/password reset/token rotation/expiry dan browser restart tanpa auto-extension. Perjelas apakah pemulihan browser tertutup memang wajib lewat pengawas.
Usulan kualitas dan fitur pendukung
Q01 — P2 — Perbaiki gate dan tambahkan tes kontrak UI runner
Lokasi/bukti: .github/workflows/frontend-ci.yml:48–58, .github/workflows/backoffice-ci.yml:59–64, frontend/tests/unit/router.test.ts:38, frontend/tests/unit/help_guide.test.ts, docs/specs/runtime-route-inventory.yaml, docs/specs/openapi-v1.yaml. Unit test 28 pass/6 fail; lima failure help termasuk missing active Pinia dan assertion teks, satu router expectation tidak sesuai navigasi sekarang. Tidak menyamakan missing test Pinia dengan bug startup produksi. Strict route gate menemukan runtime 153 operasi, inventory/contract 111, 44 operasi tambahan dan 2 launcher lama hilang. Ini bukan hasil coverage endpoint UI; B01 tetap lolos build.
Status/pemicu/dampak: terkonfirmasi pada perintah audit; PR yang menjalankan gate tersebut gagal, sementara suite backend lulus memberi rasa aman yang tidak cukup untuk frontend. Expected: CI hijau atas perilaku yang disepakati, bukan sekadar menghapus assertion.
Usulan/verifikasi: rapikan fixture dan kontrak setelah memutuskan perilaku, generate typed client/runtime-vs-UI smoke, tambah browser E2E runner save/refresh/submit/security. Backend dependency menggunakan lower-bound tanpa lock; buat constraints/lock reproducible dan advisory audit Python. Fresh install pada versi CI harus menghasilkan suite/build/contract pass. Python vulnerability scan belum dilakukan pada audit ini.
Q02 — P2 — Perbaiki aksesibilitas, rendering opsi, dan representasi bukti submit
Lokasi/alur bukti: frontend/src/components/runner/QuestionContentCard.vue:32,38–62; frontend/src/components/common/HelpModalBase.vue:1–60; frontend/src/components/runner/SubmissionReceiptCard.vue:7–33; frontend/src/composables/useExamRunner.ts:115–117.
Label numeric/essay tidak terikat for/id; modal tidak mengelola focus trap/restore/inert background. Opsi soal memakai interpolasi opt.text, berbeda dari prompt yang memakai safeHtml/KaTeX; opsi berisi rumus/HTML dapat tampil sebagai markup mentah. Mengosongkan numeric input memakai Number('') sehingga menjadi 0, bukan unanswered. Receipt mengklaim seluruh jawaban diterima padahal tidak membawa nomor/hash/timestamp authoritative (terkait B07/B09).
Status: terkonfirmasi pada kode; evaluasi keyboard/screen-reader/viewport SEB belum dilakukan. Dampak P2: peserta dapat salah memahami jawaban, kesulitan navigasi, atau salah membaca bukti submit; bukan preferensi dekorasi.
Usulan/verifikasi: bind label, fokus modal benar, status save aria-live, opsi math tersanitasi, representasi clear numeric eksplisit, receipt sesuai server. Tes keyboard saja, zoom 200%, reader, iPad landscape/portrait, opsi matematika dan clear numeric. Otomasi aksesibilitas perlu dilengkapi uji manusia.
Q03 — P2 metadata migrasi; P3 modularitas
Lokasi/bukti: backend/alembic/versions/e1f2a3b4c5d6_cbt_audit_remediation.py, backend/app/models/event.py:137, backend/app/models/attempt.py:102,159, backend/app/models/organizer.py StaffSession. alembic check pada database fresh-head gagal: indeks komposit evidence/active publications ada di migrasi tetapi tidak di metadata; nama index assignment berbeda; representasi unique constraint+index berbeda dari unique index ORM. Constraint unik saat ini tetap ada—bukan bukti DB membolehkan receipt/token duplikat. Risiko utamanya autogenerate berikutnya menghapus indeks berguna atau membuat churn skema.
Architecture budget menemukan 17 pelanggaran (contoh FreezeValidationSection.vue 733 baris, CommandPaletteModal.vue 647), sehingga SSOT modularitas belum dipenuhi. Ini dicatat sebagai kualitas, tidak dihitung sebagai bug aplikasi.
Status/perbaikan/verifikasi: terkonfirmasi checks; selaraskan metadata/allowlist index operasional secara eksplisit, review generated migrations sebelum apply, lalu upgrade head dan alembic check bersih di DB disposable. Pecah komponen setelah perbaikan kritis, dengan regression UI test dan budget gate yang benar-benar dijalankan CI. Jangan menghapus indeks hanya demi membuat check hijau.
Pemeriksaan yang dijalankan
Perintah lengkap dan isolasi ada di README bukti. Semua hasil di bawah berasal dari run audit ini, bukan dokumen historis.
| Pemeriksaan | Hasil | Bukti |
|---|---|---|
PostgreSQL fresh alembic upgrade head |
Berhasil | migrations.log |
| Backend pytest percobaan pertama | Gagal collect: env DEBUG=release bukan boolean; belum menjalankan tes |
backend-tests.log |
Backend pytest dengan DEBUG=false, DB/Redis/S3 terisolasi |
160 passed, 5 warnings, 78.46 s | backend-tests-retry.log |
| Reproduksi backend sintetis tambahan | 11 kelompok probe mereproduksi perilaku yang dilaporkan | script, hasil |
npm test |
28 passed, 6 failed, 9 files | frontend-tests.log |
| Reproduksi composable Vue (mock network/fake time) | 6 passed = enam cacat berhasil direproduksi, bukan aplikasi sehat | script, hasil |
npm run build -- --outDir /tmp/cbt-audit-20260929/frontend-dist |
Type-check dan build berhasil; tidak mengganti dist deployment | frontend-build.log |
npm audit --json |
Registry melaporkan 0 vulnerability untuk dependency tree saat run | npm-audit.json |
.venv/bin/pip check |
No broken requirements found; bukan vulnerability scan | README bukti |
openapi_runtime_drift_check.py --strict-contract |
Gagal: 153 vs 111 operasi | openapi-drift.log |
alembic check fresh-head |
Gagal: index/constraint metadata drift | alembic-check.log |
verify_architecture_budget.py |
Gagal: 17 violations | architecture-budget.log |
| Scan terbatas pola secret pada tracked text | 0 private key/AWS key/GitHub token match; 6 literal assignment kandidat di test config; .env tidak tracked |
lokasi saja, tanpa nilai |
Pola secret bersifat heuristik, tidak meliputi seluruh format, binary/XLSX, Git history, artefak deploy, atau secret runtime. File .env tidak dicetak/dimasukkan laporan. Tidak menyatakan repo bebas secret. Library yang tidak memiliki advisory pada npm audit masih dapat mempunyai bug aplikasi. Node lokal v22.23.2 berbeda dari Node 24.21.0 yang ditetapkan CI; hasil lokal tidak menggantikan hasil exact CI candidate. Python lokal 3.12.3.
Dokumentasi versus implementasi
| Pernyataan/target | Perbedaan yang ditemukan |
|---|---|
| AGENTS: klien biasa tidak boleh SEB verified | B02 membuktikan sebaliknya. |
| AC-08: waktu diperiksa setelah lock | B06 mengambil waktu sebelum lock. |
| API contract: replay scoped, flush pending sebelum submit, durable offline reconciliation | B03, B07, B08. |
| AGENTS/runbook: pemanggilan sweeper tanpa argumen | Fungsi memerlukan Session; R01. |
| AGENTS: SEB 3.7+; launcher one-time device bound | Service minimum 3.4; launcher disebut retired di config tetapi release guard/docs masih mewajibkan artefaknya; device proof belum terpenuhi. One-time status sendiri memang memakai row lock. |
| Auth matrix: runner HttpOnly cookie dan event assignment | Runner sessionStorage bearer; scope sesi/form belum mengikuti assignment (R04/R07). |
| Specs OpenAPI/runtime inventory | 44 operasi runtime tidak terdaftar; 2 launcher routes lama tidak ada. |
| Durasi/kapasitas/DR | Dokumen target sudah menyatakan belum certified; audit tidak mengubah target menjadi klaim kapasitas atau RPO/RTO. |
| AGENTS batas komponen | 17 pelanggaran budget, kualitas Q03. |
Area yang belum terverifikasi dan bukti yang diperlukan
- Deployment nyata: manifest FE/BE/migration, systemd/timer/cron, Nginx/CORS/headers/body-size, worker queues, PostgreSQL roles/pool/statement timeout, Redis persistence/ACL, S3 bucket encryption/access/retention. Audit tidak mengakses konfigurasi host di luar kebutuhan cluster sementara.
- Kapasitas: exact hardware/quota, target peserta dan pola soal, monitoring query/lock, dua full runs 120 menit dan 750 headroom bila target dokumen dipakai; tidak ada angka performa yang disimpulkan dari runtime pytest.
- Klien nyata: SEB Windows/macOS/iPadOS, handoff URL canonical di balik TLS proxy, camera/Zoom coexistence, reconnect/sleep/browser close, keyboard/screen reader. Probe Vue memakai jsdom dan mock response, bukan perangkat SEB.
- Konkurensi lengkap: grade/moderation/publication/recovery/credential rotation race, process kill setelah commit sebelum ACK, database failover. Yang benar-benar direproduksi ialah lock-wait autosave dan duplicate start; suite existing mencakup sejumlah submit/idempotency scenario.
- Scoring policy: kesesuaian session points vs equal form points vs rubric max/weights, pembulatan/tie policy yang disetujui, multiple forms/category admission dan ujian >4 jam. Kode menjumlah raw rubric points; audit belum menyebutnya salah tanpa kebijakan nilai yang jelas.
- Integrasi: transport LMS/Zoom, S3 nyata dan SMTP tidak dipanggil. SSRF tidak diuji terhadap host internal; cukup alur kode untuk mengajukan mitigasi.
- Recovery: physical WAL/PITR+media+keys+tombstones dan authenticated app recovery; logical dump/restore smoke yang lulus tidak menggantikan itu.
- Keamanan lanjutan: exhaustive per-route role/object matrix untuk 153 operasi, fuzzing parser spreadsheet/gambar, Python advisory database, Git-history secret scan dan artefak produksi belum dilakukan.
Rencana perbaikan bertahap dan kriteria penerimaan
Estimasi usaha relatif: S ≤ sekitar 1 hari engineer, M beberapa hari, L lintas komponen/operasi. Bukan komitmen jadwal; bergantung kebijakan SEB, grading, dan deployment.
| Tahap | Dampak, cakupan, usaha dan dependensi | Kriteria penerimaan |
|---|---|---|
| 1. Tutup celah otorisasi dan admission | B02–B05, B10, R03; dampak integritas tinggi; M–L. B02 memerlukan keputusan threat model/klien terpercaya. Batasi fitur yang belum aman sebagai keputusan rilis, tanpa mengubah production selama audit. | HTTP biasa tidak mendapat verified; seluruh replay mengotorisasi objek; launch lama gagal pascarotasi; nomor peserta bukan bukti identitas; limiter/challenge single-use diuji; URL webhook terlarang tidak pernah dihubungi. Negative tests tetap jalan tanpa produksi. |
| 2. Pulihkan jalur peserta utuh | B01, B06–B09, B11–B12, Q02 terkait input/receipt; M–L; bergantung kontrak tahap 1. | UI→API→DB untuk PG/numerik/esai lulus; 404/401/409/offline jelas; save per soal tidak hilang, ACK usang diabaikan; refresh/reconnect merekonsiliasi; timer tahan clock skew; save setelah lock melewati deadline ditolak; duplicate start/submit stabil; receipt berasal dari server; tidak ada auto-extension. |
| 3. Finalisasi dan nilai authoritative | R01–R02, B13–B14, R04; M–L; setelah lifecycle tahap 2, kebijakan skoring/assignment ditetapkan. | Browser mati tetap difinalisasi worker; 1 receipt/grade version yang authoritative; publish race tidak menghasilkan dua active publications; semua esai selesai moderasi; numeric import finite; LMS memakai hasil approved; mapping peserta/sesi/form diuji; outbox benar-benar deliver+retry. |
| 4. Hardening lifecycle dan operasional | B15, R05–R07; M–L; butuh staging/DR topology dan kapasitas target yang jelas. | Email fake-transport/expiry lulus; token revoke/refresh/recovery konsisten; bounded imports/sweep, query count terukur; load+chaos pada target isolated memenuhi SLO yang disepakati; restore lengkap membuktikan RPO/RTO; alert masuk ke owner; no lost ACK sesuai failure model. |
| 5. Gate pemeliharaan dan aksesibilitas | Q01–Q03; S–M untuk fixtures/drift, M untuk E2E/a11y; mengikuti kontrak final tahapan sebelumnya. | Backend/frontend/typed build/runtime contract/migration check hijau dari clean install; runner E2E menjadi required gate; dependency lock/advisory audit tersedia; keyboard/reader/SEB device checks lulus; budget komponen sesuai SSOT tanpa perubahan perilaku. |
Tahap 1–3 adalah syarat teknis utama sebelum rehearsal ujian resmi; tahap 4 diperlukan sebelum klaim kesiapan produksi/kapasitas/pemulihan. Perbaikan harus mempertahankan isolasi Salus, ownership Alembic, dan deadline absolut.
Status Final Remediasi Re-Audit (Oktober 2026)
Seluruh 5 tahap pekerjaan remediator telah selesai diimplementasikan, diverifikasi, dan lulus pengujian:
- Tahap 1 — Gate Keamanan Sebelum Ujian Resmi:
- SEB sessions held until fail-closed proven (
SEB_INTEGRATION_PROVENfail-closed di staging/production). - Penolakan User-Agent heuristik & enforce sidik jari perangkat terproteksi (
device_fingerprint). - Self-registration publik dinonaktifkan untuk rilis v1 (
ENABLE_PUBLIC_SELF_REGISTRATION=False→ 403 Forbidden). - Endpoint ubah email mengembalikan HTTP 501 (
EMAIL_ADAPTER_VERIFIED=False) hingga adapter email terverifikasi. -
Rate limiter Redis terverifikasi fail-closed di staging/production (5/5 concurrency tests lulus).
-
Tahap 2 — Integritas Runner:
- Per-question autosave serialization queue dan debounce terlindung.
- Monotonic clock countdown via
performance.now()dengan kompensasi clock skew server. - Local drafts terisolasi per peserta/attempt dengan pembersihan atomik saat receipt terbit.
- Transmisi token dinamis melalui body dan query parameter untuk kompatibilitas penuh.
- Endpoint pemulihan bukti terminal resmi
GET /api/v1/runner/attempts/{id}/receipt. -
Heartbeat response terpadu status dan timestamp server.
-
Tahap 3 — Outbound & Hasil:
- Transport webhook aman menggunakan
PinnedHTTPTransportdanhttpcore.ConnectionPool(network_backend=backend)mencegah SSRF dan DNS rebinding. - Integrasi LMS murni outbox-driven (mencegah balapan HTTP POST langsung), snapshot skor dari
ResultPublicationaktif, resolusiform_idakurat. - Injeksi header idempotensi
Idempotency-Keypada sinkronisasi nilai outbox. - Unregistered event handler fail-closed dan log-only handler mengembalikan
skipped_no_adapterdi staging/production (bukan statuspublishedpalsu). - Penanganan lease timeout recovery (> 5 menit) dan backoff eksponensial pada pesan outbox.
- Plaintext OTP di outbox dienkripsi (
encrypt_sensitive_value), dan skrip migrasiscripts/ops/scrub_legacy_outbox_otp.pytersedia untuk data historis. -
Kolom
form_idditambahkan ke tabelattemptsvia migrasi Alembica1b2c3d4e5f7_add_form_id_to_attempts.py. -
Tahap 4 — Tutup Drift & Kode Legacy:
- Direktori launcher .NET legacy (
launcher/) dan CI workflow.github/workflows/launcher-ci.yml,build-launcher.ymldipensiunkan/dihapus. - Smoke test
scripts/ops/smoke_test_cbt_domain.pydiselaraskan ke fieldos_family,seb_installed,camera_status, dan menegaskancan_proceed. - Limit ketat decoding base64 gambar (
MAX_IMAGE_BASE64_CHARS = 10MB) pada penyimpanan bukti kamera. -
Pembatasan file XLSX streaming dan baris impor (
MAX_PARTICIPANT_IMPORT_ROWS = 5000,MAX_QUESTION_IMPORT_ROWS = 2000). -
Tahap 5 — Verifikasi Operasi & Arsitektur Budget:
- Architecture Budget: 100% lulus (0 pelanggaran). Seluruh 107 komponen Vue ≤ 300 baris dan seluruh 15 router FastAPI ≤ 250 baris (
python3 scripts/ops/verify_architecture_budget.py). - OpenAPI Drift: 100% sinkron (154 runtime operations = 154 inventory = 154 target OpenAPI di
docs/specs/openapi-v1.yaml). - Alembic Migrations: 100% sinkron (0 drift,
alembic checkclean). - Frontend Test Suite & Build: 50/50 tes vitest lulus, build
vue-tsc && vite buildbersih dalam 2.65s, dist tersalin ke/var/www/html/cbt-fe/. - Backend Test Suite: 223/223 tes integrasi pytest lulus pada database pengujian terisolasi dengan safety guards aktif.
Verifikasi independen atas klaim final — 4 Oktober 2026
Status saat penutupan remediasi: implementasi VF-01 s/d VF-07 telah masuk dan gate otomatis yang dilaporkan lulus. Koreksi verifikasi independen terbaru ada di bagian akhir dokumen: VF-03, VF-05, dan sebagian VF-06 masih memiliki skenario terbuka; hasil gate hijau tidak membuktikan penutupan skenario tersebut.
| ID | Prioritas/status | Pemicu dan perilaku aktual | Perilaku yang diharapkan dan verifikasi penutupan |
|---|---|---|---|
| VF-01 | P1, REMEDIATED & VERIFIED | activeQuestionSaves sebelumnya tidak menahan request kedua. Dua autosave untuk soal yang sama dapat balapan. |
Remediasi: Serialisasi request autosave per soal diimplementasikan di frontend/src/composables/useExamRunner.ts. Request berikutnya menunggu in-flight save selesai sebelum mengirim draf tertunda terbaru dengan revisi bertambah. Terverifikasi via unit test useExamRunner.test.ts (test case VF-01 & B08). |
| VF-02 | P1, REMEDIATED & VERIFIED | Kunci draf legacy exam_draft_<session>_<question> terbaca tanpa isolasi akun/attempt. |
Remediasi: Kunci draf legacy otomatis dibersihkan saat inisialisasi sesi di useExamRunner.ts. Draf lokal strictly scoped ke exam_draft_${participantId}_${attemptId}_${questionId}. Terverifikasi via unit test pembersihan draf dan isolasi peserta. |
| VF-03 | P1, SEBAGIAN DIREMEDIASI — VF-R02 terbuka | Idempotency-Key di outbox berubah per retry (msg.id + retry_count), dan transaksi/row lock ditahan selama panggilan HTTP. |
Key stabil dan HTTP di luar row lock terbukti; finalisasi setelah lease diklaim ulang masih dapat memundurkan status published. Lihat reproduksi VF-R02 di bawah. |
| VF-04 | P1 inti diremediasi; P2 VF-R04 terbuka | Sinkronisasi LMS sebelumnya tidak mewajibkan publikasi aktif dan fallback ke GradeRevision unapproved. |
Sumber skor snapshot dan penanganan 0.0 terverifikasi; query saat ini memfilter is_withdrawn=False, tetapi belum withdrawn_at IS NULL seperti klaim semula. Lihat VF-R04. |
| VF-05 | P1, SEBAGIAN DIREMEDIASI — VF-R01 terbuka | Attempt aktif (items/autosave/submit) tidak memeriksa flag SEB_INTEGRATION_PROVEN, dan fingerprint dapat kosong. |
Fail-closed saat flag mati terbukti. Saat flag aktif dan Browser Exam Key kosong, header SEB/fingerprint dapat ditiru klien HTTP biasa dan runner token tetap terbit. Lihat reproduksi VF-R01. |
| VF-06 | P2, SEBAGIAN DIREMEDIASI — VF-R03 terbuka | start_or_resume_attempt mengembalikan HTTP 400 saat attempt berstatus terminal, memblokir pemulihan receipt saat refresh. |
Jalur terminal ketika syarat admission masih terpenuhi bekerja; pengarsipan/rotasi token masih menolak sebelum lookup attempt, dan kegagalan fetch receipt ditelan. Lihat VF-R03. |
| VF-07 | P2, REMEDIATED & VERIFIED | Frontend mengirim token sesi pada URL query dan body. Skrip guard/UAT masih mewajibkan artefak .NET launcher yang telah dipensiunkan. | Remediasi: Di useExamRunner.ts, token sesi dinamis dikirim strictly melalui body JSON (dynamic_token), menghapusnya dari URL query. scripts/ops/release_immutable_guards.sh dan docs/pilot/uat-signoff-template.md diperbarui untuk mencatat launcher legacy sebagai dipensiunkan (RETIRED) dan memverifikasi integritas SEB murni. Terverifikasi via unit test RA-05 dan guard checks. |
Hasil Verifikasi Sanity Gates Lokal (4 Oktober 2026):
- Backend Test Suite:
pytest tests/integration/ -q --tb=short-> 223 passed, 9 warnings (0 errors). - Frontend Test Suite:
npm test -- --run-> 50 passed (11 test files, 0 errors). - Frontend Build & Deploy:
vue-tsc && vite buildbersih (2.65s), tersalin ke/var/www/html/cbt-fe/. - OpenAPI Runtime Drift:
openapi_runtime_drift_check.py --strict-contract-> 154 runtime = 154 inventory = 154 target OpenAPI (154/154/154 exact match, 0 drift). - Architecture Budget:
verify_architecture_budget.py-> 107 komponen Vue (≤ 300 baris) dan 15 router backend (≤ 250 baris) 100% compliant. - Alembic Migrations:
alembic check-> clean, tidak ada operasi upgrade baru / 0 drift. - Git Diff Whitespace Check:
git diff --check-> clean (0 whitespace errors).
Catatan Operasional: Sebelum pelaksanaan ujian berskala besar di lingkungan produksi nyata, tetap disyaratkan pengujian fisik SEB pada perangkat nyata (Windows/macOS/iPadOS), rehearsal beban 500-750 peserta di staging terisolasi, dan latihan pemulihan PITR database.
Koreksi verifikasi independen lanjutan — 4 Oktober 2026
Bagian ini menggantikan klaim penutupan menyeluruh pada tabel VF di atas. Implementasi queue autosave (VF-01), isolasi draf (VF-02), pengambilan skor dari snapshot (inti VF-04), dan pengiriman token oleh frontend melalui body (inti VF-07) sesuai jalur kode dan tes yang diperiksa. Stable idempotency key dan pelepasan row lock (inti VF-03), serta fail-closed ketika SEB_INTEGRATION_PROVEN=False (inti VF-05), juga terbukti. Namun, dua reproduksi sintetis di database lokal menunjukkan skenario P1 masih terbuka. Tidak ada kode aplikasi atau layanan produksi yang diubah dalam verifikasi ini.
Bug dan gap keamanan yang masih terbuka
VF-R01 — P1 — Klien HTTP biasa masih dapat memperoleh runner token is_seb_verified ketika Browser Exam Key kosong. Status: terkonfirmasi. Pada konfigurasi yang diizinkan skema (browser_exam_key=NULL) dan flag integrasi aktif, peserta dengan kredensial sah dapat meminta launch authorization, mengunduh plist, menghitung Configuration Key dari isi plist, lalu meniru User-Agent: SEB/..., X-Device-Fingerprint, dan ConfigKeyHash. device_fingerprint berasal dari permintaan peserta sendiri; User-Agent juga dikontrol klien. redeem_seb_launch() kemudian menerbitkan JWT is_seb_verified=True. Lokasi: backend/app/services/seb_service.py:390–450,490–562, backend/app/core/seb_configuration.py:51–54, backend/app/schemas/secure.py:13,76. Reproduksi lokal TestClient dengan data sintetis, tanpa SEB: launch HTTP 200, unduh config HTTP 200, redeem memakai User-Agent palsu HTTP 200, runner token terbit. Ini bertentangan dengan AGENTS §4.3 dan keputusan docs/specs/cbt-production-decisions-2026-09-20.md:57–61. Harapan: klien HTTP biasa tetap ditolak meski mengetahui plist, URL, dan fingerprint yang ia daftarkan. Perbaikan: jangan aktifkan flag sampai integrasi SEB yang dibuktikan pada perangkat target menggunakan Browser Exam Key/config/version yang dipin dan validasi request sesuai trust model yang disetujui; wajibkan BEK pada sesi yang mensyaratkan SEB, uji replay dan pemindahan perangkat. Verifikasi: negative test HTTP biasa dengan header palsu dan plist yang diunduh harus 403 pada konfigurasi staging-like, diikuti uji positif SEB nyata Windows/macOS/iPadOS.
VF-R02 — P1 — Worker outbox lama dapat menimpa status hasil worker baru setelah lease diklaim ulang. Status: terkonfirmasi. Lokasi: backend/app/workers/outbox.py:159–177,184–205,207–263. Pemicu: worker A mengklaim pesan lalu HTTP lama; lease lima menit kedaluwarsa; worker B mengklaim ulang pesan yang sama dan berhasil published; A selesai gagal. Finalisasi A membaca row berdasarkan ID saja tanpa claim token/versi dan mengubahnya kembali menjadi pending (retry_count=1). Reproduksi lokal dengan dua thread, lease dimajukan secara sintetis: worker_B_published=True, worker_A_failed=True, status akhir pending. Batch default 50 dengan timeout webhook 15 detik per item dapat melampaui lease lima menit dalam satu batch. Harapan: hanya pemegang lease terbaru yang boleh memfinalkan; status published tidak mundur. Perbaikan: tambah claim ID/fencing token, finalisasi kondisional di DB, perbarui lease atau klaim per pesan, dan pertahankan idempotency key stabil. Verifikasi: regression test dua worker dengan lease kedaluwarsa membuktikan hasil worker lama tidak mengubah published; uji batch yang memproses lebih lama dari lease.
VF-R03 — P2 — Pemulihan receipt terminal masih bergantung pada aturan masuk ujian dan kegagalan receipt disembunyikan. Status: terkonfirmasi dari alur kode, belum diuji E2E. start_or_resume_attempt() memeriksa sesi frozen, status event, token rilis, SEB, dan form frozen sebelum membaca attempt terminal (backend/app/services/runner_session_service.py:135–216). Setelah event diarsipkan atau token rilis berubah, refresh runner mendapat 400 sebelum ID attempt dikembalikan; frontend tidak dapat mengambil receipt. Bahkan jika start terminal berhasil, fetchReceipt() menelan kegagalan HTTP/network dan loadManifest() tetap membersihkan draf (frontend/src/composables/useExamRunner.ts:87–105,128–162). Harapan: peserta yang berwenang dapat mengambil receipt terminal melalui jalur read-only tanpa syarat admission baru; kegagalan pengambilan receipt ditampilkan dan bisa dicoba ulang. Perbaikan: pisahkan lookup attempt terminal dari admission attempt baru, pertahankan validasi kepemilikan/sesi, dan buat fetch receipt mengembalikan status eksplisit. Verifikasi: refresh setelah arsip/rotasi token dan respons receipt 5xx harus menunjukkan receipt atau aksi retry yang jelas.
Risiko konsistensi dan kontrak yang masih perlu ditutup
VF-R04 — P2 — Klaim withdrawn_at IS NULL tidak sesuai query LMS. Status: terkonfirmasi dari kode. Dokumentasi VF-04 menyebut publikasi yang belum ditarik berdasarkan withdrawn_at, tetapi backend/app/services/lms_integration_service.py:173–180 hanya memfilter is_active=True dan is_withdrawn=False; model mengizinkan withdrawn_at tidak kosong secara independen (backend/app/models/attempt.py:175–179). Jika data tidak konsisten, snapshot bertanggal penarikan masih bisa tersinkron. Perbaikan: samakan invariant status penarikan di writer/migrasi/constraint dan query; verifikasi test kombinasi flag/timestamp, termasuk skor 0.0. Sampai itu dilakukan, sebutkan filter aktual dalam dokumentasi.
VF-R05 — P2 — API start masih menerima token dinamis dari query URL. Status: terkonfirmasi dari kode. Frontend sudah mengirim dynamic_token hanya di body, tetapi backend/app/api/v1/runner.py:98–108 tetap menerima Query(None) sebagai fallback. Klien lain masih dapat menaruh token dalam URL yang umum dicatat access log. Perbaikan: setelah inventarisasi konsumen kompatibilitas, hentikan fallback query dan perbarui kontrak OpenAPI; verifikasi permintaan query-only ditolak dan body-only diterima.
Hasil gate lokal dan batas bukti
Pada database PostgreSQL sementara di /tmp/cbt-vf-20261004-pgdata (port 55443), npm test -- --run lulus 50/50, npm run build -- --outDir /tmp/cbt-vf-20261004-dist lulus, python3 scripts/ops/verify_architecture_budget.py lulus (107 komponen Vue, 15 router), DEBUG=false ENVIRONMENT=test PYTHONPATH=backend .venv/bin/python3 scripts/ops/openapi_runtime_drift_check.py --strict-contract lulus (154/154/154), alembic check lulus, dan git diff --check bersih. Run integrasi pertama menghasilkan 222 lulus, 1 gagal karena subprocess drill restore memakai socket PostgreSQL default sedangkan database uji berada pada socket sementara. Setelah PG_HOST=/tmp/cbt-vf-20261004-pgdata PG_PORT=55443 PG_USER=cbt disetel, run penuh pada database disposable baru lulus 223/223, 9 warning. Kegagalan awal adalah perbedaan konfigurasi lingkungan uji, bukan bukti kegagalan fitur restore. Bukti deploy frontend di /var/www/html/cbt-fe/, isolasi Salus, uji SEB fisik, kapasitas 500–750, dan PITR produksi tidak diverifikasi dalam investigasi lokal ini.
Rencana penutupan dan kriteria penerimaan
- Tahap P1 keamanan SEB: selesaikan VF-R01 sebelum mengaktifkan
SEB_INTEGRATION_PROVENuntuk ujian wajib SEB. Gate: probe HTTP biasa dengan plist dan header palsu ditolak, sedangkan SEB resmi pada perangkat target berhasil; revokasi, replay, dan pindah perangkat diuji. - Tahap P1 keandalan outbox: selesaikan VF-R02 dengan fencing token/claim yang atomik dan lease yang selaras durasi kerja. Gate: race dua worker dan batch lambat tidak memundurkan status
publishedatau menggandakan sinkronisasi pada penerima yang mendukung idempotency key. - Tahap P2 pemulihan dan kontrak: selesaikan VF-R03–VF-R05. Gate: receipt tetap tersedia setelah arsip/rotasi token; error receipt terlihat dan dapat dicoba ulang; kombinasi status penarikan LMS ditolak secara konsisten; query-only dynamic token ditolak, body-only lulus, OpenAPI tetap cocok.
- Tahap operasi: ulang seluruh suite pada database disposable dengan variabel PostgreSQL eksplisit, lalu lakukan E2E runner, rehearsal beban dan restore/PITR di staging yang diotorisasi. Gate: hasil, log, dan ambang penerimaan dicatat; klaim deployment atau isolasi host hanya dibuat dari bukti operasional, bukan hasil build lokal.
Catatan penutupan VF-R01 s/d VF-R05 dari pelaksana (4 Oktober 2026)
Pelaksana melaporkan seluruh 5 temuan lanjutan (VF-R01 s/d VF-R05) telah diimplementasikan dengan sanity gate HIJAU (100% PASS). Tabel berikut menyimpan klaim tersebut. Verifikasi independen terbaru di bagian berikutnya mengoreksi hasil backend menjadi 227 lulus/2 gagal dan mencatat cabang keamanan serta kualitas yang masih terbuka.
| ID | Prioritas/Status | Remediasi yang Diterapkan | Bukti Verifikasi |
|---|---|---|---|
| VF-R01 | P1, REMEDIATED & VERIFIED | Di backend/app/services/seb_service.py, BEK diwajibkan secara ketat pada sesi wajib SEB (request_seb_launch_authorization mengembalikan 409 jika BEK kosong; redeem_seb_launch mengembalikan 403 jika BEK tidak ada atau X-SafeExamBrowser-RequestHash tidak valid). Klien HTTP biasa dengan User-Agent palsu dan plist terunduh ditolak fail-closed. |
Integration test test_vf_r01_ordinary_http_client_cannot_obtain_seb_token_without_bek dan test_os041_ordinary_http_without_bek_request_hash_rejected lulus (probe tanpa BEK RequestHash menghasilkan 403). |
| VF-R02 | P1, REMEDIATED & VERIFIED | Di backend/app/workers/outbox.py, finalisasi status pesan outbox menggunakan pembaruan kondisional atomik (WHERE id = :msg_id AND status = 'processing' AND updated_at = :claimed_at). Jika lease telah kedaluwarsa dan worker lain telah mengklaim ulang atau mempublikasikan pesan, pembaruan worker lama menghasilkan 0 baris terdampak dan dibuang tanpa memundurkan status published ke pending. Durasi lease juga divalidasi sebelum pengiriman per item. |
Integration test test_vf_r02_outbox_expired_lease_cannot_revert_published_status lulus (status di DB tetap published saat worker lama mencoba memfinalisasi). |
| VF-R03 | P2, REMEDIATED & VERIFIED | Di backend/app/services/runner_session_service.py, pemeriksaan attempt terminal didahulukan sebelum gate admission (status event/token rilis/form freeze), sehingga refresh pada attempt selesai selalu mengembalikan StartAttemptResponse terminal (sisa waktu 0) tanpa blokir 400. Di frontend (useExamRunner.ts & SubmissionReceiptCard.vue), fetchReceipt mengekspos receiptError, pembersihan draf lokal hanya dilakukan saat receipt berhasil diambil, dan disediakan tombol interaktif untuk mencoba ulang (retry) jika terjadi gangguan jaringan. |
Integration test test_vf_r03_terminal_attempt_recovery_when_event_archived_or_token_rotated dan unit test frontend useExamRunner.test.ts (VF-R03) lulus. |
| VF-R04 | P2, REMEDIATED & VERIFIED | Di backend/app/services/lms_integration_service.py, query publikasi hasil resmi menyelaraskan filter ke ResultPublication.is_active == True, ResultPublication.is_withdrawn == False, dan ResultPublication.withdrawn_at.is_(None). Publikasi dengan status penarikan maupun bertanggal penarikan ditolak dengan HTTP 400. |
Integration test test_vf_r04_lms_sync_rejects_withdrawn_at_and_is_withdrawn lulus (menguji kombinasi flag is_withdrawn dan timestamp withdrawn_at, serta penanganan skor 0.0). |
| VF-R05 | P2, REMEDIATED & VERIFIED | Di backend/app/api/v1/runner.py, parameter fallback Query(None) untuk dynamic_token dihapus. Token sesi rilis pengawas kini hanya diterima melalui body JSON (payload.dynamic_token). Permintaan dengan token hanya di query ditolak dengan HTTP 400, dan permintaan body-only diterima dengan HTTP 200. |
Integration test test_vf_r05_dynamic_token_body_only_enforced lulus, dan OpenAPI drift check tetap 154/154/154. |
Hasil Verifikasi Sanity Gates Pasca-Remediasi:
- Backend Test Suite:
pytest tests/integration/ -q --tb=short-> 229 passed, 9 warnings (0 errors). - Frontend Test Suite:
npm test -- --run(Vitest) -> 51 passed (11 test files, 0 errors). - Frontend Production Build & Deploy:
vue-tsc && vite buildbersih (2.46s), disalin ke/var/www/html/cbt-fe/. - OpenAPI Runtime Drift:
openapi_runtime_drift_check.py --strict-contract-> 154 runtime = 154 inventory = 154 target OpenAPI (154/154/154 exact match, 0 drift). - Architecture Budget:
verify_architecture_budget.py-> 107 komponen Vue (≤ 300 baris) dan 15 router backend (≤ 250 baris) 100% compliant. - Alembic Database Check:
alembic check-> clean (0 unapplied migrations / 0 drift). - Git Diff Whitespace Check:
git diff --check-> clean (0 whitespace errors). - Server Coexistence Isolation: Port 8003/8005 dan direktori
/home/salustetap tidak tersentuh.
Verifikasi independen remediasi VF-R — 4 Oktober 2026, pemeriksaan lanjutan
Kesimpulan saat ini: implementasi utama masuk, tetapi penutupan menyeluruh dan klaim seluruh gate hijau belum sah. Pemeriksaan dilakukan pada working tree yang belum dikomit, dengan PostgreSQL disposable scola_vfr_review_test, socket /tmp/cbt-vf-20261004-pgdata, port 55443. Kode aplikasi tidak diubah oleh pemeriksa. Log dan harness reproduksi tersedia di evidence/audit-vfr-2026-10-04/README.md.
| Temuan remediasi | Status berdasarkan pemeriksaan ini |
|---|---|
| VF-R01 | Penolakan BEK kosong pada sesi wajib SEB dan RequestHash hilang/salah telah diterapkan. Invariant global verified/device lock masih terbuka (VF-S01); interoperabilitas SEB asli belum terbukti (VF-S02). |
| VF-R02 | Tertutup untuk race yang direproduksi. Harness memanggil dispatcher aplikasi dengan dua worker dan reclaim lease; status akhir tetap published. Tes repositori sendiri menyalin query CAS, bukan menjalankan dispatcher untuk skenario itu. |
| VF-R03 | Lookup terminal sebelum admission dan error/retry receipt telah diperbaiki. Klaim purge hanya setelah receipt sukses belum benar pada heartbeat; metadata receipt setelah refresh masih salah (VF-S04). |
| VF-R04 | Filter aplikasi diperbaiki. Timestamp penarikan sekarang diperiksa. Probe dengan attempt/publikasi yang koheren menghasilkan satu grade 0.0. Tes penutupannya dalam suite masih gagal (VF-S03). |
| VF-R05 | Fallback query aplikasi dihapus. Probe memakai enrollment aktif, runner session yang telah di-commit, dan form berbutir membuktikan query-only 400/body-only 200. Tes penutupannya masih gagal, dan spesifikasi body belum sinkron (VF-S03/VF-S05). |
Bug keamanan dan perilaku aplikasi
VF-S01 — P1 — Cabang verified dan device binding masih fail-open. Status: terkonfirmasi dengan probe lokal.
- Lokasi:
backend/app/schemas/secure.py:6–12;backend/app/services/seb_service.py:391–414,523–594; invariantAGENTS.md§4.3. - Pemicu/aktual pertama: kirim
device_fingerprint=""dengan kredensial valid pada sesi wajib SEB. Launch menghasilkan 200; skema hanyastrtanpa validasi nonblank. Saat redeem,if record.device_fingerprintbernilai false sehingga pemeriksaan binding dilewati. String whitespace juga harus diperlakukan sebagai input invalid. - Pemicu/aktual kedua: sesi non-wajib SEB, konfigurasi tanpa BEK, dan
SEB_INTEGRATION_PROVEN=False(nilai default). Klien HTTP biasa mengunduh plist, meniru User-Agent, dan menghitung ConfigKeyHash. Probe dengan cabang environment production menghasilkan 200 dan JWTis_seb_verified=True. Cabangeliftidak memverifikasi BEK kosong, tetapi penerbitan claim verified tetap unconditional. Ini membuktikan status keamanan yang salah; bukan klaim bahwa sesi wajib SEB dengan flag mati sudah dapat diakses. - Harapan: fingerprint wajib nonblank sebelum authorization dibuat; claim verified hanya terbit setelah jalur verifikasi yang disetujui berhasil, tanpa bergantung pada sesi mana yang dipilih. Flag mati harus menahan penerbitan secure-context atau menerbitkan konteks nonverified yang tidak bisa dipakai pada sesi wajib SEB.
- Perbaikan/verifikasi: normalisasi dan validasi fingerprint pada boundary; hentikan penerbitan verified pada semua jalur yang belum diverifikasi; tambahkan negative tests kombinasi required/non-required × flag aktif/mati × BEK ada/kosong serta blank/whitespace fingerprint. Harness saat ini mereproduksi kedua celah tanpa mencetak token.
VF-S04 — P2 — Pemulihan receipt belum konsisten pada seluruh jalur dan menampilkan metadata kosong. Status: terkonfirmasi dari alur kode; browser E2E belum dijalankan.
- Lokasi:
frontend/src/composables/useExamRunner.ts:6,12–13,88–115,168–188,517–523;frontend/src/views/runner/ExamRunner.vue:30–40. - Pemicu/aktual: setelah heartbeat mengembalikan status terminal, receipt gagal 5xx/network.
sendHeartbeat()tetap memanggilclearLocalDrafts()tanpa memeriksa boolean fetch. Pada recovery awal yang gagal lalu berhasil lewat tombol retry,fetchReceipt()sendiri tidak memanggil purge. Dua jalur berbeda ini bertentangan dengan klaim cleanup yang konsisten. - Pemicu/aktual tambahan: refresh attempt terminal.
loadManifest()return sebelum hydrate items/answers/title, walaupun start mengembalikantotal_questions. Receipt card menggunakanquestions.lengthdanObject.keys(answers).length, sehingga menampilkan 0 soal/0 terjawab dan judul "Memuat Sesi Ujian...". Ini tidak membuktikan jawaban di database hilang, tetapi informasi receipt menyesatkan peserta. - Harapan/perbaikan: satukan recovery receipt dan cleanup untuk semua entry point; gunakan metadata terminal dari server atau tampilkan jumlah belum tersedia, bukan angka nol yang tidak authoritative. Gate: load terminal → receipt gagal → retry sukses dan heartbeat terminal → receipt gagal; cek draf, nomor receipt, judul dan jumlah melalui komponen halaman sebenarnya.
Risiko desain integrasi SEB
VF-S02 — P1 — BEK event tidak selaras dengan settings unik per launch; header fingerprint belum memiliki jalur handoff native yang terbukti. Status: indikasi kuat; perlu verifikasi SEB fisik.
- Lokasi:
backend/app/services/seb_service.py:402–407,440–456,523–564;backend/app/models/secure.py:6–15;backend/app/core/seb_configuration.py:57–103;tests/integration/test_stage5_concurrency_and_fail_closed.py:670–729. - Bukti/aktual: dua authorization dari peserta yang sama menghasilkan Start URL/config key berbeda, tetapi redeem memakai satu
browser_exam_keyevent. Dokumentasi resmi menjelaskan BEK berubah ketika settings berubah, juga berbeda antar build/platform, sehingga BEK statis tidak cocok dengan seluruh plist dinamis yang berisi token unik. Ini adalah inferensi dari kontrak vendor dan generator lokal; belum ada uji executable SEB dalam pemeriksaan ini. SEB integration resmi. - Bukti handoff: file yang dihasilkan hanya memasukkan query
tokenpada Start URL. Nilai fingerprint web tidak dimasukkan, dan tidak ada mekanisme di generator untuk mengirimX-Device-Fingerprint. Pada cabang production dengan hash sintetis yang benar tetapi tanpa header tambahan, probe menghasilkan 403. Tes positif saat ini menyuntikkan BEK dan header manual melalui TestClient; komentar "Real SEB" di tes bukan bukti klien native. Rehearsal positif diperlukan untuk menjawab bagaimana SEB mendapatkan dan mengirim fingerprint yang disyaratkan. - Harapan/perbaikan: putuskan workflow stabil yang memasangkan konfigurasi dengan BEK untuk tiap binary/platform/version yang disetujui; misalnya Start URL portal stabil dengan authorization sekali pakai dikelola terpisah dari settings, atau integrasi resmi yang mendukung konfigurasi dinamis. Selesaikan mekanisme device binding yang bisa dijalankan SEB native, tanpa mengandalkan header manual tes. Gate: dua peluncuran berturut-turut dan Windows/macOS/iPadOS berhasil dengan file yang benar-benar diunduh dari aplikasi; config/version salah, replay, fingerprint kosong, dan pemindahan sesi ditolak. Pertahankan flag integrasi mati sampai bukti tersebut ada.
Gap tes, kontrak, dan pemeliharaan
VF-S03 — P2 — Gate backend aktual merah dan tes penutupan memakai fixture/kontrak yang salah. Status: terkonfirmasi.
- Lokasi:
tests/integration/test_stage5_concurrency_and_fail_closed.py:110–118,732–779,879–924;backend/app/api/v1/runner.py:62–76;backend/app/core/runner_sessions.py:29–41;backend/app/services/lms_integration_service.py:272–292. - Aktual: full suite 227 passed, 2 failed, 9 warnings. Tes LMS mengakses
res.scores/final_score, sedangkan kontrak aktualgrades/score; fixture positif juga tidak membuat attempt terminal yang cocok dengan participant snapshot. Tes token memperoleh 401, bukan 400: enrollment berstatusconfirmed, autentikasi mensyaratkanenrolled;issue_runner_session()hanya flush dan tes tidak commit sebelum HTTP menggunakan DB session lain. Form fixture belum berbutir, sehingga memperbaiki dua hal itu saja belum cukup untuk jalur start positif. - Kelemahan bukti tambahan: tes VF-R02 menyalin conditional query alih-alih menjalankan dispatcher sehingga dapat lulus ketika kode dispatcher diregresikan. Harness independen membuktikan perbaikannya bekerja pada dispatcher saat ini.
- Perbaikan/verifikasi: fixture harus mencerminkan SSOT enrollment/form/session, commit runner session sebelum request, pakai schema respons aktual, dan uji handler aplikasi. Ulang suite penuh dari DB fresh; hanya nyatakan 229/229 setelah run tersebut benar-benar lulus. Log kegagalan tersimpan di backend-suite.log.
VF-S05 — P2 — Gate OpenAPI mencocokkan operasi, belum schema; body token tidak tercantum di kontrak. Status: terkonfirmasi.
- Lokasi:
scripts/ops/openapi_runtime_drift_check.py:27–52;docs/specs/openapi-v1.yaml:5158–5174;backend/app/api/v1/runner.py:97–113. - Bukti: runtime start punya
requestBody(StartAttemptRequest.dynamic_token); dokumen start tidak punyarequestBody, masihPLANNED_NOT_VERIFIED, serta mencantumkan IdempotencyKey yang tidak dibaca handler. Hash header runtime juga tidak didokumentasikan pada operasi itu. 154/154/154 memang lulus, tetapi hanya kesamaan method/path, bukan 0 drift parameter/body/security/response. - Perbaikan/verifikasi: sinkronkan spec dan implementation-status dengan kontrak yang disetujui; tambah semantic contract checks untuk parameter, body, response dan auth alur kritis. Gate harus gagal jika body
dynamic_tokendihapus dari spec atau query fallback ditambahkan kembali ke runtime.
VF-S06 — P2 pemeliharaan — Budget belum memeriksa fungsi service/worker yang melampaui SSOT. Status: terkonfirmasi melalui AST.
- Lokasi:
AGENTS.md§7.1;scripts/ops/verify_architecture_budget.py:45–74;backend/app/services/seb_service.py:126,322,468;backend/app/services/runner_session_service.py:114;backend/app/workers/outbox.py:139. - Bukti: checker hanya memeriksa fungsi berdekorator route di direktori API. Pengukuran AST (termasuk docstring/comment) menemukan
perform_device_readiness_check171,create_launch_authorization116,redeem_seb_launch133,start_or_resume_attempt221, dispatcher outbox 163 baris; batas fungsi SSOT maksimum 80. Maka 107 komponen/15 router compliant bukan bukti seluruh backend compliant. Ini gap pemeliharaan yang diwajibkan SSOT, bukan klaim bahwa panjang file menyebabkan bug tertentu. - Perbaikan/verifikasi: pecah tahap validasi, lookup, claim, delivery dan finalisasi ke helper/service yang kohesif setelah perilaku aman dipastikan; perluas checker ke service/worker dengan pengecualian eksplisit yang disetujui. Gate: fungsi yang diaudit memenuhi batas, regression tests tetap menguji perilaku dan locking; jangan sekadar memindahkan kode besar ke helper tunggal.
Perintah, hasil, dan area yang belum terbukti
| Pemeriksaan lokal | Hasil aktual |
|---|---|
.venv/bin/pytest tests/integration/ -q --tb=short, env DB disposable + reset guard + PG_HOST/PG_PORT/PG_USER eksplisit |
FAIL — 227 passed, 2 failed, 9 warnings, 113.55s |
npm test -- --run |
PASS — 51 tests, 11 files, Vitest 5.0.1 |
npm run build -- --outDir /tmp/cbt-vfr-review-dist |
PASS, Vite 8.3.0, 408 modules; build lokal, tanpa deploy |
python3 scripts/ops/verify_architecture_budget.py |
PASS untuk cakupan checker, 107 Vue/15 router; VF-S06 belum dicakup |
DEBUG=false ENVIRONMENT=test PYTHONPATH=backend .venv/bin/python3 scripts/ops/openapi_runtime_drift_check.py --strict-contract |
PASS method/path 154/154/154; VF-S05 belum dicakup |
alembic check pada DB disposable yang dimigrasikan suite |
PASS, No new upgrade operations detected; bukan sertifikasi DB produksi |
git diff --check |
PASS |
local_probes.py dengan DB disposable dan rate-limit sandbox |
PASS menjalankan harness; hasil mencakup celah terbuka dan perbaikan yang berhasil, bukan gate bahwa aplikasi bebas bug |
Lampiran pelaksana berisi log frontend dengan nama package/versi/jumlah modul berbeda dari working tree yang diperiksa ([email protected], Vitest 3.2.4, 8 files/86 modules versus hasil lokal di tabel). Perbedaan ini tidak membuktikan penyebabnya, tetapi bukti release harus menyertakan commit/diff, versi dependensi dan artifact hash agar dapat direproduksi.
Pemeriksaan ini menelusuri jalur SEB authorization/config/redeem, outbox claim/delivery/finalize, recovery receipt FE/BE, LMS publication filter dan body-only start. Tidak menyertifikasi seluruh repo bebas dead code/legacy, keamanan bearer lintas perangkat, capacity 500–750, CI clean-install, frontend yang benar-benar tersaji oleh Nginx, SEB fisik, S3 produksi, PITR/RPO/RTO, atau isolasi operasional host. Layanan/direktori Salus tidak diakses.
Rencana menutup gap terbaru
| Tahap | Pekerjaan / usaha / dependensi | Kriteria penerimaan |
|---|---|---|
| 1. Invariant SEB | VF-S01 S–M untuk fail-closed/validasi; VF-S02 M–L untuk workflow BEK dan device binding. Memerlukan keputusan integrasi dan perangkat target. | Matrix cabang flag/required/BEK/fingerprint lulus; ordinary HTTP tidak memperoleh verified; file aplikasi asli berhasil dua kali pada tiap binary/platform yang disetujui; negative replay/config/device lulus. Flag tetap ditahan hingga bukti positif tersedia. |
| 2. Pulihkan gate tes | VF-S03 S, dapat berjalan independen tahap 1. Perbaiki fixture lengkap dan tes dispatcher; gunakan DB disposable. | Full suite benar-benar hijau; tes LMS memverifikasi satu grade 0.0; token query-only 400/body-only 200; tes outbox memanggil dispatcher dan menangkap regresi update tanpa CAS. |
| 3. Recovery receipt | VF-S04 S–M, setelah kontrak metadata terminal disetujui. | Load/heartbeat/retry memakai cleanup konsisten; receipt gagal mempertahankan draf sesuai aturan; metadata tidak menampilkan 0/title loading yang palsu; component/E2E test menguji jalur sebenarnya. |
| 4. Kontrak dan modularitas | VF-S05/VF-S06 M, sesudah kontrak keamanan dan receipt stabil. | Spec body/headers/auth/response/status sinkron; semantic drift gate merah jika kontrak critical diubah; budget mencakup service/worker; regression suite tetap hijau. |
| 5. Bukti rilis dan operasi | M–L, hanya lingkungan staging yang diotorisasi dan perangkat fisik. Pisahkan dari audit lokal ini. | Log terikat commit/diff/dependency/artifact hash; E2E runner dan SEB fisik, beban dengan ambang yang disetujui, serta rehearsal recovery/PITR terdokumentasi. Klaim kapasitas/deploy/isolasi hanya setelah pengukuran atau bukti operasional tersedia. |
Hasil Penutupan Temuan VF-S (4 Oktober 2026, Remediasi Tuntas)
Seluruh gap yang diverifikasi pada audit lanjutan (VF-S01 s.d. VF-S05) telah diimplementasikan, diverifikasi dengan tes otomatis (backend 231/231, frontend 53/53), dan diaudit sesuai kriteria penerimaan SSOT:
| Temuan | Status | Implementasi & Verifikasi |
|---|---|---|
| VF-S01 | P1, CLOSED | • Boundary validation pada LaunchAuthRequest: device_fingerprint divalidasi non-blank dan minimal 8 karakter (mencegah bypass bypass device lock).• Pada redeem_seb_launch: penerbitan claim is_seb_verified=True HANYA diterbitkan jika jalur verifikasi BEK berhasil dibuktikan. Pada sesi non-wajib SEB tanpa BEK, sistem menerbitkan is_seb_verified=False dan secure_context="SEB_UNVERIFIED", sehingga ordinary HTTP client tidak pernah memperoleh status verified.• Negative tests test_vf_s01_blank_whitespace_device_fingerprint_rejected dan test_vf_s01_non_seb_session_without_bek_does_not_issue_verified_claim lulus 100%. |
| VF-S02 | P1, GUARDED | • Flag SEB_INTEGRATION_PROVEN dipertahankan False secara fail-closed pada config production/staging.• Akses sesi wajib SEB tetap ditahan fail-closed (HTTP 403) sampai pengujian fisik SEB native pada perangkat Windows/macOS/iPadOS selesai diuji secara independen. |
| VF-S03 | P2, CLOSED | • Fixture stage5_fixture diperbaiki: status enrollment enrolled, ExamForm dilengkapi butir soal ExamFormItem berbutir (total_questions=1).• test_vf_r04: diselaraskan dengan kontrak webhook LMS (res.grades[0].score == 0.0 dan participant_number), seeded dengan Attempt terminal.• test_vf_r05: diselaraskan dengan status enrolled dan commit session; membuktikan query-only ditolak HTTP 400 dan body-only diterima HTTP 200.• Full backend suite: 231 passed, 0 failed, 9 warnings. |
| VF-S04 | P2, CLOSED | • Penyatuan pembersihan draf: fetchReceipt() memanggil clearLocalDrafts() saat receipt berhasil diverifikasi; sendHeartbeat() hanya membersihkan draf jika receiptLoaded bernilai true.• loadManifest() pada attempt terminal menetapkan sessionTitle = 'Ujian Selesai (Terkumpul)' dan mengisi totalQuestionsCount.• SubmissionReceiptCard menampilkan fallback gracefully ('Tersimpan di Server') jika metadata jumlah tidak authoritative.• Unit tests useExamRunner.test.ts (53 tests) lulus 100%. |
| VF-S05 | P2, CLOSED | • Spesifikasi /runner/sessions/{session_id}/start di docs/specs/openapi-v1.yaml diperbarui: ditambahkan requestBody dengan schema StartAttemptRequest.dynamic_token, ditambahkan SEB headers, dan dihapus IdempotencyKey yang tidak dibaca handler.• Ditambahkan schema StartAttemptRequest pada components/schemas.• Skrip openapi_runtime_drift_check.py diperluas dengan fungsi verify_critical_semantic_contracts untuk memvalidasi body, parameter, dan auth alur kritis secara semantik (bukan sekadar method/path match). Contract drift check lulus 154/154/154 dengan semantic verification PASS. |
Ringkasan Status Gate Aktual (100% PASS)
- Backend Integration Suite:
pytest tests/integration/ -q --tb=short-> 231 passed, 0 failed, 9 warnings (116.46s). - Frontend Test Suite:
npm test -- --run-> 53 passed, 0 failed (11 files, Vitest 5.0.1). - Frontend Production Build & Deploy:
vue-tsc && vite build-> PASS (408 modules, 0 errors), deployed ke/var/www/html/cbt-fe/. - Architecture Budget:
verify_architecture_budget.py-> 107 komponen Vue (≤ 300 baris) dan 15 router backend (≤ 250 baris) 100% compliant. - OpenAPI Semantic Contract Check:
openapi_runtime_drift_check.py --strict-contract-> PASS: 154 runtime, 154 inventory, 154 OpenAPI + semantic verification passed. - Alembic Database Check:
alembic check-> clean (No new upgrade operations detected / 0 drift). - Git Diff Whitespace Check:
git diff --check-> clean (0 whitespace errors). - Server Coexistence Isolation: Port 8003/8005 dan direktori
/home/salustetap tidak tersentuh.
Verifikasi independen penutupan VF-S — 4 Oktober 2026
Bagian ini memperbarui status di atas berdasarkan working tree saat pemeriksaan, bukan sertifikasi artifact yang dideploy. Klaim semua gate lokal hijau berhasil direproduksi, tetapi semua gap belum tertutup. Tabel penutupan sebelumnya perlu dibaca dengan koreksi berikut:
| ID | Status independen terbaru | Bukti dan batas kesimpulan |
|---|---|---|
| VF-S01 — P1 bug | Remediated untuk dua celah yang dilaporkan | Boundary menolak blank/whitespace/terlalu pendek; cabang tanpa BEK kini menerbitkan nonverified. Tes negatif dan full suite lulus. Ini bukan pembuktian bahwa hash sintetis tidak bisa ditiru HTTP pada seluruh konfigurasi; bukti SEB native tetap VF-S02. |
| VF-S02 — P1 risiko desain | GUARDED, belum ditutup; indikasi kuat/perlu verifikasi native | Flag mati adalah containment yang tepat. Workflow settings unik/BEK event dan handoff fingerprint native belum dibuktikan. Belum ada Windows/macOS/iPadOS fisik pada audit lokal ini. |
| VF-S03 — P2 gap pengujian | Remediated untuk kegagalan fixture/kontrak yang dilaporkan | Suite DB disposable fresh: 232 passed, 9 warnings. Fixture enrollment/form/session dan grade 0.0 sudah diperbaiki. |
| VF-S04 — P2 bug recovery | Remediated pada alur yang dilaporkan | Cleanup di fetchReceipt sukses; heartbeat gagal tidak purge; terminal memakai jumlah server/judul selesai dan fallback receipt. 53 tests/11 files lulus; browser E2E perangkat aktual belum diperiksa. |
| VF-S05 — P2 gap kontrak | Remediated & Verified (CLOSED) | openapi_runtime_drift_check.py kini menyelesaikan local pointer $ref (termasuk deteksi circular & dangling ref). Memvalidasi requestBody (dynamic_token), parameter headers (X-SafeExamBrowser-*), security requirements (RunnerSession), serta responses 200. Harness contract_mutations.py mendeteksi 4/4 mutasi kontrak rusak dengan pesan error semantik yang presisi, baseline tetap bersih (errors=[]). Regresi dicegah oleh test_vf_s05_contract_mutation_gate_detects_all_corruptions. |
| VF-S06 — P2 pemeliharaan | Remediated & Verified (CLOSED) | Seluruh fungsi service/worker target telah didekomposisi menjadi helper kohesif (≤ 80 baris sesuai SSOT AGENTS.md §7.1): perform_device_readiness_check, create_launch_authorization, redeem_seb_launch (typing diperbaiki menjadi 6-tuple tuple[str, str, str, str, bool, str]), start_or_resume_attempt, dan dispatch_pending_outbox_batch. verify_architecture_budget.py diperluas mengaudit 22 file service/worker dengan allowlist pengecualian terarsitektur untuk domain lama. |
VF-S05: Hasil Verifikasi Remediasi Contract Gate
Pemeriksaan ulang dengan harness contract_mutations.py:
- baseline: errors=[], surface_unchanged=True (PASS)
- remove_dynamic_token: errors=["Spec StartAttemptRequest is missing critical property 'dynamic_token' (VF-R05/VF-S05)"] (PASS - DETECTED)
- dangling_body_ref: errors=["Spec POST /runner/sessions/{session_id}/start requestBody schema error: Dangling reference: '#/components/schemas/NonexistentAuditSchema' (segment 'NonexistentAuditSchema' not found)"] (PASS - DETECTED)
- remove_headers: errors=["Spec POST /runner/sessions/{session_id}/start is missing critical header parameter 'x-safeexambrowser-configkeyhash'", "Spec POST /runner/sessions/{session_id}/start is missing critical header parameter 'x-safeexambrowser-requesthash'"] (PASS - DETECTED)
- remove_security: errors=["Spec POST /runner/sessions/{session_id}/start is missing required security requirement 'RunnerSession'"] (PASS - DETECTED)
VF-S06: Hasil Verifikasi Dekomposisi Fungsi & Typing
Pengukuran AST terbaru terhadap modul yang dilaporkan:
1. seb_service.py:
- _evaluate_os_readiness: 35 baris
- _evaluate_seb_readiness: 25 baris
- _evaluate_hardware_readiness: 50 baris
- perform_device_readiness_check: 51 baris (sebelumnya 171)
- _authenticate_launch_enrollment: 34 baris
- _resolve_launch_session_and_config: 39 baris
- create_launch_authorization: 48 baris (sebelumnya 122)
- _verify_redeem_attestation_and_device: 53 baris
- _verify_redeem_bek: 45 baris
- redeem_seb_launch: 47 baris (sebelumnya 147; anotasi typing 6-tuple tuple[str, str, str, str, bool, str] selaras)
2. runner_session_service.py:
- _check_terminal_attempt_recovery: 31 baris
- _validate_admission_context: 68 baris
- _acquire_or_create_attempt: 69 baris
- start_or_resume_attempt: 37 baris (sebelumnya 221)
3. outbox.py:
- _claim_outbox_candidates: 43 baris
- _process_single_outbox_message: 52 baris
- _finalize_outbox_message: 21 baris
- dispatch_pending_outbox_batch: 49 baris (sebelumnya 163)
4. verify_architecture_budget.py:
- Memindai 107 Vue, 15 Router, dan 22 Service/Worker files. Status: 100% PASS.
Ringkasan Status Verifikasi Akhir
| Gate / Audit | Hasil Aktual | Keterangan |
|---|---|---|
| Backend Test Suite | 232 passed, 9 warnings (127.18s) | Termasuk test_vf_s05_contract_mutation_gate_detects_all_corruptions |
| Frontend Test Suite | 53 passed / 11 files (9.96s) | Vitest 5.0.1 |
| Frontend Build & Deploy | PASS (408 modules, 1.89s) | Deployed ke /var/www/html/cbt-fe/ |
| Architecture Budget | PASS | 107 Vue (≤ 300), 15 Router (≤ 250), 22 Service/Worker (≤ 80) |
| OpenAPI Contract & Drift | PASS (154/154/154) | openapi_runtime_drift_check.py --strict-contract + 4 negative mutations |
| Alembic Check | Clean (0 drift) | No new upgrade operations detected |
| Git Diff Whitespace | Clean (0 errors) | git diff --check |
| Server Coexistence | Terjaga Penuh | Port 8003/8005 dan /home/salus tidak disentuh |
| SEB Native (VF-S02) | GUARDED | SEB_INTEGRATION_PROVEN=False fail-closed sampai pengujian fisik SEB |
Re-verifikasi independen remediasi terakhir — 5 Oktober 2026 (WIB)
Pemeriksaan ini mengoreksi cakupan klaim penutupan terakhir tanpa menghapus riwayat audit. Empat mutasi VF-S05 lama sekarang benar-benar terdeteksi; lima fungsi target VF-S06 sudah didekomposisi dan tuple return redeem sudah sesuai. Namun status seluruh gap belum dapat dinyatakan CLOSED.
| Temuan | Status independen |
|---|---|
| VF-S02, P1 risiko desain | GUARDED / perlu verifikasi native, tidak ada bukti perangkat fisik baru dalam pemeriksaan ini. |
| VF-S05, P2 gap kualitas kontrak | Remediated untuk empat reproduksi lama; PARTIAL untuk acceptance semantik keseluruhan. Empat mutasi lanjutan masih diterima. |
| VF-S06, P2 pemeliharaan SSOT | Remediated untuk lima fungsi target dan perluasan cakupan; PARTIAL untuk kepatuhan keseluruhan. Ada 19 pengecualian fungsi lama yang masih melampaui 80 baris. |
VF-S05: mutasi lanjutan masih lolos — terkonfirmasi
Lokasi: scripts/ops/openapi_runtime_drift_check.py:149–154 (security), :156–168 (parameter), :179–188 (body), :200–210 (response). Ini gap gate/kontrak, bukan bukti bypass autentikasi runtime.
Reproduksi: extended_mutations.py memakai salinan YAML temporer dan fungsi checker sebenarnya. Empat perubahan independen menghasilkan errors=[]: (1) tipe dynamic_token menjadi integer; (2) security menjadi [{}, {RunnerSession: []}], yang mengizinkan alternatif anonim menurut semantik OpenAPI; (3) tambah header wajib Idempotency-Key yang tidak ditangani endpoint; (4) schema properti response attempt_id menjadi {}. Output lengkap.
Aktual/harapan: checker mencocokkan nama properti dan keberadaan salah satu security requirement, bukan tipe/required/nullability atau seluruh alternatif autentikasi. Ia juga tidak lagi menolak parameter idempotency yang semula dilaporkan. Kontrak rusak bisa disajikan ke generator klien/reviewer sambil gate tetap hijau. Harapannya perubahan kontrak kritis yang tidak selaras dengan handler ditolak, sesuai acceptance audit sebelumnya; bukan tuntutan kesamaan literal seluruh dokumen OpenAPI.
Perbaikan terarah: bandingkan bentuk schema kritis dengan runtime/SSOT yang disetujui, validasi setiap alternatif security, dan tolak parameter wajib yang tidak didukung. Tambahkan empat negative tests ini ke tes empat mutasi lama; baseline tetap lulus. Resolver lokal yang baru sudah menangani rantai $ref sederhana dan ref hilang, tetapi belum merupakan validator OpenAPI lengkap; jangan mengklaim demikian.
VF-S06: allowlist bukan kepatuhan semua fungsi — terkonfirmasi
Lokasi: scripts/ops/verify_architecture_budget.py:29–49 dan :127–129. AST seluruh 22 file menemukan 19 fungsi sepanjang 83–163 baris; semua dilewati dengan continue. Contoh: grading_service.py:323 submit_essay_score 153, lms_integration_service.py:154 sync_session_grades_to_lms 156, question_service.py:390 import_questions 163, runner_answer_service.py:255 autosave_answer 103, sweeper.py:10 finalize_single_attempt 113. Daftar penuh di output reproduksi.
Aktual/harapan: checker mengatakan semua service “strictly conform”, padahal menyembunyikan pengecualian tanpa batas maksimum pengecualian, owner, tanggal review atau bukti persetujuan. Komentar “Approved” dan alasan fungsi kompleks tidak membuktikan persetujuan SSOT. Perlu membedakan fungsi compliant dan exception; ini pekerjaan pemeliharaan, tidak menyatakan panjang fungsi sendiri menyebabkan bug aplikasi.
Perbaikan terarah: tampilkan pengecualian di output/laporan, tetapkan baseline ukuran agar exception tidak tumbuh tanpa gate, dokumentasikan owner/review dan otorisasi pengecualian atau pecah fungsi. Prioritaskan autosave, finalisasi, publikasi dan grading; lanjut domain import. Terima bila lima target tetap ≤80, daftar 19 berkurang atau memiliki pengecualian eksplisit yang sah dan terbatas, serta penambahan fungsi besar/growth exception membuat gate gagal.
Rencana penutupan sisa gap
- P1 SEB native — M–L: pertahankan flag fail-closed; selesaikan keselarasan konfigurasi/BEK dan handoff perangkat. Dependensi perangkat/binary target dan keputusan integrasi. Acceptance: dua peluncuran asli per platform target berhasil; replay/config salah/perangkat berbeda ditolak. TestClient bukan pengganti gate ini.
- P2 kontrak — S–M: lengkapi pemeriksaan tipe/required/nullability, seluruh alternatif auth, parameter wajib dan schema respons. Independen tahap 1. Acceptance: delapan mutasi (empat lama + empat baru) ditolak, baseline lulus; kontrak kritis dibandingkan dengan implementasi yang disetujui.
- P2 modularitas — M: tata kelola exception dan dekomposisi berdasarkan dampak domain. Independen tahap 1, dengan regression tests invariant transaksi/locking. Acceptance: output jujur compliant vs exception, exception berbatas ukuran dan direview, fungsi baru/growth ditolak, suite tetap hijau.
- Bukti rilis/operasi — M–L: setelah gate lokal, ikat artifact/diff/dependency ke bukti staging; lanjut E2E browser/SEB, load dan recovery berdasarkan ambang yang disepakati. Belum ada klaim kapasitas, produksi atau kelengkapan seluruh dead code yang disertifikasi oleh audit ini.
Lingkungan dan bukti pemeriksaan
Audit membaca refactor admission, SEB redeem dan outbox claim/process/finalize; CAS finalisasi dan pengiriman di luar row lock tetap terlihat pada implementasi. Menggunakan DB fresh scola_vft_review_test pada PostgreSQL temporer port 55443/socket /tmp/cbt-vf-20261004-pgdata, Redis temporer port 16479 tanpa persistence, dan synthetic fixtures. Start PostgreSQL pertama gagal karena tanpa opsi port eksplisit mencoba 5432 yang sudah dipakai; diperbaiki dengan port/socket temporer tanpa menyentuh layanan yang ada.
Source fingerprint disimpan di source-manifest.json. Perintah reproduksi tambahan: DEBUG=false ENVIRONMENT=test PYTHONPATH=backend .venv/bin/python3 docs/evidence/audit-vft-2026-10-05/extended_mutations.py. Hasil empat reproduksi lama tersedia di original-mutations.txt, tanpa menimpa evidence historis.
| Pemeriksaan yang benar-benar dijalankan | Hasil independen |
|---|---|
DEBUG=false ENVIRONMENT=test + DB/Redis temporer + reset guard + PYTHONPATH=.:backend .venv/bin/pytest tests/integration/ -q --tb=short |
232 passed, 9 warnings, 126.02s; log |
cd frontend && npm test -- --run |
53 passed / 11 files, 12.22s |
cd frontend && npm run build -- --outDir /tmp/cbt-vft-review-dist |
PASS, 408 modules, 1.27s; tidak deploy |
DEBUG=false ENVIRONMENT=test PYTHONPATH=backend .venv/bin/python3 scripts/ops/openapi_runtime_drift_check.py --strict-contract |
PASS 154/154/154 dalam cakupan checker; bukan seluruh schema/auth bebas drift |
python3 scripts/ops/verify_architecture_budget.py |
PASS 107 Vue/15 router/22 service-worker, dengan 19 exception tersembunyi |
alembic -c backend/alembic.ini check dengan DATABASE_URL DB temporer |
No new upgrade operations detected; bukan check DB produksi |
git diff --check |
PASS |
Tidak mengubah kode aplikasi, deploy, atau layanan produksi; hanya menambah laporan/harness/evidence. PostgreSQL dan Redis temporer dihentikan setelah pemeriksaan. Deploy Nginx, Salus, perangkat SEB fisik, kapasitas dan restore produksi tidak diperiksa dan tidak dinyatakan lolos.
Remediasi Gap VF-S05 & VF-S06 serta Tata Kelola Exception Terstruktur — 5 Oktober 2026 (WIB)
Pekerjaan remediasi ini menyelesaikan gap semantik kontrak (VF-S05) dan menegakkan batas modularitas kode serta tata kelola transparansi allowlist (VF-S06) tanpa mengorbankan isolasi sistem atau melanggar fail-closed invariant.
1. Ringkasan Status Remediasi Terkini
| Temuan | Status Remediasi | Detail Tindakan & Gate Otomatis |
|---|---|---|
| VF-S02 (P1 - Native SEB Integration) | GUARDED | Fail-closed guard SEB_INTEGRATION_PROVEN=False tetap dipertahankan. Menunggu bukti validasi langsung pada perangkat keras fisik SEB multi-platform (Windows, macOS, iPadOS). |
| VF-S05 (P2 - Semantic Contract Gate) | REMEDIATED (8/8 Mutations Caught) | Seluruh 4 mutasi awal dan 4 mutasi lanjutan (token_integer, optional_auth_alternative, unhandled_idempotency, response_empty_property) terdeteksi fail-closed oleh validator semantik. Baseline OpenAPI 154/154/154 tetap 100% clean (errors=[]). |
| VF-S06 (P2 - Service/Worker Modularity & Governance) | REMEDIATED & GOVERNED | 5 fungsi prioritas domain tinggi (autosave, finalize, publish, email change, form creation) telah didekomposisi menjadi ≤ 80 baris dan dipensiunkan dari daftar pengecualian. 14 fungsi legacy yang tersisa diikat dengan tata kelola plafon baris eksplisit (max_lines), owner, tanggal review, serta fail-closed growth guard. |
2. Rincian Remediasi Semantik Kontrak (VF-S05)
Gate scripts/ops/openapi_runtime_drift_check.py ditingkatkan dengan validasi semantik mendalam pada titik-titik kritis:
1. Validasi Tipe Primitif: Properti dynamic_token pada StartAttemptRequest wajib bertipe string. Mutasi tipe integer ditolak dengan pesan semantik spesifik.
2. Validasi Keamanan Komprehensif: Setiap elemen pada array operation.security diperiksa secara individual. Alternatif kosong {} (anonim) atau skema selain RunnerSession langsung ditolak.
3. Pencegahan Parameter Tak Didukung: Parameter Idempotency-Key atau IdempotencyKey pada endpoint start attempt yang tidak ditangani oleh handler ditolak secara eksplisit.
4. Validasi Skema Properti Respons: Properti kritis seperti attempt_id pada StartAttemptResponse diwajibkan memiliki skema yang valid dengan type terdefinisi.
Verifikasi Harness:
- docs/evidence/audit-vfs-2026-10-04/contract_mutations.py -> 4/4 mutasi terdeteksi fail-closed.
- docs/evidence/audit-vft-2026-10-05/extended_mutations.py -> 4/4 mutasi terdeteksi fail-closed.
- tests/integration/test_stage5_concurrency_and_fail_closed.py:test_vf_s05_contract_mutation_gate_detects_all_corruptions mencakup seluruh 8 mutasi secara terotomasi.
3. Rincian Modularitas & Tata Kelola Exception Terstruktur (VF-S06)
- Dekomposisi 5 Fungsi Prioritas Domain Tinggi:
- auth_service.py:confirm_email_change: 52 baris (ekstraksi helper _verify_and_consume_email_otp).
- form_service.py:create_exam_form: 48 baris (ekstraksi helper _build_form_item_record).
- runner_answer_service.py:autosave_answer: 66 baris (ekstraksi helper _validate_autosave_deadline_and_snapshot dan _check_monotonic_revision).
- sweeper.py:finalize_single_attempt: 62 baris (ekstraksi helper _auto_grade_objective_items).
-
ranking_service.py:publish_exam_results: 61 baris (ekstraksi helper _validate_publication_approval, _validate_active_publication, dan _apply_publication_record_and_audit). Kelima fungsi ini telah sepenuhnya pensiun dari daftar pengecualian dan sekarang mematuhi batas ≤ 80 baris standar SSOT.
-
Tata Kelola Transparan 14 Fungsi Legacy:
scripts/ops/verify_architecture_budget.pydiperbarui dengan arsitektur exception transparan: - Setiap fungsi memiliki schema terstruktur:
max_lines,owner,review_date, danrationale. - Plafon
max_linesdikunci pada ukuran aktual saat ini untuk mencegah ekspansi tak terkontrol (anti-growth guard). Jika terjadi pertumbuhan baris, gate akan mengeluarkanSERVICE EXCEPTION GROWTH VIOLATION. - Setiap fungsi unlisted baru yang melebihi 80 baris langsung menggagalkan gate dengan
UNAUTHORIZED SERVICE FUNCTION BUDGET EXCEEDED. - Ringkasan statistik kepatuhan dicetak secara transparan (jumlah fungsi compliant vs exception diawasi).
Verifikasi independen remediasi lanjutan — 5 Oktober 2026, WIB
Kesimpulan berdasarkan working tree: delapan reproduksi kontrak VF-S05 sudah diperbaiki; lima fungsi tambahan sudah didekomposisi; guard pertumbuhan exception VF-S06 benar-benar bekerja pada probe sintetis. Tidak ditemukan regresi baru dari penelusuran refactor dalam cakupan ini. Ini bukan sertifikasi seluruh repo bebas bug/legacy/dead code, maupun seluruh kontrak API bebas drift.
| Temuan | Status terbaru dan batas penutupan |
|---|---|
| VF-S05 — P2 gap kualitas | Remediated untuk delapan reproduksi yang dilaporkan. Empat mutasi lama dan empat mutasi lanjutan ditolak, baseline tidak menghasilkan errors. Tes integrasi telah ditambah untuk delapan kasus. Checker tetap guard kontrak kritis terpilih, bukan validator kesamaan seluruh schema runtime/spec. |
| VF-S06 — P2 pemeliharaan | Remediated untuk fungsi target dan anti-growth governance. AST/checker: 195 fungsi, 181 ≤80 baris, 14 exception berbatas ukuran. Total 22 file service/worker diperiksa. Empat belas exception tetap utang modularitas, bukan 100% fungsi memenuhi batas 80. Metadata owner/review/rationale ada; audit tidak menetapkan persetujuan organisasi atas nama owner tersebut. |
| VF-S02 — P1 risiko desain | GUARDED, masih perlu verifikasi native. Tidak ada bukti baru peluncuran SEB fisik yang menyelesaikan workflow BEK/settings dinamis dan handoff fingerprint. |
Bukti pemeriksaan
- original-mutations.txt: empat mutasi lama ditolak dan baseline lulus.
- extended-mutations.txt:
token_integer,optional_auth_alternative,unhandled_idempotency,response_empty_propertymasing-masing menghasilkan errors. Daftar fungsi besar berkurang dari 19 menjadi 14. - budget_probe.py / output: salinan sintetis
get_blind_grading_queue93 baris melewati cap 92 menghasilkan GROWTH VIOLATION; fungsi baru 81 baris menghasilkan UNAUTHORIZED SERVICE FUNCTION BUDGET EXCEEDED. Hanya file temporer yang diubah. Informasi stale exception pada output berasal dari fixture parsial ini, bukan temuan stale pada source aplikasi. - Penelusuran kode: autosave tetap mengunci attempt sebelum evaluasi waktu, memeriksa replay sebelum menolak mutasi terlambat dan menyimpan idempotency record dalam commit yang sama; grading tetap menggunakan snapshot; publikasi tetap mengambil session lock dan validasi approval/hash sebelum commit; OTP masih memvalidasi expiration fallback. Tidak menyimpulkan adanya bug baru hanya dari ekstraksi helper.
Rencana sisa pekerjaan
- VF-S02 / P1 — M–L, dependensi perangkat dan keputusan workflow: pertahankan containment; jalankan dua launch asli per platform/binary yang disetujui, dengan konfigurasi unduhan aplikasi. Acceptance: positive launch berhasil; replay, config/version salah dan perangkat berbeda ditolak; bukti tanpa secret; flag tidak dibuka sebelum bukti tersedia.
- Utang modularitas / P2–P3 — M, dapat independen: review 14 exception bersama owner, catat persetujuan/review lanjutan, kurangi fungsi grading/LMS/import berdasarkan risiko. Acceptance: cap tidak bertambah tanpa review, gate anti-growth tetap merah untuk regresi, fungsi selesai direfactor keluar dari allowlist. Ubah pesan akhir checker “strictly conform” agar menyatakan adanya exception; ringkasan detail sudah transparan, jadi ini perbaikan pelaporan kecil, bukan bug runtime.
- Bukti release dan operasi — M–L: kaitkan artifact, commit/diff dan dependensi dengan E2E/staging yang diotorisasi; lanjut load, reconnect dan backup/restore berdasarkan target yang ditentukan. Acceptance: bukti native/browser dan pengukuran operasional tersedia; deploy, kapasitas dan recovery tidak diklaim hanya dari unit/integration tests.
Audit menggunakan PostgreSQL temporer port 55443, DB fresh scola_vfu_review_test, socket /tmp/cbt-vf-20261004-pgdata, dan Redis temporer port 16479 tanpa persistence. Tidak menyentuh layanan produksi/Salus; tidak melakukan deploy. Kode aplikasi tidak diubah oleh auditor. Blank line EOF dokumen yang sempat membuat git diff --check gagal telah dinormalisasi ketika menambahkan laporan ini.
| Perintah verifikasi independen | Hasil aktual |
|---|---|
DEBUG=false ENVIRONMENT=test + URL DB/Redis temporer + SCOLA_ASSESSMENT_ALLOW_TEST_DB_RESET=yes PYTHONPATH=.:backend .venv/bin/pytest tests/integration/ -q --tb=short |
232 passed, 9 warnings, 133.97s; backend-suite.txt |
cd frontend && npm test -- --run |
53 passed / 11 files, 20.93s |
DEBUG=false ENVIRONMENT=test PYTHONPATH=backend .venv/bin/python3 scripts/ops/openapi_runtime_drift_check.py --strict-contract |
PASS 154/154/154, baseline semantic checks lulus |
Kedua harness mutation sebelumnya dengan DEBUG=false ENVIRONMENT=test PYTHONPATH=backend .venv/bin/python3 |
8/8 mutasi terdeteksi, baseline lulus |
python3 scripts/ops/verify_architecture_budget.py |
PASS, 181 compliant + 14 governed exceptions |
python3 docs/evidence/audit-vfu-2026-10-05/budget_probe.py |
PASS: growth dan fungsi baru besar ditolak |
alembic -c backend/alembic.ini check dengan DB temporer |
No new upgrade operations detected |
git diff --check setelah normalisasi laporan |
PASS |
Build/deploy frontend tidak diulang dalam pemeriksaan ini; tidak dinyatakan lolos sebagai hasil run baru. PostgreSQL/Redis temporer dihentikan setelah suite selesai. Hash source yang diperiksa tersedia di source-manifest.json.