# JobTracker full-application audit

Audit date: 2026-08-02  
Branch: `release-readiness`  
Scope: repository, disposable local runtime, synthetic users/data, isolated Chromium tests, and read-only external documentation/advisory lookup. No production system or real provider was contacted.

## 1. Executive summary

JobTracker is a substantial, coherent application with unusually broad automated tests, explicit tenant ownership, suggestion-only AI workflows, safe public-CV publishing, and a deployment process that at least treats backup/health as first-class concerns. It is not ready for production release in its audited state.

Six High findings remain after sceptical validation:

1. Microsoft multitenant token identity/issuer binding is unsafe for email auto-linking.
2. Password/verification links can be built from an attacker-controlled Host when public base URL is blank.
3. Known vulnerable document parsers process authenticated untrusted uploads without resource isolation.
4. Default documented SQLite fails important Career/Application Workspace endpoints.
5. Four background services cannot see tenant rows and silently do no useful work.
6. Duplicate timeline/interview routes make those core endpoints fail before business logic.

No Critical finding and no confirmed cross-user disclosure were found. Two-user tests passed for meaningful job, company, correspondence, attachment, profile, checklist, settings, and admin results; several CV/AI/workspace checks remain blocked by the application 500s rather than counted as passes.

Incremental remediation is reasonable. A rewrite is neither supported by the evidence nor recommended.

## 2. Overall application condition

**Condition: feature-rich but release-blocked.** The solution builds and its broad unit/component suites pass, yet the default runtime has broken core paths that those tests miss. Security foundations are mostly thoughtful, but identity linking/recovery-origin handling and the untrusted document-parser stack need Phase 0 attention. Reliability promises around rules/reminders/enrichment/export are currently false because of the tenant-filter/background-context interaction.

## 3. System inventory

| Component | Current implementation | Role |
|---|---|---|
| Frontend | React 19, TypeScript, MUI, React Router, Next.js 16 static export | Public/auth and authenticated job/career/CV/email/settings/admin UI |
| API | ASP.NET Core/.NET 9 | Local/social auth, REST workflows, files, integrations, billing, workers |
| Database | EF Core 9; SQLite default, Pomelo MariaDB/MySQL optional | Identity and tenant-owned application data |
| AI sidecar | FastAPI, Transformers/PyTorch, pypdf/Pillow/OCR, Ollama/Gemini/Groq routing | CV extraction, summaries, writing assistance |
| Background work | Seven API hosted services | SQLite backup, rules, reminders, daily export, enrichment, AI probe, CV queue |
| External boundaries | Google/Gmail, Microsoft/Graph, IMAP/SMTP, Stripe, Turnstile, NAV/import, LibreTranslate, AI providers | Identity, communication, billing, discovery, translation, generation |
| Delivery | Three Dockerfiles, Compose, nginx, Gitea Actions, shell deploy | Build, ingress, deployment, health/backup gating |

Generated/build/vendored/ignored paths excluded from manual code review are listed in `evidence/repository-inventory.md`.

## 4. Architecture overview

The browser talks through nginx to one ASP.NET API. The API owns authentication, EF, file storage, external providers, all hosted work, and startup migrations/reconciliation. Tenant roots carry `OwnerUserId`; request-time EF filters deny on null and match the authenticated user. The AI sidecar is private to the backend in Compose and can route to local or cloud models. SQLite stores metadata while attachments, CV artifacts/exports, backups, avatars and Data Protection keys live in a shared data root.

Boundaries are generally understandable and incremental. The main architectural fault is using an HTTP-current-user global filter for both request and background scopes without a deliberate worker tenant-bypass pattern. The second is dual schema ownership: EF migrations plus a 2,000+ line startup reconciler both manipulate schema, increasing provider/deployment risk.

## 5. What is implemented well

- Explicit authorization on tenant controllers; admin operations use role authorization.
- Deny-on-null EF filters and repeated explicit owner predicates; direct two-user checks found no disclosure in meaningful results.
- HttpOnly session cookie, CSRF cookie/header, server session table, lockout/rate limits, TOTP/recovery/trusted-device features.
- Upload names are normalised and stored under random filenames; attachment direct-file access rechecks ownership.
- Public CV requires explicit publication and a random 32-hex slug; PDF output was validated as a real PDF in Chromium.
- CV HTML renderer encodes user text and validates URLs; frontend avoids `dangerouslySetInnerHTML` in reviewed job/email paths.
- Job import blocks literal/loopback/private/reserved addresses and redirects; AI service is private-networked and token-protected in production Compose.
- Stripe webhook signature validation and current-state refresh reduce spoofing/out-of-order risk.
- AI suggestions are append-only and require user review; they do not silently mutate profile/application data.
- SQLite uses `VACUUM INTO`; the disposable database and full-data-root restore rehearsal passed.
- 462 backend, 148 frontend, 17 Python, and four Chromium tests passed locally.

## 6. Verification performed

| Area | Result |
|---|---|
| .NET restore/build | Pass; Release build 0 warnings/errors |
| Backend tests | 462/462 pass |
| Frontend tests/build | 148/148 pass; Next export pass |
| Standalone TypeScript | Fail at `nginx-config.test.ts:7` because ES2017 target rejects regex `s` flag |
| Python tests | 17/17 pass; five SWIG deprecation warnings |
| Browser smoke | 4/4 pass; exact journeys in `evidence/browser-evidence.md` |
| Formatting | Fail; 1,301 whitespace diagnostics across 16 files |
| Compose/Dockerfile checks | Pass |
| EF model | 19 migrations; no pending model changes |
| NuGet advisory | Clean |
| npm advisory | Two affected packages/four moderate advisory entries |
| Python advisory | 119 records; direct untrusted-parser paths validated separately |
| Secret patterns | One expired local JWT artifact; historical Data Protection key paths; no values printed |
| Two-user isolation | No disclosure in meaningful results; some paths blocked by 500s |
| SQLite restore | Database and full-data-root rehearsal pass |
| Safe performance sample | Warm small-data API means 6.5–17.8 ms; not a capacity test |

Every meaningful executable command and failure classification is in `verification-log.md`.

## 7. Prioritised findings

