fix(security): enforce audited account, network and resource boundaries
Implement B01-B06 with regression coverage and retained validation evidence. Preserve the production ingress preflight; F04 runtime verification remains open.
@@ -0,0 +1,64 @@
|
||||
# Jobjakt expanded production-readiness audit
|
||||
|
||||
Date: 2026-09-07. Source baseline: `main`, `7d641edc7bca866ccf06012897ba1d107c07e59f`.
|
||||
Scope: the supplied 416 audit areas, including the architecture/security/product review they reference.
|
||||
|
||||
**Verdict: needs work before a public paid launch.** The application has substantial useful safeguards, but passing its normal suites does not establish safe concurrency, bounded resource consumption, complete billing lifecycle handling, or operational recovery. No P0 exploit was confirmed in this local assessment. That is not evidence that none exists. The P1 findings and unverified launch gates below remain blocking.
|
||||
|
||||
This is a dated assessment and executable overhaul plan. Application code, production configuration and existing databases were not changed. Tests used new synthetic local accounts and temporary SQLite databases. No production deployment, external provider integration test, email delivery, payment, dependency download, or live host inspection was performed. The adversarial harness blocks remote browser requests; the existing browser suite was run as written and is not an all-process network-isolation guarantee. The original August audit remains a historical record; its closed findings are not automatically reopened here.
|
||||
|
||||
## Read the assessment
|
||||
|
||||
| Deliverable | Contents |
|
||||
|---|---|
|
||||
| [Findings and remediation](findings.md) | Priorities, source locations, root causes, reproduction, test expectations, implementation scope, staged rollout and detection |
|
||||
| [Source navigation](source-index.md) | Clickable primary implementation files for F01–F21 |
|
||||
| [Threat model and data lifecycle](threat-data.md) | Trust diagram, assets, attackers, privileges, abuse/rate limits, classification, retention and invariants |
|
||||
| [Resilience and failure answers](resilience.md) | Failure and test matrices, transaction/retry decisions, all 50 “What happens if?” answers |
|
||||
| [Product, operations and quality gates](product-operations.md) | UX/a11y/polish, glossary, component ownership, critical paths, costs/capacity, runbooks and reusable checklists |
|
||||
| [416-area coverage index](coverage.md) | Every requested area with its assessment destination and evidence boundary |
|
||||
| [Verification record](verification.md) | Commands, observed results, limitations, local artifact locations |
|
||||
| [Local adversarial harness](break-jobjakt.mjs) | Reproducible synthetic API/browser observations; remote browser requests blocked |
|
||||
|
||||
Evidence labels used throughout: **Observed** = executed during this audit; **Source** = implementation inspected, outcome inferred; **Proposal** = target design, not existing behavior; **Needs runtime verification** = missing live/staging/manual evidence. Coverage is not a pass certificate. Each test proves its exercised inputs only.
|
||||
|
||||
## What works and should be preserved
|
||||
|
||||
- A modular ASP.NET Core API is appropriate. Controllers, services, EF Core, relational constraints and an isolated Python parser are sufficient building blocks; a microservice rewrite is unjustified.
|
||||
- Tenant filters deny access without an owner. Sensitive cross-owner paths have dedicated tests; new direct CV GET/PUT/DELETE probes returned 404, and a normal user received 403 for admin audit access.
|
||||
- Local sessions are checked server-side. Role changes revoke sessions, Pro authorization reads current roles, cookie mutations use a CSRF header, and canonical external-origin handling protects generated links.
|
||||
- CV editing has a serialized save queue **within one editor**, visible save failures, retry, undo/redo, history, and navigation/unload warnings. Those controls do not prevent other tabs from overwriting it.
|
||||
- Durable AI operations have stored status, owner-scoped idempotency, capacity checks, leases, retry limits, cancellation and entitlement/privacy rechecks. Preserve this investment; do not add Redis or a broker just to have a queue.
|
||||
- Uploaded attachment handling stages/quarantines bytes and reconciles interrupted writes. Account deletion has stages, retries, session/public-link revocation and an independent restore tombstone ledger.
|
||||
- Submitted application packages preserve rendered content and copies of attachments. This is stronger historical evidence than re-rendering a live CV against an edited profile.
|
||||
- The parser already has subprocess isolation, bounded uploads, DOCX expansion limits, time limits, capacity limits and a constrained container. AI external-processing consent defaults off.
|
||||
- CI includes full test suites, dependency locks, a tracked-secret scan, an SBOM generator and browser checks. The production deployment backs up before replacement and checks public readiness separately from liveness.
|
||||
|
||||
## Current architecture and important qualifications
|
||||
|
||||
The frontend is React 19/MUI 7/React Router 7 in a Next.js 16 static-export shell. ASP.NET Core targets .NET 9 with EF Core 9. **SQLite and MariaDB/MySQL are supported; PostgreSQL is not configured.** PostgreSQL pool/index/search requests therefore translate into equivalent provider-specific reviews, not a recommendation to migrate databases. Redis and a general message broker are absent.
|
||||
|
||||
Compose defines frontend/nginx, backend, an isolated AI/parser sidecar and optional Ollama. MariaDB and the public TLS proxy are external operational dependencies. Backend/file/queue state shares the host failure domain. Private documents, provider credentials and the Data Protection key ring are accessible to the backend process. A single-backend deployment is an explicit constraint of process-local AI quota/admission locks.
|
||||
|
||||
The central architecture document still contains old controller/test counts, React Router v6 references, a claim that there is no CAPTCHA, and an outdated direction of Career Profile authority. Current code includes configurable Turnstile, relational career children, more controllers, and 753 passing backend tests. Use source and this dated assessment for these topics until that document is refreshed.
|
||||
|
||||
## Launch blockers in execution order
|
||||
|
||||
1. Close exposed-ingress/SSRF and resource-consumption risks: F03, F04, F10, F11. Verify host firewall restrictions and introduce shared PDF admission before opening registration.
|
||||
2. Prevent data loss and unsafe external side effects: F01, F02, F05, F07, F21. Add revision preconditions, operation idempotency, spreadsheet-safe export, correct timed-event round trips, and complete billing cancellation/reconciliation.
|
||||
3. Prove recovery and privacy: F12, F13, F20, plus backup restoration, key custody, retention and secret rotation gates. Gate public operation on an actual restored recovery set and worker outage recovery.
|
||||
4. Fix the builder accessibility and session/copy issues: F08, F09, F14–F18. Include builder routes in the release suite.
|
||||
|
||||
P1 means a credible security, data-loss, financial or host-availability risk within the described preconditions. P2 means a narrower reliability, usability, privacy-hardening or maintainability issue. A production evidence gap is a gate, not an invented vulnerability. No issue is marked fixed by this report.
|
||||
|
||||
## Commercial assessment
|
||||
|
||||
**My own CV:** I would use a restricted personal installation after verifying its backup and limiting public/AI/billing surfaces. I would avoid simultaneous editing and keep an independent export. I would not yet recommend entrusting the only copy of someone's career documents to a public deployment.
|
||||
|
||||
**100 paying users:** not yet. The P1 findings, working deletion-versus-billing policy, a tested restore, actionable outage/backup alerts and accessible core editor must be resolved first. The existing architecture can plausibly serve this cohort after those corrections; this audit did not measure that capacity.
|
||||
|
||||
**10,000 paying users:** not demonstrated. Measure realistic concurrency, job-history/query volume, PDF capacity, storage growth and AI service time. Expect a managed/operated MariaDB recovery strategy, bounded workers, robust billing reconciliation and deployment redundancy to matter before adding application services. Move process-local quota admission into transactional database reservations before adding backend replicas.
|
||||
|
||||
**Charge money today:** I would not approve an unrestricted launch from this evidence. Product value is present, but billing deletion, concurrency safety, editor accessibility and operational recovery remain unfinished. Small closed testing with explicit limits is different from a readiness claim.
|
||||
|
||||
**Measurable acceptance:** all P1s have failing-before/passing-after tests; no known P0 remains; owner-isolation tests pass across read/write/download/background paths; stale writes produce a conflict with recoverable edits; all costly entry points have enforced limits; billing deletion and concurrent event reconciliation pass sandbox tests; restore meets the agreed RPO/RTO; normal core routes have no unexplained errors or serious/critical axe findings; core workflows work in both languages/themes and by keyboard. The operational owner must sign the external evidence gates in [product-operations.md](product-operations.md).
|
||||
@@ -0,0 +1,221 @@
|
||||
// Local-only observational audit. Product failures are recorded, not blessed as regression expectations.
|
||||
// Run from the repository root after Release API and static UI builds (see verification.md).
|
||||
// Browser requests are restricted to loopback; integrations exercised here use local synthetic fixtures.
|
||||
import { createRequire } from 'node:module';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
|
||||
const root = process.cwd();
|
||||
const ui = path.join(root, 'job-tracker-ui');
|
||||
const require = createRequire(path.join(ui, 'package.json'));
|
||||
const { chromium, request } = require('@playwright/test');
|
||||
const AxeBuilder = require('@axe-core/playwright').default;
|
||||
const evidence = path.join(root, '.artifacts', 'expanded-audit');
|
||||
await mkdir(evidence, { recursive: true });
|
||||
const dataRoot = await mkdtemp(path.join(os.tmpdir(), 'jobjakt-audit-'));
|
||||
const apiOrigin = 'http://localhost:5402';
|
||||
const uiOrigin = 'http://localhost:3400';
|
||||
const password = 'SyntheticAudit123!';
|
||||
const findings = [];
|
||||
const consoleErrors = [];
|
||||
const networkErrors = [];
|
||||
const timings = [];
|
||||
const blockedExternalHosts = new Set();
|
||||
const children = [];
|
||||
let browser;
|
||||
const record = (scenario, meetsExpectation, observation) => findings.push({ scenario, meetsExpectation, observation });
|
||||
|
||||
async function waitReady(url) {
|
||||
for (let i = 0; i < 120; i++) {
|
||||
try { if ((await fetch(url, { signal: AbortSignal.timeout(1000) })).ok) return; } catch {}
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
throw new Error(`Local audit server did not start: ${url}`);
|
||||
}
|
||||
function start(command, args, cwd, env, logName) {
|
||||
const child = spawn(command, args, { cwd, env: { ...process.env, ...env }, windowsHide: true });
|
||||
const log = createWriteStream(path.join(evidence, logName));
|
||||
child.stdout.pipe(log); child.stderr.pipe(log);
|
||||
children.push(child);
|
||||
return child;
|
||||
}
|
||||
async function stop(child) {
|
||||
if (child.exitCode !== null) return;
|
||||
if (process.platform === 'win32') {
|
||||
await new Promise(resolve => {
|
||||
const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
||||
killer.on('exit', resolve);
|
||||
});
|
||||
} else child.kill('SIGTERM');
|
||||
}
|
||||
async function client(email) {
|
||||
const client = await request.newContext({ baseURL: apiOrigin });
|
||||
const login = await client.post('/api/auth/login', { data: { email, password } });
|
||||
if (!login.ok()) throw new Error(`Synthetic login failed: ${login.status()}`);
|
||||
const cookie = (await client.storageState()).cookies.find(c => c.name === 'XSRF-TOKEN');
|
||||
return { client, headers: { 'X-CSRF-TOKEN': cookie.value } };
|
||||
}
|
||||
async function body(response) {
|
||||
if (!response.ok()) throw new Error(`Fixture request failed: ${response.status()} ${response.url()}`);
|
||||
return response.json();
|
||||
}
|
||||
try {
|
||||
// Override outbound integrations and storage even if the developer shell has provider settings.
|
||||
start(process.env.DOTNET_HOST_PATH || path.join(os.homedir(), '.dotnet', 'dotnet.exe'),
|
||||
[path.join(root, 'JobTrackerApi/bin/Release/net9.0/JobTrackerApi.dll'), '--urls', apiOrigin],
|
||||
path.join(root, 'JobTrackerApi'), {
|
||||
ASPNETCORE_ENVIRONMENT: 'Development', DOTNET_CLI_TELEMETRY_OPTOUT: '1',
|
||||
Data__Root: dataRoot, Database__Provider: 'sqlite', ConnectionStrings__JobTracker: `Data Source=${path.join(dataRoot, 'jobtracker.db')}`,
|
||||
AccountLifecycle__TombstonesRoot: path.join(dataRoot, 'tombstones'),
|
||||
Auth__Require: 'true', Auth__JwtKey: 'audit-synthetic-signing-key-only-32-chars-minimum',
|
||||
Auth__AdminEmail: 'audit-admin@example.test', Auth__AdminPassword: password,
|
||||
Auth__TurnstileSecretKey: '', Auth__GoogleClientId: '', Auth__MicrosoftClientId: '',
|
||||
Auth__AllowRegistration: 'false', Auth__MicrosoftTenant: 'common',
|
||||
App__PublicBaseUrl: uiOrigin, Cors__Origins__0: uiOrigin, HttpsRedirection__Enabled: 'false',
|
||||
Ai__BaseUrl: 'http://127.0.0.1:1', Email__Enabled: 'false', Stripe__SecretKey: '',
|
||||
Workers__RulesEnabled: 'false', Workers__FollowUpRemindersEnabled: 'false',
|
||||
Workers__DailyExportEnabled: 'false', Workers__JobEnrichmentEnabled: 'false',
|
||||
Backups__Enabled: 'false', CV_PDF_BROWSER_PATH: chromium.executablePath(),
|
||||
}, 'break-api.log');
|
||||
await waitReady(`${apiOrigin}/health`);
|
||||
const admin = await client('audit-admin@example.test');
|
||||
for (const email of ['audit-a@example.test', 'audit-b@example.test']) {
|
||||
await body(await admin.client.post('/api/users', { headers: admin.headers, data: { email, password, roles: [] } }));
|
||||
}
|
||||
const a = await client('audit-a@example.test');
|
||||
const b = await client('audit-b@example.test');
|
||||
await body(await a.client.put('/api/career/profile', { headers: a.headers, data: { profile: {
|
||||
version: '1', contact: { fullName: 'Åse Audit 🧪', email: 'audit-a@example.test', headline: 'Software developer' },
|
||||
summary: ['Synthetic profile for local audit only.'],
|
||||
jobs: [{ company: 'Fjord Example', title: 'Developer', start: '2021', end: '2025', bullets: ['Built reliable APIs.'] }],
|
||||
skills: ['C#', 'TypeScript', 'Norsk'],
|
||||
} } }));
|
||||
const cv = await body(await a.client.post('/api/cv/variants', { headers: a.headers, data: { name: 'Audit CV' } }));
|
||||
const stale = await body(await a.client.get(`/api/cv/variants/${cv.id}`));
|
||||
const first = await a.client.put(`/api/cv/variants/${cv.id}`, { headers: a.headers, data: { name: 'Newer edit', settings: cv.settings, version: cv.version } });
|
||||
const second = await a.client.put(`/api/cv/variants/${cv.id}`, { headers: a.headers, data: { name: 'Stale tab edit', settings: stale.settings, version: stale.version } });
|
||||
const final = await body(await a.client.get(`/api/cv/variants/${cv.id}`));
|
||||
record('stale CV save', second.status() === 409 || second.status() === 412, { first: first.status(), staleSave: second.status(), overwritten: final.name === 'Stale tab edit', version: final.version });
|
||||
const concurrent = await Promise.all(['Concurrent A', 'Concurrent B'].map(name => a.client.put(`/api/cv/variants/${cv.id}`, { headers: a.headers, data: { name, settings: cv.settings } })));
|
||||
record('simultaneous CV saves detect conflict', concurrent.some(r => [409, 412].includes(r.status())), { statuses: concurrent.map(r => r.status()) });
|
||||
for (const method of ['get', 'put', 'delete']) {
|
||||
const response = await b.client[method](`/api/cv/variants/${cv.id}`, { headers: b.headers, ...(method === 'put' ? { data: { name: 'Cross-owner', settings: cv.settings } } : {}) });
|
||||
record(`CV cross-owner ${method}`, [403, 404].includes(response.status()), { status: response.status() });
|
||||
}
|
||||
const company = await body(await a.client.post('/api/companies', { headers: a.headers, data: { name: 'Åse & Sons 🧪', location: 'Oslo' } }));
|
||||
const input = { companyId: company.id, jobTitle: '=1+1', status: 'Saved', notes: '<img src=x onerror=alert(1)>' };
|
||||
const created = await Promise.all([0, 1].map(() => a.client.post('/api/jobapplications', { headers: a.headers, data: input })));
|
||||
const jobs = await Promise.all(created.map(body));
|
||||
record('duplicate job creation deduplicated', jobs[0].id === jobs[1].id, { statuses: created.map(r => r.status()), distinctIds: jobs[0].id !== jobs[1].id });
|
||||
for (const [stamp, expectedUtc] of [
|
||||
['2026-03-28T09:00:00+01:00', '20260328T080000Z'],
|
||||
['2026-03-29T09:00:00+02:00', '20260329T070000Z'],
|
||||
['2026-10-25T02:30:00+02:00', '20261025T003000Z'],
|
||||
['2026-10-25T02:30:00+01:00', '20261025T013000Z'],
|
||||
]) {
|
||||
const dated = await body(await a.client.post('/api/jobapplications', { headers: a.headers, data: { companyId: company.id, jobTitle: 'Synthetic DST probe', status: 'Saved', followUpAt: stamp } }));
|
||||
const ics = await a.client.get(`/api/calendar/jobs/${dated.id}/events.ics?kind=follow-up`);
|
||||
const content = await ics.text();
|
||||
const actual = content.match(/^DTSTART:(.+)$/m)?.[1]?.trim();
|
||||
record(`DST calendar instant ${stamp}`, actual === expectedUtc, { status: ics.status(), expectedUtc, actual });
|
||||
}
|
||||
const csv = await a.client.get('/api/export/jobs?format=csv');
|
||||
const csvText = await csv.text();
|
||||
record('CSV formula neutralized', !csvText.includes(',=1+1,'), { formulaCellPresent: csvText.includes(',=1+1,') });
|
||||
for (const route of ['/api/auth/me', `/api/cv/variants/${cv.id}`, '/api/export/jobs?format=csv']) {
|
||||
const response = await a.client.get(route);
|
||||
record(`sensitive response cache ${route.split('?')[0]}`, /no-store/i.test(response.headers()['cache-control'] || ''), { cacheControl: response.headers()['cache-control'] || null });
|
||||
}
|
||||
const adminAccess = await b.client.get('/api/admin/audit');
|
||||
record('standard user admin denial', adminAccess.status() === 403, { status: adminAccess.status() });
|
||||
for (let i = 0; i < 2; i++) await a.client.post('/api/export/account', { headers: a.headers, data: {} });
|
||||
const otherExport = await b.client.post('/api/export/account', { headers: b.headers, data: {} });
|
||||
record('account export limiter isolation', otherExport.status() === 200, { otherAccountStatusAfterTwoExports: otherExport.status() });
|
||||
|
||||
start(process.execPath, [path.join(ui, 'scripts/serve-export.mjs')], ui,
|
||||
{ PORT: '3400' }, 'break-ui.log');
|
||||
await waitReady(uiOrigin);
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||||
await context.route('**/*', route => {
|
||||
const url = new URL(route.request().url());
|
||||
if (['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'about:', 'blob:'].includes(url.protocol)) return route.continue();
|
||||
blockedExternalHosts.add(url.hostname);
|
||||
return route.abort();
|
||||
});
|
||||
await context.addInitScript(() => { if (!localStorage.getItem('uiLanguage')) localStorage.setItem('uiLanguage', 'en'); });
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', error => consoleErrors.push(error.message));
|
||||
page.on('console', message => { if (message.type() === 'error' || message.type() === 'warning') consoleErrors.push(message.text().slice(0, 500)); });
|
||||
page.on('response', response => { if (response.status() >= 400) networkErrors.push({ path: new URL(response.url()).pathname, status: response.status() }); });
|
||||
await page.goto(`${uiOrigin}/login`);
|
||||
await page.getByLabel('Email').fill('audit-a@example.test');
|
||||
await page.getByLabel('Current password').fill(password);
|
||||
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
||||
await page.waitForURL('**/dashboard');
|
||||
for (const route of ['/jobs', '/career', `/career/builder/${cv.id}`]) {
|
||||
const started = Date.now();
|
||||
await page.goto(`${uiOrigin}${route}`);
|
||||
await page.locator('main').waitFor();
|
||||
timings.push({ route, navigationToMainMs: Date.now() - started });
|
||||
}
|
||||
await page.getByLabel('CV name', { exact: true }).waitFor();
|
||||
for (const language of ['en', 'nb']) {
|
||||
await a.client.put('/api/auth/preferences/language', { headers: a.headers, data: { language } });
|
||||
for (const theme of ['light', 'dark']) {
|
||||
await page.evaluate(({ language, theme }) => { localStorage.setItem('uiLanguage', language); localStorage.setItem('jobtracker.themeMode', theme); }, { language, theme });
|
||||
await page.reload();
|
||||
await page.locator('main').waitFor();
|
||||
await page.waitForFunction(() => document.querySelector('[data-cv-page] iframe')?.getAttribute('srcdoc')?.includes('Synthetic profile'));
|
||||
await page.waitForTimeout(400);
|
||||
for (const width of [375, 1440]) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.screenshot({ path: path.join(evidence, `builder-${language}-${theme}-${width}.png`), fullPage: true });
|
||||
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
|
||||
const axe = await new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa']).analyze();
|
||||
record(`builder layout ${language}/${theme}/${width}`, overflow <= 1 && axe.violations.length === 0, { overflow,
|
||||
actualLanguage: await page.locator('html').getAttribute('lang'), actualTheme: await page.locator('html').getAttribute('data-color-scheme'),
|
||||
violations: axe.violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.map(n => ({ target: n.target, failureSummary: n.failureSummary })) })) });
|
||||
}
|
||||
}
|
||||
}
|
||||
await a.client.put('/api/auth/preferences/language', { headers: a.headers, data: { language: 'en' } });
|
||||
await page.evaluate(() => { localStorage.setItem('uiLanguage', 'en'); localStorage.setItem('jobtracker.themeMode', 'light'); });
|
||||
await page.reload();
|
||||
await page.getByLabel('CV name', { exact: true }).waitFor();
|
||||
await context.setOffline(true);
|
||||
await page.getByLabel('CV name', { exact: true }).fill('Offline synthetic draft');
|
||||
await page.waitForTimeout(1600);
|
||||
record('offline save reports failure', await page.getByText(/save failed|not saved|could not save/i).count() > 0, { matchingFailureMessages: await page.getByText(/save failed|not saved|could not save/i).count() });
|
||||
await context.setOffline(false);
|
||||
page.once('dialog', dialog => dialog.accept());
|
||||
await page.reload();
|
||||
const recoveredName = await page.getByLabel('CV name', { exact: true }).inputValue();
|
||||
record('offline draft survives confirmed refresh', recoveredName === 'Offline synthetic draft', { draftRecovered: recoveredName === 'Offline synthetic draft' });
|
||||
const secondTab = await context.newPage();
|
||||
await secondTab.goto(`${uiOrigin}/career/builder/${cv.id}`);
|
||||
await secondTab.getByLabel('CV name', { exact: true }).waitFor();
|
||||
await secondTab.waitForTimeout(1000);
|
||||
await page.evaluate(async () => {
|
||||
await fetch('http://localhost:5402/api/auth/logout', { method: 'POST', credentials: 'include' });
|
||||
localStorage.removeItem('authUserKey');
|
||||
window.dispatchEvent(new Event('auth-changed'));
|
||||
});
|
||||
await secondTab.waitForTimeout(1000);
|
||||
record('logout clears other tab immediately', !(await secondTab.getByLabel('CV name', { exact: true }).count()), { privateEditorStillMounted: (await secondTab.getByLabel('CV name', { exact: true }).count()) > 0 });
|
||||
await context.close();
|
||||
} catch (error) {
|
||||
if (browser) for (const context of browser.contexts()) for (const page of context.pages()) {
|
||||
await page.screenshot({ path: path.join(evidence, 'harness-failure.png'), fullPage: true }).catch(() => {});
|
||||
}
|
||||
findings.push({ scenario: 'harness', meetsExpectation: false, observation: String(error.stack || error) });
|
||||
process.exitCode = 2;
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
for (const child of children.reverse()) await stop(child);
|
||||
await writeFile(path.join(evidence, 'break-results.json'), JSON.stringify({ generatedAt: new Date().toISOString(), scope: 'isolated SQLite and local Chromium; synthetic data; outbound browser requests blocked', findings, consoleErrors, networkErrors, blockedExternalHosts: [...blockedExternalHosts], timings }, null, 2));
|
||||
console.log(JSON.stringify({ observations: findings.length, unmet: findings.filter(f => !f.meetsExpectation).length, evidence: '.artifacts/expanded-audit/break-results.json' }));
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
# Coverage of all 416 requested audit areas
|
||||
|
||||
This index preserves the exact numbered titles from the supplied request. Each row points to its assessment or acceptance plan and states the evidence boundary. **416 indexed areas does not mean 416 passed tests, exhaustive source review of every symbol, or production certification.** Areas requiring a live provider, deployed host, a destructive failure drill, a native screen reader or a complete endpoint/input corpus remain open. An explicit unverified row is an outstanding verification task, not a favorable finding.
|
||||
|
||||
Baseline and commands are in [verification.md](verification.md). Prioritized changes are [F01–F21](findings.md); repeated rows about the same root cause do not create extra findings. The [50 explicit failure answers](resilience.md#what-happens-if) distinguish current source behavior from proposed recovery.
|
||||
|
||||
Evidence codes:
|
||||
- **O — Observed:** executed in this audit, limited to the stated input/environment. This may be a failed expectation.
|
||||
- **S — Source:** a relevant implementation boundary was inspected; runtime outcome is inferred, not certified.
|
||||
- **P — Proposal:** recommended design, policy, checklist or acceptance test; not claimed implemented.
|
||||
- **R — Needs runtime verification:** remaining live/staging/manual/inventory evidence. Where the note says source review too, the complete source inventory is also outstanding.
|
||||
- **N — Not currently evidenced/deployed:** an optional technology should not be added solely to satisfy a heading. SQLite/MariaDB replace PostgreSQL-specific assumptions; Redis and a general broker are absent.
|
||||
|
||||
All R entries remain on the acceptance backlog. Prioritize P1-related entries first, then core user-path accessibility and operational proof. Cosmetic/optional feature entries do not justify indiscriminate platform expansion.
|
||||
|
||||
| # | Requested area | Evidence | Assessment and remaining boundary |
|
||||
|---|---|---|---|
|
||||
| 1 | Formal threat modelling | S/P | [Assessment](threat-data.md#abuse-matrix) — Trust/abuse model and proposed enforcement; distributed abuse load remains untested. |
|
||||
| 2 | Abuse-case analysis | S/P | [Assessment](threat-data.md#abuse-matrix) — Trust/abuse model and proposed enforcement; distributed abuse load remains untested. |
|
||||
| 3 | Rate-limiting strategy | S/P | [Assessment](threat-data.md#abuse-matrix) — Trust/abuse model and proposed enforcement; distributed abuse load remains untested. |
|
||||
| 4 | Data lifecycle map | S/P | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Sensitive copies and target retention mapped; live inventories and custody unverified. |
|
||||
| 5 | Data classification | S/P | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Sensitive copies and target retention mapped; live inventories and custody unverified. |
|
||||
| 6 | Concurrency testing | O/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Stale and simultaneous CV PUTs tested; job/profile concurrency and multi-replica races remain. |
|
||||
| 7 | Lost-update prevention | O | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Stale CV save overwrote the newer value; F01, expected revision precondition absent. |
|
||||
| 8 | Idempotency review | O/S | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Repeated job POST created distinct rows; durable AI idempotency exists; billing/email gaps remain. |
|
||||
| 9 | Unexpected user behaviour | O/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Repeated saves, modified owner IDs, offline refresh and Unicode tried; broader confused-user study remains. |
|
||||
| 10 | Network resilience | O/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Offline save failure visible; delayed responses, network flapping and 502 recovery not fully injected. |
|
||||
| 11 | Offline and interrupted editing | O | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — After acknowledging the unload warning, reload lost the offline edit; F09. |
|
||||
| 12 | Dependency failure testing | S/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — State/retry/failure assessment; only specifically recorded probes executed. |
|
||||
| 13 | Partial failure scenarios | S/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — State/retry/failure assessment; only specifically recorded probes executed. |
|
||||
| 14 | Transaction-boundary review | S/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Career projections, file snapshots, email and billing cross boundaries; transactional matrix specifies repairs. |
|
||||
| 15 | Background-work reliability | S/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Durable AI leases exist; outer worker dependency errors can stop host, F20; outage drill pending. |
|
||||
| 16 | Browser state audit | S/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Storage/cache patterns inspected; no all-route corrupt-storage/private-window certification. |
|
||||
| 17 | Multi-tab session behaviour | O | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Logout revoked backend session but left another tab's private editor mounted; F08. |
|
||||
| 18 | Session-expiry UX | R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Needs runtime verification: actual session expiry, reauthentication and recovery with unsaved edits. |
|
||||
| 19 | Permission model design | O/S | [Assessment](threat-data.md#assets-and-intended-access) — Owner and Pro policies inspected; tested cross-owner CV denial and ordinary-user admin denial. |
|
||||
| 20 | Administrator access model | S | [Assessment](threat-data.md#assets-and-intended-access) — Admin can access global job activity content; minimal support scopes/security audit incomplete, F13. |
|
||||
| 21 | Support access and impersonation | S/P | [Assessment](threat-data.md#assets-and-intended-access) — No support impersonation solution introduced; prefer scoped metadata and audited time-limited access. |
|
||||
| 22 | Audit-trail architecture | S/P | [Assessment](threat-data.md#assets-and-intended-access) — JobEvents activity is not an immutable privileged/security ledger; F13. |
|
||||
| 23 | Supportability and diagnostics | S/P | [Assessment](threat-data.md#assets-and-intended-access) — Trace IDs exist; safe metadata diagnostics proposed instead of routine private-content access. |
|
||||
| 24 | Error identifiers | S/P | [Assessment](threat-data.md#assets-and-intended-access) — Existing request/error identifiers useful; propagate safe reference to client, worker and support views. |
|
||||
| 25 | Content design and microcopy audit | S/P | [Assessment](product-operations.md#critical-paths-and-time-to-value) — Proposed copy/navigation/onboarding improvements; no recruited usability study. |
|
||||
| 26 | English/Bokmål terminology consistency | O/P | [Assessment](product-operations.md#critical-paths-and-time-to-value) — NB UI includes English template descriptions and inconsistent capitalization; native editorial review proposed. |
|
||||
| 27 | Onboarding audit | R | [Assessment](product-operations.md#critical-paths-and-time-to-value) — Needs runtime verification: registration/verification/first-run journey against a local mail sink. |
|
||||
| 28 | Time-to-value | S/P | [Assessment](product-operations.md#critical-paths-and-time-to-value) — Existing manual-add sequence has optional steps; offer earlier save; no timed user study. |
|
||||
| 29 | Feature discoverability | S/P | [Assessment](product-operations.md#critical-paths-and-time-to-value) — Proposed copy/navigation/onboarding improvements; no recruited usability study. |
|
||||
| 30 | Progressive disclosure | S/P | [Assessment](product-operations.md#critical-paths-and-time-to-value) — Proposed copy/navigation/onboarding improvements; no recruited usability study. |
|
||||
| 31 | Feature-flag architecture | S/P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Existing switches retained; owner/default/expiry register proposed. |
|
||||
| 32 | Feature-flag lifecycle | S/P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Existing switches retained; owner/default/expiry register proposed. |
|
||||
| 33 | Database evolution strategy | S/R | [Assessment](resilience.md#time-and-compatibility-policy) — Additive/backfill/contract stages proposed; current nontransactional SQLite migration warning observed separately. |
|
||||
| 34 | Backwards-compatible deployment | S/R | [Assessment](resilience.md#time-and-compatibility-policy) — Previous-image rollback is not schema rollback; old/new contract rehearsal needed. |
|
||||
| 35 | API compatibility strategy | S/P | [Assessment](resilience.md#time-and-compatibility-policy) — Unversioned DTO contract requires additive evolution and supported-client window; not automatic major-version routing. |
|
||||
| 36 | Frontend deployment version mismatch | R | [Assessment](resilience.md#time-and-compatibility-policy) — Needs runtime verification: old open tab versus new API/static assets; retain dirty edits before reload. |
|
||||
| 37 | Cache strategy audit | O/S | [Assessment](resilience.md#time-and-compatibility-policy) — Private responses lacked explicit cache policy; no cross-user HTTP cache exploit demonstrated. |
|
||||
| 38 | Date/time correctness | O | [Assessment](resilience.md#time-and-compatibility-policy) — Offset-bearing follow-up instants change on calendar export; F21. |
|
||||
| 39 | DST testing | O | [Assessment](resilience.md#time-and-compatibility-policy) — Four offset/DST cases fail; repeated autumn hour collapses to one exported time. |
|
||||
| 40 | Locale correctness | O/R | [Assessment](resilience.md#time-and-compatibility-policy) — EN/NB themes rendered; full date/number/plural/locale fallback matrix remains. |
|
||||
| 41 | Currency handling | P/R | [Assessment](resilience.md#time-and-compatibility-policy) — Store currency explicitly and avoid implicit conversion; full salary/offer round-trip matrix unverified. |
|
||||
| 42 | Translation-layout testing | O/R | [Assessment](resilience.md#time-and-compatibility-policy) — Eight actual locale/theme/width states tested; doubled-length pseudolocale and every route remain. |
|
||||
| 43 | Generated-document accessibility | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: tagged output, reading order and independent browser Print. |
|
||||
| 44 | Print behaviour | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: tagged output, reading order and independent browser Print. |
|
||||
| 45 | Data portability | O/S | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — CSV/account export exercised; complete import round trip and all portability formats not certified. |
|
||||
| 46 | Export security | O/S | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Formula CSV and no-store gaps observed; recent-auth owner export controls retained. |
|
||||
| 47 | Deletion semantics | S/R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Soft delete, hard deletion and immutable submitted snapshots distinguished; cross-resource races remain. |
|
||||
| 48 | Account deletion lifecycle | S/R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Staged purge and tombstones exist; external billing cancellation and restored-backup drill incomplete. |
|
||||
| 49 | Resource-exhaustion testing | S/R | [Assessment](threat-data.md#abuse-matrix) — Shared PDF admission absent; 1,000-renderer exhaustion deliberately not executed. |
|
||||
| 50 | Token-budget protection | S/R | [Assessment](threat-data.md#abuse-matrix) — AI reservation limits exist; process-local and cross-pipeline admission need concurrent proof. |
|
||||
| 51 | AI cost controls | S/P | [Assessment](threat-data.md#abuse-matrix) — Use provider actuals plus estimated reservations, per-workflow/user/global budgets and kill switches. |
|
||||
| 52 | AI observability | S/P | [Assessment](threat-data.md#abuse-matrix) — Persisted AI usage/status useful; content-free metrics and provider bill reconciliation proposed. |
|
||||
| 53 | AI model changes | P/R | [Assessment](threat-data.md#abuse-matrix) — Pin evaluated routes/prompt versions; no live model-change/evaluation performed. |
|
||||
| 54 | Property-based testing | P/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Invariant/property suite proposed; example regression suites are not property-based coverage. |
|
||||
| 55 | Fuzz testing | O/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Existing malformed parser tests pass; broad mutation/fuzz campaign not performed. |
|
||||
| 56 | Chaos/failure testing | O/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Offline/browser/API contention injected locally; DB/host/provider chaos remains in isolated staging. |
|
||||
| 57 | Disk-space exhaustion | P/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Bounded synthetic tests specified; no destructive host exhaustion or broad fuzz campaign. |
|
||||
| 58 | Memory exhaustion | P/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Bounded synthetic tests specified; no destructive host exhaustion or broad fuzz campaign. |
|
||||
| 59 | CPU exhaustion | P/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Bounded synthetic tests specified; no destructive host exhaustion or broad fuzz campaign. |
|
||||
| 60 | Long-term maintenance burden | P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Feature value/maintenance/risk table protects useful boundaries and questions speculative complexity. |
|
||||
| 61 | Build vs buy review | P/R | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Use Identity/Stripe/MUI/current queue; no researched vendor price or replacement recommendation. |
|
||||
| 62 | Telemetry strategy | P/R | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Minimal content-free telemetry proposed; deployed collector/consent not verified. |
|
||||
| 63 | Privacy-preserving analytics | P/R | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Minimal content-free telemetry proposed; deployed collector/consent not verified. |
|
||||
| 64 | Incident-response readiness | P/R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Technical runbooks supplied; issuer rotation/contact/disclosure drills remain. |
|
||||
| 65 | Security incident scenarios | P/R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Key leak, admin compromise, dependency compromise and data leak runbooks; no live attack exercise. |
|
||||
| 66 | Secret rotation | P/R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Rotate issuer credentials, revoke sessions and preserve decryptability; key-custody drill unexecuted. |
|
||||
| 67 | Security disclosure readiness | P/R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Assign private disclosure channel and triage owner before launch; delivery/ownership not verified. |
|
||||
| 68 | Release quality gates | P/R | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Concrete gates and recovery constraints supplied; no production approval claimed. |
|
||||
| 69 | Production-readiness checklist | P/R | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Concrete gates and recovery constraints supplied; no production approval claimed. |
|
||||
| 70 | Rollback-readiness | P/R | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Concrete gates and recovery constraints supplied; no production approval claimed. |
|
||||
| 71 | Performance objectives | S/P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Bottlenecks and measurement triggers mapped; no capacity or SLO achievement claim. |
|
||||
| 72 | Error-budget thinking | P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Proposed internal 99.5% core SLO; collect baseline before commitments; optional AI tracked separately. |
|
||||
| 73 | Capacity planning | S/P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Bottlenecks and measurement triggers mapped; no capacity or SLO achievement claim. |
|
||||
| 74 | Scaling triggers | S/P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Bottlenecks and measurement triggers mapped; no capacity or SLO achievement claim. |
|
||||
| 75 | Single points of failure | S | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Host, DB/files/key ring and operator are common failure domains; document recovery ownership. |
|
||||
| 76 | Dependency ownership | S/P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Public proxy, MariaDB, provider and backup custodians need named ownership outside public source. |
|
||||
| 77 | Product invariants | S/P | [Assessment](threat-data.md#product-invariants-and-enforcement) — Owner/version/quota/side-effect invariants and enforcement/test targets mapped. |
|
||||
| 78 | Database-enforced invariants | S/R | [Assessment](threat-data.md#product-invariants-and-enforcement) — Owner filters and constraints exist; CV version uniqueness/preconditions and reservations need strengthening. |
|
||||
| 79 | State-machine review | S/R | [Assessment](threat-data.md#product-invariants-and-enforcement) — AI/deletion/package transitions reviewed; model invalid/duplicate/out-of-order transitions explicitly. |
|
||||
| 80 | Safe retries | S/P | [Assessment](threat-data.md#product-invariants-and-enforcement) — Retry reads/transient work within budgets; reconcile ambiguous external side effects by operation identity. |
|
||||
| 81 | Safe failure behaviour | S/P | [Assessment](threat-data.md#product-invariants-and-enforcement) — Do not report unsaved, unsent or uncharged work as successful; preserve recoverable inputs. |
|
||||
| 82 | Accessibility under failure | O/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Failed save is visible; failure announcement/focus and screen-reader recovery remain unverified. |
|
||||
| 83 | Design quality gates | P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Both themes/languages, interaction states and core axe/manual checks proposed as release gates. |
|
||||
| 84 | Component ownership | S/P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Use MUI/theme/AppShell/shared feedback owners; consolidate demonstrated repeated callers incrementally. |
|
||||
| 85 | Storybook/component showcase | P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — A lightweight state showcase may help; Storybook is not required without recurring review need. |
|
||||
| 86 | All-state component testing | P/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Loading/empty/error/disabled/long-text/selected/focus states defined; complete component sweep remains. |
|
||||
| 87 | Extremely long-content testing | O/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Existing long-content browser cases pass; maximal text, large datasets and all states untested. |
|
||||
| 88 | Unicode and unusual-input testing | O/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Åse/emoji survive local profile/render; combining/RTL/confusable corpus remains. |
|
||||
| 89 | Copy/paste behaviour | S/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Clipboard success shown before promise resolution; permission-denied/formatted-paste matrix remains. |
|
||||
| 90 | Undo/recovery strategy | O/S | [Assessment](threat-data.md#product-invariants-and-enforcement) — Undo and unload warning exist; confirmed refresh loses offline draft, F09. |
|
||||
| 91 | Versioning strategy for CV content | O/S | [Assessment](threat-data.md#product-invariants-and-enforcement) — History version is not concurrency control; stale write succeeds, F01. |
|
||||
| 92 | Document provenance | S | [Assessment](threat-data.md#product-invariants-and-enforcement) — Submitted packages freeze content/copies; live CV settings render against current Career Profile. |
|
||||
| 93 | AI content provenance | S/P | [Assessment](threat-data.md#product-invariants-and-enforcement) — Operation/prompt metadata helps provenance; distinguish generated claims from verified user facts. |
|
||||
| 94 | Dependency-locking and reproducible builds | O/S | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Installed locked environment builds; Docker lock enforcement and mutable image/action gaps remain. |
|
||||
| 95 | Software supply-chain security | O/S | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Tracked-secret scan and supply-chain helper tests pass; no current advisory/network scan. |
|
||||
| 96 | SBOM | S/R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — SBOM generator exists; no newly verified release-image SBOM/provenance attached. |
|
||||
| 97 | Dependency update strategy | P/R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Small automated update PRs with suites/exception expiry; actual hosted update policy not verified. |
|
||||
| 98 | End-of-life tracking | R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Needs external verification: .NET/Node/Python/library support dates and maintainer/advisory status. |
|
||||
| 99 | Infrastructure documentation | S | [Assessment](product-operations.md#operational-ownership-and-alerts) — Architecture document is stale on counts/router/CAPTCHA/profile authority; F17. |
|
||||
| 100 | Operational runbooks | P | [Assessment](product-operations.md#operational-ownership-and-alerts) — Fourteen incident runbooks include diagnosis, safe recovery and verification. |
|
||||
| 101 | Monitoring ownership | P/R | [Assessment](product-operations.md#operational-ownership-and-alerts) — Signals have proposed responsible roles; actual people and backup contact not confirmed. |
|
||||
| 102 | Alert quality | P/R | [Assessment](product-operations.md#operational-ownership-and-alerts) — Dedup, impact, safe trace, runbook and recovery notifications required; no delivered alert drill. |
|
||||
| 103 | Backup monitoring | S/P/R | [Assessment](product-operations.md#operational-ownership-and-alerts) — Backup age/size/off-site failure signals proposed; independent alert delivery unproven. |
|
||||
| 104 | Restore drills | R | [Assessment](product-operations.md#operational-ownership-and-alerts) — Needs runtime verification: isolated DB/files/key/tombstone restore and measured RPO/RTO. |
|
||||
| 105 | Migration rehearsal | S/R | [Assessment](product-operations.md#operational-ownership-and-alerts) — Fresh local SQLite startup migrates with warnings; interrupted/large/MariaDB migration rehearsal remains. |
|
||||
| 106 | Production data protection in non-production | O/P | [Assessment](verification.md#environment-and-boundaries) — Only synthetic local accounts/documents used; production-data masking/access policy proposed. |
|
||||
| 107 | Test-data strategy | O | [Assessment](verification.md#environment-and-boundaries) — Deterministic synthetic accounts/profile/jobs/Unicode fixtures; no real user documents required. |
|
||||
| 108 | Environment parity | R | [Assessment](verification.md#environment-and-boundaries) — Needs runtime verification: actual Linux/Chromium/MariaDB/TLS/proxy/resource limits parity. |
|
||||
| 109 | Staging strategy | P/R | [Assessment](verification.md#environment-and-boundaries) — Isolated production-like environment required for remaining drills; not provisioned by this audit. |
|
||||
| 110 | Safe third-party test modes | R | [Assessment](verification.md#environment-and-boundaries) — Needs runtime verification: Stripe test mode, OAuth test identity and local/sandbox mail. |
|
||||
| 111 | Domain-boundary tests | O/R | [Assessment](resilience.md#time-and-compatibility-policy) — Existing domain suites pass; complete cross-boundary external contract matrix remains. |
|
||||
| 112 | Contract tests | S/R | [Assessment](resilience.md#time-and-compatibility-policy) — Provider fakes/suite coverage useful; current live provider contracts not called. |
|
||||
| 113 | API schema quality | R | [Assessment](resilience.md#time-and-compatibility-policy) — Needs runtime verification: generated schema operation coverage, examples, nullable/enums/error documentation. |
|
||||
| 114 | Consistent error contract | S/P | [Assessment](resilience.md#time-and-compatibility-policy) — Normalize actionable error/reference/retry semantics across API, workers and UI; preserve existing contracts. |
|
||||
| 115 | Graceful degradation | S/P | [Assessment](product-operations.md#critical-paths-and-time-to-value) — Core tracking/editing prioritized; optional dependency failure should degrade independently. |
|
||||
| 116 | Critical-path definition | P | [Assessment](product-operations.md#critical-paths-and-time-to-value) — Core auth/tracking/save/career/download paths explicitly prioritized above optional AI/discovery. |
|
||||
| 117 | User-impact severity | P | [Assessment](product-operations.md#critical-paths-and-time-to-value) — P0/P1/P2 reflect user impact and preconditions; evidence gap is separate from confirmed exploit. |
|
||||
| 118 | Feature maintenance scoring | P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Value/maintenance/risk judgments supplied; no feature deletion justified without usage evidence. |
|
||||
| 119 | "Should this exist?" review | P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Value/maintenance/risk judgments supplied; no feature deletion justified without usage evidence. |
|
||||
| 120 | Product coherence | P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Value/maintenance/risk judgments supplied; no feature deletion justified without usage evidence. |
|
||||
| 121 | Information architecture map | S/R | [Assessment](product-operations.md#navigation-and-domain-language) — Routes and target information architecture mapped; complete navigation-state sweep remains. |
|
||||
| 122 | Navigation depth | S/P | [Assessment](product-operations.md#navigation-and-domain-language) — Reduce early manual-add steps and contextualize secondary tools; no forced removal of useful deep links. |
|
||||
| 123 | URL design | S | [Assessment](product-operations.md#navigation-and-domain-language) — Routed job/career workspace retained; stable owner-safe resource routes and compatibility redirects. |
|
||||
| 124 | Browser Back/Forward behaviour | O/R | [Assessment](product-operations.md#navigation-and-domain-language) — Existing back/deep-link/dirty-navigation browser case passes; all modal/filter combinations remain. |
|
||||
| 125 | Deep linking | O/R | [Assessment](product-operations.md#navigation-and-domain-language) — Existing workspace deep links pass; complete unauthenticated/revoked/missing-resource link matrix remains. |
|
||||
| 126 | Search-state persistence | R | [Assessment](product-operations.md#navigation-and-domain-language) — Needs runtime verification: reload/back/forward/share of every search/filter/page state. |
|
||||
| 127 | Destructive-operation recovery | S/O/R | [Assessment](product-operations.md#navigation-and-domain-language) — Trash/staged deletion and unload warnings exist; destructive recovery/provider races need broader drills. |
|
||||
| 128 | Notification strategy | S/P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Respect channel preferences, dedup and durable delivery; F07 reminder behavior needs repair. |
|
||||
| 129 | Toast abuse | S/P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Failed saves require persistent recovery; transient toasts only for noncritical confirmation. |
|
||||
| 130 | Confirmation-dialog audit | O/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Unload warning observed; complete confirm/cancel/focus and irreversible-action audit remains. |
|
||||
| 131 | Latency perception | O/P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Local navigation timing retained; show truthful queued/loading states; no WAN performance claim. |
|
||||
| 132 | Cancellation | S/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Durable AI cancellation exists; component unmount and synchronous downstream cancellation still need proof. |
|
||||
| 133 | Retry UX | O/S | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — CV retry/failed state exists; external effects must not blindly retry ambiguous outcomes. |
|
||||
| 134 | Empty/zero-data analytics | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: zero-data charts, text alternatives and keyboard data exploration. |
|
||||
| 135 | Accessibility of charts | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: zero-data charts, text alternatives and keyboard data exploration. |
|
||||
| 136 | CSV injection | O | [Assessment](findings.md#f05--p1-csv-quoting-does-not-neutralize-spreadsheet-formulas) — Export retains harmless =1+1 formula; structural CSV quoting is insufficient, F05. |
|
||||
| 137 | Spreadsheet/import security | O/R | [Assessment](findings.md#f05--p1-csv-quoting-does-not-neutralize-spreadsheet-formulas) — Harmless formula export observed; full spreadsheet/import payload corpus remains. |
|
||||
| 138 | Email-link security | S/R | [Assessment](threat-data.md#abuse-matrix) — Canonical origin and token/session controls reviewed; safe open-redirect/one-use/expiry mail-link matrix remains. |
|
||||
| 139 | Email enumeration | S/R | [Assessment](threat-data.md#abuse-matrix) — Abuse controls exist; timing/response equivalence across register/login/resend/recovery not newly measured. |
|
||||
| 140 | Email deliverability monitoring | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: bounce/delivery, sender reputation and independent provider alerting. |
|
||||
| 141 | User-controlled URLs | S/R | [Assessment](findings.md#f11--p1-conditional-import-dns-validation-is-not-bound-to-the-actual-connection) — DNS-check/connect gap identified; no real private destination probed. |
|
||||
| 142 | SSRF-hardening of importers | S/R | [Assessment](findings.md#f11--p1-conditional-import-dns-validation-is-not-bound-to-the-actual-connection) — DNS-check/connect gap identified; no real private destination probed. |
|
||||
| 143 | HTML sanitisation | S/R | [Assessment](threat-data.md#abuse-matrix) — React/server encoding intended; private iframe flags weaken isolation; no stored-XSS exploit established. |
|
||||
| 144 | Markdown security | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: complete Markdown/raw HTML/link-scheme sink corpus; no regex sanitizer recommended. |
|
||||
| 145 | CSP strategy | S/P | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — CSP absent/incomplete at proxy; stage report-only policy then enforce reviewed directives. |
|
||||
| 146 | Cookie security | S/R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Cookie/CSRF/session controls inspected; deployed Secure/SameSite/domain/subdomain behavior remains. |
|
||||
| 147 | Subdomain trust | R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Needs runtime verification: actual DNS/subdomain ownership, cookie scope and sibling-origin trust. |
|
||||
| 148 | Reverse-proxy trust | S/R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Direct port3000 ingress and forwarded-header chain are conditional risk F04; firewall unverified. |
|
||||
| 149 | Host validation | S/R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Canonical origin/allowlists help; attacker-controlled forwarded scheme/IP must be tested at real ingress. |
|
||||
| 150 | TLS review | R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Needs runtime verification: external TLS chain, renewal, cipher policy and certificate alerts. |
|
||||
| 151 | File-storage architecture | S | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Staged file store, quarantine/reconciliation and package copies reviewed; storage shares host fate. |
|
||||
| 152 | Orphaned-file detection | S/R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Reconciliation exists; interrupted/concurrent file-record drills needed across all artifact categories. |
|
||||
| 153 | Temporary-file management | S/R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Parser temp cleanup and export DeleteOnClose exist; renderer/crash residue needs bounded cleanup proof. |
|
||||
| 154 | Malware scanning strategy | S/P | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Parser bounds are not antivirus; add scanning only with operational/retention/quarantine requirements. |
|
||||
| 155 | Document parser isolation | O/S | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — 37 parser tests pass; subprocess limits and constrained sidecar source reviewed. |
|
||||
| 156 | AI prompt-injection testing suite | P/R | [Assessment](threat-data.md#product-invariants-and-enforcement) — Adversarial job/CV instruction corpus proposed; no live-model injection suite run. |
|
||||
| 157 | AI output validation | O/S | [Assessment](threat-data.md#product-invariants-and-enforcement) — Existing parser/schema validation tests pass; probabilistic fact correctness remains separate. |
|
||||
| 158 | AI action boundaries | S/P | [Assessment](threat-data.md#product-invariants-and-enforcement) — Untrusted job/CV text must not grant tools/permissions or trigger send/payment/publication. |
|
||||
| 159 | Prompt/version management | S/P | [Assessment](threat-data.md#product-invariants-and-enforcement) — Keep versioned prompts/routes and provenance; review rollout/rollback with evaluated fixtures. |
|
||||
| 160 | AI evaluation suite | O/R | [Assessment](threat-data.md#product-invariants-and-enforcement) — Five Ollama evaluation-helper tests pass; no live model quality evaluation. |
|
||||
| 161 | Norwegian-language AI quality | R | [Assessment](threat-data.md#product-invariants-and-enforcement) — Needs runtime verification: Bokmål semantic quality, factual preservation and reviewer scoring. |
|
||||
| 162 | User fact preservation | S/P | [Assessment](threat-data.md#product-invariants-and-enforcement) — Preserve authoritative career facts; reject unsupported structured claims and require review. |
|
||||
| 163 | Human-review requirement | S/P | [Assessment](threat-data.md#product-invariants-and-enforcement) — Human review required before using generated claims in outward-facing artifacts/actions. |
|
||||
| 164 | Subscription-entitlement races | S/R | [Assessment](findings.md#f02--p1-billing-has-no-complete-account-lifecycle-or-concurrent-reconciliation) — Current-state re-fetch helps sequential events; concurrency and reconciliation need sandbox proof. |
|
||||
| 165 | Webhook-order handling | S/R | [Assessment](findings.md#f02--p1-billing-has-no-complete-account-lifecycle-or-concurrent-reconciliation) — Current-state re-fetch helps sequential events; concurrency and reconciliation need sandbox proof. |
|
||||
| 166 | Billing reconciliation | S/R | [Assessment](findings.md#f02--p1-billing-has-no-complete-account-lifecycle-or-concurrent-reconciliation) — Current-state re-fetch helps sequential events; concurrency and reconciliation need sandbox proof. |
|
||||
| 167 | GDPR request workflows | S/P/R | [Assessment](threat-data.md#retention-matrix) — Export/deletion inventory and stages mapped; legal classification/timelines require responsible review. |
|
||||
| 168 | Data-retention matrix | P/R | [Assessment](threat-data.md#retention-matrix) — Retention matrix gives proposed defaults and backup behavior; no deployed-policy compliance claim. |
|
||||
| 169 | Log-retention policy | S/P | [Assessment](threat-data.md#retention-matrix) — Docker file rotation is not complete log retention; set content-safe TTL/custody/access policy. |
|
||||
| 170 | PII-in-logs scan | S | [Assessment](threat-data.md#retention-matrix) — SMTP recipient/subject and client URL/stack logging can contain PII; F16. |
|
||||
| 171 | Sensitive exception data | S/R | [Assessment](threat-data.md#retention-matrix) — Truncation is not redaction; sanitize before logging; complete exception corpus remains. |
|
||||
| 172 | Source-map exposure | R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Needs runtime verification: source maps in final deployed assets and public path exposure. |
|
||||
| 173 | Production debug configuration | S/R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Environment config/source reviewed; final deployed debug/telemetry behavior not inspected. |
|
||||
| 174 | Admin endpoint discovery | O/S | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Admin controllers identified; ordinary-user audit request returns403; complete hosted admin surface remains. |
|
||||
| 175 | Object-level authorization regression suite | O/R | [Assessment](verification.md#normal-verification) — New CV GET/PUT/DELETE owner probes pass; broader existing tests pass; future routes need registry gate. |
|
||||
| 176 | Security regression rule | P | [Assessment](verification.md#normal-verification) — Every security fix retains a meaningful boundary regression; normal green suite is not blanket proof. |
|
||||
| 177 | "Prove the fix" requirement | P | [Assessment](verification.md#normal-verification) — Each finding specifies failing test/expected correction/staging/detection; no fix marked complete here. |
|
||||
| 178 | Root-cause requirement | S/P | [Assessment](findings.md) — Each finding gives root cause, bounded change, regression and rollout expectations. |
|
||||
| 179 | Fix scope | S/P | [Assessment](findings.md) — Each finding gives root cause, bounded change, regression and rollout expectations. |
|
||||
| 180 | Regression-risk assessment | S/P | [Assessment](findings.md) — Each finding gives root cause, bounded change, regression and rollout expectations. |
|
||||
| 181 | Migration strategy for large refactors | P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Expand/backfill/contract and compatibility tests before removing legacy representation. |
|
||||
| 182 | Strangler-style modernisation | P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Replace one demonstrated boundary/caller at a time; avoid unnecessary new services/frameworks. |
|
||||
| 183 | Dead-code confidence | P/R | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Check runtime routes, stored IDs, migrations and deployment references before declaring code dead. |
|
||||
| 184 | Comment quality | P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Explain constraints and intent; remove stale claims through focused documentation changes. |
|
||||
| 185 | Naming consistency | P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Align identifiers with existing domain glossary; avoid broad cosmetic rename churn. |
|
||||
| 186 | Domain-language consistency | S/P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Reconcile UI/API/domain glossary; distinguish job, application record and application letter. |
|
||||
| 187 | Architecture decision records | P | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Record decisions for provider, time, quota, rendering and recovery when implementation choices land. |
|
||||
| 188 | Documentation freshness | S | [Assessment](product-operations.md#feature-maintenance-and-simplification) — Current architecture labels conflict with code; use dated verified source baseline, F17. |
|
||||
| 189 | New-developer setup test | R | [Assessment](verification.md#normal-verification) — Needs runtime verification: fresh machine/clone setup; audit reused installed dependencies. |
|
||||
| 190 | One-command development startup | S/R | [Assessment](verification.md#normal-verification) — Document existing startup paths; one-command clean-environment success not established. |
|
||||
| 191 | Seed/demo data | O/R | [Assessment](verification.md#normal-verification) — Harness seeds safe demo fixtures; general public/demo reset workflow not audited. |
|
||||
| 192 | Test isolation | O/R | [Assessment](verification.md#normal-verification) — Fresh synthetic SQLite and serial backend settings used; CI parallel/shared-resource isolation remains. |
|
||||
| 193 | Flaky-test audit | O/R | [Assessment](verification.md#normal-verification) — One browser run reports zero flaky tests; repeated-run flake rate was not measured. |
|
||||
| 194 | Test-speed audit | O | [Assessment](verification.md#normal-verification) — Observed suite durations recorded; no comparative optimization benchmark performed. |
|
||||
| 195 | Mutation testing | P/R | [Assessment](verification.md#normal-verification) — Target permission/revision/time guards with mutation tests; no mutation score measured. |
|
||||
| 196 | Coverage quality | O/S | [Assessment](verification.md#normal-verification) — Green suite misses builder axe and standalone test type error; count alone is insufficient. |
|
||||
| 197 | Production smoke tests | O/S | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Local core suite passes; deployed public health/ready smoke exists in source, not executed here. |
|
||||
| 198 | Synthetic monitoring | P/R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Content-free synthetic core journey proposed; production probe identity/side effects not configured. |
|
||||
| 199 | Uptime monitoring | P/R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Independent external uptime/readiness monitoring and delivered alert need proof. |
|
||||
| 200 | DNS monitoring | R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Needs runtime verification: authoritative/recursive DNS monitoring and owner escalation. |
|
||||
| 201 | SSL expiry alerts | R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Needs runtime verification: independent certificate expiry alert and renewal drill. |
|
||||
| 202 | Dependency outage communication | P | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Communicate affected optional service/status while core tracking remains usable; avoid misleading success. |
|
||||
| 203 | Maintenance mode | P/R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Document admission/maintenance behavior; actual safe activation not tested. |
|
||||
| 204 | Read-only mode | P/R | [Assessment](product-operations.md#incident-response-and-technical-runbooks) — Read-only mode must deliberately reject mutations and stop writers; no new implementation supplied. |
|
||||
| 205 | Data integrity verification | S/P | [Assessment](threat-data.md#product-invariants-and-enforcement) — Verify owner/FK/version/usage/file invariants without reading private content unnecessarily. |
|
||||
| 206 | Storage reconciliation | S/R | [Assessment](threat-data.md#product-invariants-and-enforcement) — Staged attachment recovery is useful; reconcile CV/package/generated storage accounting too. |
|
||||
| 207 | Database integrity checks | R | [Assessment](threat-data.md#product-invariants-and-enforcement) — Needs runtime verification: production-provider integrity checks, real query plans and restored-row/file hashes. |
|
||||
| 208 | Privacy by default | S | [Assessment](threat-data.md#data-classification) — Private CV default and external-AI consent off are strengths; public copies require explicit lifecycle. |
|
||||
| 209 | Secure defaults | S/R | [Assessment](threat-data.md#data-classification) — Default switches reduce exposure; missing aggregate limits and database fallback still need repair. |
|
||||
| 210 | Fail-fast configuration | S | [Assessment](threat-data.md#data-classification) — Empty/unknown provider can silently select SQLite in app startup, F19. |
|
||||
| 211 | Configuration schema validation | S/P | [Assessment](threat-data.md#data-classification) — Deployment validation does not replace application startup provider/config validation, F19. |
|
||||
| 212 | Environment-specific safeguards | S/P | [Assessment](threat-data.md#data-classification) — Guard intended provider/origin/secrets at startup; prevent demo/development fallback in deployed environment. |
|
||||
| 213 | Secret handling in Docker | S/R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Container environment/key/volume boundaries reviewed; actual host secret custody unverified. |
|
||||
| 214 | Secret handling in CI | S/R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — CI script/runner inputs reviewed; actual secret scoping/masking/fork exposure unverified. |
|
||||
| 215 | Pull-request security | R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Needs runtime verification: untrusted PR execution, runner isolation and credential availability. |
|
||||
| 216 | Branch protection | R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Needs runtime verification: hosted branch protection, required checks and bypass permissions. |
|
||||
| 217 | Deployment permissions | R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Needs runtime verification: deploy environment approvals, scoped SSH and registry credentials. |
|
||||
| 218 | Server hardening | R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Needs runtime verification: OS patching, SSH/firewall/users/audit policy and recovery access. |
|
||||
| 219 | Docker socket exposure | S/R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Compose source does not prove other host containers lack Docker socket; inventory actual mounts. |
|
||||
| 220 | Container privilege review | S | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Backend/root Chromium lacks full privilege/resource isolation; constrained parser shows a better bounded boundary. |
|
||||
| 221 | Network segmentation | S/R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Internal AI network helps; actual shared networks, egress/firewall and lateral reachability unverified. |
|
||||
| 222 | Database exposure | R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — Needs runtime verification: external MariaDB bind/firewall/users/TLS/backups. |
|
||||
| 223 | Admin-interface exposure | S/R | [Assessment](findings.md#f18--p2-supply-chain-and-production-environment-proof-remain-incomplete) — API admin role checks exist; public route/network exposure and operator access require deployed check. |
|
||||
| 224 | CORS audit | S/R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Configured origin boundaries reviewed; full credentialed CORS/preflight/origin bypass matrix remains. |
|
||||
| 225 | CSRF model | S/O | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Cookie mutations require matching CSRF header; local authenticated harness exercises intended path. |
|
||||
| 226 | XSS persistence testing | R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Needs runtime verification: comprehensive persisted HTML/Markdown/URL payloads across all sinks. |
|
||||
| 227 | Content upload/download headers | S/O/R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Sample cache gaps observed; full content-type/disposition/nosniff matrix remains. |
|
||||
| 228 | Separate download origin | P | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Consider isolated download/render origin for untrusted content with measured requirements; not automatic new service. |
|
||||
| 229 | CSP report strategy | P/R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Report-only CSP can identify required sources; sanitize/rate-limit reports before enforcement. |
|
||||
| 230 | Referrer policy | S/R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Proxy referrer policy exists; public/private downstream-link leakage needs deployed verification. |
|
||||
| 231 | Permissions policy | S/R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Proxy permissions policy exists; check final responses and iframe requirements. |
|
||||
| 232 | Browser security headers | S/O/R | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Source proxy headers inspected and local cache samples checked; complete deployed policy unverified. |
|
||||
| 233 | Security-header regression test | P | [Assessment](findings.md#f15--p2-private-caching-and-browser-security-headers-are-incomplete) — Add endpoint/browser header regressions for actual trusted ingress and sensitive response classes. |
|
||||
| 234 | Privacy-policy alignment | P/R | [Assessment](threat-data.md#retention-matrix) — Policy must match actual mail/AI/billing/backups/logs/support access; no legal compliance conclusion. |
|
||||
| 235 | Terms/consent versioning | P/R | [Assessment](threat-data.md#retention-matrix) — Record policy/version/time for material consent; deployed persistence and consent change UX unverified. |
|
||||
| 236 | Cookie consent | P/R | [Assessment](threat-data.md#retention-matrix) — Inventory cookies/storage/external assets first; responsible advisor decides applicable consent obligations. |
|
||||
| 237 | Product deletion vs billing | S/R | [Assessment](findings.md#f02--p1-billing-has-no-complete-account-lifecycle-or-concurrent-reconciliation) — Deletion does not complete subscription cancellation/reconciliation. |
|
||||
| 238 | Email-address change workflow | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: confirm new email, notification/recovery/session consequences and collision cases. |
|
||||
| 239 | Password-change workflow | S/R | [Assessment](threat-data.md#abuse-matrix) — Session-stamp revocation patterns exist; complete change/reset/trusted-device UX matrix remains. |
|
||||
| 240 | 2FA recovery | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: recovery-code single use, lockout, device loss and accessible recovery. |
|
||||
| 241 | OAuth account linking | O/S | [Assessment](threat-data.md#abuse-matrix) — Existing linking/security tests pass; live Google/Microsoft provider journey not called. |
|
||||
| 242 | Deleted/recreated email account | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: delete/recreate email and provider identity collision; never inherit old ownership. |
|
||||
| 243 | Case-normalisation | S/R | [Assessment](threat-data.md#abuse-matrix) — Identity normalization exists; exhaustive email/source/provider case comparisons remain. |
|
||||
| 244 | Unicode-normalisation security | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: Unicode normalization/confusables in security identifiers; display spelling must remain. |
|
||||
| 245 | Job URL canonicalisation | O/S | [Assessment](threat-data.md#product-invariants-and-enforcement) — Three extension capture-URL tests pass; canonicalization must not conflate distinct resource identity. |
|
||||
| 246 | Duplicate detection | O/S | [Assessment](threat-data.md#product-invariants-and-enforcement) — Same-intent job POST duplicates observed; define dedup intent before enforcing uniqueness. |
|
||||
| 247 | Referential history | S | [Assessment](threat-data.md#product-invariants-and-enforcement) — Preserve submitted package evidence across edits/deletes; live links must handle removed resources. |
|
||||
| 248 | Event/history integrity | S/R | [Assessment](threat-data.md#product-invariants-and-enforcement) — History versions do not prevent conflict; verify append integrity and owner references across deletion. |
|
||||
| 249 | Audit vs activity timeline | S | [Assessment](threat-data.md#product-invariants-and-enforcement) — Global job activity exposes content and is not a minimal privileged security ledger; F13. |
|
||||
| 250 | Search privacy | S/R | [Assessment](threat-data.md#abuse-matrix) — Owner-filtered search required; logs/URLs and client residue can undermine privacy. |
|
||||
| 251 | Pagination security | S/R | [Assessment](threat-data.md#abuse-matrix) — Pagination cannot bypass owner scope; full filter/cursor/soft-delete authorization corpus remains. |
|
||||
| 252 | Sorting/filter injection | S/R | [Assessment](threat-data.md#abuse-matrix) — EF parameterization helps values; dynamic sort/filter names need allowlists and rejection tests. |
|
||||
| 253 | Search denial-of-service | S | [Assessment](threat-data.md#abuse-matrix) — Unbounded materialization and user-owned row growth create resource risk; F10. |
|
||||
| 254 | Regex review | R | [Assessment](threat-data.md#abuse-matrix) — Needs source/runtime verification: complete regex inventory, timeouts and adversarial backtracking corpus. |
|
||||
| 255 | Serialization review | S/R | [Assessment](threat-data.md#abuse-matrix) — DTO/JSON/parser boundaries inspected selectively; serializer depth/type/size fuzz matrix remains. |
|
||||
| 256 | DTO boundary enforcement | S/R | [Assessment](threat-data.md#abuse-matrix) — Explicit DTOs help; owner/role/entitlement/version fields need negative assignment regressions. |
|
||||
| 257 | Mass-assignment testing | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: extra JSON owner/admin/plan/deleted/version fields on all mutations. |
|
||||
| 258 | Numeric-boundary testing | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: negative/overflow/precision/NaN/infinity inputs for all numeric domains. |
|
||||
| 259 | Nullability testing | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: missing/null/empty distinctions and required fields across all DTOs. |
|
||||
| 260 | Enum evolution | P/R | [Assessment](threat-data.md#abuse-matrix) — Treat new/unknown enum values deliberately in old clients/imports; compatibility tests proposed. |
|
||||
| 261 | Soft-delete filter correctness | O/S | [Assessment](resilience.md#transaction-and-background-work-review) — Global owner/soft-delete filters and regression suites useful; administrative bypasses need explicit review. |
|
||||
| 262 | EF Core query review | S/R | [Assessment](resilience.md#transaction-and-background-work-review) — Read-all exports/boards and quota sums identified; production-provider query plan measurements pending. |
|
||||
| 263 | Connection-pool behaviour | R | [Assessment](resilience.md#transaction-and-background-work-review) — Needs runtime verification: MariaDB pool exhaustion/waits/timeouts; SQLite differs; PostgreSQL absent. |
|
||||
| 264 | Database timeout handling | R | [Assessment](resilience.md#transaction-and-background-work-review) — Needs runtime verification: database timeout UX, host survival and accepted-operation recovery. |
|
||||
| 265 | Migration locking | R | [Assessment](resilience.md#transaction-and-background-work-review) — Needs runtime verification: concurrent migrators/provider lock behavior and crash recovery. |
|
||||
| 266 | Index migration safety | R | [Assessment](resilience.md#transaction-and-background-work-review) — Needs runtime verification: index build lock/disk/time on realistic MariaDB and SQLite shape. |
|
||||
| 267 | Database backup consistency | S/R | [Assessment](resilience.md#transaction-and-background-work-review) — Live deploy tar is not consistency proof; daily VACUUM INTO is stronger; restore drill F12. |
|
||||
| 268 | Encryption at rest | R | [Assessment](threat-data.md#data-classification) — Needs runtime verification: actual database/file/backup disk encryption and off-site key custody. |
|
||||
| 269 | Encryption in transit | R | [Assessment](threat-data.md#data-classification) — Needs runtime verification: external HTTPS and DB/provider transport configuration. |
|
||||
| 270 | Field-level encryption | S/P | [Assessment](threat-data.md#data-classification) — Protect provider tokens/TOTP using existing platform primitives; no custom searchable encryption proposed. |
|
||||
| 271 | Encryption-key management | S/R | [Assessment](threat-data.md#data-classification) — Data Protection key ring is part of recovery and exposure boundary; live custody/rotation unproven. |
|
||||
| 272 | Search versus encryption trade-off | P | [Assessment](threat-data.md#data-classification) — Encrypting searchable career fields adds query/rotation cost; require threat need and explicit tradeoff. |
|
||||
| 273 | Screenshot/print privacy | P/R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Sensitive screen/print output needs user intent; browser/screenshot/print retention not globally preventable. |
|
||||
| 274 | Public-link security | S/R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Unpredictable public slug, rotation/revocation and rate rules help; aggregate PDF control missing. |
|
||||
| 275 | Public-profile privacy | S/P | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Public profile is explicit publication; show exactly what fields/metadata become visible before sharing. |
|
||||
| 276 | robots/noindex | S | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Public HTML has noindex; PDF parity incomplete; robots directives are not access control. |
|
||||
| 277 | Attachment metadata privacy | R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Needs runtime verification: EXIF/document properties and internal IDs in attachment outputs. |
|
||||
| 278 | PDF metadata | R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Needs runtime verification: generated PDF author/title/producer/path/language metadata and reading order. |
|
||||
| 279 | Generated filename safety | S/R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Filename construction reviewed selectively; Unicode/path/control-character download corpus remains. |
|
||||
| 280 | Content-disposition encoding | R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Needs runtime verification: Unicode Content-Disposition compatibility and CRLF/control rejection. |
|
||||
| 281 | Browser download behaviour | O/R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Existing PDF download browser checks pass; Safari/mobile/cancel/retry/large-download matrix remains. |
|
||||
| 282 | Temporary download URLs | S/P/R | [Assessment](threat-data.md#data-lifecycle-and-copy-map) — Owner/public-link boundaries mapped; expiry/single-use rules for any temporary capability need specific contract tests. |
|
||||
| 283 | Storage quota | S | [Assessment](threat-data.md#abuse-matrix) — Attachment byte sum is racy and excludes other retained artifacts; aggregate quota F10. |
|
||||
| 284 | Usage accounting | S | [Assessment](threat-data.md#abuse-matrix) — AI usage estimates and file accounting need reservation/reconciliation across categories/processes. |
|
||||
| 285 | Quota UX | P/R | [Assessment](threat-data.md#abuse-matrix) — Explain quota used/remaining and recoverable choices; preserve data rather than delete silently. |
|
||||
| 286 | Downgrade behaviour | S/R | [Assessment](threat-data.md#abuse-matrix) — Server entitlement rechecks help; downgrade over-quota handling and UI sandbox journey remain. |
|
||||
| 287 | Trial-expiry UX | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: configured trial states and expiry warning/action behavior. |
|
||||
| 288 | Billing failure UX | R | [Assessment](threat-data.md#abuse-matrix) — Needs runtime verification: failed payment, dunning, grace period and delayed event UX. |
|
||||
| 289 | Norwegian consumer considerations | R | [Assessment](product-operations.md#navigation-and-domain-language) — Requires responsible legal/commercial review; no Norway consumer-law compliance determination made. |
|
||||
| 290 | International readiness | P/R | [Assessment](product-operations.md#navigation-and-domain-language) — Norwegian-first product can retain explicit locale/currency/time semantics; no all-market readiness claim. |
|
||||
| 291 | Address model | P/R | [Assessment](resilience.md#time-and-compatibility-policy) — Use optional structured address/display text; international addresses and no-postcode cases need tests. |
|
||||
| 292 | Telephone-number model | P/R | [Assessment](resilience.md#time-and-compatibility-policy) — Preserve international prefix/display input; avoid Norway-only length assumptions; round-trip unverified. |
|
||||
| 293 | Salary model | P/R | [Assessment](resilience.md#time-and-compatibility-policy) — Salary needs amount/range/currency/period; never silently treat annual/monthly or currencies alike. |
|
||||
| 294 | Workplace model | P/R | [Assessment](resilience.md#time-and-compatibility-policy) — Distinguish remote/hybrid/on-site and location text; avoid country-specific required fields without need. |
|
||||
| 295 | Job-source model | S/P | [Assessment](resilience.md#time-and-compatibility-policy) — Keep source URL/provenance/acquisition time; canonicalization must not erase meaningful distinctions. |
|
||||
| 296 | Company model | P/R | [Assessment](resilience.md#time-and-compatibility-policy) — Company display spelling and normalized search separate; history/merge policy needs explicit tests. |
|
||||
| 297 | Contact model | P/R | [Assessment](resilience.md#time-and-compatibility-policy) — Contact details remain private owner data; deletion/normalization and contact role semantics need tests. |
|
||||
| 298 | Correspondence model | S/R | [Assessment](resilience.md#time-and-compatibility-policy) — Mail content/tokens/attempts have distinct lifecycle; uncertain send is not safe to repeat. |
|
||||
| 299 | Reminder model | O/S | [Assessment](resilience.md#time-and-compatibility-policy) — Calendar instant bug observed; reminder delivery/time-zone/DST recurrence remains F07/F21. |
|
||||
| 300 | Notification preferences | S/R | [Assessment](resilience.md#time-and-compatibility-policy) — Reminder preference checks exist; every channel/category/opt-out needs delivery-level verification. |
|
||||
| 301 | Accessibility preferences | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: persist/obey reduced motion, contrast/text/input preferences. |
|
||||
| 302 | High-contrast support | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: Windows forced colors/high contrast; dark mode is not equivalent. |
|
||||
| 303 | Windows accessibility testing | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: Windows Narrator/NVDA and native keyboard/platform behavior. |
|
||||
| 304 | Text-spacing WCAG testing | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: required text spacing without clipped/hidden controls or data. |
|
||||
| 305 | Reflow testing | O/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Outer builder overflow0 at375/1440; actual zoom/320 effective width reflow remains. |
|
||||
| 306 | Orientation | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: portrait/landscape and no unjustified orientation restriction. |
|
||||
| 307 | Pointer gestures | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: single-pointer alternatives to complex/path-based gestures. |
|
||||
| 308 | Drag-and-drop alternatives | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: keyboard/status-menu alternative to drag/drop and accessible announcements. |
|
||||
| 309 | Auto-updating content | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: polling/preview/autosave updates preserve focus and offer understandable announcements. |
|
||||
| 310 | Session timeout accessibility | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: timeout warning, extension/reauthentication and preserved accessible draft. |
|
||||
| 311 | Authentication accessibility | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: cognitive/keyboard/screen-reader authentication and error recovery. |
|
||||
| 312 | CAPTCHA fallback | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: configured Turnstile failure/unavailable/accessibility fallback. |
|
||||
| 313 | Password-manager support | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: password-manager fill/save, paste and change/reset flows. |
|
||||
| 314 | Autocomplete attributes | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs source/runtime verification: appropriate autocomplete/inputmode across identity/contact forms. |
|
||||
| 315 | Copy-link behaviour | S/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Clipboard promise must determine success feedback; denied permission matrix unexecuted. |
|
||||
| 316 | Focus after navigation | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: route change moves/announces focus predictably without trapping users. |
|
||||
| 317 | Page-title updates | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: distinct localized page titles for SPA routes. |
|
||||
| 318 | Live-region strategy | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: meaningful save/error/queue live regions without repeated announcement spam. |
|
||||
| 319 | AI streaming accessibility | P/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — For streamed AI output, announce status/completion without token-by-token speech; current full flow unverified. |
|
||||
| 320 | Reduced-data considerations | P/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Avoid mandatory heavy AI/assets for core tracking; reduced-data behavior not measured. |
|
||||
| 321 | Asset caching | S/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Static-export caching needs version policy; actual proxy/CDN headers and stale-asset behavior unverified. |
|
||||
| 322 | Service-worker audit | N/P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — No deployed service-worker behavior established; do not introduce private offline caches during this repair. |
|
||||
| 323 | PWA decision | P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — PWA is not a current launch requirement; first solve revision and recoverable draft contracts. |
|
||||
| 324 | Mobile keyboard behaviour | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: real mobile keyboard overlap, viewport resizing and focused fields. |
|
||||
| 325 | Safe-area insets | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: device safe-area insets and bottom action reachability. |
|
||||
| 326 | Touch hover assumptions | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: every hover-only action has touch/keyboard access. |
|
||||
| 327 | Right-click/middle-click | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: navigation links support middle/right click and modified keyboard activation. |
|
||||
| 328 | Copyability | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: key IDs/errors/output text remain selectable/copyable with clear feedback. |
|
||||
| 329 | Scroll restoration | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: useful scroll restoration for lists/detail/back/refresh. |
|
||||
| 330 | Sticky element collisions | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: stacked sticky toolbars at zoom/mobile/landscape do not obscure content. |
|
||||
| 331 | Anchor offset | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: anchor targets and focused fields clear fixed headers. |
|
||||
| 332 | Z-index audit | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs source/runtime verification: stacking contexts, overlay order and hidden focus under portals. |
|
||||
| 333 | Portal usage | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: portal theme/context/focus/click-outside behavior. |
|
||||
| 334 | Modal nesting | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: modal nesting, one focus trap and escape/back order. |
|
||||
| 335 | Escape-key consistency | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: Escape semantics across menus/dialogs/drawer/unsaved prompts. |
|
||||
| 336 | Form submission semantics | O/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Basic login/forms exercised; all Enter/submission/preventDefault/validation paths remain. |
|
||||
| 337 | Double-submit protection | O/S | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Repeated creation creates two rows; editor queue only protects one instance, F01. |
|
||||
| 338 | Optimistic UI correctness | S/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Optimistic success must reconcile failures/conflicts; complete delayed/out-of-order response suite remains. |
|
||||
| 339 | Stale cache correctness | S/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Old editor state overwrites newer server state; no complete revision/stale-cache contract. |
|
||||
| 340 | Cross-account cache clearing | O/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Cross-tab logout leaves editor content; full account-switch and browser Back cache sweep remains. |
|
||||
| 341 | Sensitive browser cache | O | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Auth/CV/CSV sampled responses lack no-store; no actual shared-cache disclosure demonstrated. |
|
||||
| 342 | Clipboard security | S/P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Copy only deliberate values; acknowledge clipboard rejection; clipboard retention is outside server control. |
|
||||
| 343 | Autofill/privacy | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: autofill suggestions, shared-device residue and sensitive form storage. |
|
||||
| 344 | Telemetry failures | R | [Assessment](resilience.md#dependency-and-operation-failure-matrix) — Needs runtime verification: telemetry loss must not block normal save/auth actions. |
|
||||
| 345 | Logging failures | S/R | [Assessment](resilience.md#dependency-and-operation-failure-matrix) — PII logging risks identified; disk-full/log-sink outage and blocking/backpressure behavior untested. |
|
||||
| 346 | Monitoring dependency | P/R | [Assessment](resilience.md#dependency-and-operation-failure-matrix) — Independent monitor/dead-man signal proposed; app host must not be sole observer of its failure. |
|
||||
| 347 | Health-check side effects | S | [Assessment](resilience.md#dependency-and-operation-failure-matrix) — Liveness/readiness separation exists; probes should not trigger costly provider calls or writes. |
|
||||
| 348 | Readiness semantics | S/R | [Assessment](resilience.md#dependency-and-operation-failure-matrix) — Ready checks database; optional-service readiness semantics and outage behavior need deployed proof. |
|
||||
| 349 | Startup ordering | O/R | [Assessment](resilience.md#dependency-and-operation-failure-matrix) — Local fresh startup succeeds; external database/proxy/model ordering and restart races untested. |
|
||||
| 350 | Graceful shutdown | S/R | [Assessment](resilience.md#dependency-and-operation-failure-matrix) — Cancellation/leases reviewed; host termination during writes/render/send needs isolated drill. |
|
||||
| 351 | Deployment interruption | R | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Needs runtime verification: interruption at backup/migration/replace/smoke checkpoints. |
|
||||
| 352 | Zero-downtime realism | S/R | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Single-host Compose replacement is not demonstrated zero downtime; define acceptable maintenance window. |
|
||||
| 353 | Database migration rollback limitations | S | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Old-image rollback cannot reverse destructive/provider-specific schema changes, F12. |
|
||||
| 354 | Backup before destructive migration | S/R | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Predeploy backup exists but consistency/integrity must be proven before destructive migration. |
|
||||
| 355 | Deployment observability | S/P | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Deploy smoke/logs help; correlate release/config/backup/migration status with user impact. |
|
||||
| 356 | Release correlation | P/R | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Expose safe commit/artifact/config identifiers in diagnostics; actual full release correlation not proven. |
|
||||
| 357 | Feature rollout strategy | P | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Use small cohort and observed gates; percentage-rollout platform unnecessary at this scale. |
|
||||
| 358 | Kill switches | S/P | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Document worker/provider/registration switches and effect on accepted work; test both states. |
|
||||
| 359 | Configuration change audit | P/R | [Assessment](product-operations.md#release-gates-and-production-readiness-checklist) — Privileged config changes need safe actor/time/reason record; actual operational audit unverified. |
|
||||
| 360 | Admin safety | S | [Assessment](threat-data.md#assets-and-intended-access) — Admin minimum scope/recent auth/immutable audit incomplete; normal user denied, F13. |
|
||||
| 361 | Bulk operations | P/R | [Assessment](threat-data.md#assets-and-intended-access) — Require bounded batches, explicit targets, preview/recovery and owner checks per item; complete workflow untested. |
|
||||
| 362 | Batch API design | P | [Assessment](threat-data.md#assets-and-intended-access) — Batch cost must be bounded per item and aggregate; specify partial-success/idempotency contract first. |
|
||||
| 363 | Background exports | S/P | [Assessment](resilience.md#transaction-and-background-work-review) — Large export generation should be bounded/streamed or durable queued; existing JSON collections buffer. |
|
||||
| 364 | Email queue | S/P | [Assessment](resilience.md#transaction-and-background-work-review) — Use database outbox for transactional auth/reminder mail; no broker required initially. |
|
||||
| 365 | Email idempotency | S | [Assessment](resilience.md#transaction-and-background-work-review) — Direct sends and commit order can duplicate or lose outcome; ambiguous external send needs reconciliation. |
|
||||
| 366 | Reminder delivery reliability | S | [Assessment](resilience.md#transaction-and-background-work-review) — Reminder loop sends before common SaveChanges; disabled sender can mark sent, F07. |
|
||||
| 367 | Time-sensitive jobs | S/R | [Assessment](resilience.md#time-and-compatibility-policy) — Backup local-hour cron has DST skip/double risk; date-based catch-up/dedup drill needed. |
|
||||
| 368 | Clock dependence | S/R | [Assessment](resilience.md#time-and-compatibility-policy) — UTC lease/deadline/time-provider conventions required; substantial clock-skew drill unexecuted. |
|
||||
| 369 | Client/server time mismatch | O/R | [Assessment](resilience.md#time-and-compatibility-policy) — Explicit offset round-trip is already wrong F21; actual client/server skew matrix remains. |
|
||||
| 370 | Pagination limits | S | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Paged endpoints do not bound unpaged board/analytics/export paths; cap all expensive work. |
|
||||
| 371 | Export limits | O/S | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Export limiter shares IP across accounts; generation still buffers, F06/F10. |
|
||||
| 372 | Search-index strategy | P/R | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Inspect actual provider plans before new search index/server; owner-scoped queries first. |
|
||||
| 373 | Redis decision | N/P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Redis absent; no measured cache/shared-state need justifies adding it now. |
|
||||
| 374 | Queue decision | S/P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Existing durable database AI queue useful; extend precise delivery semantics rather than add broker. |
|
||||
| 375 | Modular-monolith assessment | S/P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Modular monolith is appropriate; current controllers/services/EF support focused boundary repair. |
|
||||
| 376 | Microservice boundary warning | P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Independent deployment adds failure/transaction/operational cost; do not split domain services speculatively. |
|
||||
| 377 | Scalability myths | P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — User count is not concurrency/capacity proof; benchmark actual workload and bottlenecks. |
|
||||
| 378 | Cost architecture | P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Host/DB/storage/backups/egress/mail/AI/operator-time cost model supplied; no current prices claimed. |
|
||||
| 379 | Cost observability | P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Measure per-success workload cost including failed/retried attempts and actual provider charges. |
|
||||
| 380 | Cost abuse alerts | P/R | [Assessment](product-operations.md#performance-capacity-and-cost-model) — User/workflow/global budget anomaly and admission alerts proposed; delivery not tested. |
|
||||
| 381 | Data-growth projections | P | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Explicit synthetic storage growth example; history/package/backups multiply retained bytes. |
|
||||
| 382 | Archive strategy | P/R | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Set retention and owner-safe archive semantics before introducing partitioning/services. |
|
||||
| 383 | Query-plan testing | R | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Needs runtime verification: realistic SQLite/MariaDB EXPLAIN plans and worst-owner data. |
|
||||
| 384 | Performance regression testing | P/R | [Assessment](product-operations.md#performance-capacity-and-cost-model) — Define p95/p99/memory/bytes baseline and budgets; no load-regression gate measured. |
|
||||
| 385 | Bundle regression | O/P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Static build passes; route payload/bundle growth budgets proposed, not benchmarked. |
|
||||
| 386 | Lazy-loading strategy | S/P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Retain route-level loading and split only measured heavy paths; no speculative dynamic-import rewrite. |
|
||||
| 387 | Third-party frontend scripts | O/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Google Fonts request was blocked in harness; complete production third-party script inventory remains. |
|
||||
| 388 | Font strategy | O/P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Fallback-font rendering captured; evaluate approved self-hosting and compare document layout. |
|
||||
| 389 | Image strategy | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: actual asset formats/sizes/alt text/lazy behavior and layout shifts. |
|
||||
| 390 | Skeleton accessibility | P/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Skeletons must preserve layout and expose meaningful busy state; no all-component proof. |
|
||||
| 391 | Motion performance | P/R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Honor reduced motion and avoid expensive animation; profile real low-end/mobile devices. |
|
||||
| 392 | CSS performance | R | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Needs runtime verification: style recalculation/layout/paint hot paths before CSS optimization. |
|
||||
| 393 | Memory leaks frontend | R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Needs runtime verification: repeated route/editor mount, timers/listeners/iframe/blob memory growth. |
|
||||
| 394 | Request cancellation | S/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Audit cancellation at component and downstream boundaries; full late-response/unmount harness absent. |
|
||||
| 395 | Race-condition frontend testing | O/R | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Stale and simultaneous CV saves demonstrate race; all frontend response reorder cases remain. |
|
||||
| 396 | Debounce/throttle audit | S | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Editor debounce/save queue helps one instance; no cross-client conflict or durable recovery guarantee. |
|
||||
| 397 | Autosave state indicator | O/S | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Saved/failed indicator and local queue exist; prevent false success across stale/conflicting revisions. |
|
||||
| 398 | Unsaved-change protection | O | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Unload warning exists and was deliberately accepted in reload probe; do not report absent warning. |
|
||||
| 399 | Draft recovery | O | [Assessment](resilience.md#resilience-test-matrix--break-jobjakt) — Offline unsaved edit not restored after confirmed reload; scope privacy-aware draft recovery. |
|
||||
| 400 | Final cross-cutting audit deliverables | P | [Assessment](README.md#read-the-assessment) — Requested matrices, models, runbooks and acceptance deliverables supplied. |
|
||||
| 401 | Add a "What happens if...?" section | S/O/R | [Assessment](resilience.md#what-happens-if) — All 50 questions answered; observed/source/runtime boundaries stated individually. |
|
||||
| 402 | Add a "Break Jobjakt" testing phase | O/R | [Assessment](verification.md#break-jobjakt-observations) — 27 observations, 22 unmet expectations; remaining safe staging scenarios listed. |
|
||||
| 403 | Add a "Commercial product readiness" assessment | P | [Assessment](README.md#commercial-assessment) — Personal, 100-user, 10,000-user and charging-money readiness judgments with blockers. |
|
||||
| 404 | Add a "Professional polish" checklist | S/O/P | [Assessment](product-operations.md#visual-accessibility-and-professional-polish-review) — Professional polish review/checklist and retained synthetic screenshots. |
|
||||
| 405 | Console-cleanliness requirement | O/R | [Assessment](verification.md#console-network-and-backend-logs) — Local logs inspected; injected failures separated from normal warnings; production remains unverified. |
|
||||
| 406 | Network-cleanliness requirement | O/R | [Assessment](verification.md#console-network-and-backend-logs) — Local logs inspected; injected failures separated from normal warnings; production remains unverified. |
|
||||
| 407 | Backend-log cleanliness | O/R | [Assessment](verification.md#console-network-and-backend-logs) — Fresh startup migration/key warnings recorded; no recurrent normal request exception observed; production logs uninspected. |
|
||||
| 408 | Accessibility cleanliness | O/R | [Assessment](findings.md#f14--p2-launch-quality-gate-builder-accessibility-is-outside-the-green-axe-suite) — Serious builder findings in all eight states; manual screen reader still required. |
|
||||
| 409 | No known P0 before launch | P | [Assessment](README.md#launch-blockers-in-execution-order) — No confirmed P0 in scope; unresolved P1s and missing production gates block launch. |
|
||||
| 410 | Definition of Done | P | [Assessment](product-operations.md#definition-of-done-and-practical-pr-checklists) — Tailored DoD and PR/security/UI/database/deployment checklists supplied. |
|
||||
| 411 | PR checklist | P | [Assessment](product-operations.md#definition-of-done-and-practical-pr-checklists) — Tailored DoD and PR/security/UI/database/deployment checklists supplied. |
|
||||
| 412 | Security checklist | P | [Assessment](product-operations.md#definition-of-done-and-practical-pr-checklists) — Tailored DoD and PR/security/UI/database/deployment checklists supplied. |
|
||||
| 413 | UI checklist | P | [Assessment](product-operations.md#definition-of-done-and-practical-pr-checklists) — Tailored DoD and PR/security/UI/database/deployment checklists supplied. |
|
||||
| 414 | Database-change checklist | P | [Assessment](product-operations.md#definition-of-done-and-practical-pr-checklists) — Tailored DoD and PR/security/UI/database/deployment checklists supplied. |
|
||||
| 415 | Deployment checklist | P | [Assessment](product-operations.md#definition-of-done-and-practical-pr-checklists) — Tailored DoD and PR/security/UI/database/deployment checklists supplied. |
|
||||
| 416 | Final challenge | P | [Assessment](README.md#commercial-assessment) — Measurable correctness/security/a11y/recovery criteria define paid-user recommendation. |
|
||||
@@ -0,0 +1,674 @@
|
||||
{
|
||||
"generatedAt": "2026-09-07T10:27:52.479Z",
|
||||
"scope": "isolated SQLite and local Chromium; synthetic data; outbound browser requests blocked",
|
||||
"findings": [
|
||||
{
|
||||
"scenario": "stale CV save",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"first": 200,
|
||||
"staleSave": 200,
|
||||
"overwritten": true,
|
||||
"version": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "simultaneous CV saves detect conflict",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"statuses": [
|
||||
200,
|
||||
200
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "CV cross-owner get",
|
||||
"meetsExpectation": true,
|
||||
"observation": {
|
||||
"status": 404
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "CV cross-owner put",
|
||||
"meetsExpectation": true,
|
||||
"observation": {
|
||||
"status": 404
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "CV cross-owner delete",
|
||||
"meetsExpectation": true,
|
||||
"observation": {
|
||||
"status": 404
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "duplicate job creation deduplicated",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"statuses": [
|
||||
201,
|
||||
201
|
||||
],
|
||||
"distinctIds": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "DST calendar instant 2026-03-28T09:00:00+01:00",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"status": 200,
|
||||
"expectedUtc": "20260328T080000Z",
|
||||
"actual": "20260328T090000Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "DST calendar instant 2026-03-29T09:00:00+02:00",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"status": 200,
|
||||
"expectedUtc": "20260329T070000Z",
|
||||
"actual": "20260329T090000Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "DST calendar instant 2026-10-25T02:30:00+02:00",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"status": 200,
|
||||
"expectedUtc": "20261025T003000Z",
|
||||
"actual": "20261025T023000Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "DST calendar instant 2026-10-25T02:30:00+01:00",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"status": 200,
|
||||
"expectedUtc": "20261025T013000Z",
|
||||
"actual": "20261025T023000Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "CSV formula neutralized",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"formulaCellPresent": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "sensitive response cache /api/auth/me",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"cacheControl": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "sensitive response cache /api/cv/variants/1",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"cacheControl": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "sensitive response cache /api/export/jobs",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"cacheControl": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "standard user admin denial",
|
||||
"meetsExpectation": true,
|
||||
"observation": {
|
||||
"status": 403
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "account export limiter isolation",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"otherAccountStatusAfterTwoExports": 429
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "builder layout en/light/375",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"overflow": 0,
|
||||
"actualLanguage": "en",
|
||||
"actualTheme": "light",
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1guz6s > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1d1pejv > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(2) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(4) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(6) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "builder layout en/light/1440",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"overflow": 0,
|
||||
"actualLanguage": "en",
|
||||
"actualTheme": "light",
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1guz6s > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1d1pejv > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(2) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(4) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(6) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "scrollable-region-focusable",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1yn80jq"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element should have focusable content\n Element should be focusable"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "builder layout en/dark/375",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"overflow": 0,
|
||||
"actualLanguage": "en",
|
||||
"actualTheme": "dark",
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".MuiButtonGroup-lastButton"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 9.8pt (13px), font weight: bold). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-a5lp7y > .MuiButton-text.MuiButton-textPrimary.MuiButton-textSizeSmall"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1ebacnx > .MuiButton-text.MuiButton-textPrimary.MuiButton-textSizeSmall"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1ebacnx > .MuiButton-outlined.MuiButton-outlinedPrimary.MuiButton-outlinedSizeSmall"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".Mui-selected.MuiTab-root.MuiTab-textColorPrimary"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-va5cz9 > .MuiButton-outlined.MuiButton-outlinedPrimary.MuiButton-outlinedSizeSmall"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "builder layout en/dark/1440",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"overflow": 0,
|
||||
"actualLanguage": "en",
|
||||
"actualTheme": "dark",
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".MuiButtonGroup-lastButton"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 9.8pt (13px), font weight: bold). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".MuiButton-sizeMedium"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".MuiButton-text"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1ebacnx > .MuiButton-outlinedSizeSmall.css-3r48dj.MuiButton-outlined"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".Mui-selected.MuiTab-root.MuiTab-textColorPrimary"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-va5cz9 > .MuiButton-outlinedSizeSmall.css-3r48dj.MuiButton-outlined"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "scrollable-region-focusable",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1yn80jq"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element should have focusable content\n Element should be focusable"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "builder layout nb/light/375",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"overflow": 0,
|
||||
"actualLanguage": "nb-NO",
|
||||
"actualTheme": "light",
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1guz6s > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1d1pejv > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(2) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(4) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(6) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "builder layout nb/light/1440",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"overflow": 0,
|
||||
"actualLanguage": "nb-NO",
|
||||
"actualTheme": "light",
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1guz6s > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1d1pejv > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(2) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(4) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(6) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "scrollable-region-focusable",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1yn80jq"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element should have focusable content\n Element should be focusable"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "builder layout nb/dark/375",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"overflow": 0,
|
||||
"actualLanguage": "nb-NO",
|
||||
"actualTheme": "dark",
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".MuiButtonGroup-firstButton"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 9.8pt (13px), font weight: bold). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-a5lp7y > .MuiButton-text.MuiButton-textPrimary.MuiButton-textSizeSmall"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1ebacnx > .MuiButton-text.MuiButton-textPrimary.MuiButton-textSizeSmall"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1ebacnx > .MuiButton-outlined.MuiButton-outlinedPrimary.MuiButton-outlinedSizeSmall"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".Mui-selected.MuiTab-root.MuiTab-textColorPrimary"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-va5cz9 > .MuiButton-outlined.MuiButton-outlinedPrimary.MuiButton-outlinedSizeSmall"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "builder layout nb/dark/1440",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"overflow": 0,
|
||||
"actualLanguage": "nb-NO",
|
||||
"actualTheme": "dark",
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".MuiButtonGroup-firstButton"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 9.8pt (13px), font weight: bold). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".MuiButton-sizeMedium"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".MuiButton-text"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1ebacnx > .MuiButton-outlinedSizeSmall.css-3r48dj.MuiButton-outlined"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".Mui-selected.MuiTab-root.MuiTab-textColorPrimary"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-va5cz9 > .MuiButton-outlinedSizeSmall.css-3r48dj.MuiButton-outlined"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "scrollable-region-focusable",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1yn80jq"
|
||||
],
|
||||
"failureSummary": "Fix any of the following:\n Element should have focusable content\n Element should be focusable"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "offline save reports failure",
|
||||
"meetsExpectation": true,
|
||||
"observation": {
|
||||
"matchingFailureMessages": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "offline draft survives confirmed refresh",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"draftRecovered": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario": "logout clears other tab immediately",
|
||||
"meetsExpectation": false,
|
||||
"observation": {
|
||||
"privateEditorStillMounted": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: net::ERR_FAILED",
|
||||
"Failed to load resource: net::ERR_FAILED",
|
||||
"Failed to load resource: net::ERR_FAILED",
|
||||
"Failed to load resource: net::ERR_FAILED",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"Failed to load resource: net::ERR_FAILED",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"Failed to load resource: net::ERR_FAILED",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"Failed to load resource: net::ERR_FAILED",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"Failed to load resource: net::ERR_FAILED",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"Failed to load resource: net::ERR_FAILED",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"Failed to load resource: net::ERR_INTERNET_DISCONNECTED",
|
||||
"Failed to load resource: net::ERR_INTERNET_DISCONNECTED",
|
||||
"Failed to load resource: net::ERR_FAILED",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing.",
|
||||
"Failed to load resource: the server responded with a status of 401 (Unauthorized)",
|
||||
"Failed to load resource: the server responded with a status of 401 (Unauthorized)"
|
||||
],
|
||||
"networkErrors": [
|
||||
{
|
||||
"path": "/api/auth/me",
|
||||
"status": 401
|
||||
},
|
||||
{
|
||||
"path": "/api/auth/me",
|
||||
"status": 401
|
||||
}
|
||||
],
|
||||
"blockedExternalHosts": [
|
||||
"fonts.googleapis.com"
|
||||
],
|
||||
"timings": [
|
||||
{
|
||||
"route": "/jobs",
|
||||
"navigationToMainMs": 497
|
||||
},
|
||||
{
|
||||
"route": "/career",
|
||||
"navigationToMainMs": 395
|
||||
},
|
||||
{
|
||||
"route": "/career/builder/1",
|
||||
"navigationToMainMs": 382
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1,190 @@
|
||||
# Findings and implementation packages
|
||||
|
||||
Baseline and evidence labels: [README](README.md). Reproductions never require production data. P1 fixes are launch prerequisites under their stated exposure; P2s remain tracked. Estimates are relative engineering scope, not promised delivery dates.
|
||||
|
||||
## F01 — P1: stale saves silently overwrite newer CV/job/profile state
|
||||
|
||||
**Observed:** two clients read CV revision 1. Saving “Newer edit” returned 200; saving the stale copy also returned 200 and replaced it. Two simultaneous PUTs both returned 200. `CvVariantController.cs:34,124` does not accept an expected revision; `CvVariantService.cs:108` increments its own freshly read counter. `JobTrackerContext.cs:498` has a non-unique version index, not a concurrency token. `CareerProfileService.cs:51` and job PUT have the same missing client precondition. Identity's ConcurrencyStamp is not a CV conflict protocol. History may preserve settings, but it does not protect the user from overwriting newer work or guarantee unique revision labels.
|
||||
|
||||
**Root cause/scope:** shared persistence contract; module-wide first, then other editable aggregates. The editor's promise queue only serializes its own requests.
|
||||
|
||||
**Reproduction/failing test:** use `stale CV save` in the harness; require the second stale save to return 409/412. Add two independent EF contexts and a real relational database test; cover job status, Career Profile, restore-versus-autosave, and delete-versus-save. Assert that the newer state and a unique history revision remain.
|
||||
|
||||
**Fix:** add expected revision to request/response contracts and perform compare-and-update atomically; use a provider-compatible integer concurrency token. Add uniqueness after reconciling existing duplicated history versions. Return current revision and a safe conflict code. Keep edits in the editor; offer compare/reload/save-as-copy. An unconditional retry is wrong. Make profile child writes, snapshot and legacy projection one transaction or give the projection a repairable state.
|
||||
|
||||
**Passing/staging/detection:** replay five tabs against SQLite and MariaDB, including delayed responses and restore. One writer wins; others retain their drafts. Count conflicts and version-integrity failures without logging CV text. Expand schema first, ship compatible readers/writers, then require preconditions; test an older open frontend. Medium scope; do not refactor all controllers at once.
|
||||
|
||||
## F02 — P1: billing has no complete account lifecycle or concurrent reconciliation
|
||||
|
||||
**Source:** `BillingController.cs:61,147` creates new checkout sessions without a durable account idempotency key (`StripeBillingGateway.cs:14`). Webhooks verify signatures and re-fetch the subscription, which protects ordinary sequential out-of-order delivery. They then update user subscription fields and roles separately. There is no event inbox, per-customer serialization, periodic reconciliation or rule reconciling multiple subscriptions for the same user. `AccountDeletionService.cs:110,231` deletes the user and billing identifiers without cancelling an active Stripe subscription. Enabling deletion while billing is active can remove product access while charging continues.
|
||||
|
||||
**Scope:** billing module plus deletion orchestration. Not a claim that duplicate webhook delivery itself charges five times; this handler does not issue charges.
|
||||
|
||||
**Reproduction/failing tests:** fake gateway returns two checkout sessions for simultaneous calls. Model two subscriptions for one customer, one cancelled and one active, and delay one handler after its provider read. Require one correct account entitlement result. Request deletion with an active subscription and assert cancellation/verified resolution happens before identity deletion. Crash between user and role writes.
|
||||
|
||||
**Fix:** durable checkout attempt/key per intended purchase; store received event identity and processing outcome; serialize reconciliation per customer and derive entitlements from the authoritative relevant subscriptions. Re-read on processing, commit local state and entitlements together, retry safely, reconcile periodically. Add a billing-cancellation deletion stage; on unavailable Stripe retain pending state and explain it. Store only the minimal retained billing audit data approved by policy.
|
||||
|
||||
**Passing/staging/detection:** Stripe sandbox sends the same event five times, reverse order, conflicting concurrent events, deletion and checkout double-clicks. No duplicate purchase, no stale privilege restoration, no orphan active subscription. Alert on unresolved events, local/provider divergence and deletion stuck at cancellation. Preserve event records during rollback. Medium scope; actual Stripe behavior **Needs runtime verification**.
|
||||
|
||||
## F03 — P1: PDF generation can exhaust the backend host
|
||||
|
||||
**Source:** `PlaywrightCvPdfExporter.cs:37` starts a Chromium process per request, holds output bytes in memory, scans export directories before each run and keeps each generated file. It limits each process to 60 seconds but has no shared semaphore/queue. `CvVariantController.cs:206` has no PDF rate policy. Public PDF is limited by IP+slug only. `JobTrackerApi/Dockerfile` runs the API as its default root user and exporter flags include `--no-sandbox` and local-file access. Backend Compose has no corresponding CPU/memory/PID cap.
|
||||
|
||||
**Scope:** shared exporter and backend container. A malicious registered user need not buy AI credits to issue PDF requests. No overload or renderer exploit was executed.
|
||||
|
||||
**Reproduction/failing test:** replace browser launch with a controlled slow fake; invoke 20 exports through distinct entry points and assert active launches never exceed a configured small bound. Verify excess admission returns 429/503 promptly, cancellation frees the permit and partial artifacts are removed. A host test is not allowed to launch 1,000 real browsers.
|
||||
|
||||
**Fix:** bounded shared PDF admission, per-owner burst and sustained limits, explicit queue limits/deadlines and maximum rendered input. Run the backend non-root with constrained resources; verify whether Chromium sandbox can be enabled in that deployment. Isolate the renderer if it cannot be meaningfully constrained in the API container. Remove unnecessary local-file access after renderer regression tests. A durable PDF queue is justified only if exports must survive disconnect/restart.
|
||||
|
||||
**Passing/staging/detection:** small synthetic load verifies peak processes/RSS, retained files and timeouts, including many public slugs. Alert on queue delay, admission rejection and renderer memory. Roll out low capacity first. Small admission change, medium container validation.
|
||||
|
||||
## F04 — P1 conditional: direct nginx ingress can bypass trusted-header assumptions
|
||||
|
||||
**Source:** `docker-compose.yml:140` publishes `3000:80` on all host interfaces. `nginx.conf.template:64` forwards caller-supplied X-Forwarded-For/Proto. The backend trusts the nginx subnet and processes one forwarded hop (`ForwardedProxyConfiguration.cs:12`). The comment assumes an upstream proxy sanitizes headers. A direct connection with the canonical Host can skip that proxy. Host allowlisting does not authenticate the sender. Exploitability depends on the actual host firewall/proxy placement, which was not inspected.
|
||||
|
||||
**Reproduction/failing test:** in isolated Compose, request host port 3000 with canonical Host and forged forwarding headers; assert they cannot choose the API client address or bypass login throttling. Test the normal proxy route at the same time.
|
||||
|
||||
**Fix:** make ingress reachable only by the expected proxy, or discard forwarding headers on direct ingress and accept them only from a verified trusted hop. Do not blindly bind to loopback if the real proxy is on another machine. Document one topology and source of client identity. Keep backend/AI/DB ports private.
|
||||
|
||||
**Passing/staging/detection:** packet/firewall and application-IP evidence from both ingress routes, including IPv6; alert on bypass-port reachability. Small configuration scope; coordinate with the operator because an incorrect ingress change can make the site unavailable. **Needs runtime verification.**
|
||||
|
||||
## F05 — P1: CSV quoting does not neutralize spreadsheet formulas
|
||||
|
||||
**Observed:** a job title `=1+1` was exported as an unescaped formula cell. `ExportController.cs:78` only handles commas, quotes and line breaks. Imported job/company text can carry the same payload. Exploitation requires opening the export in a spreadsheet; no spreadsheet or command was executed here.
|
||||
|
||||
**Reproduction/failing test:** create harmless formula-like fields and export; assert that spreadsheet-safe export cannot evaluate leading `=`, `+`, `-`, `@`, tabs/CR and leading whitespace variants. Include quoted delimiters and Unicode. Preserve ordinary negative numeric salary cells as numbers through an explicit typed policy.
|
||||
|
||||
**Fix/scope:** shared CSV field writer for user-controlled text, with a documented spreadsheet-safe representation; preserve raw values in JSON for portability. Do not use quotation marks as the entire mitigation.
|
||||
|
||||
**Passing/staging/detection:** parser-level CSV tests plus manual Excel/LibreOffice open using harmless arithmetic fixtures. Test downstream import expectations. Small change; log export failures/counts, never field values. No bulk data migration.
|
||||
|
||||
## F06 — P2: account rate limits actually partition by IP
|
||||
|
||||
**Observed:** two successful account exports by A caused B's first export from the same address to return 429. `Program.cs:602` runs RateLimiter before Authentication at 642; email-send/account-data policies inspect an empty User and fall back to the IP. This also means one account can spread these operations across addresses. Authentication-sensitive IP policies are intentionally IP-based and should remain so.
|
||||
|
||||
Move authentication before owner-partitioned limiting, preserve independent anonymous/IP controls, and add a real middleware test with two accounts/one address and one account/two addresses. Return Retry-After when available. Retain a host-wide limit. A per-policy helper unit test alone does not prove middleware order. Small scope; counts of rejects by policy are sufficient detection.
|
||||
|
||||
## F07 — P2: email delivery is not a reliable, durable outcome
|
||||
|
||||
**Source:** `FollowUpReminderHostedService.cs:99` sends inside a loop but persists all sent timestamps at the end. A later send/commit failure can resend earlier messages on the next pass. `SmtpEmailSender.cs:32` returns successfully when sending is disabled; the worker can mark a suppressed reminder as sent. Auth verification/reset sends are direct attempts without a durable retry record. A failed verification remains recoverable through resend, but delivery is not guaranteed. Synchronous SMTP in Task.Run does not become cancellable once sending begins.
|
||||
|
||||
Reuse the durable attempt approach already used by employer email. Persist intent with an owner/job/date dedup key, record suppressed/accepted/failed/unknown separately, and do not blindly retry uncertain SMTP delivery. Retry unsent transactional verification mail within token validity. Test failure on message two, failure at commit, disabled sender and process termination. Stage with a local mailbox sink. Alert on backlog age and permanent failures. Module scope; do not build a second email client or claim exactly-once SMTP delivery.
|
||||
|
||||
## F08 — P2: logout in one tab leaves private editor state visible in another
|
||||
|
||||
**Observed:** after logging out and removing the shared auth marker in one tab, another tab's CV editor remained mounted. `auth.ts` emits same-window events; `App.tsx:263` listens to those, not the storage event. Theme/language already synchronize using storage listeners. Backend session revocation still denies subsequent authenticated requests: this is stale private UI, not proof of an authorization bypass.
|
||||
|
||||
Subscribe once to cross-tab auth changes; cancel/ignore outstanding responses, clear owner caches, and route to a reauthentication state. Preserve pending edits in a safe owner-scoped recovery flow where possible. Test logout, account switch, password/role revocation, late old-account responses and browser Back. Medium UI scope. Do not store bearer credentials in localStorage to solve synchronization.
|
||||
|
||||
## F09 — P2: interrupted edits have incomplete recovery
|
||||
|
||||
**Observed:** CV autosave displayed failure while offline, but after accepting the browser's unload warning and refreshing, the unsaved draft was gone. That warning is useful and should remain. The editor has no durable draft recovery. Session expiry clears authentication globally and can unmount long forms. Career Profile and notes need equivalent coverage; this run did not exercise every editor.
|
||||
|
||||
Prioritize recoverable long-form drafts, with explicit account ownership, schema version, expiry, clear-on-account-delete, and a user-visible restore choice. Decide whether device-local sensitive content is acceptable; a server draft only helps after network access resumes. Add conflict handling F01 before wider autosave. Test quota-denied browser storage, offline/reconnect, expired session, restore-version and failed saves. Medium scope; do not promise zero loss on device failure.
|
||||
|
||||
## F10 — P1: request limits do not bound total storage, CPU or database growth
|
||||
|
||||
**Source/Observed:** `AttachmentsController.cs:269` sums only attachment rows before writing, so simultaneous uploads race quota checks and generated PDFs, CV artifacts, package copies/history are outside that accounting. There is no total job/history quota; duplicate POSTs created two distinct jobs (201/201). Account export writes a ZIP but loads large table collections and JSON byte buffers; jobs CSV/JSON export loads all jobs. Board and several analytics paths materialize whole owner datasets. Monthly AI limits are estimates, not a global CPU limit.
|
||||
|
||||
**Failing tests:** concurrent near-quota uploads must reserve bytes atomically; package copies/PDF retention must be counted; repeated POST with one idempotency key returns one resource, and a different payload with the same key returns conflict. Test very large synthetic history with capped memory. Verify negative/extreme pagination and malformed large content without allocating unbounded bodies.
|
||||
|
||||
**Fix:** owner-level byte reservations plus reconciliation across owned file categories, hard request/document bounds, bounded PDF admission F03, and intent idempotency for creation/import actions. Keep legitimate repeat applications possible. Stream or batch exports, or queue large exports with bounded working memory. Add history retention only after a product decision about restore promises.
|
||||
|
||||
**Staging/detection:** synthetic 1k/10k/100k records with query plans/RSS/response bytes; disk/row growth and rejected admissions tracked. Small quotas first, larger export work only after measurement. Module-wide; old data remains readable on downgrade.
|
||||
|
||||
## F11 — P1 conditional: import DNS validation is not bound to the actual connection
|
||||
|
||||
**Source:** `JobImportService.cs:175` resolves and rejects non-public addresses, then a named HttpClient connects separately. No ConnectCallback or connected-peer validation was found. Redirects are rejected, which correctly blocks a redirect to localhost, but a changed DNS answer between validation and connection is a remaining SSRF boundary. IMAP has a similar preconnection guard to verify. No attack against live DNS was attempted.
|
||||
|
||||
**Failing test:** deterministic resolver returns a public address at validation and private/metadata address at connection; assert no private connect. Include IPv6/mapped IPv4/mixed answers, timeout, TLS name validation and legitimate DNS failover.
|
||||
|
||||
**Fix/staging/detection:** connect to an approved resolved address while retaining TLS/SNI host validation, or use a restrictive outbound proxy/firewall. Test against a local controlled resolver and sink. Alert on denied private targets using categories, not full private URLs. Medium network-boundary scope. Do not disable URL import globally if bounded egress solves it.
|
||||
|
||||
## F12 — P1: deploy recovery-set consistency and rollback compatibility are not proven
|
||||
|
||||
**Source:** daily SQLite backup uses `VACUUM INTO` (`DatabaseBackupRunner.cs:68`), a useful consistent DB snapshot. In contrast, SQLite pre-deploy backup tars the live data volume (`deploy.sh:314`) and validation checks archive shape/DB presence, not a restored database. DB/WAL/files may change during tar. MariaDB SQL backup and file-volume archives also need a coordinated recovery point. Automatic previous-image restoration at `deploy.sh:638` does not reverse migrations. No actual corrupt backup is claimed.
|
||||
|
||||
**Failing test:** continuous local writes while taking a recovery set, restore it into an isolated environment, verify DB integrity, FK ownership, file hashes, encrypted provider-token decryptability and deletion-tombstone replay. Assert old binaries can read the new schema before allowing automatic image rollback. Kill the backup and ensure a partial artifact cannot become “latest verified.”
|
||||
|
||||
**Fix:** reuse the consistent DB snapshot path, coordinate files/manifest with that checkpoint or a brief write pause, checksum all artifacts, and mark verified only after restore. Preserve prior verified sets and encryption/key recovery separately. Stop automatic image rollback for incompatible schema changes; favor forward repair after a successful destructive migration.
|
||||
|
||||
**Staging/detection:** restore latest and previous sets within agreed RPO/RTO, rehearse a bad migration, disk-full backup and missing off-site copy. Alert on last successful restored/off-site backup age, not file existence. Medium operational scope; production commands require operator approval.
|
||||
|
||||
## F13 — P1 launch gate: privileged support, security auditing and retention need deliberate controls
|
||||
|
||||
**Source:** `AdminAuditController.cs:42` bypasses owner filters and returns cross-user job event notes/old/new values, company/title and owner identifiers. This is a user activity timeline, not a tamper-resistant security ledger. `UsersController` can create users/roles, change roles and trigger resets; no dedicated security-event store was found for these mutations. There is no implemented restricted support/impersonation role. Infrastructure root can read all stored personal content and keys. Deletion is default-disabled pending policy approval.
|
||||
|
||||
**Failing tests:** normal users cannot call every privileged verb (extend current posture tests); admin actions produce safe durable security events; read-only diagnostics cannot return document/note contents. Verify deletion/export events and privilege revocation. A metadata audit must not accept arbitrary private notes as an event description.
|
||||
|
||||
**Fix:** retain current user/admin roles; do not introduce speculative role hierarchies. Remove unnecessary content from global diagnostics, scope each privilege, require recent authentication for high-impact actions, and record actor/action/target/outcome/time/reference in append-oriented storage with restricted writes/off-host retention. Decide support needs before adding impersonation. Approve a retention/deletion/backup policy and implement it; claims of legal compliance require separate review.
|
||||
|
||||
**Staging/detection:** attempt all admin actions as user/admin, inspect event integrity and alert on privilege changes or unusual bulk access. Audit operator access separately. Medium scope; retention policy and custody **Need runtime/owner verification**.
|
||||
|
||||
## F14 — P2, launch quality gate: builder accessibility is outside the green axe suite
|
||||
|
||||
**Observed:** the isolated builder probe reported serious `color-contrast` and `scrollable-region-focusable` violations. No outer viewport overflow was observed at the tested 375/1440 widths. Existing `e2e/accessibility.spec.ts` checks dashboard/jobs/kanban/career/settings/admin-system, but not `/career/builder/:id`. See retained JSON for exact nodes and screenshots for both locales/themes.
|
||||
|
||||
Fix contrast through shared theme tokens, make the editor scrolling surface keyboard reachable, and distinguish inaccessible template previews from decorative thumbnails. Add a builder case with real synthetic content and verify focused controls, inactive/locked states and both themes. Manual screen-reader, text-spacing, forced-colors, 200% and 400% zoom remain required; axe alone cannot certify them. Small/medium component scope.
|
||||
|
||||
## F15 — P2: private caching and browser security headers are incomplete
|
||||
|
||||
**Observed:** `/auth/me`, private CV JSON and job CSV export returned no Cache-Control. This does not prove that a browser exposed data to another user; it means explicit private-response policy is absent. nginx sets nosniff/frame/referrer/permissions headers, but no CSP or explicit static index/hashed-asset cache policy. Public CV HTML sets noindex; the PDF route does not set the same directive. The authenticated preview uses `allow-same-origin allow-scripts` and generates repeated browser sandbox warnings; encoding remains the primary defense.
|
||||
|
||||
Apply `private, no-store` to sensitive API/file/export responses and noindex to both public token-link representations. Give index revalidation and hashed static assets immutable caching. Deploy a tested CSP (report-only first; minimize report URLs), then constrain preview scripting to required functionality. Test stored HTML/URL payloads and missing-script/fonts behavior. Do not call current encoding a proven XSS exploit. Small cross-cutting policy change plus renderer compatibility verification.
|
||||
|
||||
## F16 — P2: operational logs can contain private content and unsanitized URLs
|
||||
|
||||
**Source:** `SmtpEmailSender.cs:34` logs recipient and subject when mail is suppressed; reminders embed role/company in that subject. Auth errors log email addresses. `ClientErrorsController.cs:32` truncates but preserves reported URL, message and stack prefixes. Thus tokens/query content can survive if a client submits them. The request logger excludes query strings and has trace IDs, which should be preserved.
|
||||
|
||||
Allowlist safe error fields, remove query/fragment and free-text/stack contents before logging, hash/pseudonymize only where useful, and rate-limit error ingestion. Connect the existing traceId to visible UI failures. Distinguish operational logs, security records and job timeline retention. Test synthetic secret/PII canaries and verify their values never reach a logger sink. Small shared-boundary scope.
|
||||
|
||||
## F17 — P2: build/test success hides a strict test-type error and documentation drift
|
||||
|
||||
**Observed:** backend 753, frontend 279 and normal browser 13 tests pass; production static build passes. Standalone `tsc --noEmit` fails at `src/interview-prep.test.tsx:120` because `mock.calls[0][1]` is unknown. Next's build result does not replace this test-source gate. Existing browser warning reports stale Browserslist data; dependency freshness was not checked online.
|
||||
|
||||
Type the test payload at its declaration/assertion boundary and run standalone type checking explicitly in CI. Refresh only the authoritative architecture/setup document, linking dated reports instead of copying them. Preserve old renderers/migrations until call-site and stored-ID compatibility is proven. This is a small correction, not justification for replacing Next or TypeScript.
|
||||
|
||||
## F18 — P2: supply-chain and production-environment proof remain incomplete
|
||||
|
||||
**Source:** locks and the tracked-secret scanner are present and scanner tests passed. CI actions and base images use mutable tags; pip bootstrap is fetched at runtime; the API Docker build does not copy root `global.json`/build props and does not explicitly restore with locked mode. The .NET vulnerability-list command is reporting, not an explicit parsed severity gate. No retained container scan, current CVE/license review, branch-protection inspection, host patch/firewall audit or signing evidence was collected.
|
||||
|
||||
Pin release dependencies by verified digest/SHA, run locked restores in the actual image build, retain SBOM+scan artifacts and define expiring advisory exceptions. Check supported runtime versions through current official sources when external access is authorized. Protect deploy credentials from untrusted PR execution, especially on a persistent self-hosted runner. Avoid claiming the no-value tracked scan proves Git history or runtime environments contain no secrets.
|
||||
|
||||
## F19 — P2: application startup silently falls back to SQLite
|
||||
|
||||
**Source:** `Program.cs:95–115` replaces a configured provider with SQLite when the connection string is missing and treats unknown providers as SQLite. Deployment validation is stronger, but starting the production API by another path can create an apparently empty database. Fix provider/connection validation in application startup, preserving deliberate development SQLite defaults. Test production missing/invalid provider/connection plus explicit SQLite and MariaDB choices. Small scope; no migration required.
|
||||
|
||||
## F20 — P1 when enabled: an unhandled AI-worker dependency failure can stop the API host
|
||||
|
||||
**Source:** `AiOperationQueue.cs:254` runs worker loops without a catch around `RunOnceAsync`. Claiming a lease, loading the owner, reading roles and persisting the result can throw database errors outside the classified provider-failure paths. No overriding background-service exception policy was found. Under the framework's default BackgroundService exception handling, an unhandled worker failure stops the host. This was not fault-injected into a running host during the audit.
|
||||
|
||||
**Failing test:** enable the worker against an isolated DB, make lease-claim or result persistence fail once, and assert that core API liveness remains available, the operation is recoverable and processing resumes after a bounded delay. Include shutdown cancellation and an unexpected handler exception.
|
||||
|
||||
**Fix/scope:** catch and classify failures at the worker-loop boundary; use bounded backoff with jitter, safe logging and a degraded-worker signal. Preserve the persisted lease/result protocol. Do not simply ignore BackgroundService exceptions globally, which could leave a permanently dead worker behind a green health endpoint. Module-wide, small/medium scope.
|
||||
|
||||
**Passing/staging/detection:** 30-second and longer controlled DB outages, followed by recovery, must not lose accepted work or trigger an endless restart storm. Alert on failed worker passes, stalled heartbeats and expired leases. Keep the worker kill switch available while proving this behavior. **Needs runtime verification.**
|
||||
|
||||
## F21 — P1: calendar export changes scheduled instants around DST
|
||||
|
||||
**Observed:** a follow-up submitted as `2026-03-29T09:00:00+02:00` exported as `DTSTART:20260329T090000Z`, rather than 07:00Z. Both occurrences of the repeated autumn time `2026-10-25T02:30` (+02:00 and +01:00) exported as the same 02:30Z. All four offset-aware calendar probes failed the expected-instant check while returning HTTP 200.
|
||||
|
||||
**Root cause/scope:** job fields use DateTime without a durable timezone/offset contract; SQLite materialization loses Kind, and `ApplicationCalendarController.cs:129` treats unspecified values as UTC. The application also uses server-local time for reminder scheduling. This affects externally scheduled follow-ups and needs a date/time boundary fix, not a display-format patch.
|
||||
|
||||
**Failing tests:** replay the four `DST calendar instant` cases in the harness on SQLite and MariaDB with server timezone UTC and Europe/Oslo. Preserve the same instant through POST, DB round-trip, JSON and ICS. Test date-only deadlines separately and reject or explicitly resolve nonexistent/ambiguous local times.
|
||||
|
||||
**Fix:** specify UTC instants/DateTimeOffset at timed-event boundaries, preserve date-only values as date-only, and keep a named timezone for future wall-clock recurrence if that feature is supported. Plan migration of existing values; their intended timezone cannot be safely inferred globally. Do not shift historical data blindly.
|
||||
|
||||
**Passing/staging/detection:** imported calendar events display 09:00 locally before/after DST and distinguish the autumn repeated hour. Rehearse existing-data migration with synthetic representative dates. Detect serialization/round-trip mismatches in tests rather than collecting users' event contents. Medium scope with migration risk.
|
||||
|
||||
## Implementation order and rollback
|
||||
|
||||
| Batch | Dependencies | Acceptance and rollback |
|
||||
|---|---|---|
|
||||
| A: ingress/DNS, CSV, shared PDF admission and account rate identity | Confirm actual proxy topology; validate provider config in app startup | Boundary tests; previous config/image retained; no schema change |
|
||||
| B: revision contracts, duplicate intents and timed-event conversion | Additive schema first; determine legacy date meaning | Conflict/history/offset round-trip tests on both providers; compatible old-client period; preserve drafts and historical dates |
|
||||
| C: billing and delivery lifecycle | Durable state + sandbox | Event replay/deletion tests; disable new purchase/send admission if unhealthy; retain queued records |
|
||||
| D: quota/storage/export accounting | A/B semantics established | Byte reconciliation and bounded export stress; preserve existing data on quota changes |
|
||||
| E: recovery, worker failure containment, security audit and retention | Policy/custody owner; DB-outage fixture | Restore/worker/operator drills; keep independent verified backups/tombstones; no casual DB downgrade |
|
||||
| F: editor/session/a11y and source hygiene | B conflict UX | EN/NB, keyboard, themes, failure states, strict types; staged route-by-route rollout |
|
||||
|
||||
Every P1 closure must attach the failing-before/passing-after evidence, changed scope, provider coverage, staging outcome and recurrence signal. A review saying “looks fixed” is insufficient. No wholesale redesign, broker, Redis, public PWA, generic permission platform or custom encryption scheme is required to begin these batches.
|
||||
@@ -0,0 +1,225 @@
|
||||
# Product coherence, operational readiness and reusable quality gates
|
||||
|
||||
This is a source/observational assessment plus a proposed acceptance programme. [Verification](verification.md) states what actually ran. No live uptime, provider price, current vulnerability catalogue or legal requirement was verified externally.
|
||||
|
||||
## Critical paths and time to value
|
||||
|
||||
Priority order: sign in/recover account → view/find tracked jobs → save/update application → edit Career Profile/CV → download/share an approved document → communication/follow-up → optional AI/discovery/analytics. Optional AI/email outages should not block the first four paths.
|
||||
|
||||
The existing manual add-job browser test clicks Add Job, Enter details manually, fills company/title, Continue, skips three optional steps, then Create job. That is a measurable interaction sequence, not a timed usability study. Offer a short “Save job” path after required details and let the user enrich it later; keep the reviewed import path. Never require AI or a complete CV before tracking a first job.
|
||||
|
||||
Registration depends on configuration, verification and Turnstile. Default-disabled registration is an operational rollout choice, not inherently a bug. The landing CTA must explain when signup is unavailable. First-run empty states should offer three small actions: save a job, add career details, create a CV. Explain that Career Profile supplies facts and a CV is a chosen presentation for an application. Dismissible hints beat a mandatory tutorial. Verify registration → email verification → first saved job against a local mailbox sink; this whole journey was not executed in the audit.
|
||||
|
||||
## Navigation and domain language
|
||||
|
||||
Current routes are visible in `src/App.tsx`: dashboard, jobs/detail, discovery, reminders, operations, Kanban, companies, offers, correspondence, profile, career, CV builder, trash, settings and admin. Deep job routes and career subroutes are valuable; legacy application/correspondence routes redirect. The existing browser workspace deep-link/back/dirty-navigation test passes. Not every tab/filter/modal was exercised.
|
||||
|
||||
Proposed information architecture, retaining working routes:
|
||||
|
||||
```text
|
||||
Dashboard: next useful actions
|
||||
Jobs: list / board / job workspace / reminders
|
||||
Career: facts / CVs / import review / evidence
|
||||
Communication: correspondence and send/review status
|
||||
Discovery: optional acquisition workflow
|
||||
Settings: identity, connected providers, privacy, notifications, plan, data
|
||||
Admin: account/security/health diagnostics, hidden from ordinary users
|
||||
```
|
||||
|
||||
Offer comparison, companies and operation status need not compete equally with daily job actions in primary navigation. Keep direct links for frequent users; make secondary entries contextual. Do not remove features solely because usage analytics are absent. Before deleting an apparent duplicate, inspect dynamic imports, stored template IDs, compatibility routes, migrations and deployment scripts.
|
||||
|
||||
| Concept | English / recommended Bokmål | Meaning / consistency rule |
|
||||
|---|---|---|
|
||||
| Job / opportunity | Job / Stilling | Employer opportunity; not proof of an application |
|
||||
| Tracked application | Job application / Jobbsøknad | User's pursuit of a job, with status/history |
|
||||
| Application document | Cover letter / Søknadsbrev | Written letter, not the tracked workflow itself |
|
||||
| Career Profile | Career Profile / Karriereprofil | Reusable factual source; account Profile is identity/settings |
|
||||
| CV | CV / CV | Saved presentation/settings; define draft versus submitted snapshot |
|
||||
| Interview | Interview / Intervju | Scheduled or logged interview; not interview-preparation notes |
|
||||
| Reminder | Reminder / Påminnelse | A due action; distinguish notification channel from task |
|
||||
| Correspondence | Correspondence or Job email / Korrespondanse or Jobb-e-post | Choose one label per navigation context, explain manual versus connected mail |
|
||||
| Offer | Offer / Tilbud | Employer outcome; no implicit monetary currency conversion |
|
||||
| Rejected | Rejected / Avslått | Application outcome; avoid confusing with archive |
|
||||
| Withdrawn | Withdrawn / Trukket | User ended pursuit |
|
||||
| Archive / trash / delete | Archive / Arkiv; Trash / Papirkurv; Delete / Slett | Distinguish inactive, recoverable deletion and permanent erasure |
|
||||
| Save / share / download | Save / Lagre; Share / Del; Download / Last ned | Explicit object on destructive/ambiguous actions; sentence case |
|
||||
|
||||
Current terminology is partly established in `docs/01-glossary.md`, `translations.ts` and JobPipeline. Reconcile with that vocabulary rather than creating a competing glossary. The observed Bokmål builder still shows English template descriptions and “Dashboard”; button capitalization includes “Last Ned PDF” and “AI-Verktøy”. Treat these as editorial inconsistencies to review with a native Bokmål speaker. Keep document language distinct from interface language; the English synthetic CV inside a Norwegian UI is not itself a translation failure.
|
||||
|
||||
## Visual, accessibility and professional polish review
|
||||
|
||||
**Observed:** the current static builder was inspected at 375 and 1440 pixels in both themes and both actual language states (`en` / `nb-NO`). The surrounding page had zero measured horizontal overflow. Desktop has a useful editor/preview split; mobile provides an edit/preview alternative. Shared navigation, toolbar and template thumbnails have consistent placement. These are strengths worth preserving.
|
||||
|
||||
**Confirmed problems:** F14's success/ATS chips use green text with measured 3.29:1 contrast on white in one light-theme sample; desktop scrolling regions were not keyboard focusable. All eight inspected states produced at least one serious contrast finding. F15's combined iframe sandbox flags produce repeated browser warnings. A short synthetic CV also produced a preview overflow warning; investigate the measurement/rendered document width rather than hiding the warning. External Google Fonts were blocked by the harness and fallback rendering was inspected; compare with approved self-hosted-font rendering before attributing every document-layout discrepancy to a product defect.
|
||||
|
||||
The template gallery uses small badges and substantial descriptive copy. Keep selection predictable, use stable action positions, and ensure Pro-disabled cards have an explanatory accessible name rather than opacity alone. Do not conflate “ATS-friendly” with a guarantee for every recruiter system. The saved status, language and theme should not move focus unexpectedly. No native screen-reader or actual browser-zoom certification was performed.
|
||||
|
||||
Canonical component choices should build on MUI and existing shared `theme.ts`, `layout/AppShell.tsx`, `layout` surfaces, feedback context/dialogs and spacing tokens. `GradientButton.tsx` is a special treatment to inventory, not justification for another general button package. Keep normal controls on MUI, standardize PageHeader/empty/error/loading patterns at their actual common callers, and migrate one page before broad rollout. Add a lightweight component state showcase only if repeated visual review warrants it; Storybook is not a launch dependency.
|
||||
|
||||
| Component / state | Review and target |
|
||||
|---|---|
|
||||
| Button/input/select | Clear accessible names; focus visible; Enter/paste/autofill work; busy does not imply permission; use native/MUI semantics |
|
||||
| Dialog/menu/portal | One focus trap; Escape semantics; focus return; no nested modal stack for routine edits; theme inherited |
|
||||
| Cards/badges/status | Text conveys status independent of color; keyboard equivalents for clickable cards; translated long names wrap |
|
||||
| Toast/error/empty/loading | Persistent actionable error for failed saves; important failures not transient toast only; live region announces state changes, not every token |
|
||||
| Tables/Kanban/charts | Paged data; allow keyboard change-status/reorder; meaningful zero-data explanation; charts have textual values/summary |
|
||||
| CV editor/preview | Dirty/saving/saved/failed states; recoverable conflict; accessible scrolling; output language metadata and reading order tested separately |
|
||||
| Responsive layout | 320 effective width, 375/768/1440, 200%/400% actual zoom, landscape, on-screen keyboard, safe area, text spacing; no hidden-only-on-hover action |
|
||||
| Interaction details | Real links support middle/right click; copy feedback only after clipboard success; focus/title after SPA navigation; Back restores useful list/filter/scroll state |
|
||||
|
||||
Copy action in `CvBuilderEditor.tsx` shows a success toast without awaiting clipboard completion; make success contingent on the browser result. History/preview/unsaved dialogs deserve keyboard and screen-reader runs, not only snapshots. Shared page title/focus/scroll behavior was not proven by source search; add explicit route-level checks before claiming polish.
|
||||
|
||||
Professional polish checklist (open until visually verified): consistent loading indicators and skeleton ARIA; favicon/page titles and missing assets; margins/spacing tokens; both themes; translated empty states and errors; tooltips for icon-only controls; stable actions; no layout shift; no unexplained console/network/backend warnings; no broken route/404 fallback hiding missing assets; sensible scrollbars and z-index layers; no accidental nested modals; copyable text; no forced orientation; reduced motion/forced colors; clear failed/disabled/long-content states. A screenshot is not proof of every item.
|
||||
|
||||
Generated documents require a separate gate: selectable/searchable text, intended page size/margins, logical reading order, headings/lists, document language, meaningful links, metadata, contrast and absence of hidden server paths. Existing PDF browser tests prove portions of rendering/searchability, not tagged PDF/PDF-UA or screen-reader reading order. DOCX extraction support does not imply a complete accessible DOCX generation/export feature. Verify current offered export formats before promising them. Browser Print must hide application navigation and preserve page boundaries independently of PDF export.
|
||||
|
||||
## Feature maintenance and simplification
|
||||
|
||||
Scores are judgments (high/medium/low), not usage telemetry.
|
||||
|
||||
| Subsystem | User value / maintenance / risk | Decision |
|
||||
|---|---|---|
|
||||
| Job tracking and routed workspace | High / medium / high correctness | Keep and prioritize reliable save/search/history |
|
||||
| Career Profile and CV builder | High / high / high privacy/data loss | Keep; strengthen revision/time/preview contracts before more customization |
|
||||
| Mail connectors/provider abstraction | Potentially high / high / high privacy/external effects | Retain working seams; measure adoption before adding providers or full mailbox features |
|
||||
| AI assistant/work queues | Useful / high / high privacy/cost | Keep human review, gates and current queue; narrow workloads/evaluate models |
|
||||
| Discovery/import/translation | Medium / high / high SSRF/source churn | Manual fallback, limits and per-source tests; no bespoke scraping platform |
|
||||
| Company CRM/offer comparison/analytics | Medium / medium / medium | Contextual access; measure adoption before more domain entities |
|
||||
| Public CV | Medium / medium / high publication/render abuse | Explicit review/revocation and bounded rendering; optional expiry when needed |
|
||||
| Custom auth/billing/backup orchestration | Essential / high / high | Keep Identity and Stripe primitives; repair small missing protocols; compare managed options only with requirements and costs |
|
||||
|
||||
Remain a modular monolith. A parser service boundary is justified by unsafe documents and different runtime/resource needs. A separate renderer may be justified by measurable resource/privilege isolation; explain its queue, artifact-storage, failure and deployment costs first. No reason yet for Redis, Elasticsearch, Kubernetes, a PWA/service worker or independently deployed domain microservices. An offline-capable PWA would add sensitive cache/upgrade complexity before F01/F09 are solved.
|
||||
|
||||
Existing configuration switches cover workers, registration, deletion, provider routing and paid authorization. Maintain a flag register with owner, purpose, default, rollback behavior, test of both states and review/removal date. Server-side entitlement policy remains authoritative; a feature flag does not grant access. Percentage rollout is unnecessary for a small closed cohort; explicit internal/beta allowlisting and kill switches suffice.
|
||||
|
||||
## Performance, capacity and cost model
|
||||
|
||||
Use measured traffic/service time before setting SLO commitments. The audit's localhost navigation-to-main timings exclude real WAN latency, complete data/render readiness and production contention. They are not LCP or production p95 evidence. Existing owner/page limits help, but list-of-all board/analytics/history/exports and per-request Chromium are the likely first bottlenecks.
|
||||
|
||||
Proposed initial internal targets for a declared synthetic dataset and reference device/network: core read/save API p95 <500ms and p99 <2s; authentication p95 <2s excluding provider challenge; dashboard ready <3s; LCP <2.5s, INP <200ms, CLS <0.1; search p95 <500ms; bounded PDF p95 <10s for an agreed 2-page CV; queue acceptance <1s; an immediate truthful queued/progress state for slow AI. These are acceptance hypotheses, not measured guarantees. Benchmark on the actual host before promising them. Record p50/p95/p99 and error rate, and budget the initially loaded JS based on measured gzip/Brotli bytes; fail unexpected >10% route-bundle growth unless justified.
|
||||
|
||||
| Scale | What must be measured first | Likely trigger / next step |
|
||||
|---|---|---|
|
||||
| 10 users | Real daily workflow failures, restore success, single PDF RSS | Fix correctness/UX before scaling |
|
||||
| 100 | Concurrent PDF/AI requests, monthly storage/mail growth, worst-owner queries | Bounded admission and alerting; single host may suffice but capacity unproven |
|
||||
| 1,000 | DB p95/query plans, pool use, queue age, file backup duration | Tune projections/indexes, batch/stream exports, independent worker capacity if measured |
|
||||
| 10,000 | Real active concurrency, arrival/service rates, DB recovery/availability | DB-backed quota reservation before replicas; separate durable storage/compute as needed |
|
||||
| 100,000 | Workload partitioning, retained history, operational staffing/cost | Reassess architecture with production telemetry; no exact capacity claim here |
|
||||
|
||||
Suggested scaling triggers: sustained DB CPU >70% plus missed latency targets; pool utilization >80% with connection waits; queue wait exceeds user-facing SLO for 15 minutes; disk forecast under 30 days; backup/restore exceeds target; repeated PDF admission saturation. Tune alert thresholds against baseline and avoid paging for every transient failure. First inspect query plans on the actual SQLite/MariaDB provider; SQL text or InMemory tests do not establish production query cost.
|
||||
|
||||
Cost model variables (record actual invoices/provider metering):
|
||||
|
||||
`monthly cost = fixed host + database + monitoring + stored_GB × storage_rate + backup_GB × backup_rate + egress_GB × egress_rate + email_count × email_rate + external_AI_input_tokens × input_rate + external_AI_output_tokens × output_rate + operator_time × internal_rate`.
|
||||
|
||||
Local AI substitutes memory/compute/energy and operator cost for a provider token bill; it is not free. PDF consumes CPU/process time and retains output bytes. For workload w, estimate `cost_per_success = (successful + retried + failed_attempt_cost) / successful_results`. Separate reservations, actual estimates, failures and external fallback. Character/4 estimates are not provider billing truth, especially for Bokmål and unusual text.
|
||||
|
||||
Storage planning example, **assumption not telemetry**: 1,000 users × 20 documents × 1MB = 20GB original bytes before generated files, versions, package copies and backups. Thirty full daily copies alone could multiply that substantially. Measure dedup/compression/retention rather than promising a flat quota covers all copies. Database growth includes JobEvents, CV/Profile/letter versions, mail, AI operations/usage and deletion/security records. Set retention before partition/archive infrastructure.
|
||||
|
||||
Useful privacy-preserving telemetry: workflow starts/success/failure, safe task/version/category, elapsed time, approximate size bucket, queue wait, retry count, upload bytes, quota rejection, empty-state completion. Exclude CV/letter/mail/note text, full URLs/query strings, tokens and unredacted screenshots. Keep support diagnostics usable without viewing career content. Monitor externally, but never make application health depend on the telemetry collector being online.
|
||||
|
||||
## Operational ownership and alerts
|
||||
|
||||
For a solo project, the repository owner can initially own all roles; explicitly name the backup contact before paid launch. Record actual people/contact routes outside public source. Proposed availability goal is 99.5% monthly for core reads/saves after baseline measurement; adopt a stricter promise only with operational evidence. Track separate generation/delivery success SLOs so optional AI errors do not disguise or inflate core reliability.
|
||||
|
||||
| Signal | Severity / initial rule | Owner / action |
|
||||
|---|---|---|
|
||||
| External HTTPS/readiness | P1 after 3 failed probes over ~3min; immediate if broad confirmed outage | Operator: proxy/DB runbook |
|
||||
| Core save errors | P1 sustained >1% or any confirmed data loss | App owner: freeze risky rollout, inspect safe trace IDs |
|
||||
| Worker stalled | P1 if accepted jobs miss deadline; otherwise P2 sustained lag | App operator: F20 lease/backoff runbook |
|
||||
| Backup age/failure/off-site failure | P1 beyond RPO; warning at 26h for daily schedule | Recovery owner: preserve prior set, rerun safely, verify restore |
|
||||
| Disk/inode forecast | Warn 80%, urgent 90% or <7 days forecast; adjust for volumes | Operator: stop expensive admission, inspect approved cleanup |
|
||||
| PDF/AI cost or saturation | P2 sustained queue/cost anomaly; P1 if affecting core API | App owner: kill switch, admission/abuse investigation |
|
||||
| Billing reconciliation/deletion | P1 unresolved paid-state/cancellation divergence | Billing owner: sandbox-validated reconciliation, user follow-up |
|
||||
| Privilege changes / unusual export | Security review; urgent on unauthorized activity | Security owner: account/session containment and evidence |
|
||||
| TLS/DNS | Warn certificate <21 days, urgent <7; sustained DNS mismatch | Domain/proxy owner: renewal/DNS runbook |
|
||||
|
||||
Deduplicate alerts by incident, include last good release, safe reference/metric, impact, runbook and recovery notification. Ensure a missing scheduler/monitor itself produces a dead-man alert outside the app host. Check backups becoming suspiciously small/large and off-site encryption failure, not only exit codes.
|
||||
|
||||
## Incident response and technical runbooks
|
||||
|
||||
Common sequence: identify impact and start an incident record; preserve UTC timeline, release IDs and sanitized logs; contain further writes/exposure when justified; keep a known recovery point; repair/rotate; verify user journeys and integrity; communicate factually through the approved channel; document cause and regression prevention. Do not email users automatically from this audit. Determine legal notification obligations with the responsible qualified advisor.
|
||||
|
||||
| Incident | Checks and diagnosis | Safe recovery / escalation | Verification |
|
||||
|---|---|---|---|
|
||||
| 502/site unreachable | External DNS/TLS/HTTP, frontend route, backend `/health` and `/ready`, recent deploy | Correct proxy/service/network or roll back only schema-compatible image; do not restart DB blindly | External auth/config + synthetic core journey and readiness |
|
||||
| DB unavailable | Connection reachability/pool/server health, provider identity, disk, credential changes; no credential dumps | Restore connectivity; reduce admission; recover worker leases; escalate to DB owner if integrity uncertain | Read/save synthetic record, FK/integrity checks, queued work progresses |
|
||||
| Bad migration / partial deploy | Migration history and exact failing statement/release; schema compatibility | Stop further deploys; preserve checkpoint; forward fix or restore isolated verified set; explicit approval before production restore | Old/new client contracts, data counts/hashes, app-ready with intended version |
|
||||
| Disk full | Per-volume/inode usage, DB/WAL, logs, exports, backups | Disable PDF/AI/upload admission; rotate/prune only documented disposable outputs; add capacity; never delete active DB/WAL or sole backup | Free headroom, DB integrity, safe new upload/export and backup |
|
||||
| High CPU/RSS/OOM | Active Chromium/model workers, queue size, request classes, container limits | Apply admission/kill switch, terminate only identified stuck disposable worker, restore service; inspect missing results | Resource baseline, queue recovery, no partial artifacts reported ready |
|
||||
| AI/model outage | Sidecar health vs model availability, task/model route, consent and timeout category | Keep manual product usable; disable failing route; switch only to evaluated permitted model; queue/retry bounded | Synthetic factuality/format/locale checks, no unauthorized external fallback |
|
||||
| Email unavailable | SMTP/provider response category, queue/attempt state, token scope, sender config | Reconnect/rotate as needed; retry known unsent only, reconcile uncertain sends; use local sink during diagnosis | Delivery accepted/received in safe mailbox; pending verification/reminders resolved |
|
||||
| Backup failed/corrupt | Last independently verified set, manifest/hash, backup/copy logs, disk | Preserve prior verified set; recreate consistent snapshot; restore isolated copy first | DB+files+keys+tombstones usable within RPO/RTO; record actual minutes/bytes |
|
||||
| TLS expired / DNS wrong | Cert chain/SAN/expiry, renewal job, authoritative vs recursive DNS | Renew correct certificate/fix intended record; escalate registrar/proxy access to owner | External HTTPS from independent network and renewal alert |
|
||||
| Leaked JWT signing secret | Scope/token issuance window, affected sessions, safe auth logs | Rotate signer, revoke sessions/trusted devices as required, redeploy configured key; plan forced sign-in | Old tokens rejected, new sign-in works, no secret in logs |
|
||||
| DB/SMTP/AI/OAuth/Stripe key leak | Identify specific credential, usage anomalies and accessible data | Revoke/replace at issuer and app, test dependent workflows, reconcile mail/billing side effects; rotate webhook secret with provider overlap policy | Old credential rejected, correct service restored, anomaly monitoring quiet |
|
||||
| Data Protection key exposure/loss | Which encrypted tokens/TOTP depend on ring; backup custody | Treat exposed reversible values as compromised; revoke/relink providers or re-encrypt with controlled key transition; never casually delete ring | Token decryptability/re-enrollment and restored backups tested |
|
||||
| Admin compromise / cross-user leak | Actor/session/role timeline, affected owners/objects, access logs | Revoke compromised sessions, disable account/route as needed, preserve evidence, patch authorization with failing test | Complete cross-owner suite; scoped security review; appropriate user notice process |
|
||||
| Malicious dependency / host compromise | Release provenance/SBOM, runner/host boundaries, secret exposure | Isolate host/runner; rebuild from clean trusted artifacts; rotate reachable credentials; do not trust in-place cleanup alone | Clean deployment, restored validated data, external/user smoke, evidence retained |
|
||||
|
||||
Operator inspection can use read-only service listings and `/health`/`/ready`, sanitized logs and backup manifests. Do not paste `docker compose config`, full environment, connection strings or tokens into an incident ticket. A production restart, restore, destructive cleanup or credential change is a separate authorized action. The audit did not execute these runbooks.
|
||||
|
||||
## Release gates and production-readiness checklist
|
||||
|
||||
Choose measurable targets with the owner. Proposed initial recovery target: **RPO <=24h and RTO <=4h** for the current daily-backup service; paying-user expectations may require a shorter RPO. Record restored snapshot timestamp, last committed synthetic marker, restoration duration and file/key verification. Never claim this target is achieved until a drill passes.
|
||||
|
||||
- [ ] No known P0; all applicable P1 findings closed with reproduction and regression evidence.
|
||||
- [ ] Backend build/tests, frontend tests, standalone strict types, lint and static build pass using declared locked toolchains.
|
||||
- [ ] Real relational tests cover owner predicates, revision conflicts, byte/usage reservation, idempotency and deletion; MariaDB behavior is tested when deploying MariaDB.
|
||||
- [ ] Browser critical paths pass on the production export; both languages/themes and editor failure states included; no unexplained console/network errors.
|
||||
- [ ] Zero serious/critical axe findings on core routes; manual keyboard/screen-reader/zoom/text-spacing checks recorded, with documented exceptions if unavoidable.
|
||||
- [ ] Bounded PDF/parser/AI/import/export load proves process/memory/request ceilings and recoverable errors.
|
||||
- [ ] Current dependency/advisory/container/secret scans and SBOM retained; each exception has owner, mitigation and expiry; supported toolchain verified.
|
||||
- [ ] Migration rehearsed on realistic synthetic shape; old/new compatibility window and rollback boundary stated; verified pre-deploy recovery point retained.
|
||||
- [ ] Actual DB+files+keys+tombstones restore meets chosen RPO/RTO; off-site copy and alert routing tested.
|
||||
- [ ] Host ingress/firewall, TLS renewal, trusted proxies, least privilege and production secrets validated; development fallbacks cannot activate silently.
|
||||
- [ ] Stripe sandbox proves duplicate/order/concurrency/deletion; mail sandbox proves known-unsent vs uncertain delivery; OAuth uses intended tenant/scopes.
|
||||
- [ ] Privacy/retention/export/deletion/support wording matches implemented behavior; external/legal decisions resolved by responsible owner.
|
||||
- [ ] Release version observable, post-deploy external smoke succeeds and a responsible operator can respond to the alerts.
|
||||
|
||||
## Definition of Done and practical PR checklists
|
||||
|
||||
**Every feature:** explicit acceptance behavior; correct owner/permission/validation; appropriate regression test; recoverable loading/error/empty states; no private log content; useful documentation only where behavior changes. A cosmetic edit does not need the same gates as billing or a migration.
|
||||
|
||||
**PR checklist**
|
||||
|
||||
- [ ] Problem, resulting behavior and smallest affected boundary explained.
|
||||
- [ ] Relevant tests/checks pass; omitted checks and actual risks stated.
|
||||
- [ ] Owner/security/duplicate/failure behavior considered for changed paths.
|
||||
- [ ] UI/locales/themes/accessibility checked when changed.
|
||||
- [ ] Data/migration/rollout/rollback and behavior docs updated when relevant.
|
||||
|
||||
**Endpoint/security checklist**
|
||||
|
||||
- [ ] Identity and owner come from trusted server context; test another owner's identifiers on each verb.
|
||||
- [ ] DTOs reject unauthorized fields/oversized inputs; enum/null/numeric boundaries and query allowlists tested.
|
||||
- [ ] Cookie mutations have CSRF protection; auth/admin/export changes require appropriate recent auth and safe audit.
|
||||
- [ ] Uploads have byte/type/expansion/storage limits; URLs validate connected destinations; downloads have safe headers/names.
|
||||
- [ ] AI uses bounded consent-approved data, validated output and human review; actions cannot come from untrusted prompt instructions.
|
||||
- [ ] Side effects have operation identity, correct quota admission and safe failure/retry semantics.
|
||||
|
||||
**UI checklist**
|
||||
|
||||
- [ ] Shared spacing/type/color/control conventions; every action has a meaningful name.
|
||||
- [ ] Keyboard, focus return, contrast, touch and no hover-only requirements.
|
||||
- [ ] 375/768/1440 and applicable reflow/zoom; both EN/NB and light/dark.
|
||||
- [ ] Loading/empty/error/disabled/success/long-text states; save failures retain edits.
|
||||
- [ ] Back/deep links/clipboard/scroll/title behavior and console/network cleanliness.
|
||||
|
||||
**Database-change checklist**
|
||||
|
||||
- [ ] One schema owner; migration and model snapshot agree; both supported providers handled.
|
||||
- [ ] Correct FK/unique/check/index constraints; owner scope and query plan tested.
|
||||
- [ ] Existing rows preserved; deterministic backfill; locking/disk/time measured on realistic shape.
|
||||
- [ ] Expand/contract compatibility and irreversibility documented; no blind time-zone conversion.
|
||||
- [ ] Verified backup, rehearsal, rollback/forward-repair plan and integrity check.
|
||||
|
||||
**Deployment checklist**
|
||||
|
||||
- [ ] Intended commit/artifact/config and environment verified; no secrets printed.
|
||||
- [ ] Required quality gates, backup/recovery checkpoint and migration review complete.
|
||||
- [ ] Previous compatible image/recovery path available; feature kill switches understood.
|
||||
- [ ] Deploy, then public TLS/health/readiness/auth/config + synthetic core smoke; correlate version.
|
||||
- [ ] Check safe logs/metrics and queue recovery; stop/repair if release gate fails.
|
||||
|
||||
These are repository recommendations, not evidence that branch protection, deployment permissions, legal policies or production operations currently meet them.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Resilience, partial failure and the 50 failure answers
|
||||
|
||||
Source baseline, labels and limitations: [README](README.md). Findings: [F01–F21](findings.md). “Needs runtime verification” means the scenario was not executed, even when source provides a strong prediction.
|
||||
|
||||
## Dependency and operation failure matrix
|
||||
|
||||
Timeouts marked current come from source; operational alert thresholds are proposals. Normal CRUD should remain useful during optional-provider failures.
|
||||
|
||||
| Dependency / operation | Failure and user impact | Current timeout / retry | Recovery and target UX | Alert / evidence |
|
||||
|---|---|---|---|---|
|
||||
| SQLite / MariaDB | Writes, auth/session checks and reads fail; active AI worker can stop host F20 | Provider defaults; observed SQLite command timeout 30s; no central transient-DB policy | Preserve forms, show service unavailable/reference; bounded worker recovery; never retry ambiguous writes without key | Readiness failures + sustained DB errors; real outage needs runtime verification |
|
||||
| AI sidecar | AI/extraction fails while manual work should continue | API named client 30s; classified queue failures retry within deadline | Persistent operation status and safe retry; synchronous edits remain unsent until reviewed | Task/provider failure rate and pending age; fake-provider tests passed |
|
||||
| Ollama / external AI | Missing model, DNS/HTTP failure, malformed result | Per-task/provider routing; durable default 3 attempts / 15min deadline, worker timeout 300s | Explicit failure or policy-approved fallback; never silently send private content externally | Model/task/version/category counters; live model evaluation not performed |
|
||||
| Parser | Malformed/hostile/oversized input, child timeout | 5MB file; 25s child, 512MiB configured child memory, one parser at a time; container 2GiB/2CPU | Stable error, review/import can retry, previous profile retained | Parser crash/timeout/rejection counts; 37 parser tests passed; container enforcement not exercised |
|
||||
| Chromium PDF | Missing executable/crash/timeout, partial output | 60s process timeout; no shared admission | Retain CV; explain download failed; retry only after cleanup/lookup; F03 bounds required | Active process/RSS/timeouts/files; no heavy live stress test |
|
||||
| SMTP | Verification/reminder send fails or is suppressed | Configured timeout (dev 15s), synchronous send in Task.Run; direct auth/reminder attempts | Outbox/attempt state; do not mark suppressed as delivered; resend verification within token validity | Queue age/send failures/bounces; actual delivery not tested |
|
||||
| Gmail / Graph / IMAP | Mail sync/calendar/send unavailable, revoked credentials | Provider-specific paths; send attempts preserve uncertain status | Manual tracking usable; reconnect action; do not auto-resend uncertain employer mail | Last sync success, auth/provider error, unresolved send attempt |
|
||||
| Stripe | Checkout/portal unavailable; delayed webhooks, role mismatch | SDK calls; failed webhook returns non-2xx | Preserve current local state pending authoritative reconciliation; status banner; deletion waits for cancellation | Inbox age/divergence/cancel failures; sandbox required |
|
||||
| Job site / NAV / translation | URL unavailable, oversized response, DNS fail | Job import client; redirects rejected, 4MB stream cap | Allow manual paste/edit; no empty replacement of a valid job | Timeouts/rejections/cache hit; real sites not called |
|
||||
| File storage | Permission, missing bytes, full disk | I/O exceptions; staged attachment recovery; no universal quota | Roll back or preserve recoverable stage; do not create valid-looking broken artifact | Disk/inode pressure, orphan/missing counters, failed purge |
|
||||
| Proxy / DNS / TLS | Site unreachable despite live backend | External infrastructure unknown | Correct route/cert/DNS, verify from outside; static maintenance response if necessary | External probe independent of host; not inspected live |
|
||||
| Browser network | Offline/delayed/partial save/download | Axios has no shared timeout; no blanket retry; many local async handlers | Keep dirty state, bounded deadlines, retry by operation identity; distinguish ambiguous outcome | UI safe errors and request references; offline CV probe observed failure state |
|
||||
| Logging / telemetry | Sink unavailable, disk growth, blocked script | Console logging; client errors best-effort | Drop/sample bounded optional telemetry; never block saves on a collector | Local dropped-event counter, disk alerts; failure injection pending |
|
||||
| Backups / off-site copy | Missing/partial/inconsistent set | Script checks artifact shape; defaults 30 daily days/12 monthly sets | Preserve previous verified set; restore drill; do not prune based on unverified success | Backup failure and age from independent monitor; restore pending |
|
||||
| Redis / broker / standalone document service | Not present | Not applicable | Do not add failure domains without a measured requirement | Reassess if introduced |
|
||||
|
||||
## Transaction and background-work review
|
||||
|
||||
| Workflow | Existing transaction / state design | Partial failure that matters | Minimum change |
|
||||
|---|---|---|---|
|
||||
| Create CV + version 1 | Two saves inside a relational transaction | Concurrent later edits still lack preconditions | Preserve creation transaction; add F01 |
|
||||
| Career Profile save | Relational children/profile/history transaction; controller later updates user projection | Canonical data committed but legacy projection/user update fails | One commit if same DB context can own all writes, or explicit repairable projection; do not tell user total failure after success |
|
||||
| Job create/status | Job/opportunity/history saved together in normal EF unit | Client loses response and resubmits; no creation intent key | Return/retrieve result by idempotency key |
|
||||
| Attachment upload/delete | Stage/quarantine files; DB commit; promote/purge; startup reconciliation | Commit uncertainty or crash leaves metadata/file transitions pending | Preserve existing protocol, add aggregate quota reservation and missing-file alert |
|
||||
| Submitted package capture | Renders/reads before DB transaction; copies files during transaction; unique package version | Concurrent max-version selection collides; source can change during capture; cleanup race | Allocate package revision safely, use unique attempt storage then publish; freeze source revisions; test file ownership of cleanup |
|
||||
| AI operation | Durable owner/idempotency/lease/deadline, result reference and attempts | Provider succeeds but result commit fails; worker crashes | Result persistence idempotency and lease recovery; F20 loop recovery; classify poison failures as terminal |
|
||||
| Employer send | Durable attempt and payload hash; uncertain state preserved | Provider accepted send but response/DB completion lost | Reconcile by provider message ID when possible; explicit user decision before retry |
|
||||
| Reminder / verification email | Direct send; reminder timestamps committed after loop | Duplicate/suppressed/unrecorded send F07 | Durable intent/attempt ledger; SMTP cannot guarantee exactly-once |
|
||||
| Stripe webhook | Re-fetch then user update then role mutation | Partial role update; overlapping subscription events | Per-customer transactional reconciliation + durable inbox |
|
||||
| Account deletion | Persist stages; revoke sessions/share, quarantine, delete rows, purge, tombstone | In-flight files/results after inventory; Stripe still active; crash replay | F02 cancellation stage; coordinate active work and final inventory; exercise restore tombstones |
|
||||
| Export | Account ZIP on disk; table JSON buffers; delete-on-close stream | Crash leaves ZIP; changes during export can span different logical moments | Consistency semantics/manifest; stale file cleanup; bound memory or background export |
|
||||
|
||||
A transactional outbox is justified for required transactional mail and local billing work because an external side effect cannot join the database commit. It is not a reason to wrap every CRUD handler in infrastructure. Keep APIs/providers outside long DB transactions. Retries need operation identity, a payload hash and a known outcome; GET that triggers AI or writes should be examined before treating all GETs as harmless retry candidates.
|
||||
|
||||
## Resilience test matrix / Break Jobjakt
|
||||
|
||||
| Scenario | Test method | Expected behavior | Current result | Fix / next evidence |
|
||||
|---|---|---|---|---|
|
||||
| Two tabs save same CV | Read once, sequential stale PUTs; concurrent PUT pair | Conflict; newer state preserved | **Observed fail:** 200/200, stale state replaced newer edit | F01; extend to job/profile/restore |
|
||||
| Duplicate create / Save double-click | Two identical job POSTs | Same intended action returns one resource | **Observed:** two 201s, distinct IDs | Introduce explicit intent-key contract; do not block legitimate repeat applications |
|
||||
| Other user's CV IDs | Direct GET/PUT/DELETE as account B | 403/404, no mutation | **Observed pass:** all 404 | Keep permanent owner tests |
|
||||
| Ordinary user admin call | GET admin audit | 403 | **Observed pass:** 403 | Extend all admin verbs |
|
||||
| Same-IP account export | A exports twice, B exports once | Independent account limits | **Observed fail:** B gets 429 | F06 middleware integration test |
|
||||
| Formula/Unicode/HTML input | Create synthetic job `=1+1`, Åse/company emoji, HTML note; export | Store safely, safe spreadsheet cells | **Observed:** storage works; formula preserved unsafe in CSV | F05; full stored-XSS surface still runtime needed |
|
||||
| Offset/DST calendar | Four input instants through DB to ICS | Preserve UTC instant; repeated autumn hour distinct | **Observed fail:** all four return wrong UTC values, HTTP 200 | F21; both database/server-zone matrices |
|
||||
| Offline CV save | Browser context offline during name edit | Failure shown; no false saved state | **Observed pass:** failure message present | Preserve visible state |
|
||||
| Refresh after failed save | Accept unload warning, reload | Recoverable draft if recovery supported | **Observed gap:** draft not recovered | F09; warning exists, recovery does not |
|
||||
| Logout in second tab | Shared-cookie logout + auth-marker event | Clear private UI; deny future requests | **Observed gap:** other editor remains mounted | F08; distinguish visual residue from backend denial |
|
||||
| EN/NB, light/dark, 375/1440 | Static build + Chromium + axe + screenshots | No serious violations/overflow | **Observed:** outer overflow 0; contrast errors all 8 states; desktop scroll-focus errors | F14; manual assistive tech still needed |
|
||||
| Malformed PDF/DOCX, ZIP bomb, parser hang | Existing pytest mocked/isolated child suite | Bounded rejection, no secret inheritance | **Observed:** 37 tests pass; not a live malware corpus | Run container boundary/fuzz corpus in disposable environment |
|
||||
| DB down 30s / 1h | Disconnect disposable MariaDB or fault-inject repository | Bounded errors, live host, retry recovery | **Needs runtime verification**; F20 predicts host risk with AI worker | Add loop failure test then staging drill |
|
||||
| Container restart mid-write/generation | Kill only a disposable deployment after known checkpoints | Durable work resumes; ambiguous writes recover | **Needs runtime verification**; unit lease/recovery tests pass | Exercise actual process/files/DB recovery, not mocks alone |
|
||||
| API latency/timeout/partial response | Local reverse proxy delays/truncates selected responses | Dirty state kept; no blind POST retry | **Needs runtime verification** | Test stale search replies, file upload/download, proxy deadline |
|
||||
| Browser storage corrupt/denied | Invalid JSON/unknown schema + throw on storage APIs | Safe defaults and retained form | Unit owner/theme tests pass; full route sweep **Needs runtime verification** | I18n reads/writes need denied-storage coverage |
|
||||
| Screen reader/keyboard/200–400% zoom | Narrator/NVDA + actual browser zoom + text-spacing | Reach all controls, focus/announcements, usable reflow | **Needs runtime verification** beyond existing keyboard tests/axe | Do not equate viewport width or axe with screen-reader proof |
|
||||
| PDF 1000 simultaneous / 100k jobs | Synthetic capped load/fake renderer first | Global admission, bounded queries/memory | **Needs runtime verification**, source gaps F03/F10 | Never execute uncapped exhaustion on shared host |
|
||||
| Disk/inodes/RSS exhaustion | Quota-mounted temp volumes/cgroup-limited staging | Fail before corruption; alert/recover | **Needs runtime verification** | Preserve backups; no filling the user's actual disk |
|
||||
| Billing/mailer/OAuth outage | Fake gateways then sandbox provider callbacks | Correct pending/failed/retry state | Fake-gateway suites pass; live integrations **Need runtime verification** | F02/F07 and provider-specific contract tests |
|
||||
| Backup corrupt/missing / migration halfway | Restore isolated backup, interrupt test migration | Fail deploy or forward-recover to a known checkpoint | **Needs runtime verification** beyond current migration/unit tests | F12 and signed drill record |
|
||||
|
||||
The harness is observational: `meetsExpectation=false` records an unmet desired control, not a flaky test that should be retried until green. No PDF/CPU flood, actual malicious executable, payment, employer email or production outage was attempted. Logs distinguish deliberate offline/401/blocked-font errors from normal workflow errors. Full packet-loss simulation, clean-machine setup, production network/console/log cleanliness and exhaustive UI state testing are outstanding, not implied by this matrix.
|
||||
|
||||
## What happens if…?
|
||||
|
||||
| # | Scenario | Current answer and evidence boundary |
|
||||
|---|---|---|
|
||||
| 1 | Database offline for 30 seconds | Reads/writes and DB-backed session checks fail. `/health` is process-only; `/ready` checks DB. Enabled AI-worker claim/persist errors can stop the host F20. Forms need recoverable failure UX. **Source; Needs runtime verification.** |
|
||||
| 2 | Database offline for an hour | No offline DB mode. Requests continue failing, leases age, worker/host restart behavior may recur; recovery requires DB availability and operation reconciliation. Alert early, preserve accepted work. **Needs runtime verification.** |
|
||||
| 3 | Docker host restarts | Named DB/file/key/tombstone volumes should survive; `unless-stopped` services restart. In-memory caches/limits reset; durable leases recover. External MariaDB/proxy start order still matters. **Needs runtime verification.** |
|
||||
| 4 | API dies halfway through request | Uncommitted DB transactions roll back; completed commits can be invisible to client. Staged attachments have recovery logic; synchronous PDF/email may be ambiguous or leave residue. Retry by operation ID, not blindly. **Source; Needs runtime verification.** |
|
||||
| 5 | Frontend older than backend | Current unversioned DTOs have no negotiated compatibility/reload protocol. Additive changes usually survive; required/removed fields and enum changes can break open tabs. No automatic safe update guarantee. **Needs runtime verification.** |
|
||||
| 6 | Migration fails halfway through deployment | Startup migration/reconciliation may prevent readiness; provider DDL rollback differs. Image rollback cannot unapply schema. Abort, preserve logs/checkpoint, determine forward fix versus isolated restore. **Needs runtime verification.** |
|
||||
| 7 | Disk reaches 100% | DB/file/log/backup writes can fail; current API has no universal headroom admission policy. Staging helps attachment consistency, not host capacity. Stop costly admission and recover space safely. **Needs runtime verification.** |
|
||||
| 8 | Memory exhausted | OS/container can kill API/renderer; normal process lifetime assumptions fail. Sidecar is constrained, API/PDF not globally bounded. F03/F10 apply. **Needs runtime verification.** |
|
||||
| 9 | AI requests time out | Named client has 30s timeout; durable jobs classify/retry within deadlines, synchronous callers return failure/fallback. Manual data should remain usable. Fake timeout tests pass; live provider recovery **Needs runtime verification**. |
|
||||
| 10 | Selected AI model removed | Configured route fails; privacy-approved fallback may apply. No evidence justifies silently choosing arbitrary replacement. Pin evaluated model and show retriable/unavailable state. **Source; Needs runtime verification.** |
|
||||
| 11 | Email provider unavailable | Direct auth/reminder sends fail; some account changes already commit. Employer-send attempts can retain failed/uncertain state. Verification resend is necessary; outbox gap F07. **Needs runtime verification.** |
|
||||
| 12 | Verification never delivered | User remains unverified where verification required; can request resend, subject to rate limits. No delivery guarantee, bounce proof or durable resend queue shown. Provide clear support/recovery path. **Source; Needs runtime verification.** |
|
||||
| 13 | Stripe sends same webhook five times | Signature verified; each request re-fetches current subscription and reapplies roles. Sequential final state is generally idempotent; redundant work and concurrent writes remain. Handler does not charge five times. **Source; sandbox Needs runtime verification.** |
|
||||
| 14 | Stripe events out of order | Current re-fetch mitigates sequential stale payloads. Concurrent handlers and multiple subscriptions can still conflict; no serialized customer reconciliation. F02. **Source; Needs runtime verification.** |
|
||||
| 15 | Double-click Save | CV single-editor save queue serializes local changes; API does not dedup generic creation. **Observed:** identical job POSTs returned two IDs. A UI disabled button alone is insufficient. |
|
||||
| 16 | Two tabs save same CV | **Observed:** stale and simultaneous PUTs both succeed; newer data can be overwritten. History is not conflict prevention. F01. |
|
||||
| 17 | Refresh during CV generation | Queued extraction/AI can survive through operation records; synchronous preview/PDF request may abort/continue without recoverable browser result. Unsaved client state may be lost. **Source; Needs runtime verification for generation refresh.** |
|
||||
| 18 | Session expires while editing | First 401 clears client auth and re-fetches identity; long-form state can unmount. No complete reauthentication-and-draft recovery contract found. Offline/unload warning behavior was tested separately. **Needs runtime verification for actual expiry.** |
|
||||
| 19 | Logout in another tab | **Observed:** idle private CV editor stays mounted in the other tab. Backend session is revoked and subsequent requests fail. F08. |
|
||||
| 20 | Delete CV referenced by application | Variant/history removed; job itself is not owned by CV. Already captured submitted package retains rendered content/copied bytes. Live selected-variant UI must handle missing resource. **Source and package tests; exact multi-tab deletion Needs runtime verification.** |
|
||||
| 21 | Malformed PDF | Parser/upload bounds reject invalid data or return stable extraction failure; profile should not be replaced with invalid output. **Observed existing parser tests pass**; broader parser fuzzing remains. |
|
||||
| 22 | Malicious uploaded document | Type/expansion/process limits reduce parser risk; no complete malware-scanning/quarantine-verdict service was found. Original download still requires safe headers. **Source; real malware execution not performed. Needs runtime verification of container isolation.** |
|
||||
| 23 | Job URL redirects to localhost | Import HttpClient disallows auto-redirect; fetch rejects 3xx. Redirect is not followed. **Source and importer tests.** DNS rebinding is a separate remaining risk F11. |
|
||||
| 24 | Job description contains prompt injection | It is untrusted prompt data; policy and review limit actions, but wording alone does not guarantee model obedience. No provider/network tools should be granted from model output. Synthetic eval fixtures exist; live adversarial model behavior **Needs runtime verification**. |
|
||||
| 25 | AI returns invalid JSON | Parsing/normalization boundaries can return failure/fallback; durable status records category. Exact all-workload behavior is not proven by one parser. **Source/fake-provider tests; full contract corpus Needs runtime verification.** |
|
||||
| 26 | AI invents experience | Reviewable suggestions and fact-preservation prompts exist; they cannot guarantee truth. Reject unsupported structured claims and require user review before export/send. Live English/Bokmål semantic evaluation **Needs runtime verification**. |
|
||||
| 27 | 1000 users request PDFs | No shared admission: many Chromium processes could exhaust API host. Per-public-IP+slug throttling does not bound aggregate authenticated load. **Source F03; intentionally not stress executed. Needs runtime verification after bounding.** |
|
||||
| 28 | One user creates 100k jobs | Creation lacks total row quota; list pagination does not bound board/analytics/export scans. Expect latency/memory growth, not demonstrated capacity. **Source F10; Needs runtime verification with synthetic data.** |
|
||||
| 29 | Export extremely large | ZIP file output is streamed on download, but generation loads collections/serialized buffers; jobs export materializes all rows. Can exhaust memory/disk or time out. **Source; Needs runtime verification.** |
|
||||
| 30 | Quota reached during operation | AI reserves estimates before work; actual final usage may differ. Attachment check-before-write is racy and excludes other file categories. Need reservation/reconciliation and retained inputs. **Source/unit tests; cross-pipeline load Needs runtime verification.** |
|
||||
| 31 | Subscription expires while logged in | Pro policy reads current DB roles and queued workers recheck; UI may remain stale until re-fetch. Correctness depends on billing sync updating roles. **Source/entitlement tests; delayed-provider event Needs runtime verification.** |
|
||||
| 32 | Account deletion with pending work | Staged deletion revokes sessions/shares and cancels/removes owner work, purges files and records tombstone. In-flight external effects/billing require coordination F02. **Source/deletion tests; concurrent runtime drill needed.** |
|
||||
| 33 | Backups silently fail seven days | File-age/catch-up logic exists locally and scheduled production workflow exists, but an independent delivered alert was not proven. Operator could remain unaware. Gate on independently measured backup age. **Needs runtime verification.** |
|
||||
| 34 | Latest backup corrupt | Shape checks can miss logical corruption. Restore previous independently verified set and establish lost interval; do not overwrite live state before validation. **Needs runtime verification F12.** |
|
||||
| 35 | TLS certificate expires | Browsers block/warn before application loads; backend health can remain green. Renewal belongs to external proxy operator. Independent certificate/HTTPS alert required. **Needs runtime verification.** |
|
||||
| 36 | DNS breaks | Users/provider lookups fail before API workflows; cached clients may behave differently. Check authoritative and recursive resolution separately from health. **Needs runtime verification.** |
|
||||
| 37 | Production secret leaked | Scope depends on JWT/provider/DB/CI/key material. Revoke/rotate and invalidate affected sessions/tokens; preserve evidence and identify exposed data. Key-ring rotation must preserve/re-encrypt required secrets. **Runbook proposal; Needs runtime verification.** |
|
||||
| 38 | Administrator compromised | Can mutate users/roles and read global activity metadata/content; host admin reaches DB/files/keys. Minimal support access and audited privileged reauth are incomplete F13. **Source; no live compromise simulated.** |
|
||||
| 39 | Another container compromised | Shared-network access can reach attached services; isolated AI network is helpful, but backend shares several networks and has high privileges. Actual firewall/volume/socket exposure **Needs runtime verification**. |
|
||||
| 40 | Attacker requests another user's resource ID | **Observed CV GET/PUT/DELETE 404 and admin 403; 753-test suite includes broader owner tests.** This does not prove every future/new route; maintain route inventory/owner regression gate. |
|
||||
| 41 | Browser cache contains previous user's data | Owner-scoped caches exist, but cross-tab UI residue and absent no-store remain. No cross-user HTTP cache exploit was demonstrated. **Source/partial observed; complete Back/cache account-switch test Needs runtime verification.** |
|
||||
| 42 | localStorage corrupt | Theme/auth helpers catch many failures, jobColumns JSON is guarded; language storage and some writes are less defensive. Manual modifications never grant backend roles. Full corrupt/blocked-storage route sweep **Needs runtime verification**. |
|
||||
| 43 | JavaScript error mid-form | Error boundaries can display fallback/reference, but local form state may be lost on remount. Test recoverable draft behavior and safe telemetry; no guaranteed content preservation. **Source; Needs runtime verification.** |
|
||||
| 44 | 200% zoom | Responsive layouts and tested narrow viewports help; actual browser zoom was not exercised. **Needs runtime verification.** Do not equate 375px viewport with zoom certification. |
|
||||
| 45 | Keyboard only | Existing browser flows exercise selected buttons/navigation. New builder axe finds a scrolling-region focus issue; full tab/escape/reorder/return-focus journey remains. **Partial observed; manual verification needed.** |
|
||||
| 46 | Screen reader | No live Narrator/NVDA run. Axe flags do not establish reading order, announcements or accessible generated PDFs. **Needs runtime verification.** |
|
||||
| 47 | Norwegian text twice English length | Actual EN/NB builder states showed outer overflow 0 at 375/1440; not a doubled-string pseudolocale sweep. English template descriptions remain in NB. **Partial observed; pseudolocale/all-route verification needed.** |
|
||||
| 48 | Hundreds-character company/job title | Normal browser suite has long-content workspace/CV cases and passed. Arbitrarily large inputs/100k rows are not covered; cap fields and test wrap/reflow. **Partial observed; extreme boundaries need runtime verification.** |
|
||||
| 49 | Emoji/unusual Unicode | **Observed:** Åse/company emoji and profile content survive synthetic storage/render. Full combining/RTL/confusable corpus and generated glyphs need verification. |
|
||||
| 50 | Paste formatted HTML | Plain inputs store text; React escaping/renderer encoding are intended barriers. Harness stored an HTML note but did not certify every rendering/email/Markdown sink. No rich-text sanitizer should be replaced by regex. **Source/partial observed; persistent-XSS/paste corpus Needs runtime verification.** |
|
||||
|
||||
## Time and compatibility policy
|
||||
|
||||
F21's four live HTTP/SQLite/ICS failures are concrete; the fix must distinguish **calendar date**, **absolute instant**, and **local recurring time with timezone**. Current use of DateTime, UTC timestamps and server-local reminders mixes those meanings. UTC is suitable for security events and leases. Date-only job deadlines must not shift with timezone. Recurring wall-clock behavior, if offered, needs an explicit zone and gap/fold policy. Do not add recurring reminders merely to justify a scheduling platform.
|
||||
|
||||
The production backup workflow checks for 02:00 Europe/Oslo at two UTC schedules. A spring-forward day has no 02:00 local hour; a fall-back day repeats it. Add a date-based idempotency/catch-up test rather than assuming the guard yields exactly one daily backup. Scheduler DST behavior was source-reviewed, not executed in Gitea.
|
||||
|
||||
Adopt expand/contract for externally visible DTO/schema changes: optional fields and readers first, dual-compatible writer period, measured old-client window, then removal. Keep stable IDs/routes and old enum meanings. Introduce explicit API versioning only when real independent clients require it. Build/version metadata already exists; a safe reload prompt may help, but must not discard dirty forms. Old-image compatibility is a precondition to automated rollback, not an assumption.
|
||||
@@ -0,0 +1,36 @@
|
||||
# Source navigation index
|
||||
|
||||
These are the primary source locations cited in [F01–F21](findings.md), at the audit baseline. This is a navigation aid, not an exhaustive endpoint or file inventory. Line numbers in the findings refer to that baseline; source will move as fixes land. Other assessed boundaries and unverified inventories are stated in [coverage.md](coverage.md).
|
||||
|
||||
| Source | Assessment boundary |
|
||||
|---|---|
|
||||
| [JobTrackerApi/Controllers/AdminAuditController.cs](../../../JobTrackerApi/Controllers/AdminAuditController.cs) | F13 — global activity access |
|
||||
| [JobTrackerApi/Controllers/ApplicationCalendarController.cs](../../../JobTrackerApi/Controllers/ApplicationCalendarController.cs) | F21 — DateTime to ICS conversion |
|
||||
| [JobTrackerApi/Controllers/AttachmentsController.cs](../../../JobTrackerApi/Controllers/AttachmentsController.cs) | F10 — quota check and staged upload |
|
||||
| [JobTrackerApi/Controllers/BillingController.cs](../../../JobTrackerApi/Controllers/BillingController.cs) | F02 — checkout/webhook/role updates |
|
||||
| [JobTrackerApi/Controllers/ClientErrorsController.cs](../../../JobTrackerApi/Controllers/ClientErrorsController.cs) | F16 — client URL/message/stack logging |
|
||||
| [JobTrackerApi/Controllers/CvVariantController.cs](../../../JobTrackerApi/Controllers/CvVariantController.cs) | F01/F03 — save contract and PDF entry point |
|
||||
| [JobTrackerApi/Controllers/ExportController.cs](../../../JobTrackerApi/Controllers/ExportController.cs) | F05/F10 — CSV escaping and export materialization |
|
||||
| [JobTrackerApi/Controllers/UsersController.cs](../../../JobTrackerApi/Controllers/UsersController.cs) | F13 — account and role mutations |
|
||||
| [JobTrackerApi/Data/JobTrackerContext.cs](../../../JobTrackerApi/Data/JobTrackerContext.cs) | F01/F10 — filters, indexes and persisted invariants |
|
||||
| [JobTrackerApi/Program.cs](../../../JobTrackerApi/Program.cs) | F06/F19 — middleware order, rate policy and provider defaults |
|
||||
| [JobTrackerApi/Services/AccountDeletionService.cs](../../../JobTrackerApi/Services/AccountDeletionService.cs) | F02/F13 — purge stages and external lifecycle |
|
||||
| [JobTrackerApi/Services/AiOperationQueue.cs](../../../JobTrackerApi/Services/AiOperationQueue.cs) | F20 — outer worker loop exception boundary |
|
||||
| [JobTrackerApi/Services/CareerProfileService.cs](../../../JobTrackerApi/Services/CareerProfileService.cs) | F01 — career aggregate update boundary |
|
||||
| [JobTrackerApi/Services/CvVariantService.cs](../../../JobTrackerApi/Services/CvVariantService.cs) | F01 — revision increments and history |
|
||||
| [JobTrackerApi/Services/DatabaseBackupRunner.cs](../../../JobTrackerApi/Services/DatabaseBackupRunner.cs) | F12 — SQLite snapshot primitive |
|
||||
| [JobTrackerApi/Services/FollowUpReminderHostedService.cs](../../../JobTrackerApi/Services/FollowUpReminderHostedService.cs) | F07/F21 — delivery/commit and scheduling |
|
||||
| [JobTrackerApi/Services/ForwardedProxyConfiguration.cs](../../../JobTrackerApi/Services/ForwardedProxyConfiguration.cs) | F04 — trusted hop/network configuration |
|
||||
| [JobTrackerApi/Services/JobImport/JobImportService.cs](../../../JobTrackerApi/Services/JobImport/JobImportService.cs) | F11 — URL/DNS validation versus connection |
|
||||
| [JobTrackerApi/Services/PlaywrightCvPdfExporter.cs](../../../JobTrackerApi/Services/PlaywrightCvPdfExporter.cs) | F03/F15 — browser process and output lifecycle |
|
||||
| [JobTrackerApi/Services/SmtpEmailSender.cs](../../../JobTrackerApi/Services/SmtpEmailSender.cs) | F07/F16 — suppressed outcomes and private logs |
|
||||
| [JobTrackerApi/Services/StripeBillingGateway.cs](../../../JobTrackerApi/Services/StripeBillingGateway.cs) | F02 — provider request identity |
|
||||
| [deploy/deploy.sh](../../../deploy/deploy.sh) | F12/F18 — live backup, rollback and readiness checks |
|
||||
| [docker-compose.yml](../../../docker-compose.yml) | F03/F04/F18 — exposed ports, networks and container limits |
|
||||
| [docs/architecture/current.md](../../../docs/architecture/current.md) | F17 — architecture documentation drift |
|
||||
| [job-tracker-ui/e2e/accessibility.spec.ts](../../../job-tracker-ui/e2e/accessibility.spec.ts) | F14 — existing axe route coverage |
|
||||
| [job-tracker-ui/nginx.conf.template](../../../job-tracker-ui/nginx.conf.template) | F04/F15 — forwarding and response headers |
|
||||
| [job-tracker-ui/src/App.tsx](../../../job-tracker-ui/src/App.tsx) | F08/F09 — auth lifecycle and mounted private views |
|
||||
| [job-tracker-ui/src/auth.ts](../../../job-tracker-ui/src/auth.ts) | F08 — client auth notifications and storage |
|
||||
| [job-tracker-ui/src/interview-prep.test.tsx](../../../job-tracker-ui/src/interview-prep.test.tsx) | F17 — standalone TypeScript failure |
|
||||
| [job-tracker-ui/src/views/CvBuilderEditor.tsx](../../../job-tracker-ui/src/views/CvBuilderEditor.tsx) | F01/F09/F14/F15 — save queue, preview, state and clipboard |
|
||||
@@ -0,0 +1,180 @@
|
||||
# Threat model, abuse, data lifecycle and invariants
|
||||
|
||||
This model reflects inspected source at the [audit baseline](README.md). It does not certify deployed network boundaries, encryption or legal compliance. F identifiers link to [findings](findings.md).
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
U[Untrusted browser / imported job text / document] --> P[Operator TLS proxy]
|
||||
U -. published host port 3000 .-> N[nginx frontend]
|
||||
P --> N
|
||||
N --> A[ASP.NET Core API: session, CSRF, owner, entitlement]
|
||||
A --> D[(SQLite or external MariaDB)]
|
||||
A --> F[Private files, exports, package copies]
|
||||
A --> K[Data Protection keys / provider secrets]
|
||||
A --> Q[(Durable operations / delivery attempts)]
|
||||
Q --> W[In-process workers with explicit owner scope]
|
||||
W --> A
|
||||
A --> S[Token-protected Python AI sidecar]
|
||||
S --> X[Bounded parser child process]
|
||||
S --> L[Local Ollama / summarization model]
|
||||
S -. consent + configured routing .-> E[External AI provider]
|
||||
A --> M[SMTP / Gmail / Graph / IMAP]
|
||||
A --> B[Stripe signed webhook + API]
|
||||
A --> J[Untrusted public job URL / NAV / translation]
|
||||
D --> R[Backup recovery set]
|
||||
F --> R
|
||||
K --> R
|
||||
R --> O[Off-host destination and custody]
|
||||
A --> G[Operational logs]
|
||||
CI[CI runner + deployment SSH] --> A
|
||||
CI --> R
|
||||
```
|
||||
|
||||
The backend is highly privileged within this design: it can read document contents, decrypt connected-account credentials and modify all owners' database rows. A parser child should not inherit those capabilities. Public CV rendering deliberately crosses the private-to-public boundary, and AI/provider calls cross organizational/privacy boundaries. Browser flags and localStorage do not grant authorization. The dashed ingress route is the F04 topology risk.
|
||||
|
||||
## Assets and intended access
|
||||
|
||||
| Asset / significant fields | Who should access | Who must not | Exposure route and current control | Missing control / action |
|
||||
|---|---|---|---|---|
|
||||
| Account identity, email, pending email, phone, profile name | Owner; minimal account administrator | Other users, anonymous clients | Local identity/session, owner filtering; admin user DTO | Security audit/recent auth for privileged changes; no unnecessary private profile export to support |
|
||||
| PasswordHash, SecurityStamp, session sid, recovery hashes | Auth subsystem only; limited security operator | Browser exports, ordinary admin UI, AI | Identity hashing, live session validation, token/2FA tests | Verify operational key custody; security-event trail |
|
||||
| TOTP encrypted seed, OAuth refresh/access tokens, IMAP password | Corresponding auth/provider service | Clients, unrelated workers, parser child | Data Protection and purpose-specific credential handling | Keys and encrypted values share host boundary; test rotation/restore |
|
||||
| Career profile, CV text/structure, history, drafts | Owner; explicitly enabled AI processing | Other users; support by default | Owner-scoped services, AI consent gates | Conflict preconditions, retention, minimal copies |
|
||||
| CV public slug and published fields | Anonymous holder while sharing enabled | Visitors after revocation; users guessing private IDs | Unpredictable slug; private by default; slug rotates when republished; noindex HTML | Explicit field review/expiry policy; PDF noindex/no-store; do not equate noindex with access control |
|
||||
| Jobs, notes, cover letters, correspondence, interviews, reminders | Owner; explicit employer send recipient | Other users; unrelated support staff | Parent-owner filters and ownership tests | Global admin event feed exposes content; duplicate/concurrent writes |
|
||||
| Uploaded bytes, rendered PDF, submitted package copies | Owner or deliberate public/share recipient | Other owners, arbitrary path requester | Managed paths, staging, quarantine, MIME checks, owner authorization | Global quotas, output cache policy, bounded renderer, isolated malware decision |
|
||||
| Subscription/customer IDs, plan and usage | Owner sees status; billing worker/admin sees minimum | Client cannot set entitlement, unrelated users | Stripe signature and current subscription lookup; role-derived policy | Checkout dedup, customer reconciliation, deletion cancellation |
|
||||
| API/JWT/SMTP/Stripe/AI/CI secrets, Data Protection keys | Narrow service/operator | Frontend, logs, untrusted PRs, parser | Config/env, some child-env stripping, tracked-secret scan | Encrypted custody, rotation drills, artifact/runner policy; runtime proof pending |
|
||||
| Backup/tombstone sets | Recovery operator | Application users, unrelated containers | Separate tombstone volume, restricted backup modes, retention script | Consistency/restore proof and off-host encryption/custody |
|
||||
| Logs/diagnostic IDs | Support uses safe metadata; security team limited event access | Public users and unrestricted private-content search | Request trace IDs, structured production console | PII/URL filtering, immutable security trail, retention and access ownership |
|
||||
|
||||
## Attacker analysis
|
||||
|
||||
| Actor | Likely route / intent | Most useful controls |
|
||||
|---|---|---|
|
||||
| Anonymous internet attacker / bot | Credential stuffing, signup/reset floods, public PDF overload, Host/header spoofing | Canonical origin, trusted ingress, layered auth/public admission and delivery quotas |
|
||||
| Malicious registered user / abusive paying user | ID substitution, storage/database growth, PDF/AI exhaustion, repeated imports | Owner filters at every verb, aggregate limits, atomic quotas, global cost/concurrency bounds |
|
||||
| Compromised user account | Export private career/mail data; send unwanted messages; link provider | Recent auth for export/security actions, revoke all sessions, mail confirmation, safe audit and anomaly counts |
|
||||
| Malicious uploaded document | Parser crash, resource exhaustion, embedded content or renderer escape | File type and decompression bounds, child isolation, network denial, renderer encoding and separate privilege |
|
||||
| Malicious job advertisement | SSRF through source URL; prompt injection; XSS; spreadsheet formula export | Connected-IP validation, treat text as data, encode outputs, fact review, spreadsheet-safe export |
|
||||
| Compromised dependency / CI runner | Read secrets, alter delivered frontend/backend, exfiltrate personal data | Locked/pinned builds, protected runner/workflow, least SSH privilege, artifact provenance, detection and rotation |
|
||||
| Malicious/compromised administrator | Bulk private event access, roles/resets, host/database theft | Minimal admin DTOs, recent authentication, auditable privileged actions, separate host custody |
|
||||
|
||||
STRIDE is used to ensure coverage, not generate duplicate findings: spoofing (session/ingress), tampering (stale edits/forged identifiers), repudiation (missing security audit), disclosure (admin/log/cache/provider boundaries), denial of service (PDF/storage/import), elevation (roles/paid state). Highest combined risk comes from the backend host: compromise reaches database, files and decrypting keys together.
|
||||
|
||||
## Abuse matrix
|
||||
|
||||
Likelihood/impact are qualitative estimates for an openly registered single-host deployment, not observed incident rates. H/M/L = high/medium/low. R/Q/M columns state recommended rate limit, quota, monitoring respectively; “yes” does not mean currently implemented.
|
||||
|
||||
| Abuse case | L/I | Current defence | Recommendation | R / Q / M and monitoring |
|
||||
|---|---|---|---|---|
|
||||
| Automated registration | H/M | Registration switch, auth-login policy, configurable Turnstile | Per-IP + normalized-account attempt control; staged registration | yes/yes/yes; accepted/rejected signup bursts |
|
||||
| CAPTCHA bypass/unavailability | M/M | Server verification when configured | Fail closed; accessible retry/recovery, independent rate limiting | yes/no/yes; challenge error rate |
|
||||
| Disposable email / multi-account / trial cycling | M/M | Verified-email option; no reputation policy seen | Require verified email before cost; measure before buying detection | yes/yes/yes; account/cost anomalies, no intrusive fingerprinting |
|
||||
| Login enumeration/credential stuffing | H/H | Identity/session tests, auth limit | Uniform errors/timing review; account + IP throttles, accessible 2FA | yes/no/yes; failure/success ratio |
|
||||
| Reset / verification flooding | H/M | 5/15min by IP | Recipient/account cooldown, send queue, no existence leak | yes/yes/yes; sends per recipient/provider |
|
||||
| Reminder flooding | M/M | Owner preference, daily sent date, worker switches | Durable daily dedup; per-account/channel send ceiling | yes/yes/yes; duplicate and suppression counts |
|
||||
| Employer send abuse | M/H | Explicit send, provider owner guard, durable send attempts, email-send policy | Fix policy ordering; retain unknown delivery state | yes/yes/yes; provider errors/complaints |
|
||||
| AI calls / credit drain | H/H | Pro gate, 250 calls/1M estimated tokens/month; queue caps | Bound input/output and synchronous concurrency; enforce reservations across replicas | yes/yes/yes; costs/reservations/retries |
|
||||
| Expensive-model selection / oversized output | M/H | Config routing, allowlisted external tasks, prompt bounds | Pin evaluated model per workload; no arbitrary client model/provider | yes/yes/yes; task-model costs/latency |
|
||||
| Repeat document/PDF generation | H/H | Per-process timeout; public IP+slug limit | Shared concurrency and per-account/global budget F03 | yes/yes/yes; processes, queue, output bytes |
|
||||
| Storage exhaustion / malicious hosting | H/H | 10MB attachment, 5MB CV; partial attachment quota | Atomic aggregate quota, bounded file types, safe download, quarantine decision | yes/yes/yes; bytes/category, orphan scan |
|
||||
| API scraping / analytics scraping | M/M | Auth/owner isolation; some pagination | Category sustained limits and query bounds | yes/yes/yes; scanned rows, response bytes |
|
||||
| Job-import URL fetching | H/H | HTTP(S), DNS private-IP rejection, no redirects, 4MB stream bound | Pin validated connect destination, per-owner/import/global concurrency | yes/yes/yes; denies, duration, destinations by class |
|
||||
| Search/regex expensive input | M/H | Owner filters; fixed regex patterns; some bounded import input | Max query length, timeout/non-backtracking where needed, query-plan fixtures | yes/yes/yes; p95/p99, rows scanned |
|
||||
| Notification / queue flooding | M/M | AI queue 10/account and 100/global by default | Cover all queues and persistent record retention; dedup notifications | yes/yes/yes; pending age, rejected admissions |
|
||||
| Webhook replay / out-of-order events | H/H | Signed events and current subscription re-fetch | Durable inbox + customer reconciliation/transaction F02 | yes/yes/yes; duplicates, lag, divergence |
|
||||
| Repeated checkout / payment actions | M/H | Existing-Pro check | Purchase-intent idempotency, active-session reuse | yes/yes/yes; active subscriptions/account |
|
||||
| Database-growth attack / 100k jobs | H/H | Paged list; no record quota found | Row/byte admission and bounded board/export/history | yes/yes/yes; owner/table growth |
|
||||
| CSV formula injection | M/H | CSV structural quoting | Spreadsheet-safe text escaping F05 | no/no/yes; regression gate, no content telemetry |
|
||||
| Log/error endpoint flooding | M/M | 32KB body/field truncation | Rate limit, sampled safe codes, bound log volume | yes/yes/yes; ingestion/dropped events |
|
||||
| Session/export abuse after compromise | M/H | Recent sign-in for account ZIP; session revocation | Atomic account limit and safe export audit, notify where appropriate | yes/yes/yes; export counts/bytes |
|
||||
|
||||
## Rate strategy
|
||||
|
||||
Current fixed windows: auth-login **10/5min/IP** (configurable); auth-email **5/15min/IP**; 2FA challenge **5/5min/IP**; employer-send **10/min intended owner**; account export/deletion **2/hour intended owner**; public PDF **3/min/IP+slug**. The two intended owner policies fall back to IP due to middleware order (observed F06). There is no universal request limiter or distributed limiter. AI admission and monthly accounting are distinct controls.
|
||||
|
||||
**Proposed starting limits, tune with staging measurements:** auth keep tight IP bursts plus per-account cooldowns without permanent victim lockout; normal API 120/min/owner with burst 30; search/analytics 30/min/owner and input bounds; import 10/min/owner with 2 in flight; PDF 5/min/owner, 1 active/owner, 2 active globally and a small bounded queue; AI 2 active/owner plus existing durable capacity and monthly limits; public PDF 3/min/IP plus a shared renderer cap. These are initial policy values, not capacity claims. Add a more permissive edge per-IP flood limit to protect application parsing, and distinguish webhook signature admission from browser API traffic.
|
||||
|
||||
Return 429 with retry timing, accessible explanation and preserved inputs. Global PDF/AI budgets matter even with many accounts/IPs. Keep process-local controls only while there is one backend; use database reservations before replicas. Adopt distributed rate storage only when multiple processes actually enforce the same budget.
|
||||
|
||||
## Data classification
|
||||
|
||||
These engineering categories are not determinations of statutory “special category” data.
|
||||
|
||||
| Class | Major fields/examples | Handling |
|
||||
|---|---|---|
|
||||
| Public by explicit decision | Published CV fields/slug, marketing copy, template metadata | Review before publish; revoke links; retain noindex for token links; still treat public content as untrusted input |
|
||||
| Internal operational | Build version, task type, duration, safe error code, queue state | No arbitrary user text/query strings; bounded logs; support can diagnose by reference |
|
||||
| Personal | Name, email, phone, company/contact details, salary preferences, provider subjects, IP/session metadata | Owner access; minimal support visibility; purpose-based retention; protected backup and export |
|
||||
| Sensitive career/private content | CV/profile/history, notes, applications, job-search activity, mail bodies, letters, interview notes, uploaded files and AI prompts/responses | No analytics/log capture; explicit third-party use; no-store; restricted at-rest storage/backup; deletion through every copy |
|
||||
| Authentication/security | Password hashes, TOTP/recovery material, sessions, CSRF tokens, OAuth access/refresh tokens, IMAP credential | Never export to user-data ZIP or send to AI; hash where verification-only, protect reversible secrets; recent auth and audited rotation |
|
||||
| Secrets | JWT signing key, Data Protection key ring, provider/Stripe/SMTP/CI credentials | No logs/source/frontend; separate restricted recovery custody; rotate with tested dependency plan |
|
||||
|
||||
At-rest encryption of database/files/host/backup is **not demonstrated**. Data Protection protects selected credential values but cannot defend against a host attacker who also obtains its keys. Encrypt host volumes and off-host backups with recoverable custody; do not encrypt every searchable career field in application code without a specific threat and query plan. Browser-to-proxy TLS is operational; proxy-to-API is HTTP on Docker networking; external-provider TLS is used by service clients where configured. MariaDB transport encryption **Needs runtime verification**.
|
||||
|
||||
## Data lifecycle and copy map
|
||||
|
||||
All persistent DB/file rows may enter recovery backups; exclusions must be explicit. “Export” below means account ZIP unless noted. Retention details follow in the next table.
|
||||
|
||||
| Data group | Enters / validation | Primary storage | Copies/cache/browser | Logs / third parties | Export / deletion |
|
||||
|---|---|---|---|---|---|
|
||||
| Account identity/settings | Auth/admin/profile DTO; Identity email/password checks; verified email-change workflow | AspNetUsers, roles, claims/logins | `/auth/me`; owner key and UI preferences in localStorage; cookies | Some email logs; identity/OAuth and optional mail providers | Selected safe account fields; staged account purge and session invalidation |
|
||||
| Security material | Password/2FA/OAuth/IMAP flows, token validation | Identity hashes, encrypted connection credentials, sessions/trusted devices; Data Protection key files | HttpOnly auth cookie; readable CSRF cookie; service memory | Never intentionally AI/export; log exception/URL canaries needed | Safe session metadata only; revoke/remove on security changes/deletion; key ring has independent retention |
|
||||
| Career Profile / CV information | Manual structured DTO validator or extraction review | CareerProfiles + relational children + version snapshots | Profile JSON and raw text also on ApplicationUser; editor state; sidecar cache if used | AI only through policy; no purpose for operational content logs | JSON profile and versions; account deletion removes rows/files; conflict/retention gaps |
|
||||
| Uploaded CV / extraction | Multipart 5MB, file/parser/ZIP bounds, bounded child | CvUploadArtifacts/CvExtractionRuns + artifact files | Raw file, extracted text, structured result, current artifact pointer | Local parser/AI; external processing only policy-approved; parser errors sanitized | Original/current artifacts plus safe metadata; completed extraction history pruned, current artifact preserved |
|
||||
| CV variants / generated PDFs | Settings normalization and theme/owner rules | CvVariants/CvVariantVersions; owner/date PDF files | Settings history; render against current profile; in-memory HTML/bytes; iframe | Public share if enabled; browser attempted Google Fonts in audit | JSON/files; immediate variant delete/history removal; rendered outputs expire lazily on later exports |
|
||||
| Jobs / descriptions / companies | Manual/import DTOs; owner check; URL DNS/type/body guard | Jobs + JobApplications duplicated compatibility fields; Companies | Search/list/board memory; browser filters; derived AI summaries | Job sites/NAV/translation; optional AI; admin event feed exposes title/company/notes | JSON/CSV/ZIP; soft delete/trash differs from purge; F05 formula risk |
|
||||
| Letters / notes / AI responses | Human edit/review and structured AI parsing | JobApplication drafts, CoverLetterVersions, AiWorkspaceNotes/Interactions | Versions, submitted packages, prompt context, local component state | AI provider as allowed; no automatic employer send authorized by model output | Safe readable data export; delete all related copies; persistent version retention undecided |
|
||||
| Correspondence / email drafts | Gmail/Graph/IMAP sync, owner-bound review/import/send | Correspondence, connection/review rows, email drafts/attempts | Provider mailbox remains a separate copy; extracted AI context | Mail providers; SMTP logging can expose recipient/subject | Application-owned copies exported/deleted; deleting Jobjakt does not delete remote mailbox |
|
||||
| Interviews / reminders / notifications | Manual/scheduler/calendar input and owner rules | Interview prep/events, job dates, notification/settings rows | Calendar/provider copy, browser pending state, reminder email | Email/calendar provider; delivery F07 and timed-event conversion F21 | JSON/ZIP; owner purge; already sent email/calendar copies need provider-specific policy |
|
||||
| Submitted packages | Explicit capture, owner render, managed attachment paths | Immutable package fields + copied file bytes and hashes | Profile/settings/job description/notes/rendered HTML duplicated intentionally for provenance | Should stay private except explicit download/send | Historical JSON/files; deletion follows account; live CV edits do not rewrite captured package |
|
||||
| AI operations / usage | Pro, privacy and capacity reservation | UserOperations, AiUsageRecords and referenced result rows | In-memory execution state, sidecar one-hour cache; counts in UI | Provider/model/task/duration metadata; prompts may contain career/mail data | Safe operation/result data; deletion cancels/purges and clears sidecar cache; provider retention separately governed |
|
||||
| Billing | Signed webhook + current subscription lookup | Stripe IDs/status on user; roles | Stripe is billing authority, UI role/status cache | Stripe retains its own records; no card details stored here | Safe billing identifiers; current deletion omits cancellation F02 |
|
||||
| Backup/audit/analytics | DB snapshots, file archives, job events, request/client logs | Data-root backup + external recovery sets, console/container logs; analytics derived from jobs | Off-site destination; audit event feed; support screenshots | Operator/provider custody; avoid sending private analytics elsewhere | Backups expire by policy; tombstone replay prevents erased account restoration; audit retention needs decision |
|
||||
|
||||
Unnecessary or risky copies: routine `/auth/me` returning career-related material deserves payload minimization; ApplicationUser career projections require explicit repair/consistency ownership; all-history JSON export materializes extra buffers; unbounded settings/AI history accumulates. Submitted-package copies are justified provenance and should not be deleted as “duplication.” Do not retroactively claim live CV history is a frozen rendered document: variant settings re-render against the current master profile.
|
||||
|
||||
## Retention matrix
|
||||
|
||||
| Data | Storage / purpose | Current retention | Delete behavior | Backup behavior / target decision |
|
||||
|---|---|---|---|---|
|
||||
| Account, profile, jobs, companies | DB; core service | No general time expiry found | Staged account purge; jobs may remain in trash until purge | Included; disclose delayed backup expiry |
|
||||
| CV settings/profile/letter histories | DB; recovery/provenance | No general bounded history retention found | Variant/account paths remove related rows | Define revision cap/age and pinned historical packages before pruning |
|
||||
| CV extraction results | DB/files; import review | Newest 20 completed runs, current artifact preserved | Expired/unreferenced artifact cleanup | Verify backup copies age independently |
|
||||
| Generated PDF | Owner/date folders | Default 30 days, configurable 1–365; pruning invoked on export | File cleanup/account lifecycle | Idle systems can retain files beyond interval; schedule cleanup if promising strict expiry |
|
||||
| Attachments/package snapshots | Managed files + DB | While retained by product/account | Stage/quarantine/commit/purge; reconcile interrupted states | Include manifests/hashes and original/published copies |
|
||||
| User account ZIP | Owner export folder | Stream uses DeleteOnClose after generation | Successful download disposes/removes file | Crash before streaming/disposal needs stale-artifact cleanup verification |
|
||||
| AI sidecar cache | Process memory | 1h TTL / 1024 entries | Cache purge endpoint, process restart | Not a DB backup; request/result records are separate |
|
||||
| AI usage/operations/notifications | DB; metering/recovery | No general purge policy found | Owner deletion; operation state deadlines are not row retention | Propose short result/diagnostic retention with longer aggregate usage; owner decision |
|
||||
| Email/provider connection | DB plus remote provider | Until disconnect/deletion; provider has own policy | Local token/copy removal | Cannot promise removal of mail already delivered elsewhere |
|
||||
| Sessions/trusted devices | DB/security | Credential validity expiry; row expiry cleanup not established | Revoke/delete on relevant account actions | Never restore usable sessions without security policy review |
|
||||
| Security audit / operational logs | Proposed separate ledger / current logs | Docker rotation 10MB x3 for defined services; time policy absent | Current timeline deletions are not immutable audit handling | Proposal: 30-day operational, 90-day security initial review; not legal requirements |
|
||||
| Local SQLite scheduled backups | Data-root backups | Default 14 retained snapshots | Old snapshots pruned | Local host loss still loses these; off-host complete sets needed |
|
||||
| Deploy recovery sets | Host backup directory/off-host copy option | Default 30-day daily + 12 monthly sets | Retention script pruning | Actual off-host encryption, lifecycle and restore proof required |
|
||||
| Deletion tombstones | Independent volume | No expiry established | Must survive older application restore | Keep through longest recoverable backup horizon; avoid logging owner personal content |
|
||||
| Billing metadata | User row + Stripe | Local tied to account; provider policy external | F02 unresolved subscription cancellation | Retained billing/security fields and statutory periods require owner/legal decision |
|
||||
|
||||
Access/correction/restriction/deletion requests need a documented operator channel, identity verification proportional to risk, export validation and a tracked outcome. Technical functionality does not establish compliance. Consent/privacy copy must identify local versus external AI, remote mail/calendar copies, backup lag, disabled deletion where applicable and optional public fields. Do not add a cookie banner solely because essential session cookies exist; assess actual non-essential services and jurisdiction with qualified review.
|
||||
|
||||
## Product invariants and enforcement
|
||||
|
||||
| Invariant | Current enforcement / proof | Gap / permanent gate |
|
||||
|---|---|---|
|
||||
| No cross-user read/write/download | DbContext owner filters, CurrentUserService, many authorization suites; CV probes 404 | Extend owner substitution to every newly added endpoint/background subject |
|
||||
| Referenced company/CV/job belongs to owner | Service validations, association tests; ordinary FKs | No blanket DB-level composite ownership proof; test joins/IgnoreQueryFilters/admin paths |
|
||||
| Private data not published by default | IsPublic false; explicit share and rotating slug | Preview fields and revocation/noindex/file cache tests |
|
||||
| Newer edit cannot be silently overwritten | No client revision contract | F01 currently violates; 409/412 and draft preservation |
|
||||
| History is truthful | Append snapshots and submitted-package frozen bytes | Settings history is not immutable rendering; duplicate version race remains |
|
||||
| Retry does not duplicate irreversible effects | AI keys/leases and employer send attempt states | Job create/checkout/reminder paths need intent dedup |
|
||||
| Only trusted server state grants paid access | Pro handler reads roles; queue rechecks before work | Concurrent billing synchronization/deletion F02 |
|
||||
| Failed generation leaves valid content intact | Draft/review paths, structured parsing and queue status | Fault-inject every overwrite boundary; no “ready” without valid artifact |
|
||||
| Deleted account cannot regain access after restore | Session/public revocation, staged purge, independent tombstones, tests | Billing cancellation and actual full restore drill |
|
||||
| Owner work survives worker restart | Durable operations/lease recovery tests | Synchronous work/mail and file-in-progress behavior need explicit policy |
|
||||
| Quota cannot be overspent concurrently | Process-local AI gates; partial upload check | Cross-pipeline/replica DB reservation; all-category byte accounting |
|
||||
| AI does not invent/send facts autonomously | Prompts, normalization, explicit review/send APIs and synthetic eval fixtures | Live model/factuality/Bokmål evaluation still required; substring fixtures are not semantic proof |
|
||||
| Startup cannot silently select wrong storage | Deploy script validation | F19 application-level configuration fallback |
|
||||
|
||||
Property/fuzz candidates: URL canonicalization and blocked IP families; status/date normalization; salary/numeric bounds; schema round-trip and revision monotonicity; filename/CSV quoting; bounded DOCX/PDF metadata. Use seeded bounded loops and existing frameworks first. Mutation testing is worthwhile only for owner predicates, quota comparison, session revocation and revision checks; do not optimize a whole-repo mutation or line-coverage percentage.
|
||||
@@ -0,0 +1,152 @@
|
||||
using System.Data.Common;
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using JobTrackerApi.Services.JobImport;
|
||||
using JobTrackerApi.Services.JobImport.Translation;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
|
||||
namespace IndependentAuditValidation;
|
||||
|
||||
public static class Entry
|
||||
{
|
||||
public static async Task Main(string[] args)
|
||||
{
|
||||
if (args.Any(arg => arg.StartsWith("--print-to-pdf=")))
|
||||
{
|
||||
// Inert browser substitute. It opens no network connection and renders no HTML.
|
||||
var root = Environment.GetEnvironmentVariable("VALIDATION_FAKE_BROWSER_ROOT")!;
|
||||
await File.WriteAllTextAsync(Path.Combine(root, Environment.ProcessId + ".started"), "started");
|
||||
for (var i = 0; i < 100 && !File.Exists(Path.Combine(root, "release")); i++) await Task.Delay(50);
|
||||
await File.WriteAllTextAsync(args.Single(arg => arg.StartsWith("--print-to-pdf="))[15..], "%PDF-synthetic-launch-probe");
|
||||
return;
|
||||
}
|
||||
var output = new List<object>();
|
||||
foreach (var target in new[] { "http://10.1.2.3/", "http://[::ffff:10.1.2.3]/", "http://169.254.169.254/", "http://[::ffff:169.254.169.254]/", "http://192.168.1.1/", "http://[::ffff:192.168.1.1]/", "http://[::1]/", "http://[::ffff:127.0.0.1]/" })
|
||||
{
|
||||
var handler = new RecordingHandler();
|
||||
var clientFactory = new Mock<IHttpClientFactory>();
|
||||
clientFactory.Setup(x => x.CreateClient(It.IsAny<string>())).Returns(new HttpClient(handler));
|
||||
var resolver = new Mock<IHostAddressResolver>();
|
||||
resolver.Setup(x => x.ResolveAsync(It.IsAny<string>(), It.IsAny<CancellationToken>())).ReturnsAsync([IPAddress.Parse("93.184.216.34")]);
|
||||
var service = new JobImportService(clientFactory.Object, new UniversalJobParser(), Array.Empty<IJobSitePlugin>(), new NoOpTranslationService(), resolver.Object);
|
||||
var result = await service.PreviewAsync(target, default);
|
||||
output.Add(new { probe = "URL guard with non-network HTTP fake", target, handler.Calls, result.Parser, result.Error });
|
||||
}
|
||||
var folder = Path.Combine(Path.GetTempPath(), "jobjakt-boundary-" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(folder);
|
||||
var fakeRoot = Path.Combine(folder, "fake-browser");
|
||||
Directory.CreateDirectory(fakeRoot);
|
||||
var executable = Path.ChangeExtension(typeof(Entry).Assembly.Location, OperatingSystem.IsWindows() ? ".exe" : null);
|
||||
Environment.SetEnvironmentVariable("CV_PDF_BROWSER_PATH", executable);
|
||||
Environment.SetEnvironmentVariable("VALIDATION_FAKE_BROWSER_ROOT", fakeRoot);
|
||||
var pdfConfig = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?> { ["Data:Root"] = folder }).Build();
|
||||
var environment = new Mock<IHostEnvironment>();
|
||||
environment.SetupGet(x => x.ContentRootPath).Returns(folder);
|
||||
var pdf = new PlaywrightCvPdfExporter(new AppPaths(pdfConfig, environment.Object), Microsoft.Extensions.Logging.Abstractions.NullLogger<PlaywrightCvPdfExporter>.Instance, pdfConfig);
|
||||
var renders = Enumerable.Range(0, 3).Select(i => pdf.ExportAsync("synthetic-owner", new TailoredCvRenderResult("synthetic", "synthetic.pdf", "<p>Inert</p>"), default)).ToArray();
|
||||
for (var i = 0; i < 100 && Directory.GetFiles(fakeRoot, "*.started").Length < 3; i++) await Task.Delay(50);
|
||||
var concurrentLaunches = Directory.GetFiles(fakeRoot, "*.started").Length;
|
||||
await File.WriteAllTextAsync(Path.Combine(fakeRoot, "release"), "release");
|
||||
await Task.WhenAll(renders);
|
||||
output.Add(new { probe = "actual PDF exporter with three inert child processes", concurrentLaunches });
|
||||
var connectionString = "Data Source=" + Path.Combine(folder, "independent.db");
|
||||
var options = new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(connectionString).Options;
|
||||
using (var db = new JobTrackerContext(options, new FixedOwner()))
|
||||
{
|
||||
await db.Database.EnsureCreatedAsync();
|
||||
db.Users.Add(new ApplicationUser { Id = "synthetic-owner", UserName = "owner@example.test", NormalizedUserName = "OWNER@EXAMPLE.TEST" });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
var career = new Mock<ICareerProfileService>().Object;
|
||||
var renderer = new Mock<IThemedCvRenderer>().Object;
|
||||
int id;
|
||||
using (var db = new JobTrackerContext(options, new FixedOwner()))
|
||||
id = (await new CvVariantService(db, career, renderer).CreateAsync("synthetic-owner", "Initial", null, new CvVariantSettings(), default)).Id;
|
||||
using (var dbA = new JobTrackerContext(options, new FixedOwner()))
|
||||
using (var dbB = new JobTrackerContext(options, new FixedOwner()))
|
||||
{
|
||||
var a = new CvVariantService(dbA, career, renderer);
|
||||
var b = new CvVariantService(dbB, career, renderer);
|
||||
await a.GetAsync("synthetic-owner", id, default);
|
||||
await b.GetAsync("synthetic-owner", id, default);
|
||||
await a.SaveAsync("synthetic-owner", id, "Writer A", new CvVariantSettings(), "a", default);
|
||||
await b.SaveAsync("synthetic-owner", id, "Writer B", new CvVariantSettings(), "b", default);
|
||||
}
|
||||
using (var db = new JobTrackerContext(options, new FixedOwner()))
|
||||
{
|
||||
var current = await db.CvVariants.SingleAsync();
|
||||
var versions = await db.CvVariantVersions.OrderBy(v => v.Id).Select(v => v.Version).ToListAsync();
|
||||
output.Add(new { probe = "two preloaded real SQLite contexts", current.Name, current.Version, historyVersions = versions });
|
||||
}
|
||||
foreach (var concurrency in new[] { 1, 2 })
|
||||
{
|
||||
var fault = new OneShotDbFailure();
|
||||
var config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Workers:AiOperationsEnabled"] = "true", ["AiQueue:WorkerConcurrency"] = concurrency.ToString(),
|
||||
["AiQueue:IdleDelayMs"] = "100"
|
||||
}).Build();
|
||||
using var host = new HostBuilder().ConfigureLogging(l => l.ClearProviders()).ConfigureServices(services =>
|
||||
{
|
||||
services.AddSingleton<IConfiguration>(config);
|
||||
services.AddSingleton<TimeProvider>(TimeProvider.System);
|
||||
services.AddScoped<ICurrentUserService, NeutralOwner>();
|
||||
services.AddDbContext<JobTrackerContext>(o => o.UseSqlite(connectionString).AddInterceptors(fault));
|
||||
services.AddScoped<UserOperationStore>();
|
||||
services.AddSingleton<AiPrivacyPolicy>();
|
||||
services.AddSingleton<AiOperationExecutionScope>();
|
||||
services.AddSingleton<IAiOperationHandler, UnusedHandler>();
|
||||
services.AddSingleton<AiOperationWorker>();
|
||||
services.AddSingleton<AiOperationHostedService>();
|
||||
services.AddHostedService(p => p.GetRequiredService<AiOperationHostedService>());
|
||||
}).Build();
|
||||
await host.StartAsync();
|
||||
await Task.Delay(150);
|
||||
fault.Armed = true;
|
||||
await Task.Delay(700);
|
||||
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();
|
||||
var service = host.Services.GetRequiredService<AiOperationHostedService>();
|
||||
output.Add(new { probe = "one transient database command failure", concurrency, fault.Failures, hostStopping = lifetime.ApplicationStopping.IsCancellationRequested, hostedTaskFaulted = service.ExecuteTask?.IsFaulted, error = service.ExecuteTask?.Exception?.GetBaseException().GetType().Name });
|
||||
using var shutdown = new CancellationTokenSource(TimeSpan.FromSeconds(3));
|
||||
await host.StopAsync(shutdown.Token);
|
||||
}
|
||||
Console.WriteLine(JsonSerializer.Serialize(output, new JsonSerializerOptions { WriteIndented = true }));
|
||||
}
|
||||
}
|
||||
|
||||
sealed class FixedOwner : ICurrentUserService { public string? UserId => "synthetic-owner"; }
|
||||
sealed class NeutralOwner : ICurrentUserService { public string? UserId => null; }
|
||||
sealed class RecordingHandler : HttpMessageHandler
|
||||
{
|
||||
public int Calls;
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Calls++;
|
||||
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("<html><body>Synthetic inert HTML; no sockets are opened.</body></html>") });
|
||||
}
|
||||
}
|
||||
sealed class OneShotDbFailure : DbCommandInterceptor
|
||||
{
|
||||
public volatile bool Armed;
|
||||
private int fired;
|
||||
public int Failures => fired;
|
||||
public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (Armed && Interlocked.CompareExchange(ref fired, 1, 0) == 0) throw new TimeoutException("Synthetic one-shot database timeout");
|
||||
return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
|
||||
}
|
||||
}
|
||||
sealed class UnusedHandler : IAiOperationHandler
|
||||
{
|
||||
public string TaskType => "synthetic.validation";
|
||||
public Task<AiOperationExecutionResult> ExecuteAsync(AiOperationExecutionContext context, IServiceProvider services, CancellationToken cancellationToken) => throw new InvalidOperationException("No operations were queued in this probe.");
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
# Independent validation of the Jobjakt audit
|
||||
|
||||
Validated on 2026-09-07 against application commit `7d641edc7bca866ccf06012897ba1d107c07e59f`. This is a verification and planning pass. No application behavior, schema, deployment configuration or production data was changed. The original audit is preserved.
|
||||
|
||||
**Subsequent implementation status, 2026-09-09:** [B01 HTTP import enforcement](b01-completion.md), [B02 account/session isolation](b02-completion.md), [B03 shared PDF admission](b03-completion.md), [B04 authenticated rate partitioning](b04-completion.md) and [B05 spreadsheet-safe CSV](b05-completion.md) are complete locally. B05 verification passed 902 selected backend tests, 298 frontend tests, 18 browser tests, nine B04 HTTP scenarios and the unchanged B03 admission probe. Release builds and targeted lint pass; the existing R01a TypeScript failure remains. The historical analysis below describes the pre-fix baseline. F05 is resolved locally under its defined export policy; F08/V01 and F06 are resolved locally. F11 remains open for B07/IMAP and deployment evidence. F03 admission is resolved locally, with R04 containment/capacity evidence still open. B06 was subsequently authorized; see the separate progress and remaining gates below.
|
||||
|
||||
**B06 runtime follow-up, 2026-09-09:** [Report and operator handoff](b06-completion.md#runtime-verification-follow-up-2026-09-09). **Implementation complete; full runtime/production verification incomplete; F04 open.** Fresh 166 focused backend, 3 Compose, 6 Production HTTP and 9 B04 HTTP checks pass. Public DNS/valid HTTPS are now observed; direct ports 3000/5202/8080 are unreachable from this workstation, without proof of the rejecting layer. HTTP redirects to unreachable HTTPS port 444. Docker's inference socket failure still blocks actual Compose/Traefik/nginx tests; production peers, backend client IP and host/provider firewall remain unverified. No SSH alias/user was found in permitted non-secret sources. B01–B05 preserved; B07 not started.
|
||||
|
||||
**Verdict: Suitable for private beta only.** Keep participation controlled, with informed testers, an operator recovery path and paid checkout disabled until its gates pass. This is not approval to expose the current import/PDF surfaces to unrestricted registration. The evidence does not establish a universal tenant authorization failure that would make all external use inappropriate, but it does establish account-switch corruption, silent lost updates and insufficient abuse containment. Those prevent a public free beta. Paid use adds unresolved billing/deletion and operational proof requirements.
|
||||
|
||||
**Confidence in the original audit: 7/10.** It finds substantial real problems, provides useful source locations, and often labels unverified conditions honestly. Its 416-row coverage inventory is a map of evidence and open questions, not 416 completed validations. Severity is too broad for CSV, calendar and privileged-support concerns; the account-switch consequence is understated; the import address filter has a simpler bypass than the reported DNS race. Its implementation order puts recovery too late and puts identity isolation too close to a general UI cleanup.
|
||||
|
||||
- [Validated implementation queue, ticket sizes, dependencies and handoff](implementation-queue.md)
|
||||
- [Evidence, commands, limitations and reproduction instructions](evidence.md)
|
||||
- [Independent browser/API observations](evidence/observations.json)
|
||||
- [Independent .NET boundary observations](evidence/boundary-results.json)
|
||||
|
||||
## Audit confidence and method
|
||||
|
||||
Read the original README, findings, coverage, verification, resilience, product-operations, threat-data, source-index, `break-jobjakt.mjs`, retained break results and relevant `docs/todo/work.md` items. Inspected the implementation and tests behind the high-priority claims, rather than accepting their descriptions. The pasted expanded audit request provides background; the current validation-only instruction controls this pass.
|
||||
|
||||
Fresh verification: **149 backend tests passed** in two disjoint targeted runs (70 + 79); **13 interview frontend tests passed**; standalone TypeScript failed with the exact original error. Independent probes used synthetic SQLite databases, real local HTTP endpoints, two browser tabs, the real PDF exporter with three inert child executables, and a real .NET host with a one-shot database fault. Existing original full-suite/build results are historical evidence, not new executions. No live Stripe, SMTP, DNS rebinding, production host, production database, external website or cloud metadata endpoint was contacted.
|
||||
|
||||
The original harness is useful observational evidence, not a regression gate: recording a failed expectation does not itself make its exit code fail. Its simultaneous HTTP saves do not force both EF contexts to load the same revision. Its repeated POST does not establish an advertised idempotency contract. Its combined overflow/axe result conflates layout and accessibility. The independent probes add a deterministic two-context schedule, a UTC calendar control, actual cross-account saving, and process/fault boundaries. These probes also collect observations; implementation tickets must turn the relevant failures into assertions.
|
||||
|
||||
## Every original P0/P1 finding
|
||||
|
||||
There are **no original P0s and 11 original P1s**, including conditional P1s. None is omitted below. Classifications distinguish a proven implementation defect from its unverified production impact. No new P0 is supported.
|
||||
|
||||
### F01 — stale saves and revision history
|
||||
|
||||
**Confirmed. Retain P1.** The HTTP stale save returned 200 and replaced the newer state. Two independently preloaded real SQLite contexts both saved revision 1; the final row was Writer B/revision 2 and history was **[1, 2, 2]**. This is recoverable only in part: settings snapshots exist, but variant names are not included in those snapshots, and duplicate revision numbers make restore identification ambiguous.
|
||||
|
||||
The root cause is correct: [SaveAsync](../../../../JobTrackerApi/Services/CvVariantService.cs#L108) increments an ordinary integer after reading, [SaveRequest](../../../../JobTrackerApi/Controllers/CvVariantController.cs#L34) supplies no expected revision, and [the model/index](../../../../JobTrackerApi/Data/JobTrackerContext.cs#L498) does not make the update conditional. A database transaction alone will not reject a stale client. Career Profile child/history writes already have a transaction; their missing precondition and the later legacy user projection are separate remaining problems. Job full-document updates likewise have no client revision precondition.
|
||||
|
||||
**Corrected fix:** define a small revision contract and implement it aggregate by aggregate. Atomically compare expected revision and increment on update; make history insertion part of that transaction. Reconcile existing duplicate labels without discarding snapshots before adding uniqueness. Retain the draft on 409/412; do not automatically retry it over newer content. `StructuredCvProfile.version` is a document schema version, not a concurrency revision: introduce a distinct field. Include restore and every mutation capable of changing that aggregate; do not assume one PUT covers status patches, admin undo or generated drafts.
|
||||
|
||||
**Acceptance:** the proposed tests are appropriate but require deterministic barriers/two contexts and both supported relational providers, not an in-memory provider or two loosely timed requests. Assert one winner, no partial children/projection/history, a preserved losing draft, unambiguous restore, delete-versus-save behavior and an older open frontend receiving a safe refusal. Do not promise transparent compatibility for old unconditional writers while claiming lost updates are prevented. Queue: D01–D04.
|
||||
|
||||
### F02 — billing and account deletion
|
||||
|
||||
**Mostly confirmed. Retain P1 for paid operation; provider effects need runtime verification.** [Checkout](../../../../JobTrackerApi/Controllers/BillingController.cs#L61) and [the Stripe gateway](../../../../JobTrackerApi/Services/StripeBillingGateway.cs#L14) create sessions without a durable purchase-attempt identity. Two requests can create two sessions; that is not proof of two charges, since completion still matters. The webhook verifies the signature, fetches current subscription state and checks ownership/price: ordinary sequential replay is already handled better than an event-order-only design. Identity's concurrency stamp also means some races fail and retry rather than blindly overwrite.
|
||||
|
||||
The remaining cause is split subscription/role updates plus an account model that does not reconcile all relevant subscriptions. An active subscription and a different cancelled subscription for the same account cannot be resolved reliably by treating each event's subscription as the entire account truth. [Deletion](../../../../JobTrackerApi/Services/AccountDeletionService.cs) removes identity/billing IDs without a billing cancellation stage. Deletion admission is **disabled by default**; do not describe an active paid deletion incident as observed.
|
||||
|
||||
**Corrected fix:** purchase-attempt idempotency, one explicit entitlement derivation, atomic local subscription/role state and retryable reconciliation. Start with a guarded synchronous handler and provider retries if that meets the contract. A new event inbox, broker or general outbox is not intrinsically required; durable receipt is required before acknowledging work that will happen asynchronously. Reuse existing deletion stages, preserve billing identifiers until resolution, and initially reject/defer deletion when active or uncertain billing cannot be resolved. This containment can precede full cancellation automation.
|
||||
|
||||
**Acceptance:** existing billing tests cover configured price, rejected product ID, signed sequential lifecycle/replay and bad signature; they do not prove concurrent purchase/deletion correctness. Add crash-between-local-writes, two subscriptions, delayed conflicting events, retry after an unknown checkout result, pending checkout versus deletion, unavailable provider and verified cancellation tests. Follow with an authorized Stripe sandbox run. Queue: P01–P03.
|
||||
|
||||
### F03 — PDF host exhaustion
|
||||
|
||||
**Remediation update, 2026-09-08:** [B03](b03-completion.md) closes shared admission and normal failure/cancellation lifecycle defects. Default one active render per API process, one per document owner, zero queue, validated configuration and safe 503/504 behavior now have permanent before/after and multi-route regression evidence. The original finding below remains historical; F03 is only partially resolved until R04 proves deployment containment and capacity.
|
||||
|
||||
**Confirmed missing admission control. Retain P1 for an untrusted public audience.** The singleton [exporter](../../../../JobTrackerApi/Services/PlaywrightCvPdfExporter.cs#L37) starts one child process per call. Three calls through that actual implementation started three inert child processes while all were held open. A per-process 60-second timeout bounds duration, not total concurrency. The authenticated variant export has no PDF rate policy. Other PDF endpoints share this exporter, so endpoint-only patches leave bypasses.
|
||||
|
||||
Root cause is shared resource admission and lifecycle, not a proven Chromium exploit. Root container execution and sandbox-disabled flags increase containment concerns but do not establish RCE. **Corrected fix:** a small configurable shared concurrency bound, bounded or zero waiting, cancellation/cleanup, owner burst limits and host resource limits. Preserve legitimate downloads and distinguish admission denial from renderer failure. Measure before splitting the renderer into another service; no durable queue is required for request-scoped exports. Per-process admission is sufficient only for the current process topology; account for additional API replicas explicitly.
|
||||
|
||||
**Acceptance:** retain the proposed fake-process test, cover all exporter entry points, cancellation, timeout and partial output cleanup; then a small controlled host test measures RSS/process count with container limits. The three-process probe proves missing serialization, not an exhaustion threshold or safe production capacity. Queue: B03, R04.
|
||||
|
||||
### F04 — forwarded headers and direct ingress
|
||||
|
||||
**Needs runtime verification. Retain conditional P1.** [Compose](../../../../docker-compose.yml#L140) publishes nginx on host port 3000; [nginx](../../../../job-tracker-ui/nginx.conf.template#L64) forwards incoming forwarding headers while the backend trusts the nginx network. This is a real unsafe assumption if that path is externally reachable without the expected sanitizing proxy. A canonical Host header is not caller authentication.
|
||||
|
||||
The original root cause and qualified recommendation are sound. **Do not assert that the deployed firewall is bypassed:** its rules/topology were not inspected. Verify both the direct and normal routes, IPv4/IPv6 and effective backend client IP. Then close the direct route or sanitize at the correct trusted hop. Loopback binding is appropriate only if the upstream proxy is local. Avoid a speculative proxy redesign. The proposed isolated topology test is necessary, followed by operator evidence from the actual host. Queue: B06; it gates exposed release, not local source work.
|
||||
|
||||
**Subsequent B06 status:** [Peer enforcement/configuration changes](b06-completion.md) are implemented and native API regressions pass. F04 is **not resolved**: real nginx/container execution is blocked and actual host/proxy evidence is still required. Historical findings above remain preserved.
|
||||
|
||||
### F05 — spreadsheet formula injection
|
||||
|
||||
**Confirmed. Downgrade P1 → P2.** A harmless title `=1+1` remains a formula in CSV. [Esc](../../../../JobTrackerApi/Controllers/ExportController.cs#L78) performs CSV syntax quoting, which does not change spreadsheet cell interpretation. Opening attacker-influenced imported content in a spreadsheet is a plausible additional user step, but no command execution, credential disclosure or server compromise was demonstrated. The current evidence does not justify a blanket P1.
|
||||
|
||||
The root cause and small fix are correct. Use one text-field policy, preserve JSON/raw portability, distinguish real numeric fields and verify leading whitespace/control characters and delimiters through a CSV parser. A huge generic export framework is unnecessary. The proposed acceptance needs a harmless manual spreadsheet check because parser round-trip alone does not establish spreadsheet interpretation. Fix early because it is cheap and protects a shipped export; it is not an independent reason to prohibit all external use. Queue: B05.
|
||||
|
||||
**Subsequent resolution, 2026-09-09:** [B05 completion](b05-completion.md) preserves this historical finding and closes the reproduced export failure locally. Actual Excel before/after changes `=1+1` from an evaluated formula to apostrophe-prefixed literal text; all 13 text columns share the tested policy. Typed cells and stored/JSON data remain unchanged. UTF-8 BOM plus byte-preserving browser download fixes observed Norwegian decoding. LibreOffice runtime and arbitrary spreadsheet reprocessing are not claimed verified.
|
||||
|
||||
### F10 — aggregate storage and resource growth
|
||||
|
||||
**Mostly confirmed. Retain P1 for storage/admission exposure; split off P2 duplicate-intent and unmeasured scale work.** [Attachment admission](../../../../JobTrackerApi/Controllers/AttachmentsController.cs#L269) sums current rows before staging/writing; concurrent uploads can both pass. It omits generated exports/artifacts/package copies. [Account export](../../../../JobTrackerApi/Services/AccountDataExportService.cs#L42) streams the ZIP file but materializes collections and serializes full JSON byte arrays. It is incorrect to call the entire ZIP an in-memory buffer; peak per-table memory remains a real concern.
|
||||
|
||||
No production disk exhaustion or 100k-row memory threshold was measured. Two job POSTs with the same unrecognized `Idempotency-Key` produced different IDs. That proves no implemented key contract, not an HTTP violation or a need for unique company/title jobs. Repeated applications can be legitimate. Idempotency belongs to a specific creation intent and does not replace quotas.
|
||||
|
||||
**Corrected fix:** hard admission bounds first; reuse the existing [owned-file inventory](../../../../JobTrackerApi/Services/AccountOwnedFileInventory.cs) for accounting/reconciliation. Use a database-safe owner reservation, not a process-local quota lock. Include crash/release handling and generated categories. Stream/batch large exports when measurements require it. Do not silently prune user history to meet a new limit. Separate job creation retry semantics from this resource ticket and from F01.
|
||||
|
||||
**Acceptance:** simultaneous near-limit reservations, file/DB failure cleanup, generated artifacts and multi-instance accounting; synthetic bounded-memory exports; existing over-quota accounts remain readable/exportable. Same-intent/different-payload/retry tests belong to D04. Do not require every query to be redesigned before safe initial bounds ship. Queue: D04–D07; PDF overlap belongs only to B03.
|
||||
|
||||
### F11 — outbound URL boundary
|
||||
|
||||
**Remediation status:** HTTP B01/V02 resolved in the working tree on 2026-09-08; [connection-time regression evidence and limitations](b01-completion.md). Overall F11 remains partially remediated because IMAP B07 is unstarted. The following finding/evidence is preserved from the original validation.
|
||||
|
||||
**Confirmed boundary defect; retain P1. Production reachability is unverified.** In addition to the source-confirmed validation/connection DNS gap, the real [import validator](../../../../JobTrackerApi/Services/JobImport/JobImportService.cs#L199) accepts IPv4-mapped IPv6 representations of denied IPv4 addresses. A fake HTTP handler was invoked for mapped private, link-local and metadata addresses, while ordinary IPv4 representations were blocked. No network socket was opened by that probe. This bypass does not need changing DNS answers.
|
||||
|
||||
The original explanation is incomplete: normalize mapped addresses before classification, and bind address validation to the address actually connected. The registered HTTP handler resolves separately after validation. IMAP also validates then calls `ConnectAsync(host, ...)`; it **already normalizes mapped IPv4** in its guard, so do not attribute the import-specific normalization bug to IMAP. It still needs connection-time enforcement.
|
||||
|
||||
**Corrected fix:** protect the actual socket boundary while retaining hostname/TLS validation, redirect rejection, size limits and cancellation. A proven restrictive egress layer can satisfy the boundary, but none was verified here. Reuse a narrow address policy only where the policies are genuinely the same; do not build a universal network platform or disable TLS checks. The original proposed mixed/mapped/DNS/TLS acceptance matrix is good, but it was a proposed test, not proof that the existing mapped check worked. Queue: B01 HTTP, B07 IMAP.
|
||||
|
||||
### F12 — backup/restore and rollback
|
||||
|
||||
**Mostly confirmed. Retain P1 as a release/recovery gate.** The [daily runner](../../../../JobTrackerApi/Services/DatabaseBackupRunner.cs#L68) uses SQLite `VACUUM INTO`, a consistent database snapshot. The pre-deploy [live-volume tar](../../../../deploy/deploy.sh#L314) is not equivalent; concurrently changing DB/WAL/files have no demonstrated common recovery point. MariaDB's transactional SQL dump does not make separate file archives transactionally consistent. No corrupt backup was reproduced or alleged.
|
||||
|
||||
The failure trap [restores previous images](../../../../deploy/deploy.sh#L638), not the old database schema. This is safe for some additive changes and unsafe for incompatible migrations. Do not disable every image rollback indiscriminately. **Corrected fix:** verified recovery sets, a deliberate file checkpoint/write pause, checksums and an isolated restore drill; attach schema compatibility to rollback decisions. Keep keys and deletion tombstones recoverable with appropriate independent custody. Reuse existing backup machinery.
|
||||
|
||||
**Acceptance:** restore while the source has synthetic writes, validate integrity/ownership/files/decryption/tombstone replay, kill incomplete backups and prove compatibility with the previous binary. The original tests are sufficient in intent but unexecuted for deployment recovery. Run this **before the first new schema migration**, not after concurrency, billing and quota migrations. Queue: R02–R03.
|
||||
|
||||
### F13 — privileged support, auditing and retention
|
||||
|
||||
**Partially confirmed. Downgrade engineering finding P1 → P2.** [AdminAuditController](../../../../JobTrackerApi/Controllers/AdminAuditController.cs#L12) is explicitly Admin-authorized. Cross-owner activity and undo are deliberate admin capabilities; they are not a normal-user tenant escape. Its returned private event fields and absence of a dedicated security event ledger support minimization/audit work. Infrastructure root access and absence of a support/impersonation role are not vulnerabilities by themselves.
|
||||
|
||||
The finding combines three different things: admin product permissions, accountable security mutations, and owner/legal retention decisions. **Corrected fix:** document the existing admin boundary, minimize fields where they are unnecessary, add safe actor/action/target/outcome events for high-impact changes, and use recent authentication where warranted. Do not invent impersonation, a role hierarchy or tamper-proof logging claims. A restricted support role is justified only by an actual support use case.
|
||||
|
||||
**Acceptance:** unauthorized verbs remain denied, intentionally authorized admin verbs remain functional, privilege revocation takes effect, and audit metadata excludes arbitrary content/secrets. Off-host audit custody and policy approval require operator evidence; unit tests cannot certify them. Retention/deletion policy remains a public/paid operational prerequisite without converting every suggested admin improvement into P1. Queue: W04 and P03 policy dependency.
|
||||
|
||||
### F20 — AI worker failure containment
|
||||
|
||||
**Confirmed when enabled. Retain conditional P1.** A real host running the actual [worker](../../../../JobTrackerApi/Services/AiOperationQueue.cs#L254) with concurrency 1 stopped after a single injected database timeout. With concurrency 2, one loop failed but the host and combined task did not stop during the observation window: `Task.WhenAll` waits on the surviving loop. The audit misses this second mode, where capacity silently shrinks rather than immediately stopping the host.
|
||||
|
||||
The cause is failure escaping the loop boundary, not simply the host exception policy. Compose defaults this worker **off**, so a default deployment outage is not established. **Corrected fix:** catch/classify at the loop, bounded retry/backoff, explicit degraded health and lease recovery. Do not globally ignore background-service exceptions or hide a dead worker behind a healthy API.
|
||||
|
||||
**Acceptance:** one- and multi-worker tests, claim/owner/result persistence failures, cancellation, recovery and lease expiry, followed by bounded outage drills. The local probe confirms host/worker behavior, not endpoint availability during a deployed DB outage or eventual recovery. Keep the feature disabled until those acceptance tests pass. Queue: R05.
|
||||
|
||||
### F21 — calendar/timezone
|
||||
|
||||
**Mostly confirmed. Downgrade P1 → P2 for the currently implemented workflow.** Offset-bearing input lost offset information after SQLite reload; the reproduced spring value exported 09:00Z instead of 07:00Z, and the two autumn offsets collapsed. However, the **UTC input control preserved 07:00Z**. This is not evidence that every production UTC event shifts. Server timezone affects DateTime parsing; production timezone/provider behavior was not exercised.
|
||||
|
||||
More fundamentally, the normal job editor and Interview Prep send **date-only follow-up values**. [Calendar conversion](../../../../JobTrackerApi/Controllers/ApplicationCalendarController.cs#L129) turns an unspecified date into a UTC timed event. The independent date-only control became midnightZ, which can display on a different local date. The root cause is a missing date-versus-instant contract, not a universal need for named-zone recurrence.
|
||||
|
||||
**Corrected fix:** preserve the current date-only product semantics and emit all-day follow-up/deadline events in ICS and provider links. Specify UTC normalization for any genuinely timed API field. Do not infer historical timezones or shift stored dates globally. A timed appointment/recurrence feature needs its own product scope and evidence. Current impact is workflow correctness; P1 would need evidence that a promised timed scheduling flow is routinely losing important appointments.
|
||||
|
||||
**Acceptance:** date-only all-day values and exclusive end dates, UTC control, offset cases, both providers and server timezone permutations. The original offset tests are valuable but insufficient without the date-only control. Queue: W01, with no broad timestamp migration assumed.
|
||||
|
||||
## New findings and severity increases
|
||||
|
||||
### V01 — account A's open form can overwrite account B (F08 raised to P1)
|
||||
|
||||
**Remediation status:** B02/F08/V01 resolved in the working tree on 2026-09-08. [Server session precondition, frontend isolation, before/after regression evidence and rollout limits](b02-completion.md). The original finding below is preserved as historical evidence.
|
||||
|
||||
**Confirmed with real UI and HTTP persistence.** Tab A loaded account A's Career Profile and edited its full name. Tab B used the actual sign-out menu and signed into B. Tab A remained mounted; clicking its Save changes button returned **200**. An independent API session read the A draft in **B's stored profile**, while A's stored profile remained unchanged.
|
||||
|
||||
[auth.ts](../../../../job-tracker-ui/src/auth.ts) broadcasts same-window events; [App](../../../../job-tracker-ui/src/App.tsx#L248) does not synchronize the authentication boundary across tabs. [api.ts](../../../../job-tracker-ui/src/api.ts) uses shared current cookies/CSRF state. [CareerProfilePage](../../../../job-tracker-ui/src/views/CareerProfilePage.tsx#L855) sends its retained local form to an endpoint whose owner is implicit in those cookies. Backend ownership checks correctly resolve B; the client submits A's content as B. This is not a direct-object-reference authorization bypass, and it is more serious than private text staying visible after logout.
|
||||
|
||||
**Required scope:** one account/session generation boundary; promptly clear/unmount private owner state on logout/account change, cancel or ignore late responses, and refuse dispatch from a form loaded under a different identity. Include a server-checked expected-owner/session precondition where needed to close the cookie-switch race; it can only reject a mismatch, never grant authority. Reading the latest localStorage identity at send time defeats the purpose: bind to the identity that loaded the form. Revision numbers alone do not solve this because A and B can have equal revisions. Tests must repeat this exact two-tab save and assert both accounts' stored profiles, not just navigation. Queue: B02.
|
||||
|
||||
### V02 — mapped-address import bypass (F11 strengthened)
|
||||
|
||||
The new address-normalization proof above removes DNS timing as a prerequisite for passing the application guard. Track it under F11/B01, not as a second duplicate SSRF implementation project. F20's multi-worker failure mode likewise extends its existing ticket. No additional independent P0/P1 is claimed merely because an area lacks complete test coverage.
|
||||
|
||||
## Other highlighted findings and negative evidence
|
||||
|
||||
| Area | Independent conclusion |
|
||||
|---|---|
|
||||
| F06 account/IP rate limiting | **Confirmed P2; resolved locally by [B04](b04-completion.md), 2026-09-08.** Historical behavior: A's two exports made B's first return 429 because rate limiting preceded authentication. Authentication and endpoint authorization now resolve the final trusted resource principal before account partitioning; permanent real HTTP tests prove A/B independence, shared same-account sessions, invalid/forged credential handling and preserved IP policies. Deployment proxy reachability remains B06. |
|
||||
| F09 draft recovery | **Partially confirmed P2.** The original user accepted an unload warning and refreshed while offline. Lack of durable recovery is real, but this is not a violated guaranteed-save contract. Preserve current dirty-state warnings; do not introduce persistent private browser drafts without an ownership/retention decision. W05 follows identity and revision work. |
|
||||
| F14 accessibility | **Confirmed P2, with a structural release acceptance gap.** Shared contrast defects and a responsive preview scrolling defect reproduce; see the focused review below. Axe's `serious` label is not automatically product severity P1. |
|
||||
| F15 export/cache/CSP | Owner checks and recent-auth account export are present. Missing no-store headers are hardening evidence, not a demonstrated cross-user CDN cache disclosure. Review sensitive responses and preview-compatible CSP without conflating it with V01. W03. |
|
||||
| F17 TypeScript | **Confirmed P2 CI gap**, test-only diagnostic; exact explanation below. R01. |
|
||||
| F19 provider fallback | Source confirms silent SQLite fallback for a missing connection or unknown provider. Keep P2 with a release-startup gate: production misconfiguration should fail closed. No proof the deployed instance points at the wrong database. R01. |
|
||||
| Tenant/document authorization | Fresh owner-boundary checks denied B's access to A's variant/preview/PDF. Version listing returns **200 with []**, which reveals no other-owner history and is not an IDOR. Acceptance should require no data/no effects rather than insist every safe empty listing be 404. Query filters and explicit owner checks remain important positive evidence. |
|
||||
| Auth, admin, entitlement | Fresh authorization/session/role/identity tests passed. The Pro handler re-reads user roles/AI preference, rather than relying only on stale JWT entitlement claims. No newly demonstrated ordinary-user admin access or session-revocation bypass. Admin content access remains intentional and needs policy/minimization, not an automatic security rewrite. |
|
||||
| AI and uploads | Privacy/ownership tests passed; AI output remains untrusted and must stay inside existing structured validation and review boundaries. Existing parser/file staging/type/size/owner checks constrain the obvious paths. No new cross-owner document download, executable upload or model-driven privileged action was demonstrated. Resource admission and URL boundaries remain the evidenced problems. |
|
||||
| Migrations, secrets and deployment | Fresh migration-chain tests passed; no new destructive migration/data-drop defect was reproduced. They do not prove rolling back old binaries or restoring files/keys. The tracked-secret scanner and lock files are useful but do not certify Git history, deployed secrets or current dependency advisories. External/host verification remains open; no secrets or live environments were inspected. |
|
||||
|
||||
## TypeScript and production release gates
|
||||
|
||||
Exact fresh error: `src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.`
|
||||
|
||||
The [test](../../../../job-tracker-ui/src/interview-prep.test.tsx#L120) reads `.content` from `mockedApi.post.mock.calls[0][1]`; the generic Axios request body is `unknown` at that boundary. The test's earlier runtime matcher does not narrow that later expression for TypeScript. All 13 tests in the suite passed under Babel/Jest, which transpiles TypeScript without performing this static check. The installed Next `runTypeCheck.js` explicitly filters `.test`, `.spec`, `__tests__` and `__mocks__` diagnostics; Next build success is therefore consistent with standalone tsc failure.
|
||||
|
||||
The [CI workflow](../../../../.gitea/workflows/ci-deploy.yml) runs frontend tests/lint/build/browser checks without an explicit standalone `tsc --noEmit` gate. This is a real configuration gap. The diagnostic is in a test and does not demonstrate a broken emitted production component. It would not alone justify an emergency production rollback or P1 incident, but the next planned release should have a clean explicit type gate. Correct the test's typed payload boundary narrowly; do not disable strictness, exclude all tests or use an unrelated framework migration. No fix was made in this pass.
|
||||
|
||||
## Accessibility conclusions
|
||||
|
||||
The [normal browser accessibility suite](../../../../job-tracker-ui/e2e/accessibility.spec.ts) checks `/`, `/login`, `/dashboard`, `/jobs`, `/kanban`, `/career`, `/settings` and `/admin/system`. It forces English and waits primarily for `body`/`main` visibility. It does not ensure loaded content or open interactive states, and does not systematically cover dark mode or Bokmål.
|
||||
|
||||
Missing important coverage includes `/career/builder`, `/career/builder/:id`, actual profile/notes states, job detail/edit and interview interactions, public CV, registration/reset/second-factor flows, admin users/audit, menus/dialogs, errors/conflicts and mobile preview/edit transitions. A green suite is valid evidence for its sampled states, not accessibility certification for the product.
|
||||
|
||||
The fresh loaded-builder probe found light-mode success chips at **3.29:1** (`#16a34a` on white), including Saved and ATS chips. Dark primary text appears at **3.50–3.66:1** on the two dark surfaces. [theme.ts](../../../../job-tracker-ui/src/theme.ts) reuses the accent as primary text across themes; outlined buttons/tabs/chips inherit these values. This proves a shared token/component relationship. Fix semantic foreground/background pairs and relevant MUI state overrides centrally, then verify existing surfaces; do not patch each page's color or add a new design-system framework.
|
||||
|
||||
The preview's scroll region also fails `scrollable-region-focusable` in the mobile-to-desktop transition. It is not present in every initial viewport/theme sample. The offending overflow region contains the rendered page iframe and has no explicit focusable region contract; [CvBuilderEditor:480](../../../../job-tracker-ui/src/views/CvBuilderEditor.tsx#L480) is the corresponding preview container. Repair named region/focus/keyboard scrolling and verify actual behavior, rather than adding tabindex to every Box. The editor controls already include keyboard-capable template cards; do not label every custom card inaccessible without testing it.
|
||||
|
||||
Before public release, manually complete the primary tracking/profile/builder flow using keyboard only and NVDA or Narrator, including preview scroll, tab order, focus return, error/conflict announcements and reauthentication. Test 200%/400% zoom and 320 CSS-pixel reflow, text spacing, Windows forced colors, reduced motion, touch/keyboard alternatives to drag-and-drop, and real mobile keyboard overlap. No screen-reader/manual accessibility certification was performed here. Structural keyboard and focus failures take precedence over polish. Queue: A01–A03.
|
||||
|
||||
## Shared root causes, without a generic overhaul
|
||||
|
||||
1. **Record preconditions:** CV, job and Career Profile updates lack a client/server revision contract. Share the convention and error semantics; keep aggregate-specific transactions and conflict UI. Do not confuse this with schema version numbers.
|
||||
2. **Identity lifecycle:** cross-tab cookies, retained forms and late responses have no common account-generation boundary. Fix this before adding persistent drafts or rebuilding editors. It is not fixed by per-record revisions.
|
||||
3. **Admission before side effects:** PDF launch, file quotas and owner rate policies do not consistently enforce limits at the boundary that consumes the resource. Reuse existing services, with a shared exporter bound and database-safe storage reservations. An idempotency key limits repeated intent; it is not a resource quota.
|
||||
4. **Validated target versus connected target:** import and IMAP share a time-of-check/time-of-use boundary, while only import misses mapped-address normalization. Share only the proven common policy/connection principle.
|
||||
5. **Split local/provider state:** billing/deletion and profile legacy projection need explicit commit/recovery boundaries. They do not all require a new event bus. The profile projection can usually join its transaction; external billing cannot.
|
||||
6. **Shared visual semantics:** theme tokens create cross-page contrast failures; preview focus is a local structural component issue. Fix these before changing page layouts.
|
||||
7. **Proof at the wrong layer:** helper tests do not prove middleware order, a green shell does not prove loaded accessibility, a tar file does not prove restore, and a green build does not prove the standalone TypeScript program. Add the missing boundary tests rather than expanding every suite indiscriminately.
|
||||
|
||||
## Confirmed launch blockers and release levels
|
||||
|
||||
For a **public free beta**, close V01/F08 identity corruption, F01 lost writes, F11 outbound target enforcement, F03 PDF admission and the concrete F10 storage admission gaps. Establish a verified recovery set/compatible rollback and confirm actual ingress/configuration safety. Resolve primary-flow structural accessibility failures and retain manual evidence. These are correctness/security/recovery gates; a broad UX overhaul is not required.
|
||||
|
||||
For a **limited paid beta**, additionally prove F02 purchase/entitlement/deletion handling in sandbox, a workable retention/deletion/support policy, verified key/data recovery and functioning enabled premium features. F20 blocks enabling its worker until containment/recovery tests pass. Keeping paid deletion disabled is not a complete long-term paid-account policy.
|
||||
|
||||
For a **public paid launch**, complete representative end-to-end paid journeys, accessibility/manual verification, support and recovery drills, measured capacity at the intended exposure, and production ingress/secrets/release evidence. Exact public host, Stripe, mail, off-site backup, advisory and operator controls remain unverified. Their absence from this checkout is a missing release proof, not proof that deployed controls do not exist.
|
||||
|
||||
The rejected stronger verdict is public paid launch readiness today. Conversely, the evidence does not support demanding a new broker, renderer microservice, role platform, PWA, total page redesign or 100k-record optimization as prerequisites to a controlled beta.
|
||||
|
||||
## Decisions Required From Connor
|
||||
|
||||
These decisions can be supplied when the dependent ticket starts; none is needed to read or approve this validation.
|
||||
|
||||
| Decision | Why the repository cannot decide it | Proposed default / dependency |
|
||||
|---|---|---|
|
||||
| Paid deletion timing | Whether deletion cancels paid service immediately or keeps access until period end changes provider calls, messaging and retention timing. | Immediate cancellation only on explicit delete confirmation; never erase billing identity before verified resolution. Decide before P03. |
|
||||
| Recovery objectives and custody | Acceptable data-loss window, recovery time and who can recover encryption keys require an operator/business commitment. | Use 24-hour RPO and 4-hour RTO as proposed drill targets, not measured promises; approve or replace before R03. |
|
||||
| Private device-local drafts | Persisting CV/profile content on shared devices is a privacy/product choice. | Keep memory-only drafts plus dirty-state warnings until an explicit opt-in, owner scope and expiry policy are approved. W05. |
|
||||
| Version retention/over-quota promises | Pruning historical CV/profile versions changes users' restore expectations and paid-plan promises. | Preserve existing history; reject further growth with a recoverable explanation before silently pruning. Decide before D07 expands retention. |
|
||||
| Operator content access and retention | Which support actions actually need private document/event content and how long account/export/deletion records remain are policy decisions. | Retain current admin-only boundary, minimize unnecessary content, add no impersonation role. W04/P03. |
|
||||
|
||||
Do not ask whether follow-ups should retain their existing date-only behavior, whether a concurrency token is needed, or whether to add a type gate: the repository/evidence answer those engineering questions. New timed scheduling is a separate future product request.
|
||||
|
||||
## Recommended implementation order and first handoff
|
||||
|
||||
Use the exact ticket order and permitted parallel branches in [implementation-queue.md](implementation-queue.md). The essential order is: **contain security/identity/resource defects → establish release and recovery proof → introduce data-integrity migrations → correct paid lifecycle → accessibility foundations → workflow corrections → broader UX and measured operations**. Independent theme/type tests may run earlier; they do not authorize skipping a dependency.
|
||||
|
||||
The original first handoff was **B01 only: HTTP import target enforcement and its regression tests**. B01–B05 are now complete locally; see the linked completion records above. **B06 verify and close ingress bypass path** was subsequently authorized; its implementation is present, with runtime/host gates still pending. Do not start B07. Completing B01 does not close F11's separately scoped IMAP/deployment work; B03 does not close F03's R04 containment/capacity work; B04 does not close B06's deployment ingress proof.
|
||||
|
||||
## Stop conditions
|
||||
|
||||
- Do not close a finding from source inspection alone when its acceptance requires a socket, two contexts, browser tabs, provider sandbox or restored database.
|
||||
- Do not start schema-changing tickets until R02/R03 produce a restorable set and an explicit old/new binary compatibility decision. Never delete duplicate CV snapshots merely to make an index migration pass.
|
||||
- Do not enable open registration/exposed costly features while their boundary/admission findings remain open. Keep AI workers and paid lifecycle operations gated until their own tests pass.
|
||||
- Do not begin page redesign or persistent draft work while shared identity, revision or accessibility primitives it depends on are unresolved.
|
||||
- Stop a rollout on owner-state contamination, silent conflict overwrite, unexpected entitlement divergence, unresolved billing before identity deletion, failed restore/integrity, unbounded resource admission, or primary-flow keyboard regression. Preserve evidence and existing data; use the rehearsed compatible rollback or forward repair.
|
||||
- The validation itself authorized no implementation. Subsequent explicit instructions authorized B01–B05, now complete locally. B06 was subsequently authorized and implemented with verification gates still pending. Stop before B07 or any other ticket without a further instruction.
|
||||
@@ -0,0 +1,84 @@
|
||||
# B01 — HTTP import destination enforcement
|
||||
|
||||
**Completed locally on 2026-09-08.** B01's acceptance criteria pass. The HTTP portion of F11 and the mapped-address bypass V02 are resolved in this working tree. **F11 remains open for B07 (IMAP) and deployment egress assumptions.** B02/B07 were not started. No deployment, migration, dependency upgrade or unrelated application change was made.
|
||||
|
||||
Baseline application commit: `7d641edc7bca866ccf06012897ba1d107c07e59f`. The original [boundary observations](evidence/boundary-results.json), [probe source](BoundaryProbes.cs) and audit conclusions remain as historical evidence. This document records the subsequent authorized implementation.
|
||||
|
||||
## Files changed
|
||||
|
||||
- Application: `JobTrackerApi/Program.cs`, `JobTrackerApi/Services/JobImport/JobImportService.cs`, new `JobTrackerApi/Services/JobImport/JobImportHttpTransport.cs`.
|
||||
- Tests: `JobTrackerApi.Tests/JobImportServiceTests.cs`, new `JobTrackerApi.Tests/JobImportHttpTransportTests.cs`.
|
||||
- Status/evidence: `docs/todo/work.md`; this validation directory's `README.md`, `implementation-queue.md`, `evidence.md`, new `b01-completion.md` and new `evidence/b01-test-results.json`.
|
||||
|
||||
Original audit documents and TODO changes were already present before B01. They were preserved; the list above identifies this ticket's changes only.
|
||||
|
||||
## Root cause and change
|
||||
|
||||
The import URL validator treated IPv4-mapped IPv6 as ordinary IPv6. Thus `10.1.2.3` was blocked while `::ffff:10.1.2.3` passed. Separately, hostname preflight and the original HTTP handler performed different resolutions; passing preflight did not constrain the socket destination.
|
||||
|
||||
[JobImportHttpTransport](../../../../JobTrackerApi/Services/JobImport/JobImportHttpTransport.cs) contains the shared address classification and a narrow `SocketsHttpHandler.ConnectCallback`. Both [preflight](../../../../JobTrackerApi/Services/JobImport/JobImportService.cs) and connection setup use that classification. The callback resolves once for that connection, rejects empty/mixed/denied answers before dialing, normalizes mapped IPv4, and passes an **IPEndPoint**, never a hostname, to `Socket.ConnectAsync`. Public-address fallback uses only the already checked answer. A new connection repeats validation; an existing pooled TCP connection remains bound to its approved peer.
|
||||
|
||||
[Program.cs](../../../../JobTrackerApi/Program.cs) installs this handler only for the existing `jobimport` client. It returns a plain TCP stream; .NET still performs HTTPS, SNI and certificate/hostname verification using the original request URI. There is no permissive certificate callback, hostname rewrite or custom HTTP implementation.
|
||||
|
||||
Redirects remain disabled and all 3xx responses remain import failures. Automatic decompression, request headers, parser behavior, the streaming 4,000,000-byte limit and cancellation remain intact. The original client's 100-second timeout is unchanged.
|
||||
|
||||
The address policy retains the existing loopback, private, link-local, carrier-grade NAT, benchmark, IPv6 site-local/unique-local/multicast/Teredo exclusions. It also rejects IPv4 multicast/reserved high addresses and scoped IPv6. IPv4-compatible IPv6, well-known NAT64 (`64:ff9b::/96`, `64:ff9b:1::/48`) and 6to4 are rejected explicitly so translation forms do not hide a prohibited IPv4 destination. Ordinary public IPv4, native public IPv6 and public mapped IPv4 remain supported. This is a destination policy, not a complete global IP allocation registry.
|
||||
|
||||
**Proxy behavior:** `UseProxy = false` is deliberate for this client. Otherwise a proxy can resolve/connect the origin outside this callback. No other HttpClient or deployment configuration changed. Imports now require direct outbound connectivity; a proxy-only environment must not restore automatic proxy use without an equivalent enforced origin policy. Such an environment was not evidenced in this repository and was not contacted.
|
||||
|
||||
## Permanent regression evidence
|
||||
|
||||
[JobImportServiceTests](../../../../JobTrackerApi.Tests/JobImportServiceTests.cs) adds 12 literal/encoded-address cases. Running these before changing application code produced **4 failures / 8 passes**: mapped private, link-local, metadata and hexadecimal-mapped forms reached the fake HTTP send. The same tests now all pass.
|
||||
|
||||
[JobImportHttpTransportTests](../../../../JobTrackerApi.Tests/JobImportHttpTransportTests.cs) adds 76 cases at the actual .NET HTTP handler/socket boundary:
|
||||
|
||||
| Requirement | Evidence |
|
||||
|---|---|
|
||||
| Private/local IPv4 and IPv6 | 38 DNS-answer cases cover loopback, RFC1918 boundaries, link-local, ULA/site-local, unspecified, CGNAT, benchmark, multicast/reserved and translation/scoped forms; no dial attempts. |
|
||||
| Equivalent literal forms | Handler tests reject decimal, hexadecimal, octal, mapped and native IPv6 literals without DNS or dialing; service tests also cover shortened IPv4. |
|
||||
| Real default dial path | A denied DNS answer cannot reach an inert loopback listener using the production handler without an injected dialer. |
|
||||
| Resolution failures/mixed answers | Empty, public+private and failed DNS answers fail closed; no fallback to the OS hostname connector. |
|
||||
| Rebinding window | Preflight returns public, connection resolution returns private/mapped/ULA: no dial. A third hypothetical private answer is never queried after an approved connection-time answer. |
|
||||
| Public imports | Real HTTP exchange and JobPosting parsing for hostname/literal/public IPv4/native IPv6/mapped IPv4 decisions; original Host/path retained. Public-address socket failure falls back only to another checked address. |
|
||||
| Connection lifecycle | A second connection on the same handler revalidates DNS and rejects its changed private answer. |
|
||||
| Proxy escape | A configured proxy trap receives no connection; the approved direct endpoint is used. |
|
||||
| Redirects | 301/302/303/307/308 pointing to mapped metadata return fetch failure, with exactly one approved dial and no target resolution. |
|
||||
| TLS identity | Local TLS server observes original-host SNI; trusted matching certificate succeeds; trusted wrong hostname and untrusted matching certificate fail before HTTP data. No certificate-validation bypass. |
|
||||
| Timeout/cancellation | Short test-only client timeout cancels stalled connection DNS, dial and response headers. Caller cancellation interrupts a stalled streamed response body. Production 100-second timeout is asserted unchanged. |
|
||||
| Size/decompression | Oversized declared content and gzip expansion over the byte cap fail. The existing chunked-stream test still proves reading stops at the cap plus one sentinel byte. |
|
||||
|
||||
All new tests use fake DNS and controlled loopback TCP/TLS fixtures. An internal optional dial delegate records the approved IP then routes only the fixture transport locally; production uses the concrete IP socket method. Tests exercise real .NET HTTP/TLS over that stream and do not claim to have contacted real public/private services. The test CA is trusted only by its test handler, not installed into OS trust. No public DNS, website, metadata endpoint, Stripe or other external service was contacted.
|
||||
|
||||
## Commands and exact results
|
||||
|
||||
Run from repository root in PowerShell, with existing restored packages:
|
||||
|
||||
```powershell
|
||||
# Before implementation: 12 cases, 4 failed / 8 passed / 0 skipped (expected reproduction).
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~Preview_rejects_equivalent_private_literals_before_http_send' --logger 'trx;LogFileName=b01-before.trx' --results-directory .artifacts/b01 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
|
||||
# Final focused run: 98 passed / 0 failed / 0 skipped (92 import + 6 existing IMAP literal guards).
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~JobImport|FullyQualifiedName~ImapServiceSsrfGuardTests.ConnectAsync_rejects_internal_and_metadata_hosts' --logger 'trx;LogFileName=b01-focused.trx' --results-directory .artifacts/b01 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
|
||||
# Broader backend: 840 passed / 0 failed / 0 skipped; one existing DNS-dependent case excluded.
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --no-build --filter 'FullyQualifiedName!~ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail' --logger 'trx;LogFileName=b01-backend.trx' --results-directory .artifacts/b01 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
|
||||
# API and test project compilation/analyzers: passed, 0 warnings / 0 errors.
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore -warnaserror
|
||||
|
||||
git diff --check
|
||||
```
|
||||
|
||||
The excluded existing IMAP test performs real DNS resolution, which this task did not authorize. It is an explicit filter exclusion, not a reported test skip or pass. The broader run includes the focused tests, so their counts must not be added. No frontend files changed; frontend TypeScript/build checks were not rerun. Results/hashes are retained in [b01-test-results.json](evidence/b01-test-results.json); full TRX files remain local under `.artifacts/b01/`.
|
||||
|
||||
## Security self-review and residual limitations
|
||||
|
||||
- Equivalent mapped representations go through the same IPv4 rule; native IPv6 private/local and known translation forms fail closed. Numeric parsing is delegated to `Uri`/`IPAddress`, with encoded-form regressions.
|
||||
- The exact validated IP is passed to the socket. There is no DNS lookup between classification and that connect and no unchecked proxy/redirect path. DNS/socket failures do not switch to a permissive hostname transport. Cancellation or connection failure disposes the socket.
|
||||
- TLS remains the framework's original-host validation. The test-only custom CA policy changes trust for its fixture without disabling hostname checks; production does not set it.
|
||||
- OS routing, transparent interception, operator-defined NAT64 prefixes and services internally routed under otherwise public IPs are outside an application IP classifier's visibility. Appropriate egress policy remains defense in depth; this change does not certify production routing or every special-use address allocation.
|
||||
- As before, `HttpClient.Timeout` with `ResponseHeadersRead` covers sending/receiving headers, not the entire body. The body has its byte cap and caller cancellation, but **no independent total-body deadline**. Preflight DNS likewise uses the supplied request cancellation token. No new whole-import duration guarantee is claimed; resource admission/deadline policy can be a separate bounded follow-up.
|
||||
- Public alternatives are attempted sequentially after socket failures, within the existing client timeout. No parallel connection racing or new network subsystem was added. Linux/container and real external endpoint connectivity were not exercised; local tests ran on Windows/.NET 9.
|
||||
- Reverting this handler reopens B01. Do not roll it back on an exposed import route without disabling that route or equivalent verified egress enforcement. No database rollback is involved.
|
||||
|
||||
**Acceptance verdict: B01 complete.** The reproduced bypass now fails before send, connection-time tests close the validated DNS race, legitimate imports and existing protections pass regression checks, and the wider backend/build gates pass. F11 is only partially remediated because B07/deployment evidence remain outside B01. Stop here; do not begin B02 automatically.
|
||||
@@ -0,0 +1,110 @@
|
||||
# B02 — Account isolation and retained-request protection
|
||||
|
||||
Implementation date: **2026-09-08**. **B02 / V01 / F08 resolved locally**, with the original failure reproduced before application edits and rejected after the fix. All B02 acceptance criteria pass; the pre-existing standalone TypeScript gate remains open under R01a. No schema migration, deployment, revision protocol, idempotency framework, billing change or UI redesign. B01 is preserved; B03 has not been started.
|
||||
|
||||
## Original behavior and fresh reproduction
|
||||
|
||||
The original [V01/F08 evidence](README.md#v01--account-as-open-form-can-overwrite-account-b-f08-raised-to-p1) and [browser probe](probes.mjs) remain unchanged. A new permanent API regression was run before editing application code. It signed in as A, retained A's profile/request, signed out, signed in as B, and submitted A's content with B's current cookies and CSRF value.
|
||||
|
||||
Observed before the fix: **HTTP 200**, A remained `Private A`, and B became `A draft must never enter B`. The expected safe assertion failed. This confirms the original browser result independently through the real API, authentication/session middleware, controller, Identity and isolated SQLite database. [Retained before/after facts](evidence/b02-observations.json).
|
||||
|
||||
## Ownership semantics and root cause
|
||||
|
||||
| Operation | Actual contract and authority |
|
||||
|---|---|
|
||||
| `GET/PUT /career/profile` | An account-owned singleton; PUT creates or replaces the current user's profile. `UserManager.GetUserAsync(User).Id` supplies the owner. `CareerProfileSaveRequest` contains only `Profile` and `CvText`, with no authoritative account/resource ID. The old request carried no session precondition. |
|
||||
| Profile children | Structured job/education/project `Id` values are profile-local item keys, not database row IDs or foreign keys. `CareerProfileMapper.AddChildren` assigns database `CareerProfileId` and `OwnerUserId` server-side. Incoming nested ownership properties are not DTO fields. |
|
||||
| Profile versions/restore | Version numbers are scoped to the principal's profile. Service queries select the owner profile before selecting its version. There is no standalone profile delete/attach endpoint. |
|
||||
| CV variant read/update/delete/duplicate | Explicit variant IDs. `CvVariantService.GetAsync` selects `Id + OwnerUserId`; mutations use that lookup. B receives 404 for A's private variant; safe history listing returns an empty array. |
|
||||
| CV variant create/job reference | Creates under the principal. Both controller and service check `job.Id + authenticated owner`. Unavailable and foreign job references use the existing generic 400 response; this convention is preserved. |
|
||||
| Nearby profile extraction and identity endpoints | Extraction run lookups select run ID plus principal owner. Identity/profile DTOs do not accept role, plan or owner reassignment. Anonymous public CV reads are an intentional publication path, separate from private variant access. |
|
||||
|
||||
The server was already preventing B from loading/updating A's explicit object. The demonstrated failure was **A's free-text state being submitted as a valid B singleton write**. Principal-only ownership checks cannot identify that content's earlier browser context. Reading the latest CSRF cookie in `api.ts` allowed the stale page to pass CSRF after the shared cookie switched to B. Same-window auth events did not invalidate the other tab, and late promises/caches remained usable.
|
||||
|
||||
## Exact security boundary
|
||||
|
||||
[SessionContextMiddleware](../../../../JobTrackerApi/Services/SessionContextMiddleware.cs) runs after authentication and authorization. Authenticated unsafe requests must provide exactly one nonempty `X-Session-Context` matching the **validated principal's existing `sid` claim**. Missing context returns **428 / `session_context_required`**; stale/forged/multiple context returns **409 / `session_context_mismatch`**, before the endpoint runs. Error bodies disclose no target existence, user ID or expected session value. The rule applies to cookie and bearer requests, including admin requests.
|
||||
|
||||
The header is a **rejection-only precondition**, never an owner selector or authentication credential. Existing JWT validation and `LocalSessionValidator` still verify session ID, user ID, expiry, revocation and account state against server records. The caller's actual principal still determines ownership, and object/association authorization still applies even with a matching context. The session ID was already exposed by the authenticated sessions list; possession alone grants no access.
|
||||
|
||||
`/auth/me` now supplies `sessionContext` and is explicitly non-cacheable. Authenticated reads carrying context are checked too, preventing a stale browser from fetching B's private data into an A page. Unbound reads remain available for bootstrap/direct document links, with their existing owner authorization. Explicit anonymous login/challenge/logout/webhook/public endpoints retain their existing contracts and independent validation.
|
||||
|
||||
No new token database, crypto scheme or ownership-bearing DTO was introduced. JSON `userId`, `ownerUserId`, `accountId`, role and plan over-posting cannot select another owner or change privileges. A fresh B request with B's context still cannot read/update/delete/duplicate A's variant or reference A's job.
|
||||
|
||||
## Frontend behavior and privacy
|
||||
|
||||
[sessionBoundary.ts](../../../../job-tracker-ui/src/sessionBoundary.ts) binds a JavaScript page to one server session after `/auth/me`. It **never adopts a second session**, including a new login for the same account. Requests use that in-memory context, not a freshly read localStorage identity. Storage signals, cookie changes, focus/visibility, a one-second cookie fallback and private-page BFCache restoration invalidate the page.
|
||||
|
||||
[api.ts](../../../../job-tracker-ui/src/api.ts) captures request context synchronously, aborts in-flight work on invalidation, rejects stale contexts and discards late data/errors. It does not retry a rejected mutation or fetch a new identity to make an old mutation pass. A queued callback in the old page remains invalid even if it runs after account switching.
|
||||
|
||||
[App.tsx](../../../../job-tracker-ui/src/App.tsx) unmounts the entire application/provider tree before navigating to a fresh login document. This drops forms, confirmations, prompts, toasts, cached companies, pending callbacks and route state together. Cancelled auth responses cannot clear a newer bootstrap result. The login page explains in English/Bokmål that private data and unsaved edits were cleared. Anonymous public history remains usable.
|
||||
|
||||
Repository review found no React Query, Redux or Zustand private-data store. The principal module cache is `useCompanies`; a fresh page discards it. Existing saved views and profile-headline storage are already account-keyed; B's views do not read A's keys. Theme/layout/language preferences are not private documents. Existing provider-managed Google/MSAL sign-in state is not treated as an application data cache. No persistent-draft subsystem was added, and A's unsaved draft is intentionally discarded across account boundaries.
|
||||
|
||||
Server-created background operations already store the authenticated owner and execute under that explicit owner; a browser switch does not retarget accepted work. B02 prevents newly dispatched stale-session work, not completion of previously accepted A work under A.
|
||||
|
||||
## Permanent regression coverage
|
||||
|
||||
- [13 backend middleware cases](../../../../JobTrackerApi.Tests/SessionContextMiddlewareTests.cs): mutation verbs, missing/stale/forged/multiple headers, safe bootstrap versus bound reads, matching owner, bearer-equivalent requests without cookies, missing `sid`, admin and anonymous-auth flow behavior.
|
||||
- [15 frontend boundary cases](../../../../job-tracker-ui/src/session-boundary.test.ts): real Axios interceptors with inert adapters, immutable context, fresh-cookie/stale-callback refusal, logout/account/same-account session signals, cancellation, delayed private reads, stale 401/bootstrap responses, no automatic mutation retry, BFCache and cookie fallback.
|
||||
- [4 real API/browser integration scenarios](../../../../job-tracker-ui/e2e/account-isolation.spec.ts): original A→logout→B replay, missing/forged/over-posted context, explicit CV read/update/delete/duplicate/job-association denial, same-account controls, private singleton reads, two-tab privacy clearing and Back navigation followed by a legitimate B save.
|
||||
- Existing authorization/session/profile/CV tests continue to run. The existing smoke helper now obtains the required context for its direct API mutations; no old-client bypass was added for tests.
|
||||
|
||||
After the fix, the original retained request returns **409**, with **both stored profiles unchanged**. Context-free legacy writes return 428. B's valid context does not authorize A's resources. Same-account profile and CV operations still pass.
|
||||
|
||||
## Verification results and reproduction commands
|
||||
|
||||
The full backend run passed **853 tests**; one pre-existing IMAP test that performs real DNS was explicitly excluded. The focused backend run passed **91**. The full frontend run passed **294 tests across 66 suites**. The full browser run passed **17**, and the final B02 rerun passed **4**, with no failures/skips/flaky outcomes. Backend Release build with warnings as errors, frontend production build, targeted ESLint and `git diff --check` passed. Standalone TypeScript exited **2** with the single existing diagnostic below. The [test manifest](evidence/b02-test-results.json) retains exact results and hashes of local raw outputs. Results overlap and must not be added together.
|
||||
|
||||
Root, PowerShell:
|
||||
|
||||
```powershell
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~SessionContextMiddlewareTests|FullyQualifiedName~CareerProfile|FullyQualifiedName~CvBuilder|FullyQualifiedName~AuthSessionRevocation|FullyQualifiedName~ApiAuthorizationPosture|FullyQualifiedName~LocalAuthIdentity' --logger 'trx;LogFileName=focused.trx' --results-directory .artifacts/b02 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --no-build --filter 'FullyQualifiedName!~ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail' --logger 'trx;LogFileName=backend.trx' --results-directory .artifacts/b02 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore -warnaserror
|
||||
git diff --check
|
||||
|
||||
New-Item -ItemType Directory -Force .artifacts/b02 | Out-Null
|
||||
Copy-Item docs/audits/2026-09-07/validation/evidence/b02-browser-config.ts.txt .artifacts/b02/playwright.offline.config.ts
|
||||
```
|
||||
|
||||
UI directory, PowerShell:
|
||||
|
||||
```powershell
|
||||
node node_modules/jest/bin/jest.js src/session-boundary.test.ts src/account-scoped-state.test.tsx src/login-page.test.tsx src/career-workspace-page.test.tsx src/profile-page.test.tsx --runInBand --watchAll=false --json --outputFile=../.artifacts/b02/frontend-focused.json
|
||||
node node_modules/jest/bin/jest.js --runInBand --watchAll=false --json --outputFile=../.artifacts/b02/frontend.json
|
||||
node node_modules/typescript/bin/tsc --noEmit --pretty false
|
||||
node node_modules/eslint/bin/eslint.js src/api.ts src/sessionBoundary.ts src/session-boundary.test.ts src/App.tsx src/views/LoginPage.tsx src/i18n/translations.ts e2e/account-isolation.spec.ts e2e/smoke.spec.ts --max-warnings=0
|
||||
$env:NEXT_TELEMETRY_DISABLED = '1'
|
||||
$env:NEXT_PUBLIC_API_BASE_URL = '/api'
|
||||
node --max-old-space-size=4096 node_modules/next/dist/bin/next build
|
||||
$env:DOTNET_HOST_PATH = 'C:/Users/Connor/.dotnet/dotnet.exe'
|
||||
node node_modules/@playwright/test/cli.js test --config=../.artifacts/b02/playwright.offline.config.ts --reporter=json
|
||||
node node_modules/@playwright/test/cli.js test e2e/account-isolation.spec.ts --config=../.artifacts/b02/playwright.offline.config.ts --reporter=json
|
||||
```
|
||||
|
||||
All test data is synthetic and the browser/API fixture uses fresh temporary data roots. External browser destinations were blocked and provider keys disabled for the full browser run. No production, real external provider or migration against existing data was used. Initial browser fixture corrections were an exact-label mismatch (`Username or email`) and Playwright's fixture callback name triggering the React `use` lint rule; these were test errors, not application regressions.
|
||||
|
||||
**Existing unrelated type failure:** standalone tsc still reports `src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.` This is the same previously validated test-only error; B02 introduces no new diagnostic. Jest and Next build do not replace that standalone gate. R01a remains open; no typing workaround or unrelated fix was included.
|
||||
|
||||
## Self-review, limits and rollout
|
||||
|
||||
The server rejects stale context even when B supplies a fresh CSRF token, manually crafts A's IDs or switches authentication transport. IDs/ownership in JSON cannot override the principal. Profile-local item keys are not cross-owner associations, and variant job references are checked explicitly in both controller and service. Middleware is after authorization, so it cannot grant an admin/tenant permission or replace session validation.
|
||||
|
||||
A caller genuinely authenticated as B can obtain **B's** context and deliberately submit arbitrary new text to **B's own** profile. The server cannot infer the origin of free text. That does not grant access to A or make A's old request/session valid. B02 prevents silent context substitution and cross-owner object access; it does not claim to prevent intentional copying or arbitrary same-origin malicious JavaScript from acting with a user's own credentials.
|
||||
|
||||
Browser cleanup applies to the updated bundle. A server cannot remotely erase memory in an already-open old bundle; its writes are now denied until the client updates. Standard tabs clear on storage signals; suspended pages are checked on activation, and cookie-only changes have the polling fallback. This is not a claim of instantaneous cross-process memory erasure.
|
||||
|
||||
Deploy the compatible API before the updated UI, then ensure clients load the new bundle. Old clients/API consumers must obtain `/auth/me.sessionContext` for writes; missing-context writes deliberately fail closed. Avoid a mixed API pool where some instances omit/enforce different contracts. Do not remove the server guard to restore old-client compatibility. Rolling back the API reopens B02 unless private mutations are disabled or equivalently protected. No database rollback is involved.
|
||||
|
||||
Session context is independent of record revision and creation intent. Same-session stale saves and duplicate creation remain separate open tickets. Already-authorized in-flight work remains bound to its original server principal; cancellation is not a transaction rollback.
|
||||
|
||||
## Files changed and next ticket
|
||||
|
||||
Application: `JobTrackerApi/Services/SessionContextMiddleware.cs` (new), `JobTrackerApi/Program.cs` (one middleware registration), `JobTrackerApi/Controllers/AuthController.cs`; UI `src/sessionBoundary.ts` (new), `src/api.ts`, `src/App.tsx`, `src/views/LoginPage.tsx`, `src/i18n/translations.ts`.
|
||||
|
||||
Tests: `JobTrackerApi.Tests/SessionContextMiddlewareTests.cs` (new), UI `src/session-boundary.test.ts` (new), `e2e/account-isolation.spec.ts` (new), `e2e/smoke.spec.ts` (context-aware helper). Documentation: this report, B02 evidence/manifest, validation README/queue/evidence and `docs/todo/work.md`. Pre-existing B01 files and original audit reproductions are preserved.
|
||||
|
||||
**Next recommended ticket: B03 — shared PDF admission. Do not begin it automatically.** B02 completion is not public-launch or deployment approval; the other validated release gates remain open.
|
||||
@@ -0,0 +1,125 @@
|
||||
# B03 — Shared PDF admission and process lifecycle
|
||||
|
||||
Implementation date: **2026-09-08**. **B03 is complete locally; not deployed.** B01/B02 are preserved. No schema, deployment, infrastructure, billing, rate-middleware ordering, template redesign or storage-accounting change. B04 has not been started.
|
||||
|
||||
## Original behavior and root cause
|
||||
|
||||
The original [F03 finding](README.md#f03--pdf-host-exhaustion), [boundary probe](BoundaryProbes.cs) and [three-process observation](evidence/boundary-results.json) remain historical evidence. Before editing application code, the new permanent `Default_limit_refuses_second_actual_browser_until_first_exits` test failed against the actual exporter: **two concurrent calls launched two inert browser processes**. The failure and its process count are retained in `.artifacts/b03/before.trx`.
|
||||
|
||||
`ICvPdfExporter` was already registered as a singleton, but each call launched Chromium without an admission limit. A 60-second timeout limited duration, not simultaneous processes. Endpoint rate policies could not protect every caller. The old lifecycle read stdout/stderr only after exit (a full pipe could stall rendering), killed on cancellation without awaiting termination, and retained partially written output on failure.
|
||||
|
||||
The existing class name says Playwright, but this service uses **Chromium's CLI**, not a Playwright browser pool. One admitted render can create a browser process tree with several OS processes/threads. The concurrency limit counts rendering operations, not total PIDs or memory bytes.
|
||||
|
||||
## Every current rendering path
|
||||
|
||||
| Entry point | Owner/authorization and shared boundary |
|
||||
|---|---|
|
||||
| `POST /api/cv/variants/{id}/export-pdf` | Authenticated variant lookup by owner and ID; principal owner passed to the singleton exporter. |
|
||||
| `POST /api/jobapplications/{id}/export-tailored-cv-pdf` | Existing authenticated, owner-filtered job lookup; current user passed to the same exporter. |
|
||||
| `POST /api/profile-cv/export-pdf` | Existing Pro authorization and current-account profile; AI/HTML preparation precedes the same exporter. Permanent controller test uses inert AI to prove admission without external calls. |
|
||||
| `GET /api/public-cv/{slug}/pdf` | Explicitly published variant only, existing `public-pdf` rate policy retained; resolved document owner passed to the same exporter, including anonymous visitors. |
|
||||
|
||||
Source search found one production `Process.Start`/`--print-to-pdf` rendering implementation and these four callers. No worker currently generates PDF. The package paths referenced in the original ticket prepare/store/copy existing artifacts; they do **not** launch a fifth renderer. Their existing tests remain in the affected/backend suites. Controller fallback exporters only throw when not configured; they cannot launch an unprotected process.
|
||||
|
||||
All callers resolve ownership before entering the exporter. B02 session preconditions and existing ID/owner authorization remain intact; capacity denial never authorizes access. Admin callers receive no special rendering pool. Public downloads consume the **document owner's** active allowance, so anonymous traffic cannot evade owner accounting by omitting a principal.
|
||||
|
||||
## Selected admission model and configuration
|
||||
|
||||
`Program.cs` registers one concrete [PdfRenderAdmission](../../../../JobTrackerApi/Services/PdfRenderAdmission.cs) singleton, injected into the singleton [PlaywrightCvPdfExporter](../../../../JobTrackerApi/Services/PlaywrightCvPdfExporter.cs). Even two exporter instances sharing that admission object use the same pool; a permanent process test proves this. No per-controller semaphore, queue, Redis, worker platform or database state was added.
|
||||
|
||||
| Setting/contract | Value |
|
||||
|---|---|
|
||||
| `CvExports:MaxConcurrentRenders` / `CvExports__MaxConcurrentRenders` | Default **1**, validated integer **1–8**. Eight is a configuration ceiling, not a proven safe production capacity. |
|
||||
| Per document owner | Fixed **1 active render**, shared across variants, sessions and endpoints. Owner entries disappear when the lease ends; memory is bounded by the global active limit. |
|
||||
| Waiting/queue | **None**: zero queued requests and zero admission-wait duration. Excess requests fail immediately; there is no pending work to wake up after cancellation. |
|
||||
| Exhaustion | **503**, `code: pdf_capacity_exceeded`, `Retry-After: 3`, `Cache-Control: no-store`, safe ProblemDetails message. Retry-After is advisory, not a reservation. |
|
||||
| `CvExports:RenderTimeoutSeconds` / `CvExports__RenderTimeoutSeconds` | Default **60**, validated integer **1–60**. Covers asynchronous export work after admission, including input/output IO. |
|
||||
| Render timeout | **504**, `code: pdf_render_timeout`, safe ProblemDetails message. |
|
||||
| Cancellation/shutdown | Request cancellation and `ApplicationStopping` linked to rendering; new work rejected when stopping. |
|
||||
| Termination confirmation | Kill process tree and await root exit before returning capacity, with a separate **5-second** confirmation bound. If exit cannot be confirmed, disable new PDF admission and emit a Critical log until operator recovery. |
|
||||
|
||||
Options bind through existing .NET facilities and validate on host start; the admission constructor also validates direct construction. Zero, negative, malformed and out-of-range values cannot enable unlimited operation. Values are fixed for a process lifetime; restart after changing them. No production environment was changed. Compose currently declares one backend service without a replica count or backend CPU/memory/PID budget. The resource limits on the AI sidecar are not limits on the API's Chromium processes.
|
||||
|
||||
Default one was chosen because no reliable production per-render peak RSS/CPU budget has been measured. It prevents multiplication of the unmeasured cost, but does not prove one pathological CV is safe for every host. The bounded local test deliberately uses two **inert** renders to verify the configurable limit; it is not a capacity recommendation. Verify real container/PID/memory/headroom under R04 before raising the default.
|
||||
|
||||
One document owner cannot take multiple slots when the global limit is increased. With default one, any running user occupies the sole slot. Zero waiting does not promise FIFO fairness or prevent an aggressive caller winning repeated races after completion. It prevents an unbounded backlog and simultaneous exports of one owner's documents. A visitor requesting published documents belonging to different owners can occupy multiple owner allowances, but still cannot exceed the global bound; anonymous identity fairness is not established by this policy. Existing public rate limiting remains; time-window owner/IP policy ordering is the separate B04 contract. No claim that B03 implements a per-minute quota.
|
||||
|
||||
## Lifecycle and failure safety
|
||||
|
||||
Admission happens **before** pruning, temporary-file creation or process launch. A structured lease spans the entire exporter, including file reads and cleanup; caller code throwing after return cannot retain the lease. The existing headless CLI arguments are preserved, with Chromium managing its headless profile lifecycle. Application-created input directories are removed after completion; successful PDFs retain the existing retention behavior.
|
||||
|
||||
Stdout and stderr drain concurrently to `Stream.Null`, avoiding pipe deadlocks and unbounded diagnostic strings containing CV data/URLs. Cancellation, shutdown and timeout kill the process tree and await root exit before release. Natural exit racing the kill is handled. Nonzero exit, missing output, failed launch, cancellation and timeout remove partial PDF files and release capacity. Cleanup failures are logged without document contents. Filesystem cleanup remains best effort if the OS denies deletion.
|
||||
|
||||
If the OS cannot terminate/confirm the process, the implementation fails closed: no replacement renders start, a Critical event identifies the disabled admission state, and an operator must inspect/stop remaining processes before restarting. This deliberately differs from silently losing one slot or admitting replacement work alongside an unknown process. Ordinary tested failures/cancellations/timeouts do not trigger this exceptional state. R04 remains responsible for hard process-tree/container containment, including abrupt API/host death.
|
||||
|
||||
Both frontend PDF download controls now translate 503/504 into an understandable EN/Bokmål message, including Axios blob responses. They make no automatic retry. Other API errors retain existing handling, and B02's interceptors/session lifecycle were not modified.
|
||||
|
||||
## Regression and adversarial evidence
|
||||
|
||||
- **22 new backend cases** in [PdfRenderAdmissionTests](../../../../JobTrackerApi.Tests/PdfRenderAdmissionTests.cs): actual exporter/process boundary; default bound; two slots and two exporter instances; same-owner rejection with a spare slot; global rejection across owners; cancelled admission with no delayed launch; repeated failures, no output, active cancellation and timeout; all slots reusable after three failure cycles; successful subsequent PDF; failed process launch; large output-pipe drainage; shutdown; invalid host configuration; fail-closed termination state; repeated logging failures cannot strand a slot; safe API error mapping; profile-controller path after inert AI preparation. Existing retention regression remains.
|
||||
- [PdfBrowserStub](../../../../JobTrackerApi.Tests/PdfBrowserStub/Program.cs) is a tiny test-only executable, built/copied with the test project. It launches no browser, executes no HTML, uses no network, and bounds its own wait to 30 seconds if the runner dies. Tests use fresh temporary roots and stop only their own fixture processes.
|
||||
- **2 frontend cases** prove busy/timeout messages from blob failures and no retry. B02's original 15 session-boundary tests and the surrounding account-switch suites still pass.
|
||||
- [Permanent HTTP adversarial test](../../../../job-tracker-ui/pdf-tests/admission.spec.ts), run with [playwright.pdf.config.ts](../../../../job-tracker-ui/playwright.pdf.config.ts): real local API, Identity login/session cookies, B02 context, controllers, owner checks, database and exporter; only Chromium is replaced with the inert executable.
|
||||
|
||||
The HTTP burst used three accounts, two sessions for A, multiple variants, and variant/job/public PDF routes. With limit **2**, **2 renders were admitted**, **10 attempts were rejected**, **0 queued**, and sampled/held actual child concurrency was **exactly 2**. Every admission rejection returned **503**, code `pdf_capacity_exceeded` and Retry-After **3**. A missing B02 context still returned **428** and a foreign variant **404** before rendering. After both processes exited, a job PDF returned **200**. The separately tested profile endpoint also could not bypass capacity. [Compact observations](evidence/b03-observations.json).
|
||||
|
||||
The first HTTP test run failed because the test runner and worker created different control directories. Corrected the fixture to inherit one directory; no application change was needed for that failure. Original reproduction and failed fixture output remain local; final passing output is recorded separately.
|
||||
|
||||
Real Chromium testing also caught an implementation regression that the inert fixture could not reveal: adding an explicit `--user-data-dir` made Chromium 151's CLI hang without producing a PDF. A one-paragraph local-file control timed out with the new argument; the original headless invocation generated a 43,578-byte PDF and exited with code 0 in 2,376 ms. The unnecessary argument was removed, preserving the existing browser invocation. No renderer replacement or sandbox relaxation was needed. The initial browser failure and diagnostic runs are retained separately from final verification; they are not counted as passing checks.
|
||||
|
||||
Final browser verification passed **all 17 tests**, with real public/Code PDFs taking approximately **1.3/1.4 seconds** in the exporter. Earlier development-server runs also encountered navigation timeouts and axe contrast failures on transient API-status chips; final production-static browser coverage passed without accessibility changes. The first static run passed 15/17: an account-switch test raced B02's intermediate `/login` navigation against its final `/login?sessionChanged=1` redirect, and a separate Code-PDF setup failed before rendering with SQLite Error 5 (`database is locked`) in `CareerProfileController.Put`'s existing profile/Identity projection. The account-switch test now waits for the final URL and visible login input before starting a new navigation; B02 application behavior is unchanged. The SQLite failure did not repeat and was not altered under B03; its projection/concurrency contract remains D02. The subsequent full run passed 17/17 with zero retries/flaky outcomes. This rerun does not prove the existing SQLite race is resolved.
|
||||
|
||||
## Verification results and exact commands
|
||||
|
||||
Final results: focused PDF/retention **23 passed**; affected backend **208 passed** before the last logging-fault regression was added; final broader backend **875 passed**, excluding the one pre-existing external-DNS IMAP test; focused frontend **37 passed / 5 suites**; full frontend **296 passed / 66 suites**; authenticated HTTP burst **1 passed**; full browser suite **17 passed**. Backend Release warnings-as-errors and Next production build pass. Targeted lint and `git diff --check` pass. Results and raw-output hashes are recorded in the [manifest](evidence/b03-test-results.json). Counts overlap; do not add focused runs to full-suite totals. The last logging-fault refinement is covered by the final full backend, focused and authenticated HTTP runs; frontend/browser behavior did not change after the passing frontend/browser suites.
|
||||
|
||||
Root, PowerShell (existing local packages; no network restore required for the dependency-free stub):
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force .artifacts/b03/empty-nuget | Out-Null
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe restore JobTrackerApi.Tests/PdfBrowserStub/PdfBrowserStub.csproj --source .artifacts/b03/empty-nuget
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --no-build --filter 'FullyQualifiedName~PdfRenderAdmissionTests|FullyQualifiedName~CvExportRetentionTests' --logger 'trx;LogFileName=focused-verified.trx' --results-directory .artifacts/b03 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~PdfRenderAdmissionTests|FullyQualifiedName~CvExportRetentionTests|FullyQualifiedName~CvBuilderTests|FullyQualifiedName~ProfileCvControllerTests|FullyQualifiedName~PublicCvControllerTests|FullyQualifiedName~JobApplicationsApplicationPackageTests|FullyQualifiedName~SessionContextMiddlewareTests|FullyQualifiedName~JobImport' --logger 'trx;LogFileName=affected.trx' --results-directory .artifacts/b03 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName!~ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail' --logger 'trx;LogFileName=backend-verified.trx' --results-directory .artifacts/b03 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore -warnaserror
|
||||
Copy-Item docs/audits/2026-09-07/validation/evidence/b02-browser-config.ts.txt .artifacts/b03/playwright.offline.config.ts
|
||||
git diff --check
|
||||
```
|
||||
|
||||
UI directory, PowerShell:
|
||||
|
||||
```powershell
|
||||
node node_modules/jest/bin/jest.js src/cv-builder-page.test.tsx src/cv-builder-deep-link.test.tsx src/public-cv-page.test.tsx src/session-boundary.test.ts src/account-scoped-state.test.tsx --runInBand --watchAll=false --json --outputFile=../.artifacts/b03/frontend-focused.json
|
||||
node node_modules/jest/bin/jest.js --runInBand --watchAll=false --json --outputFile=../.artifacts/b03/frontend.json
|
||||
node node_modules/typescript/bin/tsc --noEmit --pretty false
|
||||
node node_modules/eslint/bin/eslint.js playwright.pdf.config.ts pdf-tests/admission.spec.ts src/views/CvBuilderPage.tsx src/views/CvBuilderEditor.tsx src/cv-builder-page.test.tsx src/i18n/translations.ts e2e/account-isolation.spec.ts --max-warnings=0
|
||||
$env:NEXT_TELEMETRY_DISABLED='1'
|
||||
$env:NEXT_PUBLIC_API_BASE_URL='/api'
|
||||
node --max-old-space-size=4096 node_modules/next/dist/bin/next build
|
||||
$env:DOTNET_HOST_PATH='C:/Users/Connor/.dotnet/dotnet.exe'
|
||||
node node_modules/@playwright/test/cli.js test --config=playwright.pdf.config.ts --reporter=json > ../.artifacts/b03/adversarial-verified.json
|
||||
$env:PLAYWRIGHT_STATIC_EXPORT='true'
|
||||
node node_modules/@playwright/test/cli.js test --config=../.artifacts/b03/playwright.offline.config.ts --reporter=json > ../.artifacts/b03/browser-verified.json
|
||||
```
|
||||
|
||||
The passing browser rerun additionally set `PLAYWRIGHT_PREBUILT_EXPORT=true` to reuse the immediately preceding static build for `http://localhost:5302/api`. The copied local config also redirected backend logs to `.artifacts/b03/real-server.log` for lifecycle diagnostics. These are test artifacts, not deployment artifacts. No production credentials or services were used. The excluded IMAP test performs real external DNS, which was not authorized; all selected backend tests ran without skips.
|
||||
|
||||
**Existing unrelated failure:** standalone TypeScript exits **2** with only `src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.` This is the previously validated R01a diagnostic. Neither Jest nor the successful Next build closes that separate gate. No type suppression or unrelated fix was added. Lint emits only the existing Browserslist stale-data advisory; no dependency upgrade was performed.
|
||||
|
||||
## Observability, limitations and rollout
|
||||
|
||||
Existing logging is sufficient for this change: Debug admission events record active count/limit/rejection/disabled state without owner IDs; Information completion events record duration/outcome; failures record exception type; termination uncertainty emits Critical. Waiting is always zero. No metrics platform was introduced. Production dashboards/alerts for active/rejected/timeout trends and especially disabled admission should follow the existing operations work. No CV contents, raw browser stdout/stderr, or new sensitive labels are logged by the added events.
|
||||
|
||||
**This limit is process-local; if Jobjakt runs N rendering instances, effective global concurrency becomes N × configured limit.** Current Compose describes one API container, not proof of the running production topology. Additional API replicas, another rendering worker or a second DI service root sharing a host require re-budgeting total capacity or a deliberately shared rendering boundary before rollout. Do not assume the per-account limit is distributed either.
|
||||
|
||||
The gate bounds Chromium execution and the export lifecycle, not preceding authentication/DB/HTML/AI work or arbitrary single-document CPU/memory consumption. No new page count, input/output byte quota or artifact retention policy was imposed. Those require separate product/storage/containment work; browser sandbox flags, container root execution and hard resource limits remain R04. Application temporary inputs and partial outputs are cleaned; Chromium-managed profile/cache cleanup after a forced kill still needs platform/container evidence under R04. The verified real-browser smoke establishes compatibility, not safe production throughput, OOM immunity or a PID ceiling. Public links can occupy their document owner's allowance; admission is not an anti-abuse identity or time-window quota.
|
||||
|
||||
No application migration or data rollback is required. Deploy with the conservative default; expose the documented configuration keys only with measured headroom. Rolling back the admission change reopens concurrent PDF exhaustion, so disable PDF entry points or retain equivalent bounds if rollback is required. Do not remove B01/B02 protections for compatibility. Historical `BoundaryProbes.cs` is tied to the original baseline constructor; the permanent tests are the current implementation's executable regression gate.
|
||||
|
||||
## Files changed and next ticket
|
||||
|
||||
Application: `Services/PdfRenderAdmission.cs` and `Services/PdfRenderExceptionHandler.cs` (new); `Services/PlaywrightCvPdfExporter.cs`; `Program.cs` (options/singleton/exception-handler registration only); `appsettings.json`. Tests: `JobTrackerApi.Tests/PdfRenderAdmissionTests.cs` and `PdfBrowserStub/{PdfBrowserStub.csproj,Program.cs}` (new), test project fixture build/copy and `CvExportRetentionTests.cs` constructor injection. UI: PDF-error handling in `CvBuilderPage.tsx` and `CvBuilderEditor.tsx`, EN/NB translations and dashboard tests; `playwright.pdf.config.ts`, `pdf-tests/admission.spec.ts`, `package.json` test command; `e2e/account-isolation.spec.ts` final-redirect wait only. Documentation: this report, evidence/manifest, validated README/queue/evidence and TODO. Pre-existing uncommitted B01/B02 and audit work remains present; the complete working-tree diff is not exclusively B03.
|
||||
|
||||
**Acceptance: all ten B03 criteria pass locally.** All four real generation paths enter one singleton pool before expensive work; configured process/owner bounds hold across endpoints, sessions and accounts. Immediate refusal eliminates waiting/backlog cancellation problems. Permanent process tests prove ordinary exceptions, cancellation and timeout release after exit, with repeated recovery; unconfirmed termination disables replacement work. Invalid configuration fails host startup. Final broader regression/build results are healthy subject to the explicitly pre-existing TypeScript gate and observed SQLite fixture race. Process-local scaling and deployment containment limits are documented above. No schema or architectural stop condition was encountered, and no later ticket was implemented.
|
||||
|
||||
**Next recommended ticket: B04 — resolve principal before owner limiting. Not started.** B03 admission completion alone does not close F03's separate R04 deployment/containment evidence or approve public launch.
|
||||
@@ -0,0 +1,126 @@
|
||||
# B04 — Authenticated account rate-limit partitioning
|
||||
|
||||
Implementation date: **2026-09-08**. **B04 is complete locally; not deployed.** B01, B02 and B03 are preserved. No schema, deployment, authentication redesign, distributed limiter, billing or B05 change.
|
||||
|
||||
## Original behavior and root cause
|
||||
|
||||
The original [F06 observation](evidence.md) and [probe](probes.mjs) remain unchanged. Before modifying application code, the new permanent HTTP test ran real Identity logins and account exports against the current API: **A: 200/200; B on the same IP: 429/429**. Its safe assertion failed in `.artifacts/b04/before.json`. This independently confirms the original audit's 200/200/429 observation.
|
||||
|
||||
`Program.cs` registered account-aware policies correctly in intent, but called `UseRateLimiter` before `UseAuthentication`. Both `account-data` and `email-send` read an empty principal and fell back to IP. Helper-only unit tests could not detect this middleware defect. Session ID, record revision and B03 resource concurrency are different contracts and were not changed.
|
||||
|
||||
## Middleware and identity boundary
|
||||
|
||||
Before: endpoint routing → forwarded-header/host/HTTPS/error/logging middleware → CORS → **rate limiting** → existing CSRF middleware → **authentication** → authorization → B02 session context → controllers.
|
||||
|
||||
After: endpoint routing → forwarded-header/host/HTTPS/error/logging middleware → CORS → existing CSRF middleware → **authentication → authorization → rate limiting** → B02 session context → controllers. Routing is supplied by the minimal ASP.NET application and makes endpoint policies available before rate limiting; the existing explicit application middleware order otherwise stays intact. Forwarded headers still precede IP evaluation, and the exception handler still surrounds authentication and rate-limit processing.
|
||||
|
||||
Only the rate-limiter call moved. The existing `smart` scheme selects default authentication; local JWT signatures, issuer, audience, lifetime and database-backed session/user validation finish before account partitioning. **Authorization must also precede partitioning:** an endpoint's explicit `local` scheme can replace the default scheme's principal. Self-review identified this in the optional Google bearer + local cookie path. The first authentication-only placement was therefore superseded, even though its original shared-IP tests passed. The final HTTP regression proves invalid credentials are rejected before the account limiter, guarding authorization order as well as authentication order. No claim of a live Google mixed-credential test is made; the source-established scheme-selection boundary is handled by the framework's existing authorization middleware.
|
||||
|
||||
Local bearer and session-cookie credentials resolve to the same application user ID. Existing local-cookie precedence when both a local cookie and local bearer are supplied is preserved and tested. External exchanges still issue local sessions; optional external-provider configuration is unchanged, and no live OAuth provider was contacted.
|
||||
|
||||
The two account policies now share `RateLimitPartitionKeys.Account`, which calls the existing `LocalAuthIdentity.GetRequiredUserId`. It requires an authenticated principal and uses its name-identifier/subject. The key is `{policy}:user:{validated owner}` or `{policy}:ip:{RemoteIpAddress}` when authentication did not establish an owner. Separate user/IP namespaces prevent an identifier shaped like an IP from colliding with anonymous traffic. No body/query/header value, including `X-Session-Context`, selects the owner.
|
||||
|
||||
Rate limiting follows CSRF and authorization but remains before B02 session preconditions and controller validation. An authorized attempt can consume **that authenticated account's** allowance even if a later B02 check returns 409. It cannot charge the account named by the stale header. Malformed/expired/invalid-cookie/revoked/nonexistent-session credentials receive **401 before account partitioning** on protected endpoints, even after repeated attempts; a CSRF failure can still return 403 earlier. Anonymous/security endpoints marked `AllowAnonymous` still reach their IP policies, where those same invalid credentials cannot manufacture partitions. This deliberately avoids charging an anonymous or different-scheme key for an operation that only the endpoint's local principal may perform.
|
||||
|
||||
Authentication/authorization now incur their normal token/session/policy validation cost before a request can be rate-rejected, including requests with credentials to IP-scoped endpoints. This resolves the actual resource principal at this boundary; it is not a pre-authentication CPU or global ingress admission limit. Invalid credentials cannot reach protected controller work, and anonymous security actions remain rate-limited. No unverified token is parsed into an account key.
|
||||
|
||||
## Limiter inventory
|
||||
|
||||
All six ASP.NET policies remain fixed-window, zero-queue limiters. No permits or windows changed. Endpoints below are POST unless stated otherwise.
|
||||
|
||||
| Policy | Endpoints | Classification / allowance | Partition after B04 | Anonymous or failed authentication |
|
||||
|---|---|---|---|---|
|
||||
| `auth-login` | `/api/auth/login`, `/register`, `/google/exchange`, `/microsoft/exchange`; `/api/auth/2fa/setup`, `/verify-setup`, `/disable`, `/recovery-codes/regenerate` | **IP-scoped**; default 10 / 5 minutes (`RateLimits:AuthLoginPermitLimit` remains configurable) | `login:{IP}` even for authenticated callers; all listed routes share the policy | Anonymous endpoints use the same IP key; protected endpoints reject before limiting if authorization fails |
|
||||
| `auth-email` | `/api/auth/microsoft/legacy-relink/confirm`; `/api/auth/email-change/request`, `/confirm`, `/cancel`; `/api/auth/request-password-reset`, `/reset-password`, `/verify-email`, `/resend-verification-email`; `/api/users/{id}/send-password-reset`, `/api/users/send-test-email` | **IP-scoped**; 5 / 15 minutes | `email:{IP}` even for authenticated/admin callers; submitted email/user identifiers do not select the key | Anonymous endpoints use the same IP key; protected endpoints reject before limiting if authorization fails |
|
||||
| `auth-2fa-challenge` | `/api/auth/2fa/challenge` | **IP-scoped**; 5 / 5 minutes | `2fa:{IP}` | Same IP key; pending-token/code validation remains in controller |
|
||||
| `email-send` | `/api/email/send` | **Account-scoped**; 10 / minute | `email-send:user:{owner}` across sessions, IPs and credentials | Local authorization returns 401 before rate partitioning |
|
||||
| `account-data` | `/api/export/account`, `/api/account-lifecycle/delete` | **Account-scoped**; 2 / hour, shared by both routes | `account-data:user:{owner}` across sessions, IPs and credentials | Local authorization returns 401 before rate partitioning |
|
||||
| `public-pdf` | GET `/api/public-cv/{slug}/pdf` | **Combined IP + slug**; 3 / minute | `public-pdf:{IP}:{slug}`, regardless of authentication | Same IP + slug key; only published documents are downloadable |
|
||||
|
||||
Route prefixes in grouped cells are shared within the indicated controller. The account-key helper retains a defensive `{policy}:ip:{IP}` fallback if used without an authenticated owner; current account-policy endpoints require local authorization and do not admit anonymous requests to that fallback. When an actual remote address is unavailable, the existing `unknown` fallback remains; real HTTP tests have an address and prove independent anonymous IPs. The intentional IP-scoped policies can still be shared by legitimate users behind one NAT. B04 isolates **account** quotas, not every security quota.
|
||||
|
||||
**B03 is separate:** its singleton exporter admission is a process/global expensive-resource boundary, with a per-document-owner active allowance. It is not one of these fixed-window policies. Public PDF traffic passes its IP+slug rate limit before the shared exporter gate; the other PDF routes still use the same exporter. No new PDF rate policy or admission pool was added.
|
||||
|
||||
## Proxy and response behavior
|
||||
|
||||
Existing `ForwardedProxyConfiguration` requires explicit known networks, clears implicit defaults and accepts one forwarding hop. Rate policies use `Connection.RemoteIpAddress`, never raw `X-Forwarded-For`, `Forwarded` or `X-Real-IP`. The permanent HTTP fixture trusts only `127.0.0.1/32` as a synthetic proxy, using documented test addresses as forwarded clients. Actual sockets bound to untrusted `127.0.0.2` cannot evade the login limit by changing all three forwarding headers; `127.0.0.3` retains independent capacity. No production proxy configuration changed. B06 still must establish deployed proxy/firewall/direct-ingress correctness.
|
||||
|
||||
Rate rejections retain **429**, now with the fixed-window lease's **Retry-After** metadata rounded up to seconds, `Cache-Control: no-store`, and a safe ProblemDetails body (`code: rate_limit_exceeded`, generic title/detail, trace ID). No partition keys, owner IDs or credential details are returned. Existing frontend helpers already understand ProblemDetails and 429; no application frontend code changed. B03's distinct 503/504 contract is unchanged.
|
||||
|
||||
## Permanent regression and adversarial evidence
|
||||
|
||||
[HTTP tests](../../../../job-tracker-ui/rate-limit-tests/isolation.spec.ts) use real local API middleware, authentication/session cookies, bearer tokens, owner checks and synthetic SQLite data, with [test-only configuration](../../../../job-tracker-ui/playwright.rate-limits.config.ts). They add nine scenarios:
|
||||
|
||||
1. A exhausts account exports; B on the same IP still has two successful exports; both then receive 429.
|
||||
2. Reverse the order: B first, with the same independent result.
|
||||
3. Four simultaneous exports: two accounts, two sessions each, cookie/bearer mixed. All four succeed; later requests from other IPs remain limited for both owners.
|
||||
4. Craft body/query/account headers and stale/forged B02 contexts under B's authentication. All ten attempts charge B, while A retains all ten email-send permits. Unconfirmed sends return 400 before provider/delivery work; no email is sent.
|
||||
5. Export plus explicitly disabled deletion share account-data capacity; another owner retains allowance. Deletion is not enabled or performed.
|
||||
6. Anonymous, malformed bearer, correctly signed expired bearer, invalid cookie, revoked bearer, bad signature and correctly signed nonexistent session each receive repeated 401s before account partitioning. The same credentials cannot escape anonymous login's IP allowance. A legitimate session on an IP with exhausted login capacity retains its account allowance.
|
||||
7. Login/registration, reset/resend and 2FA challenge preserve their separate IP policies, including authenticated traffic and a second anonymous IP.
|
||||
8. Public PDF stays IP+slug scoped across anonymous/A/B callers; distinct slugs and IPs retain their own allowance. Missing slugs avoid rendering in this rate-policy test.
|
||||
9. Real untrusted loopback peers cannot use forwarded-header forgery to select an IP partition.
|
||||
|
||||
Two additional backend cases in [RateLimitPartitionKeysTests](../../../../JobTrackerApi.Tests/RateLimitPartitionKeysTests.cs) cover both account policies' rejection of unauthenticated claims, anonymous IP separation and user/IP namespace collision prevention. The real HTTP suite, rather than those helper tests, is the permanent middleware-order regression gate.
|
||||
|
||||
| Bounded local scenario | A | B | Result |
|
||||
|---|---|---|---|
|
||||
| Before application changes; one shared IP | 2 admitted | 0 admitted, 2 rejected | Original failure reproduced |
|
||||
| After, A first; one shared IP | 2 admitted, 1 rejected | 2 admitted, 1 rejected | Independent account partitions |
|
||||
| After, B first; one shared IP | 2 admitted, 1 rejected | 2 admitted, 1 rejected | Same result in reverse |
|
||||
| Concurrent, two sessions each, mixed cookie/bearer | 2 admitted; 2 later rejected from other IPs | 2 admitted; 2 later rejected from other IPs | Four concurrent successes, no session/IP bypass |
|
||||
|
||||
These are bounded local correctness checks, not production load tests. Original evidence is preserved; see [B04 observations](evidence/b04-observations.json) and [results/hashes](evidence/b04-test-results.json).
|
||||
|
||||
## Verification commands and results
|
||||
|
||||
Final results: **nine HTTP rate-policy scenarios passed**, **877 selected backend tests passed**, **17 browser tests passed** without retries/flaky outcomes, **36 affected frontend tests / four suites passed**, and **B03's authenticated admission probe passed** unchanged (two admitted, ten rejected, maximum two renders, recovery 200). The initial focused auth/rate/B01–B03 backend run passed **207** tests; the final full backend includes those cases. Release warnings-as-errors build passed with **zero warnings/errors**, targeted lint passed, and `git diff --check` passed. Counts overlap; do not add subset runs to full totals. [Manifest and raw-output hashes](evidence/b04-test-results.json).
|
||||
|
||||
Standalone TypeScript's final incremental run exits **1** (the initial run exited **2**) with only the existing `src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.` No B04 type error, suppression or unrelated typing fix was added. Frontend runtime code did not change, so affected Jest/browser checks and standalone typing were used; a new Next production build was not needed or claimed for B04.
|
||||
|
||||
Repository root, PowerShell:
|
||||
|
||||
```powershell
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~RateLimitPartitionKeysTests|FullyQualifiedName~LocalAuthIdentityTests|FullyQualifiedName~AuthSessionRevocationTests|FullyQualifiedName~SessionContextMiddlewareTests|FullyQualifiedName~ExternalOriginTests|FullyQualifiedName~ApiAuthorizationPostureTests|FullyQualifiedName~SessionsControllerTests|FullyQualifiedName~AuthAndSystemControllerTests|FullyQualifiedName~JobImport|FullyQualifiedName~PdfRenderAdmissionTests|FullyQualifiedName~CvExportRetentionTests' --logger 'trx;LogFileName=focused.trx' --results-directory .artifacts/b04 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --no-build --filter 'FullyQualifiedName!~ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail' --logger 'trx;LogFileName=backend-final.trx' --results-directory .artifacts/b04 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore -warnaserror
|
||||
Copy-Item docs/audits/2026-09-07/validation/evidence/b02-browser-config.ts.txt .artifacts/b04/playwright.offline.config.ts
|
||||
git diff --check
|
||||
```
|
||||
|
||||
UI directory, PowerShell (run the three API/browser suites serially because they use port 5302):
|
||||
|
||||
```powershell
|
||||
$env:DOTNET_HOST_PATH='C:/Users/Connor/.dotnet/dotnet.exe'
|
||||
$env:NEXT_TELEMETRY_DISABLED='1'
|
||||
node node_modules/@playwright/test/cli.js test --config=playwright.rate-limits.config.ts --reporter=json > ../.artifacts/b04/api-final.json
|
||||
node node_modules/@playwright/test/cli.js test --config=playwright.pdf.config.ts --reporter=json > ../.artifacts/b04/pdf-admission-final.json
|
||||
$env:PLAYWRIGHT_STATIC_EXPORT='true'
|
||||
$env:PLAYWRIGHT_PREBUILT_EXPORT='true'
|
||||
node node_modules/@playwright/test/cli.js test --config=../.artifacts/b04/playwright.offline.config.ts --reporter=json > ../.artifacts/b04/browser-verified.json
|
||||
node node_modules/jest/bin/jest.js src/login-page.test.tsx src/session-boundary.test.ts src/account-scoped-state.test.tsx src/cv-builder-page.test.tsx src/api.test.ts --runInBand --watchAll=false --json --outputFile=../.artifacts/b04/frontend-focused.json
|
||||
node node_modules/eslint/bin/eslint.js playwright.rate-limits.config.ts rate-limit-tests/isolation.spec.ts e2e/smoke.spec.ts --max-warnings=0
|
||||
node node_modules/typescript/bin/tsc --noEmit --pretty false
|
||||
```
|
||||
|
||||
The Jest selection resolves to four existing suites; no `src/api.test.ts` suite exists. Browser verification reuses B03's already verified production-static frontend targeting `localhost:5302`, since B04 changes no frontend runtime code. It starts a fresh API/data root and exercises the B04 backend. The test export is not a deployable production artifact. Existing local dependencies were used; no dependency downloads or external providers. The one excluded backend test performs external DNS and was not authorized; selected tests have no skips. An initial fixture-lint false positive interpreted Playwright's `use` callback as a React hook; naming it `provide` fixed the test without disabling lint rules.
|
||||
|
||||
The first full browser run passed 17/17 before the final authorization-order refinement. The next run passed 16/17: the anonymous public-CV fixture cleared cookies while its authenticated page was still running, so B02 correctly redirected that page to `/login?sessionChanged=1` during the next navigation. The retained error context shows the session-change sign-in screen, not a rate-limit or rendering failure. The fixture now navigates to `about:blank` before removing cookies, stopping the old page's callbacks. This is a test-only correction; the dedicated account-switch test still verifies B02's live-page redirect. The final full rerun passed **17/17**, including anonymous PDF and account switching. Failed output and `.artifacts/b04/public-cv-session-race.md` remain separate evidence.
|
||||
|
||||
## Residual limits, self-review and rollout
|
||||
|
||||
All fixed-window partitions remain **process-local**. With N API instances an account can receive up to N × the per-instance allowance if traffic reaches all instances. Restarts reset counters. B04 does not claim distributed quotas or introduce Redis. Reassess shared rate budgets when horizontal scaling actually requires them.
|
||||
|
||||
Account quotas count authorized attempts, including later B02/session/controller-validation failures, not only successful exports/sends. Requests denied by CSRF or authorization no longer consume rate capacity; they cannot reach the protected operation. The tests explicitly prove the final ordering contract. Auth validation occurs before rate admission; middleware limits do not replace host/ingress resource controls. Intentional login/security IP sharing, missing-IP fallback and configured trusted-proxy assumptions remain explicit. Public-PDF per-IP+slug limiting and B03 process concurrency are different layers; B03's R04 production sizing/containment limitations remain open.
|
||||
|
||||
Self-review traced every account-policy registration and endpoint: only server-authenticated claims reach owner keys; extra sessions and auth transport switches do not change the account ID; invalid authentication cannot choose arbitrary keys; B02 is still a precondition after authorization and is not an identity source. IP policies were neither removed nor converted. Forwarding trust is tested with real local sockets. No new service, network path, schema or synchronization primitive was introduced. Existing ASP.NET partitioned limiters provide concurrency safety.
|
||||
|
||||
No data migration or rollback transformation is needed. Rolling back the middleware order reopens shared-IP account throttling; retain authentication and endpoint authorization before limiting during any rollback. Deployment/ingress verification remains separately authorized B06 work, not implied by this local completion.
|
||||
|
||||
## Files and next ticket
|
||||
|
||||
Application: `JobTrackerApi/Program.cs` (limiter order, two key calls, safe rejection metadata) and `JobTrackerApi/Services/RateLimitPartitionKeys.cs`. Tests: `JobTrackerApi.Tests/RateLimitPartitionKeysTests.cs`, new `job-tracker-ui/rate-limit-tests/isolation.spec.ts`, new `job-tracker-ui/playwright.rate-limits.config.ts`; one package script; `job-tracker-ui/e2e/smoke.spec.ts` stops the authenticated page before the anonymous fixture clears cookies. Documentation: this report, observations/results manifest, validation README/queue/evidence and TODO. Pre-existing uncommitted B01/B02/B03 and audit work remains present; the entire working-tree diff is not exclusively B04.
|
||||
|
||||
**Recommended next ticket: B05 — spreadsheet-safe CSV fields. Not started.**
|
||||
|
||||
**Acceptance: all ten B04 criteria are satisfied locally.** Account keys use the principal established by authentication and endpoint authorization; shared-IP accounts stay isolated while same-account sessions/credentials share capacity; client-controlled identity fields cannot choose keys. Anonymous/security IP policies, B02 preconditions and B03 rendering admission remain tested. Middleware order and process-local/deployment limits are documented. The only standalone type diagnostic is the pre-existing R01a failure. No schema or architecture stop condition was encountered. This completion does not authorize deployment or B05.
|
||||
@@ -0,0 +1,109 @@
|
||||
# B05 — Spreadsheet-safe CSV fields
|
||||
|
||||
Implementation date: **2026-09-09**. Local implementation and verification only; not deployed. B01–B04 application behavior is unchanged. B06 has not been started. No schema, dependency, import-system or serialization-framework change.
|
||||
|
||||
## Original behavior and root cause
|
||||
|
||||
F05 remains a **P2 export-safety finding**, not server-side code execution. The historical [validation](README.md#f05--spreadsheet-formula-injection), [observations](evidence.md) and `probes.mjs` are preserved. Before editing application code, a new permanent controller regression exported a synthetic job with title `=1+1`. It failed as expected: the independently parsed cell was `=1+1`, rather than the required `'=1+1`. The actual [before CSV](evidence/b05-before.csv) was then opened read-only in a separate, hidden local Excel 16.0 instance: D2 had `HasFormula=true` and value **2**. Only harmless arithmetic was used; no links, commands or exfiltration payloads were exercised.
|
||||
|
||||
`ExportController.Esc` correctly escaped commas, quotes and newlines, but CSV quoting does not force spreadsheet text interpretation. All strings were passed through syntax escaping without a spreadsheet policy. The fix therefore belongs at export, before the existing syntax escaping; it does not sanitize storage or change input validation.
|
||||
|
||||
The same Excel check exposed an encoding issue: `Ærlig Økonomi Ås` opened as `Ærlig Økonomi Ã…s` in the BOM-less UTF-8 file. This is the concrete reason for adding a UTF-8 BOM. The frontend's previous `Blob.text()` followed by a new text Blob would strip that BOM, so the download helper now preserves the returned Blob bytes. JSON downloads still preserve their original bytes.
|
||||
|
||||
## Inventory and policy
|
||||
|
||||
There is **one CSV export path**: authenticated `GET /api/export/jobs?format=csv`, case-insensitive format selection and existing `includeDeleted` behavior. Settings → General → Export CSV calls it through `ImportExportJobs`. Account export archives contain JSON, not another CSV writer. Other comma-list parsing and the frontend's separate CSV-to-import-JSON converter are not CSV exports and are unchanged. No CSV writing library was present; the existing, correct `Esc` implementation is retained and verified with an independent framework CSV parser.
|
||||
|
||||
| Columns | Classification | Output policy |
|
||||
|---|---|---|
|
||||
| Company, CompanyLocation, CompanySource, JobTitle, Status, Location, Salary, SalaryCurrency, SalaryPeriod, NextAction, JobUrl, Notes, CoverLetterText | 13 text fields, including imported/user-influenced strings and free-text salary | `CsvCellSafety.SpreadsheetText`, then existing CSV escaping |
|
||||
| SalaryMin, SalaryMax | Nullable decimal | Existing invariant numeric formatting; no apostrophe, including negative numbers |
|
||||
| DateApplied, FollowUpAt | Nullable DateTime | Existing round-trip `o` formatting; no apostrophe |
|
||||
|
||||
The shared helper examines the first Unicode scalar after any leading Unicode whitespace, control or Format-category characters. If that scalar is ASCII **`=`, `+`, `-` or `@`**, prepend one ASCII apostrophe **at the absolute start of the original value**. Nothing is removed, trimmed or rewritten. Leading tab, CR/LF, NUL, vertical tab, non-breaking space, zero-width space, BOM and supplementary-plane format characters are covered by permanent tests. Existing apostrophe-prefixed text is unchanged, even on a second pass. Null becomes the existing empty CSV cell. Ordinary text, including Unicode and embedded delimiters, stays unchanged before syntax escaping.
|
||||
|
||||
This is an explicit spreadsheet-text convention, not a claim to prevent every spreadsheet's undocumented normalization or a user's later transformation. Excel's actual direct-open behavior is recorded below. For LibreOffice-compatible import, UTF-8/comma/standard double-quote parsing leaves the leading apostrophe as text rather than an initial formula marker; appearance and apostrophe handling can vary with import options. **LibreOffice was not installed, so no LibreOffice runtime verification is claimed.** Repository instructions prohibit external services without authorization; no online compatibility lookup or dependency download was used.
|
||||
|
||||
| Input / field | Before parsed CSV cell | After parsed CSV cell |
|
||||
|---|---|---|
|
||||
| Text `=1+1` | `=1+1` (Excel evaluates to 2) | `'=1+1` (Excel literal text) |
|
||||
| Text `+SUM(A1:A2)` | `+SUM(A1:A2)` | `'+SUM(A1:A2)` |
|
||||
| Text `-2+3` | `-2+3` | `'-2+3` |
|
||||
| Text `@SUM(A1:A2)` | `@SUM(A1:A2)` | `'@SUM(A1:A2)` |
|
||||
| Text space + tab + `=1+1` | ` \t=1+1` | `' \t=1+1` |
|
||||
| Text `=SUM(1,2)` | `=SUM(1,2)` | `'=SUM(1,2)`; serialized CSV is `"'=SUM(1,2)"` |
|
||||
| Decimal SalaryMin | `-750.50` | `-750.50` (Excel numeric -750.5) |
|
||||
| DateApplied | `2026-09-09T12:30:00.0000000Z` | Identical existing format |
|
||||
|
||||
Escapes in the table describe controls; the actual sample files contain the real characters. The response now uses `text/csv; charset=utf-8`, starts with bytes `EF BB BF`, and retains ASP.NET's attachment disposition and a server-generated date-only filename. No filename is derived from user text. The browser retains its generated safe filename and now downloads identical response bytes.
|
||||
|
||||
## Spreadsheet evidence and compatibility trade-offs
|
||||
|
||||
[Before Excel observation](evidence/b05-excel-before.json), [after observations for all 17 columns](evidence/b05-excel-after.json), [after original fixture](evidence/b05-after.csv), [typed/adversarial fixture](evidence/b05-typed.csv) and [local probe](evidence/b05-excel-probe.ps1) are retained. The probe opens explicit local synthetic files read-only with updates, events and macros disabled, then closes its own workbooks and Excel instance without saving.
|
||||
|
||||
Excel 16.0 after the fix: **all 17 cells in both fixtures have `HasFormula=false`**. The original title is the literal `'=1+1`; Norwegian company text is intact. The mixed fixture covers all four markers across columns, leading space/tab/CRLF/zero-width space, comma-containing expressions, multiline Norwegian text and negative free-text salary. Decimal cells remain numeric **-750.5 / 1200.75**. Dates retain their existing ISO round-trip strings; B05 does not claim to convert those strings into native spreadsheet date serials.
|
||||
|
||||
The apostrophe is visible in this Excel direct-open mode. It is deliberately retained rather than silently deleting user text. CSV is now a spreadsheet-safe presentation export, not a byte-exact text round-trip format. JSON remains the unchanged raw-data export. Importing safe CSV into another system may retain the apostrophe; manually removing it or using tooling that strips prefixes can reintroduce formula interpretation. No guarantee is made about re-saving/reopening modified files, all spreadsheet versions/import settings, or spreadsheet cell-length limits. The 70,000-character regression proves valid CSV preservation, not that Excel can display unlimited cell contents.
|
||||
|
||||
## Permanent regression coverage
|
||||
|
||||
- `JobTrackerApi.Tests/CsvExportTests.cs`: **25 cases** exercise the real controller export and independent `Microsoft.VisualBasic.FileIO.TextFieldParser`, not just the helper. They cover the original exploit, all four markers, leading controls/Unicode format variants, idempotency, normal/empty/null text, delimiter/quote/newline correctness, a delimiter-breakout-shaped value, Norwegian/emoji, every text column, untouched decimal/date cells, very long fields, case-insensitive CSV selection and deleted-row selection. Reloaded stored values and raw JSON remain unmodified.
|
||||
- `job-tracker-ui/src/csv-export.test.tsx`: **2 cases**, CSV and JSON, check actual component download bytes and safe generated filenames; CSV includes BOM and Norwegian text.
|
||||
- `job-tracker-ui/e2e/csv-export.spec.ts`: real local authentication plus B02 context for synthetic creation, real CSV headers/bytes, unchanged JSON, and an actual Settings download compared byte-for-byte with the API response.
|
||||
|
||||
## Verification results
|
||||
|
||||
Final command results and raw-output hashes are recorded in [the manifest](evidence/b05-test-results.json). No production requests or destructive database operation were used; API/browser fixtures use fresh synthetic local data.
|
||||
|
||||
Final results: **25 focused CSV backend cases**, **902 broader backend tests**, **298 frontend tests / 67 suites**, **18 browser tests**, **9 B04 rate-policy HTTP scenarios**, and **1 B03 PDF admission probe** pass. All final selected suites have zero failures/skips; browser/API suites have no flaky outcomes or retries. The focused two-test frontend export run also passed; counts overlap and must not be added together. The one excluded backend test performs external DNS and was not authorized: `ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail`. B01 import, B02 session/ownership and B03/B04 tests are included in the broader backend/browser/API checks.
|
||||
|
||||
API/test-project Release build with warnings as errors: **0 warnings / 0 errors**. Next production build: **passed**, using `/api` for the final build. Targeted ESLint and `git diff --check`: **passed** (lint prints only the existing Browserslist stale-data advisory). Standalone TypeScript's final command exits **1** with only the pre-existing `src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.` No suppression or unrelated type fix was added. A successful Next build does not close that separate R01a gate.
|
||||
|
||||
Repository root, PowerShell:
|
||||
|
||||
```powershell
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter FullyQualifiedName~CsvExportTests --logger 'trx;LogFileName=after.trx' --results-directory .artifacts/b05 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --no-build --filter 'FullyQualifiedName!~ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail' --logger 'trx;LogFileName=backend.trx' --results-directory .artifacts/b05 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore -warnaserror
|
||||
Copy-Item docs/audits/2026-09-07/validation/evidence/b02-browser-config.ts.txt .artifacts/b05/playwright.offline.config.ts
|
||||
.artifacts/b05/excel-probe.ps1 -CsvPaths .artifacts/b05/after-csv.csv,.artifacts/b05/after-typed.csv
|
||||
git diff --check
|
||||
```
|
||||
|
||||
The before-fix command used the same focused filter with `LogFileName=before.trx` and **failed 1/1 as expected**. Before CSV bytes were extracted from `B05_CSV_SAMPLE` in that TRX; after files come from `B05_CSV_SAMPLE` and `B05_TYPED_SAMPLE` in `after.trx`. The Excel before run used `.artifacts/b05/before.csv`; the retained probe was subsequently extended to inspect all 17 cells for the after run.
|
||||
|
||||
UI directory, PowerShell (the three API/browser commands ran serially because they share port 5302):
|
||||
|
||||
```powershell
|
||||
npm test -- --runInBand src/csv-export.test.tsx
|
||||
node node_modules/jest/bin/jest.js --runInBand --watchAll=false --json --outputFile=../.artifacts/b05/frontend.json
|
||||
node node_modules/eslint/bin/eslint.js src/components/ImportExportJobs.tsx src/csv-export.test.tsx e2e/csv-export.spec.ts --max-warnings=0
|
||||
$env:DOTNET_HOST_PATH='C:/Users/Connor/.dotnet/dotnet.exe'
|
||||
$env:NEXT_TELEMETRY_DISABLED='1'
|
||||
$env:PLAYWRIGHT_STATIC_EXPORT='true'
|
||||
$env:PLAYWRIGHT_PREBUILT_EXPORT='false'
|
||||
node node_modules/@playwright/test/cli.js test --config=../.artifacts/b05/playwright.offline.config.ts --reporter=json > ../.artifacts/b05/browser.json
|
||||
node node_modules/@playwright/test/cli.js test --config=playwright.rate-limits.config.ts --reporter=json > ../.artifacts/b05/rate-limits.json
|
||||
node node_modules/@playwright/test/cli.js test --config=playwright.pdf.config.ts --reporter=json > ../.artifacts/b05/pdf-admission.json
|
||||
$env:NEXT_PUBLIC_API_BASE_URL='/api'
|
||||
node --max-old-space-size=4096 node_modules/next/dist/bin/next build
|
||||
node node_modules/typescript/bin/tsc --noEmit --pretty false
|
||||
```
|
||||
|
||||
The full browser run built a fresh production-static UI for the synthetic `localhost:5302` API, using the existing offline fixture with external integrations/workers disabled. The later `/api` production build replaced that test-targeted output; neither artifact was deployed. Existing local packages and Chromium were used. Temporary fixture data is disposable synthetic data, not a production migration. Local raw logs/TRX/JSON remain under `.artifacts/b05`; the stable evidence includes hashes and safe generated CSV samples.
|
||||
|
||||
## Files changed for B05
|
||||
|
||||
- Application: `JobTrackerApi/Controllers/ExportController.cs`, new `JobTrackerApi/Services/CsvCellSafety.cs`, `job-tracker-ui/src/components/ImportExportJobs.tsx`.
|
||||
- Permanent tests: new `JobTrackerApi.Tests/CsvExportTests.cs`, `job-tracker-ui/src/csv-export.test.tsx`, `job-tracker-ui/e2e/csv-export.spec.ts`.
|
||||
- Documentation: this completion report, validation README/queue/evidence, `docs/todo/work.md`, and `evidence/b05-*` samples, Excel observations/probe and results manifest. The original audit evidence is not rewritten. Other uncommitted B01–B04/audit work and `docs/redesign` predate B05; the entire working-tree diff is not B05.
|
||||
|
||||
## Self-review, residual scope and acceptance
|
||||
|
||||
Every one of the 13 exported text columns calls the same policy before the unchanged CSV escaping. Embedded delimiters cannot create new cells after independent parsing. Prefix placement precedes controls, and applying the helper twice does not add another prefix. The query remains `AsNoTracking`; neither entity values nor JSON are changed. The four typed columns deliberately bypass text neutralization and retain prior formatting. The browser download carries server bytes without decoding. Authentication, ownership filters, middleware, HTTP imports and PDF admission were not modified.
|
||||
|
||||
No schema or rollback transformation is required. Reverting these export changes would reopen formula interpretation and the observed Excel encoding problem. New text columns or future CSV exporters must use this boundary; future typed columns should retain their typed formatting. B05 does not solve CSV size/resource limits, raw JSON spreadsheet conversion, or spreadsheet-specific reprocessing of exported files.
|
||||
|
||||
**Acceptance: B05 satisfies its eight criteria locally under the defined policy, with the pre-existing R01a standalone TypeScript failure explicitly remaining open.** The original arithmetic formula no longer evaluates in the verified Excel workflow; permanent tests establish every CSV text field and leading-character policy, unchanged storage/JSON/typed formats, standard CSV syntax and Unicode preservation. The browser/HTTP regression proves real headers and identical downloaded bytes. Broader tests and release builds pass; compatibility limits above do not imply untested cross-application guarantees. No later ticket or deployment is authorized by this completion.
|
||||
|
||||
**Recommended next ticket: B06 — verify and close ingress bypass path. Not started.** Production topology inspection/change still needs its separately scoped authorization and deployment evidence.
|
||||
@@ -0,0 +1,310 @@
|
||||
# B06 — Ingress boundary implementation and verification record
|
||||
|
||||
**Latest runtime follow-up:** [2026-09-09 observations and remaining gates](#runtime-verification-follow-up-2026-09-09). Public DNS/TLS and bounded direct-access observations are now available. Docker still cannot start; real container/Traefik verification remains blocked. **B06 implementation complete, but F04 remains open.** The initial implementation-pass record below is preserved chronologically; statements about no public requests/DNS/TLS apply to that earlier pass.
|
||||
|
||||
Date: **2026-09-09**. **Repository/application changes implemented; full local acceptance and production verification are NOT complete.** B01–B05 regression coverage passes. B07 was not started. No deployment, host firewall change, schema change, external scan, image pull or Docker reset was performed.
|
||||
|
||||
Two gates remain: (1) execute the real nginx/container topology tests with a working Docker engine and the intended image; (2) establish the deployed Traefik socket peers, forwarding configuration and host/provider network controls. **F04 remains open, conditional P1.** This file is a completion record for work performed, not a claim that the remaining gates passed.
|
||||
|
||||
## Finding and independently established boundary
|
||||
|
||||
The original [F04 validation](README.md#f04--forwarded-headers-and-direct-ingress) and [B04 deferral](b04-completion.md) remain historical evidence. The vulnerability is **not a production API host-port mapping**: rendered base Compose has no backend published port. It publishes frontend/nginx **host 3000 → container 80**, without a host-IP restriction. Nginx accepts the canonical Host and previously passed arbitrary incoming `X-Forwarded-For` / `X-Forwarded-Proto` to the API. The API legitimately trusts nginx's dedicated network, so bypassing the external sanitizing proxy could launder attacker-supplied headers through a trusted peer. A canonical Host is not proof of a trusted caller.
|
||||
|
||||
Before changing application/configuration code, `docker compose --env-file .artifacts/b06/compose.env -f docker-compose.yml --profile bundled-ollama config --format json` confirmed this exposure using synthetic values. The new permanent effective-config test failed because production had no upstream-peer enforcement setting. Source tracing confirmed verbatim forwarding in all three nginx proxy locations. **No pre-fix live nginx exploit was reproduced in this environment**, because the Docker engine was unavailable. The optional `--baseline` topology probe can test HEAD's original nginx sources before these edits are committed. It is not a substitute for an immutable baseline after committing; original source excerpts/observations remain in the audit.
|
||||
|
||||
Deployment evidence conflicts: `docs/architecture/deployment.md` asserted a firewall blocked port 3000; `deploy/README.md` claimed no frontend host mapping and container-direct Traefik routing; `sec-002-ingress-compose.md` retained a host-port compatibility dependency while also listing a contradictory “production ports absent” check. Current rendered configuration and deployment scripts take precedence. These claims are now corrected in active documentation. The old verification record is retained as historical evidence, not current proof.
|
||||
|
||||
## Production-intended topology and actual configuration evidence
|
||||
|
||||
```text
|
||||
Internet → DNS/provider network/host firewall [unverified]
|
||||
→ operator-owned Traefik [TLS termination/routing/config unverified]
|
||||
→ Docker host TCP 3000 [current compatibility mapping]
|
||||
→ frontend nginx TCP 80 [new explicit socket-peer allowlist]
|
||||
→ dedicated internal web_proxy network
|
||||
→ ASP.NET API TCP 8080 [unpublished]
|
||||
```
|
||||
|
||||
The repository establishes the expected contract, **not the observed current production topology**. No Traefik configuration, live `.env`, DNS, firewall rules or infrastructure credentials were available/used. A request for the real Traefik placement/observed peer addresses remains unanswered; “continue” was not treated as a network value or production approval.
|
||||
|
||||
| Surface | Rendered production configuration | After B06 | Evidence level |
|
||||
|---|---|---|---|
|
||||
| Frontend nginx | TCP 3000 → 80, host IP unspecified; Docker normally publishes broadly, platform IPv6 behavior must be checked | Mapping preserved; application requests require configured Traefik socket peers, single client IP and HTTPS metadata | Mapping proven by Compose; nginx enforcement source/generation checked, runtime pending |
|
||||
| API | `expose: 8080`, no `ports`; listens on container interfaces | Same exposure; explicit `ASPNETCORE_ENVIRONMENT=Production` | Proven in rendered configuration |
|
||||
| `web_proxy` | Dedicated bridge, `internal: true`, configured `WEB_PROXY_SUBNET` (synthetic/default /29) | Only frontend/backend members permitted by preflight; API known network must match exactly | Proven in configuration; live membership pending |
|
||||
| `jobtracker_shared` | External network; frontend and backend both join | Preserved for existing operator-managed service dependencies | Membership configured; actual other members/access need host inspection |
|
||||
| Backend `default` / `ai_internal` | Backend also joins both; ai-service only `ai_internal` | Unchanged | Normal bridge networks are not air gaps or protection from a Docker administrator |
|
||||
| AI 8001 / optional Ollama 11434 | No host mappings, including `bundled-ollama` profile | Unchanged; new preflight rejects publications | Proven in rendered configuration |
|
||||
| Database | External MariaDB/MySQL or volume-backed SQLite; no DB service/port in this Compose | Unchanged | External DB listener/network requires operator verification |
|
||||
| PDF renderer | API child process, no separate listening service | B03 admission unchanged | Source and regression tests |
|
||||
| Development | Explicit `-f docker-compose.dev.yml`, frontend 3000, API 5202, optional Ollama 11434 | Explicit direct nginx mode, HTTP localhost, API forwarding trust disabled | Rendered and tested separately; cannot pass production preflight |
|
||||
|
||||
Frontend and API are intended to share the canonical HTTPS origin; UI API base is `/api`. Preflight rejects a different production build-time API URL. Public 80/443 handling belongs to operator-owned Traefik and cannot be proved here. No cache/debug/control service is published by base Compose. `expose` is metadata, not a firewall. Host processes, attached containers, Docker administrators and routed/direct-container-IP configurations remain separate trust considerations.
|
||||
|
||||
## Implementation and trust rules
|
||||
|
||||
1. **Nginx original socket peer:** `INGRESS_MODE=proxy` is explicit in production. `INGRESS_PROXY_CIDRS` must list observed Traefik peers; there is no permissive default. The generated `geo $realip_remote_addr` gate checks the original socket peer, before allowing application locations. Untrusted peers receive **403**, including callers that know the canonical Host and forge every forwarding header.
|
||||
2. **Normalized forwarding:** nginx `set_real_ip_from` uses that same explicit list; `real_ip_header X-Forwarded-For`, `real_ip_recursive off`. It sends `$remote_addr` as one normalized XFF value, canonical Host/port and canonical HTTPS scheme. It strips `X-Forwarded-Host` and RFC `Forwarded`; raw headers do not select an identity. All `/api/`, canonical `/health` and `/ready` proxy locations use this rule. Authentication/B02 headers pass through normally and retain their existing server checks.
|
||||
3. **Malformed/extra metadata:** missing XFF, missing/nonexact `https`, or comma-separated XFF chains return **400** at nginx in proxy mode. An invalid single IP makes nginx realip retain the actual peer; only that peer's address is emitted, not the invalid supplied text. This safely charges the proxy peer rather than manufacturing a client partition; it also signals a broken upstream contract. This nginx fallback behavior still needs the runtime probe. ASP.NET's independent malformed/rightmost-hop behavior is tested and proven. Traefik must overwrite attacker headers, not append arbitrary chains.
|
||||
4. **Development:** `INGRESS_MODE=direct` is permitted only with an HTTP canonical origin. It does not enable realip or trust incoming forwarding metadata; nginx forwards the actual socket IP and HTTP scheme. Production preflight rejects direct mode/Development environment. Local API-port convenience remains unchanged.
|
||||
5. **Fail-safe configuration:** nginx rejects empty, broad/default-route or directive-shaped peer inputs and calls `nginx -t` before serving. The standard-library Python preflight validates the actual Compose JSON, requires narrow peer networks (private /24 or narrower IPv4; /64 or narrower IPv6; individual public proxy addresses), exact dedicated backend trust, same-origin HTTPS, Production environment, no host networking and no additional service publications. It rejects `DOTNET_ENVIRONMENT=Development` and automatic framework-forwarding enablement. Shell startup enforces CIDR characters/prefix bounds; nginx itself validates address syntax. Prefer individual /32 or /128 peers rather than trusting other containers in a subnet.
|
||||
6. **API defence:** `ForwardedProxyConfiguration` still clears implicit known proxies/networks, trusts one configured hop, processes XFF/XFP only, and now rejects `/0`. Untrusted sockets—including mapped IPv4—cannot change effective IP/scheme. Existing `ExternalOrigin` still rejects noncanonical application Hosts in Production, allows only internal liveness exceptions, generates reset/verification/OAuth links from configuration and controls secure cookies. Forwarded Host is never enabled. `AllowedHosts=*` remains as a framework setting; the existing explicit canonical-host middleware supplies the actual Production host restriction.
|
||||
7. **CORS correction:** Production previously defaulted to `http://localhost:3000` when no allowlist existed. It now defaults to the configured canonical origin; conflicting overrides/wildcards fail before listening. Credentialed requests remain allowed only for that origin. Development's configured allowlists remain usable. This is not a network-access control.
|
||||
|
||||
B04 middleware ordering is unchanged: canonical host/proxy handling → existing CSRF → authentication → endpoint authorization → rate limiter → B02 session context → controller. Owner quotas remain principal-based; anonymous/security quotas use the effective socket/proxy client IP. B03 admission is still inside the shared exporter, independent of ingress or endpoint. B01 outbound transport and B05 CSV behavior were not changed.
|
||||
|
||||
The host-port mapping is deliberately retained because the repository records a live dependency and does not reveal whether Traefik is host-local, containerized or remote. Blind loopback binding/removal could cause downtime. The new nginx peer gate is the repository's application-level closure for that compatibility listener. **If Docker/NAT makes trusted and untrusted traffic appear as the same peer, this gate is insufficient: do not allowlist that shared gateway.** Establish a verified binding/firewall or dedicated proxy route before rollout. This is a production stop condition, not a claim that a guessed CIDR fixes the host.
|
||||
|
||||
## Headers, limits and health
|
||||
|
||||
Nginx explicitly supplies `X-Content-Type-Options: nosniff`, `X-Frame-Options: DENY`, `Referrer-Policy: strict-origin-when-cross-origin` and restrictive camera/microphone/geolocation permissions on canonical responses. No new duplicate headers were added. Base API HTTPS redirection/HSTS are disabled because the external proxy owns TLS/redirects; actual Traefik HSTS/redirect/header policy is unverified. No proxy request-body or timeout tuning is configured in this nginx template: nginx defaults apply. B06 does not claim a special rate/WAF/body policy at Traefik. Existing application size/time limits and B03 admission remain; tuning broader limits is separate work.
|
||||
|
||||
Container health requires no host API publication: backend curls `127.0.0.1:8080/health`; frontend checks `127.0.0.1:80/health`. The nginx default-host `/health` returns only static liveness and remains accessible even to untrusted peers. Canonical `/health` still proxies backend liveness; `/ready` remains canonical/proxy-gated. Default-host application paths are rejected. Health checks do not prove that the new trusted-proxy route is configured correctly, so the route smoke below is mandatory.
|
||||
|
||||
## Permanent tests and local results
|
||||
|
||||
| Check | Result / exact scope |
|
||||
|---|---|
|
||||
| `scripts/test-ingress.py` | **3 tests pass**, including real production/dev Compose rendering and 17 unsafe-config mutations; no daemon needed. Before config edits, safe-production test failed as expected. |
|
||||
| `ForwardedIngressTests` plus existing origin/rate-key tests | **30 focused pass**; 13 new backend cases cover trusted/untrusted/mapped/IPv6 peers, scheme, host, malformed/rightmost chains and trust-all rejection. |
|
||||
| `ingress-tests/production.spec.ts` | **6 real HTTP scenarios pass**: Production CORS/default/unsafe startup, Host/XFH, secure-cookie scheme independence, untrusted IP-limit forgery, trusted clients and excess-chain defence. |
|
||||
| Broader backend | **915 pass**, no selected skips; includes B01–B05. One pre-existing external-DNS IMAP test excluded because internet access was not authorized. |
|
||||
| Frontend | **298 tests / 67 suites pass**, including nginx-health, B02/B03 and B05 download regressions. |
|
||||
| Browser | **18 pass**, no failures/flaky outcomes/skips; includes B02 switching and B05 actual download. |
|
||||
| B04 HTTP / B03 admission probes | **9 / 1 pass** unchanged. |
|
||||
| Build/static checks | API/test-project Release warnings-as-errors: **0 warnings/errors**. Next production build and targeted lint pass. Shell syntax checks, Python compilation and `git diff --check` pass. |
|
||||
| Standalone TypeScript | Existing unrelated `src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.` remains; exit **1**. No suppression/fix added. |
|
||||
| Entrypoint generation | **9 local guard/render cases pass**, seven rejected configurations plus production and development generation. Temporary probe substitutes only fixture paths and disables `nginx -t`; explicitly **not nginx runtime proof**. |
|
||||
| Real nginx/container topology | **BLOCKED before resource creation**. Docker Desktop engine pipe absent; attempted startup crashed on inaccessible `dockerInference` socket. No factory reset/repair, image pull or production request attempted. |
|
||||
|
||||
The first native Production fixture run passed 2/4 but inherited the project's Development launch profile despite its environment setting. The fixture now uses `dotnet run --no-launch-profile`; all six final tests prove Production behavior. This was a fixture correction, not a relaxation of host/CORS assertions. Final results/hashes: [manifest](evidence/b06-test-results.json). Safe effective topology/guard observations: [observations](evidence/b06-observations.json). Raw local files remain in `.artifacts/b06`.
|
||||
|
||||
The permanent `scripts/test-ingress-topology.py` uses only an already-installed nginx image, synthetic data, unique containers/networks and loopback-only ephemeral host publication. It exercises this repository's real nginx template/entrypoint and a header-echo backend, plus a simulated trusted proxy peer. It does **not** claim to run production Traefik or a containerized full API. API behavior is covered separately by the real native HTTP tests. The topology test asserts trusted IPv4/IPv6 forwarding, two clients, host/header normalization, unchanged B02 header transport, untrusted direct nginx denial, missing/bad scheme and chain rejection, safe malformed-IP fallback, no backend publication and denied edge-to-backend routing. It cleans up only resources it created. Its runtime result is still pending.
|
||||
|
||||
### Commands run
|
||||
|
||||
Repository root (PowerShell):
|
||||
|
||||
```powershell
|
||||
python scripts/test-ingress.py
|
||||
docker compose --env-file .artifacts/b06/compose.env -f docker-compose.yml --profile bundled-ollama config --format json > .artifacts/b06/compose-after.json
|
||||
Get-Content .artifacts/b06/compose-after.json -Raw | python scripts/validate-ingress.py
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~ForwardedIngressTests|FullyQualifiedName~ExternalOriginTests|FullyQualifiedName~RateLimitPartitionKeysTests' --logger 'trx;LogFileName=focused.trx' --results-directory .artifacts/b06 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --no-build --filter 'FullyQualifiedName!~ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail' --logger 'trx;LogFileName=backend.trx' --results-directory .artifacts/b06 -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore -warnaserror
|
||||
& C:/Users/Connor/.cache/codex-runtimes/codex-primary-runtime/dependencies/native/git/usr/bin/sh.exe -n deploy/deploy.sh
|
||||
& C:/Users/Connor/.cache/codex-runtimes/codex-primary-runtime/dependencies/native/git/usr/bin/sh.exe -n job-tracker-ui/configure-nginx-origin.sh
|
||||
python -m py_compile scripts/validate-ingress.py scripts/test-ingress.py scripts/test-ingress-topology.py
|
||||
python .artifacts/b06/render-probe.py
|
||||
python scripts/test-ingress-topology.py # blocked: Docker engine unavailable
|
||||
git diff --check
|
||||
```
|
||||
|
||||
UI directory; API suites ran serially because all use port 5302:
|
||||
|
||||
```powershell
|
||||
$env:DOTNET_HOST_PATH='C:/Users/Connor/.dotnet/dotnet.exe'
|
||||
$env:NEXT_TELEMETRY_DISABLED='1'
|
||||
node node_modules/@playwright/test/cli.js test --config=playwright.ingress.config.ts --reporter=json > ../.artifacts/b06/ingress-api.json
|
||||
node node_modules/@playwright/test/cli.js test --config=playwright.rate-limits.config.ts --reporter=json > ../.artifacts/b06/rate-limits.json
|
||||
node node_modules/@playwright/test/cli.js test --config=playwright.pdf.config.ts --reporter=json > ../.artifacts/b06/pdf-admission.json
|
||||
node node_modules/jest/bin/jest.js --runInBand --watchAll=false --json --outputFile=../.artifacts/b06/frontend.json
|
||||
$env:PLAYWRIGHT_STATIC_EXPORT='true'
|
||||
$env:PLAYWRIGHT_PREBUILT_EXPORT='false'
|
||||
node node_modules/@playwright/test/cli.js test --config=../.artifacts/b06/playwright.offline.config.ts --reporter=json > ../.artifacts/b06/browser.json
|
||||
$env:NEXT_PUBLIC_API_BASE_URL='/api'
|
||||
node --max-old-space-size=4096 node_modules/next/dist/bin/next build
|
||||
node node_modules/eslint/bin/eslint.js playwright.ingress.config.ts ingress-tests/production.spec.ts --max-warnings=0
|
||||
node node_modules/typescript/bin/tsc --noEmit --pretty false
|
||||
```
|
||||
|
||||
The offline browser configuration was copied from the existing B02 fixture; external providers/workers were disabled. Its fresh test-targeted static build was subsequently replaced by the normal `/api` build. Neither was deployed.
|
||||
|
||||
## Required deployment verification — exact safe procedure
|
||||
|
||||
Run the container probe first on a **safe development/test Docker engine**, not the production host:
|
||||
|
||||
```bash
|
||||
python3 scripts/test-ingress.py
|
||||
python3 scripts/test-ingress-topology.py --image nginx:1.29.8-alpine
|
||||
# Image must already exist. The script never pulls it. Arrange any image acquisition separately.
|
||||
```
|
||||
|
||||
On the documented Linux deployment host, use the existing checkout `/opt/job-tracker/app` and shared configuration `/opt/job-tracker/shared/.env`. The following commands are **read-only**; do not run `deploy.sh`, `up`, firewall mutation or restore commands as part of this inspection. Review outputs locally; never paste complete rendered environment variables or container environment/secrets.
|
||||
|
||||
```bash
|
||||
cd /opt/job-tracker/app
|
||||
docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml --profile bundled-ollama config --format json | python3 scripts/validate-ingress.py
|
||||
docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml ps
|
||||
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
|
||||
sudo ss -lntp
|
||||
if command -v nft >/dev/null; then sudo nft list ruleset; fi
|
||||
if command -v iptables-save >/dev/null; then sudo iptables-save; fi
|
||||
if command -v ip6tables-save >/dev/null; then sudo ip6tables-save; fi
|
||||
if command -v ufw >/dev/null; then sudo ufw status verbose; fi
|
||||
frontend_id=$(docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml ps -q frontend)
|
||||
backend_id=$(docker compose --env-file /opt/job-tracker/shared/.env -f docker-compose.yml ps -q backend)
|
||||
test -n "$frontend_id" && test -n "$backend_id"
|
||||
docker inspect -f '{{json .NetworkSettings.Ports}} {{json .NetworkSettings.Networks}}' "$frontend_id" "$backend_id"
|
||||
docker network inspect jobtracker_shared --format '{{json .IPAM.Config}} {{json .Containers}}'
|
||||
docker network ls --filter label=com.docker.compose.network=web_proxy
|
||||
docker exec "$frontend_id" nginx -T
|
||||
docker exec "$backend_id" curl -fsS http://127.0.0.1:8080/health
|
||||
docker exec "$frontend_id" wget -qO- http://127.0.0.1/health
|
||||
```
|
||||
|
||||
Identify the actual Traefik service/process from `docker ps` / `ss`. If containerized, inspect its **mount paths and networks**, then inspect the identified configuration files locally; do not assume a filename or container name. If host-managed, inspect the identified systemd/service configuration. Confirm exact Host rule, TLS certificate/entrypoint, upstream host:3000 target, redirect behavior and whether `forwardedHeaders.insecure`/trusted upstream settings permit client forgery. Determine any real CDN/LB hop separately; it is not present in this repository. Inspect actual provider firewall/security-group settings as well as Docker's NAT/FORWARD rules; a UFW INPUT rule alone is not proof for Docker-published ports.
|
||||
|
||||
Obtain the **actual socket source seen at nginx**, not a guessed Docker subnet. Where host tools permit, a bounded SYN-only capture in nginx's network namespace avoids capturing HTTP bodies/credentials:
|
||||
|
||||
```bash
|
||||
frontend_pid=$(docker inspect -f '{{.State.Pid}}' "$frontend_id")
|
||||
test "$frontend_pid" -gt 0
|
||||
sudo nsenter -t "$frontend_pid" -n tcpdump -nn -i any -c 10 'tcp dst port 80 and (tcp[tcpflags] & tcp-syn != 0)'
|
||||
```
|
||||
|
||||
Use that only while issuing explicitly controlled health/config requests through Traefik and from an untrusted test client. If `nsenter`/`tcpdump` is unavailable, use the operator's equivalent packet/flow evidence; do not install tools or change production networking implicitly. If both routes share the same observed NAT source, **stop**. Choose a topology-specific binding/firewall or dedicated proxy route with an explicit rollback before allowing that peer. Configure `INGRESS_PROXY_CIDRS` only after this evidence; a wrong list will deny legitimate ingress. Normal deployment preflight fails before service replacement if the setting is absent.
|
||||
|
||||
After the change is separately approved and deployed, from an independently controlled **external test machine**, set values from verified DNS/host inventory, not guesses:
|
||||
|
||||
```bash
|
||||
public_host='the-verified-canonical-host'
|
||||
public_ip='the-verified-host-address'
|
||||
curl --max-time 10 -sS -D - -o /dev/null "https://$public_host/api/auth/config"
|
||||
curl --max-time 10 -sS -D - -o /dev/null "https://$public_host/api/auth/config" \
|
||||
-H 'X-Forwarded-For: 198.51.100.10' -H 'X-Forwarded-Proto: http' \
|
||||
-H 'X-Forwarded-Host: attacker.example.test'
|
||||
curl --max-time 5 -sS -D - -o /dev/null "http://$public_ip:3000/api/auth/config" \
|
||||
-H "Host: $public_host" -H 'X-Forwarded-For: 198.51.100.10' -H 'X-Forwarded-Proto: https'
|
||||
curl --max-time 5 -sS -o /dev/null "http://$public_ip:5202/health"
|
||||
curl --max-time 5 -sS -o /dev/null "http://$public_ip:8080/health"
|
||||
curl --max-time 10 -sS -D - -o /dev/null "http://$public_host/api/auth/config"
|
||||
```
|
||||
|
||||
Expected: canonical TLS path succeeds with appropriate headers; spoofing at the external edge is overwritten or rejected, not trusted; direct port-3000 application request is firewall-denied or nginx **403**, never successful; development/API listeners are unavailable unless a separately documented protected service owns that port; public HTTP redirects according to the verified Traefik policy. Repeat for every published A/AAAA address, with IPv6 bracket syntax and `curl -6`; do not infer IPv6 rules from IPv4 results. Verify certificate hostname/chain without `-k`. Match DNS to the inspected ingress. Do not probe unrelated services/ports or production login quotas. A config endpoint's 200 alone cannot prove client-IP integrity; inspect trusted forwarding/flow evidence or repeat the synthetic limiter checks in staging. Capture intended login/session/CSV/PDF smoke through the canonical route with a dedicated test account after normal deployment authorization.
|
||||
|
||||
## Files, residual risk and stop conditions
|
||||
|
||||
Application/config: `docker-compose.yml`, `docker-compose.dev.yml`, nginx template/entrypoint, `ForwardedProxyConfiguration.cs`, `Program.cs` CORS block, `.env.example`; preflight in `deploy/deploy.sh`, new `scripts/validate-ingress.py`, CI safety-script invocation. Tests: `ForwardedIngressTests.cs`, `scripts/test-ingress.py`, `scripts/test-ingress-topology.py`, `playwright.ingress.config.ts`, `ingress-tests/production.spec.ts`. Documentation: active deployment docs, this record, validation README/queue/evidence, TODO and B06 evidence files. Earlier uncommitted B01–B05 changes and unrelated `docs/redesign` remain untouched; the entire working-tree diff is not B06.
|
||||
|
||||
No schema or data migration. Backup-only automation does not require the new ingress gate. A bad peer list or unexpected forwarding chain can break the intended route; preflight/syntax/liveness checks alone cannot validate the peer value. Preserve existing images/env and verify both routes before rollout. Rolling back nginx to the old behavior reopens the header-laundering assumption unless network isolation is independently enforced; rollback therefore requires verified firewall/binding protection. Do not bypass the gate with trust-all networks.
|
||||
|
||||
Limits remain process-local after B04/B03; additional API instances multiply budgets as documented there. B06 introduces no distributed system or new hosting platform. Host/container compromise and shared-network lateral access are not eliminated by a peer allowlist. Privileged Docker users can attach containers regardless of service names. No claim is made about external DB/Ollama listeners, external TLS, firewall correctness, raw backend network reachability from other trusted containers, or an uninspected additional ingress route.
|
||||
|
||||
**Local acceptance:** criteria 2/4/6/7/8/9 have repository/API/test evidence; backend publication checks for criterion 1 pass, but the nginx closure supporting criteria 1/3/5 still requires real runtime/topology verification. **Do not mark B06 locally complete or F04 resolved yet. Production verification is also incomplete.** The next action is to complete these B06 gates and supply the observed production proxy peers. **Next queued implementation ticket: B07, only after the required B06 decision/verification and a further instruction; not started.**
|
||||
|
||||
## Runtime verification follow-up 2026-09-09
|
||||
|
||||
**Outcome: B06 implementation complete, but F04 remains open.** Implementation complete describes the existing application/configuration change; it does **not** mean full local-runtime acceptance or production verification passed. This follow-up changed evidence/documentation only. No application, Compose, nginx, B01–B05 behavior, credentials, DNS, certificate or production service was changed. B07 was not started.
|
||||
|
||||
The user explicitly authorized targeted public DNS/TLS/direct-access checks in this follow-up. These were bounded unauthenticated GETs to the documented hostname and its observed A address, not production load tests. No login budgets, user data or credentials were used. Response bodies and cookies were discarded; retained headers are allowlisted. [Runtime evidence](evidence/b06-runtime-observations.json), [test/evidence manifest](evidence/b06-runtime-results.json) and [reproducible public probe](evidence/b06-runtime-public-probe.py.txt) preserve commands and observations. Raw local files are under `.artifacts/b06-runtime/`.
|
||||
|
||||
### Actual execution and listener evidence
|
||||
|
||||
The real production Compose file was rendered again, including the optional Ollama profile, with the existing synthetic `.artifacts/b06/compose.env`. The preflight passed. [Sanitized effective configuration](evidence/b06-runtime-compose.json) confirms frontend TCP **3000 → 80**, host address unspecified, and **zero published backend/AI/Ollama ports**. The internal `web_proxy` network contains frontend/backend in configuration; backend also joins `shared_services`, `default` and `ai_internal`. The synthetic `192.0.2.10/32` ingress value is a test input, **not an observed Traefik peer or a recommended production value**.
|
||||
|
||||
The actual topology could not be started:
|
||||
|
||||
- `docker desktop status` returned exit 1, unable to retrieve status.
|
||||
- `docker desktop start --detach` returned exit 0 (startup dispatched), then Docker Desktop **4.86.0** reported the same fatal inference-manager socket failure supplied by Connor. This exit code is not engine readiness.
|
||||
- A server-version request timed out at 20 seconds; a final bounded request timed out at 10 seconds. Earlier missing-pipe evidence remains preserved.
|
||||
- The failing `C:/Users/Connor/AppData/Local/Docker/run/dockerInference` entry is zero bytes with `Archive, ReparsePoint` attributes, last modified 2026-08-24. Read-only `fsutil reparsepoint query` returned **Error 1920: The file cannot be accessed by the system**. This does not establish the underlying Docker/Windows defect or prove the entry is unused.
|
||||
- A graceful `docker desktop stop` attempt did not complete; the waiting command was interrupted. Failed Desktop/backend processes remained observable. No socket, Docker data, WSL distribution or configuration was deleted/renamed/reset, and no process was force-killed to improvise a repair.
|
||||
- The inspected **Windows workstation**, not the Ubuntu deployment host, had no listeners on 80, 443, 3000 or 5202. Its `127.0.0.1:8080` listener belonged to **steamwebhelper**, not Jobjakt. Windows firewall profiles were enabled; their default actions were `NotConfigured`. Neither observation establishes production firewall/NAT behavior.
|
||||
|
||||
Consequently there is **no new running-container evidence** for `nginx -t`, actual host bindings, network membership, trusted-peer matching, direct nginx rejection, internal API reachability, container restarts or missing/invalid-variable startup. No actual peer CIDR was guessed. The existing echo-backend/simulated-peer nginx probe would be supplementary only; it cannot satisfy this request for **real Traefik + real application + actual Compose** and was not represented as such.
|
||||
|
||||
### Observed public DNS, HTTPS and direct access
|
||||
|
||||
Measurements were taken around **2026-09-09 10:43–10:47 UTC** from Connor's Windows workstation (`192.168.50.2`). Requests used `curl --noproxy '*'`, TLS verification enabled, connect timeout 4 seconds, total timeout 8 seconds and at most three simultaneous harmless GETs. Independence from the production LAN/NAT is **not established**; these observations must be repeated from a known external network.
|
||||
|
||||
| Check | Observed result | What this proves / does not prove |
|
||||
|---|---|---|
|
||||
| Resolver plus both authoritative servers, `oaklyn.ns.cloudflare.com` and `thaddeus.ns.cloudflare.com` | `jobs.cesnimda.uk` CNAME → `cesnimda.uk`; A answer **109.189.21.248**; no AAAA answer (SOA/NODATA) | Consistent current answers for this hostname. No additional A/AAAA address was observed. Intended address ownership, other hostnames and complete provider-zone inventory remain operator checks. |
|
||||
| `https://jobs.cesnimda.uk/api/auth/config` | **200**, valid hostname/chain; TLS 1.3 independently verified with Python's default trust context | Public HTTPS application path works. It does not identify Traefik, nginx's peer, deployed image or backend client IP. |
|
||||
| Certificate | SAN/CN `jobs.cesnimda.uk`, Let's Encrypt YR2; valid **2026-08-16 20:35:58 UTC to 2026-11-14 20:35:57 UTC** | Hostname/chain validation succeeded without bypass flags. No separate revocation audit is claimed. |
|
||||
| HTTP port 80 | **301** to `https://jobs.cesnimda.uk:444/api/auth/config` | An actual redirect differs from the documented standard HTTPS origin. |
|
||||
| Observed redirect target, HTTPS port 444 | **Connection failure**, curl exit **7** | HTTP-to-HTTPS navigation is broken from this vantage point. Operator must inspect the real redirect entrypoint/NAT configuration and establish the intended reachable target. No certificate or production proxy change was attempted. |
|
||||
| HTTPS with forged XFF/XFP/XFH; HTTPS with malformed XFF chain | **200** in both cases | Availability only. A 200 cannot distinguish sanitized forwarding from a vulnerable acceptance path; backend effective identity is still unobserved. |
|
||||
| HTTPS with `Host: attacker.example.test`, original canonical TLS SNI | **421 Misdirected Request** | This forged Host request was rejected at the observed public boundary. It does not prove every internal/generated-URL path. |
|
||||
| Direct A-address TCP 3000, canonical Host, six cases: no forwarding, forged XFF, forged HTTPS, forged Host, combined headers, malformed chain | **6/6 connection failures**, curl exit **7**, approximately 2.0 seconds each | No HTTP request reached a listener from this vantage point. This is **not** evidence of nginx 403 or attribution to any specific firewall. |
|
||||
| Direct A-address TCP 5202 and 8080 `/health` | **2/2 connection failures**, curl exit **7** | Neither tested port was reachable here. No claim about other interfaces, IPv6 firewall policy, host-local/container access or unknown alternate ports. |
|
||||
|
||||
Public responses include HSTS, nosniff, `X-Frame-Options: SAMEORIGIN`, `Referrer-Policy: same-origin` and a Permissions Policy. These differ in part from the checked-in nginx template (`DENY`, `strict-origin-when-cross-origin`). An upstream override or a different deployed revision could explain this; the source cannot be attributed without operator configuration/image evidence. Public success therefore cannot certify that the current B06 working tree is deployed.
|
||||
|
||||
### Fresh automated checks and preserved broader results
|
||||
|
||||
| Check in this follow-up | Result | Evidence classification |
|
||||
|---|---|---|
|
||||
| Effective production/development Compose tests, including 17 hostile mutations | **3 passed**; preflight of newly rendered production JSON also exit 0 | Proven by automated test |
|
||||
| Forwarded ingress, canonical origin, B01 HTTP transport, B02 session middleware, B03 admission, B04 rate keys and B05 CSV | **166 passed**, 0 failed/skipped; Release compilation succeeded | Proven by automated test; API/native boundary, not live proxy chain |
|
||||
| Native Production ingress HTTP suite | **6 passed**, 0 unexpected/flaky/skipped | Proven by automated test, real native API |
|
||||
| Native B04 HTTP suite | **9 passed**, 0 unexpected/flaky/skipped | Proven by automated test, real native API |
|
||||
| B04 synthetic shared-IP evidence | In each exhaustion order, A **2 admitted / 1 rejected**, B **2 admitted / 1 rejected**; concurrent two-session case admits **4 total**, then each account rejects **2** attempts using other IPs; cookie/bearer share owner budgets | Does not prove real Traefik/nginx IP propagation. No production quotas were consumed. |
|
||||
| Existing broader verification, unchanged application source | Prior **915 backend / 298 frontend / 18 browser** passes, Release builds and targeted lint retained above; broader suites were not relabeled as rerun | Preserved prior automated evidence |
|
||||
| Standalone TypeScript | Prior unrelated `src/interview-prep.test.tsx(120,10): TS2571` remains the documented result; not rerun in this evidence-only follow-up | Existing limitation; no claimed fix |
|
||||
| Final evidence checks | `git diff --check` **exit 0** (Git line-ending notices only); probe Python syntax and all new evidence JSON parse checks pass | Evidence/documentation verification |
|
||||
|
||||
Exact fresh commands (repository root unless stated otherwise):
|
||||
|
||||
```powershell
|
||||
python scripts/test-ingress.py
|
||||
docker compose --env-file .artifacts/b06/compose.env -f docker-compose.yml --profile bundled-ollama config --format json
|
||||
# Output saved to .artifacts/b06-runtime/compose-effective.json; synthetic inputs only.
|
||||
Get-Content .artifacts/b06-runtime/compose-effective.json -Raw | python scripts/validate-ingress.py
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~ForwardedIngressTests|FullyQualifiedName~ExternalOriginTests|FullyQualifiedName~JobImportHttpTransportTests|FullyQualifiedName~SessionContextMiddlewareTests|FullyQualifiedName~PdfRenderAdmissionTests|FullyQualifiedName~RateLimitPartitionKeysTests|FullyQualifiedName~CsvExportTests' --logger 'trx;LogFileName=regressions.trx' --results-directory .artifacts/b06-runtime -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
# In job-tracker-ui, serially:
|
||||
$env:DOTNET_HOST_PATH='C:/Users/Connor/.dotnet/dotnet.exe'
|
||||
$env:NEXT_TELEMETRY_DISABLED='1'
|
||||
node node_modules/@playwright/test/cli.js test --config=playwright.ingress.config.ts --reporter=json
|
||||
node node_modules/@playwright/test/cli.js test --config=playwright.rate-limits.config.ts --reporter=json
|
||||
# Repository root:
|
||||
python .artifacts/b06-runtime/probe.py
|
||||
Resolve-DnsName jobs.cesnimda.uk -Type A -DnsOnly
|
||||
Resolve-DnsName jobs.cesnimda.uk -Type AAAA -DnsOnly
|
||||
Resolve-DnsName jobs.cesnimda.uk -Type CNAME -DnsOnly
|
||||
Resolve-DnsName cesnimda.uk -Type NS -DnsOnly -NoHostsFile
|
||||
# Repeat A/AAAA/CNAME using -Server for each observed authoritative nameserver.
|
||||
Get-NetTCPConnection -State Listen
|
||||
Get-NetFirewallProfile
|
||||
git diff --check
|
||||
```
|
||||
|
||||
### Operator handoff and evidence required for closure
|
||||
|
||||
Permitted deployment docs/scripts/CI configuration refer only to `PROD_HOST`, `PROD_USER` and `PROD_SSH_KEY` secret names. `C:/Users/Connor/.ssh/config` does **not exist**. No unambiguous SSH alias/username was found; no SSH connection was guessed, and no CI secrets, keys/passwords or live `.env` were retrieved. Production read-only inspection therefore remains an operator handoff under Connor's explicit instruction.
|
||||
|
||||
Run the earlier [exact host procedure](#required-deployment-verification--exact-safe-procedure) on the actual Ubuntu host. It uses the documented `/opt/job-tracker/app` checkout and `/opt/job-tracker/shared/.env`, prints validation results rather than secret environment data, inventories listeners/published ports/networks, checks real nginx configuration/internal health and obtains SYN-only peer evidence. Add these read-only checks:
|
||||
|
||||
```bash
|
||||
cd /opt/job-tracker/app
|
||||
git rev-parse HEAD
|
||||
git status --short
|
||||
docker inspect --format '{{.Name}} image={{.Image}} ports={{json .NetworkSettings.Ports}}' "$frontend_id" "$backend_id"
|
||||
# Use frontend_id/backend_id established by the earlier procedure.
|
||||
# Observe the marked request in the existing nginx access log without sharing unrelated requests:
|
||||
docker logs --since 2m "$frontend_id" 2>&1 | grep -F '/api/auth/config?b06_probe=runtime-check'
|
||||
```
|
||||
|
||||
While capturing the SYN peer, issue `curl --noproxy '*' --max-time 10 -sS -o /dev/null 'https://jobs.cesnimda.uk/api/auth/config?b06_probe=runtime-check'` from the controlled client. Compare the original socket source with nginx's generated `geo`/`set_real_ip_from` entries, and the access log's effective client address with the known test source. The access-log format must be inspected before interpreting fields. These alone do not prove the IP finally used by backend rate limiting; that requires the real-chain synthetic A/B scenarios on an isolated staging/test topology.
|
||||
|
||||
| Evidence to return, with secrets/cookies/user content removed | Safe result | Unsafe or unresolved result |
|
||||
|---|---|---|
|
||||
| Host/provider identity, deployed revision/image IDs, Compose preflight result and frontend/backend `Ports`/network subsets | Current intended build/configuration, only intended frontend host publication; API unpublished | Old/unknown build, dev overlay, host networking or extra API/public internal-service binding |
|
||||
| Relevant `ss`, Docker NAT/FORWARD, IPv4/IPv6 firewall and provider rule excerpts | Only documented public routes; direct app path blocked or explicitly peer-restricted | Rule comments alone, unknown IPv6 handling or externally reachable API |
|
||||
| Traefik entrypoint/router/upstream and forwarded-header directives only; generated nginx trust entries; marked SYN peer observation | Actual Traefik source matches narrow allowed peer; untrusted source differs; client forwarding sanitized | Trust-all, guessed subnet or shared NAT identity for trusted and untrusted paths — stop before permitting that peer |
|
||||
| Internal nginx/API health and controlled canonical-route result | Healthy internal API with working canonical ingress | Public API port required for health, internal 502 or legitimate caller rejected |
|
||||
| Direct nginx attempts from an actually untrusted reachable test network, plus spoof/malformed variants | 403/444 or independently explained network denial; no forged identity reaches backend | Successful application response from forbidden peer or caller-selected IP/scheme/host |
|
||||
| Fresh A/AAAA/provider inventory, certificate validation and HTTP redirect result from a known external network | Intended ingress address(es), valid TLS, HTTP target reachable at canonical HTTPS origin | Stale/alternate bypass record, invalid chain or the observed unreachable `:444` redirect |
|
||||
| Isolated real-chain A/B budgets, actual backend client-IP observations and B02 context checks | Each owner isolated; sessions share owner; spoofing cannot select IP budget; stale context rejected | Backend sees only proxy IP or client-forged IP; header/session selects another owner |
|
||||
|
||||
Return only selected sanitized excerpts and the timestamp/test vantage point. **Do not send complete `.env`, full `docker inspect`, authentication headers, tokens, passwords, private keys/certificates or unrelated access logs.** Do not run restart or missing/invalid-configuration tests against production as part of this read-only handoff. Those remain mandatory in the isolated real-container test environment after Docker is operational; record startup failure, restart health, peer/network stability and repeat direct-access rejection.
|
||||
|
||||
### Closure checklist by evidence level
|
||||
|
||||
1. **Proven by automated test:** effective production port/trust constraints, fail-closed preflight, native trusted/untrusted forwarding behavior, B04 principal/IP separation, B02 session guard, B01/B03/B05 focused regressions.
|
||||
2. **Proven in running local/container topology:** **none in this follow-up**. Docker failure prevents actual Compose startup, real nginx syntax/enforcement, actual Traefik peer capture, internal routing, restart/config-failure tests and real-chain A/B verification.
|
||||
3. **Proven against the deployed public endpoint from this workstation:** DNS observations, valid HTTPS/200, forged Host 421, direct-port connection failures from this vantage, HTTP redirect to unreachable 444. This is partial public-endpoint evidence, not certification of the internal deployed chain.
|
||||
4. **Still requires operator verification:** deployed build/topology/trust CIDRs, actual socket peers, backend client IP/scheme, real-chain session/account/IP behavior, host/container listeners and other network memberships, host/provider IPv4/IPv6 firewall/NAT, independent external reachability, intended DNS-address inventory and correct HTTP redirect target.
|
||||
|
||||
The blocking items are therefore the unavailable local Docker engine, absent verified production host access/evidence, missing real-chain peer/identity tests, and the observed HTTP redirect mismatch. **B06 implementation complete, but F04 remains open.** No change to F04's conditional severity or closure status is justified by connection failures alone.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Independent verification evidence
|
||||
|
||||
Application source baseline: `7d641edc7bca866ccf06012897ba1d107c07e59f`, Windows/Europe-Paris, 2026-09-07. Existing .NET SDK under `C:\Users\Connor\.dotnet`, Node, UI dependencies and Playwright Chromium were used. Application code was not changed. The original audit's working-tree docs were already present and have been preserved.
|
||||
|
||||
**Subsequent evidence, 2026-09-08:** [B01 implementation and regression results](b01-completion.md), with a separate [test manifest](evidence/b01-test-results.json). The original observations below and retained probes are unchanged historical reproductions. Re-running them against the B01 fix will no longer reproduce its mapped-address bypass.
|
||||
|
||||
**B02 subsequent evidence, 2026-09-08:** [Account/session isolation completion](b02-completion.md), [before/after observations](evidence/b02-observations.json) and [test manifest](evidence/b02-test-results.json). The permanent regression reproduced the original 200/B-profile overwrite before application edits; it now returns 409 with both profiles unchanged. API ownership, frontend session disposal and legitimate same-account controls pass. Original probes and observations remain preserved.
|
||||
|
||||
**B03 subsequent evidence, 2026-09-08:** [Shared PDF admission completion](b03-completion.md), [before/after observations](evidence/b03-observations.json) and [test manifest](evidence/b03-test-results.json). Before application edits, two real-exporter calls launched two held inert child processes. After the fix, authenticated multi-route traffic with limit two admitted two and rejected ten; actual concurrent renders never exceeded two, and later generation succeeded. Normal exceptions, cancellation and timeouts preserve capacity. All 17 browser tests pass, including real Chromium downloads. Failed intermediate fixture/browser runs and their diagnoses are recorded separately; original audit evidence is preserved. This is local admission proof, not R04 production sizing/containment evidence.
|
||||
|
||||
## Fresh results and limits
|
||||
|
||||
**B06 runtime follow-up, 2026-09-09:** [Report and operator handoff](b06-completion.md#runtime-verification-follow-up-2026-09-09). **Implementation complete; full runtime/production verification incomplete; F04 open.** Fresh 166 focused backend, 3 Compose, 6 Production HTTP and 9 B04 HTTP checks pass. Public DNS/valid HTTPS are now observed; direct ports 3000/5202/8080 are unreachable from this workstation, without proof of the rejecting layer. HTTP redirects to unreachable HTTPS port 444. Docker's inference socket failure still blocks actual Compose/Traefik/nginx tests; production peers, backend client IP and host/provider firewall remain unverified. No SSH alias/user was found in permitted non-secret sources. B01–B05 preserved; B07 not started.
|
||||
|
||||
B06 retained [observations](evidence/b06-observations.json) and [test manifest](evidence/b06-test-results.json) distinguish repository/API proof from unavailable container/production evidence. Original observations below are preserved.
|
||||
|
||||
B06 runtime follow-up: [public/Docker/DNS observations](evidence/b06-runtime-observations.json), [fresh tests and hashes](evidence/b06-runtime-results.json), [sanitized effective Compose](evidence/b06-runtime-compose.json) and [bounded public probe source](evidence/b06-runtime-public-probe.py.txt). Public endpoint observations do not certify the internal proxy chain; no running local/container topology was obtained.
|
||||
|
||||
**B05 subsequent evidence, 2026-09-09:** [CSV completion and policy](b05-completion.md), [test manifest](evidence/b05-test-results.json), [before Excel observation](evidence/b05-excel-before.json) and [after Excel observations](evidence/b05-excel-after.json). Before edits, the permanent regression failed and actual Excel evaluated the exported `=1+1` title to 2. Afterward it is literal apostrophe-prefixed text; every cell in the mixed fixture reports no formula, genuine decimals remain numeric and Norwegian text decodes correctly with the BOM. Twenty-five new backend cases, two frontend cases and one real HTTP/browser-download scenario pass. Broader 902 backend / 298 frontend / 18 browser tests, nine unchanged B04 HTTP scenarios and B03's unchanged admission probe pass; release builds/lint pass and existing R01a TS2571 remains. Historical observations below are preserved, including the original validation's absence of a spreadsheet-open test.
|
||||
|
||||
**B04 subsequent evidence, 2026-09-08:** [Account rate-limit completion and policy inventory](b04-completion.md), [before/after observations](evidence/b04-observations.json) and [test manifest](evidence/b04-test-results.json). Before edits, real API exports produced A 200/200 and B 429/429 on the same IP. After authentication-before-limiting, both accounts retain two permits; sessions, auth transport, client IDs and B02 headers cannot select another owner. Nine permanent HTTP scenarios cover concurrent/shared-IP, invalid credentials and trusted/untrusted proxies. Original audit evidence remains unchanged below.
|
||||
|
||||
| Check | Fresh result | Retained evidence / limit |
|
||||
|---|---|---|
|
||||
| Backend audit-focused subset | 70 passed, no failures/skips | `.artifacts/audit-validation/validation.trx`; covers billing/deletion/CV/ownership/calendar/import/queue/backup/public CV/rate keys/startup schema. |
|
||||
| Backend security/identity subset | 79 passed, no failures/skips | `.artifacts/audit-validation/security-validation.trx`; disjoint from the 70-test set. Includes auth posture/session revocation/email ownership/job authorization/Pro entitlement/AI privacy/IMAP guards/local identity/migration chain. |
|
||||
| Interview Jest suite | 13 passed, one suite | `.artifacts/audit-validation/interview-jest.json`. Babel/Jest execution does not type-check the TypeScript program. |
|
||||
| Standalone TypeScript | Failed: TS2571 at test line 120 | Exact diagnostic in [test manifest](evidence/test-results.json); no fix applied. |
|
||||
| Independent HTTP/browser harness | Completed with observations; defects intentionally recorded | [observations.json](evidence/observations.json). Synthetic API on 5402 and UI on 3400; last retained run has no `probe failure`. |
|
||||
| Independent .NET boundary harness | Completed with observations | [boundary-results.json](evidence/boundary-results.json). Fake HTTP transport, real EF SQLite contexts, actual exporter with inert child processes, real hosted worker with a synthetic one-shot command fault. |
|
||||
|
||||
The [test manifest](evidence/test-results.json) retains counts, timestamps and SHA-256 hashes of the local test outputs. Large TRX/SQL/API logs remain ignored under `.artifacts`; the stable observations are copied into this validation directory. They contain synthetic data only. The original full 753-backend/279-frontend/13-browser/37-parser results and production build remain separately attributed to [the original verification](../verification.md). No claim that those full suites ran again is made.
|
||||
|
||||
The static UI export was built during the original audit against the same unchanged frontend source, with `http://localhost:5402/api`. The independent pass exercised that artifact and inspected the current source. It did not produce a new production-configured frontend build. The local export must not be used as a production release artifact; normal release configuration/build gates still apply.
|
||||
|
||||
## Key observations
|
||||
|
||||
| Probe | Observed | What it does not establish |
|
||||
|---|---|---|
|
||||
| Stale HTTP CV save | Revision 1 request accepted after a newer save; stale name persisted with 200. | Exhaustive job/profile mutation coverage. |
|
||||
| Two preloaded SQLite contexts | Writer B wins, current revision 2, history `[1,2,2]`. | MariaDB concurrency behavior or a safe duplicate-history migration. |
|
||||
| Wrong-owner document access | Variant and preview 404; PDF request 404; versions 200 with `[]`. | Every document endpoint/platform combination; no data is leaked by this empty list. |
|
||||
| Two job creations, same header | Two different job IDs despite the same `Idempotency-Key`. | A promised API key contract, automatic client retry, or a reason to prohibit legitimate repeat applications. |
|
||||
| CSV | Harmless arithmetic text exported unneutralized. | Spreadsheet command execution or exfiltration. No spreadsheet opened. |
|
||||
| Account export rate | Account A 200/200, first B request 429 from the same IP. | All policies' deployed proxy/IP behavior. |
|
||||
| Account switch | Actual sign-out/login in second tab; first tab saves A draft into B with 200; independent API confirms B changed/A unchanged. | Server-side direct-object-reference bypass. The cookie principal is B at save time. |
|
||||
| Calendar controls | Non-UTC offsets lost after SQLite reload; UTC 07:00Z preserved; date-only becomes timed midnightZ. | Production timezone/provider configuration or a promised timed scheduling product. |
|
||||
| Import address normalization | Ordinary private/link-local IPv4 blocked; mapped representations invoke the fake HTTP handler. | Real metadata/private-host access. The fake opens no socket. |
|
||||
| PDF exporter | Three real exporter calls start three inert child processes before any is released. | Host exhaustion threshold, browser exploit or safe production renderer capacity. |
|
||||
| AI worker, concurrency 1 | One DB timeout faults worker and stops a real host. | HTTP endpoint behavior during a production DB outage. |
|
||||
| AI worker, concurrency 2 | One loop fails; host/combined task remain running during the observation window. | Long-term recovery. Source `Task.WhenAll` explains why the surviving indefinite loop masks completion. |
|
||||
| Builder, loaded content | Light/dark contrast failures at desktop/mobile; scroll violation after mobile→desktop transition. | Full manual accessibility or every page/locale/zoom state. |
|
||||
|
||||
For the scroll finding, the fresh offending element was a DIV, `tabIndex=-1`, `overflow=auto`, one preview page/iframe, `scrollHeight=868`; its client height was 860 at viewport height 1000 and 774 at height 900. This maps to the preview region with `maxHeight: 86vh`, not an arbitrary editor input. The test transition matters: initial desktop samples did not all reproduce it. The final fresh run covers EN, light/dark, 1440×1000, 1440×900 and 375×900. Bokmål evidence in the original audit remains historical; the independent pass did not repeat a full EN/NB matrix.
|
||||
|
||||
## Reproduction commands
|
||||
|
||||
Use a development checkout and existing local dependencies. These commands do not authorize external dependency downloads, production operations or modifications to a user's existing database. The HTTP/browser fixture refuses an already-answering test port; it creates a unique temp data root, starts only its own child services, and stops its own process trees in `finally`. It does not delete existing development data. Do not run the original and independent browser harnesses concurrently because both use 5402/3400.
|
||||
|
||||
Repository root, PowerShell:
|
||||
|
||||
```powershell
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~BillingControllerTests|FullyQualifiedName~AccountDeletionTests|FullyQualifiedName~CvBuilderTests|FullyQualifiedName~OwnershipGuardTests|FullyQualifiedName~ApplicationCalendarControllerTests|FullyQualifiedName~JobImportServiceTests|FullyQualifiedName~AiOperationQueueTests|FullyQualifiedName~DatabaseBackupRunnerTests|FullyQualifiedName~PublicCvControllerTests|FullyQualifiedName~RateLimitPartitionKeysTests|FullyQualifiedName~StartupSchemaOwnershipTests' --logger 'trx;LogFileName=validation.trx' --results-directory .artifacts/audit-validation -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe test JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore --filter 'FullyQualifiedName~ApiAuthorizationPostureTests|FullyQualifiedName~AuthSessionRevocationTests|FullyQualifiedName~EmailOwnershipIntegrationTests|FullyQualifiedName~JobApplicationsAuthorizationTests|FullyQualifiedName~ProEntitlementAuthorizationTests|FullyQualifiedName~AiPrivacyPolicyTests|FullyQualifiedName~ImapServiceSsrfGuardTests|FullyQualifiedName~LocalAuthIdentityTests|FullyQualifiedName~MigrationChainTests' --logger 'trx;LogFileName=security-validation.trx' --results-directory .artifacts/audit-validation -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
|
||||
node docs/audits/2026-09-07/validation/probes.mjs
|
||||
```
|
||||
|
||||
UI directory, PowerShell:
|
||||
|
||||
```powershell
|
||||
node node_modules/typescript/bin/tsc --noEmit --pretty false
|
||||
node node_modules/jest/bin/jest.js src/interview-prep.test.tsx --runInBand --watchAll=false --json --outputFile=../.artifacts/audit-validation/interview-jest.json
|
||||
```
|
||||
|
||||
The retained [C# probe](BoundaryProbes.cs) is built through an ignored scratch project, so it is not part of the application/test product build. The [project template](probe-project.xml) uses paths relative to the scratch directory below. Restore used only the existing package cache and an empty local source, with NuGet advisory network lookup disabled; no new package was downloaded. From the root:
|
||||
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force .artifacts/audit-validation/probe | Out-Null
|
||||
New-Item -ItemType Directory -Force .artifacts/audit-validation/empty-feed | Out-Null
|
||||
Copy-Item -LiteralPath docs/audits/2026-09-07/validation/probe-project.xml -Destination .artifacts/audit-validation/probe/Probe.csproj
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe restore .artifacts/audit-validation/probe/Probe.csproj --source .artifacts/audit-validation/empty-feed --ignore-failed-sources -p:NuGetAudit=false
|
||||
& C:/Users/Connor/.dotnet/dotnet.exe run --project .artifacts/audit-validation/probe/Probe.csproj --configuration Release --no-restore
|
||||
```
|
||||
|
||||
If packages or the intended local UI artifact are missing, stop and report the missing prerequisite instead of connecting to external services. This harness is tailored to the recorded Windows workspace/runtime, not a portable CI runner. The fake PDF subprocess uses the compiled probe apphost; it writes a synthetic `%PDF` marker solely so the actual exporter completes its lifecycle. It renders no HTML, launches no Chromium, and does not validate PDF format/layout.
|
||||
|
||||
## Probe development corrections
|
||||
|
||||
Earlier probe iterations had fixture errors: a wrong profile route, a worker test owner scope that was not neutral, and a transient iframe document body accessed before it existed. Those were corrected in the validation probes only and are not product findings. The browser matrix was expanded to check loaded preview content, viewport changes and the actual overflowing region. Only the final successful observations are retained as evidence. Repeated fixture runs are not a flake-rate measurement or repeated independent production certification.
|
||||
|
||||
## Boundaries still open
|
||||
|
||||
- Production proxy/firewall/IP evidence, environment inputs, container privileges/resources and current host/secrets controls.
|
||||
- A coordinated restored recovery set, off-site custody, provider-token/key recovery, previous-image schema compatibility and measured operator recovery time.
|
||||
- Stripe sandbox purchase/entitlement/deletion, live email/OAuth/provider behavior and enabled AI quality/failure behavior.
|
||||
- MariaDB reproductions of the new revision/timezone probes; existing SQLite/test coverage cannot certify provider equivalence.
|
||||
- Full keyboard/screen-reader/forced-color/reflow/mobile accessibility and loaded Bokmål coverage.
|
||||
- Current dependency advisories/licenses/runtime support and Git-history/runtime-secret review. No internet lookup or external service call was performed.
|
||||
|
||||
These are named missing proofs. Do not convert them into invented incidents, and do not close them with a source-only assertion.
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"ticket": "B01",
|
||||
"completedDate": "2026-09-08",
|
||||
"baselineCommit": "7d641edc7bca866ccf06012897ba1d107c07e59f",
|
||||
"environment": "Windows, .NET SDK 9.0.317, Release, existing packages, no external services",
|
||||
"status": "complete locally; not deployed; F11 remains open for B07 and deployment evidence",
|
||||
"runs": [
|
||||
{
|
||||
"phase": "before application edits, regression reproduction",
|
||||
"path": ".artifacts/b01/b01-before.trx",
|
||||
"total": 12,
|
||||
"passed": 8,
|
||||
"failed": 4,
|
||||
"skipped": 0,
|
||||
"sha256": "320B78B4A5A6E008B66D64661816F7A7F827BF48AEE69052C03D79989D631FA7"
|
||||
},
|
||||
{
|
||||
"phase": "final focused import and existing IMAP literal guard tests",
|
||||
"path": ".artifacts/b01/b01-focused.trx",
|
||||
"total": 98,
|
||||
"passed": 98,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"sha256": "FE5BB3EB7A3D1D169C3C9B91AD46AF6B88868DE548D2AF3F228E30CBEC72B928"
|
||||
},
|
||||
{
|
||||
"phase": "broader backend including focused cases",
|
||||
"path": ".artifacts/b01/b01-backend.trx",
|
||||
"total": 840,
|
||||
"passed": 840,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"excluded": ["JobTrackerApi.Tests.ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail"],
|
||||
"exclusionReason": "Existing test performs real DNS resolution; external access was not authorized.",
|
||||
"sha256": "379541860BFA4BFB819AF0BD172F23DEDDF26F003AD6A23F4906700DD333F022"
|
||||
}
|
||||
],
|
||||
"build": {
|
||||
"command": "dotnet build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore -warnaserror",
|
||||
"exitCode": 0,
|
||||
"warnings": 0,
|
||||
"errors": 0
|
||||
},
|
||||
"newRegressionCases": 88,
|
||||
"originalEvidencePreserved": true
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copy to .artifacts/b02/playwright.offline.config.ts and run from job-tracker-ui.
|
||||
import base from "../../job-tracker-ui/playwright.config";
|
||||
import path from "node:path";
|
||||
|
||||
export default {
|
||||
...base,
|
||||
testDir: path.join(process.cwd(), "e2e"),
|
||||
use: { ...base.use, launchOptions: { args: ["--no-proxy-server", "--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE localhost, EXCLUDE 127.0.0.1"] } },
|
||||
webServer: (base.webServer as Array<Record<string, unknown>>).map(server => ({
|
||||
...server, cwd: process.cwd(), env: {
|
||||
...(server.env as Record<string, string>),
|
||||
Auth__GoogleClientId: "", Auth__MicrosoftClientId: "", Email__Enabled: "false",
|
||||
Ai__BaseUrl: "http://127.0.0.1:1", Stripe__SecretKey: "", Backups__Enabled: "false",
|
||||
Workers__AiOperationsEnabled: "false", Workers__RulesEnabled: "false", Workers__FollowUpRemindersEnabled: "false",
|
||||
Workers__DailyExportEnabled: "false", Workers__JobEnrichmentEnabled: "false"
|
||||
}
|
||||
}))
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ticket": "B02",
|
||||
"date": "2026-09-08",
|
||||
"scenario": "Retain A profile request, logout, sign in as B, submit with B cookies/fresh CSRF and retained request context",
|
||||
"before": { "status": 200, "a": "Private A", "b": "A draft must never enter B" },
|
||||
"after": { "status": 409, "a": "Private A", "b": "Private B" },
|
||||
"fixtures": "Permanent Playwright API tests against actual API, Identity/session checks and isolated SQLite; synthetic accounts only",
|
||||
"historicalEvidenceUnchanged": ["probes.mjs", "evidence/observations.json"]
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"ticket": "B02",
|
||||
"completedDate": "2026-09-08",
|
||||
"baselineCommit": "7d641edc7bca866ccf06012897ba1d107c07e59f",
|
||||
"environment": "Windows, existing .NET 9 SDK and UI dependencies, synthetic local SQLite/browser fixtures, external providers disabled",
|
||||
"status": "complete locally; not deployed; existing R01a standalone TypeScript diagnostic remains",
|
||||
"acceptance": "Original retained request rejected; direct object ownership and associations enforced; same-account controls and private state disposal pass",
|
||||
"runs": [
|
||||
{
|
||||
"phase": "before application edits: original replay expected safe assertion failed",
|
||||
"path": ".artifacts/b02/before.json",
|
||||
"sha256": "BECFB841585A287BADF04E7446DC43FB785F066981E07636534E75F6EF9D2E30",
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"failed": 1,
|
||||
"skipped": 0,
|
||||
"observation": "HTTP 200; A unchanged; B overwritten with retained A draft"
|
||||
},
|
||||
{
|
||||
"phase": "focused backend authorization/session/career/CV",
|
||||
"path": ".artifacts/b02/focused.trx",
|
||||
"sha256": "B43DF85097B6CE15B6DC7766162934E14F62132686EAA20B76F8EA014170A659",
|
||||
"total": 91,
|
||||
"passed": 91,
|
||||
"failed": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
{
|
||||
"phase": "broader backend including focused cases",
|
||||
"path": ".artifacts/b02/backend.trx",
|
||||
"sha256": "9539800BF98F5B6B2CA6E52FA37FCFB96845A77D6CDA166DFEB3948AA10AD07D",
|
||||
"total": 853,
|
||||
"passed": 853,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"excluded": [
|
||||
"JobTrackerApi.Tests.ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail"
|
||||
],
|
||||
"exclusionReason": "Existing test performs real DNS; external access not authorized."
|
||||
},
|
||||
{
|
||||
"phase": "focused frontend before final anonymous-BFCache control addition; superseded by full run",
|
||||
"path": ".artifacts/b02/frontend-focused.json",
|
||||
"sha256": "72CCCA872249B808C3C1BFAFCA64F5C4BCB1417FD6DA8643AB2DA241B045BE09",
|
||||
"suites": 5,
|
||||
"total": 51,
|
||||
"passed": 51,
|
||||
"failed": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
{
|
||||
"phase": "final full frontend",
|
||||
"path": ".artifacts/b02/frontend.json",
|
||||
"sha256": "2D0B7B4A47BF5986FC80205D70FD0024B4CDA6483202A72D225A1034297D1F16",
|
||||
"suites": 66,
|
||||
"total": 294,
|
||||
"passed": 294,
|
||||
"failed": 0,
|
||||
"skipped": 0
|
||||
},
|
||||
{
|
||||
"phase": "full browser suite before final anonymous-BFCache refinement",
|
||||
"path": ".artifacts/b02/browser.json",
|
||||
"sha256": "FA78BF60624F42F9DA4A7F7C7DA53FBC4C984297946ED98116DD93BD0FFDB844",
|
||||
"total": 17,
|
||||
"passed": 17,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
{
|
||||
"phase": "final B02 API/browser rerun after all application edits",
|
||||
"path": ".artifacts/b02/browser-final.json",
|
||||
"sha256": "A24785864DD906D04C4E6FF84A0ADE2F5D6F2A43B361C901635F41CEB81617B4",
|
||||
"total": 4,
|
||||
"passed": 4,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"observation": "HTTP 409; both A and B unchanged"
|
||||
}
|
||||
],
|
||||
"checks": [
|
||||
{
|
||||
"command": "dotnet build JobTrackerApi.Tests/JobTrackerApi.Tests.csproj --configuration Release --no-restore -warnaserror",
|
||||
"path": ".artifacts/b02/backend-build.log",
|
||||
"sha256": "3B4A8D824FDF9BC33C527A5B00FD3E045AE7C43F28FD79885CA78641DF222927",
|
||||
"exitCode": 0,
|
||||
"warnings": 0,
|
||||
"errors": 0
|
||||
},
|
||||
{
|
||||
"command": "node --max-old-space-size=4096 node_modules/next/dist/bin/next build",
|
||||
"cwd": "job-tracker-ui",
|
||||
"path": ".artifacts/b02/frontend-build.log",
|
||||
"sha256": "DC7F4BC6EAAD13C897A69F58394867B3B5CBD49B881B60C2D0DF8F7A4612E592",
|
||||
"exitCode": 0
|
||||
},
|
||||
{
|
||||
"command": "node node_modules/typescript/bin/tsc --noEmit --pretty false",
|
||||
"cwd": "job-tracker-ui",
|
||||
"path": ".artifacts/b02/typecheck.log",
|
||||
"sha256": "2F9960647ABADEFD054152A9566E4E6557A55383E98627AED742B93EDEBB16DD",
|
||||
"exitCode": 2,
|
||||
"diagnostic": "src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.",
|
||||
"classification": "Pre-existing audit R01a, unchanged, no new diagnostics"
|
||||
},
|
||||
{
|
||||
"command": "node node_modules/eslint/bin/eslint.js src/api.ts src/sessionBoundary.ts src/session-boundary.test.ts src/App.tsx src/views/LoginPage.tsx src/i18n/translations.ts e2e/account-isolation.spec.ts e2e/smoke.spec.ts --max-warnings=0",
|
||||
"cwd": "job-tracker-ui",
|
||||
"path": ".artifacts/b02/lint.log",
|
||||
"sha256": "33237F0AC9684ACA5331E14FA4337EFC24C4C03D39E589FD0FBCA54F627A9483",
|
||||
"exitCode": 0,
|
||||
"note": "Tool emitted stale Browserslist database advisory; no lint violations; no dependency update."
|
||||
},
|
||||
{
|
||||
"command": "git diff --check",
|
||||
"exitCode": 0
|
||||
}
|
||||
],
|
||||
"newRegressionCases": {
|
||||
"backendMiddleware": 13,
|
||||
"frontendBoundary": 15,
|
||||
"realApiAndBrowserScenarios": 4
|
||||
},
|
||||
"commands": "Exact test commands and local offline fixture reproduction in ../b02-completion.md and b02-browser-config.ts.txt",
|
||||
"rawOutputs": "Hashes identify local ignored .artifacts/b02 files; permanent tests, this manifest and compact observations are retained in repository.",
|
||||
"originalEvidencePreserved": true,
|
||||
"b01ApplicationCodePreserved": true,
|
||||
"schemaMigration": false,
|
||||
"deployed": false,
|
||||
"nextTicket": "B03 — shared PDF admission; not started"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"ticket": "B03",
|
||||
"date": "2026-09-08",
|
||||
"before": { "source": "Permanent actual-exporter regression before application edits", "concurrentCalls": 2, "actualBrowserStubLaunches": 2, "safeAssertion": "failed as expected" },
|
||||
"after": { "source": "Authenticated real API/controllers/exporter with inert process fixture", "configured": 2, "admittedDuringBurst": 2, "rejected": 10, "queued": 0, "maximumActualRenders": 2, "busyStatus": 503, "retryAfterSeconds": 3, "recoveryStatus": 200 },
|
||||
"limitations": "Inert subprocesses prove actual launch admission, not Chromium peak memory, throughput, sandbox or production host sizing.",
|
||||
"originalEvidencePreserved": true
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
{
|
||||
"ticket": "B03",
|
||||
"completedDate": "2026-09-08",
|
||||
"baselineCommit": "7d641edc7bca866ccf06012897ba1d107c07e59f",
|
||||
"environment": "Windows, existing .NET 9 SDK and UI dependencies; synthetic local SQLite/API/browser fixtures, external providers disabled",
|
||||
"status": "Complete locally; not deployed. B01/B02 preserved; B04 not started.",
|
||||
"acceptance": "All four expensive PDF routes use one shared admission pool; bounded actual process launches, safe rejection, repeated exception/cancellation/timeout recovery and configuration validation proven.",
|
||||
"regressionsAdded": {
|
||||
"backendCases": 22,
|
||||
"frontendCases": 2,
|
||||
"authenticatedMultiRouteApiScenarios": 1,
|
||||
"existingAccountSwitchTest": "Wait for B02 final redirect before a new navigation; application B02 behavior unchanged."
|
||||
},
|
||||
"countsOverlap": true,
|
||||
"runs": [
|
||||
{
|
||||
"path": ".artifacts/b03/before.trx",
|
||||
"sha256": "5317581B7BCF11D1D102AE59CACC211B8E5514DA2DB4AA14D4ABF08B3535123C",
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"failed": 1,
|
||||
"skipped": 0,
|
||||
"phase": "Before application edits: expected failure reproduces unrestricted launches",
|
||||
"observation": "Two simultaneous calls through the actual exporter started two held inert child processes; safe assertion failed."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/focused-verified.trx",
|
||||
"sha256": "3812F700D223CEC9F6CF7B38CBA16220BC34BFCA07C19BCA17BA5FCAE3F2B714",
|
||||
"total": 23,
|
||||
"passed": 23,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"phase": "Final focused PDF/admission and existing retention tests"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/affected.trx",
|
||||
"sha256": "23943DA4A80D623B6F6DC3F8BFE86FA922C3C670E9A4689F63AAA163813E9E4A",
|
||||
"total": 208,
|
||||
"passed": 208,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"phase": "Affected backend before the last logging-fault test; superseded by final full backend"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/backend-verified.trx",
|
||||
"sha256": "4E4D66EECA8D305923EF9D6198F0B7CACF6A68A7B29BBE076B6277ACCE416289",
|
||||
"total": 875,
|
||||
"passed": 875,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"phase": "Final broader backend, including all B01/B02/B03 tests",
|
||||
"excluded": [
|
||||
"JobTrackerApi.Tests.ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail"
|
||||
],
|
||||
"exclusionReason": "Existing test performs real external DNS; external access not authorized."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/frontend-focused.json",
|
||||
"sha256": "FB2D9A9A76DB165EA9765CDDBCC40799F2A6EB6DE3C6B97BA192CC99DEC23A36",
|
||||
"suites": 5,
|
||||
"total": 37,
|
||||
"passed": 37,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"phase": "Focused frontend"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/frontend.json",
|
||||
"sha256": "2C7580F8E82F9CFC516B35044BAB452C35F57F389CF0A5377A5876CA7CF37EDD",
|
||||
"suites": 66,
|
||||
"total": 296,
|
||||
"passed": 296,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"phase": "Full frontend"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/adversarial.json",
|
||||
"sha256": "4D7CCD07EB6195D2AE9C6D12DEFEAC2DB3BA45BA7229AE237D0ACEA769560D80",
|
||||
"passed": 0,
|
||||
"failed": 1,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 13734.423999999999,
|
||||
"startedAt": "2026-09-08T10:46:39.267Z",
|
||||
"phase": "Initial HTTP fixture run: failed control-directory setup",
|
||||
"diagnosis": "Runner and worker initially created different stub control directories; corrected test configuration to inherit one directory."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/adversarial-verified.json",
|
||||
"sha256": "A43BE3B32215073A25B72A466B37B48A6765B74F10FDF50103B948FC2E2AB26D",
|
||||
"passed": 1,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 17724.545,
|
||||
"startedAt": "2026-09-08T16:12:27.545Z",
|
||||
"phase": "Final authenticated multi-route API adversarial regression",
|
||||
"observations": {
|
||||
"configured": 2,
|
||||
"admittedDuringBurst": 2,
|
||||
"rejected": 10,
|
||||
"queued": 0,
|
||||
"maximumActualRenders": 2,
|
||||
"busyStatus": 503,
|
||||
"retryAfterSeconds": 3,
|
||||
"recoveryStatus": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/browser.json",
|
||||
"sha256": "9B334B702DCCF8C5DFAAE7252CC4901558D44223BD5A56EA925379EA6CEF98FF",
|
||||
"passed": 12,
|
||||
"failed": 5,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 317668.186,
|
||||
"startedAt": "2026-09-08T10:53:22.552Z",
|
||||
"phase": "Initial browser development-server run",
|
||||
"diagnosis": "Two real PDF timeouts from an introduced explicit Chromium user-data-dir argument, two navigation timeouts and transient API-status-chip axe contrast failures. Superseded by final passing static run."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/real-pdf-diagnostic.json",
|
||||
"sha256": "19119A7C91E822F0F66E7FAEE6554EBE73795A75CBE26656E435F90795E9E8E9",
|
||||
"passed": 0,
|
||||
"failed": 2,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 178063.123,
|
||||
"startedAt": "2026-09-08T15:50:26.573Z",
|
||||
"phase": "Real Chromium diagnostic run",
|
||||
"diagnosis": "Both PDFs reached the 60-second render timeout. Local CLI control isolated added user-data-dir argument; removed it to preserve original browser invocation."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/browser-final.json",
|
||||
"sha256": "4511D084489B4CC857243439E711ABB99F5E4A221B6CB5B3ADB6440A1B6D6F55",
|
||||
"passed": 15,
|
||||
"failed": 2,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 142642.293,
|
||||
"startedAt": "2026-09-08T16:01:38.189Z",
|
||||
"phase": "First full static-browser run after CLI correction",
|
||||
"diagnosis": "Account-switch fixture raced B02's final redirect (test wait corrected); separate Code-PDF setup hit existing SQLite Error 5 database is locked in profile/Identity projection before rendering. No database or accessibility changes."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/browser-verified.json",
|
||||
"sha256": "A78EB7BB65180AE3427AF692E15E879426A70B7F2F3EBB806DCCF9CAFCB043DF",
|
||||
"passed": 17,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 88765.154,
|
||||
"startedAt": "2026-09-08T16:05:45.871Z",
|
||||
"phase": "Final full static browser suite",
|
||||
"notes": "All 17 pass with no retries/flaky outcomes. Real public and Code PDF renders approximately 1.3/1.4 seconds. This does not prove the existing SQLite projection race resolved."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/backend-build-verified.log",
|
||||
"sha256": "640B718E87F7E6E02AEEAE33E00F058751F0C10F66202BFE3BF47B6BB9504402",
|
||||
"phase": "Final Release warnings-as-errors build",
|
||||
"exitCode": 0,
|
||||
"warnings": 0,
|
||||
"errors": 0
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/frontend-build.log",
|
||||
"sha256": "635DC750AFBC06B2E936C0F21B5B5406F875B47F7DC3DCA38819B0D552C7ADA1",
|
||||
"phase": "Next production build",
|
||||
"exitCode": 0
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/typecheck.log",
|
||||
"sha256": "2F9960647ABADEFD054152A9566E4E6557A55383E98627AED742B93EDEBB16DD",
|
||||
"phase": "Standalone TypeScript",
|
||||
"exitCode": 2,
|
||||
"diagnostic": "src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.",
|
||||
"classification": "Pre-existing R01a; no new diagnostic or suppression."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b03/lint-verified.log",
|
||||
"sha256": "33237F0AC9684ACA5331E14FA4337EFC24C4C03D39E589FD0FBCA54F627A9483",
|
||||
"phase": "Final targeted lint, including account-switch test wait",
|
||||
"exitCode": 0,
|
||||
"notes": "Existing Browserslist stale-data advisory; no lint warnings/errors."
|
||||
},
|
||||
{
|
||||
"phase": "Final git diff --check",
|
||||
"path": ".artifacts/b03/diff-check.log",
|
||||
"sha256": "C274B72A1428ACB647E415C5582D003F75341E912BA5832D478B8964228C623C",
|
||||
"exitCode": 0,
|
||||
"notes": "Repository line-ending normalization retained. Additional explicit trailing-whitespace check passed for 13 new/previously untracked B03 and documentation files."
|
||||
}
|
||||
],
|
||||
"finalSourceTiming": "Last logging-fault refinement covered by final full backend, focused PDF and authenticated API runs. No frontend or browser invocation changes after final passing frontend/browser suites.",
|
||||
"residualLimitations": [
|
||||
"Process-local; N rendering instances allow N times configured active renders. Owner allowance is also process-local.",
|
||||
"R04 still required for production memory/CPU/PID sizing, process-tree containment, browser sandbox and forced-kill Chromium profile cleanup.",
|
||||
"No FIFO queue or time-window abuse quota; zero waiting. Public requests account to document owner and may occupy multiple owners' allowances, subject to the global limit.",
|
||||
"Upstream AI/HTML/DB preparation and arbitrary single-document memory/output size are outside render admission.",
|
||||
"Unconfirmed process termination disables all new renders; operator must inspect/stop remnants before restart.",
|
||||
"Existing standalone TypeScript R01a failure and observed SQLite profile-projection fixture race remain outside B03.",
|
||||
"Local browser static export targets localhost:5302 and is not a production deployment artifact."
|
||||
],
|
||||
"evidenceRetention": "Original audit/probes retained. Raw local TRX/JSON/logs are ignored under .artifacts/b03; hashes and compact synthetic observations are retained here. Permanent tests are the current executable regression boundary.",
|
||||
"recommendedNextTicket": "B04 Resolve principal before owner limiting; requires separate user instruction."
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"ticket": "B04",
|
||||
"date": "2026-09-08",
|
||||
"boundary": "Real local API, authentication, endpoint authorization, partitioned rate limiter, B02 middleware and synthetic SQLite data",
|
||||
"before": {
|
||||
"accountAStatuses": [200, 200],
|
||||
"accountBStatusesOnSameIP": [429, 429],
|
||||
"safeAssertionFailed": true
|
||||
},
|
||||
"after": {
|
||||
"aFirst": { "A": { "admitted": 2, "rejected": 1 }, "B": { "admitted": 2, "rejected": 1 } },
|
||||
"bFirst": { "A": { "admitted": 2, "rejected": 1 }, "B": { "admitted": 2, "rejected": 1 } },
|
||||
"concurrent": { "sessionsPerAccount": 2, "auth": ["cookie", "bearer"], "admitted": 4, "subsequentRejectedAcrossOtherIPs": 4 },
|
||||
"craftedEmailSendOwner": { "B": { "chargedAttempts": 10, "rejectedByRateLimit": 1 }, "A": { "chargedAttempts": 10, "rejectedByRateLimit": 1 }, "emailsSent": 0, "staleOrForgedContextStatusBeforeExhaustion": 409 },
|
||||
"invalidCredentials": { "categories": 7, "protectedAccountEndpointStatus": 401, "accountPartitionReached": false, "anonymousLoginScope": "IP; 10 admitted validation attempts then 429 per IP" },
|
||||
"untrustedProxyHeaders": { "source": "127.0.0.2", "admittedValidationAttempts": 10, "changedForwardedHeadersCannotEvade429": true, "secondSource127_0_0_3Status": 400 },
|
||||
"rateResponse": { "status": 429, "retryAfterSeconds": "positive fixed-window lease metadata", "cacheControl": "no-store", "code": "rate_limit_exceeded" }
|
||||
},
|
||||
"finalOrder": "Existing CSRF, authentication, endpoint authorization, rate limiting, B02 session context, controller",
|
||||
"scopeLimits": "Process-local policy counters; live external OAuth and production ingress not exercised. IP security policies and B03 global rendering admission remain distinct.",
|
||||
"originalEvidencePreserved": true
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"ticket": "B04",
|
||||
"completedDate": "2026-09-08",
|
||||
"baselineCommit": "7d641edc7bca866ccf06012897ba1d107c07e59f",
|
||||
"status": "Complete locally; not deployed. B01/B02/B03 preserved; B05 not started.",
|
||||
"environment": "Windows, .NET 9 and existing UI dependencies; synthetic local SQLite/API/browser data; external providers disabled",
|
||||
"finalMiddlewareBoundary": "Existing CSRF -> authentication -> endpoint authorization -> rate limiting -> B02 session context -> controllers",
|
||||
"acceptance": "Account policies use the final authorized principal, independent across accounts and shared across same-account sessions/transports/IPs. IP security policies, B02 provenance and B03 resource admission remain covered.",
|
||||
"regressionsAdded": {
|
||||
"backendCases": 2,
|
||||
"realHttpScenarios": 9,
|
||||
"testFixtureCorrection": "Stop signed-in public-CV setup page before cookie removal; no frontend runtime change."
|
||||
},
|
||||
"countsOverlap": true,
|
||||
"runs": [
|
||||
{
|
||||
"path": ".artifacts/b04/focused.trx",
|
||||
"sha256": "DCE64904E7C1921DCC896BF12BCFD1586C5CD77A9D3FC2334ED0FDBE277C18B9",
|
||||
"total": 207,
|
||||
"passed": 207,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"phase": "Focused auth/rate/B01-B03 backend before final authorization-order refinement"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/backend-final.trx",
|
||||
"sha256": "AD8761B5C2B06D4E286856AF9E9D02E586BD6751276EDC12E6204E6A49CEC09C",
|
||||
"total": 877,
|
||||
"passed": 877,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"phase": "Final full selected backend",
|
||||
"excluded": [
|
||||
"JobTrackerApi.Tests.ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail"
|
||||
],
|
||||
"exclusionReason": "Existing test makes external DNS requests; external access not authorized."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/frontend-focused.json",
|
||||
"sha256": "273FF523A49CBB533EAF257BFC22A8A3622E457C0B50873881E4F6D4AB97CD86",
|
||||
"suites": 4,
|
||||
"total": 36,
|
||||
"passed": 36,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"phase": "Affected frontend; runtime code unchanged by B04"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/before.json",
|
||||
"sha256": "9D76376030CBBED4A8840434C4AFA44F6855B4EDDF71A14B33C70ADBB29EE4D3",
|
||||
"passed": 0,
|
||||
"failed": 1,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 7913.539999999999,
|
||||
"startedAt": "2026-09-08T21:06:28.266Z",
|
||||
"phase": "Before application edits: expected failing safe assertion",
|
||||
"observation": "A received 200/200; B on the same IP received 429/429."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/api-initial.json",
|
||||
"sha256": "97E4A1AF216F6CB84B8E7D508F6C5C7F739EE47E95260D1052321C50D26402E9",
|
||||
"passed": 9,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 11056.037,
|
||||
"startedAt": "2026-09-08T21:10:18.256Z",
|
||||
"phase": "Initial authentication-only ordering, superseded after self-review",
|
||||
"notes": "Original scenarios passed, but endpoint-specific authorization may change the default principal. Final ordering also waits for authorization."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/api-final.json",
|
||||
"sha256": "FD0BDEE6F68F7AF6438087732A8F0A6809B7A7EFD6DC7FF9A6BBB3672707026A",
|
||||
"passed": 9,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 10464.229000000001,
|
||||
"startedAt": "2026-09-08T21:20:33.327Z",
|
||||
"phase": "Final HTTP rate-policy regression, including authorization-before-partitioning",
|
||||
"scenarios": 9,
|
||||
"observations": "Two accounts each admit 2/reject 1 on the same IP in both orders; four concurrent cookie/bearer/session requests succeed, later requests across other IPs remain limited. Invalid credentials are denied before account limiting; anonymous login remains IP-limited."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/browser.json",
|
||||
"sha256": "AF984A9F9CB0F8EB1AAE88B724A1B0F718208FA0F6EFD10FA30D71D69049F33D",
|
||||
"passed": 17,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 76250.659,
|
||||
"startedAt": "2026-09-08T21:13:03.191Z",
|
||||
"phase": "Full browser before final authorization-order refinement"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/browser-final.json",
|
||||
"sha256": "D89F4F58561A2B6F161F33BE9223C9A1582E3A1BD34F4985C33C0FEBA20F453C",
|
||||
"passed": 16,
|
||||
"failed": 1,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 80244.032,
|
||||
"startedAt": "2026-09-08T21:22:28.655Z",
|
||||
"phase": "First browser run after final authorization-order refinement",
|
||||
"diagnosis": "Public CV fixture cleared cookies while signed-in page remained active, triggering B02's correct session-change redirect. Remaining 16 tests passed; fixture subsequently stops that page before cookie removal."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/browser-verified.json",
|
||||
"sha256": "9512176BAB16E19E70CEBE9767FBE590CD8EB97308628ED29F46E690ADC77382",
|
||||
"passed": 17,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 67434.35999999999,
|
||||
"startedAt": "2026-09-08T21:25:36.046Z",
|
||||
"phase": "Final full browser after fixture correction",
|
||||
"notes": "All 17 passed without retries/flaky outcomes, including account switching and real PDFs."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/pdf-admission-final.json",
|
||||
"sha256": "6213EA4CF8B5E713D5FD01D332765979BF10F492F741DE6C33854F64847D5520",
|
||||
"passed": 1,
|
||||
"failed": 0,
|
||||
"skipped": 0,
|
||||
"flaky": 0,
|
||||
"durationMs": 7616.844999999999,
|
||||
"startedAt": "2026-09-08T21:28:50.581Z",
|
||||
"phase": "Final unchanged B03 authenticated multi-route admission probe",
|
||||
"observations": {
|
||||
"configured": 2,
|
||||
"admitted": 2,
|
||||
"rejected": 10,
|
||||
"maximumActualRenders": 2,
|
||||
"recoveryStatus": 200
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/build.log",
|
||||
"sha256": "97AE4880F122E36BFC2A96184868F509DF76BA7177E9F5851D022ADED6FBB924",
|
||||
"phase": "Final Release warnings-as-errors build",
|
||||
"exitCode": 0,
|
||||
"warnings": 0,
|
||||
"errors": 0
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/lint.log",
|
||||
"sha256": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
|
||||
"phase": "Final targeted ESLint including public CV smoke fixture",
|
||||
"exitCode": 0
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/typecheck.log",
|
||||
"sha256": "2F9960647ABADEFD054152A9566E4E6557A55383E98627AED742B93EDEBB16DD",
|
||||
"phase": "Final standalone TypeScript",
|
||||
"exitCode": 1,
|
||||
"initialExitCode": 2,
|
||||
"diagnostic": "src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'.",
|
||||
"classification": "Existing R01a only; no suppression or B04 type error."
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/diff-check.log",
|
||||
"sha256": "07A41BCFE160D0FA5AA278C009F548C35372FBDDA40BDFC61BBDFF43DDF699DB",
|
||||
"phase": "git diff --check",
|
||||
"exitCode": 0
|
||||
},
|
||||
{
|
||||
"path": ".artifacts/b04/public-cv-session-race.md",
|
||||
"sha256": "118B893EC4CB2472EF8FE7DA309067188A6BF04A8CE9C4A3C78C98D6806A0BB4",
|
||||
"phase": "Retained public CV fixture failure context",
|
||||
"observation": "Session-change sign-in screen, not a PDF/rate-limit failure."
|
||||
}
|
||||
],
|
||||
"frontendBuild": "B04 changes no frontend runtime source. Affected Jest and full browser checks used B03's previously built static export targeting localhost:5302; no new Next build claimed. This artifact is not for production deployment.",
|
||||
"residualLimitations": [
|
||||
"Fixed-window counters remain process-local and reset on restart. N instances can provide N times each configured allowance.",
|
||||
"Intentional security IP limits remain shared behind NAT; account isolation does not convert every policy to an account limit.",
|
||||
"Requests denied by CSRF/authorization do not reach rate limiting. Authorized attempts can consume their own allowance before later B02/controller refusal.",
|
||||
"Authentication/session/policy evaluation occurs before rate rejection; this is not global ingress or pre-auth CPU admission.",
|
||||
"Production trusted proxy/firewall/direct-ingress proof remains B06; only controlled local proxy behavior was exercised.",
|
||||
"Optional external OAuth providers were not contacted. Authorization-before-limiting handles endpoint-specific principal selection using the existing framework.",
|
||||
"Known R01a standalone TypeScript failure remains; B03 R04 resource/containment evidence remains open."
|
||||
],
|
||||
"evidenceRetention": "Original audit/probes preserved. Raw outputs remain local under ignored .artifacts/b04; these hashes and compact observations are retained. Initial passing-but-incomplete ordering and the browser fixture failure are explicitly distinguished from final verification.",
|
||||
"recommendedNextTicket": "B05 Spreadsheet-safe CSV fields; requires separate user instruction."
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
Company,CompanyLocation,CompanySource,JobTitle,Status,DateApplied,Location,Salary,SalaryMin,SalaryMax,SalaryCurrency,SalaryPeriod,NextAction,FollowUpAt,JobUrl,Notes,CoverLetterText
|
||||
Ærlig Økonomi Ås,,,'=1+1,Applied,,,,,,,,,,,,
|
||||
|
@@ -0,0 +1,2 @@
|
||||
Company,CompanyLocation,CompanySource,JobTitle,Status,DateApplied,Location,Salary,SalaryMin,SalaryMax,SalaryCurrency,SalaryPeriod,NextAction,FollowUpAt,JobUrl,Notes,CoverLetterText
|
||||
Ærlig Økonomi Ås,,,=1+1,Applied,,,,,,,,,,,,
|
||||
|
@@ -0,0 +1,190 @@
|
||||
[
|
||||
{
|
||||
"file": "after-csv.csv",
|
||||
"excelVersion": "16.0",
|
||||
"company": "Ærlig Økonomi Ås",
|
||||
"title": "'=1+1",
|
||||
"titleHasFormula": false,
|
||||
"cells": [
|
||||
{
|
||||
"column": 1,
|
||||
"value": "Ærlig Økonomi Ås",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 2,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 3,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 4,
|
||||
"value": "'=1+1",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 5,
|
||||
"value": "Applied",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 6,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 7,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 8,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 9,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 10,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 11,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 12,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 13,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 14,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 15,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 16,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 17,
|
||||
"value": null,
|
||||
"hasFormula": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "after-typed.csv",
|
||||
"excelVersion": "16.0",
|
||||
"company": "'=1+1",
|
||||
"title": "'@SUM(A1:A2)",
|
||||
"titleHasFormula": false,
|
||||
"cells": [
|
||||
{
|
||||
"column": 1,
|
||||
"value": "'=1+1",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 2,
|
||||
"value": "' +SUM(1,2)",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 3,
|
||||
"value": "'\t-2+3",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 4,
|
||||
"value": "'@SUM(A1:A2)",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 5,
|
||||
"value": "'\r\n=1+1",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 6,
|
||||
"value": "2026-09-09T12:30:00.0000000Z",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 7,
|
||||
"value": "'=1+1",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 8,
|
||||
"value": "'-750",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 9,
|
||||
"value": -750.5,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 10,
|
||||
"value": 1200.75,
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 11,
|
||||
"value": "'+NOK",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 12,
|
||||
"value": "'@year",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 13,
|
||||
"value": "'=1+1",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 14,
|
||||
"value": "2026-09-10T12:30:00.0000000Z",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 15,
|
||||
"value": "'+1",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 16,
|
||||
"value": "'=SUM(1,2)\næøå",
|
||||
"hasFormula": false
|
||||
},
|
||||
{
|
||||
"column": 17,
|
||||
"value": "'=1+1\r\nÆØÅ",
|
||||
"hasFormula": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"file": "before.csv",
|
||||
"excelVersion": "16.0",
|
||||
"company": "Ærlig Økonomi Ås",
|
||||
"title": 2.0,
|
||||
"titleHasFormula": true
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
param([string[]]$CsvPaths)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$excel = $null
|
||||
$results = @()
|
||||
try {
|
||||
$excel = New-Object -ComObject Excel.Application
|
||||
$excel.Visible = $false
|
||||
$excel.DisplayAlerts = $false
|
||||
$excel.EnableEvents = $false
|
||||
$excel.AskToUpdateLinks = $false
|
||||
$excel.AutomationSecurity = 3
|
||||
foreach ($csvPath in $CsvPaths) {
|
||||
$book = $null
|
||||
try {
|
||||
$resolved = (Resolve-Path -LiteralPath $csvPath).Path
|
||||
$book = $excel.Workbooks.Open($resolved, 0, $true)
|
||||
$sheet = $book.Worksheets.Item(1)
|
||||
$company = $sheet.Cells.Item(2, 1)
|
||||
$title = $sheet.Cells.Item(2, 4)
|
||||
$cells = @()
|
||||
for ($column = 1; $column -le 17; $column++) {
|
||||
$cell = $sheet.Cells.Item(2, $column)
|
||||
$cells += [ordered]@{ column = $column; value = $cell.Value2; hasFormula = $cell.HasFormula }
|
||||
[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($cell)
|
||||
}
|
||||
$results += [ordered]@{ file = [IO.Path]::GetFileName($resolved); excelVersion = $excel.Version; company = $company.Value2; title = $title.Value2; titleHasFormula = $title.HasFormula; cells = $cells }
|
||||
[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($company)
|
||||
[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($title)
|
||||
[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($sheet)
|
||||
} finally {
|
||||
if ($null -ne $book) { $book.Close($false); [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($book) }
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if ($null -ne $excel) { $excel.Quit(); [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($excel) }
|
||||
}
|
||||
$results | ConvertTo-Json -Depth 6
|
||||
@@ -0,0 +1,132 @@
|
||||
{
|
||||
"ticket": "B05",
|
||||
"date": "2026-09-09",
|
||||
"scope": "Local only; B01-B04 preserved; B06 not started",
|
||||
"results": {
|
||||
"beforeRegression": {
|
||||
"expectedFailure": true,
|
||||
"passed": 0,
|
||||
"exitCode": 1,
|
||||
"failed": 1
|
||||
},
|
||||
"focusedCsvBackend": {
|
||||
"passed": 25,
|
||||
"exitCode": 0,
|
||||
"failed": 0
|
||||
},
|
||||
"backend": {
|
||||
"passed": 902,
|
||||
"skipped": 0,
|
||||
"exitCode": 0,
|
||||
"failed": 0,
|
||||
"excluded": "ImapServiceSsrfGuardTests.ConnectAsync_rejects_unresolvable_host_without_leaking_dns_detail (external DNS not authorized)"
|
||||
},
|
||||
"focusedFrontend": {
|
||||
"exitCode": 0,
|
||||
"passed": 2
|
||||
},
|
||||
"frontend": {
|
||||
"suites": 67,
|
||||
"passed": 298,
|
||||
"exitCode": 0
|
||||
},
|
||||
"browser": {
|
||||
"flaky": 0,
|
||||
"passed": 18,
|
||||
"exitCode": 0,
|
||||
"failed": 0
|
||||
},
|
||||
"b04Http": {
|
||||
"exitCode": 0,
|
||||
"passed": 9
|
||||
},
|
||||
"b03Admission": {
|
||||
"exitCode": 0,
|
||||
"passed": 1
|
||||
},
|
||||
"releaseApi": {
|
||||
"warnings": 0,
|
||||
"errors": 0,
|
||||
"exitCode": 0
|
||||
},
|
||||
"releaseUi": {
|
||||
"apiBase": "/api",
|
||||
"exitCode": 0
|
||||
},
|
||||
"targetedLint": {
|
||||
"exitCode": 0
|
||||
},
|
||||
"diffCheck": {
|
||||
"exitCode": 0
|
||||
},
|
||||
"standaloneTypeScript": {
|
||||
"preExisting": true,
|
||||
"exitCode": 1,
|
||||
"diagnostic": "src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'."
|
||||
},
|
||||
"excel": {
|
||||
"numericCells": [
|
||||
-750.5,
|
||||
1200.75
|
||||
],
|
||||
"afterFormula": false,
|
||||
"version": "16.0",
|
||||
"typedFixtureAll17CellsNonFormula": true,
|
||||
"norwegianPreserved": true,
|
||||
"beforeValue": 2,
|
||||
"afterTitle": "'=1+1",
|
||||
"beforeFormula": true
|
||||
},
|
||||
"libreOffice": "Unavailable; not runtime-tested"
|
||||
},
|
||||
"rawArtifacts": [
|
||||
{
|
||||
"path": ".artifacts\\b05\\before.trx",
|
||||
"sha256": "7c2e14872cebf6cd57ac22b9dba65bdd78909ec1a690f001483db1cb8002b68a"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\after.trx",
|
||||
"sha256": "dc707e7e533b68e81b7dec0a81b20d90e66423ab315ff877d10525567b7aa781"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\backend.trx",
|
||||
"sha256": "cba13821abdc4f38f51285a5b4dc8171239c24d699bfb24f29bf8f99ec1b0d57"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\frontend.json",
|
||||
"sha256": "6e9b59bde603784b9bb9945e1463e1e066f313dc8fb3ea0a24d5a229804b0b9f"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\browser.json",
|
||||
"sha256": "f4da84c36d73b6edd0e68116fc8aedd4a5c132285807cc7a42f8963c7cb6db6e"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\rate-limits.json",
|
||||
"sha256": "81b05cee7468e2afab5b364c90bee10e3b588dad8d50414a8a6edf9dd63b28ed"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\pdf-admission.json",
|
||||
"sha256": "4d567517777037f1dc45a261ef5392348af4acd2d6ea8b47530c7940109027ff"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\release-api.log",
|
||||
"sha256": "ac70f0ed302111ed18456885e702a4707eeea66c6c8aaebcb6485fe0df3bbfdc"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\release-ui.log",
|
||||
"sha256": "97d12bc08f21f0a0c1620482239965b5a6d0671a40782074c32b9ae2c8a6fc8f"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\lint.log",
|
||||
"sha256": "33237f0ac9684aca5331e14fa4337efc24c4c03d39e589fd0fbca54f627a9483"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\typescript.log",
|
||||
"sha256": "2f9960647abadefd054152a9566e4e6557a55383e98627aed742b93edebb16dd"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b05\\diff-check.log",
|
||||
"sha256": "4be06aeae2b435b624173459707d759d86b8e0dcb15f711722fd5b93eb1979de"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
Company,CompanyLocation,CompanySource,JobTitle,Status,DateApplied,Location,Salary,SalaryMin,SalaryMax,SalaryCurrency,SalaryPeriod,NextAction,FollowUpAt,JobUrl,Notes,CoverLetterText
|
||||
'=1+1,"' +SUM(1,2)",' -2+3,'@SUM(A1:A2),"'
|
||||
=1+1",2026-09-09T12:30:00.0000000Z,'=1+1,'-750,-750.50,1200.75,'+NOK,'@year,'=1+1,2026-09-10T12:30:00.0000000Z,'+1,"'=SUM(1,2)
|
||||
æøå","'=1+1
|
||||
ÆØÅ"
|
||||
|
@@ -0,0 +1,84 @@
|
||||
geo $realip_remote_addr $ingress_peer_trusted {
|
||||
default 0;
|
||||
192.0.2.1/32 1;
|
||||
2001:db8::1/128 1;
|
||||
}
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive off;
|
||||
set_real_ip_from 192.0.2.1/32;
|
||||
set_real_ip_from 2001:db8::1/128;
|
||||
|
||||
server {
|
||||
listen 80 default_server;
|
||||
server_name "";
|
||||
|
||||
location = /health {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
|
||||
location / {
|
||||
return 444;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name jobs.example.test;
|
||||
|
||||
if ($ingress_peer_trusted = 0) { return 403; }
|
||||
if ($http_x_forwarded_proto != "https") { return 400; }
|
||||
if ($http_x_forwarded_for = "") { return 400; }
|
||||
if ($http_x_forwarded_for ~ ",") { return 400; }
|
||||
# A malformed address is not forwarded verbatim: realip leaves the socket IP intact.
|
||||
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
location = /health {
|
||||
proxy_pass http://backend-web:8080/health;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host jobs.example.test;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
# realip resolves one sanitized address only from an explicitly trusted socket peer.
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Host "";
|
||||
proxy_set_header Forwarded "";
|
||||
}
|
||||
|
||||
location = /ready {
|
||||
proxy_pass http://backend-web:8080/ready;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host jobs.example.test;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Host "";
|
||||
proxy_set_header Forwarded "";
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://backend-web:8080;
|
||||
proxy_http_version 1.1;
|
||||
|
||||
proxy_set_header Host jobs.example.test;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header X-Forwarded-Host "";
|
||||
proxy_set_header Forwarded "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"date": "2026-09-09",
|
||||
"ticket": "B06",
|
||||
"status": "Implemented; full local and production verification pending",
|
||||
"effectiveCompose": {
|
||||
"after": {
|
||||
"services": [
|
||||
{
|
||||
"service": "ai-service",
|
||||
"ports": null,
|
||||
"networks": [
|
||||
"ai_internal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"service": "backend",
|
||||
"ports": null,
|
||||
"networks": [
|
||||
"ai_internal",
|
||||
"default",
|
||||
"shared_services",
|
||||
"web_proxy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"service": "frontend",
|
||||
"ports": [
|
||||
{
|
||||
"mode": "ingress",
|
||||
"target": 80,
|
||||
"published": "3000",
|
||||
"protocol": "tcp"
|
||||
}
|
||||
],
|
||||
"networks": [
|
||||
"shared_services",
|
||||
"web_proxy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"service": "ollama",
|
||||
"ports": null,
|
||||
"networks": [
|
||||
"ai_internal"
|
||||
]
|
||||
}
|
||||
],
|
||||
"frontendMode": "proxy",
|
||||
"frontendPeers": "192.0.2.10/32",
|
||||
"backendEnvironment": "Production",
|
||||
"webProxy": {
|
||||
"name": "jobtracker_web_proxy",
|
||||
"ipam": {
|
||||
"config": [
|
||||
{
|
||||
"subnet": "172.31.250.0/29"
|
||||
}
|
||||
]
|
||||
},
|
||||
"internal": true
|
||||
}
|
||||
},
|
||||
"before": {
|
||||
"services": [
|
||||
{
|
||||
"service": "ai-service",
|
||||
"ports": null,
|
||||
"networks": [
|
||||
"ai_internal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"service": "backend",
|
||||
"ports": null,
|
||||
"networks": [
|
||||
"ai_internal",
|
||||
"default",
|
||||
"shared_services",
|
||||
"web_proxy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"service": "frontend",
|
||||
"ports": [
|
||||
{
|
||||
"mode": "ingress",
|
||||
"target": 80,
|
||||
"published": "3000",
|
||||
"protocol": "tcp"
|
||||
}
|
||||
],
|
||||
"networks": [
|
||||
"shared_services",
|
||||
"web_proxy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"service": "ollama",
|
||||
"ports": null,
|
||||
"networks": [
|
||||
"ai_internal"
|
||||
]
|
||||
}
|
||||
],
|
||||
"frontendMode": null,
|
||||
"frontendPeers": null,
|
||||
"backendEnvironment": null,
|
||||
"webProxy": {
|
||||
"name": "jobtracker_web_proxy",
|
||||
"ipam": {
|
||||
"config": [
|
||||
{
|
||||
"subnet": "172.31.250.0/29"
|
||||
}
|
||||
]
|
||||
},
|
||||
"internal": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"inputs": "Explicit synthetic env file; no production env inspected",
|
||||
"nginxGeneration": {
|
||||
"scope": "Real entrypoint generation/guards; nginx -t replaced because runtime unavailable",
|
||||
"cases": [
|
||||
{
|
||||
"case": "missing peers",
|
||||
"exit": 1
|
||||
},
|
||||
{
|
||||
"case": "trust-all IPv4",
|
||||
"exit": 1
|
||||
},
|
||||
{
|
||||
"case": "trust-all IPv6",
|
||||
"exit": 1
|
||||
},
|
||||
{
|
||||
"case": "broad peer net",
|
||||
"exit": 1
|
||||
},
|
||||
{
|
||||
"case": "directive injection",
|
||||
"exit": 1
|
||||
},
|
||||
{
|
||||
"case": "direct HTTPS",
|
||||
"exit": 1
|
||||
},
|
||||
{
|
||||
"case": "proxy HTTP",
|
||||
"exit": 1
|
||||
},
|
||||
{
|
||||
"case": "trusted production",
|
||||
"exit": 0
|
||||
},
|
||||
{
|
||||
"case": "direct development",
|
||||
"exit": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"topology": {
|
||||
"actualTraefikProven": false,
|
||||
"reason": "Docker engine pipe absent; Docker Desktop startup crashed on inaccessible dockerInference socket",
|
||||
"actualNginxRuntimeProven": false,
|
||||
"status": "Blocked before creating resources"
|
||||
},
|
||||
"production": {
|
||||
"tls": "Not inspected",
|
||||
"dns": "Not inspected",
|
||||
"firewall": "Not inspected",
|
||||
"peerAddresses": "Unknown; not guessed",
|
||||
"deployed": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"command": [
|
||||
"docker",
|
||||
"compose",
|
||||
"--env-file",
|
||||
".artifacts/b06/compose.env",
|
||||
"-f",
|
||||
"docker-compose.yml",
|
||||
"--profile",
|
||||
"bundled-ollama",
|
||||
"config",
|
||||
"--format",
|
||||
"json"
|
||||
],
|
||||
"input": "Synthetic test env, not deployed .env; peer 192.0.2.10/32 is NOT an observed Traefik peer",
|
||||
"services": {
|
||||
"ai-service": {
|
||||
"ports": [],
|
||||
"expose": [
|
||||
"8001"
|
||||
],
|
||||
"networks": {
|
||||
"ai_internal": null
|
||||
}
|
||||
},
|
||||
"backend": {
|
||||
"ports": [],
|
||||
"expose": [
|
||||
"8080"
|
||||
],
|
||||
"networks": {
|
||||
"ai_internal": null,
|
||||
"default": null,
|
||||
"shared_services": null,
|
||||
"web_proxy": {
|
||||
"aliases": [
|
||||
"backend-web"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"frontend": {
|
||||
"ports": [
|
||||
{
|
||||
"mode": "ingress",
|
||||
"target": 80,
|
||||
"published": "3000",
|
||||
"protocol": "tcp"
|
||||
}
|
||||
],
|
||||
"expose": [
|
||||
"80"
|
||||
],
|
||||
"networks": {
|
||||
"shared_services": null,
|
||||
"web_proxy": null
|
||||
}
|
||||
},
|
||||
"ollama": {
|
||||
"ports": [],
|
||||
"expose": [
|
||||
"11434"
|
||||
],
|
||||
"networks": {
|
||||
"ai_internal": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"networks": {
|
||||
"ai_internal": {
|
||||
"name": "jobtracker_ai_internal",
|
||||
"driver": "bridge",
|
||||
"ipam": {}
|
||||
},
|
||||
"default": {
|
||||
"name": "jobtracker_default",
|
||||
"ipam": {}
|
||||
},
|
||||
"shared_services": {
|
||||
"name": "jobtracker_shared",
|
||||
"ipam": {},
|
||||
"external": true
|
||||
},
|
||||
"web_proxy": {
|
||||
"name": "jobtracker_web_proxy",
|
||||
"ipam": {
|
||||
"config": [
|
||||
{
|
||||
"subnet": "172.31.250.0/29"
|
||||
}
|
||||
]
|
||||
},
|
||||
"internal": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,805 @@
|
||||
{
|
||||
"ticket": "B06",
|
||||
"utc": "2026-09-09T16:06:49.668967+00:00",
|
||||
"status": "Implementation complete; full local runtime and production verification incomplete; F04 open",
|
||||
"public": {
|
||||
"utc": "2026-09-09T10:43:53.552659+00:00",
|
||||
"hostname": "jobs.cesnimda.uk",
|
||||
"dns_addresses": [
|
||||
"109.189.21.248"
|
||||
],
|
||||
"certificate": {
|
||||
"verified": true,
|
||||
"peer": [
|
||||
"109.189.21.248",
|
||||
443
|
||||
],
|
||||
"tls": "TLSv1.3",
|
||||
"certificate": {
|
||||
"subject": [
|
||||
[
|
||||
[
|
||||
"commonName",
|
||||
"jobs.cesnimda.uk"
|
||||
]
|
||||
]
|
||||
],
|
||||
"issuer": [
|
||||
[
|
||||
[
|
||||
"countryName",
|
||||
"US"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"organizationName",
|
||||
"Let's Encrypt"
|
||||
]
|
||||
],
|
||||
[
|
||||
[
|
||||
"commonName",
|
||||
"YR2"
|
||||
]
|
||||
]
|
||||
],
|
||||
"version": 3,
|
||||
"serialNumber": "060998FB27348950371ABE6EE336566BDEFE",
|
||||
"notBefore": "Aug 16 20:35:58 2026 GMT",
|
||||
"notAfter": "Nov 14 20:35:57 2026 GMT",
|
||||
"subjectAltName": [
|
||||
[
|
||||
"DNS",
|
||||
"jobs.cesnimda.uk"
|
||||
]
|
||||
],
|
||||
"caIssuers": [
|
||||
"http://yr2.i.lencr.org/"
|
||||
],
|
||||
"crlDistributionPoints": [
|
||||
"http://yr2.c.lencr.org/127.crl"
|
||||
]
|
||||
}
|
||||
},
|
||||
"requests": [
|
||||
{
|
||||
"name": "public_https",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"https://jobs.cesnimda.uk/api/auth/config"
|
||||
],
|
||||
"exit": 0,
|
||||
"headers": [
|
||||
"HTTP/1.1 200 OK",
|
||||
"Content-Type: application/json; charset=utf-8",
|
||||
"Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()",
|
||||
"Referrer-Policy: same-origin",
|
||||
"Strict-Transport-Security: max-age=63072000; includeSubDomains; preload",
|
||||
"X-Content-Type-Options: nosniff",
|
||||
"X-Frame-Options: SAMEORIGIN"
|
||||
],
|
||||
"metadata": {
|
||||
"http_code": 200,
|
||||
"remote_ip": "109.189.21.248",
|
||||
"remote_port": 443,
|
||||
"local_ip": "192.168.50.2",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 0.172905,
|
||||
"exitcode": 0
|
||||
},
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"name": "public_http",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"http://jobs.cesnimda.uk/api/auth/config"
|
||||
],
|
||||
"exit": 0,
|
||||
"headers": [
|
||||
"HTTP/1.1 301 Moved Permanently",
|
||||
"Location: https://jobs.cesnimda.uk:444/api/auth/config",
|
||||
"Content-Length: 17"
|
||||
],
|
||||
"metadata": {
|
||||
"http_code": 301,
|
||||
"remote_ip": "109.189.21.248",
|
||||
"remote_port": 80,
|
||||
"local_ip": "192.168.50.2",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 0.023086,
|
||||
"exitcode": 0
|
||||
},
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"name": "public_https_forged_forwarding",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"https://jobs.cesnimda.uk/api/auth/config",
|
||||
"--header",
|
||||
"X-Forwarded-For: 198.51.100.10",
|
||||
"--header",
|
||||
"X-Forwarded-Proto: http",
|
||||
"--header",
|
||||
"X-Forwarded-Host: attacker.example.test"
|
||||
],
|
||||
"exit": 0,
|
||||
"headers": [
|
||||
"HTTP/1.1 200 OK",
|
||||
"Content-Type: application/json; charset=utf-8",
|
||||
"Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()",
|
||||
"Referrer-Policy: same-origin",
|
||||
"Strict-Transport-Security: max-age=63072000; includeSubDomains; preload",
|
||||
"X-Content-Type-Options: nosniff",
|
||||
"X-Frame-Options: SAMEORIGIN"
|
||||
],
|
||||
"metadata": {
|
||||
"http_code": 200,
|
||||
"remote_ip": "109.189.21.248",
|
||||
"remote_port": 443,
|
||||
"local_ip": "192.168.50.2",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 0.173702,
|
||||
"exitcode": 0
|
||||
},
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"name": "direct_3000_109.189.21.248_none",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"http://109.189.21.248:3000/api/auth/config",
|
||||
"--header",
|
||||
"Host: jobs.cesnimda.uk"
|
||||
],
|
||||
"exit": 7,
|
||||
"headers": [],
|
||||
"metadata": {
|
||||
"http_code": 0,
|
||||
"remote_ip": "",
|
||||
"remote_port": -1,
|
||||
"local_ip": "",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 2.040384,
|
||||
"exitcode": 7
|
||||
},
|
||||
"stderr": "curl: (7) Failed to connect to 109.189.21.248:3000 after 2040 ms: Could not connect to server"
|
||||
},
|
||||
{
|
||||
"name": "direct_3000_109.189.21.248_xff",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"http://109.189.21.248:3000/api/auth/config",
|
||||
"--header",
|
||||
"Host: jobs.cesnimda.uk",
|
||||
"--header",
|
||||
"X-Forwarded-For: 198.51.100.10"
|
||||
],
|
||||
"exit": 7,
|
||||
"headers": [],
|
||||
"metadata": {
|
||||
"http_code": 0,
|
||||
"remote_ip": "",
|
||||
"remote_port": -1,
|
||||
"local_ip": "",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 2.049992,
|
||||
"exitcode": 7
|
||||
},
|
||||
"stderr": "curl: (7) Failed to connect to 109.189.21.248:3000 after 2049 ms: Could not connect to server"
|
||||
},
|
||||
{
|
||||
"name": "direct_3000_109.189.21.248_proto",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"http://109.189.21.248:3000/api/auth/config",
|
||||
"--header",
|
||||
"Host: jobs.cesnimda.uk",
|
||||
"--header",
|
||||
"X-Forwarded-Proto: https"
|
||||
],
|
||||
"exit": 7,
|
||||
"headers": [],
|
||||
"metadata": {
|
||||
"http_code": 0,
|
||||
"remote_ip": "",
|
||||
"remote_port": -1,
|
||||
"local_ip": "",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 2.041284,
|
||||
"exitcode": 7
|
||||
},
|
||||
"stderr": "curl: (7) Failed to connect to 109.189.21.248:3000 after 2041 ms: Could not connect to server"
|
||||
},
|
||||
{
|
||||
"name": "direct_3000_109.189.21.248_host",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"http://109.189.21.248:3000/api/auth/config",
|
||||
"--header",
|
||||
"Host: attacker.example.test"
|
||||
],
|
||||
"exit": 7,
|
||||
"headers": [],
|
||||
"metadata": {
|
||||
"http_code": 0,
|
||||
"remote_ip": "",
|
||||
"remote_port": -1,
|
||||
"local_ip": "",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 2.053169,
|
||||
"exitcode": 7
|
||||
},
|
||||
"stderr": "curl: (7) Failed to connect to 109.189.21.248:3000 after 2053 ms: Could not connect to server"
|
||||
},
|
||||
{
|
||||
"name": "direct_3000_109.189.21.248_combined",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"http://109.189.21.248:3000/api/auth/config",
|
||||
"--header",
|
||||
"Host: jobs.cesnimda.uk",
|
||||
"--header",
|
||||
"X-Forwarded-For: 198.51.100.10",
|
||||
"--header",
|
||||
"X-Forwarded-Proto: https",
|
||||
"--header",
|
||||
"X-Forwarded-Host: attacker.example.test"
|
||||
],
|
||||
"exit": 7,
|
||||
"headers": [],
|
||||
"metadata": {
|
||||
"http_code": 0,
|
||||
"remote_ip": "",
|
||||
"remote_port": -1,
|
||||
"local_ip": "",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 2.042874,
|
||||
"exitcode": 7
|
||||
},
|
||||
"stderr": "curl: (7) Failed to connect to 109.189.21.248:3000 after 2042 ms: Could not connect to server"
|
||||
},
|
||||
{
|
||||
"name": "direct_3000_109.189.21.248_malformed_chain",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"http://109.189.21.248:3000/api/auth/config",
|
||||
"--header",
|
||||
"Host: jobs.cesnimda.uk",
|
||||
"--header",
|
||||
"X-Forwarded-For: not-an-ip, 198.51.100.10",
|
||||
"--header",
|
||||
"X-Forwarded-Proto: https"
|
||||
],
|
||||
"exit": 7,
|
||||
"headers": [],
|
||||
"metadata": {
|
||||
"http_code": 0,
|
||||
"remote_ip": "",
|
||||
"remote_port": -1,
|
||||
"local_ip": "",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 2.042874,
|
||||
"exitcode": 7
|
||||
},
|
||||
"stderr": "curl: (7) Failed to connect to 109.189.21.248:3000 after 2042 ms: Could not connect to server"
|
||||
},
|
||||
{
|
||||
"name": "direct_api_109.189.21.248_5202",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"http://109.189.21.248:5202/health",
|
||||
"--header",
|
||||
"Host: jobs.cesnimda.uk"
|
||||
],
|
||||
"exit": 7,
|
||||
"headers": [],
|
||||
"metadata": {
|
||||
"http_code": 0,
|
||||
"remote_ip": "",
|
||||
"remote_port": -1,
|
||||
"local_ip": "",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 2.043141,
|
||||
"exitcode": 7
|
||||
},
|
||||
"stderr": "curl: (7) Failed to connect to 109.189.21.248:5202 after 2043 ms: Could not connect to server"
|
||||
},
|
||||
{
|
||||
"name": "direct_api_109.189.21.248_8080",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"http://109.189.21.248:8080/health",
|
||||
"--header",
|
||||
"Host: jobs.cesnimda.uk"
|
||||
],
|
||||
"exit": 7,
|
||||
"headers": [],
|
||||
"metadata": {
|
||||
"http_code": 0,
|
||||
"remote_ip": "",
|
||||
"remote_port": -1,
|
||||
"local_ip": "",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 2.040558,
|
||||
"exitcode": 7
|
||||
},
|
||||
"stderr": "curl: (7) Failed to connect to 109.189.21.248:8080 after 2040 ms: Could not connect to server"
|
||||
}
|
||||
],
|
||||
"vantage": "Connor Windows workstation; external independence/NAT hairpin not established",
|
||||
"limitations": [
|
||||
"No assertion of Traefik identity or upstream header values from public status alone",
|
||||
"Connection failure does not identify which firewall/NAT/listener rejected it"
|
||||
]
|
||||
},
|
||||
"publicFollowup": [
|
||||
{
|
||||
"name": "observed_http_redirect_target",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"https://jobs.cesnimda.uk:444/api/auth/config"
|
||||
],
|
||||
"exit": 7,
|
||||
"headers": [],
|
||||
"metadata": {
|
||||
"http_code": 0,
|
||||
"remote_ip": "",
|
||||
"remote_port": -1,
|
||||
"local_ip": "",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 2.063806,
|
||||
"exitcode": 7
|
||||
},
|
||||
"stderr": "curl: (7) Failed to connect to jobs.cesnimda.uk:444 after 2063 ms: Could not connect to server"
|
||||
},
|
||||
{
|
||||
"name": "https_forged_host",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"https://jobs.cesnimda.uk/api/auth/config",
|
||||
"--header",
|
||||
"Host: attacker.example.test"
|
||||
],
|
||||
"exit": 0,
|
||||
"headers": [
|
||||
"HTTP/1.1 421 Misdirected Request",
|
||||
"Content-Type: text/plain; charset=utf-8",
|
||||
"X-Content-Type-Options: nosniff",
|
||||
"Content-Length: 20"
|
||||
],
|
||||
"metadata": {
|
||||
"http_code": 421,
|
||||
"remote_ip": "109.189.21.248",
|
||||
"remote_port": 443,
|
||||
"local_ip": "192.168.50.2",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 0.041514,
|
||||
"exitcode": 0
|
||||
},
|
||||
"stderr": ""
|
||||
},
|
||||
{
|
||||
"name": "https_malformed_chain",
|
||||
"command": [
|
||||
"curl.exe",
|
||||
"--noproxy",
|
||||
"*",
|
||||
"--connect-timeout",
|
||||
"4",
|
||||
"--max-time",
|
||||
"8",
|
||||
"--silent",
|
||||
"--show-error",
|
||||
"--dump-header",
|
||||
"-",
|
||||
"--output",
|
||||
"NUL",
|
||||
"--write-out",
|
||||
"\nB06_META %{json}\n",
|
||||
"https://jobs.cesnimda.uk/api/auth/config",
|
||||
"--header",
|
||||
"X-Forwarded-For: not-an-ip, 198.51.100.10",
|
||||
"--header",
|
||||
"X-Forwarded-Proto: https"
|
||||
],
|
||||
"exit": 0,
|
||||
"headers": [
|
||||
"HTTP/1.1 200 OK",
|
||||
"Content-Type: application/json; charset=utf-8",
|
||||
"Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()",
|
||||
"Referrer-Policy: same-origin",
|
||||
"Strict-Transport-Security: max-age=63072000; includeSubDomains; preload",
|
||||
"X-Content-Type-Options: nosniff",
|
||||
"X-Frame-Options: SAMEORIGIN"
|
||||
],
|
||||
"metadata": {
|
||||
"http_code": 200,
|
||||
"remote_ip": "109.189.21.248",
|
||||
"remote_port": 443,
|
||||
"local_ip": "192.168.50.2",
|
||||
"ssl_verify_result": 0,
|
||||
"time_total": 0.037985,
|
||||
"exitcode": 0
|
||||
},
|
||||
"stderr": ""
|
||||
}
|
||||
],
|
||||
"dns": [
|
||||
{
|
||||
"Query": "A",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "jobs.cesnimda.uk",
|
||||
"Type": 1,
|
||||
"IPAddress": "109.189.21.248",
|
||||
"NameHost": null,
|
||||
"TTL": 208
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Query": "AAAA",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "cesnimda.uk",
|
||||
"Type": 6,
|
||||
"IPAddress": null,
|
||||
"NameHost": null,
|
||||
"TTL": 1708
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Query": "CNAME",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "jobs.cesnimda.uk",
|
||||
"Type": 5,
|
||||
"IPAddress": null,
|
||||
"NameHost": "cesnimda.uk",
|
||||
"TTL": 300
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"authoritativeDns": [
|
||||
{
|
||||
"Server": "oaklyn.ns.cloudflare.com",
|
||||
"Query": "A",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "jobs.cesnimda.uk",
|
||||
"Type": 1,
|
||||
"IPAddress": "109.189.21.248",
|
||||
"NameHost": null,
|
||||
"TTL": 300
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Server": "oaklyn.ns.cloudflare.com",
|
||||
"Query": "AAAA",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "cesnimda.uk",
|
||||
"Type": 6,
|
||||
"IPAddress": null,
|
||||
"NameHost": null,
|
||||
"TTL": 1800
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Server": "oaklyn.ns.cloudflare.com",
|
||||
"Query": "CNAME",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "jobs.cesnimda.uk",
|
||||
"Type": 5,
|
||||
"IPAddress": null,
|
||||
"NameHost": "cesnimda.uk",
|
||||
"TTL": 300
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Server": "thaddeus.ns.cloudflare.com",
|
||||
"Query": "A",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "jobs.cesnimda.uk",
|
||||
"Type": 1,
|
||||
"IPAddress": "109.189.21.248",
|
||||
"NameHost": null,
|
||||
"TTL": 300
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Server": "thaddeus.ns.cloudflare.com",
|
||||
"Query": "AAAA",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "cesnimda.uk",
|
||||
"Type": 6,
|
||||
"IPAddress": null,
|
||||
"NameHost": null,
|
||||
"TTL": 1800
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Server": "thaddeus.ns.cloudflare.com",
|
||||
"Query": "CNAME",
|
||||
"Records": [
|
||||
{
|
||||
"Name": "jobs.cesnimda.uk",
|
||||
"Type": 5,
|
||||
"IPAddress": null,
|
||||
"NameHost": "cesnimda.uk",
|
||||
"TTL": 300
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dockerStart": {
|
||||
"command": [
|
||||
"C:/Users/Connor/AppData/Local/Programs/DockerDesktop/resources/bin/docker.exe",
|
||||
"desktop",
|
||||
"start",
|
||||
"--detach"
|
||||
],
|
||||
"exit": 0,
|
||||
"stdout": "\u00e2\u0153\u201c Starting Docker Desktop\n",
|
||||
"stderr": ""
|
||||
},
|
||||
"dockerEngine": {
|
||||
"exit": null,
|
||||
"timeoutSeconds": 10,
|
||||
"command": [
|
||||
"docker",
|
||||
"version",
|
||||
"--format",
|
||||
"{{json .Server}}"
|
||||
],
|
||||
"utc": "2026-09-09T10:48:29.844623+00:00"
|
||||
},
|
||||
"dockerFailure": {
|
||||
"message": "starting services: initializing Inference manager: listening on unix://C:/Users/Connor/AppData/Local/Docker/run/dockerInference: remove C:/Users/Connor/AppData/Local/Docker/run/dockerInference: The file cannot be accessed by the system. (listener: The filename, directory name, or volume label syntax is incorrect.)",
|
||||
"socketObserved": {
|
||||
"length": 0,
|
||||
"attributes": [
|
||||
"Archive",
|
||||
"ReparsePoint"
|
||||
],
|
||||
"lastWriteDate": "2026-08-24",
|
||||
"reparseQueryExit": 1,
|
||||
"reparseQueryError": 1920
|
||||
},
|
||||
"gracefulStop": "Did not complete; waiting command interrupted. Desktop/backend processes remained. No forced process termination or socket/data changes."
|
||||
},
|
||||
"windowsWorkstation": {
|
||||
"listeners": {
|
||||
"LocalAddress": "127.0.0.1",
|
||||
"LocalPort": 8080,
|
||||
"OwningProcess": 17996
|
||||
},
|
||||
"port8080Process": "steamwebhelper; unrelated to Jobjakt",
|
||||
"firewallProfiles": [
|
||||
{
|
||||
"Name": "Domain",
|
||||
"Enabled": 1,
|
||||
"DefaultInboundAction": 0,
|
||||
"DefaultOutboundAction": 0
|
||||
},
|
||||
{
|
||||
"Name": "Private",
|
||||
"Enabled": 1,
|
||||
"DefaultInboundAction": 0,
|
||||
"DefaultOutboundAction": 0
|
||||
},
|
||||
{
|
||||
"Name": "Public",
|
||||
"Enabled": 1,
|
||||
"DefaultInboundAction": 0,
|
||||
"DefaultOutboundAction": 0
|
||||
}
|
||||
],
|
||||
"productionHost": false
|
||||
},
|
||||
"sshDiscovery": {
|
||||
"configExists": false,
|
||||
"nonSecretRepoSources": "Only PROD_HOST/PROD_USER/PROD_SSH_KEY secret references found; no unambiguous alias/user",
|
||||
"sshConnectionAttempted": false,
|
||||
"credentialsAccessed": false
|
||||
},
|
||||
"runtimeTopologyStarted": false,
|
||||
"observedTraefikPeer": null,
|
||||
"actualBackendClientIp": null,
|
||||
"applicationConfigurationChangesThisFollowup": []
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Bounded, read-only B06 observations; no credentials, cookies or response bodies retained."""
|
||||
import concurrent.futures
|
||||
import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
import socket
|
||||
import ssl
|
||||
import subprocess
|
||||
|
||||
OUT = Path(__file__).resolve().parent
|
||||
HOST = "jobs.cesnimda.uk" # .env.example canonical production hostname
|
||||
SAFE_HEADERS = {"server", "location", "content-type", "content-length", "strict-transport-security",
|
||||
"x-content-type-options", "x-frame-options", "referrer-policy", "permissions-policy"}
|
||||
|
||||
|
||||
def request(name, url, headers=()):
|
||||
command = ["curl.exe", "--noproxy", "*", "--connect-timeout", "4", "--max-time", "8",
|
||||
"--silent", "--show-error", "--dump-header", "-", "--output", "NUL",
|
||||
"--write-out", "\nB06_META %{json}\n", url]
|
||||
for header in headers:
|
||||
command.extend(["--header", header])
|
||||
result = subprocess.run(command, capture_output=True, text=True, timeout=12)
|
||||
raw_headers, _, metadata = result.stdout.partition("B06_META ")
|
||||
safe = []
|
||||
for line in raw_headers.splitlines():
|
||||
if line.startswith("HTTP/") or line.partition(":")[0].lower() in SAFE_HEADERS:
|
||||
safe.append(line)
|
||||
try:
|
||||
info = json.loads(metadata)
|
||||
meta = {key: info.get(key) for key in ("http_code", "remote_ip", "remote_port", "local_ip",
|
||||
"ssl_verify_result", "time_total", "exitcode")}
|
||||
except ValueError:
|
||||
meta = {}
|
||||
return {"name": name, "command": command, "exit": result.returncode, "headers": safe,
|
||||
"metadata": meta, "stderr": result.stderr.strip()}
|
||||
|
||||
|
||||
def certificate():
|
||||
try:
|
||||
context = ssl.create_default_context()
|
||||
with socket.create_connection((HOST, 443), timeout=8) as tcp:
|
||||
with context.wrap_socket(tcp, server_hostname=HOST) as tls:
|
||||
return {"verified": True, "peer": tls.getpeername(), "tls": tls.version(),
|
||||
"certificate": tls.getpeercert()}
|
||||
except Exception as error:
|
||||
return {"verified": False, "error": str(error)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
addresses = sorted({item[4][0] for item in socket.getaddrinfo(HOST, None, type=socket.SOCK_STREAM)})
|
||||
cases = [
|
||||
("public_https", f"https://{HOST}/api/auth/config", []),
|
||||
("public_http", f"http://{HOST}/api/auth/config", []),
|
||||
("public_https_forged_forwarding", f"https://{HOST}/api/auth/config",
|
||||
["X-Forwarded-For: 198.51.100.10", "X-Forwarded-Proto: http", "X-Forwarded-Host: attacker.example.test"]),
|
||||
]
|
||||
for address in addresses:
|
||||
literal = f"[{address}]" if ":" in address else address
|
||||
for name, headers in [
|
||||
("none", []),
|
||||
("xff", ["X-Forwarded-For: 198.51.100.10"]),
|
||||
("proto", ["X-Forwarded-Proto: https"]),
|
||||
("host", ["Host: attacker.example.test"]),
|
||||
("combined", ["X-Forwarded-For: 198.51.100.10", "X-Forwarded-Proto: https",
|
||||
"X-Forwarded-Host: attacker.example.test"]),
|
||||
("malformed_chain", ["X-Forwarded-For: not-an-ip, 198.51.100.10", "X-Forwarded-Proto: https"]),
|
||||
]:
|
||||
actual = headers if name == "host" else [f"Host: {HOST}"] + headers
|
||||
cases.append((f"direct_3000_{address}_{name}", f"http://{literal}:3000/api/auth/config", actual))
|
||||
for port in (5202, 8080):
|
||||
cases.append((f"direct_api_{address}_{port}", f"http://{literal}:{port}/health", [f"Host: {HOST}"]))
|
||||
# Three simultaneous harmless GETs at most; no login, mutations or quota exhaustion.
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:
|
||||
observations = list(pool.map(lambda args: request(*args), cases))
|
||||
report = {"utc": datetime.datetime.now(datetime.timezone.utc).isoformat(), "hostname": HOST,
|
||||
"dns_addresses": addresses, "certificate": certificate(), "requests": observations,
|
||||
"vantage": "Connor Windows workstation; external independence/NAT hairpin not established",
|
||||
"limitations": ["No assertion of Traefik identity or upstream header values from public status alone",
|
||||
"Connection failure does not identify which firewall/NAT/listener rejected it"]}
|
||||
(OUT / "public-observations.json").write_text(json.dumps(report, indent=2), encoding="utf-8")
|
||||
print(json.dumps(report, indent=2))
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"ticket": "B06 runtime follow-up",
|
||||
"utc": "2026-09-09T16:06:49.668967+00:00",
|
||||
"backend": {
|
||||
"total": "166",
|
||||
"executed": "166",
|
||||
"passed": "166",
|
||||
"failed": "0",
|
||||
"error": "0",
|
||||
"timeout": "0",
|
||||
"aborted": "0",
|
||||
"inconclusive": "0",
|
||||
"passedButRunAborted": "0",
|
||||
"notRunnable": "0",
|
||||
"notExecuted": "0",
|
||||
"disconnected": "0",
|
||||
"warning": "0",
|
||||
"completed": "0",
|
||||
"inProgress": "0",
|
||||
"pending": "0"
|
||||
},
|
||||
"http": {
|
||||
"productionHttp": {
|
||||
"startTime": "2026-09-09T10:47:16.476Z",
|
||||
"duration": 7559.485,
|
||||
"expected": 6,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
},
|
||||
"b04Http": {
|
||||
"startTime": "2026-09-09T10:47:36.514Z",
|
||||
"duration": 13004.1,
|
||||
"expected": 9,
|
||||
"skipped": 0,
|
||||
"unexpected": 0,
|
||||
"flaky": 0
|
||||
}
|
||||
},
|
||||
"compose": {
|
||||
"passed": 3,
|
||||
"adversarialMutations": 17,
|
||||
"preflightExit": 0
|
||||
},
|
||||
"runtimeTopology": "Blocked: Docker engine unavailable",
|
||||
"priorBroaderEvidence": "b06-test-results.json; not rerun/relabelled",
|
||||
"sourceHashes": {
|
||||
"docker-compose.yml": "4ba32b2a61989fc068001addfc11cea3f04eaf1fa06baa2c26981b02ed75f526",
|
||||
"docker-compose.dev.yml": "93795a046410680fe580e2e2a0e475847852675f747a8d0fef43f71a05255f02",
|
||||
"job-tracker-ui/nginx.conf.template": "fbf478ccf84d7abad36155f56b3c06b0428b9c255cbca3c57473312d391435cc",
|
||||
"job-tracker-ui/configure-nginx-origin.sh": "142a4f5d2cb348b636eda7602d17d4a7313fa22644933008b7fee8b094110b20",
|
||||
"JobTrackerApi/Program.cs": "bbf8bdf2fd1d4af671ed81230265c66ab54cce6ca9bd5895f067753850fb7d14",
|
||||
"JobTrackerApi/Services/ForwardedProxyConfiguration.cs": "7f514586a082b5004056ae4e02afbd083bce4ec4181dd516cc7fe5ef454eacf4",
|
||||
"scripts/validate-ingress.py": "4872155c960a583cf646e1d82397617febf39c0ff3d71dcb62dcf672eeaa02bf"
|
||||
},
|
||||
"rawArtifacts": {
|
||||
"compose-effective.json": {
|
||||
"sha256": "f2df4491f8cdea58b45d9dffa1e7ad275bf3a52586788e455a88dff304dcdbbb",
|
||||
"bytes": 9992
|
||||
},
|
||||
"compose-summary.json": {
|
||||
"sha256": "104b5fcd0d69eaa54933785bbbb10c60f37460746e4f4fdb6c992bb7c06db9ec",
|
||||
"bytes": 1849
|
||||
},
|
||||
"config-tests.log": {
|
||||
"sha256": "303a0b2b8dbd504e7349cb2a51bd4300e1d5a2b1f4752afc7757a788e88a2748",
|
||||
"bytes": 106
|
||||
},
|
||||
"dns-authoritative.json": {
|
||||
"sha256": "cef242601f48748fc04624c0359e318a28b5b8737134bd4997fc4d1d1bfe0522",
|
||||
"bytes": 1547
|
||||
},
|
||||
"dns-nameservers.json": {
|
||||
"sha256": "46bef28f0faea3fdc350361b4281cb2202b2ddee3e1c4f53beaba309204e33c8",
|
||||
"bytes": 243
|
||||
},
|
||||
"dns.json": {
|
||||
"sha256": "fb8ab0cb3ca931e1b7dd421ab1110d94283563123f0ab4408db0d0656005941a",
|
||||
"bytes": 644
|
||||
},
|
||||
"docker-engine.json": {
|
||||
"sha256": "681f367adebe6b1660900483fd08169b6950ee13e21255de3dac5bd37b8967c8",
|
||||
"bytes": 185
|
||||
},
|
||||
"docker-engine.log": {
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"bytes": 0
|
||||
},
|
||||
"docker-start.json": {
|
||||
"sha256": "282494220ad25c8baa73b7e56fd9b2abf5ac7d7129514d507e3facbb8d9abe29",
|
||||
"bytes": 249
|
||||
},
|
||||
"ingress-api-stderr.log": {
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"bytes": 0
|
||||
},
|
||||
"ingress-api.json": {
|
||||
"sha256": "069f35cc5661df4f95b65da1c02b7bd4d48dab214de9a1517f20591f4c367540",
|
||||
"bytes": 10052
|
||||
},
|
||||
"preflight.log": {
|
||||
"sha256": "a6908efb401b7502ed125ce099b0394160aa3d1f50cd97bdb476a779af849fd0",
|
||||
"bytes": 101
|
||||
},
|
||||
"probe.py": {
|
||||
"sha256": "b5ad60d09f7710eab5a7d389e53387efd190b2f16c312abad1f4b5e0fbea6ad8",
|
||||
"bytes": 4393
|
||||
},
|
||||
"public-followup.json": {
|
||||
"sha256": "4d0ea815c0e6faf86d15f42d05e460520b491c83a748a261473269319b586b02",
|
||||
"bytes": 2912
|
||||
},
|
||||
"public-observations.json": {
|
||||
"sha256": "ce15b075650f8897ccc5b9417989c08697677352b3063b23757c6578216b3852",
|
||||
"bytes": 12591
|
||||
},
|
||||
"rate-limits-stderr.log": {
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"bytes": 0
|
||||
},
|
||||
"rate-limits.json": {
|
||||
"sha256": "d69bdf0c86e2789470e1a6b653ffeb24f1d14a9e528d1b80bf5756190e32e6e9",
|
||||
"bytes": 14395
|
||||
},
|
||||
"regressions.log": {
|
||||
"sha256": "1d215cbb352a54d85f2f739dded31adf27d4ad8c6dc1e9aafe2e00628c2b77fb",
|
||||
"bytes": 859
|
||||
},
|
||||
"regressions.trx": {
|
||||
"sha256": "6a80d605d8766ae0f171159d2a4233171877e549008d67f2f13853911f2e9bee",
|
||||
"bytes": 258140
|
||||
},
|
||||
"windows-firewall-profiles.json": {
|
||||
"sha256": "00934da15a36b9e3e8e82eee6b061bb114dc4865bc5aad75afe37161a04abf01",
|
||||
"bytes": 357
|
||||
},
|
||||
"windows-listeners.json": {
|
||||
"sha256": "c7f2be7b50f66d49b821870224d53a668f1b37c2e40a90e6a2ed6875fdcb9345",
|
||||
"bytes": 86
|
||||
},
|
||||
"diff-check.log": {
|
||||
"sha256": "73f0da2f8d5f4b51e09c9fe9c9914808c601c29c79df39e22cc4d39c64d53c7b",
|
||||
"bytes": 4026
|
||||
}
|
||||
},
|
||||
"diffCheck": {
|
||||
"exit": 0,
|
||||
"notes": "Git LF-to-CRLF notices; no whitespace errors"
|
||||
},
|
||||
"probeSyntax": "passed",
|
||||
"evidenceJson": "all three new JSON files parse"
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
{
|
||||
"ticket": "B06",
|
||||
"date": "2026-09-09",
|
||||
"status": "Implementation present; local topology and production verification incomplete",
|
||||
"results": {
|
||||
"productionHttp": {
|
||||
"passed": 6
|
||||
},
|
||||
"lint": {
|
||||
"exit": 0
|
||||
},
|
||||
"focusedBackend": {
|
||||
"passed": 30
|
||||
},
|
||||
"releaseApi": {
|
||||
"warnings": 0,
|
||||
"errors": 0,
|
||||
"exit": 0
|
||||
},
|
||||
"b03Admission": {
|
||||
"passed": 1
|
||||
},
|
||||
"shellSyntax": {
|
||||
"exit": 0
|
||||
},
|
||||
"browser": {
|
||||
"flaky": 0,
|
||||
"passed": 18
|
||||
},
|
||||
"nginxGeneration": {
|
||||
"passed": 9,
|
||||
"nginxRuntimeCheck": false
|
||||
},
|
||||
"typeScript": {
|
||||
"preExisting": "src/interview-prep.test.tsx(120,10): TS2571",
|
||||
"exit": 1
|
||||
},
|
||||
"b04Http": {
|
||||
"passed": 9
|
||||
},
|
||||
"releaseUi": {
|
||||
"exit": 0
|
||||
},
|
||||
"frontend": {
|
||||
"suites": 67,
|
||||
"passed": 298
|
||||
},
|
||||
"backend": {
|
||||
"skipped": 0,
|
||||
"passed": 915,
|
||||
"excluded": "One pre-existing external-DNS IMAP test, not authorized"
|
||||
},
|
||||
"config": {
|
||||
"adversarialMutations": 17,
|
||||
"passed": 3
|
||||
},
|
||||
"topology": {
|
||||
"blocked": true,
|
||||
"exit": 1,
|
||||
"passed": false
|
||||
}
|
||||
},
|
||||
"rawArtifacts": [
|
||||
{
|
||||
"path": ".artifacts\\b06\\compose-before.json",
|
||||
"sha256": "75e54b0ea70449f558e39e71726a66d8b76fb37f202a831a6db61bfe26b33df5"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\compose-after.json",
|
||||
"sha256": "c296d84614d30af6c86714816ba3cf13f6d99936115f41490700ec4329c1cbb4"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\config-before.log",
|
||||
"sha256": "04faeccf7f7921071797f79c2598d4cb9b62abe002d2314a3ffaac7d0973b33a"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\config-after.log",
|
||||
"sha256": "87b9200e68ab8f8a2c45b7a10aadb08566aca31868ef3b3bd940d5b5e6a8c7b8"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\focused.trx",
|
||||
"sha256": "d5b8bdd96ab89fdd33d886baa386630f3848a6c4ffb140cd4428359988d21c0c"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\backend.trx",
|
||||
"sha256": "0359405752d4892363a282cbaecac9cf762a6e789d1919c904016112036af212"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\ingress-api.json",
|
||||
"sha256": "91eaeddf553e36f89e32a1c7a76e9ec5835f90436c398c1ddd53b1d322a3a4f1"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\rate-limits.json",
|
||||
"sha256": "ad4bbd48cef90daf05c0760db496019f1835fbf216cc232f4dff28ba10d6718f"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\pdf-admission.json",
|
||||
"sha256": "d685192c7943e0d72b01483610b1913285bad09ba60160c6e3bf3269703f6d89"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\frontend.json",
|
||||
"sha256": "c02e5af87e6be0d679f0af44cdd5df7366554843e2d8c326ee32cfca50ce17b8"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\browser.json",
|
||||
"sha256": "7b05ed5e7c7b3fd52201bfb845bae43887fde1da4fb510db0b39a6a869eb565b"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\release-api.log",
|
||||
"sha256": "f46f190e97c022c0d1ecaae8de03680e267bd21b866cf722c76018f3ba3645ae"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\release-ui.log",
|
||||
"sha256": "8c5a8f22d14fddb8969520e182f56c19e066350ae1830997b7be44ab8269164f"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\lint.log",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\typescript.log",
|
||||
"sha256": "2f9960647abadefd054152a9566e4e6557a55383e98627aed742b93edebb16dd"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\shell-syntax.log",
|
||||
"sha256": "66ae0a81de0ac43e2b0acc263c168c4716072368f3265699733e9054a03c94f4"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\render-probe.json",
|
||||
"sha256": "e4a776186029656716786452a866ba510c4aa899f3a23917a38da0d74bda725e"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\nginx-proxy.conf",
|
||||
"sha256": "2027619cf3294138bc272a7c2e25089bbe5465faee4c798cea763862c3ffc1d9"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\nginx-direct.conf",
|
||||
"sha256": "7a9573711cd424cbc4544ef341717556a6d84e56cb0a0a277cf58b13d35946f0"
|
||||
},
|
||||
{
|
||||
"path": ".artifacts\\b06\\topology.log",
|
||||
"sha256": "100b36f3954f4c0823117c2cab42694135128dcac29f78097d0c696df2165312"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
Traceback (most recent call last):
|
||||
File "E:\Storage\Projects\Active\Job tracker\scripts\test-ingress-topology.py", line 126, in <module>
|
||||
main()
|
||||
File "E:\Storage\Projects\Active\Job tracker\scripts\test-ingress-topology.py", line 27, in main
|
||||
docker("info", "--format", "{{.ServerVersion}}")
|
||||
File "E:\Storage\Projects\Active\Job tracker\scripts\test-ingress-topology.py", line 18, in docker
|
||||
raise RuntimeError(result.stderr.strip())
|
||||
RuntimeError: failed to connect to the docker API at npipe:////./pipe/dockerDesktopLinuxEngine; check if the path is correct and if the daemon is running: open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified.
|
||||
@@ -0,0 +1,88 @@
|
||||
[
|
||||
{
|
||||
"probe": "URL guard with non-network HTTP fake",
|
||||
"target": "http://10.1.2.3/",
|
||||
"Calls": 0,
|
||||
"Parser": "none",
|
||||
"Error": "Local or private network URLs are not allowed."
|
||||
},
|
||||
{
|
||||
"probe": "URL guard with non-network HTTP fake",
|
||||
"target": "http://[::ffff:10.1.2.3]/",
|
||||
"Calls": 1,
|
||||
"Parser": "universal",
|
||||
"Error": "No JobPosting schema found."
|
||||
},
|
||||
{
|
||||
"probe": "URL guard with non-network HTTP fake",
|
||||
"target": "http://169.254.169.254/",
|
||||
"Calls": 0,
|
||||
"Parser": "none",
|
||||
"Error": "Local or private network URLs are not allowed."
|
||||
},
|
||||
{
|
||||
"probe": "URL guard with non-network HTTP fake",
|
||||
"target": "http://[::ffff:169.254.169.254]/",
|
||||
"Calls": 1,
|
||||
"Parser": "universal",
|
||||
"Error": "No JobPosting schema found."
|
||||
},
|
||||
{
|
||||
"probe": "URL guard with non-network HTTP fake",
|
||||
"target": "http://192.168.1.1/",
|
||||
"Calls": 0,
|
||||
"Parser": "none",
|
||||
"Error": "Local or private network URLs are not allowed."
|
||||
},
|
||||
{
|
||||
"probe": "URL guard with non-network HTTP fake",
|
||||
"target": "http://[::ffff:192.168.1.1]/",
|
||||
"Calls": 1,
|
||||
"Parser": "universal",
|
||||
"Error": "No JobPosting schema found."
|
||||
},
|
||||
{
|
||||
"probe": "URL guard with non-network HTTP fake",
|
||||
"target": "http://[::1]/",
|
||||
"Calls": 0,
|
||||
"Parser": "none",
|
||||
"Error": "Local or private network URLs are not allowed."
|
||||
},
|
||||
{
|
||||
"probe": "URL guard with non-network HTTP fake",
|
||||
"target": "http://[::ffff:127.0.0.1]/",
|
||||
"Calls": 0,
|
||||
"Parser": "none",
|
||||
"Error": "Local or private network URLs are not allowed."
|
||||
},
|
||||
{
|
||||
"probe": "actual PDF exporter with three inert child processes",
|
||||
"concurrentLaunches": 3
|
||||
},
|
||||
{
|
||||
"probe": "two preloaded real SQLite contexts",
|
||||
"Name": "Writer B",
|
||||
"Version": 2,
|
||||
"historyVersions": [
|
||||
1,
|
||||
2,
|
||||
2
|
||||
]
|
||||
},
|
||||
{
|
||||
"probe": "one transient database command failure",
|
||||
"concurrency": 1,
|
||||
"Failures": 1,
|
||||
"hostStopping": true,
|
||||
"hostedTaskFaulted": true,
|
||||
"error": "TimeoutException"
|
||||
},
|
||||
{
|
||||
"probe": "one transient database command failure",
|
||||
"concurrency": 2,
|
||||
"Failures": 1,
|
||||
"hostStopping": false,
|
||||
"hostedTaskFaulted": false,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,421 @@
|
||||
{
|
||||
"date": "2026-09-07T20:47:58.493Z",
|
||||
"syntheticDataRoot": "C:\\Users\\Connor\\AppData\\Local\\Temp\\jobjakt-independent-ju4FBt",
|
||||
"observations": [
|
||||
{
|
||||
"caseName": "stale revision",
|
||||
"firstRevision": 2,
|
||||
"staleStatus": 200,
|
||||
"resultingName": "Stale writer",
|
||||
"resultingRevision": 3
|
||||
},
|
||||
{
|
||||
"caseName": "document owner boundaries",
|
||||
"results": [
|
||||
{
|
||||
"route": "",
|
||||
"status": 404,
|
||||
"body": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"0HNOD452KNGOS:0000000B\"}"
|
||||
},
|
||||
{
|
||||
"route": "/preview",
|
||||
"status": 404,
|
||||
"body": "{\"type\":\"https://tools.ietf.org/html/rfc9110#section-15.5.5\",\"title\":\"Not Found\",\"status\":404,\"traceId\":\"0HNOD452KNGOS:0000000C\"}"
|
||||
},
|
||||
{
|
||||
"route": "/versions",
|
||||
"status": 200,
|
||||
"body": "[]"
|
||||
},
|
||||
{
|
||||
"route": "/export-pdf",
|
||||
"status": 404
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"caseName": "repeated POST with same key",
|
||||
"ids": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"deduplicated": false
|
||||
},
|
||||
{
|
||||
"caseName": "date round trip",
|
||||
"input": "2026-03-29T09:00:00+02:00",
|
||||
"createValue": "2026-03-29T09:00:00+02:00",
|
||||
"reloadValue": "2026-03-29T09:00:00",
|
||||
"start": "DTSTART:20260329T090000Z"
|
||||
},
|
||||
{
|
||||
"caseName": "date round trip",
|
||||
"input": "2026-03-29T07:00:00Z",
|
||||
"createValue": "2026-03-29T07:00:00Z",
|
||||
"reloadValue": "2026-03-29T07:00:00",
|
||||
"start": "DTSTART:20260329T070000Z"
|
||||
},
|
||||
{
|
||||
"caseName": "date round trip",
|
||||
"input": "2026-03-29",
|
||||
"createValue": "2026-03-29T00:00:00",
|
||||
"reloadValue": "2026-03-29T00:00:00",
|
||||
"start": "DTSTART:20260329T000000Z"
|
||||
},
|
||||
{
|
||||
"caseName": "date round trip",
|
||||
"input": "2026-10-25T02:30:00+02:00",
|
||||
"createValue": "2026-10-25T02:30:00+02:00",
|
||||
"reloadValue": "2026-10-25T02:30:00",
|
||||
"start": "DTSTART:20261025T023000Z"
|
||||
},
|
||||
{
|
||||
"caseName": "date round trip",
|
||||
"input": "2026-10-25T02:30:00+01:00",
|
||||
"createValue": "2026-10-25T02:30:00+01:00",
|
||||
"reloadValue": "2026-10-25T02:30:00",
|
||||
"start": "DTSTART:20261025T023000Z"
|
||||
},
|
||||
{
|
||||
"caseName": "CSV arithmetic payload",
|
||||
"containsUnneutralizedFormula": true
|
||||
},
|
||||
{
|
||||
"caseName": "owner rate isolation",
|
||||
"accountA": [
|
||||
200,
|
||||
200
|
||||
],
|
||||
"firstAccountB": 429
|
||||
},
|
||||
{
|
||||
"caseName": "real UI cross-account stale form save",
|
||||
"oldVisible": "A edit must never enter B account",
|
||||
"saveStatus": 200,
|
||||
"profileB": "A edit must never enter B account",
|
||||
"profileA": "Private profile A"
|
||||
},
|
||||
{
|
||||
"caseName": "builder axe light",
|
||||
"width": 1440,
|
||||
"height": 1000,
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1guz6s > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1d1pejv > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(2) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(4) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(6) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"caseName": "builder axe light",
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1guz6s > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1d1pejv > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(2) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(4) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(6) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"caseName": "builder axe light",
|
||||
"width": 375,
|
||||
"height": 900,
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1guz6s > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1d1pejv > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(2) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(4) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-de8i06.MuiPaper-outlined.MuiPaper-rounded:nth-child(6) > .css-1wz1o3r.MuiStack-root > .css-w4rcel.MuiChip-colorSuccess.MuiChip-outlinedSuccess > .MuiChip-label.MuiChip-labelSmall.css-d6wmh0"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.29 (foreground color: #16a34a, background color: #ffffff, font size: 7.5pt (10px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"caseName": "builder axe dark",
|
||||
"width": 1440,
|
||||
"height": 1000,
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".MuiButtonGroup-lastButton"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 9.8pt (13px), font weight: bold). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".MuiButton-sizeMedium"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".MuiButton-text"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1ebacnx > .MuiButton-outlinedSizeSmall.css-3r48dj.MuiButton-outlined"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".Mui-selected.MuiTab-root.MuiTab-textColorPrimary"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-va5cz9 > .MuiButton-outlinedSizeSmall.css-3r48dj.MuiButton-outlined"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "scrollable-region-focusable",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1yn80jq"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element should have focusable content\n Element should be focusable"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"caseName": "scroll region structure",
|
||||
"tag": "DIV",
|
||||
"tabIndex": -1,
|
||||
"overflow": "auto",
|
||||
"maxHeight": "860px",
|
||||
"clientHeight": 860,
|
||||
"scrollHeight": 868,
|
||||
"previewPages": 1,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"caseName": "builder axe dark",
|
||||
"width": 1440,
|
||||
"height": 900,
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".MuiButtonGroup-lastButton"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 9.8pt (13px), font weight: bold). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".MuiButton-sizeMedium"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".MuiButton-text"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1ebacnx > .MuiButton-outlinedSizeSmall.css-3r48dj.MuiButton-outlined"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".Mui-selected.MuiTab-root.MuiTab-textColorPrimary"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-va5cz9 > .MuiButton-outlinedSizeSmall.css-3r48dj.MuiButton-outlined"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "scrollable-region-focusable",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".css-1yn80jq"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element should have focusable content\n Element should be focusable"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"caseName": "scroll region structure",
|
||||
"tag": "DIV",
|
||||
"tabIndex": -1,
|
||||
"overflow": "auto",
|
||||
"maxHeight": "774px",
|
||||
"clientHeight": 774,
|
||||
"scrollHeight": 868,
|
||||
"previewPages": 1,
|
||||
"frames": 1
|
||||
},
|
||||
{
|
||||
"caseName": "builder axe dark",
|
||||
"width": 375,
|
||||
"height": 900,
|
||||
"violations": [
|
||||
{
|
||||
"id": "color-contrast",
|
||||
"impact": "serious",
|
||||
"nodes": [
|
||||
{
|
||||
"target": [
|
||||
".MuiButtonGroup-lastButton"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.66 (foreground color: #5b5bd6, background color: #0b0b0e, font size: 9.8pt (13px), font weight: bold). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-a5lp7y > .MuiButton-text.MuiButton-textPrimary.MuiButton-textSizeSmall"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1ebacnx > .MuiButton-text.MuiButton-textPrimary.MuiButton-textSizeSmall"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-1ebacnx > .MuiButton-outlined.MuiButton-outlinedPrimary.MuiButton-outlinedSizeSmall"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".Mui-selected.MuiTab-root.MuiTab-textColorPrimary"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 10.5pt (14px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
},
|
||||
{
|
||||
"target": [
|
||||
".css-va5cz9 > .MuiButton-outlined.MuiButton-outlinedPrimary.MuiButton-outlinedSizeSmall"
|
||||
],
|
||||
"summary": "Fix any of the following:\n Element has insufficient color contrast of 3.5 (foreground color: #5b5bd6, background color: #111116, font size: 9.8pt (13px), font weight: normal). Expected contrast ratio of 4.5:1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"applicationCommit": "7d641edc7bca866ccf06012897ba1d107c07e59f",
|
||||
"backendRuns": [
|
||||
{
|
||||
"artifact": "validation.trx",
|
||||
"total": 70,
|
||||
"passed": 70,
|
||||
"failed": 0,
|
||||
"start": "2026-09-07T17:59:12.5528301+02:00",
|
||||
"finish": "2026-09-07T17:59:17.2551797+02:00",
|
||||
"sha256": "8D27447E90FD7473BB47C9E4D1D57EF567A3F280BD8D547A1EF4072373FA4383"
|
||||
},
|
||||
{
|
||||
"artifact": "security-validation.trx",
|
||||
"total": 79,
|
||||
"passed": 79,
|
||||
"failed": 0,
|
||||
"start": "2026-09-07T22:46:38.2482618+02:00",
|
||||
"finish": "2026-09-07T22:46:44.9279673+02:00",
|
||||
"sha256": "AE89D456BEF9BC418A0F8890DDC9C56857605434CB1536B79C94443576EBAF87"
|
||||
}
|
||||
],
|
||||
"frontend": {
|
||||
"artifact": "interview-jest.json",
|
||||
"suites": 1,
|
||||
"passed": 13,
|
||||
"failed": 0,
|
||||
"sha256": "2F201E9E1D95A1C676337FB36E991BC260EA1CD1C493E1063B3B039684F954ED"
|
||||
},
|
||||
"typecheck": "src/interview-prep.test.tsx(120,10): error TS2571: Object is of type 'unknown'."
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
# Validated implementation queue
|
||||
|
||||
Companion to the [independent verdict](README.md). This is a proposed queue, not authorization to start implementation. Original finding IDs remain stable; V01 strengthens F08 and V02 strengthens F11. No application changes were made by this pass.
|
||||
|
||||
**Status update, 2026-09-09:** the user separately authorized B01–B05, now complete locally: [B01](b01-completion.md), [B02](b02-completion.md), [B03](b03-completion.md), [B04](b04-completion.md), [B05](b05-completion.md). B05 resolves F05 under its documented spreadsheet-text policy; actual Excel before/after proves the arithmetic formula is now text and Norwegian encoding is preserved. Final verification: 902 selected backend, 298 frontend, 18 browser tests, nine B04 rate-policy HTTP scenarios and the unchanged B03 admission probe pass. Release builds and targeted lint pass; R01a's pre-existing standalone TypeScript error remains. HTTP F11/V02, F08/V01 and F06 are resolved locally; overall F11 remains open for B07/deployment. F03 admission is resolved locally; R04 containment/capacity evidence remains open. **B06 was subsequently authorized; implementation and pending verification are recorded below.**
|
||||
|
||||
**B06 runtime follow-up, 2026-09-09:** [Report and operator handoff](b06-completion.md#runtime-verification-follow-up-2026-09-09). **Implementation complete; full runtime/production verification incomplete; F04 open.** Fresh 166 focused backend, 3 Compose, 6 Production HTTP and 9 B04 HTTP checks pass. Public DNS/valid HTTPS are now observed; direct ports 3000/5202/8080 are unreachable from this workstation, without proof of the rejecting layer. HTTP redirects to unreachable HTTPS port 444. Docker's inference socket failure still blocks actual Compose/Traefik/nginx tests; production peers, backend client IP and host/provider firewall remain unverified. No SSH alias/user was found in permitted non-secret sources. B01–B05 preserved; B07 not started.
|
||||
|
||||
**Sizing:** S = one narrow boundary/configuration change and its meaningful tests; M = one cohesive service/API/client contract that can be understood, implemented, tested and reviewed in one focused session. These are scope estimates, not elapsed-time promises. If a ticket needs unrelated modules, split it at the acceptance boundary below. Do not create a generic framework merely to make tickets share code.
|
||||
|
||||
The numbered order is the default integration order. Independent work may start sooner only along the explicitly permitted parallel branches. A conditional feature can stay disabled while unrelated safe work proceeds; a failed recovery gate blocks migrations, not read-only planning or non-schema tests.
|
||||
|
||||
## Batch 0 — Security, identity and immediate admission
|
||||
|
||||
**Findings:** F11/V02, F08/V01, F03, F06, F05 and conditional F04. These require no data migration and address the most direct ways to cross a boundary or consume shared resources. CSV remains P2 but is inexpensive and isolated.
|
||||
|
||||
| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk |
|
||||
|---|---|---|---|
|
||||
| **B01 HTTP import destination enforcement** | M. `Services/JobImport/JobImportService.cs`, resolver/address helper only if needed, named import HTTP registration in `Program.cs`, import/network tests. | Normalize mapped addresses and enforce the same approved-address policy at the actual socket connection. Keep HTTP/HTTPS, redirect refusal, bounded content, cancellation and hostname/TLS verification. Before/after tests must show mapped private/link-local targets never reach a connect/send boundary, validation-to-connect DNS changes cannot redirect a socket privately, and an allowed destination still works. | First ticket. No schema. Network-handler rollback reopens the defect; retain the previous handler only as an emergency operational option with equivalent egress restriction. Do not alter all HttpClients. |
|
||||
| **B02 Cross-account form/session boundary** | M. `src/auth.ts`, `api.ts`, `App.tsx`, profile/editor state binding, a narrow server precondition if required, auth/browser tests. | Bind retained state and outgoing writes to the identity/session generation that loaded it. Synchronize tabs, invalidate owner caches, ignore late responses, refuse mismatched dispatch. Close the shared-cookie race with a server-checked mismatch precondition where client timing alone is insufficient. Test actual A edit → B login in second tab → A Save, asserting B and A database values unchanged; include slow old GET, logout, Back and same-account reauth. | Independent of record revisions, and must precede persistent drafts or editor refactoring. Coordinate `Program.cs` integration with B01/B04 if adding middleware. No schema. Older clients lacking required protection must be forced to refresh/re-auth safely; do not silently fall back to unsafe writes. |
|
||||
| **B03 Shared PDF admission — complete locally** | M. Singleton exporter, PDF callers/error mapping, exporter tests. | [Completion and evidence](b03-completion.md). Default one active render per process, one per document owner, zero waiting; validated options, safe 503/504, cancellation/timeout/shutdown cleanup and failure recovery. Permanent public/variant/job/profile boundary tests plus real Chromium smoke. Implementation inspection corrected the original package reference: packages do not render PDFs. | No schema. The simultaneous owner allowance is enforced inside the exporter independently of middleware; time-window owner/IP rate limits remain B04. Tune process capacity and verify containment under R04. No durable queue or renderer service added. |
|
||||
| **B04 Resolve principal before owner limiting — complete locally** | S. `Program.cs`, existing rate-key helper, permanent real HTTP tests. | [Completion, inventory and evidence](b04-completion.md). Authentication **and endpoint authorization** precede account limiting, including explicit-scheme principal selection; validated owner/IP namespaces, shared-IP account isolation, shared same-account sessions/credentials, preserved IP controls, safe 429/Retry-After. Concurrent, crafted identity, invalid credential, B02 context and trusted/untrusted proxy tests pass. | No schema or policy-window changes. B01/B02/B03 preserved. Process-local counters and B06 deployed ingress verification remain explicit. |
|
||||
| **B05 Spreadsheet-safe CSV fields — complete locally** | S. `ExportController`, small text helper, byte-preserving UI download and export tests. | [Completion and evidence](b05-completion.md). All 13 text columns neutralize leading `= + - @`, including after whitespace/controls/Unicode format characters, before existing CSV escaping. Typed numbers/dates, stored data and JSON remain unchanged. Parser, HTTP/browser-byte and local Excel checks pass; UTF-8 BOM corrects reproduced Norwegian decoding. | No schema/framework/dependency change. Spreadsheet-safe CSV may visibly retain the apostrophe; raw JSON remains available. LibreOffice runtime verification unavailable; defined policy and compatibility limits documented. |
|
||||
| **B06 Verify and close ingress bypass path — implementation complete; runtime verification pending** | S source/config change plus operator verification. Compose/nginx/forwarded-proxy tests/docs. | [Implementation, runtime observations and gates](b06-completion.md). Production render has frontend 3000 and no API publication. Fresh API/config tests pass. DNS/valid public HTTPS and failed direct-port connections are observed; HTTP redirects to unreachable port 444. Actual Compose/Traefik/nginx execution is blocked by Docker failure; deployed peer/IP/firewall evidence is absent. F04 remains open. | Read-only production inspection authorized, but no unambiguous SSH alias/user exists in permitted sources; use operator handoff. Deployment changes remain separate. Complete real-container and production evidence; do not start B07. A bad binding can cause downtime; retain a tested topology-specific rollback. |
|
||||
| **B07 IMAP connected-address enforcement** | M. `ImapService`, focused network policy/connector and IMAP SSRF tests. | Reuse the B01 address-policy conclusion, but respect MailKit's transport. Enforce an approved connected peer on initial connection and reconnect; retain certificate hostname validation and timeouts. Existing mapped normalization is already correct: do not fix it again. | After B01 establishes the small shared policy, with sequential integration if it shares helper code. No schema. Test TLS identity with an injected/local fixture; do not disable validation to connect by IP. |
|
||||
|
||||
**Batch stop condition:** V01's actual persisted cross-account save is prevented; HTTP/IMAP outbound address paths and shared PDF admission have failing-before/passing-after evidence; correct owner/IP partitioning remains enforced. Actual ingress reachability proof is required before a public deployment. B01 alone does not close all of F11.
|
||||
|
||||
## Batch 1 — Release pipeline and recovery before migrations
|
||||
|
||||
**Findings:** F12, F17, F19, conditional F20; the immediate containment portion of F03/F18. This batch intentionally precedes schema-changing concurrency and billing work. A migration is not safe merely because its unit tests pass when the recovery/rollback path is unproven.
|
||||
|
||||
| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk |
|
||||
|---|---|---|---|
|
||||
| **R01a Standalone type gate** | S. `interview-prep.test.tsx`, UI package script, CI workflow. | Narrowly type/narrow the mocked request payload, add explicit standalone type checking to CI. Prove 13 interview tests, standalone tsc, and the changed workflow command. Keep strictness and test coverage. | Can run in parallel with Batch 0. No production-code behavior or schema. Do not weaken the gate to preserve a green build. |
|
||||
| **R01b Production DB configuration validation** | S. Provider-selection code in `Program.cs` and startup tests. | Fail startup for an unknown production provider or missing required configured connection. Preserve deliberate dev SQLite defaults and valid explicit SQLite/MariaDB. Assert no fallback database is created on rejected startup. | Integrate after Batch 0 `Program.cs` mutations. No schema; rejecting an existing invalid environment can prevent boot, so validate deployed inputs first. |
|
||||
| **R02 Consistent recovery-set creation** | M. `deploy.sh`, existing backup runner/scripts, isolated fixture and operator documentation. | Replace live SQLite tar as the DB consistency mechanism; coordinate file inventory/checkpoint or a short write pause, manifest/checksums and incomplete-set marking. Preserve keys/tombstone recovery. For MariaDB, coordinate the file set with the consistent SQL dump. | No new app schema. Can develop synthetic fixture in parallel with Batch 0. A backup that is only present is not yet verified. Keep previous verified sets. |
|
||||
| **R03 Restore drill and schema-compatible rollback** | M. Deploy rollback logic, isolated restore harness and runbook; no production restore in the implementation session. | Restore latest/previous synthetic sets under active-source writes; integrity, FK owner boundaries, attachment hashes, provider-token decryptability and deletion replay must hold. Run previous/new binaries against the intended schema; allow automatic image rollback only when compatible. Record measured RPO/RTO, required key custody and operator steps. | Depends on R02 and Connor's recovery objectives. **Blocks every schema-changing ticket below.** Failed restore is a stop, not permission to reset the source DB. Do not run downgrade migrations automatically. |
|
||||
| **R04 PDF/container containment evidence** | S/M. API Dockerfile/Compose and bounded local container test. | Verify non-root filesystem permissions, resource/PID limits and renderer operation. Evaluate sandbox/local-file flags against real templates. Measure a small controlled burst after B03 and pick capacity from evidence. | Depends on B03; coordinate Compose changes with B06. Container changes can break rendering/storage; verify permission/health/PDF before rollout. A separate renderer service is a decision only if adequate containment cannot otherwise be demonstrated. |
|
||||
| **R05 AI worker survival and recovery** | M. `AiOperationQueue.cs`, worker tests/health signal. | Catch at each worker loop, classify cancellation/transient/fatal outcomes, bound retries and expose lost/degraded workers. Inject claim/owner/result persistence failures at concurrency 1 and 2; core host survives recoverable faults, accepted operations resume after lease recovery, no lost result or endless tight retry. | Independent of schema work; required before enabling this worker. Keep its existing kill switch. Do not change the global background exception policy. |
|
||||
|
||||
**Batch stop condition:** strict frontend typing and configured-provider startup are deterministic; a recovery set actually restores, and compatibility governs rollback. AI/PDF enabled features need their own containment proof. R03 must be green before Batch 2 or paid-ledger schema changes are applied.
|
||||
|
||||
## Batch 2 — Record integrity, intent retries and storage accounting
|
||||
|
||||
**Findings:** F01 and the concrete F10 integrity/admission subset. Agree the convention in D01, then apply it to bounded aggregates. Do not introduce a generic concurrency service, universal event sourcing or globally unique job content.
|
||||
|
||||
| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk |
|
||||
|---|---|---|---|
|
||||
| **D01 CV variant revision protocol** | M. CV variant model/service/controller/editor, context migration and CV tests. | Expected revision/conditional update, transactional history, distinct conflict response and retained draft. Reconcile duplicate revision labels preserving every snapshot, then enforce uniqueness. Include restore-versus-save, stale name/settings and old-client handling. Prove with two contexts and SQLite/MariaDB. | B02 + R03. Establish request/response convention before D02/D03. One cohesive vertical change; keep templates/layout/AI tooling outside the diff. Additive rollout first; old unconditional writers must be rejected or refreshed when enforcement starts. Do not revert to an old writer without a compatibility plan. |
|
||||
| **D02 Career Profile atomic revision/projection** | M. Career profile service/controller/model, legacy user projection, profile page/tests and migration. | Separate aggregate revision from structured-document schema `version`. Atomically replace children, append history and update the derived legacy projection, or document/test a recoverable projection only if a shared transaction is demonstrably impossible. Cover restore, projection-write failure, delete-versus-save and losing drafts. | D01 contract + R03. Preserve old versions/child IDs as required; no identity/profile-field redesign. Same-account transaction correctness does not replace B02. |
|
||||
| **D03a Job mutation preconditions** | M. Job aggregate/controller/status operations, admin undo and generated-draft writers that touch the same aggregate; model/migration/tests. | Inventory exact conflicting write sets; apply the agreed revision rule to user read-modify-write paths and prevent background writes from silently replacing newer user drafts. One conflict must not partially update events/opportunity projection. Explicitly justify field-disjoint writes allowed to commute. | D01 convention + R03. Own the schema snapshot serially. If inventory is too broad, split status/full-edit and draft-generation writers into consecutive tickets with each unprotected path left visibly open. Do not claim F01 closed between them. |
|
||||
| **D03b Job client conflict handling** | M. Existing job detail/edit/status/draft client paths and browser tests. | Send loaded revisions, retain losing edits, explain reload/compare/copy without blind retries; delayed responses cannot replace current owner state. | D03a + B02. Integrate before enabling mandatory job preconditions. No independent schema. No dashboard redesign. |
|
||||
| **D04 Creation intent idempotency** | M. Manual/imported job creation boundary, relevant client submission state, narrow intent persistence and relational tests. | One key per user creation intent, unique owner/key constraint, payload fingerprint and returned resource identity committed with the creation. Same key/same payload returns the original; same key/different payload conflicts; two owners are independent; a deliberate new application uses a new key. Test lost response/retry and concurrent identical requests. | R03 and settled job creation transaction. Can prepare after D01; integrate after D03 schema changes. Do not deduplicate by company/title/URL globally or retrofit every endpoint. Retain intent records for an explicit retry window; expired-key behavior must be documented. |
|
||||
| **D05 Atomic attachment byte reservation** | M. Attachment admission/storage service, owner quota state and focused migration/tests. | Reserve bytes atomically before staging, release/commit correctly on cancellation/failure; two near-limit uploads cannot both pass. DB transaction/reservation works across processes. Preserve existing staged-file reconciliation and safe path handling. | R03; integrate after other schema tickets. No silently deleted files to meet a quota. Reservation cleanup/reconciliation must survive crashes. |
|
||||
| **D06 Generated-file accounting** | M. Existing `AccountOwnedFileInventory`, PDF/artifact/package producers, quota reconciliation/tests. | Account for generated artifacts and temporary/retained export copies with explicit limits and cleanup. Reuse D05 accounting contract; avoid counting only attachments or charging the same retained file twice. Test copy/delete/crash and old existing inventory. | D05 + B03. Rollback retains existing files; a reconciliation disagreement blocks new growth safely and alerts, rather than purging unknown user data. |
|
||||
| **D07 Bounded exports/history growth** | M per export family. Account export first; jobs export separately if needed. | Measure synthetic 1k/10k owner histories, enforce peak-memory/request bounds, stream JSON/rows into the current ZIP/output as needed. Add conservative growth admission where required. Test account export remains available for existing over-quota data. History pruning is excluded until Connor approves restore promises. | D05/D06 for resource contract; R03 for any persistent limits. Do not jump directly to a queued export platform or optimize 100k rows without evidence. No destructive retention migration. |
|
||||
|
||||
**Batch stop condition:** stale client writes cannot win silently; histories remain complete/unambiguous; projection/children updates are atomic; creation retries have a stated contract; bytes cannot exceed admission through concurrency or omitted producers. Both providers need meaningful relational evidence. Preserve all pre-migration data and safe old-client behavior.
|
||||
|
||||
## Batch 3 — Paid account lifecycle
|
||||
|
||||
**Finding:** F02, with retention/deletion policy and conditional premium worker dependencies. Correct entitlement and deletion behavior precedes billing UX. This batch can be prepared earlier, but paid behavior stays gated until it is complete.
|
||||
|
||||
| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk |
|
||||
|---|---|---|---|
|
||||
| **P01 Purchase intent** | M. Billing checkout/gateway, narrow persisted attempt and billing tests. | Stable purchase-attempt identity reused after double-click/timeout, provider idempotency and explicit handling of an existing/pending subscription. Two simultaneous requests and an unknown first result cannot create a second intended purchase. | R03; reuse D04's intent principles, not necessarily its table. Billing schema integration has one owner. Retain attempt/customer IDs during rollback; disable new checkout on unresolved state. |
|
||||
| **P02 Entitlement reconciliation** | M. Billing handler, entitlement derivation, local transaction/reconciliation and tests. | Reconcile relevant current customer subscriptions, not whichever event's subscription was processed last. Atomically commit local provider state and role consequences; failures cause safe retry. Test active+cancelled subscriptions, delayed/out-of-order/concurrent events, duplicate delivery and failure between local writes. | P01/local identity contract. A synchronous design with provider retries is acceptable; durable inbox only if acknowledging deferred work. Reconciliation must recover missed delivery. Old images cannot safely reinterpret a new billing ledger without a reviewed compatibility plan. |
|
||||
| **P03 Deletion versus paid lifecycle** | M. Existing deletion stages, billing cancellation seam, UI confirmation only as required and lifecycle tests. | First guard active/uncertain billing and pending checkout. Then implement the chosen cancellation/access policy; retain provider identifiers until confirmed resolution. Retry unavailable provider safely, handle deletion racing checkout/webhooks, and preserve minimal approved billing/deletion records. | P01/P02 + Connor's deletion/retention policy. Sandbox must prove the charged-account scenario. Do not erase the identity then attempt cancellation. Preserve pending stages across rollback; suspend new deletion admission if reconciliation is unhealthy. |
|
||||
|
||||
**Batch stop condition:** authorized Stripe sandbox confirms one intended purchase, correct entitlement, provider retry/reconciliation and no continuing unresolved charge after completed deletion. Disabled deletion is a temporary containment state, not completion. Enabled premium features also require R05 where applicable. No paid-beta admission until these conditions and recovery/accessibility gates hold.
|
||||
|
||||
## Batch 4 — Accessibility and shared design foundations
|
||||
|
||||
**Finding:** F14 plus the test coverage gap. These tickets may begin in parallel early on their own files; their integration must preserve B02/D01 form behavior. Structural fixes come before cosmetic redesign.
|
||||
|
||||
| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk |
|
||||
|---|---|---|---|
|
||||
| **A01 Semantic theme/component colors** | S/M. `src/theme.ts`, necessary shared MUI overrides and loaded-state accessibility fixtures. | Correct small-text success/primary contrast in light/dark default/hover/focus/selected states. Check actual foreground/background pairs across chips, tabs and outlined buttons; preserve filled-button contrast. | Can proceed independently of backend work. No schema. Theme changes have broad visual impact: inspect both themes on existing pages before integration. Do not change every page or replace MUI. |
|
||||
| **A02 Builder preview keyboard structure** | S. `CvBuilderEditor` preview region and focused browser tests. | Make the overflowing preview region named/focusable as appropriate; prove keyboard scrolling, focus visibility and no trap for iframe/page content. Include mobile→desktop transition, one/multiple pages and editing/preview controls. | A01 can proceed independently, but do not edit this file simultaneously with D01. No schema; avoid blanket tabindex additions. |
|
||||
| **A03 Accessibility coverage and manual gate** | M test/documentation session; defects found become bounded follow-ups. | Loaded builder/profile/job detail + key dialogs/errors, EN/NB, both themes, mobile and desktop. Wait for meaningful data, not merely main. Complete keyboard/NVDA-or-Narrator, zoom/reflow/text spacing, forced colors and focus/error/reauthentication checks. | A01/A02 and stable B02/D01 conflicts. Automated success cannot substitute for manual evidence. No new accessibility compliance claim from axe alone. |
|
||||
|
||||
**Batch stop condition:** primary workflows, save/conflict recovery and preview are operable by keyboard with usable focus and announcements; shared contrast fixes and representative loaded-state axe coverage pass. New primary-flow structural defects block public exposure; purely cosmetic work does not.
|
||||
|
||||
## Batch 5 — Product and workflow corrections
|
||||
|
||||
**Findings:** F21, F07, F15, F13, F16 and F09. These are scoped product/operational fixes, not grounds for a simultaneous whole-app rewrite.
|
||||
|
||||
| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk |
|
||||
|---|---|---|---|
|
||||
| **W01 Date-only calendar contract** | S/M. Calendar shared event conversion/provider-link/ICS tests and minimal labels if necessary. | Follow-ups/deadlines remain dates and export all-day with correct exclusive end date. Test UTC control, offset-bearing inputs separately, DST transitions and both database/server timezone settings. | Can prepare early; avoid database date migration by default. Resolve any real timed-field evidence separately. Existing external events will not be magically corrected; describe behavior for newly exported events. |
|
||||
| **W02 Reminder outcome semantics** | M. Reminder worker/sender result contract and existing durable email-attempt seam where reusable. | Distinguish suppressed, accepted, failed and unknown; persist per-reminder outcome/dedup intent. Test later-send failure, commit failure, disabled sender and uncertain SMTP result. Auth resend remains recoverable; do not promise exactly-once SMTP. | Use R05 failure-boundary convention and R03 before any attempt schema. No second email platform. Provider sandbox/local mailbox sink before enabling revised sending. Unknown sends are not blindly replayed. |
|
||||
| **W03 Sensitive response cache policy** | S. Private export/document/auth response policy and header tests. | Apply appropriate no-store to sensitive personalized responses, retain public/shared route semantics and verify proxy behavior. Handle CSP separately if it affects preview frames. | B02 is independently required; headers do not fix stale React state. No schema. Test public CV/static assets and PDF preview before changing global headers. |
|
||||
| **W04 Admin event/data minimization** | M. Admin diagnostics and security mutation metadata; targeted logging cleanup may be a separate S ticket. | Minimize unnecessary private fields, record high-impact actor/action/target/outcome, avoid full URLs/documents/provider errors in logs, test role revocation/denied verbs. Preserve intentional admin capabilities. | Connor's actual support/retention policy; R03 before new audit storage. No speculative support role, impersonation or tamper-proof claims. Audit record migration must retain existing evidence. |
|
||||
| **W05 Draft recovery decision and narrow pilot** | M, one editor only. Existing draft/dirty-state helpers and the chosen editor. | Keep warnings and in-memory edits; if approved, add opt-in owner/schema/expiry-scoped local recovery with explicit restore. Test offline, expired session, denied/quota storage, account change and conflicting revision. | **Must follow B02 and D01/D02**, plus Connor's local-content decision. No service worker/PWA or indiscriminate private caches. Rollback must not mix or silently apply old drafts. |
|
||||
|
||||
**Batch stop condition:** each workflow's narrower acceptance passes; no silent time shift, suppressed-as-delivered message, ownerless draft or accidental private logging. Decisions about retention/local drafts remain explicit rather than hidden in implementation defaults.
|
||||
|
||||
## Batch 6 — Broader UX and measured operational hardening
|
||||
|
||||
**Findings:** remaining F16/F18 and measured F10 scale concerns; original product proposals with no reproduced blocker. CPU admission, quota safety, restore and worker containment are already in earlier batches.
|
||||
|
||||
| Order / ticket | Size and ownership | Concrete scope and acceptance | Dependencies / rollback risk |
|
||||
|---|---|---|---|
|
||||
| **U01 Route-by-route UX correction** | S/M per chosen workflow, one page/interaction family. | Prioritize observed task completion problems, terminology and empty/error/loading states. Preserve owner/revision/focus primitives. Test the full user task in EN/NB and both themes. | B02/D01–D03/A01–A03. Do not redesign every route in one session; no schema unless separately scoped. Revert one route safely. |
|
||||
| **O01 Reproducible release inputs** | S/M per build ecosystem. API Docker restore/SDK inputs first, then other lock/pin/scan evidence. | Actual image build honors intended SDK/build props and lock files; retain release provenance and an explicit advisory exception policy. Verify supported versions/current advisories only when external access is authorized. | Coordinate with R04 Dockerfile and R01 CI ownership; do not combine broad runtime upgrades with audit repairs. Rebuild/test the changed artifact and preserve known-good images. |
|
||||
| **O02 Capacity, monitoring and operator drills** | M per measured bottleneck or drill. | Measure representative rows/export sizes/query plans/RSS; fix demonstrated hotspots, alert on storage, restored-backup age, worker health and billing divergence. Validate support/deletion/key rotation/incident procedures. | Early bounds and recovery already complete. Tune limits with evidence; no speculative Redis, broker, read replica or service split. No destructive load against production. |
|
||||
|
||||
**Batch stop condition:** release evidence matches the intended audience and enabled features. Public paid readiness depends on verified real operational/support behavior, not completion of every cosmetic item in the original audit inventory.
|
||||
|
||||
## Safe parallel work and prohibited overlap
|
||||
|
||||
- B01 HTTP boundary, B02 identity analysis/tests, B03 exporter, B05 CSV, R01a typing, R02 synthetic backup fixture and A01 theme can proceed independently with assigned file ownership. Shared `Program.cs`, CI and Compose edits still integrate sequentially.
|
||||
- B07 follows the address-policy decision in B01. Do not develop competing outbound-policy helpers or two versions of the same HTTP handler.
|
||||
- Only **one migration/context snapshot owner** integrates at a time. D01 → D02 → D03a → D04 → D05/P01 schema commits are serialized even if isolated test/client work is prepared in parallel.
|
||||
- D03b may be prepared against D03a's agreed API but cannot independently change the protocol. P01/P02/P03 share billing state and must integrate in that order; do not build billing UX against unsettled entitlement rules.
|
||||
- B02/auth/editor lifecycle must settle before W05 persistent drafts. D01 and A02 both touch `CvBuilderEditor`: serialize edits or explicitly divide non-overlapping responsibility and review their combined behavior.
|
||||
- A01 shared tokens precedes U01 visual redesign. A03 final accessibility evidence follows the combined form/identity/conflict changes, even if fixtures are written earlier.
|
||||
- R02/R03 may run alongside non-schema security fixes. Their failure blocks migration/deployment advancement; it does not block safe source inspection or independent unit-test preparation.
|
||||
|
||||
## B01 first-session handoff — completed 2026-09-08 (scope retained)
|
||||
|
||||
**Take B01 only. Objective:** prevent HTTP job import from connecting to private/local/link-local destinations through mapped IPv4 or a validation-versus-connection DNS change, while preserving legitimate imports.
|
||||
|
||||
1. Read the validated F11/V02 evidence and current `JobImportService`, `IHostAddressResolver`, named import HttpClient registration and existing import tests. Reproduce the mapped-address guard failure with a non-network fake.
|
||||
2. Add the narrow mapped-address normalization and move/enforce policy at the actual connection boundary. Prefer the existing .NET HTTP/socket facilities; preserve original hostname/SNI/certificate checks, redirects disabled, bounded response size and cancellation. Do not modify unrelated HttpClients or add a network framework.
|
||||
3. Add regression tests for IPv4/IPv6/mapped/loopback/link-local/mixed answers, differing resolution answers, allowed address, timeout/cancel and TLS hostname behavior. Use controlled local/fake transport; no real private host/metadata or third-party DNS calls. A validator-only unit test does not prove the DNS race is closed.
|
||||
4. Run the focused backend import/network tests and the affected API build. Review the diff for accidental request, TLS, proxy or parser changes; document any deployment egress assumptions and the remaining IMAP ticket.
|
||||
|
||||
**Owned files:** import service and a narrowly justified adjacent address/connection helper, the named import HTTP registration in `Program.cs`, and related tests. **Excluded:** IMAP implementation (B07), CV/billing/schema/UI changes, deployment, global policy rewrites and dependency upgrades. No migration. If safe connected-peer enforcement cannot be verified locally, retain the passing normalization fix as a bounded partial change but leave B01/F11 open; do not claim validator-only protection closes SSRF.
|
||||
|
||||
**Handoff acceptance:** the retained exploit fixtures fail against the original and pass against the change; connection-time tests prove no denied peer is attempted; allowed requests still behave correctly; no TLS relaxation, source data change or unrelated edits. These criteria now pass for B01; see the [completion record](b01-completion.md). Subsequently authorized B02–B05 also pass their local acceptance criteria with documented limits; see their completion records above. B06 was subsequently authorized and implemented, with local topology and production verification still pending. Complete those gates before closure; B07 and the broad overhaul remain unstarted.
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><TargetFramework>net9.0</TargetFramework><OutputType>Exe</OutputType><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../../JobTrackerApi.Tests/JobTrackerApi.Tests.csproj" />
|
||||
<Compile Include="../../../docs/audits/2026-09-07/validation/BoundaryProbes.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,181 @@
|
||||
// Independent audit validation. Synthetic local data only; no product behavior changes.
|
||||
// Requires existing Release API build and static UI export targeting http://localhost:5402/api.
|
||||
import { createRequire } from 'node:module';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
||||
import { createWriteStream } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
const root = process.cwd(), ui = path.join(root, 'job-tracker-ui');
|
||||
const require = createRequire(path.join(ui, 'package.json'));
|
||||
const { chromium, request } = require('@playwright/test');
|
||||
const AxeBuilder = require('@axe-core/playwright').default;
|
||||
const output = path.join(root, '.artifacts/audit-validation');
|
||||
await mkdir(output, { recursive: true });
|
||||
const data = await mkdtemp(path.join(os.tmpdir(), 'jobjakt-independent-'));
|
||||
const api = 'http://localhost:5402', site = 'http://localhost:3400';
|
||||
const password = 'ValidationSynthetic123!';
|
||||
const observations = [], children = [];
|
||||
let browser;
|
||||
const record = (caseName, facts) => { observations.push({ caseName, ...facts }); console.log(JSON.stringify({ caseName, ...facts })); };
|
||||
function start(exe, args, cwd, env, log) {
|
||||
const child = spawn(exe, args, { cwd, env: { ...process.env, ...env }, windowsHide: true });
|
||||
const sink = createWriteStream(path.join(output, log));
|
||||
child.stdout.pipe(sink); child.stderr.pipe(sink);
|
||||
children.push(child);
|
||||
}
|
||||
async function ready(url) {
|
||||
for (let i = 0; i < 100; i++) {
|
||||
try { if ((await fetch(url, { signal: AbortSignal.timeout(500) })).ok) return; } catch {}
|
||||
await new Promise(r => setTimeout(r, 400));
|
||||
}
|
||||
throw Error('Owned test service unavailable: ' + url);
|
||||
}
|
||||
async function json(response) {
|
||||
if (!response.ok()) throw Error('Fixture HTTP ' + response.status() + ': ' + response.url());
|
||||
return response.json();
|
||||
}
|
||||
async function client(email) {
|
||||
const c = await request.newContext({ baseURL: api });
|
||||
await json(await c.post('/api/auth/login', { data: { email, password } }));
|
||||
const csrf = (await c.storageState()).cookies.find(x => x.name === 'XSRF-TOKEN').value;
|
||||
return { c, h: { 'X-CSRF-TOKEN': csrf } };
|
||||
}
|
||||
async function uiLogin(page, email) {
|
||||
await page.goto(site + '/login');
|
||||
await page.getByLabel('Email').fill(email);
|
||||
await page.getByLabel('Current password').fill(password);
|
||||
await page.getByRole('button', { name: 'Sign in', exact: true }).click();
|
||||
await page.waitForURL('**/dashboard');
|
||||
}
|
||||
try {
|
||||
for (const origin of [api, site]) {
|
||||
try {
|
||||
await fetch(origin + '/health', { signal: AbortSignal.timeout(300) });
|
||||
throw Error('Refusing to reuse occupied audit port ' + origin);
|
||||
} catch (e) { if (String(e).includes('Refusing')) throw e; }
|
||||
}
|
||||
start(process.env.DOTNET_HOST_PATH || path.join(os.homedir(), '.dotnet/dotnet.exe'),
|
||||
[path.join(root, 'JobTrackerApi/bin/Release/net9.0/JobTrackerApi.dll'), '--urls', api],
|
||||
path.join(root, 'JobTrackerApi'), {
|
||||
ASPNETCORE_ENVIRONMENT: 'Development', DOTNET_CLI_TELEMETRY_OPTOUT: '1',
|
||||
Data__Root: data, Database__Provider: 'sqlite', ConnectionStrings__JobTracker: 'Data Source=' + path.join(data, 'jobtracker.db'),
|
||||
AccountLifecycle__TombstonesRoot: path.join(data, 'tombstones'),
|
||||
Auth__Require: 'true', Auth__JwtKey: 'independent-validation-synthetic-key-at-least32',
|
||||
Auth__AdminEmail: 'validation-admin@example.test', Auth__AdminPassword: password,
|
||||
Auth__TurnstileSecretKey: '', Auth__GoogleClientId: '', Auth__MicrosoftClientId: '',
|
||||
Auth__AllowRegistration: 'false', App__PublicBaseUrl: site, Cors__Origins__0: site,
|
||||
Ai__BaseUrl: 'http://127.0.0.1:1', Email__Enabled: 'false', Stripe__SecretKey: '',
|
||||
Workers__AiOperationsEnabled: 'false', Workers__RulesEnabled: 'false',
|
||||
Workers__FollowUpRemindersEnabled: 'false', Workers__DailyExportEnabled: 'false',
|
||||
Workers__JobEnrichmentEnabled: 'false', Backups__Enabled: 'false',
|
||||
CV_PDF_BROWSER_PATH: chromium.executablePath(),
|
||||
}, 'api.log');
|
||||
await ready(api + '/health');
|
||||
const admin = await client('validation-admin@example.test');
|
||||
for (const email of ['validation-a@example.test', 'validation-b@example.test']) {
|
||||
await json(await admin.c.post('/api/users', { headers: admin.h, data: { email, password, roles: [] } }));
|
||||
}
|
||||
const a = await client('validation-a@example.test'), b = await client('validation-b@example.test');
|
||||
for (const [c, name] of [[a, 'Private profile A'], [b, 'Private profile B']]) {
|
||||
await json(await c.c.put('/api/career/profile', { headers: c.h, data: { profile: {
|
||||
version: '1', contact: { fullName: name }, summary: [name + ' factual summary'], skills: ['C#'],
|
||||
} } }));
|
||||
}
|
||||
const variant = await json(await a.c.post('/api/cv/variants', { headers: a.h, data: { name: 'Independent variant' } }));
|
||||
const first = await json(await a.c.put('/api/cv/variants/' + variant.id, { headers: a.h, data: { name: 'First writer', settings: { ...variant.settings, titleOverride: 'New content' }, version: variant.version } }));
|
||||
const secondResponse = await a.c.put('/api/cv/variants/' + variant.id, { headers: a.h, data: { name: 'Stale writer', settings: variant.settings, version: variant.version } });
|
||||
const second = await json(secondResponse);
|
||||
record('stale revision', { firstRevision: first.version, staleStatus: secondResponse.status(), resultingName: second.name, resultingRevision: second.version });
|
||||
const wrongOwner = [];
|
||||
for (const route of ['', '/preview', '/versions']) {
|
||||
const response = await b.c.get('/api/cv/variants/' + variant.id + route);
|
||||
wrongOwner.push({ route, status: response.status(), body: await response.text() });
|
||||
}
|
||||
wrongOwner.push({ route: '/export-pdf', status: (await b.c.post('/api/cv/variants/' + variant.id + '/export-pdf', { headers: b.h })).status() });
|
||||
record('document owner boundaries', { results: wrongOwner });
|
||||
const company = await json(await a.c.post('/api/companies', { headers: a.h, data: { name: 'Synthetic validation company' } }));
|
||||
const ids = [];
|
||||
for (let i = 0; i < 2; i++) ids.push((await json(await a.c.post('/api/jobapplications', {
|
||||
headers: { ...a.h, 'Idempotency-Key': 'same-intent-synthetic' },
|
||||
data: { companyId: company.id, jobTitle: '=1+1', status: 'Saved' },
|
||||
}))).id);
|
||||
record('repeated POST with same key', { ids, deduplicated: ids[0] === ids[1] });
|
||||
for (const stamp of ['2026-03-29T09:00:00+02:00', '2026-03-29T07:00:00Z', '2026-03-29', '2026-10-25T02:30:00+02:00', '2026-10-25T02:30:00+01:00']) {
|
||||
const job = await json(await a.c.post('/api/jobapplications', { headers: a.h, data: { companyId: company.id, jobTitle: 'Date control', status: 'Saved', followUpAt: stamp } }));
|
||||
const reload = await json(await a.c.get('/api/jobapplications/' + job.id));
|
||||
const ics = await (await a.c.get('/api/calendar/jobs/' + job.id + '/events.ics?kind=follow-up')).text();
|
||||
record('date round trip', { input: stamp, createValue: job.followUpAt, reloadValue: reload.followUpAt, start: ics.match(/^DTSTART[^\r\n]*/m)?.[0] });
|
||||
}
|
||||
const csv = await (await a.c.get('/api/export/jobs?format=csv')).text();
|
||||
record('CSV arithmetic payload', { containsUnneutralizedFormula: csv.includes(',=1+1,') });
|
||||
const exports = [];
|
||||
for (let i = 0; i < 2; i++) exports.push((await a.c.post('/api/export/account', { headers: a.h, data: {} })).status());
|
||||
exports.push((await b.c.post('/api/export/account', { headers: b.h, data: {} })).status());
|
||||
record('owner rate isolation', { accountA: exports.slice(0, 2), firstAccountB: exports[2] });
|
||||
start(process.execPath, [path.join(ui, 'scripts/serve-export.mjs')], ui, { PORT: '3400' }, 'ui.log');
|
||||
await ready(site);
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
|
||||
await context.route('**/*', route => {
|
||||
const url = new URL(route.request().url());
|
||||
return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'about:', 'blob:'].includes(url.protocol) ? route.continue() : route.abort();
|
||||
});
|
||||
await context.addInitScript(() => { if (!localStorage.getItem('uiLanguage')) localStorage.setItem('uiLanguage', 'en'); });
|
||||
const pageA = await context.newPage();
|
||||
await uiLogin(pageA, 'validation-a@example.test');
|
||||
await pageA.goto(site + '/career?section=profile');
|
||||
const nameA = pageA.getByLabel('Full name', { exact: true });
|
||||
await nameA.waitFor();
|
||||
await nameA.fill('A edit must never enter B account');
|
||||
const pageB = await context.newPage();
|
||||
await pageB.goto(site + '/dashboard');
|
||||
await pageB.getByRole('button', { name: 'User', exact: true }).click();
|
||||
await pageB.getByRole('menuitem', { name: 'Sign out', exact: true }).click();
|
||||
await pageB.waitForURL('**/login');
|
||||
await uiLogin(pageB, 'validation-b@example.test');
|
||||
const oldVisible = await nameA.count() ? await nameA.inputValue() : null;
|
||||
if (oldVisible === 'A edit must never enter B account') {
|
||||
const savedResponse = pageA.waitForResponse(r => r.url().endsWith('/api/career/profile') && r.request().method() === 'PUT');
|
||||
await pageA.getByRole('button', { name: 'Save changes', exact: true }).click();
|
||||
const saveStatus = (await savedResponse).status();
|
||||
const actualB = await json(await b.c.get('/api/career/profile'));
|
||||
const actualA = await json(await a.c.get('/api/career/profile'));
|
||||
record('real UI cross-account stale form save', { oldVisible, saveStatus, profileB: actualB.profile.contact.fullName, profileA: actualA.profile.contact.fullName });
|
||||
} else record('real UI cross-account stale form save', { oldVisible, protectedByClearing: true });
|
||||
await pageB.screenshot({ path: path.join(output, 'account-b-after-switch.png'), fullPage: true });
|
||||
// Reauthenticate A after the intentional UI logout revoked only its browser session.
|
||||
const editor = await context.newPage();
|
||||
await uiLogin(editor, 'validation-a@example.test');
|
||||
await editor.goto(site + '/career/builder/' + variant.id);
|
||||
await editor.getByLabel('CV name', { exact: true }).waitFor();
|
||||
for (const theme of ['light', 'dark']) {
|
||||
await editor.evaluate(theme => localStorage.setItem('jobtracker.themeMode', theme), theme);
|
||||
await editor.reload();
|
||||
await editor.getByLabel('CV name', { exact: true }).waitFor();
|
||||
await editor.waitForFunction(() => [...document.querySelectorAll('[data-cv-page] iframe')].some(frame => frame.contentDocument?.body?.textContent?.includes('Private profile A')));
|
||||
for (const [width, height] of [[1440, 1000], [1440, 900], [375, 900]]) {
|
||||
await editor.setViewportSize({ width, height });
|
||||
const axe = await new AxeBuilder({ page: editor }).withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']).analyze();
|
||||
record('builder axe ' + theme, { width, height, violations: axe.violations.map(v => ({ id: v.id, impact: v.impact, nodes: v.nodes.map(n => ({ target: n.target, summary: n.failureSummary })) })) });
|
||||
for (const node of axe.violations.filter(v => v.id === 'scrollable-region-focusable').flatMap(v => v.nodes)) {
|
||||
if (node.target.length === 1 && typeof node.target[0] === 'string') record('scroll region structure', await editor.locator(node.target[0]).evaluate(el => ({
|
||||
tag: el.tagName, tabIndex: el.tabIndex, overflow: getComputedStyle(el).overflow,
|
||||
maxHeight: getComputedStyle(el).maxHeight, clientHeight: el.clientHeight, scrollHeight: el.scrollHeight,
|
||||
previewPages: el.querySelectorAll('[data-cv-page]').length, frames: el.querySelectorAll('iframe').length,
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
await context.close();
|
||||
} catch (e) {
|
||||
record('probe failure', { error: String(e.stack || e) });
|
||||
process.exitCode = 2;
|
||||
} finally {
|
||||
if (browser) await browser.close();
|
||||
for (const child of children.reverse()) if (child.exitCode === null) {
|
||||
if (process.platform === 'win32') await new Promise(resolve => spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }).on('exit', resolve));
|
||||
else child.kill('SIGTERM');
|
||||
}
|
||||
await writeFile(path.join(output, 'observations.json'), JSON.stringify({ date: new Date().toISOString(), syntheticDataRoot: data, observations }, null, 2));
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
# Verification record
|
||||
|
||||
Assessment date: 2026-09-07. Source baseline: `main`, `7d641edc7bca866ccf06012897ba1d107c07e59f`. The starting working tree was clean. The audit adds only this report directory, an observational harness and a TODO entry. No application/controller/schema/deployment correction is represented as completed.
|
||||
|
||||
## Environment and boundaries
|
||||
|
||||
Windows with installed .NET SDK 9.0.317 at `C:\Users\Connor\.dotnet\dotnet.exe`, Node 24.19.0, the existing UI dependencies/Chromium, and `tools/summarizer/.venv/Scripts/python.exe`. The `dotnet` initially on PATH had no SDK; the explicit SDK path above was used. No dependency installation/download was needed.
|
||||
|
||||
Backend checks used test fixtures and synthetic local data. The normal browser suite starts its own temporary SQLite API on port 5302 and UI on 3300. The additional harness starts an independent temporary SQLite API on 5402 and a production static export on 3400. Fixtures use reserved `example.test` addresses, a clearly synthetic password, a short fictional profile and harmless formula text. Existing development/production databases were not migrated or reset. Fresh temporary test databases do run application migrations.
|
||||
|
||||
No live payment, mail delivery, OAuth login, external AI/NAV integration test, host inspection, production deployment or destructive failure exercise occurred. The adversarial browser aborts non-loopback remote requests, including Google Fonts. Its AI URL is a closed loopback port and relevant integrations/workers are disabled or unconfigured. The normal browser suite ran as written; it is not an all-process egress isolation proof. No current external advisory, price, legal or runtime-support lookup was performed, respecting the repository's restriction on external services.
|
||||
|
||||
This is one local baseline run per normal suite, plus development iterations of the observational harness. Only the final successful harness run is retained as the reported 27 observations. Earlier fixture/selector/locale corrections are not product findings. One successful run does not establish a flake rate or production-provider equivalence.
|
||||
|
||||
## Normal verification
|
||||
|
||||
| Check | Result | Evidence / limit |
|
||||
|---|---|---|
|
||||
| Backend Release tests | **753 passed, 0 failed, 0 skipped** | TRX; reported test execution about 9s after build. Test collection concurrency disabled for this run. |
|
||||
| UI Jest | **65 suites / 279 tests passed**, none failed/pending | JSON; about 93.5s. Tests passing does not imply standalone TypeScript success. |
|
||||
| Parser pytest | **37 passed** | Model loading skipped and HF/Transformers offline; about 15.8s. No live model quality proof. |
|
||||
| Existing Playwright suite | **13 passed, 0 failed, 0 skipped, 0 flaky** | JSON; 159.9s. Covers existing core/a11y/long-content/PDF flows; builder route was outside that axe coverage. |
|
||||
| Supply-chain helper tests | **3 passed** | Local script tests. |
|
||||
| Tracked-secret scan | **Passed** | No high-confidence tracked-file patterns reported; not a scan of Git history, deployed environment or every possible secret. |
|
||||
| Ollama evaluation-helper tests | **5 passed** | Evaluation tooling behavior, not live model factuality or Bokmål quality. |
|
||||
| Browser-extension capture URL tests | **3 passed** | Local Node test suite. |
|
||||
| ESLint, zero-warning threshold | **Passed, exit 0** | Stale Browserslist data notice remains in tooling output. |
|
||||
| Standalone TypeScript | **Failed, exit 2** | `src/interview-prep.test.tsx(120,10): TS2571: Object is of type 'unknown'.` F17. |
|
||||
| Next production static build | **Passed, exit 0** | Next 16.2.12; compiler about 3.6s and build type phase about 10.6s. Output was built for the local audit API. |
|
||||
|
||||
Do not deploy this audit's generated `out/` as a production artifact: it intentionally embeds the loopback test API URL. A real release must rebuild with the intended configuration through its normal process. Build output remains ignored and was not added to Git.
|
||||
|
||||
Commands below use PowerShell. Run the root commands from the repository root and the UI commands from `job-tracker-ui`. These are recorded checks, not permission to run production operations.
|
||||
|
||||
```powershell
|
||||
# Repository root
|
||||
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
|
||||
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1'
|
||||
& 'C:\Users\Connor\.dotnet\dotnet.exe' test JobTrackerApi.Tests\JobTrackerApi.Tests.csproj --configuration Release --no-restore --logger 'trx;LogFileName=expanded-audit.trx' --results-directory .artifacts\expanded-audit -- xUnit.parallelizeTestCollections=false xUnit.maxParallelThreads=1
|
||||
python scripts/test-supply-chain.py
|
||||
python scripts/supply-chain.py scan-secrets
|
||||
python scripts/test-ollama-evaluation.py
|
||||
node --test browser-extension/tests/capture-url.test.mjs
|
||||
|
||||
# tools/summarizer directory
|
||||
$env:AI_SERVICE_SKIP_MODEL_LOAD = '1'
|
||||
$env:HF_HUB_OFFLINE = '1'
|
||||
$env:TRANSFORMERS_OFFLINE = '1'
|
||||
& .venv/Scripts/python.exe -m pytest -q --disable-warnings
|
||||
|
||||
# job-tracker-ui directory
|
||||
node node_modules/jest/bin/jest.js --watchAll=false --runInBand --json --outputFile=../.artifacts/expanded-audit/jest.json
|
||||
node node_modules/typescript/bin/tsc --noEmit --pretty false
|
||||
node node_modules/eslint/bin/eslint.js . --max-warnings=0
|
||||
$env:DOTNET_HOST_PATH = 'C:\Users\Connor\.dotnet\dotnet.exe'
|
||||
$env:Ai__BaseUrl = 'http://127.0.0.1:1'
|
||||
$env:NEXT_TELEMETRY_DISABLED = '1'
|
||||
node node_modules/@playwright/test/cli.js test --reporter=json
|
||||
```
|
||||
|
||||
Console/JSON redirection during the audit retained `.artifacts/expanded-audit/expanded-audit.trx`, `jest.json`, `playwright.json`, `jest.log`, `lint.log`, `typecheck.log`, `build.log` and `playwright-server.log`. These large local logs remain ignored; the selected synthetic evidence below is included with the report. Parser/helper counts were recorded from command output rather than a newly retained test report.
|
||||
|
||||
## Break Jobjakt observations
|
||||
|
||||
The [harness](break-jobjakt.mjs) is an observational tool. **Exit 0 means the harness finished; it does not mean the desired product behavior passed.** An unexpected harness failure exits 2. Product expectations are stored individually as `meetsExpectation`; consume those explicitly if converting a scenario to a regression test. Several expectations are proposed safety contracts, such as same-intent creation deduplication, rather than promises already made by the current API.
|
||||
|
||||
Prerequisites: installed UI packages and Chromium; free loopback ports 5402/3400; a Release API build; a static UI export built with the matching loopback API. The script does not install dependencies. It creates a unique temporary data directory, starts its own child processes and stops those process trees on exit. Synthetic temporary data is retained for diagnosis; it does not delete existing database directories. On this Windows machine its default SDK path works; `DOTNET_HOST_PATH` can override it.
|
||||
|
||||
```powershell
|
||||
# job-tracker-ui directory; test-only build configuration
|
||||
$env:NEXT_TELEMETRY_DISABLED = '1'
|
||||
$env:NEXT_PUBLIC_API_BASE_URL = 'http://localhost:5402/api'
|
||||
node --max-old-space-size=4096 node_modules/next/dist/bin/next build
|
||||
|
||||
# Repository root, after the Release API and static UI builds
|
||||
$env:DOTNET_HOST_PATH = 'C:\Users\Connor\.dotnet\dotnet.exe'
|
||||
node docs/audits/2026-09-07/break-jobjakt.mjs
|
||||
```
|
||||
|
||||
Final run: **27 observations, 22 unmet desired expectations, 5 met**. Multiple states test the same cause: eight builder combinations and four date/offset examples are not twelve unrelated vulnerabilities. These observations feed the 21 prioritized source/observed findings, not a 22-vulnerability severity total.
|
||||
|
||||
| Scenario | Observed result | Interpretation |
|
||||
|---|---|---|
|
||||
| Stale CV save | First and stale PUT both 200; stale value becomes current; version reaches 3 | Missing revision precondition, F01. |
|
||||
| Simultaneous CV save | Both PUTs return 200 | No cross-editor conflict contract; single-editor save queue does not protect another client. |
|
||||
| Another owner's CV GET/PUT/DELETE | All 404 | Three exercised owner boundaries pass. |
|
||||
| Repeated job creation | Both 201, different IDs | No same-intent deduplication. Distinct POST requests are not inherently an HTTP specification violation; define operation identity before fixing. |
|
||||
| Four follow-up offset/DST round trips | All ICS requests 200, all expected UTC instants wrong | F21; exact values below. |
|
||||
| CSV formula | Harmless `=1+1` remains a formula cell | F05; quoting separators alone is insufficient. |
|
||||
| Auth identity, private CV and CSV response caching | No explicit Cache-Control on all three samples | F15 hardening gap; does not prove a cross-user cache leak. |
|
||||
| Standard-user admin audit access | 403 | Exercised role boundary passes. |
|
||||
| Export account isolation | After A's two exports, B's first from same IP gets 429 | Account limiter executes before authenticated identity is populated, F06. |
|
||||
| Builder EN/NB x light/dark x 375/1440 | All eight have serious axe contrast findings; desktop also has scrolling-region keyboard finding; outer overflow0 | F14. Measured viewport is not a zoom or screen-reader test. |
|
||||
| Save while offline | Visible failed-save state | Useful existing recovery feedback passes. |
|
||||
| Confirm reload after failed offline save | Draft not recovered after acknowledging unload warning | F09; the warning exists and was not bypassed accidentally. |
|
||||
| Logout in another tab | Idle private editor remains mounted; subsequent backend identity requests 401 | F08 visual/session UX residue, not successful unauthorized API access. |
|
||||
|
||||
| Input follow-up | Expected ICS UTC | Actual ICS UTC |
|
||||
|---|---|---|
|
||||
| 2026-03-28T09:00:00+01:00 | 20260328T080000Z | 20260328T090000Z |
|
||||
| 2026-03-29T09:00:00+02:00 | 20260329T070000Z | 20260329T090000Z |
|
||||
| 2026-10-25T02:30:00+02:00 | 20261025T003000Z | 20261025T023000Z |
|
||||
| 2026-10-25T02:30:00+01:00 | 20261025T013000Z | 20261025T023000Z |
|
||||
|
||||
The timestamps demonstrate an offset/instant preservation bug, not a defect limited only to DST dates. MariaDB and other server timezones need a permanent round-trip regression after the boundary is corrected. Do not blindly reinterpret existing user dates during migration.
|
||||
|
||||
Raw synthetic observations: [evidence/break-results.json](evidence/break-results.json), generated `2026-09-07T10:27:52.479Z`, SHA-256 `bccc69965d0505800624151fa046d973f845b109ea7ca770f9da8e44e7f3a468`. This contains axe targets/ratios, actual language/theme markers, console/network observations and timings; no real customer data. The included harness differs from the executed final script only by its explanatory header comments.
|
||||
|
||||
Synthetic screenshots (all eight states retained):
|
||||
|
||||
| Locale / theme | 375px | 1440px |
|
||||
|---|---|---|
|
||||
| English / light | [Mobile](evidence/builder-en-light-375.png) | [Desktop](evidence/builder-en-light-1440.png) |
|
||||
| English / dark | [Mobile](evidence/builder-en-dark-375.png) | [Desktop](evidence/builder-en-dark-1440.png) |
|
||||
| Bokmål / light | [Mobile](evidence/builder-nb-light-375.png) | [Desktop](evidence/builder-nb-light-1440.png) |
|
||||
| Bokmål / dark | [Mobile](evidence/builder-nb-dark-375.png) | [Desktop](evidence/builder-nb-dark-1440.png) |
|
||||
|
||||
## Console, network and backend logs
|
||||
|
||||
The normal Playwright suite reported no failure/flakiness, but it does not establish that every normal route is console-clean. The additional browser capture recorded repeated warnings that combining iframe `allow-scripts` and `allow-same-origin` can allow sandbox escape. This is weakened isolation, not proof of exploitable generated HTML. It also recorded intentionally induced offline request failures, post-logout 401s, and Google Fonts blocked by the harness. Those expected failures are not unexplained production outages.
|
||||
|
||||
The NB UI was genuinely set to `nb-NO` rather than just labelled Norwegian in screenshot filenames. It still contains English template descriptions. Light-theme chips included green text at 3.29:1 contrast against white, below the axe threshold for that small text. A short profile displayed a preview horizontal-overflow warning despite zero measured outer-page overflow; those are different measurements. Investigate the document measurement/layout with both fallback and approved fonts.
|
||||
|
||||
Local navigation-to-main measurements were jobs 497ms, career 395ms and builder 382ms. These are single localhost shell measurements, not full data completion, LCP, INP, production p95, waterfall analysis or load capacity. Response-size budgets, duplicate/polling requests, post-unmount requests and production source-map errors require a fuller network/performance sweep.
|
||||
|
||||
The additional API log was inspected for warning/error categories. Fresh SQLite migration produced EF warnings 30200 and 20410 for `AddJobEntityAndProspectStages`: a table rebuild/SQL ordering warning and nontransactional `PRAGMA foreign_keys = 0`, with partial-migration recovery implications. Fresh startup completed; interruption/replay was not tested. Data Protection warning 35 said no XML encryptor was configured for the synthetic local key. These are actionable migration/key-custody evidence, not proof that production has identical conditions. No recurring normal-request `fail:`/exception category was observed in that log. Verbose development SQL includes schema/content field names; it is not shipped as a public diagnostic artifact.
|
||||
|
||||
Tooling reported stale Browserslist metadata. No online update/advisory fetch was done. The standalone TypeScript error is retained above; it was not hidden by the successful build/lint/test result.
|
||||
|
||||
## Outstanding acceptance evidence
|
||||
|
||||
The [coverage index](coverage.md) lists narrower gaps individually. Highest-value remaining checks are:
|
||||
|
||||
1. After fixes, real SQLite and MariaDB revision/idempotency/quota/date tests; concurrent authorization/deletion and shared-account operations.
|
||||
2. An isolated DB-outage/recovery and worker-failure drill; renderer/export stress only after strict resource admission is in place. No 1,000-Chromium or host disk-exhaustion experiment was attempted on the user's computer.
|
||||
3. Actual consistent backup restoration of DB, files, keys and tombstones; interrupted/large migration, old-image compatibility and measured RPO/RTO.
|
||||
4. Stripe sandbox duplicate/order/concurrency/deletion and uncertain email-send reconciliation using safe test identities and sinks.
|
||||
5. Actual proxy/firewall/TLS/host/container/runner/deployment permissions, provider transport and secret-rotation evidence; current advisory/license/support review when external access is authorized.
|
||||
6. Core routes including builder with Narrator/NVDA, complete keyboard flow, actual zoom, text spacing, forced colors, reduced motion, mobile keyboard and generated-document reading order.
|
||||
7. Large synthetic tenants/query plans, browser leak/unmount/cancellation/old-client tests, complete endpoint DTO/serialization/regex/XSS corpus, and live model EN/Bokmål evaluation.
|
||||
|
||||
These gaps are explicit limits of the completed local assessment. The report supplies prioritization and test/rollout criteria; it does not certify the application for public paid operation or mark the proposed overhaul implemented.
|
||||