// Live audit of krona-staff against a real PostgreSQL 17. Read-only for the repository: // data is seeded in a throwaway local database, tokens are random and never printed. import { createRequire } from 'node:module'; import { spawn } from 'node:child_process'; import { randomBytes, createECDH } from 'node:crypto'; import net from 'node:net'; const REPO = 'C:/Users/Ochenstarik/Agent_projects/krona-staff-audit'; const require = createRequire(`${REPO}/package.json`); const pg = require('pg'); const { ulid } = require('ulid'); const SUPER = 'postgres://postgres:audit-superuser@127.0.0.1:55432/krona_staff'; const APP = 'postgres://krona_staff_app:audit-app-password@127.0.0.1:55432/krona_staff'; const PORT = 18080; const BASE = `http://127.0.0.1:${PORT}/api/v1/staff`; const TENANT = ulid(); const ACTOR = ulid(); const TERMINAL_ACTOR = ulid(); const MANAGER_TOKEN = randomBytes(32).toString('base64url'); const TERMINAL_TOKEN = randomBytes(32).toString('base64url'); const results = []; function record(id, verdict, detail) { results.push({ id, verdict, detail }); console.log(`[${id}] ${verdict} — ${detail}`); } async function superQuery(sql, params = []) { const client = new pg.Client({ connectionString: SUPER }); await client.connect(); try { return await client.query(sql, params); } finally { await client.end(); } } function startServer(extraEnv) { const env = { ...process.env, NODE_ENV: 'development', HOST: '127.0.0.1', PORT: String(PORT), LOG_LEVEL: 'silent', STAFF_RUNTIME_MODE: 'standalone', DATABASE_URL: APP, STANDALONE_TENANT_ID: TENANT, STANDALONE_ACTOR_ID: ACTOR, STANDALONE_TERMINAL_ACTOR_ID: TERMINAL_ACTOR, ...extraEnv, }; for (const [key, value] of Object.entries(env)) if (value === undefined) delete env[key]; const child = spawn(process.execPath, ['node_modules/tsx/dist/cli.mjs', 'src/server.ts'], { cwd: REPO, env, stdio: ['ignore', 'ignore', 'pipe'], }); let stderr = ''; child.stderr.on('data', (chunk) => (stderr += chunk)); child.stderrText = () => stderr; return child; } async function waitReady() { for (let attempt = 0; attempt < 120; attempt += 1) { try { const response = await fetch(`${BASE}/live`); if (response.ok) return; } catch {} await new Promise((resolve) => setTimeout(resolve, 500)); } throw new Error('server did not start'); } async function stop(child) { child.kill(); await new Promise((resolve) => setTimeout(resolve, 800)); } async function call(method, path, { token, body, headers = {} } = {}) { const response = await fetch(`${BASE}${path}`, { method, headers: { ...(body !== undefined ? { 'content-type': 'application/json' } : {}), ...(token ? { authorization: `Bearer ${token}` } : {}), ...headers, }, body: body !== undefined ? JSON.stringify(body) : undefined, }); const text = await response.text(); let json; try { json = JSON.parse(text); } catch { json = text; } return { status: response.status, json, headers: response.headers }; } function brief(value) { const text = typeof value === 'string' ? value : JSON.stringify(value); return text.length > 220 ? `${text.slice(0, 220)}…` : text; } // ---------------------------------------------------------------- scenario A async function scenarioOpenMode(access) { const server = startServer({ STANDALONE_ACCESS: access }); try { await waitReady(); const portal = await call('GET', '/portal'); const payroll = await call('GET', '/payroll'); const finance = await call('GET', '/finance'); const writeProfession = await call('POST', '/professions', { body: { name: `audit-open-${access}-${Date.now()}` }, }); const session = await call('POST', '/session', { body: { access_token: 'x'.repeat(40) }, headers: { origin: `http://127.0.0.1:${PORT}` }, }); record( 'F2', portal.status === 200 || payroll.status === 200 ? 'ПОДТВЕРЖДЕНО' : 'не подтверждено', `STANDALONE_ACCESS=${access}, без токенов и без Authorization: GET /portal ${portal.status}, GET /payroll ${payroll.status}, GET /finance ${finance.status}, POST /professions ${writeProfession.status}; POST /session ${session.status}`, ); } finally { await stop(server); } } // ---------------------------------------------------------------- scenario B async function seed() { const orgUnit = ulid(); const employee = ulid(); const pushEmployee = ulid(); const session = ulid(); await superQuery( `INSERT INTO staff_org_units (id, tenant_id, code, name) VALUES ($1, $2, 'audit-unit', 'Аудиторская точка')`, [orgUnit, TENANT], ); await superQuery( `INSERT INTO staff_employees (id, tenant_id, personnel_number, display_name, status) VALUES ($1, $3, 'A-001', 'Аудит Сотрудник', 'active'), ($2, $3, 'A-002', 'Аудит Push', 'active')`, [employee, pushEmployee, TENANT], ); await superQuery( `INSERT INTO staff_work_sessions (id, tenant_id, employee_id, org_unit_id, business_date, started_at, ended_at, worked_minutes, status) VALUES ($1, $2, $3, $4, '2026-09-10', '2026-09-10T02:00:00Z', '2026-09-10T10:00:00Z', 480, 'pending_review')`, [session, TENANT, employee, orgUnit], ); return { orgUnit, employee, pushEmployee, session }; } async function scenarioWithTokens() { const ids = await seed(); const server = startServer({ STANDALONE_ACCESS: 'employee', STANDALONE_MANAGER_TOKEN: MANAGER_TOKEN, STANDALONE_TERMINAL_TOKEN: TERMINAL_TOKEN, }); try { await waitReady(); const origin = { origin: `http://127.0.0.1:${PORT}` }; // F4: brute force on /session const statuses = {}; for (let index = 0; index < 60; index += 1) { const response = await call('POST', '/session', { body: { access_token: randomBytes(32).toString('base64url') }, }); statuses[response.status] = (statuses[response.status] ?? 0) + 1; } record( 'F4', statuses[429] ? 'не подтверждено' : 'ПОДТВЕРЖДЕНО', `60 неверных POST /session → ${JSON.stringify(statuses)}`, ); // F3: cookie carries the raw token and survives logout const login = await call('POST', '/session', { body: { access_token: MANAGER_TOKEN } }); const setCookie = login.headers.get('set-cookie') ?? ''; const cookieValue = decodeURIComponent(setCookie.split(';')[0].split('=').slice(1).join('=')); const cookieIsToken = cookieValue === MANAGER_TOKEN; const cookieHeader = setCookie.split(';')[0]; const logout = await call('DELETE', '/session', { headers: { cookie: cookieHeader } }); const afterLogout = await call('GET', '/portal', { headers: { cookie: cookieHeader } }); record( 'F3', cookieIsToken && afterLogout.status === 200 ? 'ПОДТВЕРЖДЕНО' : 'частично', `вход ${login.status}, выход ${logout.status}; cookie содержит сам токен менеджера: ${cookieIsToken}; после DELETE /session старая cookie → GET /portal ${afterLogout.status}; флаги: ${setCookie.split(';').slice(1).map((part) => part.trim()).join('; ')}`, ); // N1: create employee const created = await call('POST', '/employees', { token: MANAGER_TOKEN, body: { personnel_number: 'A-100', display_name: 'Новый Сотрудник', position: 'Повар' }, }); record( 'N1', created.status >= 500 ? 'ПОДТВЕРЖДЕНО' : 'не подтверждено', `POST /employees → ${created.status} ${brief(created.json)}`, ); // N2: confirm session and calculate const confirm = await call('PATCH', `/work-sessions/${ids.session}`, { token: MANAGER_TOKEN, body: { status: 'confirmed' }, }); const rules = await call('GET', '/pay-rules', { token: MANAGER_TOKEN }); const ruleList = rules.json?.pay_rules ?? []; const hourlyRule = ruleList.find((rule) => /hour/.test(String(rule.code))) ?? ruleList[0]; const assignment = hourlyRule ? await call('PUT', `/employees/${ids.employee}/pay-assignment`, { token: MANAGER_TOKEN, body: { pay_rule_id: hourlyRule.id, org_unit_id: ids.orgUnit, rate_minor: 30000, effective_from: '2026-09-01', }, }) : { status: 'нет правил', json: rules.json }; const calculate = await call('POST', '/payroll/calculate', { token: MANAGER_TOKEN, body: { month: '2026-09' }, }); record( 'N2', confirm.status >= 500 || calculate.status >= 500 ? 'ПОДТВЕРЖДЕНО' : 'не подтверждено', `PATCH /work-sessions (confirmed) → ${confirm.status} ${brief(confirm.json)}; GET /pay-rules → ${rules.status} (${ruleList.length} правил, выбрано ${hourlyRule?.code}); PUT pay-assignment → ${assignment.status}; POST /payroll/calculate → ${calculate.status} ${brief(calculate.json)}`, ); // N4: overlapping runs, after bypassing N2 in this throwaway database only await superQuery('ALTER TABLE staff_outbox ALTER COLUMN occurred_at SET DEFAULT now()'); try { const confirmAgain = await call('PATCH', `/work-sessions/${ids.session}`, { token: MANAGER_TOKEN, body: { status: 'confirmed' }, }); const periods = [ { month: '2026-09' }, { from: '2026-09-02', to: '2026-09-30' }, { month: '2026-09', org_unit_id: ids.orgUnit }, ]; const steps = []; for (const period of periods) { const run = await call('POST', '/payroll/calculate', { token: MANAGER_TOKEN, body: period }); const runId = run.json?.run_id ?? run.json?.id ?? run.json?.run?.id; const approve = runId ? await call('POST', `/payroll/runs/${runId}/approve`, { token: MANAGER_TOKEN }) : { status: 'нет run_id', json: run.json }; steps.push(`${JSON.stringify(period)}: calculate ${run.status}, approve ${approve.status}`); } const ledger = await superQuery( `SELECT count(*)::int AS entries, COALESCE(sum(amount_minor), 0)::bigint AS total FROM staff_payroll_ledger WHERE tenant_id = $1 AND employee_id = $2 AND kind = 'accrual'`, [TENANT, ids.employee], ); const { entries, total } = ledger.rows[0]; record( 'N4', entries > 1 ? 'ПОДТВЕРЖДЕНО' : 'не подтверждено', `повторное подтверждение смены → ${confirmAgain.status}; ${steps.join('; ')}; в реестре начислений за одну смену 8 ч × 300 ₽: записей ${entries}, сумма ${Number(total) / 100} ₽`, ); // D5: a session already paid by an approved run can still be rejected or edited const rejectPaid = await call('PATCH', `/work-sessions/${ids.session}`, { token: MANAGER_TOKEN, body: { status: 'rejected', correction_note: 'audit: after approval' }, }); const ledgerAfter = await superQuery( `SELECT count(*)::int AS entries FROM staff_payroll_ledger WHERE tenant_id = $1 AND employee_id = $2`, [TENANT, ids.employee], ); record( 'D5', rejectPaid.status < 300 ? 'ПОДТВЕРЖДЕНО' : 'не подтверждено', `отклонение смены, уже начисленной в утверждённой ведомости → ${rejectPaid.status}; записей в реестре после этого: ${ledgerAfter.rows[0].entries} (сторно не создано)`, ); // N13: two first calculations of the same new period at once await superQuery( `INSERT INTO staff_work_sessions (id, tenant_id, employee_id, org_unit_id, business_date, worked_minutes, status) VALUES ($1, $2, $3, $4, '2026-10-05', 480, 'confirmed')`, [ulid(), TENANT, ids.employee, ids.orgUnit], ); const race = await Promise.all([ call('POST', '/payroll/calculate', { token: MANAGER_TOKEN, body: { month: '2026-10' } }), call('POST', '/payroll/calculate', { token: MANAGER_TOKEN, body: { month: '2026-10' } }), ]); record( 'N13', race.some((response) => response.status >= 500) ? 'ПОДТВЕРЖДЕНО' : 'не воспроизведено', `два одновременных первых расчёта октября → ${race.map((response) => `${response.status} ${brief(response.json).slice(0, 90)}`).join(' | ')}`, ); // Validation holes: impossible month and effective date const badMonth = await call('POST', '/payroll/calculate', { token: MANAGER_TOKEN, body: { month: '2026-13' }, }); const hugeRate = await call('PUT', `/employees/${ids.employee}/pay-assignment`, { token: MANAGER_TOKEN, body: { pay_rule_id: hourlyRule?.id, org_unit_id: ids.orgUnit, rate_minor: 1e20, effective_from: '2026-11-01', unexpected_field: 'accepted?', }, }); record( 'D9', 'проверка', `month=2026-13 → ${badMonth.status}; rate_minor=1e20 и лишнее поле → ${hugeRate.status} ${brief(hugeRate.json).slice(0, 120)}`, ); } finally { await superQuery('ALTER TABLE staff_outbox ALTER COLUMN occurred_at DROP DEFAULT'); } // N3: SSRF through a push subscription const inbound = { connections: 0, tlsHello: false }; const listener = net.createServer((socket) => { inbound.connections += 1; socket.once('data', (chunk) => { if (chunk[0] === 0x16) inbound.tlsHello = true; socket.destroy(); }); }); await new Promise((resolve) => listener.listen(18443, '127.0.0.1', resolve)); try { const issued = await call('POST', `/employees/${ids.pushEmployee}/access-token`, { token: MANAGER_TOKEN, }); const employeeToken = issued.json?.access_token; const ecdh = createECDH('prime256v1'); ecdh.generateKeys(); const subscribe = await call('POST', '/push/subscriptions', { token: employeeToken, body: { endpoint: 'https://127.0.0.1:18443/internal-admin', keys: { p256dh: ecdh.getPublicKey().toString('base64url'), auth: randomBytes(16).toString('base64url'), }, }, }); const clock = await call('POST', '/attendance/events', { token: TERMINAL_TOKEN, body: { employee_id: ids.pushEmployee, event_type: 'check_in', occurred_at: new Date().toISOString(), source_event_id: `audit-${Date.now()}`, org_unit_id: ids.orgUnit, }, }); await new Promise((resolve) => setTimeout(resolve, 3000)); record( 'N3', inbound.connections > 0 ? 'ПОДТВЕРЖДЕНО' : 'не подтверждено', `выдача ключа сотруднику → ${issued.status}; подписка на https://127.0.0.1:18443 → ${subscribe.status}; отметка терминала → ${clock.status}; входящих соединений на внутренний адрес: ${inbound.connections}, TLS ClientHello: ${inbound.tlsHello}`, ); // N9: another employee re-binds the same endpoint const other = await call('POST', `/employees/${ids.employee}/access-token`, { token: MANAGER_TOKEN, }); const rebind = await call('POST', '/push/subscriptions', { token: other.json?.access_token, body: { endpoint: 'https://127.0.0.1:18443/internal-admin', keys: { p256dh: ecdh.getPublicKey().toString('base64url'), auth: randomBytes(16).toString('base64url') }, }, }); const owner = await superQuery( `SELECT employee_id FROM staff_push_subscriptions WHERE tenant_id = $1 AND endpoint = $2`, [TENANT, 'https://127.0.0.1:18443/internal-admin'], ).catch((error) => ({ rows: [{ employee_id: `ошибка: ${error.message}` }] })); record( 'N9', rebind.status < 300 && owner.rows[0]?.employee_id === ids.employee ? 'ПОДТВЕРЖДЕНО' : 'не подтверждено', `сотрудник Б подписывает endpoint сотрудника А → ${rebind.status}; подписка теперь принадлежит сотруднику Б: ${owner.rows[0]?.employee_id === ids.employee}`, ); // A4: internal error details leak in 500 responses const badFk = await call('POST', '/employees', { token: MANAGER_TOKEN, body: { personnel_number: 'A-200', display_name: 'X', profession_id: ulid() }, }); record('A4', 'проверка', `500 с деталями: POST /employees с несуществующей профессией → ${badFk.status} ${brief(badFk.json)}`); // N10: reference data without permission checks const professionsByTerminal = await call('GET', '/professions', { token: TERMINAL_TOKEN }); const orgUnitsByTerminal = await call('GET', '/org-units', { token: TERMINAL_TOKEN }); record( 'N10', 'проверка', `терминал: GET /professions → ${professionsByTerminal.status} (${Array.isArray(professionsByTerminal.json) ? professionsByTerminal.json.length : brief(professionsByTerminal.json)}); GET /org-units → ${orgUnitsByTerminal.status} ${brief(orgUnitsByTerminal.json)}`, ); // Security headers on the portal const page = await fetch(`http://127.0.0.1:${PORT}/staff`); record( 'HDR', 'проверка', `GET /staff: content-security-policy="${page.headers.get('content-security-policy')}", strict-transport-security="${page.headers.get('strict-transport-security')}"`, ); } finally { listener.close(); } } finally { const serverErrors = server.stderrText(); await stop(server); if (serverErrors.trim()) console.log(`server stderr: ${brief(serverErrors)}`); } } await scenarioOpenMode('manager'); await scenarioOpenMode('employee'); await scenarioWithTokens(); console.log('\nJSON', JSON.stringify(results));