From 4939313eb8611b951575f1fdf7c62ede12fa02a1 Mon Sep 17 00:00:00 2001 From: zh Date: Tue, 29 Sep 2026 21:21:08 +0800 Subject: [PATCH] =?UTF-8?q?test(core):=20A4-G=20=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E5=A4=8D=E6=A0=B8=E8=84=9A=E6=9C=AC=EF=BC=88=E4=B8=89=E7=B1=BB?= =?UTF-8?q?=E8=B4=9F=E4=BE=8B=20+=20=E8=B7=AF=E5=BE=84=E7=A9=B7=E4=B8=BE?= =?UTF-8?q?=EF=BC=8C=E5=8F=AF=E8=B7=91=E5=87=BA=E8=AF=81=E6=8D=AE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/a4-review.mjs | 210 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 scripts/a4-review.mjs diff --git a/scripts/a4-review.mjs b/scripts/a4-review.mjs new file mode 100644 index 0000000..ebb93f5 --- /dev/null +++ b/scripts/a4-review.mjs @@ -0,0 +1,210 @@ +/** + * 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(); \ No newline at end of file