| Priority | ID | Severity | Finding |
|---:|---|---|---|
| 1 | JT-001 | High | Microsoft multitenant identity is not safely bound before email auto-link |
| 2 | JT-002 | High | Recovery and verification links can trust attacker-controlled Host |
| 3 | JT-006 | High | Vulnerable parsers handle untrusted documents without resource isolation |
| 4 | JT-003 | High | Default SQLite breaks Career/Application Workspace APIs |
| 5 | JT-005 | High | Tenant filters make four hosted services inert |
| 6 | JT-004 | High | Duplicate timeline/interview routes always fail |
| 7 | JT-007 | Medium | Email verification is bypassed at registration and email change |
| 8 | JT-008 | Medium | Logout/password recovery do not revoke outstanding sessions |
| 9 | JT-009 | Medium | Account deletion/export do not cover user data lifecycle |
| 10 | JT-013 | Medium | Recovery excludes required files/keys and MariaDB has no tested procedure |
| 11 | JT-010 | Medium | Attachment file and database mutations are not atomic |
| 12 | JT-011 | Medium | Import/upload limits are enforced after whole-body buffering |
| 13 | JT-012 | Medium | Notification preferences are cosmetic client-only state |
| 14 | JT-014 | Medium | Test/CI gates miss observed route/provider/worker defects |
| 15 | JT-015 | Medium | Important controls lack names or keyboard semantics |
| 16 | JT-017 | Medium | Dependency/provenance controls leave known and mutable supply-chain risk |
| 17 | JT-018 | Medium | Current documentation materially disagrees with the application |
| 18 | JT-019 | Medium | Startup schema reconciler duplicates migration ownership |
| 19 | JT-022 | Medium | AI recipient/opt-out/data-minimisation controls are incomplete |
| 20 | JT-016 | Low | Standalone type and format baselines are red |
| 21 | JT-020 | Low | Credential-like artifacts remain tracked/in history |
| 22 | JT-021 | Low | Admin and correspondence scaling limits need measurement |
| 23 | JT-023 | Low | Public-CV PDF rate limit is shared per slug |
| 24 | JT-024 | Low | DNS rebinding remains after hostname validation |
| 25 | JT-025 | Low | Same-origin authenticated CV previews lack sandbox defence-in-depth |

## 8. Detailed findings

### JT-001 — Microsoft multitenant identity is not safely bound before email auto-link

- **Category / severity / confidence / classification:** Authentication; High; High; likely defect (security).
- **Affected component/location:** `JobTrackerApi/Services/MicrosoftTokenValidator.cs:52-99`; `JobTrackerApi/Controllers/AuthController.cs:298-344`.
- **User journey:** Microsoft sign-in/linking (UJ-06).
- **Reproduction/prerequisites:** A valid Microsoft token for the configured client from a supported tenant, with a mutable email-like claim matching a local victim. External exploit was not attempted.
- **Evidence:** issuer validation is disabled and replaced by hostname/suffix shape; `tid` is not tied to issuer; `oid` is not tenant-namespaced; `preferred_username` is accepted and marked verified; Auth auto-links by that email. Microsoft guidance says multitenant issuer must bind to `tid`, `tid` must be part of the data key, and mutable `preferred_username` must not drive authorization.
- **Existing mitigations:** signature/audience/lifetime/signing-key validation; Microsoft hostname shape; victim 2FA still challenges.
- **Impact:** plausible local-account takeover/incorrect account linking with victim job, CV, correspondence and integrations exposed.
- **Recommended remediation:** use Microsoft.Identity.Web or equivalent exact multitenant issuer validator; require GUID `tid`, exact `iss`/`tid` relationship, and store provider key as `(tid, oid)`; remove automatic linking by mutable email or require an authenticated explicit link/strong verified ownership ceremony.
- **Effort / breaking implications:** Medium; existing `MicrosoftSubject` data needs a tenant-aware migration/relink plan.
- **Acceptance criteria:** tokens with mismatched/missing tenant, shape-only issuer, or mutable-only email cannot create/link a session; existing valid tenant users have a documented transition.
- **Regression tests:** signed test tokens for allowed tenant, different tenant, bad `tid`/`iss`, duplicate `oid` across tenants, mutable username collision, explicit-link flow, and 2FA.

### JT-002 — Recovery and verification links can trust attacker-controlled Host

- **Category / severity / confidence / classification:** Authentication; High; High; likely defect (security).
- **Affected component/location:** `AuthController.cs:695-701,806-812`; `UsersController.cs:149-155`; `appsettings.json:12`; `docker-compose.yml:52`; `job-tracker-ui/nginx.conf:3,21,31`; `deploy/first-production-deployment.md:52`.
- **User journey:** password reset/email verification (UJ-05).
- **Reproduction/prerequisites:** `App:PublicBaseUrl` blank, public ingress accepts arbitrary Host, victim clicks the generated email. Compose permits blank, nginx accepts `_` and forwards `$host`, and AllowedHosts is `*`. No real email was sent.
- **Evidence:** all three link builders fall back to `Request.Host`.
- **Existing mitigations:** auth-email rate limit; configured public URL avoids fallback; victim 2FA still applies.
- **Impact:** attacker-domain reset/verification link can disclose token and enable password takeover or verification confusion.
- **Recommended remediation:** require and validate an absolute allowlisted public origin whenever email flows are enabled; never derive security links from request Host; restrict ingress/AllowedHosts.
- **Effort / breaking implications:** Small; deployment configuration becomes required/fail-fast.
- **Acceptance criteria:** startup fails or email flow refuses safely without valid origin; hostile Host never appears in link; canonical HTTPS origin only.
- **Regression tests:** hostile/Unicode/port Host with blank and configured base URL; reverse-proxy headers; production-config test.

### JT-003 — Default SQLite breaks Career/Application Workspace APIs

