363 lines
14 KiB
JavaScript
363 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
function parseArgs(argv) {
|
||
const args = { caseDir: '', target: '', entry: '', param: '', api: '', force: false, json: false };
|
||
for (let i = 2; i < argv.length; i++) {
|
||
const a = argv[i];
|
||
if (a === '--case-dir' || a === '--dir' || a === '-d') args.caseDir = argv[++i] || '';
|
||
else if (a === '--target') args.target = argv[++i] || '';
|
||
else if (a === '--entry') args.entry = argv[++i] || '';
|
||
else if (a === '--param') args.param = argv[++i] || '';
|
||
else if (a === '--api') args.api = argv[++i] || '';
|
||
else if (a === '--force') args.force = true;
|
||
else if (a === '--json') args.json = true;
|
||
else if (a === '--help' || a === '-h') args.help = true;
|
||
else throw new Error(`未知参数:${a}`);
|
||
}
|
||
return args;
|
||
}
|
||
|
||
function usage() {
|
||
return `用法:
|
||
node scripts/init_env_case.js --case-dir case --target app.js --entry window.makeSign --param sign --api https://example.com/api/search
|
||
|
||
说明:已有文件默认不覆盖;如需覆盖模板文件,请显式添加 --force。`;
|
||
}
|
||
|
||
function isDangerousDir(p) {
|
||
const resolved = path.resolve(p);
|
||
const root = path.parse(resolved).root;
|
||
return resolved === root || resolved.length <= root.length + 2;
|
||
}
|
||
|
||
function ensureDir(p, actions) {
|
||
if (!fs.existsSync(p)) {
|
||
fs.mkdirSync(p, { recursive: true });
|
||
actions.push({ action: 'create-dir', path: p });
|
||
} else {
|
||
actions.push({ action: 'keep-dir', path: p });
|
||
}
|
||
}
|
||
|
||
function writeFileIfNeeded(p, content, force, actions) {
|
||
if (fs.existsSync(p) && !force) {
|
||
actions.push({ action: 'keep-file', path: p });
|
||
return;
|
||
}
|
||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||
const existed = fs.existsSync(p);
|
||
fs.writeFileSync(p, content, 'utf8');
|
||
actions.push({ action: existed ? 'write-file' : 'create-file', path: p });
|
||
}
|
||
|
||
function initCase(args) {
|
||
if (!args.caseDir) throw new Error('必须提供 --case-dir');
|
||
const caseDir = path.resolve(args.caseDir);
|
||
if (isDangerousDir(caseDir)) throw new Error(`拒绝在危险目录中初始化:${caseDir}`);
|
||
const actions = [];
|
||
const dirs = ['js/original', 'js/pretty', 'js/extracted', 'requests', 'fixtures', 'notes', 'hooks', 'env', 'ruyi-trace/logs', 'browser/ruyipage', 'result', 'tmp'];
|
||
ensureDir(caseDir, actions);
|
||
for (const d of dirs) ensureDir(path.join(caseDir, d), actions);
|
||
|
||
const fixture = {
|
||
name: 'sample-001',
|
||
pageUrl: '',
|
||
apiUrl: args.api || '',
|
||
method: '',
|
||
param: args.param || '',
|
||
paramLocation: '',
|
||
browser: {
|
||
userAgent: '',
|
||
language: 'zh-CN',
|
||
timezone: 'Asia/Shanghai',
|
||
screen: { width: 1920, height: 1080 },
|
||
},
|
||
request: { query: {}, headers: {}, body: {}, cookies: {}, localStorage: {}, sessionStorage: {} },
|
||
runtime: { now: null, performanceNow: null, randomBytes: [] },
|
||
fingerprint: {
|
||
source: { mode: '', pageUrl: '', userAgent: '', timezone: '', locale: '', capturedAt: '' },
|
||
canvas: { toDataURL: [], toBlob: [], measureText: [], getImageData: [] },
|
||
webgl: { getParameter: [], getSupportedExtensions: null, getExtension: [], getShaderPrecisionFormat: [], readPixels: [] },
|
||
webgpu: { requestAdapter: [] },
|
||
audio: { startRendering: [], getChannelData: [] },
|
||
domGeometry: { getBoundingClientRect: [], getClientRects: [], offset: [] },
|
||
},
|
||
entry: args.entry || '',
|
||
args: [],
|
||
expected: args.param ? { [args.param]: '' } : {},
|
||
};
|
||
|
||
writeFileIfNeeded(path.join(caseDir, 'fixtures', 'sample.fixture.json'), JSON.stringify(fixture, null, 2) + '\n', args.force, actions);
|
||
|
||
const fingerprintFixture = {
|
||
version: 1,
|
||
source: { mode: '', pageUrl: '', userAgent: '', timezone: '', locale: '', capturedAt: '' },
|
||
canvas: { toDataURL: [], toBlob: [], measureText: [], getImageData: [] },
|
||
webgl: { getParameter: [], getSupportedExtensions: null, getExtension: [], getShaderPrecisionFormat: [], readPixels: [] },
|
||
webgpu: { requestAdapter: [] },
|
||
audio: { startRendering: [], getChannelData: [] },
|
||
domGeometry: { getBoundingClientRect: [], getClientRects: [], offset: [] },
|
||
};
|
||
writeFileIfNeeded(path.join(caseDir, 'fixtures', 'fingerprint.fixture.json'), JSON.stringify(fingerprintFixture, null, 2) + '\n', args.force, actions);
|
||
|
||
const notes = `# Node.js 补环境任务笔记
|
||
|
||
## 基本信息
|
||
|
||
- 目标 API:${args.api || ''}
|
||
- 目标 JS:${args.target || ''}
|
||
- 入口函数:${args.entry || ''}
|
||
- 目标参数:${args.param || ''}
|
||
|
||
## 前置材料检查
|
||
|
||
- [ ] 请求样本已确认
|
||
- [ ] 已列出请求样本中的全部可疑加密参数
|
||
- [ ] 用户已确认本次要分析哪些加密参数
|
||
- [ ] 加密参数已在请求中确认
|
||
- [ ] 已输出 source / entry / builder / writer 四层链路
|
||
- [ ] 目标 JS 文件已保存
|
||
- [ ] 加密入口已定位
|
||
- [ ] 浏览器取证模式已由用户选择:ruyiPage + RuyiTrace / ruyiPage / CloakBrowser / 手动取证 / AI 自行决定
|
||
- [ ] 如选择 RuyiTrace,NDJSON 日志已导入或确认不需要
|
||
- [ ] Node 泄露阻断已检查
|
||
- [ ] 补环境初始化阶段已加载 / 检测 addon,并记录可用性、导出 API 或降级原因
|
||
- [ ] 默认启用 addon-first;如用户明确要求不使用 addon,已记录豁免原因
|
||
- [ ] 默认启用 toString / 属性描述符 / 原型链 / 访问器 / 实例对象保护;如用户明确要求关闭,已记录原因
|
||
- [ ] 已读取 code-style.md,并先规划 result 目录、文件职责和调用关系
|
||
- [ ] 补环境代码默认要求简洁、模块化、具名函数清晰,并写入中文注释
|
||
- [ ] 中文注释使用 UTF-8 正常显示,没有问号、连续问号或乱码
|
||
- [ ] 已创建 / 读取 notes/代码变更记忆.md;修改关键源码前已搜索历史记录,修改后已记录失败尝试、禁止回退、验证范围和遗留风险
|
||
- [ ] 六项纯计算预检已执行或明确不需要
|
||
- [ ] 如目标访问 Canvas / WebGL / WebGPU / Audio / 字体 / DOM 几何指纹,已采集真实浏览器终端 API 返回值并写入 fixtures/fingerprint.fixture.json
|
||
- [ ] 用户已确认进入补环境阶段
|
||
|
||
## 缺失环境追踪
|
||
|
||
- trace 文件:tmp/env-trace.jsonl
|
||
- 缺失环境摘要:tmp/missing-env.json
|
||
- trace 分析:notes/missing-env-priority.md
|
||
|
||
## 可疑加密参数确认
|
||
|
||
| 参数名 | 位置 | 样本值摘要 | 疑似原因 | 用户是否确认分析 |
|
||
|---|---|---|---|---|
|
||
|
||
> cURL / HAR 中已有的加密参数值只能作为 fixture expected,最终产物必须通过补环境重新生成,不能直接复用。
|
||
|
||
## 补齐记录
|
||
|
||
| 轮次 | 缺失环境 | 处理方式 | 验证结果 |
|
||
|---|---|---|---|
|
||
|
||
## 通用代码变更记忆
|
||
|
||
- 记录文件:notes/代码变更记忆.md
|
||
- [ ] 修改关键源码前已读取该文件
|
||
- [ ] 修改关键源码后已追加本轮变更记录
|
||
- [ ] 已记录修改前逻辑、问题证据、本次修改、修改理由、已失败尝试、禁止回退、验证命令、验证结果、当前验证范围、遗留风险和当前状态
|
||
- [ ] 已运行 scripts/check_change_memory.js --case-dir case --markdown
|
||
|
||
## 代码质量
|
||
|
||
- [ ] 已运行 scripts/check_code_quality.js --case-dir case --markdown
|
||
- [ ] 已修复超长行、超长函数、压缩堆叠代码和过深嵌套
|
||
- [ ] 已确认每个手写源码文件顶部都有中文职责注释
|
||
- [ ] 已确认关键 WebAPI、getter / setter、addon-first、fallback、指纹回放和加密入口有中文说明
|
||
- [ ] 已确认中文注释没有问号、连续问号或乱码
|
||
|
||
## 指纹值回放
|
||
|
||
- [ ] 已确认哪些指纹终端 API 被目标 JS 访问
|
||
- [ ] 已使用真实浏览器采样值回放,而不是在 Node.js 中真实模拟渲染
|
||
- [ ] 未把浏览器自动化代码放入最终 result 目录
|
||
- [ ] 缺失指纹样本时已阻塞并提示补采样,没有静默伪造默认值
|
||
|
||
## 最终请求验证
|
||
|
||
- [ ] fixtures 已通过
|
||
- [ ] 用户已确认是否发起真实请求
|
||
- [ ] 如普通客户端疑似 TLS 指纹不一致,已确认是否使用 CycleTLS / curl_cffi / cffi_curl / cyCronet
|
||
- [ ] 请求次数、状态码、响应关键字段已记录
|
||
- [ ] 临时响应和敏感请求副本已清理
|
||
|
||
## 最终总结
|
||
|
||
- [ ] 已从前置阶段开始生成 case/阶段报告/01-需求信息确认.md 等中文命名阶段报告
|
||
- [ ] 项目完成后已默认生成 result/最终项目总结.md
|
||
- [ ] 最终总结使用 UTF-8 写入,中文正常显示
|
||
- [ ] 总结包含 native addon / NativeProtect 使用情况、加密参数生成与样本复用检查、最终交付结构、测试结果和清理结果
|
||
- [ ] 如用户明确要求不生成最终总结,已记录原因
|
||
|
||
## 清理检查
|
||
|
||
- [ ] 已删除临时 trace / 日志
|
||
- [ ] 已删除失败 runner
|
||
- [ ] 已保留必要 fixtures 和 notes
|
||
`;
|
||
writeFileIfNeeded(path.join(caseDir, 'notes', 'env-task.md'), notes, args.force, actions);
|
||
|
||
const codeChangeMemory = `# 代码变更记忆
|
||
|
||
本文件由通用代码变更记忆机制维护,用于记录复杂 case 中关键源码修改的事实、原因、失败尝试、禁止回退、验证范围和遗留风险。不要把“当前报错消失”直接写成永久结论。
|
||
|
||
## 使用规则
|
||
|
||
- 修改关键源码前先读取本文件,并搜索文件名、函数名、错误关键词和参数名。
|
||
- 修改关键源码后立即追加一条“变更”记录。
|
||
- 不删除失败记录;如历史记录被新证据替代,新增记录说明替代原因。
|
||
- 状态只能使用:临时修复 / 当前验证通过 / 已失败 / 被替代 / 稳定基线。
|
||
|
||
## 变更 001 - 待填写
|
||
|
||
- 时间:YYYY-MM-DD HH:mm
|
||
- 涉及文件:
|
||
- 涉及函数 / 模块:
|
||
- 当前状态:临时修复
|
||
|
||
### 修改前逻辑
|
||
|
||
待填写。
|
||
|
||
### 问题证据
|
||
|
||
待填写。
|
||
|
||
### 本次修改
|
||
|
||
待填写。
|
||
|
||
### 修改理由
|
||
|
||
待填写。
|
||
|
||
### 已失败尝试
|
||
|
||
待填写。
|
||
|
||
### 禁止回退
|
||
|
||
待填写。
|
||
|
||
### 验证命令
|
||
|
||
\`\`\`bash
|
||
# 待填写
|
||
\`\`\`
|
||
|
||
### 验证结果
|
||
|
||
待填写。
|
||
|
||
### 当前验证范围
|
||
|
||
待填写。
|
||
|
||
### 遗留风险
|
||
|
||
待填写。
|
||
`;
|
||
writeFileIfNeeded(path.join(caseDir, 'notes', '代码变更记忆.md'), codeChangeMemory, args.force, actions);
|
||
|
||
const entryChain = `# 加密入口链路记录
|
||
|
||
## writer 最终写入
|
||
|
||
- 类型:fetch / XHR / URLSearchParams / Header / Cookie / Body
|
||
- 文件与行列号:
|
||
- 调用栈:
|
||
- 写入前对象快照:
|
||
- 写入后对象快照:
|
||
|
||
## builder 请求构造
|
||
|
||
- 函数 / 方法:
|
||
- 所在模块:
|
||
- 输入:
|
||
- 输出:
|
||
- 是否经过拦截器:
|
||
|
||
## entry 加密入口
|
||
|
||
- 函数名 / 表达式:${args.entry || ''}
|
||
- 文件 / chunk / module id:${args.target || ''}
|
||
- 行列号:
|
||
- 入参:
|
||
- 返回:
|
||
- 是否需要初始化:
|
||
|
||
## source 数据源
|
||
|
||
- URL / Query:
|
||
- Body:
|
||
- Cookie:
|
||
- localStorage / sessionStorage:
|
||
- 时间 / 随机数:
|
||
- navigator / screen / document / canvas / WebGL:
|
||
- Canvas / WebGL / WebGPU / Audio / 字体 / DOM 几何指纹 fixture:
|
||
- 其他:
|
||
|
||
## 可信度
|
||
|
||
- 证据来源:
|
||
- 是否可进入补环境:否
|
||
- 阻塞点:
|
||
`;
|
||
writeFileIfNeeded(path.join(caseDir, 'notes', 'entry-chain.md'), entryChain, args.force, actions);
|
||
|
||
const silentChecklist = `# 静默失败排查清单
|
||
|
||
当目标 JS 不报错但 ${args.param || '目标参数'} 不一致时逐项检查。
|
||
|
||
- [ ] 请求 URL、Query、Body、Header 与浏览器样本一致
|
||
- [ ] SDK 初始化参数、版本、appId、页面配置一致
|
||
- [ ] Cookie、localStorage、sessionStorage 已按 fixture 注入
|
||
- [ ] Date.now / new Date / performance.now 可复现
|
||
- [ ] Math.random / crypto.getRandomValues 可复现
|
||
- [ ] Canvas / WebGL / WebGPU / Audio / 字体 / DOM 几何等指纹终端 API 返回值来自真实浏览器采样并可回放
|
||
- [ ] navigator / screen / timezone / language 与浏览器样本一致
|
||
- [ ] JS 加载顺序、runtime chunk、动态 chunk 完整
|
||
- [ ] Function.prototype.toString / Object.prototype.toString 已处理
|
||
- [ ] 属性描述符与原型链已记录
|
||
- [ ] document.all 等特殊对象已处理或确认未触发
|
||
- [ ] Worker / WASM / postMessage 已排查
|
||
`;
|
||
writeFileIfNeeded(path.join(caseDir, 'notes', 'silent-failure-checklist.md'), silentChecklist, args.force, actions);
|
||
|
||
const trustMatrix = `# 证据可信度矩阵
|
||
|
||
| 项目 | 当前值 / 结论 | 来源 | 等级 A/B/C/D | 待验证 |
|
||
|---|---|---|---|---|
|
||
| 目标 API | ${args.api || ''} | 用户输入 | B | |
|
||
| 目标参数 | ${args.param || ''} | 用户输入 | B | |
|
||
| 加密入口 | ${args.entry || ''} | 用户输入 / 待验证 | C | |
|
||
| JS 文件 | ${args.target || ''} | 用户输入 / 待验证 | C | |
|
||
| Cookie / token | 仅记录键名和脱敏摘要 | fixture | A/B | |
|
||
`;
|
||
writeFileIfNeeded(path.join(caseDir, 'notes', 'trust-matrix.md'), trustMatrix, args.force, actions);
|
||
return { caseDir, actions };
|
||
}
|
||
|
||
function render(result) {
|
||
const label = { 'create-dir': '创建目录', 'keep-dir': '保留目录', 'create-file': '创建文件', 'write-file': '写入文件', 'keep-file': '保留已有文件' };
|
||
const lines = ['# 补环境 case 初始化结果', '', `- case 目录:${result.caseDir}`];
|
||
for (const a of result.actions) lines.push(`- ${label[a.action] || a.action}:${a.path}`);
|
||
return lines.join('\n') + '\n';
|
||
}
|
||
|
||
try {
|
||
const args = parseArgs(process.argv);
|
||
if (args.help) { console.log(usage()); process.exit(0); }
|
||
const result = initCase(args);
|
||
if (args.json) console.log(JSON.stringify(result, null, 2));
|
||
else process.stdout.write(render(result));
|
||
} catch (err) {
|
||
console.error(err.message || String(err));
|
||
console.error(usage());
|
||
process.exit(1);
|
||
}
|