diff --git a/tests/e2e/windows.mjs b/tests/e2e/windows.mjs index a9442b0..1d78b4a 100644 --- a/tests/e2e/windows.mjs +++ b/tests/e2e/windows.mjs @@ -223,6 +223,58 @@ async function main() { assert.equal(r.status, 'failed'); assert.equal(r.error.name, 'E_CAPABILITY_DISABLED'); }); + + // ---------- v2.1.0 L2 系统级审计(T05,Windows 真机)---------- + // 字段与 packages/protocol/src/capabilities/manifest/system.ts 的 + // system.audit.l2 returns_schema(四种 action 的属性并集)逐字对齐: + // status → platform/supported/auditpol/legacy_policy_disabled/ + // cmdline_logging/sysmon_installed/file/file_exists(/note) + // collect → collected/written/since/limit/file(/note) + // verify → ok/checked(/broken_at) + // 信任边界(不夸大):本地 audit-l2.log 只是归一化副本,防篡改依赖转发出机。 + await test('v2.1.0 L2 status:Windows 审计策略状态(returns_schema 全字段)', async () => { + const c = await connect(); + const r = await c.invoke('system.audit.l2', { action: 'status' }, 90_000); + c.close(); + assert.equal(r.status, 'ok', `L2 status 失败: ${JSON.stringify(r.error)}`); + assert.equal(r.data.platform, 'windows', 'platform 必须回 windows'); + assert.equal(r.data.supported, true, 'Windows 上必须 supported=true'); + assert.equal(typeof r.data.auditpol, 'string', 'auditpol 原始输出应为字符串'); + assert.equal(typeof r.data.legacy_policy_disabled, 'boolean', 'SCENoApplyLegacyAuditPolicy 判定应为布尔'); + assert.equal(typeof r.data.cmdline_logging, 'boolean', '命令行记录开关应为布尔'); + assert.equal(typeof r.data.sysmon_installed, 'boolean', 'Sysmon 检测结果应为布尔'); + assert.equal(typeof r.data.file, 'string', 'L2 副本路径应为字符串'); + assert.equal(typeof r.data.file_exists, 'boolean', 'file_exists 应为布尔'); + }); + + await test('v2.1.0 L2 collect:拉取 Security 事件归一化落盘(since/limit 回显)', async () => { + const c = await connect(); + const since = Date.now() - 60 * 60 * 1000; // 近 1 小时:缩小扫描窗口,CI 上更快 + const r = await c.invoke('system.audit.l2', { action: 'collect', since, limit: 100 }, 120_000); + c.close(); + // 读 Security 日志需要管理员;非提权环境明确跳过(与 winget 缺失同一容错哲学, + // 不把「环境没条件」伪装成「功能坏了」) + if (r.status === 'failed' && r.error?.name === 'E_EXECUTION_FAILED') return 'skip'; + assert.equal(r.status, 'ok', `L2 collect 失败: ${JSON.stringify(r.error)}`); + assert.equal(r.data.since, since, 'since 必须原样回显(默认 24h 的参数契约)'); + assert.equal(r.data.limit, 100, 'limit 必须原样回显'); + assert.ok(Number.isInteger(r.data.collected) && r.data.collected >= 0, 'collected 应为非负整数'); + assert.ok( + Number.isInteger(r.data.written) && r.data.written >= 0 && r.data.written <= r.data.collected, + 'written 应为非负整数且不超过 collected', + ); + assert.equal(typeof r.data.file, 'string', '落盘路径应为字符串'); + assert.equal(typeof r.data.note, 'string', '信任边界提示必须随 collect 返回(不得假装本地副本不可篡改)'); + }); + + await test('v2.1.0 L2 verify:归一化链完整性校验(空链亦为 ok)', async () => { + const c = await connect(); + const r = await c.invoke('system.audit.l2', { action: 'verify' }, 60_000); + c.close(); + assert.equal(r.status, 'ok', `L2 verify 失败: ${JSON.stringify(r.error)}`); + assert.equal(r.data.ok, true, `L2 链应完整${r.data.ok ? '' : `: ${JSON.stringify(r.data.broken_at)}`}`); + assert.ok(Number.isInteger(r.data.checked) && r.data.checked >= 0, 'checked 应为非负整数'); + }); } finally { agent.kill(); await new Promise((r) => setTimeout(r, 300));