- **Category / severity / confidence / classification:** Backend/data compatibility; High; Confirmed; confirmed defect.
- **Affected component/location:** `CvVariantService.cs:52-53`; `ProfileCvController.cs:209-213` and related run ordering; `AiWorkspaceService.cs:164-166`; `AiWorkspaceController.cs:48-53`; `AiUsageController.cs:35-38`; `ApplicationWorkspaceService.cs:67-92`; `ApplicationAssetsService.cs:112-115`.
- **User journey:** empty first run, Career Profile/CV, AI assistance, job workspace (UJ-08/UJ-11/UJ-14/UJ-16/UJ-21).
- **Reproduction:** run documented/default SQLite API; authenticate; request CV variants, extraction runs, AI history/usage, or workspace. Owner and empty B both reproduced 500s.
- **Evidence:** EF SQLite cannot translate relational ordering/comparison over `DateTimeOffset`; sibling services already document/materialise around this limitation, but these paths do not.
- **Existing mitigations:** generic 500 hides internals; MariaDB may translate; some unrelated controllers order locally.
- **Impact:** CV Builder/history, AI usage/history and job workspace cannot reliably function in default local/self-host setup.
- **Recommended remediation:** apply the existing repository pattern: constrain/shape in SQL where supported, materialise bounded owner-scoped sets, then order/compare locally for SQLite; or map compatible UTC storage consistently. Fix shared query roots, not individual responses.
- **Effort / breaking implications:** Medium; no schema migration should be needed for local materialisation; storage-type change would require migration.
- **Acceptance criteria:** all reproduced endpoints return correct 200/404 for owner/empty/non-owner on SQLite and MariaDB.
- **Regression tests:** HTTP integration tests against fresh SQLite with zero/one/many rows; provider-parity suite for affected queries.

### JT-004 — Duplicate timeline/interview routes always fail

- **Category / severity / confidence / classification:** API routing; High; Confirmed; confirmed defect.
- **Affected component/location:** `JobApplicationsController.cs:1085-1086,1679`; `ApplicationIntelligenceController.cs:14-35`; `InterviewPrepController.cs:14-32`.
- **User journey:** application timeline/interview preparation (UJ-11).
- **Reproduction:** authenticated or copied-ID `GET /api/jobapplications/1/timeline` and `/interview-prep`; both returned 500 from ambiguous action matching.
- **Evidence:** two actions resolve to each identical final route; failure occurs before owner logic for A and B.
- **Existing mitigations:** none; sibling analysis/match/checklist endpoints work.
- **Impact:** core workspace timeline and interview-prep unavailable; isolation behaviour on those URLs cannot be verified.
- **Recommended remediation:** select one canonical implementation/DTO per route; remove/rename the duplicate after checking frontend and API callers.
- **Effort / breaking implications:** Small to Medium; response-contract compatibility must be chosen deliberately.
- **Acceptance criteria:** endpoint table has one action per verb/path; owner 200, non-owner 404, anonymous 401.
- **Regression tests:** application-start route ambiguity assertion and HTTP contract tests for both routes.

### JT-005 — Tenant filters make four hosted services inert

- **Category / severity / confidence / classification:** Reliability/background processing; High; Confirmed; confirmed defect.
- **Affected component/location:** `CurrentUserService.cs:10-19`; `JobTrackerContext.cs:20,67-76` and owned filters; `RulesHostedService.cs:22-64`; `FollowUpReminderHostedService.cs:51-76`; `DailyExportHostedService.cs:76-91`; `JobEnrichmentHostedService.cs:24-89`.
- **User journey:** pipeline automation, follow-ups, notifications, exports, job enrichment (UJ-10/UJ-11/UJ-20/UJ-21).
- **Reproduction:** old Applied synthetic job with null deterministic tags/summary; restart and wait past worker initial delays; status/tags/summary unchanged.
- **Evidence:** hosted scopes have no HttpContext, current user is null, and deny-on-null filters produce no rows. Daily export/rules/reminders use the same pattern. Rules also swallows all exceptions without logging. CV queue correctly demonstrates an explicit `IgnoreQueryFilters` worker pattern.
- **Existing mitigations:** worker loops continue; follow-up email default may be disabled; CV queue is correctly explicit.
- **Impact:** promised status rules, reminders, enrichment and scheduled export silently do nothing or create empty output.
- **Recommended remediation:** create an explicit background data-access pattern that bypasses filters only for worker enumeration, groups by owner, and re-enters owner-scoped processing; add structured result/error counts. Review privacy before activating automatic AI enrichment.
- **Effort / breaking implications:** Medium; behaviour starts running for real, so rollout/notification/AI effects need gating.
- **Acceptance criteria:** deterministic disposable rows are processed once for each enabled worker; disabled features remain inactive; one tenant cannot influence another.
- **Regression tests:** real hosted-service integration tests with two owners, null HttpContext, enabled/disabled flags, retries and idempotency.

### JT-006 — Vulnerable parsers handle untrusted documents without resource isolation

- **Category / severity / confidence / classification:** Dependency security/availability; High; High; likely defect.
- **Affected component/location:** `tools/summarizer/requirements.txt:3-12`; `tools/summarizer/app.py:38-53,840-899`; `tools/summarizer/Dockerfile`; `docker-compose.yml:138-151`.
- **User journey:** CV/document import (UJ-14).
- **Reproduction/prerequisites:** authenticated user submits crafted PDF/image/multipart through backend to sidecar. No exploit file was run.
- **Evidence:** pip-audit finds 35 unique pypdf, 17 Pillow, 6 multipart and 7 Starlette advisories; many pypdf descriptions directly cover infinite loops/RAM/CPU on read/text extraction used here. Pillow detects content, so renamed formats can reach decoders. Sidecar/container have no CPU/memory/PID limits and run as root.
- **Existing mitigations:** authenticated app path, extension/eight-MB limits, private network, production service token, no host port.
- **Impact:** malicious tenant can exhaust AI service/host; memory-corruption advisories may increase container-compromise risk and expose cloud AI keys/egress.
- **Recommended remediation:** test and upgrade direct parser/framework packages to non-vulnerable compatible releases; verify file magic; stream/spool with pre-parse limits; run non-root with resource/PID/time limits and killable isolated parse work. Treat model-loader advisories separately unless reachable.
- **Effort / breaking implications:** Medium to Large; parser output/regression corpus may change; no DB migration.
- **Acceptance criteria:** audit clean for reachable parser advisories or documented exception; malicious corpus terminates within strict CPU/memory/time; normal CV corpus remains correct.
- **Regression tests:** crafted/oversized/renamed PDF/image/multipart corpus, timeout/memory enforcement, sidecar auth and network isolation.

