feat/Update_Controllers_to_Allow_for_Premium_Membership

This commit is contained in:
cesnimda
2026-08-03 09:17:28 +02:00
parent de937d25dc
commit c3f4a57195
187 changed files with 26062 additions and 991 deletions
+465
View File
@@ -0,0 +1,465 @@
# JobTracker full-application audit progress
Audit started: 2026-08-02
Overall status: Complete to all safe/local evidence boundaries; blocked checks are explicitly recorded.
Scope: repository-wide implementation, user-journey, security, privacy, supply-chain, reliability, testing, performance, and documentation audit. Application code and configuration are read-only for this audit.
## Phase 1 — Repository discovery
Status: Complete
### Work completed
- Captured initial Git status.
- Located and read the repository `AGENTS.md`.
- Began inventorying tracked source, documentation, configuration, generated output, archived material, vendor code, and auxiliary tools.
- Read current README, architecture, roadmap, TODO/blocker, environment, deployment, backup, release, package, container, CI, and AI-sidecar material.
- Traced executable entry points, authentication/authorization setup, EF ownership model, frontend routing, hosted workers, storage, integrations, and deployment topology.
- Compared documentation with the current source and classified ignored/generated/vendored paths.
- Searched production source for unfinished-code markers.
### Commands executed
- `git status --short --branch`
- `rg --files -g AGENTS.md -g '!**/node_modules/**' -g '!**/bin/**' -g '!**/obj/**'`
- `Get-ChildItem -Force | Select-Object Mode,Length,LastWriteTime,Name`
- `rg --files -g '!**/node_modules/**' -g '!**/bin/**' -g '!**/obj/**' -g '!**/.git/**' | Measure-Object | Select-Object -ExpandProperty Count`
- `Get-Content -Raw -LiteralPath AGENTS.md`
- Repository documentation, directory, CI-workflow, and tracked-file listings using `rg`, `Get-ChildItem`, and `git ls-files`.
- `git status --short --ignored | Select-Object -First 250`
- `rg -n -i ... '(TODO|FIXME|HACK|temporary|placeholder|\\bstub\\b|not implemented|NotImplementedException)' ...`
- Targeted line-numbered inspection of `Program.cs`, `JobTrackerContext.cs`, controllers, services, models, frontend routes/auth/API client, package manifests, Dockerfiles, Compose, nginx, CI, and the AI sidecar.
### Evidence collected
- Initial branch: `release-readiness` tracking `origin/release-readiness`.
- Pre-existing worktree changes: deleted `.agent.md`; untracked `AGENTS.md`.
- Initial top-level component and documentation listings.
- `docs/audits/evidence/repository-inventory.md`.
### Findings recorded
- Documentation drift identified; detailed finding IDs will be assigned after cross-phase validation.
### Checks that remain
- Validate build/test/tooling baseline and confirm whether documentation claims still hold.
### Blockers and limitations
- No Phase 1 blocker. Ignored local copies and generated output were excluded from handwritten-code review.
### Next phase
- Phase 2 — build and verification baseline.
## Phase 2 — Build and verification baseline
Status: Complete
### Work completed
- Classified planned commands as non-destructive; builds/tests may update ignored build output and local package caches only.
- Restored/validated declared dependencies without changing manifests or lockfiles.
- Built the .NET solution and frontend production export.
- Ran backend, frontend, AI-sidecar, and isolated Chromium suites.
- Ran TypeScript and formatting checks without rewriting source.
- Validated Compose, migration/model state, Dockerfiles, dependencies, and tracked-secret patterns.
### Commands executed
- Exact commands and results are recorded in `docs/audits/verification-log.md` (V-001 through V-027).
### Evidence collected
- Build/test outputs above plus the verification log.
- Current tracked tree and reachable-history secret-pattern scans with values suppressed.
### Findings recorded
- Standalone TypeScript check failure, formatting-baseline failure, npm advisories, Python advisory volume, tracked expired token artifact, and reproducibility gaps require cross-phase validation and finding IDs.
### Checks that remain
- Container image CVE scanning was unavailable locally.
- Advisory applicability and severity need source-path review.
### Blockers and limitations
- `gitleaks`, `trivy`, and `hadolint` unavailable.
- Production/remote CI status is outside this local audit; no production system was contacted.
### Next phase
- Phase 3 — architecture, backend, frontend, and data review.
## Phase 3 — Architecture and code-quality audit
Status: Complete
### Work completed
- Traced controller/service/data paths for jobs, career profiles, CVs, application workspaces, correspondence, attachments, AI, identity, rules, exports, and backups.
- Reviewed frontend routing, API-client use, state/error/empty flows, forms, persistence, rendering, responsiveness, and accessibility affordances.
- Executed default-SQLite paths identified as risky by source inspection.
### Commands executed
- Targeted `rg`, `Get-Content`, EF model/migration inspection, and disposable endpoint/worker checks V-031 through V-033.
### Evidence collected
- `evidence/runtime-evidence.md`, `evidence/two-user-isolation.md`, and line-numbered source locations used in the main report.
### Findings recorded
- Confirmed default-SQLite API failures, ambiguous routes, inert tenant-scoped workers, non-atomic attachment/file operations, and client-only notification preferences.
### Checks that remain
- Manual browser-dependent UX/accessibility checks remain blocked.
### Blockers and limitations
- No MariaDB server was available, so provider parity beyond source/migration inspection is unverified.
### Next phase
- Phase 4 — hands-on user journeys.
## Phase 4 — Hands-on user journey audit
Status: Complete to the available evidence boundary
### Work completed
- Ran isolated Chromium coverage for login, manual saved-job creation, Career Workspace shell, and anonymous public-CV/PDF.
- Used two synthetic accounts for empty-account, ownership, job, correspondence, CV, workspace, attachment, settings, and admin API checks.
- Classified every discovered workflow in `user-journey-audit.md`.
### Commands executed
- V-026 and V-030 through V-040 in `verification-log.md`.
### Evidence collected
- `evidence/browser-evidence.md`, `evidence/runtime-evidence.md`, and `evidence/two-user-isolation.md`.
### Findings recorded
- Core Career/Application Workspace failures and accessibility/browser-regression gaps.
### Checks that remain
- Manual viewport, keyboard, console/network, slow-network, multi-tab, and failure-injection journeys.
### Blockers and limitations
- Mandatory in-app browser client missing; no permitted fallback and no screenshots.
- Real email/OAuth/AI/billing services intentionally not contacted.
### Next phase
- Phase 5 — threat model and security audit.
## Phase 5 — Threat model and security audit
Status: Complete
### Work completed
- Modelled assets, roles, entry points, trust boundaries, flows, attacker capabilities, abuse cases, mitigations, and high-risk paths.
- Reviewed authentication, authorization/IDOR, sessions, OAuth/OIDC, CSRF/XSS/SSRF, uploads, CORS/headers, secrets, rate limiting, containers, and AI boundaries.
- Performed two-user direct-ID and live logout/verification lifecycle checks.
### Commands executed
- V-031 and V-035 through V-037, secret scans, source searches, and official Microsoft identity-documentation lookup.
### Evidence collected
- `security-threat-model.md`, two-user matrix, runtime evidence, and filenames-only secret evidence.
### Findings recorded
- Microsoft identity binding, host-derived recovery links, verification/session gaps, parser advisories, and lower-severity SSRF/rendering hardening.
### Checks that remain
- External-provider exploit reproduction was not safe/in scope; prerequisites remain explicit.
### Blockers and limitations
- No aggressive testing, production contact, real provider tokens, or real email.
### Next phase
- Phase 6 — technical privacy assessment.
## Phase 6 — Technical privacy assessment
Status: Complete
### Work completed
- Traced identity, profile/CV, job, correspondence, attachment, provider token, AI, document, log, backup, export, and deletion lifecycles.
- Separated technical controls from legal-policy questions.
### Commands executed
- Targeted owner/entity/file/export/delete/provider/AI source inspection.
### Evidence collected
- Privacy sections in the main report and threat model.
### Findings recorded
- Incomplete admin deletion/export and missing per-user global AI control/provider-recipient explanation.
### Checks that remain
- Production retention, logs, backups, processor contracts, and legal basis require operator/legal evidence.
### Blockers and limitations
- Technical assessment only; production/provider contracts not accessed.
### Next phase
- Phase 7 — dependencies and supply chain.
## Phase 7 — Dependencies and supply chain
Status: Complete
### Work completed
- Audited advisories, deprecations, version drift, locks, Docker bases, CI actions, remote installers, and licence/SBOM controls.
- Re-read advisory descriptions against actual upload/model paths.
### Commands executed
- V-020 through V-025, V-028/V-029, V-041/V-042, and Dockerfile/CI inspection.
### Evidence collected
- `evidence/dependency-evidence.md`.
### Findings recorded
- Reachable document-parser denial of service, moderate React Router advisories, and reproducibility/provenance gaps.
### Checks that remain
- Container package CVEs and full licence compatibility need dedicated scanners/legal review.
### Blockers and limitations
- Trivy/gitleaks/hadolint unavailable; no dependency upgraded.
### Next phase
- Phase 8 — reliability, deployment, and recovery.
## Phase 8 — Reliability, deployment, and recovery
Status: Complete
### Work completed
- Reviewed Compose/Dockerfiles, health/startup, shutdown, resources, migrations/reconciliation, deploy/rollback, logging, metrics, workers, partial failure, and backups.
- Rehearsed SQLite database and full-data-root restoration with disposable data.
### Commands executed
- V-016/V-017/V-023 through V-025/V-033/V-034 plus deployment-source inspection.
### Evidence collected
- Restore results in `evidence/runtime-evidence.md`.
### Findings recorded
- SQLite restore passes; files/keys/config are separate; MariaDB needs external backup; no RPO/RTO or routine restore proof; workers fail silently; startup reconciler is risky complexity.
### Checks that remain
- MariaDB restore, production rollback, restart/resource pressure, and monitoring delivery.
### Blockers and limitations
- No production deployment, registry, remote host, or MariaDB instance used.
### Next phase
- Phase 9 — testing assessment.
## Phase 9 — Testing assessment
Status: Complete
### Work completed
- Mapped backend, frontend, Python, and browser tests to core journeys and observed defects.
- Reviewed determinism, isolation, authorization, failure paths, accessibility, and CI gates.
### Commands executed
- V-010/V-012/V-014/V-026 plus test-file and CI-workflow inventories.
### Evidence collected
- Test mapping in the main and journey reports.
### Findings recorded
- Missing route-table, default-SQLite HTTP, worker-context, account-lifecycle, accessibility, and Python CI gates.
### Checks that remain
- Remote CI execution status was not queried.
### Blockers and limitations
- Raw line coverage was not used as proof of quality.
### Next phase
- Phase 10 — performance assessment.
## Phase 10 — Performance assessment
Status: Complete to safe-local scope
### Work completed
- Measured warm local API latency and aggregate export size; inspected pagination, query patterns, upload buffering, worker sequencing, and admin N+1 behaviour.
- Separated measured results, clear inefficiencies, measurement-needed risks, and optional optimisation.
### Commands executed
- V-038/V-039 and query-loop/pagination inspection.
### Evidence collected
- `evidence/runtime-evidence.md` performance table.
### Findings recorded
- Small-data timings healthy; pre-limit buffering clearly inefficient; larger-data/browser capacity unverified.
### Checks that remain
- Production-like transfer, memory, query counts, AI latency, email throughput, and large datasets.
### Blockers and limitations
- No load test; browser performance tooling blocked.
### Next phase
- Phase 11 — documentation and developer experience.
## Phase 11 — Documentation and developer experience
Status: Complete
### Work completed
- Compared feature/setup/architecture/migration/test/deploy/API claims with source and runtime; evaluated clean onboarding.
### Commands executed
- Phase 1 documentation inventory plus toolchain/build/runtime verification.
### Evidence collected
- Repository inventory and main-report documentation section.
### Findings recorded
- Obsolete CRA README, unsupported PostgreSQL advice, stale API architecture, no `global.json`, incomplete environment reference, and stale CI comments.
### Checks that remain
- Operator-only documentation may exist outside the repository.
### Blockers and limitations
- External documentation not accessed.
### Next phase
- Phase 12 — sceptical validation.
## Phase 12 — Sceptical validation
Status: Complete
### Work completed
- Re-read every Critical/High candidate end to end, searched mitigations, repeated safe reproductions, checked prerequisites, and merged/downgraded overlap.
- Separated parser reachability from fixed-model loader advisories.
- Confirmed no cross-user disclosure in meaningful two-user results.
### Commands executed
- V-031 through V-043, official Microsoft identity guidance lookup, source rereads, and final cleanup/status checks.
### Evidence collected
- All deliverables and evidence under `docs/audits/`.
### Findings recorded
- No Critical finding. High findings retain explicit prerequisites; unperformed external exploits remain labelled unverified.
### Checks that remain
- Only blocked/production/external checks listed in the reports.
### Blockers and limitations
- Missing browser client, no MariaDB, no container CVE scanner, no production/provider access.
### Next phase
- Stop after delivery and await remediation approval.
## Post-audit programme execution — POL-002
Status: Implemented; browser/production verification incomplete (2026-08-03).
### Work completed
- Revalidated AI privacy/provider paths and implemented server-persisted AI opt-out plus explicit external-processing consent.
- Added independent backend/sidecar administrator gates and a deny-by-default permission header at the shared `/cv/*` boundary.
- Added Settings UI, additive migration, architecture/verification documentation and synthetic-only tests.
### Commands and evidence
- Verification-log entries V-089 through V-095.
- `docs/verification/pol-002-ai-privacy.md` and `docs/audits/evidence/pol-002/README.md`.
### Findings / limitations
- No production, paid provider, real private data or browser was used.
- Historical EF-only clean SQLite migration remains blocked before the new migration; the new SQL/defaults and model snapshot pass inspection.
- AI-001/002 must add durable policy snapshots, actual provider/reason recording and bounded local-first fallback before rollout.
### Next phase
- AI-001 durable AI queue/worker admission, reusing OPS-001A/B/C.
## Post-audit programme execution — AI-001
Status: Implemented; real-handler/browser/production verification incomplete (2026-08-03).
- **Work completed:** shared Pro/privacy admission, bounded capacity/priority, typed default-off worker, owner/policy recheck, heartbeat/timeout/retry/cancellation integration and configuration.
- **Commands/evidence:** verification-log V-096/V-097; `docs/verification/ai-001-durable-ai-queue.md`.
- **Findings:** no new schema/raw private queue payload; multi-replica capacity needs a future database reservation; actual 202 producers and provider controls remain AI-003/004 and AI-002.
- **Blockers:** browser, MariaDB and production unavailable; worker intentionally off.
- **Next phase:** AI-002 Ollama adapter and central local-first routing.
+653
View File
@@ -0,0 +1,653 @@
# JobTracker audit remediation backlog
Prepared: 2026-08-02
No remediation has been implemented. Ordering follows exploitable security, cross-user exposure, data integrity, broken core workflows, production reliability, regression protection, accessibility/usability, maintainability, then optional hardening.
No cross-user exposure was confirmed, so the backlog does not invent one. Every item references validated findings and keeps unrelated work deferred.
## Phase 0 — validated implementation design
This design revalidates the Phase 0 findings against their complete call paths, current settings, Docker/deployment behavior and existing tests. It is a design only: no application code, dependency, configuration, schema or migration has been changed.
### Revalidation record
- Worktree before design: branch `release-readiness`; existing `D .agent.md`, `?? AGENTS.md` and `?? docs/audits/` were preserved.
- End-of-design status also showed unrelated `?? docs/todo/`; it was not modified. No existing work was discarded, overwritten or committed.
- All files under `docs/audits/`, including every evidence file, were read before revalidation.
- Targeted .NET baseline: `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj -c Release --no-restore --filter "FullyQualifiedName~MicrosoftTokenValidatorTests|FullyQualifiedName~AuthAndSystemControllerTests|FullyQualifiedName~SessionsControllerTests|FullyQualifiedName~AttachmentsControllerTests|FullyQualifiedName~AttachmentFlagsRecomputeTests|FullyQualifiedName~ProfileCvControllerTests|FullyQualifiedName~ProductionConfigTests" --logger "console;verbosity=minimal"` — **73 passed, 0 failed**. Some assertions intentionally describe the current unsafe behavior; passing is not remediation evidence.
- Python parser baseline: `python -m pytest tools/summarizer/tests -q` — **17 passed, 0 failed**, with five SWIG deprecation warnings.
- The production Compose merge was inspected without expanding secret values. Because `deploy/deploy.sh` invokes plain `docker compose`, `docker-compose.override.yml` is auto-loaded: the merged configuration publishes backend `5202:8080` and frontend `3000:80`, and sets backend proxy trust to `false`. This materially strengthens the JT-002 deployment concern.
- No malicious file was parsed. Advisory presence and parser reachability are confirmed; exploitation is not.
### Ordering decision and dependency graph
The recommended sequence requires one correction. JT-002 must precede legacy Microsoft relinking and pending-email/recovery links: those flows cannot safely prove ownership while security links can still inherit an attacker-controlled Host. Session-revocation primitives should also land before email-change and recovery transitions use them.
| Execution order | Work package | Findings | Reason |
|---|---|---|---|
| 1 | **P0-2A** Canonical application origin and Host guard | JT-002 | Establishes the trusted URL boundary needed by identity recovery. |
| 2 | **P0-2B** Production ingress, proxy and Compose alignment | JT-002 | Removes auto-loaded development exposure and defines the Traefik/nginx trust chain. |
| 3 | **P0-1A** Microsoft issuer/tenant validation | JT-001 | Stops accepting identities without a tenant-qualified key; no database change. |
| 4 | **P0-1B** Canonical Microsoft links and legacy relinking | JT-001 | Additive migration plus a non-merging transition after safe links exist. |
| 5 | **P0-4A** Session invalidation primitives and recovery use | JT-008 | Shared concrete revocation behavior first, without a speculative abstraction. |
| 6 | **P0-4B** Registration and pending-email state machine | JT-007/JT-008 | Uses canonical links and the revocation primitive. |
| 7 | **P0-3A** Parser dependency compatibility update | JT-006 | Isolates dependency compatibility from behavioral hardening. |
| 8 | **P0-3B** Bounded parser subprocess and safe fallback | JT-006/JT-011 | Adds input/CPU/memory/time limits and removes the unsafe backend fallback. |
| 9 | **P0-3C** Container hardening and abandoned-work cleanup | JT-006 | Deployable separately after measured resource sizing. |
| 10 | **P0-5** Recoverable attachment mutations | JT-010 | Establishes file/row invariants reused by account deletion. |
| 11 | **P0-6A** Owner-scoped export inventory and readable export | JT-009 | Makes ownership explicit before deletion uses it. |
| 12 | **P0-6B** Idempotent deletion lifecycle and backup tombstones | JT-009 | Last because it depends on identity, attachment and ownership invariants. |
P0-3 can proceed in parallel with P0-1/P0-4 after P0-2; it has no identity dependency. P0-5 can also proceed after its storage journal format is coordinated with P0-6. P0-6B must not precede P0-5 or P0-6A.
### JT-001 — Microsoft tenant/issuer validation and safe identity linking
#### Revalidated finding
- **Final severity/confidence/classification:** **High / High / likely defect**. The acceptance and auto-linking paths are confirmed. A production account takeover was not attempted, and exploitability still requires a token for the configured client plus a colliding trusted email or legacy identifier.
- **Confirmed execution path:** `job-tracker-ui/src/components/MicrosoftAuthCard.tsx:27-30,74-83` uses MSAL authority `common`, obtains an ID token and posts it to `/auth/microsoft/exchange` or `/auth/microsoft/link`. `JobTrackerApi/Services/MicrosoftTokenValidator.cs:26,57,72-90,97-99` loads `common` discovery, validates signature/audience/lifetime with `ValidateIssuer = false`, performs only a login.microsoftonline.com issuer-shape check, selects `oid` then `sub`, and treats `email` or `preferred_username` as a verified email. `JobTrackerApi/Controllers/AuthController.cs:285-344` finds an account by `MicrosoftSubject` **or `MicrosoftEmail`,** then auto-links an existing local user returned by `FindByEmailAsync`; `:515-583` applies the same subject-or-email collision rule during explicit linking. `JobTrackerApi/Models/ApplicationUser.cs:19-21` stores only `MicrosoftSubject`, `MicrosoftEmail` and link time. `JobTrackerApi/Program.cs:371-381` exposes a second raw Microsoft bearer scheme using `common` and disabled issuer validation even though the UI exchanges ID tokens for local sessions.
- **Existing mitigations:** Microsoft token signature, audience and lifetime are validated; issuer hostname/path shape is checked; local registration can be disabled; duplicate app emails are constrained by Identity normalization; explicit linking requires a local authenticated session. These do not bind the external identity to a tenant or make mutable email a safe account key.
- **Existing tests:** validator tests cover accepted issuer shape and an unrelated issuer; controller tests cover new-user exchange and disabled registration. They do not cover `tid`, issuer/tenant mismatch, same `oid` across tenants, email collisions, or a safe legacy transition.
#### Stable identity and configuration contract
- The relationship is owned by the verified pair **(`tid`, `oid`)**, each parsed and stored as a normalized GUID string. `oid` alone is tenant-scoped; `sub` and email/`preferred_username` must never own or merge the relationship.
- The exact expected issuer is `https://login.microsoftonline.com/{tid}/v2.0`; the signed `tid` must equal the issuer path tenant and satisfy the configured account mode.
- Add one sign-in setting, `Auth:MicrosoftTenant`, distinct from `Microsoft:TenantId` used for Graph mailbox OAuth:
- GUID: single-tenant, exact `tid` only.
- `organizations`: Entra organizational tenants; reject personal Microsoft accounts.
- `consumers`: personal Microsoft accounts only.
- `common`: explicit organizational plus personal multitenant support.
- Production with Microsoft sign-in enabled must specify this setting. Development/Test may explicitly use `common`; any backward-compatible default is Development/Test-only and emits a warning.
- The validator requires GUID-shaped `tid` and `oid`, exact issuer/tenant agreement, configured audience, signature and lifetime. It returns tenant ID, object ID and display/email claims as metadata. It does **not** assert that an email claim proves mailbox ownership.
- Remove the unused raw Microsoft bearer branch from the smart authentication policy instead of maintaining two trust decisions. If an undiscovered API consumer needs it, it must be documented and validated by the same tenant policy before removal is reconsidered.
#### Existing-link transition and migration
- Add nullable `MicrosoftTenantId` and `MicrosoftObjectId` columns to `AspNetUsers`, bounded to 36 characters, with a filtered/nullable unique composite index. Keep `MicrosoftSubject` and `MicrosoftEmail` temporarily as legacy evidence and rollback metadata; stop writing `MicrosoftSubject` after cutover. Do not index or reinterpret it because existing values may be `oid` or `sub` and the current provider cannot prove their tenant retrospectively.
- Before deployment, produce counts only: total legacy links, duplicate legacy subjects/emails, and whether each affected account has an alternate password or Google credential. Do not print claim or email values.
- Do **not** backfill tenant IDs from email, `common`, Graph mailbox settings or the next token seen. Unknown tenant means unknown ownership.
- A currently authenticated local user may explicitly relink after fresh Microsoft authentication. The canonical pair must be unused; email similarity is informational only.
- A social-only legacy user needs a one-time recovery ceremony: a valid tenant-qualified Microsoft token **and** a purpose-bound proof sent to the already stored, confirmed application email. The recovery token binds application user ID plus proposed `tid`/`oid` and expires. Multiple candidates, an unconfirmed/unavailable email, or any collision moves to operator-assisted identity verification; the system never silently merges accounts.
- New Microsoft registration binds (`tid`, `oid`) immediately. The provider email may prefill the app email but remains `EmailConfirmed = false`; when app email verification is required, no local session is issued until it is verified. If verification is disabled, the Microsoft identity may establish the session, but recovery/notification behavior still treats the mailbox as unverified.
- Single-tenant deployments reject all other `tid` values. Multitenant deployments retain one local account per canonical pair; two tenants presenting the same email remain separate unless an already authenticated user explicitly links and proves both sides.
#### Implementation contract
- **Proposed design:** extend the existing validator and controller directly; add no one-implementation identity-provider framework. Centralize one tenant policy/value parser used by exchange and explicit link. Change lookup/conflict checks to the canonical composite key only. Require recent local reauthentication for link/unlink, and refuse unlink if it would remove the last usable sign-in credential. Treat the legacy relink flow as a temporary, separately gated endpoint that can be removed after the migration window.
- **Affected files/components:** `JobTrackerApi/Services/MicrosoftTokenValidator.cs`, `JobTrackerApi/Controllers/AuthController.cs`, `JobTrackerApi/Program.cs`, `JobTrackerApi/Models/ApplicationUser.cs`, `JobTrackerApi/Data/ApplicationDbContext.cs`, a new EF migration and snapshot update, auth DTOs, `job-tracker-ui/src/components/MicrosoftAuthCard.tsx`, login/link UI, `.env.example`, `appsettings*.json`, deployment environment validation, and Microsoft/auth tests.
- **Database/configuration migration:** additive nullable columns and unique index first; no destructive backfill. Add the required production tenant-mode setting. Keep legacy columns through at least one verified release and the relinking window. Do not add these columns to the startup schema reconciler as a second schema owner.
- **Backward-compatibility risks:** legacy Microsoft-only users cannot be transparently mapped; stricter tenant modes may reject accounts previously accepted through `common`; email-collision users may see a new-account/recovery choice; removing raw bearer support can affect undocumented clients. Inventory and a temporary relink window reduce lockout without accepting unsafe fallback behavior.
- **Rollback:** disable Microsoft exchange/link endpoints with an existing-style kill switch; leave local/password/Google login available. Roll back application binaries while additive columns and legacy fields remain. Never roll back to email auto-linking. If a canonical link has been created, do not delete or reassign it during rollback.
- **Deployment sequence:** (1) finish P0-2A; (2) inventory legacy rows and alternate credentials; (3) deploy tenant validation and raw-bearer removal; (4) apply the additive migration; (5) deploy canonical lookups plus relink UI disabled; (6) enable relinking for affected users, monitor collision/lockout counters without PII; (7) close the transition endpoint after the documented period; (8) remove legacy columns only in a later approved migration.
- **Documentation changes:** supported account-mode matrix, separate sign-in versus Graph tenant settings, operator relink/recovery procedure, collision behavior, raw-bearer removal, and user-facing explanation that Microsoft email does not automatically merge an account.
- **Dependencies:** P0-2A for safe recovery URLs; P0-4A/B for recent reauthentication, email proof and revocation. The validator-only P0-1A can land before P0-4, while migration/relink P0-1B cannot be considered complete before it.
#### Required tests and acceptance criteria
- Unit-test each mode with valid and invalid `tid`; missing/non-GUID `tid`/`oid`; exact issuer mismatch; wrong audience/signature/lifetime; personal versus organizational tenant; and the same `oid` in two tenants.
- Integration-test new account, existing canonical link, canonical-pair collision, same email across tenants, explicit link/unlink with recent/expired reauthentication, 2FA account, last-credential guard and disabled registration.
- Migration-test fresh SQLite/MariaDB and representative legacy rows with null/duplicate/ambiguous legacy values. Assert no legacy row is silently assigned or merged.
- End-to-end test a new Microsoft account and a synthetic legacy relink with mocked tokens and email. No real Microsoft account or email is used.
- **Acceptance:** every accepted token has a verified configured tenant and exact issuer; account lookup is solely (`tid`, `oid`); email cannot auto-link; ambiguous legacy rows remain unlinked and recoverable; no unrelated identities merge; single/multitenant behavior matches the documented matrix; and Microsoft-only users have a tested non-silent recovery path.
### JT-002 — canonical external origin and Host-header handling
#### Revalidated finding
- **Final severity/confidence/classification:** **High / High / likely defect**. Code-level Host poisoning is confirmed. Production exploitability is not claimed as confirmed because the external Traefik/firewall configuration is absent, but repo-defined deployment exposes direct backend/frontend ports and makes the prerequisites realistic.
- **Confirmed execution path:** `JobTrackerApi/Controllers/AuthController.cs:695-701,806-812` and `JobTrackerApi/Controllers/UsersController.cs:149-155` build verification/reset/admin-reset links from `App:PublicBaseUrl` and fall back to `Request.Scheme`/`Request.Host`. `GmailController.cs:1013-1023` and `MicrosoftGraphController.cs:112-122` do the same for OAuth callbacks; `BillingController.cs:223-230` requires the base URL, while `FollowUpReminderHostedService.cs:48` accepts older aliases. `JobTrackerApi/appsettings.json` has `AllowedHosts: "*"`. nginx accepts `server_name _`, forwards `$host`, and overwrites forwarded proto with its internal `$scheme`. `JobTrackerApi/Program.cs:454-464` enables one-hop forwarded processing only when configured and clears known proxies/networks. `docker-compose.yml:18,52` enables proxy trust and passes the possibly blank origin, but the auto-loaded `docker-compose.override.yml:5` disables proxy trust and publishes `5202`/`3000`; `deploy/deploy.sh` invokes plain Compose. Repository evidence contains no Traefik router/host allowlist.
- **Existing mitigations:** `APP_PUBLIC_BASE_URL` can provide a safe origin; token links expire and still require victim action; production can be protected by an external exact-host router/firewall; forwarded-header limit is one. These mitigations are optional, undocumented as a hard contract, or contradicted by the merged Compose deployment.
- **Code exposure versus exploitability:** any request that reaches a link-generating endpoint with blank `PublicBaseUrl` can influence the generated origin. Exploitation requires reachability using an untrusted Host, email delivery, and a recipient following the link. A correctly configured unobserved Traefik/firewall could prevent that, so the path is a confirmed code defect and a high-confidence deployment risk rather than a reproduced production compromise.
#### Canonical origin and proxy design
- Reuse `App:PublicBaseUrl` as the **only** external-origin setting. At startup parse it once into a concrete immutable value; do not create an interface/factory for one value.
- Production requires an absolute HTTPS URL with no userinfo, query, fragment or non-root path. Normalize scheme, ASCII host and explicit non-default port once. Development/Test may use an explicit `http://localhost:3000`; no environment may fall back to request headers for security links.
- All verification/reset/admin reset URLs, OAuth callbacks, billing redirects, reminder links and absolute frontend links use the parsed value. Cookie `Secure` behavior in Production follows the canonical HTTPS origin, not an untrusted header.
- Derive the application Host allowlist from that canonical host rather than introducing a second public-host setting. In Production reject unknown API Host values with 400/421 before authentication/routing. Permit only narrowly documented internal health names (`backend`, `localhost`, `127.0.0.1`) for internal health probes; they must never be used to generate links.
- The production Compose command must name only `docker-compose.yml`; move/rename the auto-loaded override to an explicitly selected development file. Production must not publish backend or frontend host ports. Traefik reaches only the frontend on `shared_services`; nginx reaches backend on the private application network.
- Traefik's operator contract is an exact canonical `Host()` rule, TLS termination, no direct published application ports, and replacement of client forwarding headers. nginx must pass the sanitized external Host/proto rather than replace proto with its internal HTTP scheme. Backend forwarded-header processing must trust explicit proxy IP/network configuration; never clear both trusted proxy collections. Document the actual two-hop Traefik → nginx → backend chain and its forwarding limit.
#### Implementation contract
- **Proposed design:** P0-2A adds startup validation, one shared origin value, caller replacement and application Host filtering. P0-2B changes Compose selection/exposure, nginx forwarding and explicit proxy trust. This fixes the root origin source once and removes request-specific guards.
- **Affected files/components:** `Program.cs`; auth, admin-user, Gmail, Microsoft Graph and billing controllers; follow-up hosted service; URL/config helpers; `appsettings*.json`; `.env.example`; `docker-compose.yml`, development override naming, `deploy/deploy.sh`, `deploy/README.md`; nginx configuration; production/config/controller tests.
- **Database/configuration migration:** no database migration. `APP_PUBLIC_BASE_URL` becomes required in Production. Add explicit trusted proxy/network values if forwarded headers are enabled. Any Traefik labels/config live in the operator deployment and must be supplied before production exploitability can be closed.
- **Backward-compatibility risks:** environments relying on blank base URL will fail fast; noncanonical aliases or direct port access will be rejected; incorrect proxy IP/network values can cause wrong client IP/scheme; OAuth registered redirect URIs must exactly match the canonical callbacks. Local/Test defaults must remain explicit and isolated.
- **Rollback:** restore the last known valid canonical URL/proxy configuration and prior image if necessary, but do not restore request-host fallback or wildcard production Host acceptance. Keep the backend unexposed during rollback. A config validation failure should stop deployment before traffic shifts.
- **Deployment sequence:** (1) inventory the production URL, OAuth redirect URIs, proxy network/IP and firewall; (2) deploy P0-2A with canonical setting supplied and test canonical/hostile hosts; (3) update provider redirect registrations if necessary; (4) deploy P0-2B using explicit production Compose files; (5) verify no bound `3000/5202` ports, exact Traefik routing and sanitized headers; (6) run email/OAuth smoke tests with mocks/non-sending sinks; (7) monitor rejected Host and forwarded-header warnings without logging tokens.
- **Documentation changes:** make the origin and exact ingress contract authoritative; remove conflicting one-hop claims; add local/dev Compose commands, production Compose command, required variables, host/proxy smoke checks and OAuth callback examples.
- **Dependencies:** P0-2A is a prerequisite for JT-001 relinking and JT-007 email change/recovery. P0-2B is operationally coupled but has no database dependency.
#### Required tests and acceptance criteria
- Unit/config tests for missing/HTTP/malformed/userinfo/query/fragment/path production URLs, IDN/Unicode host normalization, ports, and local/Test HTTP behavior.
- Controller tests send hostile `Host`, `X-Forwarded-Host` and `X-Forwarded-Proto` values and assert every emitted absolute URL remains canonical.
- Host-filter tests assert canonical API requests pass, unknown hosts fail, internal health probes work without becoming link origins, and direct backend Host spoofing is rejected.
- Deployment tests inspect merged production and development Compose output without revealing environment values; Production has no bound application ports and does not auto-load the development override.
- Proxy integration tests cover the two-hop TLS request, exact forwarded proto/host, explicit known proxy, unknown proxy header rejection and secure cookies.
- **Acceptance:** Production fails before serving when canonical origin/proxy trust is invalid; request/forwarded hosts never influence outbound URLs; unknown production hosts are rejected at ingress and application; direct application ports are closed; OAuth/email URLs match registrations; local development and test startup remain documented and green.
### JT-006 — document-parser upgrades and resource isolation
#### Revalidated finding and package inventory
- **Final severity/confidence/classification:** **High / High / likely defect**. Vulnerable versions and reachable untrusted parser paths are confirmed. Successful exploitation, denial of service or code execution is **not** confirmed and was not attempted.
- Manifest pins: FastAPI 0.115.12, Uvicorn 0.34.0, Transformers 4.48.3, cachetools 5.5.2, Pydantic 2.10.6, Torch 2.6.0, Pillow 11.1.0, pytesseract 0.3.13, pypdf 5.4.0, PyMuPDF 1.25.5, python-docx 1.1.2 and python-multipart 0.0.20. Audited transitive Starlette is 0.46.2. The current shared workstation has Pydantic 2.13.4, demonstrating environment drift; the deployment manifest remains authoritative.
- Reachable advisory snapshot from the audit evidence:
| Package | Installed | Advisory/CVE identifiers | Highest audited fixed floor |
|---|---:|---|---:|
| Pillow | 11.1.0 | PYSEC-2026-165, PYSEC-2026-2250, PYSEC-2026-2253, PYSEC-2026-2255, PYSEC-2026-2257, PYSEC-2026-2256, PYSEC-2026-2254, PYSEC-2026-2252, PYSEC-2026-2249, PYSEC-2026-2874, PYSEC-2026-3453, PYSEC-2026-3451, PYSEC-2026-3454, PYSEC-2026-3495, PYSEC-2026-3496, PYSEC-2026-3494, PYSEC-2026-3493 | 12.3.0 |
| pypdf | 5.4.0 | PYSEC-2026-1833, PYSEC-2026-1829, PYSEC-2026-1832, PYSEC-2026-1830, PYSEC-2026-1831, PYSEC-2026-1827, PYSEC-2026-1828, PYSEC-2026-3023, PYSEC-2026-3022, PYSEC-2026-3017, PYSEC-2026-3019, PYSEC-2026-3020, PYSEC-2026-3018, PYSEC-2026-3021, PYSEC-2026-3011, PYSEC-2026-3007, PYSEC-2026-3004, PYSEC-2026-3005, PYSEC-2026-3006, PYSEC-2026-3014, PYSEC-2026-3024, PYSEC-2026-3026, PYSEC-2026-3016, PYSEC-2026-3010, PYSEC-2026-3015, PYSEC-2026-3025, PYSEC-2026-3013, PYSEC-2026-3009, PYSEC-2026-3012, PYSEC-2026-3027, GHSA-jm82-fx9c-mx94, CVE-2026-59938, CVE-2026-59937, CVE-2026-59935, CVE-2026-59936 | 6.14.2 |
| python-multipart | 0.0.20 | PYSEC-2026-1852, PYSEC-2026-3038, PYSEC-2026-3037, PYSEC-2026-3036, PYSEC-2026-3040, PYSEC-2026-3039 | 0.0.31 |
| Starlette | 0.46.2 | PYSEC-2026-161, PYSEC-2026-248, PYSEC-2026-249, PYSEC-2026-1942, PYSEC-2026-1941, PYSEC-2026-2281, PYSEC-2026-2280 | 1.3.1 |
Fixed floors are advisory-clearing candidates, not a compatibility approval. FastAPI 0.115.12 constrains Starlette to `<0.47`; therefore Starlette 1.3.1 cannot be installed with the current FastAPI pin. The exact compatible fixed FastAPI/Starlette pair remains **unverified** until approved package-index/advisory resolution is performed. Do not guess it. Model-stack advisories are tracked under JT-017 and are not used to inflate JT-006 unless a reachable model-loading path is shown.
#### Confirmed parser path and mitigations
- `JobTrackerApi/Controllers/ProfileCvController.cs:138-194` accepts an authenticated CV, enforces an extension and 5 MiB application limit, writes an artifact/run row and synchronously calls extraction. `ProfileCvController.Pipeline.cs:176-232` stores/dispatches it, and `JobTrackerApi/Services/SummarizerService.cs:342-390` sends multipart to `/extract-text` with a 30-second HTTP timeout.
- Starlette/python-multipart accepts the upload; `tools/summarizer/app.py:868-899` then performs `await file.read()` before its 8 MiB check. `:832-858` uses pypdf text extraction and, when text is sparse, renders every page at 2x through PyMuPDF/Pillow/Tesseract. Images use Pillow/Tesseract; DOCX uses python-docx. There are no page, dimension, decoded-pixel, decompression or per-stage CPU/memory limits.
- If the sidecar fails, `JobTrackerApi/Controllers/ProfileCvController.Parsing.cs:1276-1310` reads the whole bounded file; DOCX opens ZIP `word/document.xml` without entry/decompression/ratio limits. Isolating only Python would leave this parser path reachable.
- The in-memory CV queue is unbounded. Database runs let startup discover interrupted work, but parser temporary files/processes are not durably tracked. Raw exception messages can be stored/emailed.
- **Existing mitigations:** authentication/ownership, backend 5 MiB request/file limit, extension allowlist, 30-second client timeout, service token, private `ai_internal` network, no sidecar host port, artifact/run status and retention pruning. These reduce reachability but do not bound decoded work or terminate parser descendants. The Python container currently runs as root, uses mutable `python:3.11`, and lacks read-only/non-root/PID/CPU/memory controls.
#### Bounded-processing design
- P0-3A resolves and pins a compatible fixed dependency set. Initial candidate floors are pypdf 6.14.2, Pillow 12.3.0 and python-multipart 0.0.31; choose FastAPI/Starlette together only after resolver and benign-corpus verification. Produce a lock/hash artifact and record any accepted exception. No package is changed in this design phase.
- Validate content with standard-library signature/container checks before parser dispatch: `%PDF-`; PNG/JPEG/WebP signatures; DOCX ZIP containing `[Content_Types].xml` and `word/document.xml`; bounded text encoding/no-NUL rules. Extension and detected type must agree.
- Use one 5 MiB file-content cap at backend and sidecar, a 6 MiB HTTP/multipart cap for framing overhead, and bounded chunked reads into a private spool file; never `await file.read()` an untrusted upload. Align nginx `client_max_body_size` without making it the only enforcement.
- Initial configurable ceilings, selected to be testable rather than unlimited: 40 PDF pages; 200,000 extracted characters; 256 DOCX ZIP entries; 32 MiB total declared uncompressed ZIP content; 8 MiB largest entry/`document.xml`; 100:1 compression ratio; 12,000 pixels per image dimension; 40 megapixels per image; single-frame raster images; 160 MiB maximum decoded image bytes; 12 megapixels rendered per PDF page and 120 megapixels total OCR work.
- Run each parse in a child process with a minimal environment and no AI-provider keys. On Linux use `resource.setrlimit` for CPU (20 seconds), address space (initial 512 MiB for the parser child), output/file size and open descriptors. Apply a 25-second parent deadline, start a new process group and kill the group on timeout so Tesseract descendants do not survive. Limit parent concurrency to one parse initially; measure before increasing it.
- The 512 MiB child limit does not become a guessed whole-sidecar limit: the parent also hosts ML models. Measure its healthy resident set and assign a container memory ceiling with explicit headroom in P0-3C. Use a private `0700` temp directory, `0600` UUID spool files, `finally` deletion and startup cleanup of files older than one hour; mount temp storage as size-bounded tmpfs.
- Remove nontrivial backend PDF/DOCX/image fallback. A bounded plain-text fallback may remain. If the isolated parser is unavailable, fail safely with a stable 422/503 code; do not re-enter an unisolated parser.
- Bound the in-memory channel or return backpressure. Mark interrupted/expired runs failed or retryable with stable error codes. Never store/email raw parser exception text or paths. Reconciliation removes stale temp work and applies the existing artifact retention policy.
- P0-3C runs the sidecar as non-root with read-only root filesystem, `cap_drop: ALL`, `no-new-privileges`, tmpfs, PID/CPU/memory limits and the existing private network/service token. Child-process isolation in one container is the minimum design; a separate parser service is deferred unless measurement or platform limitations show that this boundary is insufficient.
#### Implementation contract
- **Proposed design:** P0-3A proves one compatible fixed dependency set; P0-3B moves untrusted decode into the bounded child process and removes binary/DOCX fallback; P0-3C applies measured container controls. Reuse Python/OS standard process and resource controls and existing request-size facilities before considering a new sandbox service or package.
- **Affected files/components:** `tools/summarizer/requirements.txt` and a generated lock/hash file; `tools/summarizer/app.py` plus a small parser-child module; summarizer Dockerfile/Compose settings; `ProfileCvController` upload/pipeline/parsing partials; `SummarizerService`; `CvProcessingQueue`; nginx upload settings; parser/backend tests and benign fixtures.
- **Database/configuration migration:** no required schema migration. Add explicit parser-limit/environment settings with the conservative defaults above and container runtime budgets. If a durable retry field proves necessary, it requires a separate additive migration and review rather than being smuggled into the hardening patch.
- **Backward-compatibility risks:** large/page-heavy/image-heavy CVs that previously attempted processing will be rejected; extraction output can change after parser upgrades; OCR may time out; non-Linux developer environments cannot enforce the same rlimits and must fail/skip explicitly in tests; removing fallback reduces availability when the sidecar is down.
- **Rollback:** retain the previous image digest for diagnosis, but disable CV import/extraction rather than re-expose a known vulnerable/unbounded parser. Database artifacts remain intact and can be retried after a corrected image. Roll back individual conservative limits by configuration only after measured benign evidence.
- **Deployment sequence:** (1) build P0-3A image and run dependency audit/resolution plus benign regression corpus; (2) deploy to a non-production local environment with parsing disabled by default; (3) add P0-3B boundary/failure tests and enable for synthetic files; (4) size healthy parent/child memory; (5) deploy P0-3C runtime controls; (6) canary enable CV import, monitor timeouts/rejections/memory without logging content; (7) widen only evidenced-too-small limits through reviewed config.
- **Documentation changes:** exact formats/limits/error codes; parser/subprocess/container trust boundary; temporary-file cleanup; dependency/advisory exception record; safe update procedure and regression corpus; operational disable/retry procedure.
- **Dependencies:** P0-3A before P0-3B/C; P0-2 is not a parser prerequisite. Coordinate account-deletion cleanup with P0-6, but do not delay vulnerable dependency removal for it.
#### Required tests and acceptance criteria
- Dependency resolution/audit test shows no unaccepted reachable High/Critical parser advisory; assert the final FastAPI/Starlette pair is compatible. Clean-image install uses locked versions/hashes.
- Generated benign boundary fixtures test exact/over file, page, ZIP entry/uncompressed/ratio, dimension/pixel, character and multipart limits; extension/magic mismatches; truncated/corrupt files; Unicode/Norwegian text; and a normal CV corpus for extraction quality.
- A harmless sleeping child tests timeout/process-group termination; generated oversized metadata tests resource rejection without malicious payloads. Assert temp files and children are gone after success, error, cancellation and simulated restart.
- Backend integration tests assert sidecar outage never invokes PDF/DOCX/image fallback, errors are stable/sanitized, queue backpressure works and owner isolation remains.
- Container tests assert non-root, read-only, dropped capabilities, private network, token requirement, bounded tmpfs/PIDs/resources and no published port.
- **Acceptance:** untrusted input is bounded before buffering and before expensive decode; every parser descendant is terminated on deadline; failed/abandoned work is cleaned; no raw parser details leak; normal corpus remains acceptable; and any remaining advisory has an explicit reviewed exception and compensating control. No claim of exploit reproduction is required.
### JT-007 and JT-008 — email ownership and session invalidation
#### Revalidated findings
- **JT-007 final severity/confidence/classification:** **Medium / Confirmed / confirmed defect**. With email verification enabled, registration issues a normal local session before confirmation; profile update changes email/username without ownership proof and leaves confirmation state unchanged. Runtime evidence confirms protected access and the retained confirmation flag.
- **JT-008 final severity/confidence/classification:** **Medium / Confirmed / confirmed defect**. Logout clears cookies only; a copied cookie remains valid. Successful password reset/change does not revoke existing `UserSessions` or trusted devices.
- **Confirmed execution paths:** `JobTrackerApi/Controllers/AuthController.cs:132-180` creates an unconfirmed Identity user, sends verification, then calls `CompleteSignInAsync`; login later rejects unconfirmed users, so initial and later behavior conflict. Verification/resend are at `:748-799`. `UpdateProfile` at `:405-439` assigns email and username directly. Logout `:347-355`, password change `:655-674`, and reset `:678-743` contain no session-revocation call. `JobTrackerApi/Services/AppSessionIssuer.cs` creates a database `sid`; `Program.cs` token validation, `LocalSessionValidator.cs` and `SessionsController.cs` prove a revocation mechanism already exists.
- **Existing mitigations:** later login checks confirmation; Identity email/password tokens expire and are data-protected; session rows can be listed/revoked manually; signed local JWTs bind user and `sid`; 2FA/recovery codes/trusted devices exist. The current validator should still query by both `sid` and principal user ID defensively.
- **Existing tests:** tests currently expect registration success/session in relevant configurations and direct profile email update; session tests cover explicit revocation but not logout/reset/change revocation. These tests must be changed deliberately, not merely supplemented.
#### Complete state-transition contract
| Event | Preconditions | State/result | Session/trusted-device effect |
|---|---|---|---|
| Register, verification required | unique valid email/password | `PendingVerification`; send link; return 202 `verification_required`; no protected session | create no `UserSession`, session cookie or CSRF cookie |
| Verify registration email | valid latest Identity token for same user/email | `Active`; require a normal login, no implicit sign-in | none to revoke |
| Resend verification | unconfirmed account; generic response; rate limit | invalidate naturally via security stamp/latest-token policy where supported; send only to stored address | none |
| Register, verification disabled | normal policy | `Active` and existing sign-in behavior | create normal session |
| Request email change | authenticated + recent reauthentication; unique new email | retain old email/confirmation; store `PendingEmail` and request time; notify new address and old address | current sessions continue until proof |
| Confirm email change | valid token and exact current `PendingEmail`; user still active | atomically use Identity `ChangeEmailAsync`; update username only if it equalled old normalized email; clear pending state | revoke **all** sessions and trusted devices; require login with new address |
| Cancel/replace email change | authenticated; latest request wins | clear/replace pending; old email remains active | no revocation |
| Logout | valid, expired or malformed current cookies | best-effort revoke matching (`sid`, user); always clear session/CSRF cookies; return idempotent 204 | current session only; trusted device retained |
| Change password | authenticated + current password + 2FA where policy requires | update password/security stamp | revoke all **other** sessions; retain reissued current session; trusted devices other than current are removed |
| Request reset | generic response; only confirmed local-password accounts receive mail | send purpose-bound token to stored confirmed email | no revocation until success |
| Complete reset | valid token/new password | update password/security stamp; preserve 2FA requirement | revoke all sessions and all trusted devices; require login and normal 2FA/recovery code |
| Admin password reset | authorized admin, not unsafe self/last-admin case | same credential/security-stamp change | revoke all target sessions/trusted devices |
| Provider link/unlink or 2FA disable/recovery-code regeneration | recent reauthentication; never remove last credential | apply security-sensitive change | revoke other sessions; remove trusted devices when 2FA assurance changed |
Email reset is not a 2FA bypass. Passwordless accounts do not receive a password-reset flow unless a separately designed credential-creation path is approved. A provider token may satisfy recent reauthentication only through an already-linked immutable provider identity, never through a matching email claim.
#### Implementation contract
- **Proposed design:** reuse the existing session table/validator. Add concrete revocation methods for current, other and all sessions plus trusted-device removal; no interface is needed. Make logout `AllowAnonymous`/idempotent so stale cookies can always be cleared. Include user ID in session lookup. Update the security stamp on recovery-sensitive changes and bind pending 2FA state to the current stamp/version so old pending challenges fail.
- Remove email from generic profile update. Add request/confirm/cancel email endpoints using Identity's email-change token API. Store nullable `PendingEmail` and `PendingEmailRequestedAtUtc` so the current request is visible and latest-wins behavior is enforceable. Old confirmed email remains authoritative until atomic confirmation.
- Registration with required verification returns a typed 202 and never calls session issuance. The UI displays resend/verify guidance and does not call `/auth/me` or navigate as authenticated.
- **Affected files/components:** `AuthController`, profile/auth DTOs, `AppSessionIssuer`, `LocalSessionValidator`, `SessionsController`, trusted-device/2FA services, `ApplicationUser`, `ApplicationDbContext`, additive EF migration/snapshot, login/verification/profile/settings UI, email templates/service calls, auth/session/controller/E2E tests.
- **Database/configuration migration:** add bounded nullable pending-email and request-time columns. No new session table is needed. Apply any security-stamp-aware pending-2FA serialization change compatibly by rejecting old cache entries after deployment.
- **Backward-compatibility risks:** unverified registrants no longer enter the app immediately; clients expecting registration 200/auth cookies or profile-email update via the generic endpoint must change; password reset logs out every device; passwordless/provider-only recovery becomes more explicit. Existing active sessions remain valid until a defined security event; an optional one-time deployment revoke-all is not necessary for this defect and is deferred unless incident response requires it.
- **Rollback:** registration/email-change endpoints can be disabled while verification/login remains available. Additive pending columns remain harmless. Do not restore non-revoking logout/reset. If UI/API version skew occurs, reject email mutation safely and keep the old confirmed email.
- **Deployment sequence:** (1) P0-2A canonical links; (2) deploy P0-4A revocation helper and logout/reset/change behavior; (3) apply additive pending-email migration; (4) deploy P0-4B API/UI together with registration behavior; (5) verify email sink and copied-session tests; (6) monitor failed confirmation/recovery counts without addresses; (7) update support procedures before enabling self-service email change.
- **Documentation changes:** state diagram and API responses; registration/resend behavior; email-change notices; logout/password reset/change effects; 2FA-preserving recovery; passwordless recovery/support playbook.
- **Dependencies:** P0-2A for safe links; P0-4A before P0-4B; P0-1B uses the same recent-reauth/revocation rules. P0-6 deletion also uses revoke-all.
#### Required tests and acceptance criteria
- Unit-test state transitions, generic enumeration-resistant responses, latest pending email, token/email mismatch, expiry/replay, username preservation and the last-credential rule.
- Integration-test zero session rows/cookies after verified-required registration; verify then login; duplicate email; old/new email login before/after confirmation; all/current/other session counts; copied cookie after logout/reset; security-stamp and pending-2FA invalidation; 2FA/recovery codes; passwordless/provider-linked users.
- End-to-end test registration, resend, verification, pending/cancel/confirm email, logout in two tabs, reset with two sessions and recovery with 2FA using a local email sink/mocks only.
- **Acceptance:** an unverified mailbox never grants protected access or becomes the account's active email; old email remains usable until proof; logout invalidates its exact copied `sid`; successful reset/recovery invalidates all prior sessions/trusted devices without disabling 2FA; password change retains only a reissued current session; and legitimate recovery never depends on an unverified email/provider claim.
### JT-010 — attachment filesystem/database consistency
#### Revalidated finding
- **Final severity/confidence/classification:** **Medium / High / likely defect**. The failure windows are confirmed in code; an actual production orphan/missing file was not reproduced.
- **Confirmed execution path:** `JobTrackerApi/Controllers/AttachmentsController.cs:199-258` validates/writes batch files sequentially before all later files and before `SaveChanges`; a later invalid file or DB failure can leave files without rows. Rename at `:117-196` moves the physical random storage file before saving metadata, so DB failure can leave a stale path. Delete commits row/flag changes, then best-effort deletes at `:189` and swallows failure, leaving an orphan. Existing tests cover type/size/derived flags, not these failure boundaries.
- **Existing mitigations:** authenticated owner-scoped application lookup; random filenames; root path helpers; extension/size/quota checks; database transactions in some metadata operations; derived-flag recomputation. There is no durable cross-resource commit or reconciliation record.
#### Recoverable mutation design
- Add one concrete attachment file store plus a small JSON operation journal under the attachment root; do not add a general storage-provider interface. Journal writes use same-volume atomic replace, UUID operation IDs, normalized root-validated paths and no user content/secrets.
- **Upload:** validate the entire batch and quota before writing; stream each file into `.staging/{operationId}`; open a DB transaction, create rows with final random paths and recompute flags; persist the journal; atomically move staged files to final paths; commit; remove the journal. Any pre-commit failure rolls back rows and removes staged/final files. Restart reconciliation treats rows-plus-final-files as committed and removes the journal, or removes stage/final files when rows do not exist.
- **Rename:** update display `FileName`, purpose and derived flags in one DB transaction. Never move the randomized physical `FilePath`. Preserve/validate the actual extension so display rename cannot disguise content.
- **Delete:** persist a journal and atomically move the file to `.trash/{operationId}`; delete row/recompute flags in one transaction; commit; then purge trash. On restart, restore the file when the row still exists, or purge it when the row is gone. A missing source becomes an observable inconsistency, not a swallowed success.
- Run reconciliation on startup and periodically for stale journals/staging/trash. Log operation ID, stage and counts only. Refuse paths outside the configured root and symlink/reparse-point escape.
#### Implementation contract
- **Proposed design:** implement the minimal journaled saga above around existing controller actions and derived-flag logic. P0-6 reuses its invariants but owns a separate deletion-quarantine journal.
- **Affected files/components:** `AttachmentsController`, attachment path/storage helper(s), application startup/hosted reconciliation, attachment model/context only if an operation ID later proves necessary, filesystem settings, and controller/storage integration tests.
- **Database/configuration migration:** none in the preferred design; the durable journal is filesystem-based. Add only staging/trash retention/reconciliation settings if defaults cannot be constants.
- **Backward-compatibility risks:** rename no longer changes the physical random filename (not an external contract); locked/permission-denied files return retryable failure instead of false success; startup may discover pre-existing orphans that lack a journal and must report rather than guess.
- **Rollback:** drain/reconcile all journals before reverting. Staged/trash files remain recoverable by operation ID; never delete an unknown pre-existing orphan automatically. No schema rollback.
- **Deployment sequence:** (1) back up/manifest attachment rows and files; (2) deploy reconciler in report-only mode for pre-existing inconsistencies; (3) resolve only reviewed synthetic/test inconsistencies; (4) enable journaled upload/rename/delete; (5) inject safe failures locally; (6) monitor stale journal/missing/orphan counters; (7) let P0-6 depend on the documented invariants.
- **Documentation changes:** row/file invariants, staging/journal/trash layout, restart decisions, retention, report-only handling for legacy orphans and operator recovery procedure.
- **Dependencies:** coordinate journal/root validation with P0-6A/B. No dependency on identity or parser packages.
#### Required tests and acceptance criteria
- Integration tests inject: invalid second file, cancellation during copy, DB save/commit failure, move collision, locked file, delete/purge failure and process restart at each journal stage.
- Test exact quota/size boundaries, repeated/double submissions, idempotent reconciliation, traversal/symlink escape and two-user isolation.
- Assert rename changes only metadata, derived flags remain consistent, and logs/errors contain no file content or unsafe path.
- **Acceptance:** after every injected failure, state is either fully committed or represented by one retryable journal; committed rows always have the expected file; deleted rows have no untracked live file; owner isolation remains; and legacy unknown orphans are reported without destructive guessing.
#### Repository implementation record (2026-08-02)
SEC-008 implements the same durable state machine with `<final>.uploading` and `<final>.deleting` marker files instead of a separate JSON record. The generated collision-resistant final path is already the operation identity, so a second file and parser would duplicate state without improving recovery. Startup reconciles markers against durable rows, logs counts only, preserves unknown plain orphans and rejects unmanaged paths. Transaction cleanup now occurs only after confirmed rollback; uncertain commit outcomes retain the marker. Focused tests pass 20/20 and the full backend passes 525/525. Browser, production report-only inventory, MariaDB and executable symlink checks remain unverified; see `docs/verification/sec-008-attachment-consistency.md`.
### JT-009 — complete account export and deletion
#### Revalidated data inventory and finding
- **Final severity/confidence/classification:** **Medium / Confirmed / confirmed defect**. `JobTrackerApi/Controllers/UsersController.cs` admin deletion calls Identity `UserManager.DeleteAsync`, while `JobTrackerApi/Data/ApplicationDbContext.cs` shows that most domain roots have no FK to `AspNetUsers`; owned rows, files and credentials remain. `BackupController` exports a partial application-key-encrypted backup, and `ExportController` exports jobs only; neither is a complete user-readable portability export.
- **Confirmed execution path:** the admin Users API deletes only the `ApplicationUser` through Identity. Identity-owned claims/logins/tokens/roles follow their own relationships, but domain entities remain keyed by user IDs or indirectly through applications without an `AspNetUsers` cascade. `JobTrackerApi/Services/AppPaths.cs` locates attachment/CV/export roots; `AvatarStorage.cs` owns hashed avatar directories; CV generators write date/candidate-derived exports without a durable owner mapping. Provider connection rows retain encrypted tokens/credentials, queued CV runs remain discoverable from the database, and current caches/logs/backups have no per-user erase operation.
- **Direct/indirect database ownership:** Identity user fields/claims/logins/tokens/roles; companies, jobs and job applications; correspondence, job events, attachments, tailored CV drafts, interview prep/AI notes, AI interactions, checklist items, cover-letter versions and interview-prep items; Career Profile, versions, experiences, education, skills, projects, certifications and languages; CV variants/versions; CV upload artifacts/extraction runs; Gmail/Graph/IMAP connections and Gmail review decisions; per-user rules; recovery codes, trusted devices and user sessions. Global `RuleSettings` and `SystemEmailSettings` are not user-owned and must not be exported/deleted as such.
- **Files/documents:** attachment files under job IDs; CV artifacts under user IDs; avatars under hashed user directories; generated CV PDFs/DOCX under date/candidate-derived paths without an owner record; daily export files, including legacy filenames containing raw owner IDs; crashed `jobtracker-cv-pdf` temp files. Data-protection keys are global and never deleted per user.
- **Tokens/queues/cache/logs:** encrypted Gmail/Graph tokens and IMAP credentials; database CV runs plus in-memory queue; backend AI result caches (up to six hours), sidecar cache (one hour), OAuth state (15 minutes), pending 2FA (five minutes), browser local storage; rotated application logs; backups and external provider/model services. Current caches are not owner-invalidatable, and backup retention is not defined.
- **Existing mitigations:** many queries use owner filters/soft delete; connection secrets are encrypted; sessions/trusted devices are owner-keyed; artifacts have pruning; generated CVs have time cleanup; OAuth callbacks recheck state/user in some paths. These are not a complete export/deletion lifecycle.
#### Readable export design
- Add an authenticated, recent-reauthenticated, rate-limited export request that streams a ZIP through a bounded private temporary file. It contains:
- `manifest.json`: schema version, generated time, category/file counts, SHA-256 checksums and missing/unavailable warnings;
- readable JSON/CSV for safe account/profile fields; company/job/application/workspace data; correspondence/events; Career Profile current/version/children; CV variants/versions/extraction history; AI drafts/notes/interactions/usage; checklist/cover/interview data; settings; provider connection/sync metadata; and session/trusted-device metadata;
- original owned attachment/CV artifact bytes and owned generated documents;
- `README.txt` describing formats, exclusions, external-provider data and backup/log retention.
- Exclude password hash, security stamp, TOTP secret, recovery/token hashes, provider access/refresh tokens, IMAP password, data-protection keys, internal authorization secrets and other users/global settings. Export linked-provider identifiers only to the degree useful to the user, with tokens redacted.
- Before enabling deletion, make future generated CV/daily export paths owner-scoped using an opaque owner directory and UUID filename. Current date-only generated outputs cannot be attributed safely; because they are regenerable and unreferenced, purge them as a separately reviewed rollout operation after root verification rather than guessing ownership. Future generators receive owner ID explicitly.
#### Deletion lifecycle design
- Add `DeletionStatus`/`DeletionRequestedAtUtc` to the user and durable `AccountDeletionRequest` plus `AccountDeletionFile`/manifest records. The request survives user-row deletion, records stage/attempt/count/sanitized error, and never stores content or credentials.
- Both self-service and admin deletion call one coordinator. Require recent reauthentication and exact confirmation; prevent last-admin/unsafe self-admin deletion. Return 202 and immediately mark the account deletion-pending, block sign-in/new mutations, unpublish public CVs, revoke all sessions/trusted devices and stop/cancel new queued work.
- The coordinator is an idempotent staged saga:
1. **Preflight:** enumerate every row/file by owner with `IgnoreQueryFilters`, verify paths stay under configured roots, record counts/checksums and acquire an in-process per-user mutation gate. Multi-instance distributed locking is deferred until multiple API replicas are supported.
2. **Provider cleanup:** best-effort revoke Google consent/token before deleting encrypted rows; delete Microsoft Graph local tokens and provide Microsoft consent-removal instructions rather than request broad revoke scopes; delete IMAP credentials. Retry transient provider failures for a bounded window, then complete local deletion with an auditable warning rather than retain credentials indefinitely.
3. **File quarantine:** atomically move all owned live files to `DeletionQuarantine/{requestId}` on the same volume and journal each path. If any required move fails, do not delete database rows; retry/restore idempotently.
4. **Database transaction:** explicitly delete deepest children first: job-application workspace children; attachment metadata; CV variant versions/variants; Career children/versions/profile; extraction runs/artifacts; provider connections/decisions/user rules; applications, jobs then companies; recovery/trusted/session rows; Identity claims/logins/tokens/roles and user last. Assert pre/post counts. Do not depend on missing FKs or global query filters.
5. **Final purge:** after DB commit, purge quarantine and owner caches/temp state. A purge failure remains a retryable stage; it never recreates rows. Before DB commit, a failure restores quarantined files. OAuth callbacks and workers recheck active-user state so stale work cannot recreate data.
- Record a minimal deletion tombstone on a restricted append-only store separate from the restored database. It needs only a pseudonymous/raw stable user key, request ID and completion time sufficient to replay deletions after an old backup restore. Retain at least as long as the longest backup. This is required because the current indefinite backup retention could resurrect deleted accounts.
- **Immediate versus retained:** live DB rows/files/tokens/sessions/caches are removed on saga completion; rotated logs age out under documented retention and should contain only pseudonymous IDs; immutable backups remain until scheduled expiry and are not edited in place; any restore must replay tombstones before readiness. External provider/legal retention is documented separately. Exact backup/log retention and lawful requirements require operator/legal decision and are not certified by this technical design.
#### Implementation contract
- **Proposed design:** P0-6A creates one inventory/export manifest and owner-scoped generated paths. P0-6B reuses that inventory for the deletion saga. Do not overhaul every FK or build a general workflow engine in Phase 0.
- **Affected files/components:** `ApplicationUser`, `ApplicationDbContext`, new deletion request/file entities and EF migration; admin users controller and new account export/deletion endpoints/service; all owner root repositories/controllers; session/trusted-device/provider services; CV queue/workers; attachment/CV/avatar/export path helpers; generated CV/daily export code; cache keys; OAuth callbacks; UI account settings/admin UI; backup/restore deployment scripts/runbooks; comprehensive integration/E2E tests.
- **Database/configuration migration:** additive lifecycle/request/file tables and user status/time fields with bounded indexes; owner-scoped path settings; separate restricted tombstone volume/key and explicit live/log/backup/quarantine retention settings. Do not remove data or add broad cascade FKs in the migration.
- **Backward-compatibility risks:** deletion becomes asynchronous; pending users lose access immediately; generated download paths change; provider revocation can fail independently; an application rollback after a completed deletion cannot restore purged live data; old backups require tombstone replay before service. Existing encrypted admin backup format remains a backup artifact, not relabeled as user export.
- **Rollback:** P0-6A export/path changes are additive; keep old-path read support only for a bounded transition and purge only regenerable reviewed files. P0-6B can be disabled before accepting requests. Once a request reaches DB commit/purge it is intentionally irreversible; rollback means finishing/reconciling the saga, not restoring user data. Preserve request/tombstone records across application rollback.
- **Deployment sequence:** (1) decide/log backup retention, tombstone custody and provider semantics; (2) deploy owner-scoped generated paths and inventory/export disabled; (3) validate a two-user export manifest and purge only verified regenerable legacy generated outputs; (4) apply additive deletion migration and mount restricted ledger/quarantine storage; (5) deploy coordinator dark, exercise one disposable synthetic user including restart/failure; (6) verify backup-restore tombstone replay; (7) enable user export; (8) enable admin deletion; (9) enable self-service deletion only after support/runbook readiness.
- **Documentation changes:** exact export category/schema/exclusions; deletion state and timing; provider revoke limitations; immediate/live versus log/backup/external retention; operator retry/reconcile and tombstone restore procedure; legal-review questions; user-facing confirmation/warnings.
- **Dependencies:** P0-4A revoke-all; P0-5 attachment invariants; P0-6A before P0-6B; P2-2 backup rehearsal is needed before production self-service enablement even if implementation code is complete. Parser cleanup from P0-3 must expose owner/run cancellation hooks.
#### Required tests and acceptance criteria
- Build two synthetic users, each with every direct/indirect entity, attachment/CV/avatar/generated file, provider state, session/trusted device, queued/interrupted run and cache entry. Export/delete User A and assert User B byte/row counts and access are unchanged.
- Validate manifest schema/counts/checksums, readable JSON/CSV, binary file inclusion, redaction list, missing-file warnings, exact boundary/stream cancellation and repeated export.
- Inject provider timeout, file move/purge failure, DB failure before/after commit, worker race, OAuth callback, restart at every stage and repeated deletion request. Assert idempotent convergence and sanitized audit records.
- Restore a disposable pre-deletion backup, replay the separate tombstone and assert the deleted account/data never becomes ready/accessible. Verify log/backup retention is reported, not falsely claimed as immediate erasure.
- End-to-end test self/admin confirmation, last-admin guard, immediate pending lockout, progress/final status and unavailable completed account using disposable local accounts only.
- **Acceptance:** readable export covers every documented owned category or explicitly warns/excludes it; secret material is absent; deletion removes all live owned DB rows/files/tokens/queued work/cache without affecting another user; every partial failure is retryable/auditable; provider limitations and backup retention are truthful; restored backups cannot resurrect a completed deletion.
### Independently reviewable Phase 0 work packages
#### P0-2A — canonical application origin and Host guard
- **Scope/findings:** JT-002 application boundary only: parse `App:PublicBaseUrl` once; remove every request-host fallback; derive Host filtering; secure-cookie decision; config/controller tests.
- **Dependencies:** confirmed production URL and internal health host names.
- **Acceptance/tests:** JT-002 application tests above; all generated links/callbacks canonical; hostile Host/forwarded headers rejected; Production fails fast; local/Test remain green.
- **Rollback:** supply the last valid origin or roll back binaries; never restore request-host fallback/wildcard host.
- **Documentation/config:** `.env.example`, app settings contract and deployment preflight. No DB/dependency migration.
- **Effort:** Small.
- **Deferred:** Compose/nginx/Traefik changes to P0-2B; multi-origin support.
#### P0-2B — production ingress, proxy and Compose alignment
- **Scope/findings:** JT-002 deployment boundary: explicit production/dev Compose files, close bound ports, exact Traefik contract, nginx forwarding and explicit known-proxy configuration.
- **Dependencies:** P0-2A canonical host and actual operator-supplied Traefik proxy/network values.
- **Acceptance/tests:** merged-config and two-hop proxy tests above; exact router works, unknown host/direct ports do not.
- **Rollback:** keep direct ports closed; restore last valid proxy network/config.
- **Documentation:** production/development Compose commands and ingress smoke/runbook.
- **Effort:** Small to Medium.
- **Deferred:** service mesh/CDN/multiple public origins.
#### P0-1A — Microsoft tenant and issuer trust policy
- **Scope/findings:** JT-001 validator/config/raw-bearer branch; no data migration or legacy assignment.
- **Dependencies:** supported tenant mode chosen and configured.
- **Acceptance/tests:** validator/account-mode tests above; invalid/mismatched tenant rejected; no raw alternate trust path.
- **Rollback:** disable Microsoft auth; never re-enable issuer-blind validation.
- **Documentation:** tenant-mode matrix and sign-in/Graph setting distinction.
- **Effort:** Small to Medium.
- **Deferred:** canonical row migration/relink to P0-1B.
#### P0-1B — canonical Microsoft links and legacy relinking
- **Scope/findings:** JT-001 additive (`tid`,`oid`) schema, composite lookup, explicit linking, inventory and temporary recovery ceremony.
- **Dependencies:** P0-2A, P0-1A and P0-4 recent reauthentication/email proof for the complete legacy flow.
- **Acceptance/tests:** migration/collision/relink/last-credential tests above; zero silent backfill/merge.
- **Rollback:** retain additive columns/legacy evidence; disable relink/exchange; keep established canonical links immutable.
- **Documentation:** user/support/operator transition guide.
- **Effort:** Medium.
- **Deferred:** legacy-column removal to a later approved migration; Graph permission redesign.
#### P0-4A — session revocation primitives and security-event policy
- **Scope/findings:** JT-008 current/other/all revocation, logout, password change/reset/admin reset, trusted devices, stamp-aware pending 2FA.
- **Dependencies:** existing session table/validator only.
- **Acceptance/tests:** copied-session and security-event matrix above; idempotent logout; no 2FA bypass.
- **Rollback:** retain revocation behavior; disable affected mutation endpoints if version skew occurs.
- **Documentation:** session effects/recovery support matrix.
- **Effort:** Small to Medium.
- **Deferred:** passkeys and deployment-wide revoke-all without an incident need.
#### P0-4B — verified registration and pending-email transition
- **Scope/findings:** JT-007/JT-008 registration response/session behavior, pending email schema/endpoints/UI and all-session revocation on confirmation.
- **Dependencies:** P0-2A and P0-4A; reuse P0-1 recent reauthentication rules.
- **Acceptance/tests:** full registration/email transition E2E above; old email retained until proof; no protected unverified session.
- **Rollback:** disable registration/email change; keep old confirmed email and additive fields.
- **Documentation:** API/UI state transitions and resend/recovery guidance.
- **Effort:** Medium.
- **Deferred:** passwordless credential creation and passkeys.
#### P0-3A — compatible parser dependency update
- **Scope/findings:** JT-006 exact parser dependency resolution, lock/hashes, advisory review and benign extraction corpus only.
- **Dependencies:** approved package-index/advisory access during implementation; final compatible FastAPI/Starlette pair.
- **Acceptance/tests:** no unaccepted reachable High/Critical advisory; clean locked install; benign corpus parity.
- **Rollback:** disable parsing rather than deploy the vulnerable image; retain prior digest only for diagnosis.
- **Documentation:** version/advisory/exception record.
- **Effort:** Medium.
- **Deferred:** Transformers/Torch upgrade absent demonstrated JT-006 reachability.
#### P0-3B — bounded isolated parser and safe backend failure
- **Scope/findings:** JT-006/JT-011 streaming/signature/format limits, subprocess deadline/rlimits, queue backpressure, sanitized failures and removal of binary/DOCX fallback.
- **Dependencies:** P0-3A and an approved benign boundary corpus.
- **Acceptance/tests:** generated boundary, harmless timeout, cleanup/outage tests above; no unbounded path remains.
- **Rollback:** disable import/extraction; preserve artifacts for retry.
- **Documentation:** supported limits/errors/retry and isolation boundary.
- **Effort:** Large.
- **Deferred:** multi-process throughput and separate parser service until measured.
#### P0-3C — parser container controls and cleanup
- **Scope/findings:** JT-006 non-root/read-only/capability/PID/tmpfs/resource/network controls and stale temp cleanup.
- **Dependencies:** measured parent ML plus parser memory from P0-3B.
- **Acceptance/tests:** container assertions and restart cleanup above; healthy corpus fits explicit budget.
- **Rollback:** conservative budget adjustment or disable parsing; never restore root/unbounded exposure in Production.
- **Documentation:** sizing and operator cleanup/runbook.
- **Effort:** Small to Medium.
- **Deferred:** orchestration platform/sandbox service.
#### P0-5 — recoverable attachment mutations
- **Scope/findings:** JT-010 journaled validate-stage-commit/promotion, metadata-only rename, trash delete and reconciliation.
- **Dependencies:** shared root/path conventions coordinated with P0-6.
- **Acceptance/tests:** all injected filesystem/DB/restart/two-user tests above.
- **Rollback:** reconcile/drain journals first; preserve unknown legacy orphans.
- **Documentation:** invariants, journal states and operator recovery.
- **Effort:** Medium.
- **Deferred:** object storage, deduplication and general storage abstraction.
#### P0-6A — owner inventory, generated-path ownership and readable export
- **Scope/findings:** JT-009 one authoritative inventory/manifest; readable ZIP; redactions; owner-scoped future generated documents; reviewed legacy generated-file purge.
- **Dependencies:** retention/exclusion decisions and P0-5 path conventions.
- **Acceptance/tests:** two-user manifest/checksum/redaction/stream tests above.
- **Rollback:** disable export; retain old-path read only for transition; regenerate reviewed legacy outputs.
- **Documentation:** schema/categories/exclusions and retention statement.
- **Effort:** Medium to Large.
- **Deferred:** cross-product portability and importing the export.
#### P0-6B — idempotent deletion lifecycle and restore tombstones
- **Scope/findings:** JT-009 pending-account gate, provider cleanup, file quarantine, ordered DB purge, retry/audit, tombstone replay and UI/admin flows.
- **Dependencies:** P0-4A, P0-5, P0-6A; backup-retention/tombstone decisions and P2-2 rehearsal before production enablement.
- **Acceptance/tests:** two-user full inventory, fault/restart/idempotence, provider and backup-restore tests above.
- **Rollback:** disable new requests; reconcile accepted requests forward. Completed purge is intentionally irreversible.
- **Documentation:** confirmation, stages, retry, provider/backup/legal limitations and restore runbook.
- **Effort:** Large.
- **Deferred:** general workflow engine, broad cascade-FK rewrite, multi-replica distributed coordinator and legal certification.
## Phase 1 — broken core workflows and production reliability
### P1-1 — Restore SQLite/MariaDB parity for Career and Application Workspace
- **Findings/scope:** JT-003; every affected `DateTimeOffset` order/compare path through shared services/controllers.
- **Dependencies:** choose bounded materialisation versus common UTC storage mapping; preserve provider behaviour.
- **Acceptance criteria:** CV variants/runs, AI history/usage and workspace return correct owner/empty/non-owner results on SQLite and MariaDB.
- **Tests:** fresh/seeded HTTP provider matrix with zero/one/many dates and month boundary/timezone cases.
- **Rollback:** service-level query changes are reversible; any storage migration requires verified down/restore plan.
- **Documentation:** supported provider/date mapping and local setup.
- **Effort:** Medium.
- **Deferred:** adding PostgreSQL.
### P1-2 — Remove ambiguous workspace routes
- **Findings/scope:** JT-004; timeline/interview canonical actions and response DTOs.
- **Dependencies:** inventory frontend/test/API consumers and select compatible contract.
- **Acceptance criteria:** exactly one action per verb/path; owner 200, non-owner 404, anonymous 401; UI panels load.
- **Tests:** route-table uniqueness plus HTTP and browser panel tests.
- **Rollback:** keep a temporary differently named compatibility route only if a real caller requires it; do not reintroduce ambiguity.
- **Documentation:** current application-workspace endpoint reference.
- **Effort:** Small to Medium.
- **Deferred:** redesigning workspace API.
### P1-3 — Establish explicit owner-aware worker execution
- **Findings/scope:** JT-005; rules, reminders, daily export and enrichment; structured outcomes/errors.
- **Dependencies:** P0-4 notification/session policy where relevant; P1-4/P0-3 for AI safety; P1-4 below for user preferences/AI consent before activating sends/calls.
- **Acceptance criteria:** each enabled worker processes correct owners once; disabled work stays off; failures visible; no cross-owner data/output; restart idempotent.
- **Tests:** two-owner hosted integration, no HttpContext, enable/disable, retry/restart, clock boundary, email/AI fakes.
- **Rollback:** per-worker kill switches default safe; staged rollout; preserve prior data/status for reversal where possible.
- **Documentation:** worker schedule, idempotency, privacy effects and operator diagnostics.
- **Effort:** Medium.
- **Deferred:** distributed queue/multi-replica scheduler until scale requires it.
### P1-4 — Make notification and AI privacy controls real before worker activation
- **Findings/scope:** JT-012/JT-022; persistent per-user notification channels, AI enable/recipient/data summary and server enforcement.
- **Dependencies:** product/privacy decision on defaults and providers; P1-3 worker owner context.
- **Acceptance criteria:** user opt-out suppresses sends/calls; settings persist across devices; provider/data categories displayed; module payloads limited to documented fields.
- **Tests:** mixed users/default migration, fake email/AI call capture, attachment inclusion, background enrichment disabled/enabled.
- **Rollback:** global email/AI kill switches; conservative default disabled during migration.
- **Documentation:** privacy explanation, provider matrix, settings semantics.
- **Effort:** Medium.
- **Deferred:** per-module provider marketplace and advanced consent receipts.
### P1-5 — Bound job-import and request-body reads before buffering
- **Findings/scope:** remaining JT-011 job import and API/sidecar request limits.
- **Dependencies:** standard bounded-stream helper or framework request-size configuration; no new dependency needed.
- **Acceptance criteria:** declared/chunked oversize aborts before allocating/download beyond limit; slow/cancelled streams terminate.
- **Tests:** exact boundary, content-length over, chunked over, slow stream, cancellation and timeout.
- **Rollback:** configurable conservative limit; no unbounded fallback.
- **Documentation:** import/upload limits and error messages.
- **Effort:** Small to Medium.
- **Deferred:** large-document support.
## Phase 2 — regression tests, observability and recovery
### P2-1 — Add boundary-focused CI gates
- **Findings/scope:** JT-014/JT-016; route uniqueness, SQLite HTTP journeys, worker context, two-user auth, Python tests/audit, `tsc --noEmit`, minimum accessibility checks.
- **Dependencies:** Phase 1 fixes so new tests can start green; decide advisory exception policy.
- **Acceptance criteria:** every confirmed High runtime defect has a failing-before/fixed-after test; all gates run on PR/main without file whitelists.
- **Tests:** the new integration/browser/worker/security tests themselves; CI clean-cache rehearsal.
- **Rollback:** flaky test may be quarantined only with owner/expiry/evidence; never silently omit whole suites.
- **Documentation:** one authoritative local/CI command matrix.
- **Effort:** Medium.
- **Deferred:** raw coverage targets and exhaustive browser matrix.
### P2-2 — Define and rehearse complete provider recovery
- **Findings/scope:** JT-013; SQLite/MariaDB DB, attachments/CV artifacts/exports, key ring, secrets/config, RPO/RTO, off-host retention.
- **Dependencies:** operator storage/backup destination and encryption/key custody decisions.
- **Acceptance criteria:** automated artifact manifest; isolated restore for both providers within RTO/RPO; protected tokens and file downloads work; evidence retained.
- **Tests:** scheduled integrity/count/file manifest and disposable restore after migrations.
- **Rollback:** preserve immutable pre-restore backup; documented abort/forward migration decision.
- **Documentation:** exact backup/restore/rotation/runbook and rehearsal log template.
- **Effort:** Large operational.
- **Deferred:** multi-region disaster recovery unless business requirements demand it.
### P2-3 — Add actionable worker/deployment observability
- **Findings/scope:** JT-005/JT-013/JT-021; structured worker run counts/durations/errors, backup age, provider health, alerting, correlation.
- **Dependencies:** P1-3 worker semantics and chosen monitoring destination.
- **Acceptance criteria:** failed/stale worker or backup produces an actionable alert; logs contain owner-safe IDs/counters, not content/tokens; health distinguishes readiness/dependencies.
- **Tests:** fake failures, stale backup, partial AI/mail outage, alert routing in non-production sink.
- **Rollback:** log/metric additions are non-breaking; alert thresholds versioned and suppressible.
- **Documentation:** dashboards, alerts, runbooks and sensitive-log policy.
- **Effort:** Medium.
- **Deferred:** full tracing platform if logs/metrics meet current scale.
### P2-4 — Harden build provenance and secret scanning
- **Findings/scope:** JT-017/JT-020; action/image/installer pinning, SDK/locks/hashes, SBOM, container/secret scan, archive fixtures.
- **Dependencies:** approved update cadence and scanner availability.
- **Acceptance criteria:** immutable CI dependencies; reproducible documented toolchain; scans block policy-defined severity; no live credential patterns.
- **Tests:** clean-cache build, intentional canary secret/advisory fixture, SBOM diff review.
- **Rollback:** update pins through reviewed commits; avoid history rewrite without separate approval/coordination.
- **Documentation:** provenance/advisory exception and secret-response policy.
- **Effort:** Medium.
- **Deferred:** enterprise signing/attestation service if not needed yet.
## Phase 3 — architecture and maintainability
### P3-1 — Reduce dual schema ownership incrementally
- **Findings/scope:** JT-019; inventory reconciler operations and move stable schema ownership to EF migrations in small steps.
- **Dependencies:** provider upgrade fixtures and P2-2 restore safety.
- **Acceptance criteria:** every schema mutation has one owner; fresh/upgrade/repair matrices pass on SQLite/MariaDB; startup does no undocumented DDL.
- **Tests:** representative historical snapshots, interrupted migration/restart and malformed-empty legacy cases.
- **Rollback:** additive migrations first, backup-gated deployment, retain scoped legacy repair until telemetry proves removable.
- **Documentation:** schema ownership map and migration runbook.
- **Effort:** Large.
- **Deferred:** wholesale ORM/database rewrite.
### P3-2 — Rebuild the current developer/operator documentation
- **Findings/scope:** JT-018 plus JT-016; frontend README, supported database matrix, architecture/API/env/setup/test/deploy source of truth.
- **Dependencies:** Phase 0–2 behaviour/config decisions to avoid documenting transient state.
- **Acceptance criteria:** unfamiliar developer follows docs from clean clone through build/tests/local start; no CRA/PostgreSQL/stale API claims.
- **Tests:** execute every documented command in CI or scheduled clean environment where practical.
- **Rollback:** documentation-only; retain historical docs under clearly marked archive.
- **Documentation:** this item is the documentation change.
- **Effort:** Medium.
- **Deferred:** generated public API portal unless consumers require it.
## Phase 4 — accessibility, performance and UX
### P4-1 — Fix key accessibility semantics and verify responsive layouts
- **Findings/scope:** JT-015; identified icon buttons, CV cards, public CV width, keyboard/focus/viewport review.
- **Dependencies:** restored browser-control/test capability; approved accessible naming text.
- **Acceptance criteria:** role/name and keyboard parity on core pages; logical focus; no 375/768 overflow; 1440 layout remains readable.
- **Tests:** RTL role/name, axe, Playwright keyboard and 375/768/1440 screenshots; manual focus/contrast/reduced-motion check.
- **Rollback:** semantic attributes/layout adjustments can be individually reverted; no API/data risk.
- **Documentation:** accessibility test checklist and known limitations.
- **Effort:** Medium.
- **Deferred:** formal WCAG certification until manual assistive-technology audit.
### P4-2 — Measure before optimising large-data/admin/mail paths
- **Findings/scope:** JT-021; users/roles N+1, inbox cap/pagination, provider sync, bundle/route transfer, memory/query counts.
- **Dependencies:** representative synthetic large dataset and browser performance tooling.
- **Acceptance criteria:** agreed p95/query/memory/transfer targets; only measured failures changed; results remain complete/paged.
- **Tests:** query-count fixture, pagination boundaries, route bundle budget and non-disruptive sync benchmark.
- **Rollback:** retain old API contract behind compatibility version if pagination changes; compare before/after.
- **Documentation:** measurement method and capacity assumptions.
- **Effort:** Medium.
- **Deferred:** caching, queues or horizontal scaling without evidence.
## Phase 5 — optional enhancements/hardening
### P5-1 — Refine public-CV PDF abuse controls
- **Findings/scope:** JT-023; cache generated public PDF and/or partition client/slug/global limits.
- **Dependencies:** observed abuse/cost and privacy-safe cache invalidation.
- **Acceptance criteria:** one client cannot deny all viewers; total PDF generation remains bounded.
- **Tests:** multi-client same-slug and invalidation/burst cases.
- **Rollback:** revert limiter/caching policy; public slug contract unchanged.
- **Documentation:** public rate/cache behaviour.
- **Effort:** Small.
- **Deferred:** CDN until traffic justifies it.
### P5-2 — Close DNS-rebinding TOCTOU if deployment threat requires it
- **Findings/scope:** JT-024; pin validated public IP/connected peer for HTTP and IMAP while retaining TLS host validation.
- **Dependencies:** deterministic resolver/connect support and CDN/multi-address requirements.
- **Acceptance criteria:** a changed private answer/peer is rejected without breaking valid IPv4/IPv6 failover.
- **Tests:** resolver changes, mixed public/private answers, TLS SNI/certificate and timeout cases.
- **Rollback:** feature/config switch to current strict literal checks if pinning breaks legitimate providers; document residual risk.
- **Documentation:** supported network-resolution behaviour.
- **Effort:** Medium.
- **Deferred:** general outbound proxy/egress firewall unless infrastructure adopts one.
### P5-3 — Sandbox authenticated CV preview if compatibility permits
- **Findings/scope:** JT-025; minimum iframe sandbox and hostile renderer regression corpus.
- **Dependencies:** verify links/fonts/print/export under sandbox.
- **Acceptance criteria:** preview remains functional; hostile markup cannot execute or access parent origin.
- **Tests:** sandbox attribute, script/URL payloads, preview/PDF parity.
- **Rollback:** revert individual sandbox flag only if renderer encoding tests remain and issue is documented.
- **Documentation:** preview trust boundary.
- **Effort:** Small.
- **Deferred:** separate preview origin unless renderer threat changes.
## Recommended first approval slice
Approve **P0-2A — canonical application origin and Host guard** as the first implementation package, with this exact boundary:
1. Parse existing `App:PublicBaseUrl` once and fail Production startup unless it is canonical HTTPS.
2. Replace every `Request.Scheme`/`Request.Host` fallback and older base-URL alias in auth, admin reset, Gmail, Microsoft Graph, billing and follow-up URL creation.
3. Derive and enforce the production application Host allowlist from that origin, retaining only explicit internal health hosts.
4. Derive Production secure-cookie behavior from the canonical origin rather than forwarded request input.
5. Make `APP_PUBLIC_BASE_URL` required in environment/deployment preflight and document the Development/Test localhost rule.
6. Add the hostile Host/forwarded-header, malformed-origin, URL-caller and local/Test regression tests specified under JT-002.
P0-2A has **no database migration and no dependency change**. It must not include Microsoft linking, email/session behavior, Compose/nginx/Traefik changes, or unrelated URL abstractions. Review and commit it independently. Its acceptance gate is that request headers cannot influence any generated external URL, unknown production Hosts are rejected, Production fails fast on an unsafe origin, and existing local tests remain green.
Immediately after P0-2A, implement P0-2B to close repo-defined direct port/proxy exposure before enabling the P0-1B legacy relink or P0-4B email-change flows. Do not activate the currently inert AI/email workers as part of Phase 0.
@@ -0,0 +1,21 @@
# Accessibility code-review evidence
Captured: 2026-08-02
Classification: inspected in code and component tests only. Manual browser/assistive-technology verification was blocked by the missing in-app browser client.
## Confirmed code-level issues
- Icon-only controls lack programmatic names in `CompaniesTable.tsx:139,186`, `Correspondence.tsx:472`, `JobTable.tsx:493,704,745-748`, `SavedViewsMenu.tsx:93,153`, and `CvBuilderPage.tsx:96`. Tooltips or HTML `title` alone are not a reliable accessible name.
- Attachment preview/download/rename/delete buttons use `title` without `aria-label` at `Attachments.tsx:320-330`.
- CV cards are clickable `Paper` elements with pointer styling but no link/button role, `tabIndex`, or keyboard activation at `CvBuilderPage.tsx:91-98`.
- The public CV iframe is fixed at 210mm wide at `PublicCvPage.tsx:53-62`; at 375px and 768px this is a code-supported overflow risk, but visual clipping was not manually verified.
## Positive evidence
- Forms generally use MUI labels and many dialog close/action buttons have explicit `aria-label` values.
- CV editor move/hide controls and checklist controls carry accessible names.
- MUI supplies baseline focus and dialog semantics, though manual focus trapping/return was not checked.
- Public CV iframe has a title and sandbox attribute.
No axe, pa11y, or Lighthouse dependency/configuration is present. No automated accessibility gate runs in CI.
+10
View File
@@ -0,0 +1,10 @@
# AI-001 evidence index
- `../../../verification/ai-001-durable-ai-queue.md`
- `../../../architecture/durable-operations.md`
- `../../verification-log.md` entries V-096 and V-097
- `JobTrackerApi.Tests/AiOperationQueueTests.cs`
- Existing OPS evidence under sibling `ops-001a`, `ops-001b` and `ops-001c` directories
All execution used synthetic local SQLite data and fake handlers. No model, provider, browser or production service was invoked.
+29
View File
@@ -0,0 +1,29 @@
# Browser evidence and blocker
Captured: 2026-08-02
## Genuine running-browser coverage
The repository's own Playwright configuration created an isolated API, Next application, Chromium browser, and temporary database. `npm run test:e2e` passed 4/4:
1. local login establishes an authenticated session and reaches Dashboard;
2. a saved job is created through the reviewed multi-step UI;
3. the Career Workspace shell renders its heading and explanatory copy;
4. an explicitly published synthetic CV renders anonymously and its PDF response is a real `%PDF-` document.
The Career Workspace assertion proves only the shell rendered; it does not prove every downstream panel loaded. Later direct API checks confirmed that CV-list and application-workspace endpoints fail under default SQLite.
## Interactive browser blocker
The required `browser:control-in-app-browser` skill was selected for the broader manual journey, console, keyboard, responsive, and screenshot review. Its mandatory client module was absent from the installed plugin bundle:
`C:/Users/Cesnimda/.codex/plugins/cache/openai-bundled/browser/26.721.81911/scripts/browser-client.mjs`
Import through the required browser runtime failed with `Module not found`. The skill explicitly forbids substituting standalone Playwright or another browser-control implementation when its client is missing, so the interactive review stopped at that boundary.
## Consequences
- No audit screenshots were captured.
- Browser console/network monitoring beyond the passing repository tests was not performed.
- 375px, 768px, and 1440px viewport checks, keyboard-only navigation, modal focus, reduced motion, dark-theme comparison, back/forward, refresh, multi-tab, and throttled-network checks are blocked.
- Code inspection and component tests are labelled as such; they are not represented as manual browser testing.
@@ -0,0 +1 @@
Synthetic attachment for local SEC-008 verification only.
@@ -0,0 +1,30 @@
# Dependency and supply-chain evidence
Captured: 2026-08-02
## Advisory checks
- NuGet: no known vulnerable direct or transitive package was reported.
- npm: two affected package entries (`react-router` and `react-router-dom`) cover four moderate advisories. One narrow redirect issue is fixed in 6.30.4; the remaining audit-suggested resolution is React Router 7.18.2, a breaking major upgrade. No upgrade was attempted.
- Python: `pip-audit` returned 119 records across six installed packages; after deduplicating repeated aliases, the affected counts were `transformers` 21, `torch` 22, `pillow` 17, `pypdf` 35, `python-multipart` 6, and transitive `starlette` 7.
Reachability matters:
- `pypdf` directly parses authenticated user PDF uploads and multiple advisories describe infinite loops, excessive CPU, or memory exhaustion from crafted PDFs.
- Pillow directly opens authenticated image uploads; advisories include decompression bombs and memory-corruption cases. Extension routing is not a content-signature check.
- `python-multipart`/Starlette parse the sidecar upload before the endpoint's eight-megabyte post-read check; several advisories are request-parsing denial of service.
- Many `torch`/`transformers` advisories concern model or checkpoint loading. The application loads a fixed configured model, not a user-supplied model, so those records are not all treated as directly exploitable.
Existing mitigations: authenticated backend upload path, private backend-only AI network, required production service token, eight-megabyte application limit, accepted-extension list, and no host-published sidecar port. Residual risk: containers have no resource limits and the parser handles untrusted bytes in-process.
## Reproducibility and provenance
- npm has `package-lock.json` and uses `npm ci`.
- NuGet has no lockfile; the repository has no `global.json`, so local builds selected SDK 10 while the project targets .NET 9.
- Python top-level requirements are exact pins, but transitive dependencies are not hash-locked.
- Docker base images use mutable tags rather than digests.
- Gitea Actions use mutable major tags for checkout/setup-node, an unpinned remote `dotnet-install.sh`, and a tagged SSH action rather than immutable commit SHAs.
- The AI image upgrades pip/setuptools/wheel during build and downloads the configured Hugging Face model at runtime unless already cached.
- No SBOM generation, package licence gate, container CVE scan, or signed-provenance check is configured.
`dotnet list ... --deprecated` marked xUnit 2.9.2 and its transitive xUnit 2 packages as legacy. This is maintenance information, not a current security defect.
@@ -0,0 +1,8 @@
START TRANSACTION;
DROP TABLE `UserOperations`;
DELETE FROM `__EFMigrationsHistory`
WHERE `MigrationId` = '20260802224646_AddUserOperations';
COMMIT;
@@ -0,0 +1,42 @@
START TRANSACTION;
CREATE TABLE `UserOperations` (
`Id` char(36) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
`OwnerUserId` varchar(255) NOT NULL,
`TaskType` varchar(64) NOT NULL,
`IdempotencyKey` varchar(128) NOT NULL,
`Status` varchar(32) NOT NULL,
`Priority` int NOT NULL,
`EntitlementDecision` varchar(32) NOT NULL,
`PrivacyPolicy` varchar(32) NOT NULL,
`SubjectType` varchar(64) NULL,
`SubjectId` varchar(128) NULL,
`Provider` varchar(128) NULL,
`Model` varchar(128) NULL,
`AttemptCount` int NOT NULL,
`MaxAttempts` int NOT NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`AvailableAtUtc` datetime(6) NOT NULL,
`StartedAtUtc` datetime(6) NULL,
`CompletedAtUtc` datetime(6) NULL,
`DeadlineAtUtc` datetime(6) NULL,
`CancellationRequestedAtUtc` datetime(6) NULL,
`LeaseToken` char(32) CHARACTER SET ascii COLLATE ascii_general_ci NULL,
`LeaseExpiresAtUtc` datetime(6) NULL,
`LastHeartbeatAtUtc` datetime(6) NULL,
`ProgressStage` varchar(64) NULL,
`ProgressPercent` int NULL,
`FailureCategory` varchar(64) NULL,
`FailureMessage` varchar(512) NULL,
`ResultReference` varchar(256) NULL,
CONSTRAINT `PK_UserOperations` PRIMARY KEY (`Id`)
) CHARACTER SET=utf8mb4;
CREATE UNIQUE INDEX `IX_UserOperations_OwnerUserId_TaskType_IdempotencyKey` ON `UserOperations` (`OwnerUserId`, `TaskType`, `IdempotencyKey`);
CREATE INDEX `IX_UserOperations_Status_AvailableAtUtc_Priority` ON `UserOperations` (`Status`, `AvailableAtUtc`, `Priority`);
INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20260802224646_AddUserOperations', '9.0.14');
COMMIT;
@@ -0,0 +1,8 @@
BEGIN TRANSACTION;
DROP TABLE "UserOperations";
DELETE FROM "__EFMigrationsHistory"
WHERE "MigrationId" = '20260802224646_AddUserOperations';
COMMIT;
@@ -0,0 +1,41 @@
BEGIN TRANSACTION;
CREATE TABLE "UserOperations" (
"Id" TEXT NOT NULL CONSTRAINT "PK_UserOperations" PRIMARY KEY,
"OwnerUserId" TEXT NOT NULL,
"TaskType" TEXT NOT NULL,
"IdempotencyKey" TEXT NOT NULL,
"Status" TEXT NOT NULL,
"Priority" INTEGER NOT NULL,
"EntitlementDecision" TEXT NOT NULL,
"PrivacyPolicy" TEXT NOT NULL,
"SubjectType" TEXT NULL,
"SubjectId" TEXT NULL,
"Provider" TEXT NULL,
"Model" TEXT NULL,
"AttemptCount" INTEGER NOT NULL,
"MaxAttempts" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"AvailableAtUtc" TEXT NOT NULL,
"StartedAtUtc" TEXT NULL,
"CompletedAtUtc" TEXT NULL,
"DeadlineAtUtc" TEXT NULL,
"CancellationRequestedAtUtc" TEXT NULL,
"LeaseToken" TEXT NULL,
"LeaseExpiresAtUtc" TEXT NULL,
"LastHeartbeatAtUtc" TEXT NULL,
"ProgressStage" TEXT NULL,
"ProgressPercent" INTEGER NULL,
"FailureCategory" TEXT NULL,
"FailureMessage" TEXT NULL,
"ResultReference" TEXT NULL
);
CREATE UNIQUE INDEX "IX_UserOperations_OwnerUserId_TaskType_IdempotencyKey" ON "UserOperations" ("OwnerUserId", "TaskType", "IdempotencyKey");
CREATE INDEX "IX_UserOperations_Status_AvailableAtUtc_Priority" ON "UserOperations" ("Status", "AvailableAtUtc", "Priority");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260802224646_AddUserOperations', '9.0.14');
COMMIT;
@@ -0,0 +1,8 @@
START TRANSACTION;
DROP TABLE `UserNotifications`;
DELETE FROM `__EFMigrationsHistory`
WHERE `MigrationId` = '20260802225941_AddUserNotifications';
COMMIT;
@@ -0,0 +1,26 @@
START TRANSACTION;
CREATE TABLE `UserNotifications` (
`Id` char(36) CHARACTER SET ascii COLLATE ascii_general_ci NOT NULL,
`OwnerUserId` varchar(255) NOT NULL,
`OperationId` char(36) CHARACTER SET ascii COLLATE ascii_general_ci NULL,
`Kind` varchar(64) NOT NULL,
`Title` varchar(160) NOT NULL,
`Message` varchar(512) NOT NULL,
`LinkPath` varchar(256) NULL,
`CreatedAtUtc` datetime(6) NOT NULL,
`ReadAtUtc` datetime(6) NULL,
`DismissedAtUtc` datetime(6) NULL,
CONSTRAINT `PK_UserNotifications` PRIMARY KEY (`Id`),
CONSTRAINT `FK_UserNotifications_UserOperations_OperationId`
FOREIGN KEY (`OperationId`) REFERENCES `UserOperations` (`Id`) ON DELETE SET NULL
) CHARACTER SET=utf8mb4;
CREATE UNIQUE INDEX `IX_UserNotifications_OperationId` ON `UserNotifications` (`OperationId`);
CREATE INDEX `IX_UserNotifications_OwnerUserId_DismissedAtUtc_ReadAtUtc_Create` ON `UserNotifications` (`OwnerUserId`, `DismissedAtUtc`, `ReadAtUtc`, `CreatedAtUtc`);
INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`)
VALUES ('20260802225941_AddUserNotifications', '9.0.14');
COMMIT;
@@ -0,0 +1,8 @@
BEGIN TRANSACTION;
DROP TABLE "UserNotifications";
DELETE FROM "__EFMigrationsHistory"
WHERE "MigrationId" = '20260802225941_AddUserNotifications';
COMMIT;
@@ -0,0 +1,24 @@
BEGIN TRANSACTION;
CREATE TABLE "UserNotifications" (
"Id" TEXT NOT NULL CONSTRAINT "PK_UserNotifications" PRIMARY KEY,
"OwnerUserId" TEXT NOT NULL,
"OperationId" TEXT NULL,
"Kind" TEXT NOT NULL,
"Title" TEXT NOT NULL,
"Message" TEXT NOT NULL,
"LinkPath" TEXT NULL,
"CreatedAtUtc" TEXT NOT NULL,
"ReadAtUtc" TEXT NULL,
"DismissedAtUtc" TEXT NULL,
CONSTRAINT "FK_UserNotifications_UserOperations_OperationId" FOREIGN KEY ("OperationId") REFERENCES "UserOperations" ("Id") ON DELETE SET NULL
);
CREATE UNIQUE INDEX "IX_UserNotifications_OperationId" ON "UserNotifications" ("OperationId");
CREATE INDEX "IX_UserNotifications_OwnerUserId_DismissedAtUtc_ReadAtUtc_CreatedAtUtc" ON "UserNotifications" ("OwnerUserId", "DismissedAtUtc", "ReadAtUtc", "CreatedAtUtc");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20260802225941_AddUserNotifications', '9.0.14');
COMMIT;
+10
View File
@@ -0,0 +1,10 @@
# POL-001 evidence index
- Focused backend policy/worker/CV/controller slice: 74/74 passed.
- Full backend regression: 568/568 passed.
- Focused frontend AI/usage/job/CV slice: 22/22 passed.
- Full frontend regression: 47 suites and 157 tests passed.
- Frontend production build: passed.
- Patch hygiene: passed, with repository line-ending notices only.
No browser, provider, paid AI, email, production service or production data was used.
+10
View File
@@ -0,0 +1,10 @@
# POL-002 evidence index
- `../../../verification/pol-002-ai-privacy.md` — implementation and verification record.
- `../../../architecture/ai-privacy.md` — enforced configuration and trust-boundary contract.
- `../../verification-log.md` entries V-089 through V-095 — exact commands and outcomes.
- Synthetic-only provider routing tests: `tools/summarizer/tests/test_app.py`.
- Server policy tests: `JobTrackerApi.Tests/AiPrivacyPolicyTests.cs` and `ProEntitlementAuthorizationTests.cs`.
No screenshot was captured because localhost browser access is denied by administrator policy. No real CV, email, credential, provider call or production data was used.
+10
View File
@@ -0,0 +1,10 @@
# PROD-002 evidence index
- Synthetic fixture validation: 1/1 passed.
- Full backend: 569/569 passed.
- Full frontend: 47 suites, 157 tests passed.
- Frontend production build: passed.
- Model/provider calls: none.
- Real user data: none.
Primary evidence: `docs/verification/prod-002-ai-evaluation.md` and `docs/ai/workload-inventory.md`.
@@ -0,0 +1,60 @@
# Repository inventory evidence
Captured: 2026-08-02
## Scope and worktree
- Branch: `release-readiness`, tracking `origin/release-readiness`.
- Pre-existing user changes preserved: deleted tracked `.agent.md`; untracked `AGENTS.md`.
- Audit-created paths: `docs/audits/` only.
- Tracked-file distribution: 301 documentation files, 194 API files, 171 frontend files, 67 API-test files, 13 AI/tool files, 12 scripts, and 6 deployment files.
## Executable components
| Component | Implementation | Responsibility |
|---|---|---|
| Browser client | `job-tracker-ui/` — React 19, TypeScript, MUI, React Router inside a Next.js static-export shell | Public landing/auth pages and authenticated job, career, CV, email, settings, and administration workflows |
| API host | `JobTrackerApi/` — ASP.NET Core / .NET 9 | Authentication, authorization, REST endpoints, application workflows, data/file access, integrations, and hosted services |
| Data layer | EF Core 9; SQLite default or Pomelo MariaDB/MySQL | Identity and tenant-owned job, profile, CV, correspondence, attachment, AI, and workflow state |
| AI sidecar | `tools/summarizer/` — FastAPI, Transformers, OCR/document parsers | Local summaries and extraction; routes selected generation calls to Ollama, Gemini, or Groq |
| Background processing | Seven hosted services in the API process | Backups, rules, reminders, daily export, enrichment, AI readiness probing, and queued CV processing |
| Security fixture tool | `tools/hostile-fixture-db/` | Generates synthetic hostile database fixtures for local authorization testing |
| Delivery | Dockerfiles, Docker Compose, nginx, `deploy/deploy.sh`, Gitea Actions | Builds, health checks, backup-gated deployment, and direct-to-production replacement after CI |
## External boundaries
- Authentication: local ASP.NET Identity/JWT/cookie sessions; Google and Microsoft ID-token exchange/linking; TOTP 2FA.
- Mail: Gmail OAuth/API, Microsoft Graph, IMAP, and SMTP. Audit tests must mock these boundaries.
- Billing and abuse prevention: Stripe hosted flows/webhook and Cloudflare Turnstile.
- Job discovery/import: NAV feed and site-specific URL parsers for Finn, LinkedIn, and Jobbnorge; optional LibreTranslate.
- AI: private sidecar; Ollama locally or Gemini/Groq when configured.
- File/PDF: local data-root storage and headless Chromium PDF export.
## Data ownership and trust boundaries
- `ApplicationUser` is the identity root.
- Tenant entities use `OwnerUserId`; EF global query filters deny access when the current user is absent and scope reads to that owner.
- Several child entities rely on filtered parent navigation or explicit owner predicates rather than their own owner column.
- Public CV is the main anonymous data-release boundary and requires an explicit `IsPublic` flag plus a random slug.
- nginx is intended as the only production ingress to the API; the AI service is on a private backend-only network.
- The API process owns database migrations/reconciliation and all seven workers; the current deployment assumes one API replica.
## Documentation-to-code differences observed during discovery
- `job-tracker-ui/README.md` is obsolete Create React App boilerplate; the frontend now uses Next.js/Jest directly.
- `docs/architecture/current.md` reports a smaller/older controller surface and stale file sizes; the current controller directory contains 29 controller classes plus partials/DTO files.
- Root README API documentation omits substantial implemented surfaces including CV variants, billing, career profiles, AI workspace/history, job discovery, sessions, and several application-workspace APIs.
- `deploy/README.md` recommends PostgreSQL, but the application implements SQLite and MariaDB/MySQL providers only.
- Ignored local `vendor/`, `JobTrackerBackend/`, `.claude/worktrees/`, build outputs, databases, virtual environments, and frontend dependencies remain on disk but are not current tracked application source.
## Manual-audit exclusions
- Generated/build/runtime: `.next/`, `out/`, `build/`, `node_modules/`, `bin/`, `obj/`, local databases, backups, keys, CV artifacts, test results, caches, and virtual environments.
- Ignored historical/local copies: `.claude/worktrees/`, `JobTrackerBackend/`, `vendor/`, `tmp/`.
- Archived documentation under `docs/_archive/` is historical evidence, not the current implementation contract.
- Package lockfiles and EF generated migrations/model snapshot are reviewed for supply-chain and schema implications, not line-by-line as handwritten application logic.
## Unfinished-marker search
No production-code `TODO`, `FIXME`, `HACK`, stub, or `NotImplementedException` was found outside deliberate test doubles, normal placeholder UI text, and a guided-acceptance script template. This does not prove feature completeness; incomplete behaviour is assessed through routes, tests, and browser journeys.
+96
View File
@@ -0,0 +1,96 @@
# Disposable runtime evidence
Captured: 2026-08-02
Environment: local Development configuration, compiled Release API, Next development server, disposable SQLite data root under the current user's temporary directory. No production service, real mailbox, paid AI provider, or personal data was used.
## Default-provider endpoint results
Two synthetic local users were registered. User A owned one company, one job (`Syntetisk utvikler æøå`), one correspondence record, two CV variants, one career profile, and one text attachment. User B had no data.
| Request | User | Result | Evidence |
|---|---|---:|---|
| `GET /api/jobapplications?page=1&pageSize=20` | B | 200, 0 items | Empty-account isolation works. |
| `GET /api/companies` | B | 200, 0 items | Empty-account isolation works. |
| `GET /api/correspondence?jobApplicationId=1` | B | 200, 0 items | User A's message was not disclosed. |
| `GET /api/careerprofile` | B | 404 | User A's profile was not disclosed. |
| `GET /api/jobapplications/1` | A / B | 200 / 404 | Job ownership enforced. |
| `GET /api/attachments/1` | A / B | 200 / 404 | Attachment row and file ownership enforced. |
| `GET /api/cv/variants` | A and empty B | 500 | EF SQLite cannot translate the `DateTimeOffset` ordering. |
| `GET /api/jobapplications/1/workspace` | A | 500 | Same provider-translation family, reached on the core workspace. |
| `GET /api/jobapplications/1/ai/history` | A | 500 | Same provider-translation family. |
| `GET /api/ai/usage` | A | 500 | Same provider-translation family. |
| `GET /api/jobapplications/1/timeline` | A and B | 500 | Ambiguous endpoint selection occurs before ownership evaluation. |
| `GET /api/jobapplications/1/interview-prep` | A and B | 500 | Ambiguous endpoint selection occurs before ownership evaluation. |
| `GET /api/jobapplications/1/checklist` | A | 200 | Sibling workspace endpoint works. |
| `GET /api/jobapplications/1/analysis` | A | 200 | Sibling intelligence endpoint works. |
| `GET /api/jobapplications/1/match` | A | 200 | Sibling intelligence endpoint works. |
The application returned generic 500 problem responses; no secret-bearing exception details were exposed to the client.
## Background-service reproduction
The synthetic job was set to `Applied`, dated 40 days earlier than the audit, and had `Tags` and `ShortSummary` set to null. After a controlled API restart and more than the documented initial delays:
- status remained `Applied`, although the default rules threshold should have marked it ghosted;
- `Tags` remained null despite deterministic `SkillTagger` input containing `C#`;
- `ShortSummary` remained null.
This reproduces the deny-on-null tenant filter in a background scope with no HTTP user. The same query structure exists in rules, follow-up reminders, enrichment, and daily export. The queued CV processor is the counterexample: it deliberately uses `IgnoreQueryFilters()` for cross-tenant worker reads.
## Authentication lifecycle reproductions
### Logout
1. Logged in as User A and copied the issued synthetic cookie jar.
2. Posted logout with the CSRF cookie/header pair.
3. The browser jar received cookie deletion and subsequently got 401.
4. The copied pre-logout cookie still received 200 from `/api/auth/me`.
Result: logout clears client cookies but does not revoke the server-side `UserSession`.
### Email verification
A separate disposable API started with `Auth:RequireEmailVerification=true`, registration enabled, and email delivery disabled.
| Check | Result |
|---|---|
| Register synthetic account | 200 |
| Stored `EmailConfirmed` | `0` |
| Immediate `/api/auth/me` with registration-issued session | 200 |
The same synthetic row was then marked confirmed solely to isolate the email-change path. A normal login followed by `PUT /api/auth/profile` changed the email, returned 204, and left `EmailConfirmed=1`. No verification challenge was required for the new address.
## Backup and restoration rehearsal
The built-in SQLite backup runner created `jobtracker_backup_20260802_184234.db` from the disposable database. A read-only copied snapshot passed `PRAGMA integrity_check` and matched the current database:
| Dataset | Current | Restored copy |
|---|---:|---:|
| Users | 2 | 2 |
| Jobs | 1 | 1 |
| Companies | 1 | 1 |
| Attachment rows | 1 | 1 |
| CV variants | 2 | 2 |
A second isolated API was started from the restored database plus separately copied `Attachments/` and `keys/`. `/health` returned 200; synthetic login, job retrieval, and attachment download all returned 200.
Conclusion: SQLite database snapshotting works. Complete recovery additionally requires file storage, the Data Protection key ring, deployment secrets/configuration, and a documented restore procedure. MariaDB/MySQL backup is intentionally unsupported by the in-process runner and was not available for rehearsal.
## Safe local performance samples
Release API, disposable SQLite database with one job, ten requests after warm-up:
| Endpoint | Mean | p95 | Response bytes |
|---|---:|---:|---:|
| `/health` | 11.3 ms | 43.9 ms | 35 |
| `/api/jobapplications?page=1&pageSize=20` | 17.8 ms | 111.3 ms | 1,547 |
| `/api/companies` | 6.5 ms | 33.3 ms | 255 |
The static export contained 49 JavaScript chunks totalling 2,762,618 uncompressed bytes and one 293-byte CSS file. This aggregate is not an initial-page transfer measurement because Next.js loads route chunks selectively.
No load test was performed. Results do not establish production capacity.
## Cleanup
Listeners on audit ports 3001, 5402, 5403, and 5404 were resolved to their exact audit-owned command lines and stopped. Temporary evidence remains under the disposable local data root; no repository source/configuration was changed.
@@ -0,0 +1,40 @@
# Two-user isolation evidence
Captured: 2026-08-02
Scope: disposable local SQLite environment only. User A and User B used synthetic `@audit.invalid` identities. No production data or credentials were used.
## Results
| Resource or operation | UI level | API direct-ID test | Service/query protection | Result |
|---|---|---|---|---|
| Jobs | Browser blocked | A 200; B 404 for A's job; B list 0 | Explicit owner predicates and global `JobApplication` filter | Pass at API/data-query levels |
| Companies | Browser blocked | B 404 for A's company; B list 0 | Explicit owner predicates and global `Company` filter | Pass at API/data-query levels |
| Correspondence | Browser blocked | B list for A's job returned 0; copied ID returned 404 | Filter through owned `JobApplication` navigation | Pass at API/data-query levels |
| Attachments | Browser blocked | A upload/list/download 200; B job access and copied attachment download 404 | Owned-job query before file operation | Pass at API/data-query levels |
| Career Profile | Browser blocked | B's current-profile request 404 | Current-user lookup plus owner query filter | Pass at API/data-query levels |
| CV variants | Browser blocked | Copied A variant ID returned 404, but B's own list returned 500 | Owner predicate/filter exists; list blocked by SQLite translation | Partial: protection inspected and direct ID passed; list broken |
| Application workspace | Browser blocked | B copied A ID returned 404; A request returned 500 | Owner predicate exists; owner path blocked by SQLite translation | Partial |
| Checklist | Browser blocked | B copied A job returned 404; A 200 | Owner predicate on job/items | Pass at API/data-query levels |
| Timeline | Browser blocked | Both users received 500 | Ambiguous route selection occurs before authorization logic | Blocked by endpoint defect; no exposure observed |
| Interview preparation | Browser blocked | Both users received 500 | Ambiguous route selection occurs before authorization logic | Blocked by endpoint defect; no exposure observed |
| AI results | Browser blocked | History endpoint returned 500 | Owner predicates and query filter exist | Blocked by SQLite translation |
| Settings/session | Browser blocked | B `/auth/me` returned only B | Identity/session-bound current user | Pass at API level |
| Administrative operation | Browser blocked | B request returned 403 | `[Authorize(Roles = "Admin")]` | Pass at API level |
| Email threads/provider data | External providers not connected | Local correspondence passed; provider-specific IDs not live-tested | Owner-scoped connection and correspondence queries inspected | Code-inspected/partially tested |
## Protection layers observed
- UI: protected routes require an authenticated shell, but hiding was not counted as authorization.
- API: controllers use explicit local-auth or admin authorization attributes.
- Service: important reads carry `OwnerUserId` or owned-parent predicates.
- EF: global filters deny on null current user and match owner IDs.
- Database: tenant ownership is primarily enforced in application queries; many owner columns are not foreign keys to `AspNetUsers`, so database constraints alone do not provide tenant isolation.
## Limitations
- The in-app browser control client was missing, so UI navigation as A/B was not performed.
- Timeline/interview and several CV/AI paths failed before an ownership result could be observed.
- No Gmail, Microsoft Graph, IMAP, Stripe, cloud AI, or remote object storage was contacted.
No cross-user disclosure was confirmed in the paths that returned a meaningful result.
+536
View File
@@ -0,0 +1,536 @@
# 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.
+178
View File
@@ -0,0 +1,178 @@
# JobTracker security threat model
Audit date: 2026-08-02
Scope: technical threat model and bounded local security audit. It is not penetration-test certification and did not touch production, real users, mailboxes, payment accounts, or paid AI providers.
## System and trust boundaries
```mermaid
flowchart LR
V[Anonymous visitor] --> N[nginx / static UI]
U[Authenticated browser] --> N
N --> A[ASP.NET Core API]
A --> D[(SQLite or MariaDB/MySQL)]
A --> F[Local data volume: attachments, CVs, exports, keys, backups]
A --> I[Private AI sidecar]
I --> O[Ollama or Gemini/Groq]
A --> G[Google/Gmail]
A --> M[Microsoft identity/Graph]
A --> E[IMAP/SMTP]
A --> S[Stripe]
A --> W[Job-advert/NAV web sources]
S -->|signed webhook| A
B[Hosted workers] --> D
B --> F
B --> I
```
Important boundaries:
1. anonymous public pages/public-CV slugs versus authenticated tenant data;
2. browser cookies/CSRF token versus the API session store;
3. user/administrator roles;
4. tenant-owned EF queries versus background/admin cross-tenant work;
5. untrusted job descriptions, email, files, URLs, and AI output versus parsers/renderers;
6. API versus private sidecar and cloud AI recipients;
7. local database versus separately stored files, keys, and backups;
8. external identity, mailbox, payment, and job-source providers.
## Sensitive assets
- Password hashes, session records, TOTP secrets/recovery codes, trusted devices, provider subjects, email verification/reset tokens.
- OAuth refresh/access tokens for Gmail/Microsoft and IMAP/SMTP credentials.
- Job searches, applications, notes, contacts, deadlines, salary expectations, correspondence and mailbox metadata.
- Career Profile, imported CV text/files, generated variants, public CV slugs, exports and attachments.
- AI prompts/results, usage records, job/profile text sent to a configured provider, and provider API keys.
- Stripe customer/subscription identifiers and webhook secret.
- JWT signing material, Data Protection key ring, SMTP credentials, database credentials, service token, deployment SSH key.
- Backups and logs containing identifiers or derived usage/activity data.
## Roles and privileged operations
| Role | Capabilities | Sensitive operations |
|---|---|---|
| Anonymous visitor | Landing/auth config, registration/reset requests, public CV/PDF by slug, health | Trigger password/verification mail; fetch explicitly public CV |
| Authenticated user | Own jobs/profile/CVs/attachments/correspondence/integrations/AI/settings | Upload/parse files, import URLs, connect mailbox, publish CV, generate/send drafts, change identity/security settings |
| Administrator | User/role/system/audit management | Create/delete users, reset password, SMTP/system settings, cross-user audit data |
| API/hosted service | Cross-component data and file access | Migrate/reconcile schema, backups, exports, rules/reminders/enrichment, provider calls |
| AI sidecar | Parse files and generate text; cloud-provider egress | Consume AI keys, process private prompts/files |
| External provider/webhook | Identity/mail/payment/job data | Assert identity, deliver mailbox/payment state, return untrusted content |
## Entry points
- Public/local auth, Google/Microsoft token exchange/linking, email verification/reset, TOTP challenge/recovery.
- Tenant REST APIs for jobs, companies, profiles, CVs, workspaces, messages, attachments, analytics, settings and billing.
- Public-CV HTML/PDF endpoints.
- Stripe webhook.
- Gmail/Graph OAuth callbacks, IMAP/SMTP settings and provider responses.
- URL job import/NAV discovery and translation.
- Multipart CV/image/document upload and attachment upload.
- AI prompt/context fields and AI sidecar HTTP endpoints.
- Admin APIs, startup configuration/environment, migration reconciler, Docker/CI/deploy scripts.
## Attacker capabilities considered
- Anonymous internet client controlling URL, headers, Host, body, rate and navigation.
- Authenticated malicious tenant user controlling own job text, uploads, AI context and import URLs.
- User attempting to alter/guess another tenant's integer IDs or public slugs.
- Attacker with a copied session cookie.
- Attacker with a valid token issued by another Microsoft tenant for the configured multitenant client.
- Malicious or compromised external provider returning hostile HTML/text/files/redirects.
- Supply-chain compromise of mutable action/image/script/package sources.
- Compromised container attempting lateral movement or secret/egress abuse.
## Highest-risk attack paths
| Path | Execution path and prerequisites | Existing mitigation | Assessment |
|---|---|---|---|
| Microsoft auto-link account takeover | Obtain a valid configured-audience token whose mutable email/preferred-username value matches a victim local account; weak issuer shape accepts it; exchange auto-links by email | Signature, audience, lifetime, Microsoft hostname shape; victim 2FA still challenges | High; code defect confirmed, external exploit not reproduced (JT-001) |
| Password-reset link poisoning | Public base URL blank; arbitrary Host reaches nginx (`server_name _`, forwarded `$host`); request victim reset; victim clicks attacker-host link and discloses token | Email-request rate limit; explicit public URL avoids path; 2FA limits password-only takeover | High; complete code/config path, no real email sent (JT-002) |
| Crafted document resource exhaustion | Authenticated user uploads renamed/malicious PDF/image/multipart; vulnerable pypdf/Pillow/parser path runs in sidecar without container resource limits | Authentication, private network/service token, extension/8 MB limits | High availability risk; known versions and direct parser path, exploit not attempted (JT-006) |
| Cross-user IDOR | Authenticated user guesses A's integer IDs | Explicit controller auth, owner predicates, deny-on-null global filters, owned-parent filters | No disclosure confirmed in meaningful two-user results; some paths blocked by unrelated 500s |
| AI prompt/private-data abuse | Untrusted advert/email/context influences prompt or provider receives broader profile | Prompt guardrail, React Markdown rendering, explicit user generation, attachment `UseForAi`, no automatic application | Residual privacy/injection risk; provider response not adversarially tested (JT-022/JT-025) |
| URL/import SSRF | User supplies private IP or DNS name that changes after validation | http/https-only, loopback/private/reserved address checks, redirects disabled | Direct SSRF mitigated; DNS re-resolution/rebinding remains unverified hardening risk (JT-024) |
## Authentication and session review
Strengths:
- ASP.NET Identity password policy, failed-login lockout, rate-limited auth-email routes, optional Turnstile.
- HttpOnly JWT session cookie plus readable double-submit CSRF cookie/header; unsafe API methods enforce CSRF.
- Server-tracked sessions with expiry/revocation endpoints, trusted devices, TOTP and one-time recovery-code design.
- Login uses generic 401 for missing/wrong/locked accounts; reset request is enumeration-resistant.
Findings:
- `MicrosoftTokenValidator.cs:52-99` sets `ValidateIssuer=false`, checks only hostname/suffix, omits `tid` binding, uses `oid` without tenant namespace, and treats `preferred_username` presence as verified email. `AuthController.cs:298-344` then auto-links by email. Microsoft states that issuer validation mitigates cross-tenant forwarding, multitenant applications must tie issuer to `tid`, and `tid` must be part of the data key; it also states `preferred_username` is mutable and must not drive authorization decisions: [issuer validation](https://learn.microsoft.com/en-us/dotnet/api/microsoft.identitymodel.tokens.tokenvalidationparameters.validateissuer), [multitenant validation](https://learn.microsoft.com/en-us/entra/identity-platform/access-tokens), [claims reference](https://learn.microsoft.com/en-us/entra/identity-platform/id-token-claims-reference). See JT-001.
- `AuthController.cs:158-180` authenticates the newly created unconfirmed user; reproduced with verification required. `AuthController.cs:421-439` changes email directly and leaves confirmation true; reproduced. See JT-007.
- `AuthController.cs:347-355` clears cookies only. A copied pre-logout session remained valid. Password change/reset paths at `AuthController.cs:655-674,723-743` also do not revoke other sessions. See JT-008.
- Registration returns `User already exists` at `AuthController.cs:155-156`, a low-severity enumeration difference from login/reset.
- External validator exception text is returned in 401 responses at `AuthController.cs:288-296`; restrict to a stable public error.
- Recovery-code/trusted-device updates lack explicit concurrency tokens/transactions; concurrent reuse is a lower-confidence risk requiring possession of a valid challenge and code.
## Authorization and tenant isolation
- Controllers are explicitly local-authorized or admin-role-authorized; the API does not rely only on the configurable fallback policy.
- `JobTrackerContext.cs:67-424` applies deny-on-null owner filters to tenant roots and most owned entities. Correspondence/events filter through the owned job.
- Direct User B tests denied User A jobs, companies, correspondence, attachment files, CV ID, workspace/checklist ID, settings and admin operations.
- UI hiding was not counted as authorization.
- The same filters accidentally hide all rows from background scopes with no HTTP user, causing JT-005. Cross-tenant worker/admin operations must use an explicit audited bypass and re-establish owner scope.
- Many tenant rows are not database-FK-linked to `AspNetUsers`; authorization therefore depends on correct application queries and deletion does not cascade (JT-009).
## Input, output, browser and network security
| Area | Evidence-based assessment |
|---|---|
| CSRF | Double-submit cookie/header enforced on unsafe authenticated requests; logout was exercised with CSRF. |
| CORS | Credentialed CORS is tied to configured origins; wildcard-with-credentials is rejected in startup validation. |
| XSS | React text/Markdown nodes avoid raw HTML; CV renderer HTML-encodes profile content and validates URLs. Same-origin `srcDoc` iframes are unsandboxed in two authenticated previews, but current renderer mitigates direct injection (JT-025). |
| SQL injection | EF parameterization dominates; custom startup DDL is static/provider-generated, not request input. No request-controlled raw SQL path found. |
| Command injection | No request-controlled process command found. PDF browser path comes from configuration, not a user field. |
| SSRF | Job import and IMAP reject literal/private/reserved hosts and job redirects are disabled. DNS rebinding between validation and connection is not eliminated (JT-024). |
| Path traversal | Upload names use `Path.GetFileName`, generated stored names and server-selected roots. Public/user IDs do not become arbitrary paths. |
| Open redirect | Application-return routes are generally internal; installed React Router version has moderate redirect advisories (JT-017). |
| File upload | Extension/size/storage-quota checks and generated filenames exist. Magic validation is uneven; sidecar reads whole body before size check and vulnerable parsers handle bytes (JT-006/JT-011). |
| Unsafe email rendering | Stored messages render as text/React content in reviewed UI. Provider content was not live-tested; no `dangerouslySetInnerHTML` path found. |
| Security headers | nginx adds baseline hardening headers and CSP-related configuration was inspected; effectiveness at a real public ingress was not measured. |
| Rate limiting | Auth email/login, public PDF and selected high-cost routes are limited. Public PDF limit is keyed by slug and can be consumed for all viewers of a known CV (JT-023). |
## External-service security
- Google tokens require signature/audience/lifetime and verified-email semantics; explicit link endpoints exist.
- Microsoft identity binding is JT-001.
- Stripe webhook validates signature and then refreshes current subscription state, reducing out-of-order webhook risk.
- AI sidecar is on a private two-member network, not host-published, and production Compose requires a shared token. It still runs as root, has cloud egress/keys, and lacks CPU/memory/PID constraints.
- Gmail/Graph/IMAP connections are owner-scoped. Provider tokens are protected with ASP.NET Data Protection; restoring/moving them requires the original key ring.
- SMTP settings are admin-only and password values are not returned by status APIs.
## Secret exposure review
No secret value is reproduced here.
| Secret type | Location | Exposure risk | Remediation |
|---|---|---|---|
| Expired local JWT artifact | `docs/_archive/artifacts/s06-acceptance/.dev-auth-token.txt:1` | Tracked/historical credential pattern; token is expired and lacks current required session ID | Remove from reachable source history according to repository policy; use redacted fixtures |
| ASP.NET Data Protection keys | Historical `JobTrackerApi/keys/...` and `keys/...` paths | Historical key material may decrypt data protected under the matching ring | Rotation is documented as completed but not independently verified; preserve incident evidence and rotate affected protected data/tokens as needed |
| Production/runtime secrets | Compose `.env` inputs and CI secret store | Values not present in audit output; mutable CI/deploy dependencies can access them | Pin provenance and least-privilege runners/actions; keep values out of repository/logs |
## AI-specific abuse cases
- Prompt injection from job adverts/email/additional context: guardrail says preserve facts and return suggestion only; cannot guarantee instruction hierarchy against a model. Treat output as untrusted and retain human approval.
- Excessive permissions: sidecar can spend cloud key and access provider egress; private network/token help, but resource/egress controls are absent.
- Private-data disclosure: most AI modules send the selected job plus full master-profile text; attachment inclusion is separately controlled. No per-user global AI opt-out or clear recipient/provider data summary is implemented (JT-022).
- Automatic enrichment would send job descriptions without a per-user opt-in if its worker were repaired; currently the worker cannot see tenant rows. Remediation must not silently activate that disclosure.
- AI output is stored append-only with owner ID and is not applied automatically; this is a strong trust-loop control.
## Two-user conclusion
No cross-user disclosure was confirmed. Protection exists at API, service-query, and EF-filter levels for the meaningful results. CV/AI/timeline/interview paths that returned 500 remain unverified rather than passed. Full matrix: `evidence/two-user-isolation.md`.
## Residual and blocked security checks
- No real multitenant Microsoft token, poisoned reset email, malicious parser exploit, DNS-rebinding host, provider webhook, mailbox, or cloud AI request was executed.
- No DAST, aggressive fuzzing, brute-force, load test, container CVE scan, or production ingress/header test.
- Browser manual XSS/keyboard/network observation blocked by missing browser client.
- MariaDB-specific authorization/migration behaviour not executed.
These limitations are reflected in finding confidence and classification; they are not represented as passes.
+74
View File
@@ -0,0 +1,74 @@
# JobTracker user-journey audit
Audit date: 2026-08-02
## Evidence classification
- **Tested in a running browser:** repository Playwright test drove real Chromium against isolated running API/UI services.
- **Tested with mocked services:** backend unit tests or frontend Jest/RTL tests replaced external or HTTP boundaries.
- **Inspected only in code:** execution path was read but not operated end to end.
- **Blocked:** the required browser-control client, safe external provider, or a functioning prerequisite endpoint was unavailable.
- **Not applicable:** the product has no such workflow.
No screenshot references exist: interactive browser control failed before navigation because its required client module was absent. The four repository Playwright tests capture screenshots only on failure and all passed.
## Journey results
Each record includes persona, preconditions, steps, expected/actual results, classification, status, console/network evidence, screenshots, and related findings.
| ID | Journey | Persona / preconditions | Steps and expected result | Actual result | Classification | Status | Console errors / failed requests / screenshot | Findings |
|---|---|---|---|---|---|---|---|---|
| UJ-01 | Landing and public navigation | Logged-out visitor | Open landing; follow public navigation without gaining private access | Landing/routes inspected; public CV opened in real Chromium. Landing navigation itself was not interactively exercised. | Inspected only in code / browser for public CV | Partial | No captured console error; screenshot none | JT-018 |
| UJ-02 | Local sign-in and invalid credentials | Returning local user; isolated Playwright DB | Enter credentials; expect Dashboard and visible identity. Invalid credentials should stay unauthenticated with generic response. | Real Chromium login passed. Generic 401/lockout logic and UI error state pass unit/component tests; invalid login was not manually typed. | Running browser / mocked | Pass for valid login; partial for invalid | No Playwright failure; screenshot none | — |
| UJ-03 | Manual registration and email verification | New synthetic user; isolated API with verification required and email disabled | Register; expect a verification-required state and no authenticated access until confirmation | Registration returned 200, stored `EmailConfirmed=0`, but registration-issued session immediately accessed `/auth/me` (200). | Running API; email mocked/disabled | Fail | Failed expectation at auth lifecycle; no email sent; screenshot none | JT-007 |
| UJ-04 | Sign-out and session expiry | Returning synthetic user with copied pre-logout cookie | Log out; expect current server session/token to be unusable | Browser cookie was cleared (subsequent 401), but copied pre-logout cookie remained accepted (200). Natural expiry was inspected, not waited out. | Running API / code-inspected | Fail for revocation; partial for expiry | No console; requests 204, 401, and copied-session 200 | JT-008 |
| UJ-05 | Password reset/change and email change | Local account; verification-required disposable API | Change email/password or reset; require verification/recovery semantics and revoke compromised sessions | Email changed with 204 and stayed confirmed. Password reset/change revoke no session rows by code inspection. Reset email delivery was not invoked against real SMTP. | Running API for email / mocked+code for passwords | Fail/partial | No real email; screenshot none | JT-007, JT-008, JT-002 |
| UJ-06 | Google/Microsoft sign-in, linking, 2FA and recovery | Synthetic principals/mocked validators only | Validate provider identity, explicit linking, 2FA/recovery, failure handling | Extensive unit tests cover success/failure; no real provider or TOTP browser journey. Microsoft path trusts mutable email-like claims and weak issuer shape for auto-linking. | Mocked services / code-inspected | Partial | External providers intentionally not contacted | JT-001 |
| UJ-07 | Protected routes and unauthorized API | Logged-out visitor and User B | Open private API/route without valid owner/session; expect 401/404/403 | Unauthenticated API returned 401; User B got 404 for A's resources and 403 for admin. UI redirect logic inspected. | Running API / code-inspected UI | Pass at API level | No screenshot | — |
| UJ-08 | First-run empty account and onboarding | User B with no data/incomplete profile | Expect actionable empty state, profile/integration guidance, and resumable setup | B received empty job/company lists and 404 own profile. Onboarding/empty-state components pass mocked tests; no browser keyboard/navigation review. | Running API / mocked UI | Partial | CV list instead returned 500 even for empty B | JT-003, JT-014 |
| UJ-09 | Create a job manually | Returning Playwright user | Add Job → manual details → company/title → skip optional steps → create; expect saved job visible | Passed in real Chromium. Synthetic Unicode/Norwegian job also existed in local API data. | Running browser | Pass | No Playwright failure; screenshot none | — |
| UJ-10 | Edit/delete/move/search/filter/sort/return | Returning user with jobs | Modify a job, move stages, soft-delete/restore, search/filter/sort, navigate away/back, prevent accidental destructive actions | Component and controller tests cover these flows; code uses disabled save states and confirm helpers. Not manually exercised in browser. No idempotency key for repeated submissions. | Mocked services / code-inspected | Partial | No browser console/network capture | JT-014 |
| UJ-11 | Job workspace, notes, deadlines, contacts and follow-ups | User A owns job 1 | Open workspace; expect overview/assets/activity/AI and linked subpanels | `/workspace` returned 500 under default SQLite. `/timeline` and `/interview-prep` returned 500 due duplicate routes; checklist/analysis/match returned 200. | Running API | Fail | Failed requests: workspace/timeline/interview-prep 500 | JT-003, JT-004 |
| UJ-12 | Attachments | User A/B and synthetic text file | Upload/list/download/rename/delete with ownership and file/DB consistency | A upload/list/download passed; B copied file ID returned 404. Rename/delete inspected only. Failure paths can orphan files or desynchronise path/row. | Running API / code-inspected failure paths | Partial | No failed owner request; screenshot none | JT-010 |
| UJ-13 | Import job from URL | Authenticated user; no external site contacted | Validate URL, reject private networks, safely bound fetch, populate draft | Parser/SSRF unit tests pass and direct/private IP checks exist. Real site import blocked; response body is buffered before the four-megabyte check. | Mocked services / code-inspected | Partial | External fetch not performed | JT-011, JT-024 |
| UJ-14 | Career Profile and CV import review | User with synthetic CV/profile | Create/edit profile; import CV; review diffs; accept selected, reject others, edit before acceptance; preserve existing data | Profile/diff/pipeline backend and frontend tests are broad and approval is explicit. No manual browser run. CV run/list endpoints contain default-SQLite failures. | Mocked services / code-inspected; blocked browser | Partial/fail | `GET /api/profile-cv/runs` 500 in local runtime | JT-003, JT-014 |
| UJ-15 | CV variants, edit/reorder/hide/preview/export | User with career data | Create variant; edit/reorder/hide; preview; export; return without lost work | Public synthetic CV creation/publish/render/PDF passed in Chromium. Authenticated variant list returned 500; editor/autosave/DOCX-related paths were component/code-only. | Browser for public PDF / mocked and code-only for editor | Partial/fail | CV list 500; screenshot none | JT-003, JT-015 |
| UJ-16 | Deterministic match and AI assistance | User A with job/profile; cloud/local AI not configured | View match; generate suggestion; accept/reject/edit; handle empty/malformed/delay/failure; never auto-apply | Deterministic match returned 200. Mocked tests show AI results are suggestions/history and user approval is required. Usage/history fail on SQLite; no paid provider invoked. | Running API for match / mocked AI / blocked provider | Partial | `/api/ai/usage` and AI history 500 | JT-003, JT-022 |
| UJ-17 | AI trust and private-data boundary | Synthetic untrusted job/email text | Treat content as untrusted, communicate limitations, restrict payload to relevant job/profile/selected attachments | Prompts include guardrails and labelled source text; Markdown renders as React nodes. AI workspace sends the current job plus full master profile for most modules. No per-user global opt-out/provider-recipient explanation exists. Prompt-injection resilience was code-inspected, not adversarially provider-tested. | Code-inspected / mocked | Partial | External AI intentionally blocked | JT-022, JT-025 |
| UJ-18 | Connect/disconnect email provider | Synthetic provider mocks | Connect/cancel/reject/expire/disconnect safely and show state | Gmail/Graph/IMAP controller/provider/component tests cover mocked cases. No real OAuth or mailbox used. | Mocked services / code-inspected | Partial | No external requests | — |
| UJ-19 | Link/view/draft/cancel/send correspondence | Synthetic messages only | Associate correct job/category; view sent/received; draft/discard; require explicit send; archive/pin/read-later/spam/trash; render safely | Local correspondence ownership passed. Provider imports, category state, and explicit send actions covered by tests/code. No real message sent. Some requested mailbox categories are not implemented as a unified workflow. | Running API for local records / mocked provider / code-only | Partial | No real email; no screenshot | JT-014 |
| UJ-20 | Dashboard and analytics | Empty B and small synthetic A | Verify empty/data KPIs, counts, trends, follow-ups, dates/timezones, drill-down | Analytics services/tests cover calculations, but no manual browser KPI comparison against substantial data. One-job local dataset is insufficient for accuracy claims. | Mocked/code-inspected | Partial | Browser blocked; no screenshot | JT-014 |
| UJ-21 | Settings/preferences/AI controls | Authenticated user | Persist real preferences; enable/disable AI; select/understand provider/privacy; show usage | Theme/language/table settings persist client-side. Notification checkboxes only update localStorage and do not govern server reminders. AI usage returns 500 on SQLite; no global per-user AI disable/provider choice. | Code-inspected / running API for usage | Fail/partial | AI usage 500 | JT-003, JT-012, JT-022 |
| UJ-22 | Data export and account deletion | Authenticated user/admin | Export all user data; confirm irreversible deletion; remove DB/files/tokens/backups as documented | No self-service account deletion. Admin delete removes only Identity user. Downloadable backup is app-key-encrypted and omits career/CV/AI/provider/session data and file bytes. | Code-inspected | Fail | Not irreversibly executed | JT-009 |
| UJ-23 | Administrator | Normal User B; admin implementation present | Normal user must be denied; admin can safely manage users/system/audit | B got 403. Admin browser workflows and destructive admin delete were not run. | Running API for denial / code-inspected admin | Partial | 403 as expected | JT-009, JT-014 |
| UJ-24 | Edge cases and failure states | Synthetic users/data | Empty/invalid/long/Unicode, duplicates, double-click, refresh/back, tabs, slow/interrupted network, missing records, concurrent edits, unsupported/oversized uploads | Validation and many failure paths have tests; Unicode job stored. Unsupported/oversized upload guards inspected. Multi-tab, throttling, refresh mid-save, concurrent edit and double-click were not manually exercised; no optimistic concurrency tokens are evident. | Mocked/code-inspected; browser blocked | Partial | No screenshots/console; route failures above | JT-010, JT-011, JT-014 |
| UJ-25 | Responsive and accessibility | Keyboard-only and 375/768/1440px users | No clipping; readable contrast; named controls; visible focus; modal focus; usable tables/boards/previews/themes | Manual checks blocked. Code confirms unnamed icon buttons, keyboard-inaccessible CV cards, and fixed 210mm public-CV iframe. Positive MUI labels/dialog semantics also observed. | Inspected only in code / blocked browser | Fail for confirmed semantics; blocked visually | Screenshot none | JT-015 |
| UJ-26 | Two-user isolation | Disposable User A and B | B must not access A by UI or copied/guessed IDs | No disclosure in meaningful API results; several CV/AI/workspace routes blocked by 500s. UI-level navigation not run. | Running API / code-inspected UI | Pass/partial | Detailed matrix in evidence; screenshot none | JT-003, JT-004 |
## Persona coverage
| Persona | Coverage |
|---|---|
| Logged-out visitor | Browser public CV; unauthorized API; public route code inspection |
| New user | Registration/verification running API; onboarding mocked/code-only |
| Returning user | Real Chromium login and job creation |
| User with no data | User B running API; UI empty states mocked |
| User with substantial synthetic data | Blocked; only small disposable dataset used |
| User with incomplete profile | User B/API plus mocked UI |
| User encountering API failures | Running API failures captured; UI display mostly mocked |
| User with expired session | Natural expiry not waited; copied-session/logout behaviour tested |
| Keyboard-only/mobile-width user | Blocked; code inspection only |
| Administrator | Normal-user denial tested; admin UI blocked |
## Most serious journey failures
1. Default SQLite breaks CV lists/runs, AI history/usage, and the application workspace.
2. Timeline and interview-prep URLs are ambiguous and always fail before user logic.
3. Registration grants an authenticated session to an unverified address; profile email changes remain confirmed.
4. Logout does not revoke the server session represented by a copied cookie.
5. Rules, enrichment, reminders, and scheduled exports cannot see tenant rows in background scopes.
6. Notification preferences and full data-deletion/export expectations are not enforced by server behaviour.
## Browser limitation
The exact browser-plugin blocker and the four genuine Chromium workflows are recorded in `evidence/browser-evidence.md`. No other workflow is claimed as browser-tested.
+126
View File
@@ -0,0 +1,126 @@
# JobTracker verification log
Audit date: 2026-08-02
Only non-destructive commands are run. Commands that restore dependencies may populate local caches or existing build-output directories but do not modify declared dependencies or application source.
| ID | Exact command | Directory | Purpose | Result | Relevant errors or warnings | Failure classification |
|---|---|---|---|---|---|---|
| V-001 | `dotnet --info` | Repository root | Capture .NET toolchain | PASS — SDK 10.0.201; SDK 9.0.200 also installed; target runtime 9.0.2 present | No `global.json`; normal builds therefore selected SDK 10 | N/A |
| V-002 | `node --version; npm --version` | Repository root | Capture frontend toolchain | PASS — Node 22.23.1, npm 10.9.8 | CI/container use Node 20 | N/A |
| V-003 | `python --version; tools\\summarizer\\.venv\\Scripts\\python.exe --version` | Repository root | Capture Python toolchain | PASS — Python 3.12.3 for both | Docker uses floating `python:3.11` | N/A |
| V-004 | `docker version --format '{{.Client.Version}} client / {{.Server.Version}} server'; docker compose version` | Repository root | Capture container tooling | PASS — Docker 29.6.1; Compose 5.2.0 | None | N/A |
| V-005 | `dotnet restore "Job tracker.sln"` | Repository root | Restore declared NuGet dependencies | PASS — all projects up to date | Package cache/`obj` may be refreshed | N/A |
| V-006 | `npm ci --dry-run --ignore-scripts --no-audit --no-fund` | `job-tracker-ui` | Validate npm lock/install plan without replacing current `node_modules` | PASS — dry run resolved the lockfile | Reported 59 platform/optional packages it would add | N/A |
| V-007 | `.\\.venv\\Scripts\\python.exe -m pip install --dry-run -r requirements-dev.txt` | `tools/summarizer` | Validate Python requirement resolution without changes | PASS — runtime requirements satisfied; declared pytest would be restored | Detected pytest version drift | N/A |
| V-008 | `.\\.venv\\Scripts\\python.exe -m pip install -r requirements-dev.txt` | `tools/summarizer` | Restore the declared local test dependency | PASS — project-local ignored venv restored from pytest 9.1.1 to declared 8.3.5 | Local ignored venv changed; no manifest/source change | N/A |
| V-009 | `dotnet build "Job tracker.sln" --configuration Release --no-restore` | Repository root | Release build and compiler/static-analysis baseline | PASS — 0 warnings, 0 errors | None | N/A |
| V-010 | `dotnet test JobTrackerApi.Tests\\JobTrackerApi.Tests.csproj --configuration Release --no-build -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1` | Repository root | Full backend suite, deterministic order | PASS — 462/462, 0 skipped | None | N/A |
| V-011 | `npx tsc --noEmit` | `job-tracker-ui` | Standalone strict TypeScript check including test sources | FAIL | `src/nginx-config.test.ts:7` uses regex `s` flag while target is ES2017 | Application/tooling-related |
| V-012 | `npm test -- --watchAll=false --runInBand` | `job-tracker-ui` | Full frontend Jest/RTL suite | PASS — 43 suites, 148 tests | None | N/A |
| V-013 | `npm run build` | `job-tracker-ui` | Production Next.js static-export build | PASS | Build's TypeScript pass did not catch the failing test-source check | N/A |
| V-014 | `.\\.venv\\Scripts\\python.exe -m pytest -q` | `tools/summarizer` | AI-sidecar test suite | PASS — 17 tests | Five SWIG deprecation warnings | N/A |
| V-015 | `dotnet format "Job tracker.sln" --verify-no-changes --no-restore` | Repository root | Formatting check without rewriting | FAIL — 1,301 whitespace diagnostics across 16 files | Formatting is not enforced in CI | Application/tooling-related |
| V-016 | `docker compose config --no-interpolate --quiet` | Repository root | Validate Compose structure without reading/interpolating secret values | PASS | None | N/A |
| V-017 | `$env:AI_SERVICE_TOKEN='audit-placeholder-not-a-secret'; $env:AUTH_JWT_KEY='audit-placeholder-not-a-secret'; docker compose --env-file .env.example config --quiet` | Repository root | Validate full Compose interpolation using safe placeholders | PASS | External network existence is not checked by `config` | N/A |
| V-018 | `dotnet ef migrations list --no-connect --no-build --configuration Release` | `JobTrackerApi` | List migrations without connecting to or changing a database | PASS — 19 migrations listed | Applied status intentionally unavailable under `--no-connect` | N/A |
| V-019 | `dotnet ef migrations has-pending-model-changes --no-build --configuration Release` | `JobTrackerApi` | Compare current EF model with snapshot without applying migrations | PASS — no pending model changes | Design-time host initialization only | N/A |
| V-020 | `dotnet list "Job tracker.sln" package --vulnerable --include-transitive` | Repository root | NuGet advisory audit | PASS — no known vulnerable packages in either project | Queried NuGet.org | N/A |
| V-021 | `npm audit --audit-level=low` | `job-tracker-ui` | npm production and development advisory audit | FAIL — two moderate vulnerabilities | Documented React Router redirect/SSR advisories; suggested fix is a breaking major change | Application/supply-chain-related |
| V-022 | `pipx run pip-audit -r requirements.txt` | `tools/summarizer` | Python advisory audit in an isolated audit-tool environment | FAIL — 119 advisory records affecting 6 packages | `transformers`, `torch`, `pillow`, `pypdf`, `python-multipart`, and transitive `starlette`; applicability/severity requires path-level assessment | Application/supply-chain-related |
| V-023 | `docker build --check --file JobTrackerApi\\Dockerfile .` | Repository root | Backend Dockerfile static check | PASS — no warnings | Loaded base-image metadata only | N/A |
| V-024 | `docker build --check --file Dockerfile .` | `job-tracker-ui` | Frontend Dockerfile static check | PASS — no warnings | Loaded base-image metadata only | N/A |
| V-025 | `docker build --check --file Dockerfile .` | `tools/summarizer` | AI Dockerfile static check | PASS — no warnings | Loaded base-image metadata only | N/A |
| V-026 | `npm run test:e2e` | `job-tracker-ui` | Isolated Chromium browser smoke suite | PASS — 4/4 | Covered login, saved-job create, Career Workspace load, anonymous public CV/PDF | N/A |
| V-027 | `git status --short --branch` | Repository root | Confirm audit preserved source worktree | PASS | Only pre-existing `.agent.md`/`AGENTS.md` plus `docs/audits/` are visible | N/A |
| V-028 | `dotnet list "Job tracker.sln" package --deprecated --include-transitive` | Repository root | Identify deprecated NuGet packages | WARN — API none; test project xUnit 2.9.2/transitives marked legacy | Migration to xUnit v3 is available but not required for this audit | Maintenance |
| V-029 | `npm outdated --json` | `job-tracker-ui` | Inventory version drift without changing packages | WARN — exited 1 because updates exist | React Router 7, MUI 9, testing-library, Node types, TypeScript, and web-vitals include major updates | Maintenance |
| V-030 | `curl.exe -sS -o NUL -w '%{http_code} %{time_total}' http://127.0.0.1:5402/health` and equivalent frontend request | Repository root | Verify disposable API/UI startup | PASS — API and frontend returned 200 | First development responses were about 2.2 s and 2.5 s; not production startup measurements | N/A |
| V-031 | Bounded `curl.exe -b <synthetic-cookie-jar> -w '%{http_code}'` requests for copied User A job/company/correspondence/attachment/CV/workspace IDs | Repository root | Direct-ID two-user isolation test | PASS/PARTIAL — meaningful job/company/message/attachment/profile/checklist results denied B; CV/AI/workspace/timeline/interview paths partly blocked by 500 defects | Exact status matrix in `evidence/two-user-isolation.md` | Application blockers on partial paths |
| V-032 | Authenticated `curl.exe` requests to `/api/cv/variants`, `/api/profile-cv/runs`, `/api/jobapplications/1/ai/history`, `/api/ai/usage`, `/api/jobapplications/1/workspace`, `/timeline`, and `/interview-prep` | Repository root | Exercise default SQLite career/application workspace APIs | FAIL — listed DateTimeOffset paths and the two ambiguous routes returned 500 | Sibling checklist/analysis/match paths returned 200 | Application-related |
| V-033 | Reset synthetic job fields in disposable SQLite; restart Release API; query row after hosted-service initial delays | Repository root / `JobTrackerApi` | Verify rules/enrichment workers see tenant data | FAIL — old Applied status and null tags/summary remained | Runtime agrees with deny-on-null query-filter execution path | Application-related |
| V-034 | Copy built-in SQLite backup to a new disposable restore path; `PRAGMA integrity_check`; start a second API with restored DB plus copied `Attachments/` and `keys/`; request `/health`, login, job, and attachment | Repository root / `JobTrackerApi` | Safe restoration rehearsal | PASS/PARTIAL — DB integrity/counts and restored application/file access passed | Complete recovery requires files/keys/config outside DB; MariaDB unavailable | Environmental/provider limitation |
| V-035 | Login; copy synthetic cookie jar; POST `/api/auth/logout` with CSRF header; request `/api/auth/me` with cleared and copied jars | Repository root | Verify logout revocation semantics | FAIL — cleared jar 401, copied pre-logout session 200 | Server session was not revoked | Application-related |
| V-036 | Start isolated API with `Auth__RequireEmailVerification=true`; register synthetic user; query stored `EmailConfirmed`; request `/api/auth/me` | Repository root / `JobTrackerApi` | Verify initial verification gate | FAIL — register 200, stored confirmation false, immediate authenticated request 200 | Email sender disabled; no mail sent | Application-related |
| V-037 | Mark only the disposable account confirmed; login; `PUT /api/auth/profile` with a new synthetic email; query identity row | Repository root | Verify email-change verification lifecycle | FAIL — update 204 and new address remained confirmed | No verification challenge | Application-related |
| V-038 | Ten warm `curl.exe` samples for `/health`, paged jobs, and companies; compute mean/p95 | Repository root | Safe local API timing baseline | PASS — 11.3/17.8/6.5 ms means respectively | One-row SQLite dataset; no capacity claim | N/A |
| V-039 | `Get-ChildItem job-tracker-ui\out\_next\static -Recurse -File` and sum `.js`/`.css` lengths | Repository root | Static-export size inventory | PASS — 49 JS chunks, 2,762,618 uncompressed bytes; CSS 293 bytes | Aggregate is not initial-route transfer size | N/A |
| V-040 | Import mandatory `browser-client.mjs` through the skill-required browser runtime | Browser skill runtime | Start interactive browser audit | BLOCKED — module absent from installed plugin bundle | Skill forbids fallback browser automation; no screenshots/manual viewport checks | Environmental/plugin packaging |
| V-041 | `pipx run pip-audit -r requirements.txt -f json` with local summary by package/unique advisory/fix version | `tools/summarizer` | Sceptically validate Python advisory reachability | FAIL — active PDF/image/multipart parsers have numerous crafted-input DoS/memory advisories | Model-loading advisories separated from user-upload reachability | Application/supply-chain-related |
| V-042 | `npm audit --json` with title/range/fix extraction | `job-tracker-ui` | Validate npm advisory scope | FAIL — four moderate advisory entries across two installed packages | Audit fix requires React Router 7.18.2 major for remaining issues | Application/supply-chain-related |
| V-043 | Resolve listeners with `Get-NetTCPConnection`; validate command lines with `Get-CimInstance`; stop exact PIDs; re-run `git status --short --branch` | Repository root | Clean up audit runtime and reconfirm source preservation | PASS — audit ports closed; only pre-existing and audit-report changes visible | Temporary disposable evidence retained outside repository | N/A |
| V-044 | `dotnet test JobTrackerApi.Tests\JobTrackerApi.Tests.csproj --no-restore --filter "FullyQualifiedName~AuthAndSystemControllerTests\|FullyQualifiedName~AuthSessionRevocationTests"` | Repository root | SEC-005B focused registration/email/session checks | PASS — 35/35 | None | N/A |
| V-045 | `dotnet test JobTrackerApi.Tests\JobTrackerApi.Tests.csproj --no-restore` | Repository root | Full backend regression after SEC-005B | PASS — 501/501 | None | N/A |
| V-046 | `npm run build`; `npm test -- --runInBand` | `job-tracker-ui` | Frontend build and full regression after registration/profile/confirmation UI | PASS — build; 43 suites and 151/151 tests | First regression run exposed a stale nginx filename/alias test from SEC-002; test was corrected to the deployed template contract and the complete rerun passed | Application test-maintenance issue resolved |
| V-047 | `dotnet ef migrations script 20260731115022_AddStripeBillingState 20260802205800_AddPendingEmailChange --project JobTrackerApi\JobTrackerApi.csproj --startup-project JobTrackerApi\JobTrackerApi.csproj --no-build` with SQLite and MariaDB design-time configuration; `dotnet ef database update ...` against disposable SQLite | Repository root | Validate SEC-005B migration SQL and safe upgrade | PASS/PARTIAL — provider SQL and upgrade pass; fresh empty migration chain fails before SEC-005B | Pre-existing `AddJobEntityAndProspectStages` expects missing `LastReminderEmailSentAt`; MariaDB execution unavailable | Application CORE-001 / provider limitation |
| V-048 | Isolated `dotnet run --no-build --no-launch-profile --project JobTrackerApi\JobTrackerApi.csproj --urls http://127.0.0.1:5302`; synthetic `Invoke-WebRequest` registration/login with email disabled | Repository root | Runtime verification-required registration boundary | PASS — register 202/typed flag/no `Set-Cookie`/zero cookies; login 403 `email_not_verified`/zero cookies | Synthetic disposable account only; no email sent | N/A |
| V-049 | Browser skill runtime `getForUrl("http://localhost:3100/register")`, new tab and navigation | In-app browser | Real browser registration/profile verification | BLOCKED before navigation | Administrator policy check was unavailable and denied localhost; no browser claim or screenshot made | Environmental/browser policy |
| V-050 | `Get-CimInstance Win32_Process ...`; `Stop-Process -Id 43004,32708`; listener recheck | Repository root | Stop exact isolated API/UI child processes | PASS — 5302/3100 listeners closed; pre-existing Docker services untouched | Recursive cleanup of the verified temp runtime directory was rejected by execution policy | Environmental cleanup limitation |
| V-051 | SEC-004 focused/full backend/frontend tests and dual-provider migration scripts | Repository root | Canonical Microsoft identity regression and migration verification | PASS/PARTIAL — 34 focused, 507 backend, 152 frontend; SQLite upgrade/unique rehearsal and SQLite/MariaDB SQL pass | No real Microsoft token/account, MariaDB execution, browser or production inventory | Provider/environment limitation |
| V-052 | `dotnet test ... --filter "FullyQualifiedName~CvBuilderTests|...|FullyQualifiedName~ApplicationAssetsTests"` | Repository root | CORE-001 focused affected-service regression | PASS — 81/81 | Original audit runtime remains the pre-fix reproduction | N/A |
| V-053 | `dotnet test ... --filter "FullyQualifiedName~SqliteDateTimeOffsetCompatibilityTests"` | Repository root | Execute CORE-001 against real SQLite and generate MariaDB/Pomelo ordering/range SQL | PASS — 3/3; owner-scoped ordering, range, usage, workspace, artifact and isolation paths pass | MariaDB SQL generation only; no server connection | Provider limitation |
| V-054 | `dotnet test JobTrackerApi.Tests\JobTrackerApi.Tests.csproj --no-restore`; `git diff --check` | Repository root | CORE-001 full backend regression and patch hygiene | PASS — 509/509; diff check clean apart from line-ending notices | None | N/A |
| V-055 | Isolated `dotnet run --no-launch-profile ... --environment=Development -- --urls=http://127.0.0.1:5303`; synthetic registration/login and HTTP matrix | Repository root | Fresh default-SQLite startup and affected API/runtime isolation | PASS — empty variants/runs/history/usage 200, missing workspace 404, owner workspace 200, User B workspace 404/variants empty | Browser and MariaDB unavailable; no external calls or real data | Environmental/provider limitation |
| V-056 | Resolve listener with `Get-NetTCPConnection`; inspect exact process; `Stop-Process`; listener recheck | Repository root | Stop CORE-001 isolated API safely | PASS — exact `JobTrackerApi` PID 15400 stopped and port 5303 closed | Exact nested disposable data cleanup rejected by execution policy | Environmental cleanup limitation |
| V-057 | `dotnet test ... --filter "FullyQualifiedName~RouteUniquenessTests|...ApplicationIntelligenceTests|...InterviewPrepTests|...InterviewPrepPersistenceTests"` | Repository root | CORE-002 route uniqueness and focused behavior | PASS — 31/31 | None | N/A |
| V-058 | `npm test -- --runInBand src/application-route-contracts.test.ts src/application-intelligence.test.tsx src/interview-prep.test.tsx` | `job-tracker-ui` | Verify distinct frontend timeline/board/brief contracts | PASS — 21/21 | Static brief-route contract complements existing rendered board/timeline tests | N/A |
| V-059 | Full backend and frontend suites; frontend production build | Repository root / `job-tracker-ui` | CORE-002 regression | PASS — backend 511/511; frontend 45 suites/153 tests; build passes | Browser unavailable | Environmental browser limitation |
| V-060 | Isolated SQLite API on 5304; synthetic owner/User B/anonymous GET matrix for timeline, interview board and brief; exact listener shutdown | Repository root | Prove ambiguity removal and authorization at runtime | PASS — each owner 200, other user 404, anonymous 401; exact PID 23904 stopped and port closed | No browser, MariaDB or production smoke | Environmental/provider limitation |
| V-061 | `dotnet test ... --filter "FullyQualifiedName~AttachmentConsistencyTests|FullyQualifiedName~AttachmentFlagsRecomputeTests|FullyQualifiedName~AttachmentsControllerTests"` | Repository root | SEC-008 failure-boundary and storage-invariant regression | PASS — 20/20 | Symlink creation unavailable on this host | Environmental capability limitation |
| V-062 | `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore`; `git diff --check` | Repository root | SEC-008 full backend regression and patch hygiene | PASS — 525/525; no whitespace errors | Repository line-ending notices only | N/A |
| V-063 | Isolated SQLite API on 5305; synthetic User A upload/list/download/rename/delete and User B direct-ID denial; exact listener shutdown | Repository root | SEC-008 runtime and tenant isolation | PASS — owner operations 200/204, User B 404, final owner list empty; exact PID 4592 stopped and port closed | Browser, MariaDB and production inventory unavailable | Environmental/provider limitation |
| V-064 | Disposable local symbolic-link capability probe using .NET filesystem APIs | System temporary directory | Determine whether child reparse escape can be executed safely | BLOCKED — host denied symbolic-link creation with `RuntimeException`; temporary directories removed | Refusal branch code-inspected; traversal/outside-root test passes | Environmental capability limitation |
| V-065 | `dotnet test ... --filter "FullyQualifiedName~BackgroundWorkerTenantTests|FullyQualifiedName~CurrentUserIdLiveEvaluationTests|FullyQualifiedName~RulesEngineTests"` | Repository root | BG-001 owner scope, switches and fake side-effect integration | PASS — 9/9 after final trust-boundary assertion | No real email/AI invoked | N/A |
| V-066 | `dotnet test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --no-restore` | Repository root | BG-001 full backend regression | PASS — 532/532 after final trust-boundary test | None | N/A |
| V-067 | `docker compose config --quiet`; `git diff --check` | Repository root | Default-off production configuration and patch hygiene | PASS — Compose valid; no whitespace errors | Unset optional/local environment warnings and line-ending notices only | N/A |
| V-068 | Isolated API on 5306 with disposable SQLite data and all four worker switches false; health/no-export check; exact PID shutdown | Repository root | BG-001 safe startup without worker side effects | PASS — health 200, no export directory, exact PID 44980 stopped, port closed | No browser, production canary, SMTP or AI provider | Environmental/provider limitation |
| V-069 | `dotnet test ... --filter "FullyQualifiedName~UserOperationStoreTests"`; full backend suite | Repository root | OPS-001A state, concurrency, owner and regression checks | PASS — focused 7/7; full 539/539 | No handler/UI yet | N/A |
| V-070 | `dotnet ef migrations has-pending-model-changes`; generated SQLite/MariaDB migration up/down scripts | Repository root | OPS-001A model/migration parity | PASS — snapshot current; both providers create/drop; MariaDB has bounded types/eight `datetime(6)` and no unbounded text | MariaDB generation only | Provider limitation |
| V-071 | Disposable existing SQLite `database update`, downgrade and re-upgrade | Repository root | OPS-001A upgrade/rollback rehearsal | PASS | Synthetic local database only | N/A |
| V-072 | Fresh isolated application startup on 5307; health; EF update no-op; exact process shutdown | Repository root | Reconciler/migration startup order with EF-only new table | PASS — health 200; database already current; exact PID 37276 stopped and port closed | No MariaDB/production runtime | Provider limitation |
| V-073 | `dotnet test JobTrackerApi.Tests\JobTrackerApi.Tests.csproj --no-restore --filter FullyQualifiedName~UserOperationStoreTests` | Repository root | OPS-001B terminal notification, owner-state and rollback checks | PASS — 9/9 | Real file-backed SQLite; no email/worker | N/A |
| V-074 | `dotnet ef migrations script 20260802224646_AddUserOperations 20260802225941_AddUserNotifications ...` and reverse for SQLite/MariaDB | Repository root | OPS-001B provider-safe up/down SQL | PASS after correction — bounded MariaDB/SQLite create/drop scripts retained | First MariaDB attempt used the wrong connection-string key and emitted SQLite SQL; invalid output was overwritten and not treated as evidence; MariaDB not executed | Verification setup corrected / provider limitation |
| V-075 | Disposable SQLite notification upgrade, downgrade and re-upgrade; `dotnet ef migrations has-pending-model-changes ... --no-build` | Repository root | OPS-001B safe additive migration and model parity | PASS — all transitions succeeded; no pending model changes | Synthetic copy of OPS-001A evidence database only | N/A |
| V-076 | `dotnet test JobTrackerApi.Tests\JobTrackerApi.Tests.csproj --no-restore`; `git diff --check` | Repository root | OPS-001B backend regression and patch hygiene | PASS — 541/541; no whitespace errors | Line-ending notices only | N/A |
| V-077 | Focused operation controller/store tests; full backend suite | Repository root | OPS-001C owner APIs, safe DTOs, mutations and regressions | PASS — 12/12 focused; 544/544 full | Direct controller tests use real SQLite | N/A |
| V-078 | Focused operations/shell Jest; full Jest; `npm run build` | `job-tracker-ui` | OPS-001C states, action lock, bell accessibility, regression and TypeScript build | PASS — 3/3 focused; 47/47 suites and 156/156 full; build pass | First full npm invocation used repository root and failed for missing root `package.json`; corrected working directory passed | Verification command corrected |
| V-079 | Isolated API on 5310; two disposable users; synthetic SQLite operation/notification rows; authenticated/anonymous direct HTTP matrix | Repository root | OPS-001C runtime authorization, CSRF and lifecycle | PASS — owner detail/cancel/read/dismiss/retry 200/204; four copied cross-owner paths 404; anonymous 401; listener stopped | Initial launch inherited local connection; exact synthetic users/sessions were removed with zero-count proof. Lowercase raw GUID seed was replaced before the passing matrix | Verification setup corrected |
| V-080 | Evidence tree/key inspection; exact generated Data Protection key deletion; listener and local synthetic-account count checks | Repository root | Prevent secret/test-data leakage and confirm cleanup | PASS — key absent; no 5308/5309/5310 listeners; local pre-existing DB has zero synthetic accounts | Disposable synthetic SQLite evidence/backups retained | N/A |
| V-081 | `rg`/bounded `Get-Content` inventory of `AccountPlans`, billing/auth DTOs, every `ISummarizerService` call, controller route, worker and frontend caller | Repository root | Trace POL-001 end to end before enforcement | PASS — all current model-call paths classified in `pol-001-free-pro-entitlements.md` | Usage accounting is complete only for AI Workspace; landing-page claims remain PRODUCT-001 | Application gap/dependency |
| V-082 | `dotnet build JobTrackerApi.sln --no-restore`; `npm run build` | Repository root | Initial POL-001 build attempt | FAIL — solution filename and npm working directory were wrong | Corrected immediately to the actual project/UI paths; no files or dependencies changed | Command/operator-related |
| V-083 | `dotnet build JobTrackerApi/JobTrackerApi.csproj --no-restore`; `npm run build` | Repository root / `job-tracker-ui` | Compile backend and production frontend after policy/UI changes | PASS — backend 0 warnings/errors; frontend TypeScript/static build passed | Frontend build repeated after final changes in V-086 | N/A |
| V-084 | `dotnet test ... --filter "...AccountPlansTests|...ProEntitlementAuthorizationTests|...BackgroundWorkerTenantTests|...ProfileCvControllerTests|...JobApplicationsEndpointBehaviorTests"` | Repository root | Free/Pro/Admin/downgrade, stable 403, worker recheck and core behavior | PASS — 74/74 final | Interim runs exposed a missing test `using`, absent role services and an outdated SQLite expectation; fixtures were corrected without weakening behavior | Test-fixture maintenance resolved |
| V-085 | `npm test -- --runInBand ai-workspace-panel.test.tsx ai-usage-card.test.tsx job-details-generated-drafts.test.tsx profile-page.test.tsx quick-capture.test.tsx` | `job-tracker-ui` | Locked state, no false generation, usage and unaffected job/CV flows | PASS — 5 suites, 22/22 tests | Component tests; not a browser claim | N/A |
| V-086 | Full backend `dotnet test`; full frontend `npm test -- --runInBand`; `npm run build`; `git diff --check` | Repository root / `job-tracker-ui` | POL-001 regression and patch hygiene | PASS — backend 568/568; frontend 47/47 suites, 157/157; build passed; no whitespace errors | First backend full run had one outdated direct-controller expectation; corrected to Pro because the test targets SQLite aggregation, then full rerun passed. Line-ending notices only | Test expectation resolved |
| V-087 | Bounded source/route/provider inventory plus `dotnet test ... --filter "FullyQualifiedName~AiEvaluationFixtureTests"` | Repository root | PROD-002 workload classification and synthetic fixture safety/coverage | PASS — 19 synthetic cases; 1/1 validator | First validator run showed the email regex retained sentence punctuation; regex boundary corrected and rerun passed | Test validator corrected |
| V-088 | Full backend `dotnet test`; full frontend `npm test -- --runInBand`; `npm run build` | Repository root / `job-tracker-ui` | PROD-002 wider regression after final entitlement contract and fixture | PASS — backend 569/569; frontend 47/47 suites, 157/157; build passed | No model/provider/internet/real data used | N/A |
| V-089 | Bounded `rg`/`Get-Content` of POL-002 source, account/settings/authorization/workers, named AI client, sidecar router, Compose and tests | Repository root | Revalidate privacy and provider execution paths | PASS — global `AI_PROVIDER` reached full `/cv/*` payloads without per-user consent; `/summarize` remained local | Background queued calls have no HTTP identity and therefore need later operation policy snapshots | Confirmed implementation gap |
| V-090 | `dotnet build JobTrackerApi.csproj --no-restore`; `dotnet ef migrations add AddAiPrivacyPreferences ... --no-build` | `JobTrackerApi` | Compile policy/API and create additive preference migration | PASS — build 0 warnings/errors; migration created | First migration attempt exposed a misplaced fluent `AddHttpMessageHandler`; corrected at the named client and rebuilt | Implementation error resolved |
| V-091 | Focused backend policy/entitlement/worker/CV tests; final `AiPrivacyPolicyTests|ProEntitlementAuthorizationTests` rerun | Repository root | Verify live opt-out, Pro/admin/user gates and backend permission header | PASS — 72/72 then final policy 28/28 | Interim test used a nonexistent framework overload and misplaced a `using`; test construction corrected without weakening policy | Test-code errors resolved |
| V-092 | `.\.venv\Scripts\python.exe -m pytest -q` | `tools/summarizer` | Verify sidecar requires administrator gate plus backend consent header and remains local otherwise | PASS — 18/18 | Five existing SWIG deprecation warnings | N/A |
| V-093 | Focused UI `npm test -- --runInBand --runTestsByPath ...`; rerun after fixture correction | `job-tracker-ui` | Verify server-backed privacy controls and default local-only disclosure | PASS — 3 suites, 8/8 final | First run failed because the expanded settings test lacked an `/ai/usage` fixture; fixture added, production code unchanged | Test-fixture error resolved |
| V-094 | `dotnet ef migrations has-pending-model-changes`; migration script; disposable clean `dotnet ef database update` | `JobTrackerApi` | Validate model/migration defaults and clean application path | PARTIAL — no pending model changes; new SQLite SQL correctly uses AI enabled `1` and external consent `0`; clean chain failed before the new migration | SQLite idempotent scripts are unsupported; historical `AddJobEntityAndProspectStages` expects a reconciler-added column (pre-existing EF-only blank-chain defect) | Existing application-related migration-chain limitation |
| V-095 | Full backend `dotnet test`; full frontend `npm test -- --runInBand`; `npm run build`; `docker compose config --quiet`; `git diff --check` | Repository root / `job-tracker-ui` | POL-002 regression, production build, deployment syntax and patch hygiene | PASS — backend 576/576; frontend 47 suites/158 tests; build/config/diff pass | Expected unset optional Compose variables and line-ending notices only. Temporary API launch command was rejected before execution by policy; no service started | Runtime/browser/production verification blocked |
| V-096 | Source re-read plus `dotnet test ... --filter "...AiOperationQueueTests|...UserOperationStoreTests|...OperationsControllerTests"` | Repository root | AI-001 admission, capacity, priority, claim, owner execution, policy recheck and existing state/API regression | PASS — 17/17 final | Initial 15/15 and 16/16 passes preceded priority and retry/downgrade additions | N/A |
| V-097 | Full `dotnet test`; `docker compose config --quiet`; `git diff --check` | Repository root | AI-001 full backend regression and deployment/patch syntax | PASS — backend 581/581; config/diff pass | Expected unset optional Compose variables and line-ending notices only; worker remains default-off with no real task handler | Browser/provider/production verification pending later feature packages |
## Secret-scan commands
The tracked-tree scan read only paths returned by `git ls-files`, skipped binary/large files, and applied high-confidence regular expressions for private-key headers, JWTs, common cloud tokens, and Stripe secrets. It emitted only secret type, path, and line number; never matched values. Result: one JWT-like token at `docs/_archive/artifacts/s06-acceptance/.dev-auth-token.txt:1`.
The JWT was decoded locally without printing claims or token text. It is an expired HS256 local-app token (expired 2026-03-27), has no current required session ID, and cannot pass current session validation.
The history scan used:
```powershell
$pattern='-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----|eyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}|(AKIA|ASIA)[A-Z0-9]{16}|AIza[0-9A-Za-z_-]{35}|gh[pousr]_[A-Za-z0-9]{30,}|(sk|rk)_(live|test)_[A-Za-z0-9]{16,}|whsec_[A-Za-z0-9]{16,}'
foreach($commit in (git rev-list --all)) { git grep -I -l -E -e $pattern $commit -- }
```
Output was reduced to filenames and commit counts. The token artifact appears under its current and former paths across history. A filename-only history check also found two historical ASP.NET Data Protection XML key files; no key material was printed. Current documentation states those production keys were rotated on 2026-08-02; that operational claim was not independently verified against production.
## Checks not available/configured
- No repository lint script or ESLint configuration is present.
- `gitleaks`, `trivy`, and `hadolint` are not installed; equivalent bounded checks were performed as described above, but no container CVE scan was available.
- Dependency audit results are advisory matches, not proof that every advisory is reachable or exploitable.