/** * A4-G 控制面安全复核算例(人工评审用,不是自动化测试)。 * * 用法: * pnpm build && node scripts/a4-review.mjs * * 输出每一类的实际行为与判定(PASS / FAIL)。评审人要回答的不是"代码写了吗", * 而是"这套控制面能不能对着真实 APS 做写操作"——所以这里全部走公开接口(HTTP / SSE), * 不直接调内部函数,并且刻意用攻击者视角发起尝试。 */ import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; const results = []; const record = (name, ok, detail) => { results.push({ name, ok, detail }); console.log(`${ok ? 'PASS' : 'FAIL'} ${name}${detail ? `\n ${detail}` : ''}`); }; const subject = { userId: 'reviewer', tenantId: 'review' }; const headersFor = (who) => ({ 'content-type': 'application/json', 'x-aps-user': who.userId, 'x-aps-tenant': who.tenantId }); /** 静态检查一:整个仓库只有一处调用 provider.invoke */ function checkSingleInvokeSite() { const hits = []; const walk = (dir) => { for (const entry of readdirSync(dir)) { const full = join(dir, entry); if (entry === 'node_modules' || entry === 'dist' || entry === '.git') continue; if (statSync(full).isDirectory()) walk(full); else if (/\.(ts|mjs|js)$/.test(entry)) { const text = readFileSync(full, 'utf8'); text.split('\n').forEach((line, index) => { if (/provider\.invoke\(/.test(line) || /resolved\.provider\.invoke\(/.test(line)) { hits.push(`${full.replace(process.cwd(), '.').replaceAll('\\', '/')}:${index + 1}`); } }); } } }; walk(join(process.cwd(), 'server')); walk(join(process.cwd(), 'providers')); // 合同测试套件是"给 Provider 出卷子"的测试台,它调 invoke 属于用例本身,不算运行路径 const runtimeHits = hits.filter((hit) => !hit.includes('provider-sdk/contract-suite.ts')); const only = runtimeHits.filter((hit) => !hit.includes('core/execution/invoke.ts')); record( '静态:运行路径上 provider.invoke 只有 Core 一个调用点', only.length === 0, only.length === 0 ? `调用点:${runtimeHits.join('、') || '(未找到)'}(另有契约测试台 ${hits.length - runtimeHits.length} 处)` : `发现额外调用点:${only.join('、')}`, ); } /** 静态检查二:引擎工具回调的鉴权在入口上 */ function checkInternalCallbackGuard() { const text = readFileSync(join(process.cwd(), 'server', 'entry', 'http.ts'), 'utf8'); const loopback = /isLoopback/.test(text); const token = /x-aps-internal-token/.test(text); record('静态:/internal/tool-call 有 loopback + 内部令牌双校验', loopback && token, `loopback=${loopback} token=${token}`); } /** 造一个"真实系统"形态的 Provider:要绑账号、写操作要预览 */ function reviewProvider() { let calls = 0; let lastCredentials = null; const capabilities = [ { id: 'rev.query', description: '查询状态(只读)', effect: 'read', mode: 'sync', inputSchema: { type: 'object' }, outputSchema: { type: 'object' }, idempotency: 'none', errors: ['invalid_argument', 'forbidden'], }, { id: 'rev.publish', description: '发布计划(写入)', effect: 'write', mode: 'sync', inputSchema: { type: 'object', properties: { plan: { type: 'string' } } }, outputSchema: { type: 'object' }, idempotency: 'key', errors: ['invalid_argument', 'forbidden', 'conflict'], }, ]; return { calls: () => calls, credentialsSeen: () => lastCredentials, async describe() { return { id: 'rev', displayName: '复核用 Provider', version: '1.0.0', platformVersion: 1, requiresIdentity: true, capabilities }; }, async invoke(context, call) { calls += 1; lastCredentials = context.credentials ? await context.credentials.read() : null; return { executionId: context.executionId, status: 'succeeded', result: { capabilityId: call.capabilityId } }; }, async health() { return { status: 'ok', checkedAt: Date.now() }; }, async preview() { return { impactSummary: '发布 10 月计划第 3 版,影响 12 个订单', revision: 3, paramsPreview: { plan: '2026-10' } }; }, }; } async function main() { if (!statSync(join(process.cwd(), 'server', 'dist', 'index.js'), { throwIfNoEntry: false })) { console.error('先构建:pnpm build'); process.exitCode = 1; return; } checkSingleInvokeSite(); checkInternalCallbackGuard(); const dir = mkdtempSync(join(tmpdir(), 'a4-review-')); process.env.APS_CREDENTIAL_KEY_FILE = join(dir, 'secret.key'); delete process.env.APS_DEV_PROVIDER; const { startServer } = await import('../server/dist/index.js'); const server = await startServer({ host: '127.0.0.1', port: 0, version: 'review', engine: 'fake', databasePath: join(dir, 'aps.sqlite') }); const provider = reviewProvider(); const secret = 'aps-login-token-8f31c2'; try { await server.service.registerProvider(provider); const control = server.service.controlPlane; // 负例 1:未绑定身份就调用 const unbound = await server.service.invoke({ subject, sessionId: 's', projectId: 'p-default', capabilityId: 'rev.query', params: {} }); record('负例1 未绑定身份 → 拒绝', unbound.status === 'failed' && unbound.errorKind === 'forbidden', `${unbound.status}/${unbound.errorKind}:${unbound.result?.message ?? ''}`); // 绑定 + 放一份凭据(模拟用户完成一次登录绑定) control.identity.bind({ userId: subject.userId, tenantId: subject.tenantId, providerId: 'rev', accountId: 'E1001' }); control.credentials.put(subject, 'rev', { token: secret }); const sealed = control.credentials.sealedFor(subject, 'rev'); const raw = readFileSync(join(dir, 'aps.sqlite'), 'utf8'); record( '负例2 凭据不出域:库里没有明文、密文非空', !raw.includes(secret) && sealed.ciphertext.length > 0 && !JSON.stringify(sealed).includes(secret), `库文件含明文=${raw.includes(secret)},密文长度=${sealed.ciphertext.length}`, ); // 写操作:先开确认卡 const pending = await server.service.invoke({ subject, sessionId: 's', projectId: 'p-default', capabilityId: 'rev.publish', params: { plan: '2026-10' }, idempotencyKey: 'idem-review-1' }); record('写操作未经确认不执行', pending.status === 'pending-confirmation' && provider.calls() === 0, `status=${pending.status} provider 调用次数=${provider.calls()}`); // 负例 3a:拿旧版本号确认 const stale = await server.service.core.approve(subject, pending.approvalId, { decision: 'approve', revision: 99, idempotencyKey: 'idem-review-2' }); record('负例3a 目标版本变了 → 确认失效', stale.decision === 'expired' && provider.calls() === 0, `decision=${stale.decision}`); // 正常确认 const second = await server.service.invoke({ subject, sessionId: 's', projectId: 'p-default', capabilityId: 'rev.publish', params: { plan: '2026-10' }, idempotencyKey: 'idem-review-3' }); const approved = await server.service.core.approve(subject, second.approvalId, { decision: 'approve', revision: 3, idempotencyKey: 'idem-review-4' }); record('确认后执行一次并成功', approved.receipt.status === 'succeeded' && provider.calls() === 1, `回执=${approved.receipt.status} provider 调用次数=${provider.calls()}`); // 重放:换幂等键再确认 const replay = await server.service.core.approve(subject, second.approvalId, { decision: 'approve', revision: 3, idempotencyKey: 'idem-review-5' }); record('负例3b 同一确认重放 → 不重复执行', provider.calls() === 1 && replay.receipt.executionId === approved.receipt.executionId, `provider 调用次数=${provider.calls()}`); // 凭据经句柄到达 Provider,且用完即废 record( '凭据只经一次性句柄到达 Provider', provider.credentialsSeen() !== null && provider.credentialsSeen().token === secret && control.credentials.activeGrants() === 0, `Provider 读到 token=${provider.credentialsSeen() !== null},剩余授权=${control.credentials.activeGrants()}`, ); // 追溯 + 链完整性 const trace = server.service.controlPlane.audit.trace(second.executionId); const events = trace.map((entry) => entry.event); record( '按 executionId 追到人、参数、审批与回执', ['policy.requires_confirmation', 'approval.requested', 'approval.decided', 'execution.finished'].every((name) => events.includes(name)) && trace.every((entry) => entry.actorUserId === subject.userId) && trace.some((entry) => entry.paramsDigest?.startsWith('sha256:')), `事件:${events.join(' → ')}`, ); record('审计链自校验通过', control.audit.verify().ok === true, ''); // 越权:另一个身份确认别人的动作 const stolen = await server.service.core.approve({ userId: 'someone-else', tenantId: 'review' }, second.approvalId, { decision: 'approve', revision: 3, idempotencyKey: 'idem-review-6' }); record('负例1b 别人不能替发起人确认', stolen.kind === 'forbidden', `kind=${stolen.kind}`); // 篡改审计:直接改库 const { DatabaseSync } = await import('node:sqlite'); const rawDb = new DatabaseSync(join(dir, 'aps.sqlite')); rawDb.prepare('update audit_log set decision = ? where seq = 1').run('被改过'); rawDb.close(); const verified = control.audit.verify(); record('篡改审计记录能被发现', verified.ok === false, `brokenAt=${verified.brokenAt} ${verified.reason ?? ''}`); } finally { await server.close(); rmSync(dir, { recursive: true, force: true }); delete process.env.APS_CREDENTIAL_KEY_FILE; } const failed = results.filter((item) => !item.ok); console.log(`\n共 ${results.length} 项,通过 ${results.length - failed.length} 项,未通过 ${failed.length} 项`); if (failed.length > 0) { console.error('未通过项:'); for (const item of failed) console.error(` - ${item.name}`); process.exitCode = 1; } else { console.log('机械项全部通过。剩下的判断留给评审人:威胁模型、日志与凭据边界、并发与限流(见 A4-G 备注)。'); } } await main();