### JT-007 — Email verification is bypassed at registration and email change

- **Category / severity / confidence / classification:** Authentication; Medium; Confirmed; confirmed defect.
- **Affected component/location:** `AuthController.cs:158-180,421-439`; `AuthAndSystemControllerTests.cs:167-201,480-503`.
- **User journey:** registration/profile email (UJ-03/UJ-05).
- **Reproduction:** verification-required disposable API: unconfirmed registration immediately got authenticated 200. Confirmed disposable user changed email; new address stayed confirmed.
- **Evidence:** runtime row/status plus direct call to `CompleteSignInAsync`; update assigns email directly rather than Identity change/confirmation token flow.
- **Existing mitigations:** later password login rejects unconfirmed users; verification email/resend exists; current session is authenticated.
- **Impact:** verification requirement does not establish initial or changed email ownership; notifications/recovery can target unverified address.
- **Recommended remediation:** registration should return verification-required without session (or issue strictly limited pending state); email changes use a pending address/token and revoke/refresh relevant sessions after confirmation.
- **Effort / breaking implications:** Medium; frontend auth/profile flow changes; possible pending-email schema/migration.
- **Acceptance criteria:** unconfirmed account cannot access protected APIs; old email remains active until new one confirmed; duplicate/collision safe.
- **Regression tests:** registration, resend, expired/used token, email change, old/new login/recovery, concurrent change, social/local accounts.

### JT-008 — Logout/password recovery do not revoke outstanding sessions

- **Category / severity / confidence / classification:** Session security; Medium; Confirmed; confirmed defect.
- **Affected component/location:** `AuthController.cs:347-355,655-674,723-743,861-866`; `LocalSessionValidator` and session controllers.
- **User journey:** sign-out/password recovery (UJ-04/UJ-05).
- **Reproduction:** copied pre-logout cookie remained 200 after logout while cleared browser jar became 401.
- **Evidence:** logout deletes cookies only; password reset/change update Identity credentials without revoking `UserSession` rows.
- **Existing mitigations:** sessions expire and users can explicitly revoke sessions; possession of cookie required.
- **Impact:** stolen cookie persists after logout/password recovery, weakening incident recovery.
- **Recommended remediation:** authenticate best-effort logout and revoke current `sid`; revoke all or all-other sessions on reset/change according to explicit policy; rotate current session when retained.
- **Effort / breaking implications:** Small to Medium; intentional multi-device sign-out behaviour change.
- **Acceptance criteria:** copied cookie fails immediately after logout; password reset invalidates all old sessions; audit event recorded.
- **Regression tests:** current/other sessions, anonymous logout, expired cookie, password reset/change, concurrent requests.

### JT-009 — Account deletion/export do not cover user data lifecycle

- **Category / severity / confidence / classification:** Privacy/data integrity; Medium; Confirmed; confirmed defect.
- **Affected component/location:** `UsersController.cs:126-136`; `BackupController.cs:33-86`; file roots and owner models in `JobTrackerContext.cs`.
- **User journey:** data export/account deletion/admin (UJ-22/UJ-23).
- **Reproduction:** code path only; irreversible deletion intentionally not performed.
- **Evidence:** admin delete calls only `UserManager.DeleteAsync`; many owned tables lack FK cascade and files/tokens remain. Export includes companies/jobs/correspondence/attachment metadata/events/rules only, omits file bytes/profile/CVs/AI/provider/session data, and is encrypted to the app key ring with no restore/import path. No self-service deletion.
- **Existing mitigations:** admin role required; tenant filters hide orphaned rows after user removal.
- **Impact:** deletion does not delete; orphaned sensitive data/files persist and user cannot obtain a complete portable export.
- **Recommended remediation:** transactional deletion manifest for all owner rows plus staged/retryable file deletion/provider revocation; user-readable complete export; explicit self-service/admin confirmation and audit trail; define backup-retention effect.
- **Effort / breaking implications:** Large; migrations/FKs or deletion ledger may be needed; irreversible operation needs rollback/retention policy.
- **Acceptance criteria:** disposable user deletion leaves no live owned rows/files/tokens/sessions; export inventory is complete/readable; partial failure is visible/retryable.
- **Regression tests:** two users, every entity/file type, provider token, retry after file failure, backup/retention documentation.

### JT-010 — Attachment file and database mutations are not atomic

- **Category / severity / confidence / classification:** Data integrity; Medium; High; likely defect.
- **Affected component/location:** `AttachmentsController.cs:117-196,199-258`.
- **User journey:** attachments (UJ-12/UJ-24).
- **Reproduction/prerequisites:** multi-file upload where a later file is invalid/cancelled or DB save fails; rename DB failure after move; delete file failure after row commit. Failure injection not run.
- **Evidence:** file writes/move happen before DB commit without cleanup; delete commits row before best-effort file delete and swallows error.
- **Existing mitigations:** generated filenames avoid overwrite; normal A upload/download passed; filesystem operations are bounded to storage root.
- **Impact:** orphan files consume quota/storage; moved file can leave DB path broken; deleted metadata can leave private bytes behind.
- **Recommended remediation:** validate whole batch first; stage files; commit metadata then atomically promote with compensating cleanup/ledger; make delete retryable/observable; reconcile orphans.
- **Effort / breaking implications:** Medium; no public API break required; optional cleanup job/ledger migration.
- **Acceptance criteria:** injected failure at every boundary leaves either complete operation or recoverable recorded state; no silent orphan.
- **Regression tests:** invalid second file, cancellation, DB failure, move conflict, delete permission failure, restart reconciliation.

### JT-011 — Import/upload limits are enforced after whole-body buffering

