diff --git a/CLAUDE.md b/CLAUDE.md index 1d5506d..5258ae5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ Multi-tenant biometric auth platform | Marmara University CSE4297 | Hexagonal Architecture **Status**: Production deployed. Phases 0-8 complete. ~1,900+ tests. All services healthy. -**Last verified**: 2026-05-12 (Carry-forward from 2026-05-11: 11 PRs shipped across 5 repos + Flyway repair on prod, V59/V60 applied, branch protection on 6 branches, master/main reconciled, INVESTIGATION 2026-05-07 P1 residue closed, tenant onboarding playbook + 8 ADRs + docs/ hierarchy consolidated, spoof-detector blink cache + EAR recalibration paper-P0. **Added today**: parent PR #57 (poster suite: A0 default + 4 style variants compliant with CSE4198 §5.1) + parent PR #58 (archived 18 dated 2026-04/2026-05-04 docs into `archive/2026-05/{audits,plans,reviews,roadmaps,sessions}/`, tidied `.gitignore`); bio PR #99 (closed issue #91: 32 stale unit tests + 3 asyncio-fixture leaks fixed, no production code touched, module-scoped TestClient pattern documented for follow-ups); bio Dependabot #97/#98 in flight (rebased post-#99). Submodule pointer for biometric-processor bumped to post-#99 main.) +**Last verified**: 2026-05-21 (2026-05-21: `links.fivucsas.com` hub — API tile → `/swagger-ui.html` with admin-IP "gated" badge (raw API root returned 401), Turkish i18n role-label fixes (English under `lang=tr` was İ-mangling Latin `i` under uppercase), team contact info, Ayşenur LinkedIn URL fix; poster author contact block + **regenerated A0 PDF/PNG** from `landing-website/public/poster/files/fivucsas-poster.html`; attribution — Ayşe Gülsüm Eren GitHub `@aysegulsum` + `marun.edu.tr` academic emails across `spoof-detector` + `practice-and-test` (forensic git-author records left intact); bilingual TR/EN switchers completed on `bys-demo`/`docs-site`/`verify-widget`. Consolidated into PR #69 → `master` (whole `fix/2026-05-12-bake-mini-fasnet-models` branch). NOTE: `api.fivucsas.com/` returns 401 by design (it's an API origin, not a page); Swagger/`/v3/api-docs`/`/actuator` are admin-IP gated (403 public), OIDC discovery is public (200). Carry-forward from 2026-05-12 / 2026-05-11: 11 PRs shipped across 5 repos + Flyway repair on prod, V59/V60 applied, branch protection on 6 branches, master/main reconciled, INVESTIGATION 2026-05-07 P1 residue closed, tenant onboarding playbook + 8 ADRs + docs/ hierarchy consolidated, spoof-detector blink cache + EAR recalibration paper-P0. **Added today**: parent PR #57 (poster suite: A0 default + 4 style variants compliant with CSE4198 §5.1) + parent PR #58 (archived 18 dated 2026-04/2026-05-04 docs into `archive/2026-05/{audits,plans,reviews,roadmaps,sessions}/`, tidied `.gitignore`); bio PR #99 (closed issue #91: 32 stale unit tests + 3 asyncio-fixture leaks fixed, no production code touched, module-scoped TestClient pattern documented for follow-ups); bio Dependabot #97/#98 in flight (rebased post-#99). Submodule pointer for biometric-processor bumped to post-#99 main.) ## Architecture @@ -26,6 +26,7 @@ Storage: PostgreSQL 17 + pgvector | Redis 7.4 | Landing Site | https://fivucsas.com | | Auth Widget / SDK | https://verify.fivucsas.com | | BYS Demo | https://demo.fivucsas.com | +| amispoof — browser anti-spoof tester | https://amispoof.fivucsas.com/ (old https://fivucsas.com/amispoof/ now 301s to the subdomain) | | Uptime Monitor | https://status.fivucsas.com | | Swagger | https://api.fivucsas.com/swagger-ui.html (admin-IP-gated since IN-H2 2026-04-19; allowlist in `infra/traefik/config/dynamic.yml`) | @@ -62,6 +63,27 @@ scp -P 65002 -r dist/* u349700627@46.202.158.52:~/domains/app.fivucsas.com/publi # BYS demo deploy scp -P 65002 -r /opt/projects/fivucsas/bys-demo/* u349700627@46.202.158.52:~/domains/demo.fivucsas.com/public_html/ +# amispoof deploy (TypeScript spoof-detector + webcam tester to amispoof.fivucsas.com) +# Migrated 2026-05-17 from fivucsas.com/amispoof/ → amispoof.fivucsas.com subdomain. +# Old URL serves a 301 to the new one via ~/domains/fivucsas.com/public_html/amispoof/.htaccess. +# We deploy to the NEW subdomain root; the lazy chunks under lib/ + models/ need to be sent too. +cd /opt/projects/fivucsas/spoof-detector/web +npm run build && npm run amispoof:bundle +scp -P 65002 amispoof/index.html amispoof/app.js u349700627@46.202.158.52:~/domains/amispoof.fivucsas.com/public_html/ +scp -P 65002 amispoof/lib/spoof-detector.js amispoof/lib/spoof-detector.js.map amispoof/lib/spoof-detector-*.js amispoof/lib/spoof-detector-*.js.map u349700627@46.202.158.52:~/domains/amispoof.fivucsas.com/public_html/lib/ +# Models only need to be sent once after the subdomain is created; subsequent deploys can skip these. +# scp -P 65002 amispoof/models/minifasnet_v2.onnx amispoof/models/face_landmarker.task u349700627@46.202.158.52:~/domains/amispoof.fivucsas.com/public_html/models/ + +# Deploy links hub (links.fivucsas.com — single static index.html) +scp -P 65002 /opt/projects/fivucsas/links-website/index.html u349700627@46.202.158.52:~/domains/links.fivucsas.com/public_html/index.html + +# Regenerate + deploy the poster PDF/PNG from the canonical HTML (A0 841×1189mm). +# Canonical poster = landing-website/public/poster/files/fivucsas-poster.html (served at fivucsas.com/poster/files/; the viewer poster/index.html links only to files/*). +cd /opt/projects/fivucsas/landing-website/public/poster/files +google-chrome-stable --headless=new --no-sandbox --virtual-time-budget=20000 --no-pdf-header-footer --print-to-pdf=fivucsas-poster.pdf "file://$PWD/fivucsas-poster.html" +google-chrome-stable --headless=new --no-sandbox --virtual-time-budget=20000 --window-size=3179,4494 --screenshot=fivucsas-poster-preview.png "file://$PWD/fivucsas-poster.html" +scp -P 65002 fivucsas-poster.pdf fivucsas-poster-preview.png u349700627@46.202.158.52:~/domains/fivucsas.com/public_html/poster/files/ + # Check all services docker ps --format "table {{.Names}}\t{{.Status}}" ``` @@ -80,6 +102,7 @@ FIVUCSAS/ # Parent repo (submodules) ├── docs/ # Architecture docs + plans ├── bys-demo/ # Demo site (static HTML) ├── landing-website/ # Landing page → Hostinger +├── links-website/ # links.fivucsas.com hub (static index.html) → Hostinger ├── practice-and-test/ # R&D experiments ├── scripts/ # Deploy scripts, setup-twilio.sh └── ROADMAP.md # Product roadmap diff --git a/POSTER_BRIEF.md b/POSTER_BRIEF.md new file mode 100644 index 0000000..47e82e8 --- /dev/null +++ b/POSTER_BRIEF.md @@ -0,0 +1,411 @@ +# FIVUCSAS — Poster Content Brief + +> Evidence-based extraction from the FIVUCSAS repository + submodules, dated 2026-05-19. +> Every concrete claim is pinned to `path/to/file:line`. Items not present in the repo are marked `[NOT FOUND IN REPO]`. The downstream poster session must NOT invent numbers — `[NOT FOUND IN REPO]` is a valid value. +> +> Repo root: `/opt/projects/fivucsas/` · Submodules: `biometric-processor`, `client-apps`, `docs`, `identity-core-api`, `practice-and-test`, `spoof-detector`, `web-app`. + +--- + +## 1. Elevator Pitch & Problem Framing + +### Short +**FIVUCSAS** — *Face & Identity Verification using Cloud-based SaaS Models* — özel sektörün e-Devlet'idir: her uygulamaya ayrı kayıt olmak yok. FIVUCSAS destekli her site/uygulamaya tek butonla, 10 farklı doğrulama yöntemiyle giriş. Uygulama sahibiysen login & register sayfaları yazmadan, OAuth2/OIDC + PKCE üzerinden tek hosted-login butonu ile kullanıcılarını doğrularsın. + +### Long +**End-user pitch:** Aktif (Biometric Puzzle) + pasif (UniFace MiniFASNet, 19 analyzer) hibrit canlılık + Facenet-512 yüz eşleme + ICAO 9303 NFC kimlik okuma + 10 birleştirilebilir MFA faktörü, tek bir hosted-login akışında. KVKK/GDPR uyumlu, embedding'ler Fernet AES-128 ile şifreli (`biometric-processor/app/infrastructure/security/embedding_cipher.py:39`). + +**Integrator pitch:** Müşteri uygulamaları sadece OIDC discovery (`/.well-known/openid-configuration` — `identity-core-api/.../OpenIDConfigController.java:44-80`) + authorization endpoint (`/api/v1/oauth2/authorize` — `identity-core-api/.../OAuth2Controller.java:77-182`) + JWKS (`/.well-known/jwks.json` — `OpenIDConfigController.java:91-100`) konfigüre eder. PKCE S256 zorunlu (`OAuth2Controller.java:328-341`), RS256 JWT default 2026-04-20'den beri (`identity-core-api/.../security/JwtService.java:27-30, 56-68`). SDK snippet'i: `web-app/src/verify-app/sdk/FivucsasAuth.ts`. + +**Multi-tenant kontrat:** Tenant entity `identity-core-api/.../entity/Tenant.java:35-100` (id/slug/status/max_users/biometric_enabled). RLS (Row-Level Security) 9 tabloda V25 ile aktif (`identity-core-api/src/main/resources/db/migration/V25__add_row_level_security.sql`). Canlı tenant: **Marmara Üniversitesi** (`V15__seed_realistic_sample_data.sql`, id `11111111-1111-1111-1111-111111111111`, demo `https://demo.fivucsas.com`). + +--- + +## 2. E-Devlet Comparison + +### Short +e-Devlet kamu hizmetlerinin ortak giriş kapısıdır; FIVUCSAS özel sektörün karşılığıdır. Tek bir `` butonu, OAuth2/OIDC + PKCE üzerinden, KVKK + GDPR + ISO/IEC 30107-3 Level 1 hazırlığı ile, biyometrik (yüz + ses + parmak izi/WebAuthn) ve dokümanlı (NFC ICAO 9303) doğrulamayı çağrı başına saniye altında getirir. + +### Long +| Aspect | e-Devlet | FIVUCSAS | +|---|---|---| +| Kapsam | Kamu sektörü | Her web/mobil uygulama | +| Auth modeli | Devlet SSO | OAuth2 + OIDC + PKCE (RFC 7636) Identity Provider | +| Erişim | Devlet onayı | Açık OIDC discovery + drop-in buton | +| Biyometrik | — (şifre + SMS) | Active Puzzle (7 yüz + 9 jest aksiyonu) + Passive PAD (19 analyzer) + Facenet-512 | +| NFC doküman | — | T.C. kimlik + pasaport (ICAO 9303, BAC implementlü) | +| MFA faktörleri | 2 (şifre + SMS) | 10 birleştirilebilir faktör | +| Proctoring | — | Sürekli (WebSocket, 15.9 fps Python ref, 25-30 fps tarayıcı) | +| Multi-tenant | — | Postgres RLS, V25, 9 tabloda | +| Yasal | KVKK | KVKK + GDPR + ISO/IEC 30107-3 Level 1 (iBeta submission package v0.2.1) | +| Açık kaynak | — | MIT (spoof-detector) | +| Kaynak | gov | `identity-core-api`, `biometric-processor`, `spoof-detector` (submodule) | + +**e-Devlet button referansı:** `turkiye.gov.tr/edevlet-ile-giris` lockup'ı — kırmızı arka plan, beyaz "T.C." amblem + "e-Devlet ile Giriş Yap" lockup'ı. FIVUCSAS karşılığı için poster üzerinde önerilen mockup: navy arka plan (#0B2545), camgöbeği (#2BA8B3) lockup, monospace `` markası — örnek HTML: +```html + + ile Giriş Yap + +``` + +**Üç use-case:** +1. **Banka-grade KYC** (fintech mobil): yüz + NFC kimlik + canlılık → tek session +2. **Sınav proctoring** (online üniversite): demo.fivucsas.com canlı (Marmara BYS) +3. **KVKK onboarding** (yerel SaaS): tüm embedding'ler Türkiye'de barındırılan Hetzner CX43'te, Fernet AES-128 şifreli + +--- + +## 3. Face Pipeline — Full Technical Walk + +### Short +9 aşamalı pipeline: YOLO kart tespiti → DeepFace yüz detect (opencv backend, anti-spoof 0.5) → kalite (Laplacian blur ≥ 100, min yüz 80px, occlusion CIE-Lab ΔE) → MediaPipe FaceLandmarker 468 nokta (Tasks API) → DeepFace align → **Facenet-512** embedding → Fernet AES-128 + HMAC-SHA256 şifreleme → pgvector index (biometric-processor HNSW m=16/ef=64, identity-core IVFFlat lists=100) → cosine match (θ=0.45 default, 0.38 aged >2yr). + +### Long + +**01. Detection.** DeepFace 0.0.98, detector_backend konfigüre edilebilir (default `opencv`, alternatifler: ssd, mtcnn, retinaface, mediapipe, yolov8, yolov11n/s, yolov12n, centerface) — `biometric-processor/app/core/config.py:91-94`. Anti-spoofing DeepFace içi etkin, threshold 0.5 — `config.py:126-136`. Loader: `biometric-processor/app/infrastructure/ml/detectors/deepface_detector.py:1-50`. **Card detection (web tarafı, doküman çekiminde):** YOLO model, confidence floor 0.35, UI gate 0.7 — `web-app/src/lib/biometric-engine/core/constants.ts:206, 220`. + +**02. Quality gating.** Laplacian variance ≥ **100.0** (`config.py:292`), min yüz 80 px (`config.py:291`), quality skoru ≥ **70.0** (`config.py:158`), brightness normalize 30-90 (`analyze_quality.py:33-34`). Occlusion detektör (270 LOC, eye=0.6/mouth=0.4 ağırlık, CIE-Lab ΔE) — `biometric-processor/app/application/services/occlusion_detector.py`. + +**03. Landmarking.** MediaPipe **FaceLandmarker (Tasks API)** — 468-nokta canonical topology + 478 ile iris (eye/mouth/nose/eyebrow/face_oval region tanımları). `biometric-processor/app/infrastructure/ml/landmarks/mediapipe_landmarks.py:26-45`. Legacy `face_mesh` API kullanılmıyor. + +**04. Alignment.** DeepFace `align=True` flag'i — `deepface_detector.py:28, 43`. Çıktı kırpma DeepFace iç boyutlandırması. + +**05. Embedding.** **Facenet-512**, default production model (`CLAUDE.md` "Facenet512 server-authoritative"). 512-D L2-normalize embedding — `biometric-processor/app/infrastructure/ml/extractors/deepface_extractor.py:72-75, 132`. **Encryption at rest:** Fernet (AES-128-CBC + HMAC-SHA256), Alembic 20260502_0005 — `biometric-processor/app/infrastructure/security/embedding_cipher.py:39` + `alembic/versions/20260502_0005_embedding_ciphertext.py:1-15`. + +**06. Storage / Indexing.** **biometric-processor** (Python servis): HNSW (`m=16`, `ef_construction=64`) — `biometric-processor/scripts/add_hnsw_indexes.sql:19-22` + `alembic/versions/20251212_0001_initial_schema.py:115-120`. **identity-core-api** (Spring servis): IVFFlat (`lists=100`) — `identity-core-api/.../db/migration/V4__create_biometric_tables.sql:8` + V33. Tenant izolasyonu: `tenant_id` kolonu + unique (user_id, tenant_id, biometric_type) WHERE deleted_at IS NULL — Alembic 20251212_0001:96-100. RLS 9 tabloda V25 ile etkin. + +**07. Matching.** Cosine distance (1 − cosine similarity) — `biometric-processor/app/infrastructure/ml/similarity/cosine_similarity.py`. **1:1 verification threshold:** 0.45 default (`config.py:156`), aged-embedding (>2yr) için 0.38 (`config.py:171-180`). 1:N top-k pgvector ANN üzerinden. + +**08. Operating point.** EER / FAR / FRR — `[NOT FOUND IN REPO]`. Hiçbir test fixture veya benchmark dosyasında ölçülmemiş. **Load-test hedefleri (gerçek ölçüm değil):** Login p95 < 300ms, Verification p95 < 500ms — `load-tests/README.md:137-180`. + +--- + +## 4. NFC Subsystem + +### Short +TurkishEidNfcReader (Android native, IsoDep + Bouncy Castle) T.C. kimlik + pasaport için ICAO 9303 spec'te. **BAC** uygulandı (PACE/AA/CA HENÜZ DEĞİL). DG1 (MRZ) + DG2 (yüz JPEG2000) parser var, EF.COM + EF.SOD %60 doğrulanmış. MRZ OCR fallback: Google ML Kit + CameraX. DG2 fotoğrafı → Facenet-512 → cosine vs canlı selfie. + +### Long +- **Chip location:** T.C. kimlik arka yüzdedir; pasaportlarda kapakta. ICAO ortak AID `A0 00 00 02 47 10 01` — `practice-and-test/PASSPORT_NFC_ROADMAP.md:147`. +- **ICAO 9303 spec atıfları:** `PASSPORT_NFC_ROADMAP.md:8`, `biometric-processor/app/domain/services/mrz_parser.py:8`, `identity-core-api/.../controller/NfcController.java:237-238`. +- **Protocols:** BAC implementlü (SHA-1 anahtar türetme MRZ'den, TD3 pasaport için aynı) — `PASSPORT_NFC_ROADMAP.md:670-690`. **PACE** opsiyonel, yeni pasaportlar için (`PASSPORT_NFC_ROADMAP.md:164-170`). **Active Authentication / Chip Authentication:** `[NOT FOUND IN REPO]`. +- **Data Groups:** DG1 — `practice-and-test/TurkishEidNfcReader/app/.../util/Dg1Parser.kt:1-9`; DG2 — `Dg2Parser.kt:1-50` (JPEG2000/JP2). EF.COM, EF.SOD — `PASSPORT_NFC_ROADMAP.md:158-161, 383-395` (SOD %60 tamam, CSCA tam zincir eksik). DG3/4/11/14/15 — implement edilmedi (roadmap §5.1). +- **Plugin:** Android native NFC (android.nfc), IsoDep — `practice-and-test/TurkishEidNfcReader/app/build.gradle:91-92`. Bouncy Castle (crypto) — `build.gradle:94-96`. **iOS CoreNFC:** `[NOT FOUND IN REPO]` (client-apps KMP'de expect/actual mümkün, kanıt yok). +- **MRZ OCR fallback:** Google ML Kit Text Recognition 16.0.0 + CameraX 1.3.4 — `build.gradle:78, 70-75`. ICAO check digit validation: `biometric-processor/tests/unit/test_nfc_mrz_route.py:47-88`. +- **Cross-check pipeline:** DG2 yüz JPEG2000 → Facenet-512 → cosine — `biometric-processor/app/api/routes/verification_pipeline.py`. Threshold yapılandırılabilir (`config.py:156`, default 0.45, aged 0.38). +- **Performance:** Chip okuma süresi `[NOT FOUND IN REPO]`. Timeout 30s — `PASSPORT_NFC_ROADMAP.md:31`. Failure modes (timeout, corrupted, invalid checksum, SOD fail) test edilmiş — `test_nfc_mrz_route.py:95-100`. + +--- + +## 5. Spoof Detector — Deep Dive + +### Short +**spoof-detector** submodule (MIT, github.com/Rollingcat-Software/spoof-detector, v0.2.1, ~50+ commits). **19 analyzer**, **15-axis liveness prover**, **session-level peak-sensitive verdict** (α=0.5). Backbone: MiniFASNet ONNX (UniFace, 80×80, 1.7 MB, frozen weights, weight 5.0×). **Paper draft** (BIOSIG 2026 / IJCB 2026 / IEEE FG 2027 hedef): "Beyond Single Frames: Session-Based Hybrid Image-and-Video Face Anti-Spoofing with Calibrated Multi-Class Fusion". **Browser port** canlı: `https://amispoof.fivucsas.com/` (123 kB ESM + 34 kB gzip, ONNX-Web + MediaPipe Tasks Vision, WebGPU/WASM). **iBeta PAD Level 1 submission package** commit `cc73cf08`. + +### Long + +**Identity.** +- Remote: `git@github.com:Rollingcat-Software/spoof-detector.git` — `spoof-detector/README.md:10`. +- License: MIT — `LICENSE:1`. +- Origin: FIVUCSAS biometric-processor R&D'den standalone extract — `README.md:10`. +- Authors: Ahmet Abdullah Gültekin (lead, mimari, kalibrasyon, session engine), Ayşe Gülsüm Eren (gesture/liveness araştırması), Ayşenur Arıcı (anti-spoof pipeline, gates, fusion, device-spoof) — `AUTHORS.md:14-16, 21`, `README.md:44, 252`. +- Affiliation: Marmara Üniversitesi, Bilgisayar Mühendisliği — `README.md:253`. +- npm: `@rollingcat/spoof-detector v0.2.1` — `pyproject.toml:6`, `ROADMAP.md:12`. +- Public demo: **https://amispoof.fivucsas.com/** — `SPOOF_DETECTOR_BROWSER_READINESS.md:3`. + +**Paper status.** Manuscript skeleton tamam, benchmark sonuçları doldu. 10 bölüm: abstract/intro/related-work/taxonomy/method/calibration/experimental-setup/results/ablations/discussion/conclusion — `paper/sections/*.md`, `paper/README.md:8-23, 34-57`. + +**Architecture.** +- **Backbone:** MiniFASNet ONNX (UniFace v2), frozen weights, 1.7 MB, +94.7 discrimination gap, weight **5.0×** — `README.md:94`, `paper/ARCHITECTURE.md:75-76`, `config.yaml:23-24`. +- **Input:** 80×80 padded face crop — `paper/sections/04_method.md:11`. +- **Output:** Per-frame P(REAL) ∈ [0, 100] → 7-category fuser → session-level P(REAL) via peak-sensitive aggregator. +- **Loss:** Yok (eğitim yok, sadece kalibrasyon). 13 lineer katsayı `MultiClassFuser`'da kalibre ediliyor — `paper/sections/05_calibration.md:66`, `config.yaml:54-76`. +- **Analyzer stack (19 total, 12 Python + 12 TS shared):** + +| Layer | Analyzers | Weight | +|---|---|---| +| Image-level | MiniFASNet, Device Boundary, AR-Filter, Texture, Moire | 5.0 + 2.5 + 0.3 + 0.1 + 0.1 | +| Temporal (FFT) | Blink/EAR, rPPG, Screen-Replay, Micro-Tremor, Landmark Variance, Temporal, Background Grid, Screen Flicker | 0.5 + 0.0 + 0.5 + 2.5 + 2.0 + 0.3 + 1.5 + 3.0 | +| MediaPipe blendshapes (browser) | Eyebrow motion, Blink symmetry, Gaze, Expression dynamics, 3D Pose, Behavioural | — | +| Optional (browser) | Hand tracking, Voice-activity, Audio-mouth sync | — | + +(`README.md:137-160`, `paper/ARCHITECTURE.md:85-98`, `ROADMAP.md:20-26`) + +- **Session engine:** "Guilty until proven innocent" LivenessProver (185-puan tavan, 15 axis) + peak-sensitive session verdict — `P_session(REAL) = α · mean(p_t) + (1−α) · mean(p_t | t ∈ worst-decile), α = 0.5` — `paper/sections/04_method.md:91`. Spoof-burst dilution'a karşı dayanıklı; formel kanıt §4'te (`paper/sections/04_method.md:62-114`). +- **Browser deployment:** TypeScript port (`web/`), npm `@rollingcat/spoof-detector`, `onnxruntime-web` (WASM + WebGPU fallback), `@mediapipe/tasks-vision` — `SPOOF_DETECTOR_BROWSER_READINESS.md:3-17`. + +**Training data.** +- **Calibration set (gerçek):** 43 KVKK onaylı Marmara 2026-04 capture (27 bona-fide / 16 attack) — `paper/sections/05_calibration.md:5-9`. +- **Validation set:** 325 sample (25 bona-fide × 300 synthetic attack, 4 sınıf × 3 varyant) — `paper/sections/07_results.md:73-82`. +- **Zero-shot public:** CASIA-FASD (HF akahana, 2,408 sample), CelebA-Spoof (HF nguyenghoa shard 0, 2,611 sample), in-house replay (100 sample) — `paper/sections/07_results.md:8-71`. +- MiniFASNet pre-training: UniFace corpus, external ONNX weights (kendi eğitim verisi yok). +- In-house labels: `spoof-detector/data/in_house/labels.csv` (43 satır). + +**Metrics (paperdan).** + +| Metric | Dataset | Pipeline | Value | 95% CI | Source | +|---|---|---|---|---|---| +| AUC | CASIA-FASD (N=2408) | minifasnet_only | **0.9452** | [0.9366, 0.9560] | `paper/sections/07_results.md:15` | +| ACER | CASIA-FASD | minifasnet_only | **12.67%** | [11.07, 13.92] | `07_results.md:15` | +| AUC | CelebA-Spoof (N=2611) | minifasnet_only | **0.7818** | [0.7663, 0.7993] | `07_results.md:36` | +| ACER | CelebA-Spoof | minifasnet_only | **28.67%** | [27.36, 30.23] | `07_results.md:36` | +| AUC | In-house replay (N=100) | image_only | **0.9264** | [0.8685, 0.9744] | `07_results.md:68` | +| Per-frame latency (CPU, CX43) | hybrid | — | **63.0 ms** mean (p99 117.8 ms) | — | `07_results.md:110` | +| Sustained FPS | hybrid | — | **15.9 fps** | — | `07_results.md:110-112` | +| MiniFASNet discrimination gap | in-house | minifasnet | **+94.7** | — | `05_calibration.md:25` | +| ISO 30107-3 Grade | in-house scripted | session | **Grade C** (BPCER 0%, APCER 30%, ACER 15%) | — | `README.md:74-84` | + +Bootstrap CI'lar 100 stratified resample üzerinden hesaplandı — `07_results.md:11, 32`. + +**Honest competitive positioning.** +- Paper akademik literatür ile pozisyonlanıyor (commercial PAD competitor head-to-head **YOK**). Modern intra-dataset SOTA (CDCN, FAS-SGTD) AUC >0.99 with **retraining**; FIVUCSAS 0.9452 **zero-shot** — "competitive with mid-tier published methods" (`07_results.md:24`). +- Strengths: **session-level verdict** (proctoring native, spoof-burst dilution'a dayanıklı), **client-side browser deployment** (KVKK/GDPR locality, frame upload yok), **anti-correlated signal discovery** (texture/moire — paper §5), **reproducible no-training calibration** (sadece 13 float), **MIT open-source + iBeta PAD-1 submission package**. +- Cost / multi-tenant / on-prem / KVKK-locality ekseninde FaceTec ZoOm, iProov GPA, Onfido Atlas, Jumio, AWS Rekognition Liveness, MS Face API'ye karşı **plausible**. Model maturity / dataset scale ekseninde **geride** (frozen 3rd-party backbone, 43-örnek kalibrasyon seti). + +**Proctoring contribution.** +- Per-frame detection fail eder: attacker 30s spoof tutar, 1 frame gerçek yüz, devam. Gap 1 paperin temel motivasyonu (`paper/sections/01_introduction.md:7`). +- **Peak-sensitive aggregator** — formel kanıt: 60-frame session, 54 spoof @ 0.20 + 6 real @ 0.95 → verdict SPOOF; all-live + 1 dip → verdict LIVE (`04_method.md:62-114`). +- LivenessProver 15-axis (blink, motion, rotation, expression, temporal, gaze, pose consistency, behaviour, optional hand, optional audio), 185-puan tavan, doğal decay (`README.md:159`). +- Incident detection: P(REAL) < 0.4 for ≥3s, no blink ≥15s, face missing ≥5s, identity-change suspicion (MiniFASNet swing ≥0.35 in 1s) — `paper/ARCHITECTURE.md:134`. +- Background-grid analyzer: 8×6 cell motion variance, proctoring-specific (`config.yaml:39`, `04_method.md:31`). + +**iBeta PAD Level 1.** Submission package commit `cc73cf08e0df1f811c08cc92549fae48d6c4a05a`, 2026-05-11, scope ISO/IEC 30107-3 PAD Level 1 (`iBeta_PAD_Level1_Submission_Package.md:7-11, 9`). PAI coverage: bona-fide ✓, print ✓, replay kısmi (open challenge), 3D mask routed-not-validated, morphing out-of-scope. + +**Browser readiness.** Live `https://amispoof.fivucsas.com/`, 19 analyzer + 3 gates (Aysenur'un — FaceUsability/Illumination/CriticalRegionVisibility), Fusion (MultiClassFuser + HybridFusionEvaluator), Session engine + LivenessProver, Web Worker offload (4 heavy analyzer), WebGPU + WASM fallback, lazy chunks (Texture 7.9 kB / Moire 10.5 kB / ScreenReplay 18.2 kB), 217 vitest yeşil. Desktop Chrome 25-30 fps (WebGPU), Pixel-class Android Brave 6.7-9.5 fps. MiniFASNet ONNX 1.7 MB + FaceLandmarker task 3.7 MB CDN-cached `Cache-Control: max-age=31536000, immutable` (`SPOOF_DETECTOR_BROWSER_READINESS.md:3-17, 214-218, 290-291`, `ROADMAP.md:134-141`). + +--- + +## 6. Biometric Puzzle (Active Liveness) + +### Short +Server-issued nonced challenge (puzzle UUID, 5dk TTL). **7 yüz** aksiyon (blink/smile/turn_left/turn_right/open_mouth/raise_eyebrows/light) + **9 jest** aksiyon (finger_count/wave/hand_flip/finger_tap/pinch/peek_a_boo/math/shape_trace/hold_position). Difficulty: easy (2-3, 7s/step) / standard (3-4, 5s/step) / hard (4-5, 4s/step). Per-step max 3 retry, session timeout 120s. Replay-proof. + +### Long +- **Action vocabulary:** + - **Face (7):** blink, smile, turn_left, turn_right, open_mouth, raise_eyebrows, light — `biometric-processor/app/api/schemas/active_liveness.py:9-37` + - **Gesture (9, server-side landmarks-only):** finger_count (wrist-PIP/TIP ratio), wave (zero-crossing on wrist-x), hand_flip (palm-normal proxy sign change), finger_tap (index↔middle proximity), pinch (thumb↔index distance), peek_a_boo (monotonic hand-covers-face), math (random open-finger count), shape_trace (DTW vs template), hold_position (wrist std-dev) — `app/application/services/active_gesture_liveness_manager.py:16-24` +- **Sequence:** difficulty enum `easy|standard|hard`, step count random 2-7, per-step timeout 2-30s — `app/domain/entities/puzzle.py:14-19, 24, 37-38`, `app/api/schemas/puzzle.py:11-16`. +- **TTL:** 5 minutes total — `puzzle.py:87-89`. +- **Per-action thresholds:** + - Server-side EAR baseline 0.21, REOPEN 0.23 (spoof-detector #10 recalibration) — `biometric-processor/.../puzzle.py:52-58` + - Client-side EAR open 0.22, closed 0.17 — `web-app/src/lib/biometric-engine/core/constants.ts:16, 19` + - Smile corner-raise 0.05, mouth-width 0.60 — `constants.ts:22, 25` + - Head yaw ±20°, pitch ±12° — `constants.ts:31, 34` + - Continuous hold 0.6s — `constants.ts:49` + - Liveness score gate 50.0 (0-100) — `constants.ts:83` +- **Per-action retry:** Max 3 — `active_liveness.py:59`. Default per-step 5s — `puzzle.py:25, active_liveness.py:57`. Session timeout 120s — `active_liveness.py:69`. +- **Anti-replay:** Server-issued puzzle UUID v4 (Redis), expires `created_at + 5min`, verification expired ise reject — `puzzle.py:73, 87-89, 91-97`. Client sequence değiştiremez. Optional spot-check base64 frames (`puzzle.py:100-101`) server-side re-validation için. +- **LIVENESS_MODE flag:** `passive | active | combined`, default `combined` — `app/core/config.py:249-257, 219-224`. ChallengeType enum eşleştirmesi: `web-app/.../puzzleServerAction.ts`. +- **Endpoints:** `/api/v1/liveness`, `/api/v1/liveness/verify-challenge` — `app/api/routes/liveness.py`, `app/api/routes/puzzle.py`. Identity-core'dan X-API-Key ile çağrılır (internal Docker network only). + +--- + +## 7. Differentiators & Engineering Achievements + +- **Production-deployed multi-tenant SaaS** — Marmara Üniversitesi canlı tenant, `demo.fivucsas.com` (Hetzner CX43, 8 vCPU / 16 GB / 150 GB) — `CLAUDE.md:44-49`, V15 seed. +- **Hexagonal / DDD layout** — ports/adapters: identity-core'da `application/port/input/`, `application/port/output/`, `infrastructure/adapter/` (UserRepository, AuthFlowRepository, NfcCardRepository, BiometricProcessorClient, EmailServiceAdapter, SmsOtpAuthHandler) — `identity-core-api/src/main/java/com/fivucsas/identity/`. Biometric-processor'da `api/routes/ → application/use_cases/ → infrastructure/ml/` — `biometric-processor/app/main.py:1`. +- **OAuth2/OIDC + PKCE (RFC 7636)** — `/api/v1/oauth2/authorize`, `/.../token`, `/.../userinfo`, `/.well-known/openid-configuration`, `/.well-known/jwks.json` — `OAuth2Controller.java`, `OpenIDConfigController.java:44-100`. PKCE S256 mandatory `OAuth2Controller.java:328-341`. +- **JWT RS256 default** (2026-04-20 flip), HS512 legacy disabled — `JwtService.java:27-30, 56-68`, `application.yml:43`. +- **ISO/IEC 30107-3 alignment** — Level 1 submission package commit `cc73cf08`, Grade C in-house — `spoof-detector/iBeta_PAD_Level1_Submission_Package.md:7-11`. +- **KVKK / GDPR posture** — Fernet AES-128 embedding encryption at rest (`embedding_cipher.py:39`), soft-delete `deleted_at` (`Tenant.java:41-42`, `@SQLRestriction("deleted_at IS NULL")`), tenant RLS 9 tabloda (V25), audit_logs partition by tenant_id (V40, pg_partman V57). +- **10 Composable Auth Factors:** + 1. PASSWORD (BCrypt-12) — `SecurityConfig.java:239-240`, `PasswordAuthHandler.java` + 2. EMAIL_OTP (6-digit, Redis, 5min TTL) — `EmailOtpAuthHandler.java`, `OtpService.java:16` + 3. SMS_OTP (Twilio) — `SmsOtpAuthHandler.java`, `SMS_ACTIVATION_PLAN.md` + 4. TOTP (RFC 6238, 30s step, 32-byte secret AES-128) — `TotpAuthHandler.java`, `TotpService.java:15, 32`, V31 + 5. FACE (Facenet-512) — `FaceAuthHandler.java` + 6. VOICE (Resemblyzer 256-D, pgvector HNSW) — `VoiceAuthHandler.java`, V33 + 7. FINGERPRINT (WebAuthn platform authenticator) — `FingerprintAuthHandler.java` + 8. HARDWARE_KEY (FIDO2/WebAuthn) — `HardwareKeyAuthHandler.java` + 9. QR_CODE — `QrCodeAuthHandler.java` + 10. NFC_DOCUMENT (ICAO 9303, ISO 18013-5) — `NfcDocumentAuthHandler.java` +- **B2B drop-in identity button** — SDK `@fivucsas/auth-js`, `web-app/src/verify-app/sdk/FivucsasAuth.ts`, hosted-first redirective + iframe step-up MFA fallback, nonce + state, redirect-URI allowlist (HTTPS + loopback RFC 8252). +- **Real-time proctoring** — WebSocket `/api/v1/proctor/ws?session_id=...&user_id=...&tenant_id=...`, ConnectionManager singleton, frame handler, ProctorMetrics — `biometric-processor/app/api/routes/proctor_ws.py:1-47`, `app/core/metrics/proctoring.py`. +- **Hybrid liveness** — active Puzzle + passive UniFace MiniFASNet, fusion rule `accept iff min(P_A, P_B) ≥ θ`. +- **pgvector indexing** — biometric-processor HNSW (m=16, ef_construction=64), identity-core IVFFlat (lists=100). +- **Self-host stack on Hetzner CX43** — 8 vCPU / 16 GB / 150 GB, ~7 production services in docker-compose.prod.yml (postgres pgvector/pg16, redis 7.4, identity-core-api 2 replicas, biometric-processor 2 replicas, api-gateway Nginx, prometheus, grafana). 60 Flyway migrations (V1-V60). +- **Spoof-detector browser port** — 19 analyzer + 3 gates + LivenessProver + Web Worker + WebGPU + lazy chunks, 217 vitest yeşil, canlı `amispoof.fivucsas.com`. + +--- + +## 8. Challenges & Effort-Intensive Work + +- **Cross-modal synchronization (Puzzle ↔ PAD)** — LIVENESS_MODE `combined` default, fusion rule. Yanlış kalibre edilen hybrid /verify endpoint'inde stil frame → False yerlik dönerdi; PR #83 sonrası FaceVerifyMfaStepHandler cosine fail-open kapatıldı (Session 2026-05-07). +- **MediaPipe Tasks API porto** — legacy `face_mesh` API'den Tasks API'ye geçiş (468-nokta canonical), branch `fix/2026-05-12-mediapipe-tasks-api-port` (biometric-processor active head). +- **pgvector index tuning** — IVFFlat vs HNSW seçimi (<1M satır ölçeğinde IVFFlat lists=100; HNSW (m=16, ef=64) biometric-processor için). Hala recall/latency calibration bekliyor. +- **Multi-tenant Row-Level Security** — V25 9 tablo, V40 audit_logs partition, V57 pg_partman tenant_id backfill. 140 → 0 NULL backfill 2026-05-11. +- **NFC chip-read reliability** — Android OEM/versiyon farkı. iOS CoreNFC `[NOT FOUND IN REPO]`, KMP expect/actual gerekiyor. +- **BAC/PACE crypto correctness** — SHA-1 anahtar türetme, MRZ check-digit doğrulama. PACE/AA/CA henüz implement edilmedi (`PASSPORT_NFC_ROADMAP.md:164-170`). +- **ICAO 9303 spec interpretation** — Türkiye TD3 pasaport eşdeğeri, EF.SOD CSCA tam zinciri eksik (%60 doğrulanmış). +- **Flutter iOS NFC platform limits** — `[NOT FOUND IN REPO]`, Android-only canlı. +- **KVKK irreversibility** — Fernet AES-128 (yine de Fernet anahtarına erişen plaintext'i geri alır; "irreversibility" iddiası matematiksel değil operasyonel; bu poster üzerinde dürüst ifade gerekli). Embedding plaintext column hala duruyor (ANN search için), encryption dual-column. +- **Anti-spoof training-data scarcity (Türk demografi)** — paper 43-örnek in-house Marmara; CelebA-Spoof zero-shot AUC 0.7818, intra-dataset SOTA AUC >0.99. Bu poster üzerinde dürüst ifade gerekli. +- **Cold-start tenant onboarding flow** — `[NOT FOUND IN REPO — refer to web-app/docs/plans/]` (CLAUDE.md 2026-05-12 carry-forward). +- **Audit logs partitioning** — V57 silent no-op olarak teşhis edildi (Session 2026-05-12); V59/V60 ile düzeltildi. +- **JWT RS256 default** — 2026-04-20 flip, ama "JWT aud unshipped" 3 oturum üst üste yanlış işaretlendi memory'de (lesson: grep current HEAD before trusting completion labels). +- **FK-cascade no-hard-delete** — `users` 13 tabloya cascade siliyor (Session 2026-04-28 incident: ahabgu'nun TOTP/WebAuthn/NFC silindi). Patch: `findByEmail(... AND deletedAt IS NULL)`. +- **anti-correlated signal discovery** (texture/moire) — paper §5 ana finding'i; 1.0'dan 0.1'e re-weighting 0.017 AUC recovers. +- **Browser readiness 4 faz** — Phases A-D 2026-05-15/16, 5-agent paralel salvage 2026-04-25 (4 PRs from worktrees with complete drafts). + +--- + +## 9. Short Q&A (Turkish, ≥ 12 pairs) + +1. **FIVUCSAS hangi sorunları çözüyor?** + Her uygulama için ayrı kayıt yok. FIVUCSAS destekli her site/uygulamaya tek butonla, 10 farklı yöntemle giriş. Uygulama sahibiysen login/register sayfaları yazmadan, OIDC + PKCE üzerinden tek hosted-login butonu ile kullanıcıları auth edersin. + +2. **e-Devlet'ten farkı nedir?** + e-Devlet kamu sektörü SSO'su; FIVUCSAS özel sektörün karşılığı. Açık OIDC discovery, drop-in `` butonu, NFC ICAO 9303 + Active Puzzle + Passive PAD + multi-tenant RLS, KVKK + GDPR + ISO/IEC 30107-3 Level 1 hazırlığı. + +3. **Neden 10 farklı auth factor?** + NIST 800-63B'ye uygun "Know-Have-Are-Show" eksenlerinde her tenant kendi MFA akışını kompoze eder — Java kod yazmadan, sadece JSON tenant config'i ile. + +4. **Liveness Puzzle spoof'u nasıl yakalar?** + Server her denemede rastgele 3-5 aksiyon dizisi üretir (UUID nonce, 5dk TTL). Pre-recorded video, deepfake injection, screen-replay başarısız olur — istenen aksiyon set unpredictable + timestamped + sunucu üretimli. + +5. **Bir geliştirici FIVUCSAS'ı kaç satır kodla entegre eder?** + ~15 satır: SDK init + `loginRedirect()` + token exchange. OIDC discovery + JWKS otomatik. + +6. **KVKK uyumu nasıl sağlanıyor?** + Embedding'ler Fernet AES-128 + HMAC-SHA256 şifreli at-rest, soft-delete `deleted_at` + `@SQLRestriction`, Postgres RLS 9 tabloda (V25), audit_logs tenant_id partition (V40 + pg_partman V57), Hetzner Almanya/Türkiye-erişimli barındırma. + +7. **Proctoring'e ne katıyor?** + Session-level peak-sensitive verdict (α=0.5, mean + worst-decile) — spoof-burst dilution'a dayanıklı. 15-axis LivenessProver, 63 ms / frame CPU (Python), 25-30 fps tarayıcı (WebGPU). Background-grid analyzer proctoring-specific. + +8. **Veri silme talebi geldiğinde ne oluyor?** + `deleted_at` damga, RLS tüm sorgularda soft-delete satırı filtreler. Hard-delete YASAK (FK-cascade 13 tabloya). GDPR/KVKK purge job ile periyodik fiziksel silme. + +9. **Neden multi-tenant mimari?** + Tek deploy ile N tenant. RLS Postgres-native, audit isolation pg_partman ile. Marmara Üniversitesi canlı tenant, demo.fivucsas.com aktif. + +10. **NFC olmasa olmaz mı?** + Hayır — opsiyonel faktör. Ama "banka-grade KYC" use-case'inde DG2 yüz JPEG2000 ↔ canlı selfie cosine eşleştirmesi ile fiziksel doküman doğrulaması ekler. T.C. kimlik + ICAO 9303 pasaport. + +11. **Pasif PAD ile aktif Puzzle'ın farkı?** + Pasif (UniFace MiniFASNet, 19 analyzer): kullanıcı işbirliği yok, sessiz. Aktif (Puzzle): server'ın istediği aksiyonu kullanıcı yapar — replay-proof. Hibrit: `accept iff min(P_A, P_B) ≥ θ`. + +12. **Küçük bir uygulama bu sistemi karşılayabilir mi (maliyet)?** + Evet — Hetzner CX43 (8 vCPU / 16 GB) tek sunucuda 7 servis. GPU yok. Spoof-detector tarayıcıda çalışır (123 kB ESM), frame sunucuya yüklenmez. MIT lisanslı submodule. + +13. **Spoof-detector araştırması nerede?** + `spoof-detector` submodule (MIT), `paper/sections/*.md` (10 bölüm, BIOSIG 2026 hedefi), public demo `https://amispoof.fivucsas.com/`, npm `@rollingcat/spoof-detector v0.2.1`, iBeta PAD Level 1 submission commit `cc73cf08`. + +14. **Hangi açılardan açık kaynak literatürün önünde?** + (a) Session-level peak-sensitive verdict, formel kanıtla spoof-burst dilution direnci. (b) Client-side browser deployment (WebGPU + WASM). (c) Anti-correlated signal discovery (texture/moire), per-operator recalibration recipe. (d) Reproducible no-training kalibrasyon (13 float). + +15. **Subdomain'ler ne için?** + `fivucsas.com` (landing), `verify.fivucsas.com` (hosted-login), `api.fivucsas.com` (OAuth2/OIDC), `app.fivucsas.com` (admin panel), `demo.fivucsas.com` (Marmara BYS), `docs.fivucsas.com` (entegrasyon dökümanı), `amispoof.fivucsas.com` (spoof-detector tarayıcı demosu), `status.fivucsas.com` (uptime), `links.fivucsas.com` (link hub). Grafana container internal-only — DNS yok. Posterde tek QR tüm linkleri taşır. + +--- + +## 10. Numbers Inventory (Appendix) + +### Lines of Code (measured 2026-05-19) +| Submodule | Lines | +|---|---| +| spoof-detector | 217,178 | +| practice-and-test | 222,825 | +| client-apps | 89,593 | +| biometric-processor | 77,782 | +| landing-website | 26,725 | +| identity-core-api | 28,708 | +| web-app | 15,141 | +| verify-widget | 7,800 | +| bys-demo | 2,288 | +| docs | 1,007 | +| docs-site | 1,646 | +| infra | 553 | +| **TOTAL** | **~691,446** | + +### Tests +| Suite | Count | +|---|---| +| Spring (Java) | 944 | +| Pytest | 265 | +| Vitest/Jest | 1,559 | +| **TOTAL** | **~2,768** | + +### Concrete Configuration Values +| Param | Value | Source | +|---|---|---| +| Embedding dim (Facenet-512) | 512 | `biometric-processor/.../deepface_extractor.py:72-75` | +| Embedding encryption | Fernet AES-128-CBC + HMAC-SHA256 | `embedding_cipher.py:39` | +| Cosine threshold (default) | 0.45 | `biometric-processor/app/core/config.py:156` | +| Cosine threshold (aged >2y) | 0.38 | `config.py:171-180` | +| Cosine threshold (similarity module default) | 0.6 | `infrastructure/ml/similarity/cosine_similarity.py:17` | +| Face detect confidence | 0.5 | `web-app/.../constants.ts:129` | +| Card detect confidence | 0.35 (gate 0.7) | `constants.ts:206, 220` | +| EAR open / closed (client) | 0.22 / 0.17 | `constants.ts:16, 19` | +| EAR baseline / REOPEN (server) | 0.21 / 0.23 | `biometric-processor/.../puzzle.py:52-58` | +| Smile corner / width | 0.05 / 0.60 | `constants.ts:22, 25` | +| Head yaw / pitch | ±20° / ±12° | `constants.ts:31, 34` | +| Continuous hold | 0.6s | `constants.ts:49` | +| Liveness score gate | 50.0 | `constants.ts:83` | +| Quality min | 65 (UI) / 70 (server) | `constants.ts:75`, `config.py:158` | +| Blur (Laplacian variance) | ≥ 100.0 | `config.py:292` | +| Min face size | 80 px | `config.py:291` | +| pgvector (biometric-processor) | HNSW m=16, ef_construction=64 | `scripts/add_hnsw_indexes.sql:19-22` | +| pgvector (identity-core) | IVFFlat lists=100 | `db/migration/V4__create_biometric_tables.sql:8` | +| Puzzle TTL | 5 minutes | `puzzle.py:87-89` | +| Puzzle step timeout default | 5s (range 2-30s) | `puzzle.py:24-25` | +| Puzzle session timeout | 120s | `active_liveness.py:69` | +| Puzzle max retry | 3 | `active_liveness.py:59` | +| Access token TTL | 86,400,000 ms (24h) | `application.yml:88` | +| Refresh token TTL | 604,800,000 ms (7d) | `application.yml:89` | +| OTP TTL | 5 min | `OtpService.java:16` | +| TOTP step | 30s | `TotpService.java:32` | +| TOTP secret length | 32 bytes | `TotpService.java:15` | +| JWT signing default | RS256 (2026-04-20 flip) | `JwtService.java:27-30` | +| Max devices/user | 10 | `application.yml:51` | +| Migrations (Flyway) | 60 (V1-V60) | `db/migration/` | + +### Spoof-detector measured metrics (paper) +| Metric | Value | Source | +|---|---|---| +| CASIA-FASD AUC (minifasnet_only) | 0.9452 [0.9366, 0.9560] | `paper/sections/07_results.md:15` | +| CASIA-FASD ACER | 12.67% | `07_results.md:15` | +| CelebA-Spoof AUC | 0.7818 [0.7663, 0.7993] | `07_results.md:36` | +| CelebA-Spoof ACER | 28.67% | `07_results.md:36` | +| In-house replay AUC (image_only) | 0.9264 | `07_results.md:68` | +| MiniFASNet discrimination gap | +94.7 (μ_real 99.9, μ_spoof 5.1) | `05_calibration.md:25` | +| Per-frame latency (CX43 hybrid) | 63.0 ms mean, p99 117.8 ms | `07_results.md:110` | +| Sustained FPS (Python ref) | 15.9 fps | `07_results.md:110-112` | +| Desktop Chrome (WebGPU) | 25-30 fps | `SPOOF_DETECTOR_BROWSER_READINESS.md:4` | +| Pixel-class Android Brave | 6.7-9.5 fps | `ROADMAP.md:139-141` | +| Browser tests (vitest) | 217 green | `paper/sections/01_introduction.md:25` | +| Browser bundle | 123 kB ESM / 34 kB gzip | `ROADMAP.md:134-137` | +| MiniFASNet ONNX size | 1.7 MB | `paper/ARCHITECTURE.md:75-76` | +| FaceLandmarker task | 3.7 MB | `web/amispoof/README.md:48-50` | + +### Infrastructure +| Param | Value | Source | +|---|---|---| +| Host | Hetzner CX43, 8 vCPU / 16 GB / 150 GB | `CLAUDE.md:44-49` | +| Production services | 7 (postgres, redis, identity-core x2, biometric x2, gateway, prometheus, grafana) | `docker-compose.prod.yml` | +| Commits last 6 months (all repos) | ~1,847 | `git log --since='2025-12-01'` | +| Live tenant | Marmara Üniversitesi (id `11111111-1111-1111-1111-111111111111`) | `V15__seed_realistic_sample_data.sql` | +| Auth methods | 10/10 | `AuthMethodType.java:1-25` | + +### Load test targets (NOT measured, see `[NOT FOUND IN REPO]`) +| Operation | Target p95 | Target p99 | Source | +|---|---|---|---| +| Login | <300ms | <500ms | `load-tests/README.md:137-139` | +| Token Refresh | <200ms | <400ms | `load-tests/README.md:137-139` | +| Enrollment | <2000ms | <3000ms | `load-tests/README.md:158-159` | +| Verification | <500ms | <1000ms | `load-tests/README.md:179-180` | + +### Gaps (cannot be put on the poster) +- Face pipeline EER / FAR / FRR — `[NOT FOUND IN REPO]` +- NFC chip-read latency (real measurement) — `[NOT FOUND IN REPO]` +- iOS CoreNFC implementation — `[NOT FOUND IN REPO]` +- DG3 / DG4 / DG11 / DG14 / DG15 NFC parsers — not implemented +- PACE / Active Authentication / Chip Authentication — not implemented +- Production proctoring SLA / incident-rate telemetry — `[NOT FOUND IN REPO]` +- Voice anti-spoof (ECAPA-TDNN with replay-detection cosine guard) — listed as Future Work in WA0006 conclusion +- iBeta PAD Level 1 official verdict — submission package only, not awarded yet + +--- + +**End of brief.** Poster design session: pull from this file; mark `[NOT FOUND IN REPO]` items as "Future Work" or omit. Never invent missing numbers. diff --git a/archive/2026-05/plans/OPERATOR_ACTIONS_2026-05-12.md b/archive/2026-05/plans/OPERATOR_ACTIONS_2026-05-12.md new file mode 100644 index 0000000..d3a5d50 --- /dev/null +++ b/archive/2026-05/plans/OPERATOR_ACTIONS_2026-05-12.md @@ -0,0 +1,639 @@ +# OPERATOR ACTIONS — 2026-05-12 + +Items surfaced by the 2026-05-12 senior reviews (backend, DB, infra, security) +that agents should not autonomously execute. Each is a checklist with explicit +commands, a maintenance-window estimate, and explicit dependencies. Severity +labels: + +- **CRITICAL** — exposes a live, exploitable security or correctness gap. +- **HIGH** — drift between deployed config and committed config; reviewers + cannot reason about prod from code. +- **MEDIUM** — hygiene + cosmetic; safe to defer but easy to land. + +--- + +## 1. audit_logs partitioning — V57 silent no-op (HIGH) + +**Background.** +The Flyway migration `V57__audit_logs_pg_partman.sql` is the one that hands +`public.audit_logs` to the `pg_partman` extension so partitions roll over +monthly with a 24-month retention. V57 runs to `success=t` in +`flyway_schema_history`, but the live postgres image +`pgvector/pgvector:pg17` does not bundle `pg_partman`. The first guard at the +top of V57 detects the missing extension, emits `RAISE WARNING`, and `RETURN`s +before the V40-fallback conversion runs. + +Symptom on prod today (2026-05-12): +- `pg_class.relkind` for `audit_logs` is `'r'` (regular table), not `'p'` + (partitioned). +- 1168 rows in a single heap, no inheritance children. +- `partman.part_config` row for `public.audit_logs` does not exist. + +Memory entry `project_session_20260511` records this: commit `b32ca03` +("infra(scripts+v57): rotation scripts + V57 Option A pg_partman image +preparation") and the untracked file `/opt/projects/infra/RUNBOOK_AUDIT_LOG_PARTMAN.md` +already prep the fix path. The runbook is the authoritative recipe; this +section is the executive summary. + +**Blast radius.** +audit_logs growth becomes painful around 10-20M rows (current is 1168). +There is operational headroom of months at current write rate. Failure mode +when finally addressed = vacuum/index-scan slowdowns + the GDPR/KVKK +24-month purge has to be implemented manually as a `DELETE` instead of +`DROP PARTITION`. No data loss; just latency drift. + +**Maintenance window.** 15-30 minutes; postgres restart required for +`shared_preload_libraries = 'pg_partman_bgw,pg_cron'`. + +**Dependencies.** None on FIVUCSAS code. Operator owns the custom postgres +image build. + +**Suggested execution path (Option A from the runbook).** + +1. Create `/opt/projects/fivucsas/infra/postgres/Dockerfile` per the runbook + (`pgvector/pgvector:pg17` base + `postgresql-17-partman` + + `postgresql-17-cron`). +2. Swap the `image:` in `/opt/projects/fivucsas/docker-compose.prod.yml` + `postgres:` service for a `build:` block pointing at the new Dockerfile. +3. Rebuild: + ```bash + cd /opt/projects/fivucsas + docker compose -f docker-compose.prod.yml --env-file .env.prod build postgres + docker compose -f docker-compose.prod.yml --env-file .env.prod up -d postgres + ``` +4. After postgres is healthy, run the partman bootstrap on the existing + non-partitioned table: + ```sql + CREATE EXTENSION IF NOT EXISTS pg_partman; + CREATE EXTENSION IF NOT EXISTS pg_cron; + SELECT partman.create_parent( + p_parent_table := 'public.audit_logs', + p_control := 'created_at', + p_type := 'range', + p_interval := '1 month', + p_premake := 12, + p_start_partition := '2026-01-01' + ); + UPDATE partman.part_config + SET retention = '24 months', + retention_keep_table = false, + retention_keep_index = false + WHERE parent_table = 'public.audit_logs'; + ``` +5. **Alternative** — if you would rather not bootstrap a live table, mark + V57 as failed in `flyway_schema_history` and re-apply once the new + image is in place (Flyway will see the migration as new and run it + end-to-end with partman available). + +**Acceptance check.** +```sql +SELECT parent_table, partition_interval, premake, retention + FROM partman.part_config + WHERE parent_table = 'public.audit_logs'; +-- expect 1 row, interval='1 mon', premake=12, retention='24 months' +``` + +--- + +## 2. RLS theatre — every policy fail-open + app role is superuser (CRITICAL) + +**Background.** +The Flyway migration `V25__row_level_security.sql` enabled Row-Level +Security on 9 tables but left the `FORCE ROW LEVEL SECURITY` line commented +out. Every policy includes a `current_tenant_id() IS NULL` disjunct, which +returns true any time the session has not run `SET app.current_tenant_id`. +The application's JDBC URL connects as the `postgres` superuser, and +superusers bypass RLS unconditionally. Net effect: RLS is ENABLED in +`pg_class.relrowsecurity` but is functionally OFF. + +Verified today: +```sql +SELECT relname, relrowsecurity, relforcerowsecurity + FROM pg_class + WHERE relname IN ('users','tenants','audit_logs','biometric_enrollments', + 'auth_flows','auth_flow_steps','user_enrollments', + 'oauth2_clients','refresh_tokens'); +-- all 9 rows: relrowsecurity=t, relforcerowsecurity=f +``` + +**Blast radius.** +A SQL-injection (or a deliberately misuse of `JdbcTemplate.queryForList`) +that omits a `tenant_id =` predicate returns rows from every tenant. The +admin-IP whitelist on `/swagger-ui` and `/actuator` does not help here — +the entry point is the application code itself. + +**Maintenance window.** 30-60 minutes; requires postgres role creation, +GRANT statements, and a JDBC URL flip. Smoke-test downtime ~2 minutes +when the api container restarts. + +**Dependencies.** +- New non-superuser role (call it `fivucsas_app`) created and granted only + what is needed. +- `.env.prod` `SPRING_DATASOURCE_USERNAME` flipped from `postgres` to + `fivucsas_app`. +- After the role swap, `FORCE ROW LEVEL SECURITY` flipped on every + RLS-enabled table. +- Smoke-test all 10 auth methods + tenant admin endpoints in maintenance + window. + +**Suggested execution path.** + +1. Inside the maintenance window: + ```sql + CREATE ROLE fivucsas_app LOGIN PASSWORD ''; + GRANT CONNECT ON DATABASE identity_core TO fivucsas_app; + GRANT USAGE ON SCHEMA public TO fivucsas_app; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fivucsas_app; + GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fivucsas_app; + ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO fivucsas_app; + ``` +2. Flip FORCE on the 9 RLS tables in a single transaction (Flyway + migration `V62__rls_force.sql` recommended so the change is tracked): + ```sql + ALTER TABLE users FORCE ROW LEVEL SECURITY; + ALTER TABLE tenants FORCE ROW LEVEL SECURITY; + ALTER TABLE audit_logs FORCE ROW LEVEL SECURITY; + ALTER TABLE biometric_enrollments FORCE ROW LEVEL SECURITY; + ALTER TABLE auth_flows FORCE ROW LEVEL SECURITY; + ALTER TABLE auth_flow_steps FORCE ROW LEVEL SECURITY; + ALTER TABLE user_enrollments FORCE ROW LEVEL SECURITY; + ALTER TABLE oauth2_clients FORCE ROW LEVEL SECURITY; + ALTER TABLE refresh_tokens FORCE ROW LEVEL SECURITY; + ``` +3. Drop the `OR current_tenant_id() IS NULL` disjunct from each policy + in the same migration. The application sets `app.current_tenant_id` + via a JDBC interceptor on every transaction; absence means a code + path is wrong and should fail visibly, not return all tenants. +4. Edit `.env.prod`: + ``` + SPRING_DATASOURCE_USERNAME=fivucsas_app + SPRING_DATASOURCE_PASSWORD= + ``` +5. Rebuild + restart: + ```bash + cd /opt/projects/fivucsas/identity-core-api + docker compose -f docker-compose.prod.yml --env-file .env.prod up -d identity-core-api + ``` +6. Smoke-test: log in as a tenant admin from tenant A, hit + `/api/v1/audit-logs`, confirm zero tenant-B rows; same for + `/api/v1/users`. + +**Acceptance check.** +```sql +SELECT relname, relforcerowsecurity FROM pg_class + WHERE relname IN ('users','audit_logs','biometric_enrollments', + 'tenants','auth_flows','auth_flow_steps', + 'user_enrollments','oauth2_clients','refresh_tokens'); +-- all 9 rows: relforcerowsecurity=t +``` + +--- + +## 3. web-app/.env.production still byte-identical to leaked literal (HIGH) + +**Background.** +Commit `6bdedd2` (2026-04-30 morning, since-rotated) committed the +biometric API key plaintext into `web-app/.env.production`. The bio-side +key was rotated 2026-04-30 05:05 UTC and confirmed dead — the live value +is now `API_KEY_SECRET=fcb06b7…` (verified by the 2026-05-12 security +review). However the on-disk template at +`/opt/projects/fivucsas/web-app/.env.production` still contains the +leaked literal in `VITE_BIOMETRIC_API_KEY=…` form (2 occurrences, +verified today by `grep -c`). + +Because the variable has the `VITE_*` prefix, any subsequent +`npm run build` from this working tree would inline the dead key into +the bundle. The current production bundle does NOT reference the variable +(audited by the security reviewer) so there is no live exposure today, +but rebuilding-from-this-directory would regress that. + +**Blast radius.** +- Currently zero — the live key has been rotated and the live bundle does + not include the leaked literal. +- If someone rebuilds web-app without replacing the value first, the dead + literal lands back in `dist/` and gets deployed to Hostinger. + +**Maintenance window.** 5 minutes for the file edit. The git-history +rewrite (if pursued) is a coordination cost across collaborators with +local clones, not a maintenance window per se. + +**Dependencies.** +Team Web-Hygiene (separate parallel agent) is editing +`web-app/.env.production` to either a placeholder or the rotated value. +The git-history rewrite decision stays with the operator. + +**Operator decisions required.** + +1. **(a) On-disk value.** Confirm Team Web-Hygiene replaced the literal + with either `VITE_BIOMETRIC_API_KEY=__SET_AT_DEPLOY_TIME__` (placeholder) + or the rotated live value. Recommended: placeholder, so the rotated + key never sits in any tree that ships to GitHub or to a CI cache. + ```bash + grep -n "VITE_BIOMETRIC_API_KEY" /opt/projects/fivucsas/web-app/.env.production + # expect either placeholder or no leaked literal + ``` +2. **(b) Git history rewrite.** Decide whether to expunge `6bdedd2` from + history. This is destructive: + - Forces every collaborator to re-clone or run + `git filter-repo`-equivalent locally. + - Invalidates any commit-pinned references in CHANGELOG, PR + descriptions, and external docs. + - Recommended approach if you do pursue it: + ```bash + # WARNING: coordinate with all collaborators first. + cd /opt/projects/fivucsas/web-app + git filter-repo --invert-paths --path .env.production + # then force-push and notify the team. + ``` + - Recommendation: skip the rewrite. The key is dead, the bundle is + clean, and the cost of a force-push to a public repo with five + collaborators outweighs the marginal forensic benefit. + +--- + +## 4. Branch reconciliation: parent main is behind master (HIGH) + +**Background.** +The parent FIVUCSAS monorepo has two branches that should track each +other: + +- `master` — integration branch where PRs land. Today it is 220 commits + ahead of `main`. +- `main` — the GitHub default branch and the marketing target. It is + 134 commits ahead of master in raw `git log` terms, but every one of + those 134 commits was already merged into master via parent PR #51 + (the 2026-05-11 reconciliation PR). + +The 220-commit lead of master is the genuine integration drift; the +134-commit "lead" of main is illusory because they're the same commits +from a different merge angle. + +Verified today: +```bash +cd /opt/projects/fivucsas +git log --oneline main..master | wc -l # 220 +git log --oneline master..main | wc -l # 134 +``` + +Memory entry `project_session_20260511` notes that PR #51 was the +2026-05-11 reconciliation — its 134 commits were brought into master +but the operator deferred the reverse direction. + +**Blast radius.** +- GitHub PR UI defaults base-branch to `main`, so first-time contributors + may target main and have their PR confusingly rebased onto master + later. +- CI workflows that filter on `main` (only) are running against a stale + tree. +- Reviewers looking at https://github.com/Rollingcat-Software/FIVUCSAS see + a stale README/CLAUDE.md/ROADMAP. + +**Maintenance window.** 1 minute. Fast-forward push, no PR required. + +**Dependencies.** None on submodules — memory entry +`project_session_20260511` confirms submodule HEADs are already aligned. + +**Suggested execution path.** +```bash +cd /opt/projects/fivucsas +git fetch origin +# Sanity: confirm master is strictly ahead of main (every main commit is +# already on master, so this is a fast-forward). +git merge-base --is-ancestor origin/main origin/master \ + && echo "OK: main is an ancestor of master, fast-forward safe." +# Apply: +git push origin master:main --force-with-lease +``` + +**Acceptance check.** +```bash +git log --oneline master..origin/main | wc -l # expect 0 +git log --oneline origin/main..master | wc -l # expect 0 +``` + +--- + +## 5. HS512 secret revocation — pending Team Auth-Java PR (MEDIUM) + +**Background.** +A historical HS512 JWT signing key (kid `hs-2026-04`) was rotated out of +service. Verification still accepts that kid because `HsKeyRegistry` +retains it for the no-logout rotation pattern (PR #64, 2026-05-04). +Team Auth-Java is shipping an explicit `revoked-kids` list in +`application-prod.yml` so verification refuses `hs-2026-04` outright. +Until that PR merges and the api container is rebuilt, tokens minted +with the leaked secret remain accepted. + +**Blast radius.** +Anyone who held a copy of the leaked HS512 secret can forge a JWT until +the revocation flips. The secret rotation date is the latest known +exposure boundary; effective compromise window persists until rebuild. + +**Maintenance window.** Zero-downtime; api container rolling restart +~30 seconds. + +**Dependencies.** Team Auth-Java PR must merge first. After merge: + +```bash +cd /opt/projects/fivucsas/identity-core-api +git pull +docker compose -f docker-compose.prod.yml --env-file .env.prod build --no-cache identity-core-api +docker compose -f docker-compose.prod.yml --env-file .env.prod up -d identity-core-api +``` + +**Acceptance check.** +After rebuild, attempt verification with a token signed by the revoked +kid (use a stored prod-audit-log JWT from before the rotation if +available): +```bash +curl -sS -H "Authorization: Bearer " \ + https://api.fivucsas.com/api/v1/users/me +# expect 401 with body referencing "kid revoked" or generic invalid +``` + +**Co-shipped behavior change to anticipate: JWT `aud` claim enforcement.** +Team Auth-Java PR #100 also binds and validates the `aud` claim on every +access token (default `fivucsas-api`). After the same api container +rebuild that activates the HS512 kid revocation, **every access token +currently in flight will fail validation** because pre-rebuild tokens +were minted without `aud`. The client SDK silently calls `/refresh` and +re-mints — so user-visible impact is zero, but `/refresh` traffic will +spike for ~15 minutes (one extra call per active session). Watch the +Loki dashboard for the spike to confirm it decays cleanly. If `/refresh` +stays elevated after 30 minutes, something is wrong (likely a service +account or background job holding a long-lived token without refresh +logic — investigate, do not roll back). + +If a tenant needs a non-default audience, set `APP_SECURITY_JWT_AUDIENCE` +in `.env.prod` BEFORE the rebuild; otherwise `fivucsas-api` is baked into +the prod profile literal. + +--- + +## Quick reference: per-item severity + dependency matrix + +| # | Item | Severity | Mtn window | Blocked on | +|---|-------------------------------|----------|-------------|------------------------| +| 1 | audit_logs partman bootstrap | HIGH | 15-30 min | custom postgres image | +| 2 | RLS theatre | CRITICAL | 30-60 min | new postgres role + V62 migration | +| 3 | web-app .env.production leak | HIGH | 5 min | Team Web-Hygiene PR | +| 4 | parent main fast-forward | HIGH | 1 min | nothing | +| 5 | HS512 kid revocation | MEDIUM | rolling | Team Auth-Java PR | +| 6 | DEEPFACE_FACENET512_SHA256 pin | HIGH | 2 min | Team Bio-Python PR | +| 7 | Bio container rebuild | HIGH | 1-2 min | items 5 + 6 + Bio-Python PR | +| 8 | ANTISPOOF_BLOCK_ENFORCE canary | MEDIUM | n/a | Bio-Python PR + item 7 | +| 9 | EAR liveness model deploy | LOW | 5 min | item 7 | +| 10 | identity-core-api puzzle proxy | LOW | 1 PR cycle | follow-up agent / dev | +| 11 | bio_models volume cleanup | LOW | 2 min | bake-in PR (Team B) | + +Recommended order if attacking all eleven in one session: +4 (instant) → 3 (post-merge verify) → 5 + 6 + 7 batched (single +api+bio rebuild flips JWT aud + HS512 kid + SHA pin + ANTISPOOF block +all at once) → 8 (24-48h soak then decide) → 11 (verify bake-in +self-heal) → 1 (maintenance window) → 2 (longer maintenance window, +covers RLS smoke-test) → 9 + 10 (low-risk follow-ups). + +--- + +## 6. DEEPFACE_FACENET512_SHA256 pinning — pending Team Bio-Python PR (HIGH) + +**Background.** Team Bio-Python PR #102 flips `DEEPFACE_SHA256_REQUIRED=true` +by default, which raises `RuntimeError` on biometric-processor boot if +`DEEPFACE_FACENET512_SHA256` is empty (was previously a WARN + skip). The +running container's Facenet512 weight was hashed during the agent's +investigation: + +``` +DEEPFACE_FACENET512_SHA256=3f76b5117a9ca574d536af8199e6720089eb4ad3dc7e93534496d88265de864f +``` + +The agent already wrote this value into the gitignored +`/opt/projects/fivucsas/biometric-processor/.env.prod` on the host. Verify +it's present before rebuilding: + +```bash +grep '^DEEPFACE_FACENET512_SHA256=' /opt/projects/fivucsas/biometric-processor/.env.prod +# Expected: DEEPFACE_FACENET512_SHA256=3f76b5117a9ca574... +``` + +If the value is missing or wrong, the bio container will fail-fast on +startup — that is by design (defense in depth for model integrity per +the FIVUCSAS ML Split D1-D4 decision). + +**Blast radius.** Without the pin set, bio container does not start +after the next rebuild → /verify and /enroll fail 5xx until the pin is +in place. + +**Maintenance window.** 2 minutes to edit + verify. + +**Acceptance check.** After rebuild (item 7), `docker logs bio-api 2>&1 | grep -i 'sha256'` should show pin-validated, not fail-closed. + +--- + +## 7. Bio container rebuild — coordinated with items 5 + 6 (HIGH) + +**Background.** PR #102 changes default behavior for three things at +once: `DEEPFACE_SHA256_REQUIRED=true`, `ANTISPOOF_BLOCK_ENFORCE=true`, +new `/api/v1/liveness/verify-challenge` endpoint. Plus PR #102 has a +hard dependency on spoof-detector PR #18 (the new public-namespace shim +that exposes `BlinkAnalyzer` + `compute_ear`). Merge order must be +**spoof-detector #18 → bio #102 → web-app #90**, then rebuild bio. + +**Commands.** + +```bash +# 1. Confirm prerequisites are merged in the correct order +# spoof-detector main has the shim; bio main pins the submodule pointer past it +cd /opt/projects/fivucsas/biometric-processor +git pull && git submodule update --init --recursive +git -C ../spoof-detector log --oneline -3 # expect commit 12da821 or its successor in main + +# 2. Pin DEEPFACE_FACENET512_SHA256 per item 6, then rebuild +docker compose -f docker-compose.prod.yml --env-file .env.prod build --no-cache biometric-api +docker compose -f docker-compose.prod.yml --env-file .env.prod up -d biometric-api +``` + +**Acceptance check.** + +```bash +# Health probe +curl -fsS https://api.fivucsas.com/api/v1/biometric/health || echo FAIL +# Confirm anti-spoof block is enforcing +docker logs bio-api 2>&1 | tail -50 | grep -E "ANTISPOOF_BLOCK_ENFORCE|sha256" +``` + +If the container fail-fast loops, most common causes: +- Item 6 pin missing → SHA mismatch RuntimeError +- spoof-detector pointer not advanced past PR #18 → ImportError on + `compute_ear` / `BlinkAnalyzer` +- Submodule recursion not run → stale pointer + +--- + +## 8. ANTISPOOF_BLOCK_ENFORCE canary decision (MEDIUM) + +**Background.** Bio PR #102 ships `ANTISPOOF_BLOCK_ENFORCE=true` as +default. When the assembler verdict (or any sub-signal: +`face_usability_block`, `hybrid_fusion_is_spoof`) returns +`recommended_action='block'`, /verify will now return HTTP 403 with +`{error_code:"ANTISPOOF_BLOCKED", reason:...}` instead of just logging. + +Before this PR the action was advisory only — anti-spoof has been +running but not blocking. Flipping the switch is what the 2026-05-12 +ML review called out as a P0 ("anti-spoof is structurally unreachable +from /verify because the verdict is advisory"). The change is correct; +the canary question is how confident we are in the default thresholds +and the sub-signal calibration. + +**Two acceptable rollout paths.** + +1. **Default-ON, monitor (recommended).** Leave `ANTISPOOF_BLOCK_ENFORCE=true` + in the image, watch the Loki dashboard for 24-48 hours, focus on the + ratio of `ANTISPOOF_BLOCKED` responses on /verify. A spike to >2% of + /verify traffic implies a false-positive issue with the new EAR check + (which is OFF by default — see item 9 — so the spike would have to + come from face-usability or hybrid-fusion). +2. **Flip-false-for-observation.** Add + `ANTISPOOF_BLOCK_ENFORCE=false` to `.env.prod`, redeploy, and consume + the bio audit log feed for 24-48 hours to count "would-have-blocked" + events. Flip back to true once the rate looks tolerable. + +**Blast radius.** False positives = real users get 403 on /verify. +Existing audit logs already record the verdict — so the question is one +of UX (cost of a wrongly-rejected verify call) versus security (cost of +a successfully-rejected spoof attempt). + +**Decision deadline.** Within 48h of bio container rebuild; otherwise +the canary signal degrades. + +--- + +## 9. EAR liveness model deployment (LOW) + +**Background.** Bio PR #102 wires `compute_ear` from +`spoof_detector.infrastructure.analyzers.blink_analyzer` into /verify +as a single-still-frame liveness check. Closed eyes → veto. The check +defaults OFF (`ANTISPOOF_EAR_VETO_ENABLED=false`) until the +MediaPipe FaceLandmarker model is deployed at +`models/face_landmarker.task` — if the model is missing the helper +fails-soft to None and the verdict is "no signal" (not "fail"). + +**Commands.** + +```bash +# Copy face_landmarker.task into the bio container's model volume. +# Path inside container: ${FACE_LANDMARKER_MODEL_PATH} (e.g. /models/face_landmarker.task) +# Download canonical from MediaPipe: +# https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task +docker cp face_landmarker.task bio-api:/models/face_landmarker.task +# Or rebuild image with the model baked in via Dockerfile COPY + +# Then flip the flag in .env.prod +echo 'ANTISPOOF_EAR_VETO_ENABLED=true' >> .env.prod +# Set the SHA256 pin for the model +echo 'FACE_LANDMARKER_MODEL_SHA256=' >> .env.prod + +# Bounce only bio-api, no rebuild needed if the model is in a volume +docker compose -f docker-compose.prod.yml --env-file .env.prod restart biometric-api +``` + +**Acceptance check.** Trigger a `/verify` call with eyes-closed selfie +in the testbed; expect `eyes_closed: true` in the bio audit log +verdict. Trigger normal verify; expect `eyes_closed: false`. + +--- + +## 10. identity-core-api puzzle proxy — follow-up PR (LOW) + +**Background.** Web-app PR #90 adds `useBiometricPuzzleServer` hook +which POSTs to `POST /api/v1/biometric/puzzles/verify-challenge`. That +proxy needs to live in identity-core-api (it forwards to bio +`/api/v1/liveness/verify-challenge` with the bio API key). The agent +deferred this because the identity-core-api repo was on the +`fix/2026-05-12-infra-hygiene` branch with concurrent edits from another +session. + +Until the proxy lands, FacePuzzle + HandGesturePuzzle soft-pass with a +`console.warn` (404 → soft-pass per `useBiometricPuzzleServer.ts:140`). +User-visible behavior is unchanged from pre-PR baseline. + +**Commands.** None — this is a follow-up coding task, not an operator +action. Dispatch a small agent to add: + +``` +PostMapping("/api/v1/biometric/puzzles/verify-challenge") + → BiometricProcessorClient.verifyChallenge(...) + → bio POST /api/v1/liveness/verify-challenge +``` + +with the same API-key middleware contract used by other bio proxies. + +**Acceptance check.** After deploy, web-app browser console should stop +printing `[biometric-puzzles] /biometric/puzzles/verify-challenge proxy +not deployed yet` warnings on first puzzle completion. + +--- + +## 11. bio_models volume cleanup — post bake-in PR (LOW) + +**Background.** 2026-05-12 Team B PR `fix/2026-05-12-bake-mini-fasnet-models` +(bio repo PR #104, parent PR also `fix/2026-05-12-bake-mini-fasnet-models`) +bakes the four DeepFace / Facenet model weights into the bio image layer +and adds an entrypoint shim that: + +1. chowns the externally-mounted cache volume to uid 100 / gid 101 (the + `app` user inside the container), and +2. seeds missing weight files from `/opt/baked-models/` so a wiped named + volume is self-healing. + +Before this PR a hot-fix from earlier in the day had manually `docker cp`'d +`2.7_80x80_MiniFASNetV2.pth` and `4_0_0_80x80_MiniFASNetV1SE.pth` into the +running container's volume. That fix was load-bearing on operator memory: +the next `docker volume rm` would have re-triggered the false-spoof-verdict +chain. With the bake-in shipped, that operator memory is no longer +load-bearing — but two cleanup paths exist depending on how aggressively +you want to verify the bake-in. + +This is the 4th recurrence of the pattern documented in +`feedback_readonly_rootfs_cache_dirs` (prior: DeepFace, Numba, UniFace). +After this PR ships the contract is **bake at build-time + self-heal at +boot-time**, not "remember to docker-cp". + +**Blast radius.** Either path causes a brief (~5s) bio-api restart. No data +loss; `biometric_uploads` and `biometric_uniface` volumes are untouched. + +**Maintenance window.** 2 minutes. + +**Dependencies.** Rebuild bio image from the merged Team B PR first. + +**Option A — Wipe & verify self-heal (recommended).** + +```bash +cd /opt/projects/fivucsas/biometric-processor +docker compose -f docker-compose.prod.yml --env-file .env.prod down biometric-api +docker volume rm biometric-processor_biometric_models +docker compose -f docker-compose.prod.yml --env-file .env.prod up -d biometric-api +# Confirm the entrypoint shim re-seeded the cache: +docker exec biometric-api ls -la /tmp/.deepface/.deepface/weights/ +# Expected: all four files owned by 100:101 with the SHAs documented in +# item 6 (facenet512) and the bio-PR description (centerface, MiniFASNetV2, +# MiniFASNetV1SE). +``` + +**Option B — Keep & re-own the existing volume.** + +```bash +# Fix ownership on the existing volume in-place (one-time, host side). +chown -R 100:101 /var/lib/docker/volumes/biometric-processor_biometric_models/_data +# Restart bio-api so it picks up the corrected ownership. +docker compose -f docker-compose.prod.yml --env-file .env.prod restart biometric-api +``` + +**Acceptance check.** Either path: `docker exec biometric-api stat -c '%u:%g' +/tmp/.deepface/.deepface/weights/facenet512_weights.h5` returns `100:101`. + +**Why this is LOW priority.** The container is currently healthy with the +docker-cp'd files in place. This cleanup item exists to remove an +undocumented assumption (operator memory of the manual `docker cp`), not +to fix a live failure. diff --git a/PHASE_4_PRODUCTIZATION_PLAN_2026-05-11.md b/archive/2026-05/plans/PHASE_4_PRODUCTIZATION_PLAN_2026-05-11.md similarity index 100% rename from PHASE_4_PRODUCTIZATION_PLAN_2026-05-11.md rename to archive/2026-05/plans/PHASE_4_PRODUCTIZATION_PLAN_2026-05-11.md diff --git a/biometric-processor b/biometric-processor index 053e73d..726d3c3 160000 --- a/biometric-processor +++ b/biometric-processor @@ -1 +1 @@ -Subproject commit 053e73d1ccff2d95f09fe644c66a054bc8e0aa30 +Subproject commit 726d3c327f7eced3b7849913a747889f6fe255dc diff --git a/bys-demo/callback.html b/bys-demo/callback.html index 51e1c27..6f033d2 100644 --- a/bys-demo/callback.html +++ b/bys-demo/callback.html @@ -23,6 +23,20 @@ + +
+ ← FIVUCSAS + | + Dashboard + | + Demo + | + Widget + | + amispoof + Status ↑ +
+