/** * 会话取证 · 逐次工具结果分类(沙箱拒绝 / 文件策略拒绝 / 命令错误 / OK)+ 审批事件时间线 * 精确分类:每次 bash 调用的结果性质(沙箱拒绝 / 命令错误 / 成功) * 用法:node classify-bash.mjs */ import { readFileSync } from 'node:fs' import { zstdDecompressSync } from 'node:zlib' const raw = readFileSync(process.argv[2]) const MAGIC = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]) const offs = [] for (let i = 0; i + 4 <= raw.length; i++) if (raw.compare(MAGIC, 0, 4, i, i + 4) === 0) offs.push(i) let text = '' for (let f = 0; f < (offs.length ? offs : [0]).length; f++) { const s = offs[f], e = f + 1 < offs.length ? offs[f + 1] : raw.length try { text += zstdDecompressSync(raw.subarray(s, e)).toString('utf8') } catch {} } const evs = [] for (const l of text.split('\n')) { if (!l.trim()) continue; try { evs.push(JSON.parse(l)) } catch {} } const t = (ms) => new Date(Number(ms)).toLocaleString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' }) // tool/call:id → {name, args(截断)} const callInfo = new Map() for (const e of evs) { if (e.type !== 'tool/call') continue const s = JSON.stringify(e.data ?? {}) const id = (s.match(/"callId":"([^"]+)"/) ?? [])[1] const name = (s.match(/"name":"([^"]+)"/) ?? [])[1] if (!id) continue const cmd = (s.match(/"command":"((?:[^"\\]|\\.)*)"/) ?? [])[1] ?? '' callInfo.set(id, { name, cmd: cmd.replace(/\\n/g, ' ').slice(0, 90) }) } // tool/result:id → 文本 const rows = [] for (const e of evs) { if (e.type !== 'tool/result') continue const s = JSON.stringify(e.data ?? {}) const id = (s.match(/"toolCallId":"([^"]+)"/) ?? [])[1] const info = callInfo.get(id) ?? { name: '?', cmd: '' } const txt = (e.data?.message?.content ?? []).map((c) => (c.content ?? []).map((x) => x.text ?? '').join('')).join('\n') const flat = txt.replace(/\s+/g, ' ').trim() let kind = 'OK' if (/sandbox mode .* is requested but no sandbox backend/.test(flat)) kind = '沙箱拒绝' else if (/file access denied under/.test(flat)) kind = '文件策略拒绝' else if (/^Error:|^error:|"error"|Command failed|exit code [1-9]/.test(flat)) kind = '命令错误' rows.push({ time: e.time, name: info.name, cmd: info.cmd, kind, head: flat.slice(0, 150) }) } console.log('=== 逐次 tool/result 分类(共 %d)===', rows.length) for (const r of rows) { if (r.name !== 'bash' && r.kind === 'OK') continue console.log(' %s %-6s %-10s %s', t(r.time), r.name, r.kind, (r.cmd || r.head).slice(0, 110)) } const byKind = {} for (const r of rows) byKind[r.kind] = (byKind[r.kind] ?? 0) + 1 console.log('\n汇总:', JSON.stringify(byKind)) const sb = rows.filter((r) => r.kind === '沙箱拒绝') console.log('\n沙箱拒绝时间线: %s → %s(共 %d 次)', t(sb[0]?.time), t(sb[sb.length - 1]?.time), sb.length) const after = sb.filter((r) => r.time > 1789116 * 1000).length console.log(' 其中 22:16 切档位之后:', after, '次')