- **Category / severity / confidence / classification:** Resource handling; Medium; High; confirmed defect in code, exploit unverified.
- **Affected component/location:** `JobImportService.cs:109-130`; `tools/summarizer/app.py:868-876`.
- **User journey:** URL import/CV upload (UJ-13/UJ-14/UJ-24).
- **Reproduction/prerequisites:** authenticated user points import at a server with a very large/chunked body or sends large sidecar multipart through trusted backend.
- **Evidence:** `ResponseHeadersRead` is followed by `ReadAsByteArrayAsync` before four-MB check; sidecar calls `await file.read()` before eight-MB check.
- **Existing mitigations:** job client timeout/redirect/SSRF controls; backend upload limits; sidecar private/token-protected.
- **Impact:** avoidable memory/network consumption and denial of service before rejection.
- **Recommended remediation:** reject oversized declared content length; stream through a bounded reader and abort at limit; enforce server/multipart maximum request sizes before parsing.
- **Effort / breaking implications:** Small to Medium; malformed/unknown-length requests may fail earlier.
- **Acceptance criteria:** process never buffers beyond limit plus small overhead; chunked oversize aborts; valid boundary-size input passes.
- **Regression tests:** content-length over, chunked over, exact boundary, slow stream, cancellation.

### JT-012 — Notification preferences are cosmetic client-only state

- **Category / severity / confidence / classification:** Product correctness; Medium; Confirmed; confirmed defect.
- **Affected component/location:** `SettingsView.tsx:53-83,210-238`; `FollowUpReminderHostedService.cs:43-113`.
- **User journey:** notification settings (UJ-21).
- **Reproduction:** toggle settings; source shows only localStorage; worker reads global configuration/rules and never preferences.
- **Evidence:** labels promise email reminders/ghosted alerts/in-app highlights without server persistence/enforcement.
- **Existing mitigations:** explanatory text points SMTP to admin; worker currently inert for a separate reason.
- **Impact:** users believe they opted in/out but server behaviour will ignore the choice once worker is fixed.
- **Recommended remediation:** persist defined user preferences and enforce at every producer, or remove/rename controls until supported.
- **Effort / breaking implications:** Medium with user-settings migration; Small if UI removed.
- **Acceptance criteria:** toggles survive devices and demonstrably suppress/enable each channel.
- **Regression tests:** per-user mixed preferences, worker execution, default/upgrade behaviour.

### JT-013 — Recovery excludes required files/keys and MariaDB has no tested procedure

- **Category / severity / confidence / classification:** Backup/recovery; Medium; Confirmed; architectural concern.
- **Affected component/location:** `DatabaseBackupRunner.cs:16-102`; `DatabaseBackupHostedService.cs:24-80`; data roots; deployment docs.
- **User journey:** backup/settings and operator recovery (UJ-22).
- **Reproduction:** disposable SQLite DB snapshot and full-data-root restore rehearsal.
- **Evidence:** DB backup passed, but attachment access required separately copied `Attachments/` and keys. MariaDB runner explicitly unsupported. No repository RPO/RTO or recurring restoration proof.
- **Existing mitigations:** daily catch-up SQLite snapshots/retention; deploy scripts take provider-aware backups and gate deployment; deployment docs warn to verify restore.
- **Impact:** DB-only backup cannot fully restore files/protected provider tokens; MariaDB recovery depends on unverified external operations.
- **Recommended remediation:** define per-provider backup set (DB, files, keys, config), encryption/access, RPO/RTO, off-host retention, restore runbook and scheduled rehearsal evidence.
- **Effort / breaking implications:** Medium to Large operational work; no app API break.
- **Acceptance criteria:** isolated restore from documented artifacts recovers job/file/token access within RTO and expected RPO for both supported providers.
- **Regression tests:** automated integrity/count/file manifest plus quarterly disposable restore; migration-forward/backward rehearsal.

### JT-014 — Test/CI gates miss observed route/provider/worker defects

- **Category / severity / confidence / classification:** Testing/CI; Medium; Confirmed; maintainability improvement.
- **Affected component/location:** `.gitea/workflows/ci-deploy.yml`; `job-tracker-ui/e2e/smoke.spec.ts:48-53`; unit-heavy controller/service tests.
- **User journey:** all core workflows, especially UJ-11/UJ-14/UJ-16.
- **Reproduction:** 462 backend and 148 frontend tests pass while default runtime endpoints fail. Career e2e asserts shell text only.
- **Evidence:** no route-table ambiguity gate, no affected SQLite HTTP journey, no hosted-service/no-HttpContext integration, no Python tests/audit in CI, no typecheck/accessibility gate.
- **Existing mitigations:** broad deterministic suites, four isolated browser smokes, dependency audits for NuGet/npm, fresh SQLite in several tests.
- **Impact:** green CI can deploy broken core workflows/background automation.
- **Recommended remediation:** add minimum HTTP integration tests for core routes on fresh SQLite, route ambiguity startup test, two-owner worker tests, browser assertions for downstream data/error absence, Python test/audit, and standalone typecheck.
- **Effort / breaking implications:** Medium; CI time increases modestly.
- **Acceptance criteria:** each confirmed defect would fail before remediation; gates run on PR/main without whitelisting.
- **Regression tests:** the gates themselves are the tests; keep smallest representative journey per boundary.

### JT-015 — Important controls lack names or keyboard semantics

- **Category / severity / confidence / classification:** Accessibility/UX; Medium; High; confirmed code-level defect.
- **Affected component/location:** `CompaniesTable.tsx:139,186`; `Correspondence.tsx:472`; `Attachments.tsx:320-330`; `JobTable.tsx:493,704,745-748`; `SavedViewsMenu.tsx:93,153`; `CvBuilderPage.tsx:91-98`; `PublicCvPage.tsx:53-62`.
- **User journey:** keyboard/mobile/accessibility (UJ-25).
- **Reproduction:** inspect accessible-name/semantics; manual assistive-tech run blocked.
- **Evidence:** icon buttons lack `aria-label`; tooltip/title is inconsistent; clickable `Paper` has no role/tabIndex/key handler; public iframe fixed 210mm wide.
- **Existing mitigations:** MUI baseline semantics; many other controls correctly labelled; renderer iframe has title.
- **Impact:** screen-reader/keyboard users cannot identify/activate core edit/menu/navigation actions; likely mobile overflow.
- **Recommended remediation:** add contextual accessible names; make CV card a real link/button; ensure focus/keyboard activation; responsive iframe container; add automated axe plus manual keyboard viewport checks.
- **Effort / breaking implications:** Small to Medium; no data/API change.
- **Acceptance criteria:** named controls, logical tab order, visible focus, keyboard parity, no 375/768 overflow.
- **Regression tests:** role/name queries, axe on key pages, Playwright keyboard and viewport tests.

