Files
jobtrackingapp/docs/audits/2026-09-07/validation/probes.mjs
T
cesnimda 0954876295 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.
2026-09-09 18:21:47 +02:00

182 lines
12 KiB
JavaScript

// 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));
}