### JT-016 — Standalone type and format baselines are red

- **Category / severity / confidence / classification:** Developer experience; Low; Confirmed; maintainability improvement.
- **Affected component/location:** `job-tracker-ui/src/nginx-config.test.ts:7`; `job-tracker-ui/tsconfig.json`; 16 dotnet-format files.
- **User journey:** none directly.
- **Reproduction:** `npx tsc --noEmit`; `dotnet format ... --verify-no-changes`.
- **Evidence:** TS1501 regex `s` flag with ES2017 target; 1,301 whitespace diagnostics. Next build excludes/circumvents the test-source mismatch.
- **Existing mitigations:** product builds/tests pass; formatting does not imply runtime failure.
- **Impact:** clean quality gates cannot be enabled and real type drift may hide.
- **Recommended remediation:** align test syntax/compiler target and agree on baseline formatting in a dedicated mechanical change, not mixed with behaviour fixes.
- **Effort / breaking implications:** Small; potentially noisy formatting diff.
- **Acceptance criteria/tests:** both commands pass in CI without rewriting during check.

### JT-017 — Dependency/provenance controls leave known and mutable supply-chain risk

- **Category / severity / confidence / classification:** Supply chain; Medium; High; security hardening.
- **Affected component/location:** `job-tracker-ui/package.json`; no NuGet lock/global.json; Python requirements; three Dockerfiles; `.gitea/workflows/ci-deploy.yml:14-35,129`; bootstrap scripts.
- **User journey:** public navigation/import/deployment indirectly.
- **Reproduction:** npm/pip audits and manifest/CI inspection.
- **Evidence:** moderate React Router redirects/XSS advisories; mutable base tags/action major tags; unverified `dotnet-install.sh`; Python transitive deps unhashed; no image CVE/SBOM/licence gate. NuGet advisory clean.
- **Existing mitigations:** npm lock/`npm ci`, exact Python top-level pins, high-severity npm CI gate, signed NuGet packages/integrity, Docker static checks.
- **Impact:** known client redirect risk and upstream mutation/compromise can affect builds/deploy credentials.
- **Recommended remediation:** separately test supported React Router fix path; pin actions/images/installers by immutable digest/SHA/hash; add `global.json`, appropriate locks/hashes, SBOM/container scan and review licences.
- **Effort / breaking implications:** Medium; Router major may break APIs; image/action pins need update process.
- **Acceptance criteria:** repeatable toolchain/build; no unreviewed high/critical advisory; documented exceptions; immutable CI dependencies.
- **Regression tests:** auth/navigation redirect tests, build from clean cache, SBOM/advisory gates.

### JT-018 — Current documentation materially disagrees with the application

- **Category / severity / confidence / classification:** Documentation/DX; Medium; Confirmed; maintainability improvement.
- **Affected component/location:** `job-tracker-ui/README.md`; root `README.md`; `docs/architecture/current.md`; `deploy/README.md`; `.gitea/workflows/ci-deploy.yml` comments.
- **User journey:** developer/operator setup and feature expectations (UJ-01).
- **Reproduction:** compare docs with package/scripts/routes/providers/runtime.
- **Evidence:** CRA boilerplate versus Next/Jest; PostgreSQL recommended though unsupported; stale controller/API inventory; root API docs omit major surfaces; CI comments still describe CRA; no complete env/service reference.
- **Existing mitigations:** detailed phase/production docs contain useful operational context; Compose example exists.
- **Impact:** new developers/operators choose wrong commands/provider and misunderstand supported/released features.
- **Recommended remediation:** after behaviour fixes, replace frontend README, correct provider matrix, generate/maintain concise current architecture/API/env/setup/test/deploy source of truth; clearly mark archived/roadmap status.
- **Effort / breaking implications:** Medium documentation-only.
- **Acceptance criteria:** clean-machine documented setup/build/test succeeds; no unsupported provider or obsolete tool claims.
- **Regression tests:** CI doc command smoke where practical; review checklist tied to package scripts/config.

### JT-019 — Startup schema reconciler duplicates migration ownership

- **Category / severity / confidence / classification:** Architecture/deployment; Medium; High; architectural concern.
- **Affected component/location:** `StartupInitializationExtensions.cs` (2,000+ lines); EF migrations; deployment docs.
- **User journey:** startup/deployment/recovery.
- **Reproduction:** source/migration inspection; fresh SQLite startup passed; MariaDB not available.
- **Evidence:** startup runs hand-authored provider DDL around each EF migration and may repair/drop empty malformed tables; two mechanisms own schema order/state.
- **Existing mitigations:** extensive comments/tests, readiness gate, no pending model changes, deploy backups, provider-specific repair logic.
- **Impact:** higher risk of environment-specific destructive or order-dependent schema changes and difficult rollback.
- **Recommended remediation:** do not rewrite. Inventory each reconciler operation, assign one owner, move stable changes into migrations in small verified steps, retain only idempotent precondition checks/legacy repair until telemetry proves removable.
- **Effort / breaking implications:** Large, migration-sensitive.
- **Acceptance criteria:** fresh/upgrade/partially repaired SQLite and MariaDB matrices pass; startup performs no undocumented schema mutation.
- **Regression tests:** disposable snapshots at supported upgrade points and failure/restart recovery.

### JT-020 — Credential-like artifacts remain tracked/in history

- **Category / severity / confidence / classification:** Secrets hygiene; Low; Confirmed; security hardening.
- **Affected component/location:** expired JWT at `docs/_archive/artifacts/s06-acceptance/.dev-auth-token.txt:1`; historical Data Protection key paths.
- **User journey:** none.
- **Reproduction:** values-suppressed tracked/history regex scan and local JWT metadata validation.
- **Evidence:** expired token cannot pass current sid validation; documentation claims production key rotation but audit did not verify it.
- **Existing mitigations:** expiry/current session validation; current keys ignored; documented rotation.
- **Impact:** normalises secret artifacts and may expose historical protected data if old key material remains useful.
- **Recommended remediation:** replace with redacted fixtures; follow repository history/incident policy; independently verify rotation/revocation without publishing values.
- **Effort / breaking implications:** Small to Medium; history rewrite is disruptive and needs explicit approval—not part of audit.
- **Acceptance criteria:** scans find no live credential patterns; tests use generated/redacted tokens.
- **Regression tests:** secret scan in CI with allowlisted synthetic formats only.

### JT-021 — Admin and correspondence scaling limits need measurement

- **Category / severity / confidence / classification:** Performance/scalability; Low; High; unverified risk.
- **Affected component/location:** `UsersController.cs:43-57`; `CorrespondenceController.cs:87-90`; Gmail controller bounded scans.
- **User journey:** admin and large mailbox/dashboard.
- **Reproduction/prerequisites:** many users/messages; not generated in audit.
- **Evidence:** admin loads all users then queries roles per user (N+1); correspondence inbox silently caps at 200 without pagination contract. Small-data API timings were healthy.
- **Existing mitigations:** admin-only, bounded provider scans, jobs pagination.
- **Impact:** slow admin/incomplete large inbox at scale.
- **Recommended remediation:** measure with representative data; project roles in bounded/paged queries and add cursor/page metadata only when thresholds are reached.
- **Effort / breaking implications:** Small to Medium; pagination contract can be breaking.
- **Acceptance criteria:** defined large-data p95/query-count target and complete navigable results.
- **Regression tests:** query-count/large fixture and pagination boundary tests.

### JT-022 — AI recipient/opt-out/data-minimisation controls are incomplete

- **Category / severity / confidence / classification:** Technical privacy/AI governance; Medium; High; architectural concern.
- **Affected component/location:** `AiWorkspaceService.cs:104-160`; `JobEnrichmentHostedService.cs:30-82`; `SettingsView.tsx`; `Attachments.cs:19`.
- **User journey:** AI assistance/settings (UJ-16/UJ-17/UJ-21).
- **Reproduction:** inspect prompt construction/settings; external provider intentionally not invoked.
- **Evidence:** most modules send job text plus full master-profile text; provider selected globally; settings show usage but no global per-user opt-out/recipient explanation. Attachments have per-file inclusion. Repaired enrichment would automatically send job descriptions without an opt-in.
- **Existing mitigations:** explicit user generation for workspace, append-only suggestions, guardrails, private sidecar, attachment flags, local Ollama option.
- **Impact:** users may not understand which provider receives which personal/job data; background activation could change disclosure silently.
- **Recommended remediation:** before enabling worker, add explicit per-user AI enable/recipient/data summary, minimise module payload, default attachments off or explicit, and enforce policy server-side; record provider/purpose without sensitive prompt logs.
- **Effort / breaking implications:** Medium; user-preference migration and product copy.
- **Acceptance criteria:** disabled user causes no external AI call; UI states provider/data categories; each module sends only documented fields.
- **Regression tests:** fake provider captures field inventory for enabled/disabled users and selected attachments.

### JT-023 — Public-CV PDF rate limit is shared per slug

- **Category / severity / confidence / classification:** Availability; Low; High; security hardening.
- **Affected component/location:** public-CV rate-limit policy and PDF endpoint.
- **User journey:** public CV download (UJ-01/UJ-15).
- **Reproduction/prerequisites:** know public slug and consume its request budget; not stress-tested.
- **Evidence:** limiter key is slug-oriented, so unrelated viewers share allowance.
- **Existing mitigations:** long random slug; rate limit protects expensive PDF generation; cached/rendered behaviour may reduce cost.
- **Impact:** known published CV can be temporarily denied to legitimate viewers.
- **Recommended remediation:** combine IP/user and slug budgets or cache immutable PDF; preserve global abuse ceiling.
- **Effort / breaking implications:** Small.
- **Acceptance criteria:** one client cannot exhaust all viewer allowance; generation remains bounded.
- **Regression tests:** two IP partitions, same slug, burst/global ceiling.

### JT-024 — DNS rebinding remains after hostname validation

- **Category / severity / confidence / classification:** SSRF hardening; Low; Medium; unverified risk.
- **Affected component/location:** `JobImportService.ValidateUrlAsync/FetchHtmlAsync`; `ImapService` host validation/connect.
- **User journey:** URL import/provider connection (UJ-13/UJ-18).
- **Reproduction/prerequisites:** attacker DNS changes between validation and client resolution; not attempted.
- **Evidence:** validation resolves/checks addresses, then HttpClient/MailKit connects by hostname and can resolve again.
- **Existing mitigations:** scheme/literal/private/reserved checks, redirects disabled, connection timeouts, authentication.
- **Impact:** possible private-network connection if DNS rebinding succeeds.
- **Recommended remediation:** only if threat model warrants: pin validated addresses/connect callback or revalidate the actual connected peer; keep TLS hostname verification.
- **Effort / breaking implications:** Medium; networking complexity and CDN/multi-IP compatibility risk.
- **Acceptance criteria:** connected peer is within validated public address set; private peer rejected.
- **Regression tests:** deterministic DNS resolver that changes answers; IPv4/IPv6/multi-address/TLS cases.

### JT-025 — Same-origin authenticated CV previews lack sandbox defence-in-depth

- **Category / severity / confidence / classification:** XSS hardening; Low; Medium; security hardening.
- **Affected component/location:** `JobDetailsDialog.tsx:1044`; `CvBuilderEditor.tsx:284-294`; renderer encoding in `CvTemplateRenderer.cs`.
- **User journey:** CV preview (UJ-15/UJ-17).
- **Reproduction/prerequisites:** renderer encoding/URL validation must first be bypassed; no such bypass found.
- **Evidence:** `srcDoc` iframes are same-origin and unsandboxed; public CV iframe is sandboxed. Current server renderer encodes user data.
- **Existing mitigations:** strong output encoding/safe URL handling; no raw arbitrary user HTML path identified.
- **Impact:** a future renderer regression would have a higher-impact same-origin execution context.
- **Recommended remediation:** sandbox preview with the minimum capabilities and keep renderer tests; avoid `allow-same-origin` plus script together.
- **Effort / breaking implications:** Small, but verify PDF/fonts/links/printing.
- **Acceptance criteria:** preview works under restrictive sandbox; injected markup stays inert.
- **Regression tests:** hostile profile/job strings, URL schemes, iframe sandbox attribute and export parity.

## 9. Security assessment

No Critical issue or demonstrated cross-user exposure. Security design is stronger than average around explicit authorization, CSRF, tenant filters, public-CV release, Stripe validation, private AI networking, encoded rendering and SSRF literals. Phase 0 must still address JT-001, JT-002 and JT-006 before internet exposure. JT-007/JT-008 close account-recovery gaps. Full model and abuse paths are in `security-threat-model.md`.

## 10. Technical privacy assessment

Collected data includes identity/security state, job/application data, contacts/correspondence, Career Profile/CVs, files, provider tokens, AI prompts/results/usage and billing identifiers. It is stored in the relational DB, data-root files, key ring, backups and browser localStorage; external recipients can include identity/mail/payment/job/translation/AI providers.

Technical positives: owner scoping, encrypted provider tokens via Data Protection, file inclusion flag for AI, private sidecar, no prompt/body logging found in normal paths, explicit CV publication and suggestion-only AI.

Gaps: JT-009 deletion/export, JT-013 complete recovery/retention, JT-022 AI consent/data-minimisation, and client-only notification preference. Log/backups/provider retention and legal basis/processor contracts are operator/legal questions, not verified technical facts. No legal compliance certification is claimed.

## 11. User-journey assessment

Genuinely browser-tested: login, saved-job manual creation, Career Workspace shell rendering, and anonymous public CV/PDF. The wider manual journey was blocked by the missing browser client. Serious failures are default-SQLite Career/Application APIs, duplicate timeline/interview routes, verification/session lifecycle, and inert workers. Complete classification and step/result records are in `user-journey-audit.md`.

## 12. Accessibility assessment

Code confirms unnamed icon buttons and keyboard-inaccessible CV cards; public CV has a fixed-width overflow risk. Many MUI labels/dialog controls are sound. Manual 375/768/1440, keyboard, focus, contrast, reduced-motion and theme checks were blocked; no axe/pa11y/Lighthouse gate exists. See JT-015 and `evidence/accessibility-evidence.md`.

## 13. Testing gaps

The project has strong unit/component volume and meaningful authorization/service tests. The observed failures demonstrate the missing boundary tests: route-table uniqueness, default-provider HTTP journeys, no-HttpContext worker execution, full email/session lifecycle, two-user tests on every core resource, accessibility, Python CI, and deeper browser assertions. Raw line coverage was not used as proof.

## 14. Reliability and deployment assessment

Compose validates, Docker static checks pass, health checks and dependency ordering exist, deployment verifies commit and backs up before replacement. Weaknesses are inert/silent workers, no container resource limits/non-root users, AI not a deploy gate by design, mutable build sources, no clear metrics/alerts/SLOs, and complex startup schema reconciliation. Logs are mostly structured, but Rules swallows failures and there is no evidence of operational alerting.

## 15. Backup and recovery assessment

SQLite snapshot and restored API/file access passed when DB, attachments and keys were restored together. Database-only recovery is incomplete. MariaDB requires external backup; no safe instance existed for rehearsal. RPO/RTO, off-host encrypted retention, key/config recovery and recurring restore evidence are absent. See JT-013.

## 16. Performance assessment

Measured warm small-data API performance was healthy: means 6.5–17.8 ms, p95 33.3–111.3 ms. Static export contains 2.76 MB aggregate uncompressed JS across route chunks; this is not initial transfer size. Clearly inefficient code: whole-body buffering before size checks and admin user-role N+1. Plausible, unmeasured: large inbox caps, provider sync throughput, PDF/AI memory, large CV/profile rendering and bundle route weight. No load test or confirmed production performance defect is claimed.

## 17. Documentation and developer experience

A developer can find the solution, build/test it and start services with effort, but the repository sends contradictory signals: obsolete CRA README, unsupported PostgreSQL recommendation, stale API/architecture inventory, no SDK pin, incomplete environment/service matrix and stale CI comments. Runtime failures are generic 500s while broad tests are green, making diagnosis harder. The custom migration/reconciler split is documented in pieces but not simple to reason about safely.

## 18. Quick wins

- Remove duplicate timeline/interview routes after choosing the canonical response contract.
- Require canonical `App:PublicBaseUrl` and restrict Host at startup/ingress.
- Revoke current session on logout and sessions on password recovery.
- Fix the standalone TS target/test mismatch; add command to CI.
- Add accessible names and real link/button semantics to identified controls.
- Replace obsolete frontend/provider documentation after behaviour changes.
- Log rules-worker exceptions/results instead of swallowing them.

## 19. Larger improvements

- Correct Microsoft tenant/subject identity and migrate existing links.
- Upgrade/isolate untrusted parsers with resource/time boundaries.
- Establish explicit background-owner processing and safely activate workers.
- Make deletion/export complete and file operations recoverable.
- Build provider-parity HTTP integration/restore matrices.
- Gradually reduce schema reconciler ownership in favour of tested migrations.
- Define AI consent/recipient/data-minimisation and operational RPO/RTO/monitoring.

## 20. Uncertainties and blocked checks

- Interactive browser client missing: no screenshots, console/network capture, manual responsive/keyboard/theme/slow-network/multi-tab checks.
- No MariaDB runtime/provider-parity or restore test.
- No production ingress, deployment, backup, logging, monitoring, key rotation or provider contract evidence.
- No real Google/Microsoft/Gmail/Graph/IMAP/SMTP/Stripe/Turnstile/translation/AI flow.
- No poisoned email, malicious parser file, DNS rebinding, DAST, aggressive fuzzing or load test.
- No Trivy/gitleaks/hadolint/container CVE scan or full licence analysis.
- Substantial-data analytics/performance not measured.

Anything above that was only code-inspected or blocked is labelled accordingly; it is not represented as tested behaviour.
